From 6aeaa171dc1ca43392f53cbd02097f76e1b1c5a0 Mon Sep 17 00:00:00 2001 From: David Moc Date: Sun, 31 May 2026 03:47:04 +0200 Subject: Hardened API, tetris, MD-View --- src/ecex.c | 1675 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 1570 insertions(+), 105 deletions(-) (limited to 'src/ecex.c') diff --git a/src/ecex.c b/src/ecex.c index 2f4c962..564d05f 100644 --- a/src/ecex.c +++ b/src/ecex.c @@ -1,13 +1,18 @@ #include "ecex.h" +#include "media.h" #include "ccdjit.h" #include "common.h" #include "config.h" #include "util.h" +#include "path.h" #include #include #include +#include +#include +#include extern FILE *popen(const char *command, const char *type); extern int pclose(FILE *stream); @@ -21,6 +26,639 @@ extern int pclose(FILE *stream); ecex_window_t *ecex_current_window(ecex_t *ed); + +void *ecex_config_alloc(size_t size) { + return malloc(size ? size : 1); +} + +void *ecex_config_calloc(size_t count, size_t size) { + if (count == 0) count = 1; + if (size == 0) size = 1; + return calloc(count, size); +} + +void ecex_config_free(void *ptr) { + free(ptr); +} + +double ecex_time_seconds(void) { + struct timeval tv; + gettimeofday(&tv, NULL); + return (double)tv.tv_sec + (double)tv.tv_usec / 1000000.0; +} + +static int ecex_env_enabled(const char *name) { + const char *v; + if (!name) return 0; + v = getenv(name); + return v && v[0] && v[0] != '0'; +} + +static int ecex_log_enabled(void) { + return ecex_env_enabled("ECEX_LOG") || + ecex_env_enabled("ECEX_TRACE_CALLBACKS") || + ecex_env_enabled("ECEX_TRACE_TETRIS"); +} + +void ecex_log(const char *message) { + if (!ecex_log_enabled()) return; + fprintf(stderr, "ecex-log: %s\n", message ? message : "(null)"); + fflush(stderr); +} + +void ecex_log_int(const char *message, int value) { + if (!ecex_log_enabled()) return; + fprintf(stderr, "ecex-log: %s%d\n", message ? message : "", value); + fflush(stderr); +} + +void ecex_log_double(const char *message, double value) { + if (!ecex_log_enabled()) return; + fprintf(stderr, "ecex-log: %s%.6f\n", message ? message : "", value); + fflush(stderr); +} + +void ecex_log_ptr(const char *message, const void *ptr) { + if (!ecex_log_enabled()) return; + fprintf(stderr, "ecex-log: %s%p\n", message ? message : "", ptr); + fflush(stderr); +} + +void ecex_mem_zero(void *ptr, size_t size) { + if (!ptr || size == 0) return; + memset(ptr, 0, size); +} + + +static ecex_object_entry_t *ecex_object_find_entry(ecex_t *ed, void *object) { + if (!ed || !object) return NULL; + for (size_t i = 0; i < ed->object_count; ++i) { + if (ed->objects[i].ptr == object) return &ed->objects[i]; + } + return NULL; +} + +static int ecex_reserve_objects(ecex_t *ed, size_t needed) { + ecex_object_entry_t *new_objects; + size_t new_cap; + if (!ed) return ECEX_ERR; + if (needed <= ed->object_cap) return ECEX_OK; + new_cap = ed->object_cap ? ed->object_cap * 2 : 16; + while (new_cap < needed) new_cap *= 2; + new_objects = realloc(ed->objects, new_cap * sizeof(*new_objects)); + if (!new_objects) return ECEX_ERR; + memset(new_objects + ed->object_cap, 0, (new_cap - ed->object_cap) * sizeof(*new_objects)); + ed->objects = new_objects; + ed->object_cap = new_cap; + return ECEX_OK; +} + +void *ecex_object_alloc(ecex_t *ed, size_t size) { + void *ptr; + if (!ed) return NULL; + if (size == 0) size = 1; + if (ecex_reserve_objects(ed, ed->object_count + 1) != ECEX_OK) return NULL; + ptr = malloc(size); + if (!ptr) return NULL; + ed->objects[ed->object_count].ptr = ptr; + ed->objects[ed->object_count].size = size; + ed->object_count++; + return ptr; +} + +void *ecex_object_calloc(ecex_t *ed, size_t count, size_t size) { + void *ptr; + size_t total; + if (!ed) return NULL; + if (count == 0) count = 1; + if (size == 0) size = 1; + if (count > ((size_t)-1) / size) return NULL; + total = count * size; + ptr = ecex_object_alloc(ed, total); + if (!ptr) return NULL; + memset(ptr, 0, total); + return ptr; +} + +int ecex_object_free(ecex_t *ed, void *object) { + if (!ed || !object) return ECEX_ERR; + for (size_t i = 0; i < ed->object_count; ++i) { + if (ed->objects[i].ptr == object) { + free(ed->objects[i].ptr); + if (i + 1 < ed->object_count) { + memmove(&ed->objects[i], &ed->objects[i + 1], (ed->object_count - i - 1) * sizeof(ed->objects[i])); + } + --ed->object_count; + if (ed->object_count < ed->object_cap) memset(&ed->objects[ed->object_count], 0, sizeof(ed->objects[ed->object_count])); + return ECEX_OK; + } + } + return ECEX_ERR; +} + +int ecex_object_valid(ecex_t *ed, void *object) { + return ecex_object_find_entry(ed, object) ? 1 : 0; +} + +int ecex_object_i32_get(ecex_t *ed, void *object, size_t byte_offset, int fallback) { + ecex_object_entry_t *o = ecex_object_find_entry(ed, object); + int value; + if (!o || !o->ptr || byte_offset > o->size || o->size - byte_offset < sizeof(int)) return fallback; + memcpy(&value, (const char *)o->ptr + byte_offset, sizeof(value)); + return value; +} + +int ecex_object_i32_set(ecex_t *ed, void *object, size_t byte_offset, int value) { + ecex_object_entry_t *o = ecex_object_find_entry(ed, object); + if (!o || !o->ptr || byte_offset > o->size || o->size - byte_offset < sizeof(int)) return ECEX_ERR; + memcpy((char *)o->ptr + byte_offset, &value, sizeof(value)); + return ECEX_OK; +} + +void *ecex_object_ptr_get(ecex_t *ed, void *object, size_t byte_offset) { + ecex_object_entry_t *o = ecex_object_find_entry(ed, object); + void *value = NULL; + if (!o || !o->ptr || byte_offset > o->size || o->size - byte_offset < sizeof(void *)) return NULL; + memcpy(&value, (const char *)o->ptr + byte_offset, sizeof(value)); + return value; +} + +int ecex_object_ptr_set(ecex_t *ed, void *object, size_t byte_offset, void *value) { + ecex_object_entry_t *o = ecex_object_find_entry(ed, object); + if (!o || !o->ptr || byte_offset > o->size || o->size - byte_offset < sizeof(void *)) return ECEX_ERR; + memcpy((char *)o->ptr + byte_offset, &value, sizeof(value)); + return ECEX_OK; +} + +int ecex_i32_get(const int *items, size_t index) { + if (!items) return 0; + return items[index]; +} + +void ecex_i32_set(int *items, size_t index, int value) { + if (!items) return; + items[index] = value; +} + +static char *ecex_var_strdup(const char *s) { + size_t n; + char *out; + if (!s) s = ""; + n = strlen(s) + 1; + out = malloc(n); + if (!out) return NULL; + memcpy(out, s, n); + return out; +} + +static int ecex_var_name_eq(const char *a, const char *b) { + if (!a) a = ""; + if (!b) b = ""; + return strcmp(a, b) == 0; +} + +static ecex_var_entry_t *ecex_var_find_entry(ecex_t *ed, void *owner, const char *name) { + size_t i; + if (!ed || !name) return NULL; + for (i = 0; i < ed->var_count; ++i) { + ecex_var_entry_t *v = &ed->vars[i]; + if (v->owner == owner && ecex_var_name_eq(v->name, name)) return v; + } + return NULL; +} + +static int ecex_reserve_vars(ecex_t *ed, size_t needed) { + ecex_var_entry_t *new_vars; + size_t new_cap; + if (!ed) return ECEX_ERR; + if (needed <= ed->var_cap) return ECEX_OK; + new_cap = ed->var_cap ? ed->var_cap * 2 : 16; + while (new_cap < needed) new_cap *= 2; + new_vars = realloc(ed->vars, new_cap * sizeof(*new_vars)); + if (!new_vars) return ECEX_ERR; + memset(new_vars + ed->var_cap, 0, (new_cap - ed->var_cap) * sizeof(*new_vars)); + ed->vars = new_vars; + ed->var_cap = new_cap; + return ECEX_OK; +} + +void *ecex_var_get(ecex_t *ed, void *owner, const char *name) { + ecex_var_entry_t *v = ecex_var_find_entry(ed, owner, name); + return v ? v->data : NULL; +} + +void *ecex_var_get_or_alloc(ecex_t *ed, void *owner, const char *name, size_t count, size_t elem_size) { + ecex_var_entry_t *v; + void *data; + if (!ed || !name) return NULL; + if (count == 0) count = 1; + if (elem_size == 0) elem_size = 1; + + v = ecex_var_find_entry(ed, owner, name); + if (v) { + if (v->elem_size != elem_size) { + ecex_log("ecex_var_get_or_alloc: existing slot element-size mismatch"); + return NULL; + } + if (v->count < count) { + void *new_data; + if (!v->dynamic) { + ecex_log("ecex_var_get_or_alloc: static slot too small"); + return NULL; + } + new_data = realloc(v->data, count * elem_size); + if (!new_data) return NULL; + memset((char *)new_data + v->count * elem_size, 0, (count - v->count) * elem_size); + v->data = new_data; + v->count = count; + } + return v->data; + } + + if (ecex_reserve_vars(ed, ed->var_count + 1) != ECEX_OK) return NULL; + data = calloc(count, elem_size); + if (!data) return NULL; + + v = &ed->vars[ed->var_count++]; + memset(v, 0, sizeof(*v)); + v->owner = owner; + v->name = ecex_var_strdup(name); + if (!v->name) { + free(data); + --ed->var_count; + return NULL; + } + v->data = data; + v->elem_size = elem_size; + v->count = count; + v->kind = (elem_size == sizeof(int)) ? ECEX_VAR_I32 : ECEX_VAR_BYTES; + v->dynamic = 1; + return data; +} + +int ecex_var_bind_static(ecex_t *ed, void *owner, const char *name, void *data, size_t count, size_t elem_size) { + ecex_var_entry_t *v; + if (!ed || !name || !data) return ECEX_ERR; + if (count == 0) count = 1; + if (elem_size == 0) elem_size = 1; + + v = ecex_var_find_entry(ed, owner, name); + if (!v) { + if (ecex_reserve_vars(ed, ed->var_count + 1) != ECEX_OK) return ECEX_ERR; + v = &ed->vars[ed->var_count++]; + memset(v, 0, sizeof(*v)); + v->name = ecex_var_strdup(name); + if (!v->name) { + --ed->var_count; + return ECEX_ERR; + } + } else if (v->dynamic) { + free(v->data); + } + + v->owner = owner; + v->data = data; + v->elem_size = elem_size; + v->count = count; + v->kind = (elem_size == sizeof(int)) ? ECEX_VAR_I32 : ECEX_VAR_BYTES; + v->dynamic = 0; + return ECEX_OK; +} + +static void ecex_var_entry_clear(ecex_var_entry_t *v) { + if (!v) return; + if (v->dynamic) free(v->data); + free(v->name); + memset(v, 0, sizeof(*v)); +} + +int ecex_var_free(ecex_t *ed, void *owner, const char *name) { + size_t i; + if (!ed || !name) return ECEX_ERR; + for (i = 0; i < ed->var_count; ++i) { + if (ed->vars[i].owner == owner && ecex_var_name_eq(ed->vars[i].name, name)) { + ecex_var_entry_clear(&ed->vars[i]); + if (i + 1 < ed->var_count) { + memmove(&ed->vars[i], &ed->vars[i + 1], (ed->var_count - i - 1) * sizeof(ed->vars[i])); + } + --ed->var_count; + if (ed->var_count < ed->var_cap) memset(&ed->vars[ed->var_count], 0, sizeof(ed->vars[ed->var_count])); + return ECEX_OK; + } + } + return ECEX_ERR; +} + +int ecex_var_free_owner(ecex_t *ed, void *owner) { + size_t i; + if (!ed) return ECEX_ERR; + i = 0; + while (i < ed->var_count) { + if (ed->vars[i].owner == owner) { + ecex_var_entry_clear(&ed->vars[i]); + if (i + 1 < ed->var_count) { + memmove(&ed->vars[i], &ed->vars[i + 1], (ed->var_count - i - 1) * sizeof(ed->vars[i])); + } + --ed->var_count; + if (ed->var_count < ed->var_cap) memset(&ed->vars[ed->var_count], 0, sizeof(ed->vars[ed->var_count])); + continue; + } + ++i; + } + return ECEX_OK; +} + +int ecex_var_i32_get(ecex_t *ed, void *owner, const char *name, size_t index, int fallback) { + ecex_var_entry_t *v = ecex_var_find_entry(ed, owner, name); + if (!v || !v->data || v->elem_size != sizeof(int) || index >= v->count) return fallback; + return ((int *)v->data)[index]; +} + +int ecex_var_i32_set(ecex_t *ed, void *owner, const char *name, size_t index, int value) { + int *data; + if (!ed || !name) return ECEX_ERR; + data = (int *)ecex_var_get_or_alloc(ed, owner, name, index + 1, sizeof(int)); + if (!data) return ECEX_ERR; + data[index] = value; + return ECEX_OK; +} + +int ecex_var_i32(ecex_t *ed, void *owner, const char *name, int fallback) { + return ecex_var_i32_get(ed, owner, name, 0, fallback); +} + +int ecex_var_i32_set_scalar(ecex_t *ed, void *owner, const char *name, int value) { + return ecex_var_i32_set(ed, owner, name, 0, value); +} + + + +static ecex_text_entry_t *ecex_text_find_entry(ecex_t *ed, void *owner, int id) { + if (!ed) return NULL; + for (size_t i = 0; i < ed->text_count; ++i) { + ecex_text_entry_t *t = &ed->texts[i]; + if (t->owner == owner && t->id == id) return t; + } + return NULL; +} + +static int ecex_reserve_texts(ecex_t *ed, size_t needed) { + ecex_text_entry_t *new_texts; + size_t new_cap; + if (!ed) return ECEX_ERR; + if (needed <= ed->text_cap) return ECEX_OK; + new_cap = ed->text_cap ? ed->text_cap * 2 : 32; + while (new_cap < needed) new_cap *= 2; + new_texts = realloc(ed->texts, new_cap * sizeof(*new_texts)); + if (!new_texts) return ECEX_ERR; + memset(new_texts + ed->text_cap, 0, (new_cap - ed->text_cap) * sizeof(*new_texts)); + ed->texts = new_texts; + ed->text_cap = new_cap; + return ECEX_OK; +} + +static void ecex_text_entry_clear(ecex_text_entry_t *t) { + if (!t) return; + free(t->text); + memset(t, 0, sizeof(*t)); +} + +int ecex_text_set(ecex_t *ed, void *owner, int id, const char *text, int len) { + ecex_text_entry_t *t; + char *copy; + size_t n; + if (!ed || id < 0) return ECEX_ERR; + if (!text) text = ""; + if (len < 0) n = strlen(text); + else n = (size_t)len; + copy = malloc(n + 1); + if (!copy) return ECEX_ERR; + if (n) memcpy(copy, text, n); + copy[n] = '\0'; + + t = ecex_text_find_entry(ed, owner, id); + if (!t) { + if (ecex_reserve_texts(ed, ed->text_count + 1) != ECEX_OK) { + free(copy); + return ECEX_ERR; + } + t = &ed->texts[ed->text_count++]; + memset(t, 0, sizeof(*t)); + t->owner = owner; + t->id = id; + } + free(t->text); + t->text = copy; + t->len = n; + return ECEX_OK; +} + +int ecex_text_set_buffer_title(ecex_t *ed, void *owner, int id, buffer_t *buffer) { + const char *title = NULL; + if (buffer) title = buffer->path ? buffer->path : buffer->name; + return ecex_text_set(ed, owner, id, title ? title : "(unnamed)", -1); +} + +int ecex_text_free(ecex_t *ed, void *owner, int id) { + if (!ed) return ECEX_ERR; + for (size_t i = 0; i < ed->text_count; ++i) { + if (ed->texts[i].owner == owner && ed->texts[i].id == id) { + ecex_text_entry_clear(&ed->texts[i]); + if (i + 1 < ed->text_count) { + memmove(&ed->texts[i], &ed->texts[i + 1], (ed->text_count - i - 1) * sizeof(ed->texts[i])); + } + --ed->text_count; + if (ed->text_count < ed->text_cap) memset(&ed->texts[ed->text_count], 0, sizeof(ed->texts[ed->text_count])); + return ECEX_OK; + } + } + return ECEX_ERR; +} + +int ecex_text_free_owner(ecex_t *ed, void *owner) { + if (!ed) return ECEX_ERR; + for (size_t i = 0; i < ed->text_count;) { + if (ed->texts[i].owner == owner) { + ecex_text_entry_clear(&ed->texts[i]); + if (i + 1 < ed->text_count) { + memmove(&ed->texts[i], &ed->texts[i + 1], (ed->text_count - i - 1) * sizeof(ed->texts[i])); + } + --ed->text_count; + if (ed->text_count < ed->text_cap) memset(&ed->texts[ed->text_count], 0, sizeof(ed->texts[ed->text_count])); + continue; + } + ++i; + } + return ECEX_OK; +} + +const char *ecex_text_get_for_draw(ecex_t *ed, void *owner, int id) { + ecex_text_entry_t *t = ecex_text_find_entry(ed, owner, id); + return t && t->text ? t->text : ""; +} + +static int ecex_ascii_equal_ci(const char *a, const char *b) { + unsigned char ca; + unsigned char cb; + if (!a || !b) return 0; + while (*a && *b) { + ca = (unsigned char)*a; + cb = (unsigned char)*b; + if (ca >= 'A' && ca <= 'Z') ca = (unsigned char)(ca - 'A' + 'a'); + if (cb >= 'A' && cb <= 'Z') cb = (unsigned char)(cb - 'A' + 'a'); + if (ca != cb) return 0; + ++a; + ++b; + } + return *a == '\0' && *b == '\0'; +} + +static const char *ecex_path_extension(const char *path) { + const char *dot; + if (!path) return NULL; + dot = strrchr(path, '.'); + return dot && dot[0] ? dot : NULL; +} + +static int ecex_reserve_file_handlers(ecex_t *ed, size_t needed) { + ecex_file_handler_t *new_handlers; + size_t new_cap; + if (!ed) return ECEX_ERR; + if (needed <= ed->file_handler_cap) return ECEX_OK; + new_cap = ed->file_handler_cap ? ed->file_handler_cap * 2 : 8; + while (new_cap < needed) new_cap *= 2; + new_handlers = realloc(ed->file_handlers, new_cap * sizeof(*new_handlers)); + if (!new_handlers) return ECEX_ERR; + memset(new_handlers + ed->file_handler_cap, 0, (new_cap - ed->file_handler_cap) * sizeof(*new_handlers)); + ed->file_handlers = new_handlers; + ed->file_handler_cap = new_cap; + return ECEX_OK; +} + +int ecex_register_file_handler(ecex_t *ed, const char *extension, ecex_file_handler_fn fn) { + char *copy; + if (!ed || !extension || !extension[0] || !fn) return ECEX_ERR; + for (size_t i = 0; i < ed->file_handler_count; ++i) { + if (ecex_ascii_equal_ci(ed->file_handlers[i].extension, extension)) { + ed->file_handlers[i].fn = fn; + return ECEX_OK; + } + } + if (ecex_reserve_file_handlers(ed, ed->file_handler_count + 1) != ECEX_OK) return ECEX_ERR; + copy = ecex_var_strdup(extension); + if (!copy) return ECEX_ERR; + ed->file_handlers[ed->file_handler_count].extension = copy; + ed->file_handlers[ed->file_handler_count].fn = fn; + ed->file_handler_count++; + return ECEX_OK; +} + +int ecex_run_file_handlers(ecex_t *ed, buffer_t *buffer) { + const char *ext; + const char *path; + if (!ed || !buffer) return ECEX_ERR; + path = buffer->path ? buffer->path : buffer->name; + ext = ecex_path_extension(path); + if (!ext) return ECEX_OK; + for (size_t i = 0; i < ed->file_handler_count; ++i) { + if (ecex_ascii_equal_ci(ed->file_handlers[i].extension, ext)) { + return ed->file_handlers[i].fn(ed, buffer); + } + } + return ECEX_OK; +} + +int ecex_prng_next_bounded(unsigned int *state, int bound) { + unsigned int x; + unsigned int limit; + + if (!state || bound <= 0) return 0; + + x = *state; + if (x == 0u) x = 0x6d2b79f5u; + + /* Xorshift32. Keep this host-side so CCDJIT plugins do not depend on + * unsigned multiply/divide lowering details for game logic randomness. */ + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + if (x == 0u) x = 0xa5a5a5a5u; + *state = x; + + /* Avoid modulo bias enough for tiny bounds without libc. */ + limit = 0xffffffffu - (0xffffffffu % (unsigned int)bound); + while (x >= limit) { + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + if (x == 0u) x = 0x9e3779b9u; + *state = x; + } + + return (int)(x % (unsigned int)bound); +} + + +int ecex_random_bounded(int bound) { + static unsigned int state = 0x7f4a7c15u; + unsigned int mix; + + if (bound <= 0) return 0; + /* Host-owned PRNG for plugins that should not pass writable state pointers + * through the JIT ABI. Stir in a stack address so separate launches differ + * enough for games/demos without requiring libc time calls from plugins. */ + mix = (unsigned int)(size_t)&bound; + state ^= mix + 0x9e3779b9u + (state << 6) + (state >> 2); + return ecex_prng_next_bounded(&state, bound); +} + +int ecex_tetris_shape_cell(int piece, int rot, int col, int row) { + int p = piece % 7; + int r = rot & 3; + if (p < 0) p += 7; + if (col < 0 || col >= 4 || row < 0 || row >= 4) return 0; + + /* I */ + if (p == 0) { + if ((r & 1) == 0) return row == 1; + return col == 1; + } + /* O */ + if (p == 1) return (row == 1 || row == 2) && (col == 1 || col == 2); + /* T */ + if (p == 2) { + if (r == 0) return (row == 1 && col >= 0 && col <= 2) || (row == 2 && col == 1); + if (r == 1) return (col == 1 && row >= 0 && row <= 2) || (row == 1 && col == 2); + if (r == 2) return (row == 1 && col >= 0 && col <= 2) || (row == 0 && col == 1); + return (col == 1 && row >= 0 && row <= 2) || (row == 1 && col == 0); + } + /* S */ + if (p == 3) { + if ((r & 1) == 0) return (row == 1 && (col == 1 || col == 2)) || (row == 2 && (col == 0 || col == 1)); + return (col == 1 && (row == 0 || row == 1)) || (col == 2 && (row == 1 || row == 2)); + } + /* Z */ + if (p == 4) { + if ((r & 1) == 0) return (row == 1 && (col == 0 || col == 1)) || (row == 2 && (col == 1 || col == 2)); + return (col == 2 && (row == 0 || row == 1)) || (col == 1 && (row == 1 || row == 2)); + } + /* J */ + if (p == 5) { + if (r == 0) return (row == 1 && col >= 0 && col <= 2) || (row == 0 && col == 0); + if (r == 1) return (col == 1 && row >= 0 && row <= 2) || (row == 0 && col == 2); + if (r == 2) return (row == 1 && col >= 0 && col <= 2) || (row == 2 && col == 2); + return (col == 1 && row >= 0 && row <= 2) || (row == 2 && col == 0); + } + /* L */ + if (r == 0) return (row == 1 && col >= 0 && col <= 2) || (row == 0 && col == 2); + if (r == 1) return (col == 1 && row >= 0 && row <= 2) || (row == 2 && col == 2); + if (r == 2) return (row == 1 && col >= 0 && col <= 2) || (row == 2 && col == 0); + return (col == 1 && row >= 0 && row <= 2) || (row == 0 && col == 0); +} + +static int ecex_file_browser_preview_current(ecex_t *ed); +static int ecex_file_browser_update_preview_if_enabled(ecex_t *ed); + static ecex_color_t ecex_color(float r, float g, float b) { ecex_color_t c = {r, g, b}; return c; @@ -71,6 +709,16 @@ static void ecex_theme_set_defaults(ecex_t *ed) { if (!buf) return ECEX_ERR static int cmd_quit(ecex_t *ed) { + if (!ed) return ECEX_ERR; + if (ecex_has_modified_buffers(ed)) { + fprintf(stderr, "ecex: refusing to quit; modified buffers exist. Save or use force-quit.\n"); + return ECEX_ERR; + } + ed->should_quit = 1; + return ECEX_OK; +} + +static int cmd_force_quit(ecex_t *ed) { if (!ed) return ECEX_ERR; ed->should_quit = 1; return ECEX_OK; @@ -88,8 +736,18 @@ static int cmd_balance_windows(ecex_t *ed) { return ecex_balance_windows(ed); } static int cmd_move_left(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); buffer_move_left(buf); return ECEX_OK; } static int cmd_move_right(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); buffer_move_right(buf); return ECEX_OK; } -static int cmd_move_up(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); buffer_move_up(buf); return ECEX_OK; } -static int cmd_move_down(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); buffer_move_down(buf); return ECEX_OK; } +static int cmd_move_up(ecex_t *ed) { + CURRENT_BUFFER_OR_ERR(ed); + buffer_move_up(buf); + ecex_file_browser_update_preview_if_enabled(ed); + return ECEX_OK; +} +static int cmd_move_down(ecex_t *ed) { + CURRENT_BUFFER_OR_ERR(ed); + buffer_move_down(buf); + ecex_file_browser_update_preview_if_enabled(ed); + return ECEX_OK; +} static int cmd_move_word_left(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); buffer_move_word_left(buf); return ECEX_OK; } static int cmd_move_word_right(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); buffer_move_word_right(buf); return ECEX_OK; } static int cmd_beginning_of_line(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); buffer_move_beginning_of_line(buf); return ECEX_OK; } @@ -145,91 +803,484 @@ static int cmd_kill_region(ecex_t *ed) { size_t end = 0; buffer_selection_range(buf, &start, &end); - char *text = buffer_substring(buf, start, end); - if (!text) return ECEX_ERR; + char *text = buffer_substring(buf, start, end); + if (!text) return ECEX_ERR; + + int result = ecex_clipboard_set(ed, text); + free(text); + if (result != ECEX_OK) return result; + + return buffer_delete_selection(buf); +} + +static int cmd_list_commands(ecex_t *ed) { return ecex_list_commands(ed); } +static int cmd_list_buffers(ecex_t *ed) { return ecex_list_buffers(ed); } +static int cmd_switch_buffer(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_SWITCH_BUFFER, "Switch buffer: "); return ECEX_OK; } +static int cmd_kill_buffer_command(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_KILL_BUFFER, "Kill buffer: "); return ECEX_OK; } +static int cmd_force_kill_buffer_command(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_FORCE_KILL_BUFFER, "Force kill buffer: "); return ECEX_OK; } +static int cmd_compile(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_COMPILE, "Compile: "); return ECEX_OK; } +static int cmd_grep(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_GREP, "Grep: "); return ECEX_OK; } +static int cmd_recompile(ecex_t *ed) { return ecex_rerun_compile(ed); } +static int cmd_regrep(ecex_t *ed) { return ecex_rerun_grep(ed); } +static int cmd_next_error(ecex_t *ed) { return ecex_next_interactive_action(ed); } +static int cmd_previous_error(ecex_t *ed) { return ecex_previous_interactive_action(ed); } +static int cmd_comment_region(ecex_t *ed) { return ecex_comment_region(ed); } +static int cmd_uncomment_region(ecex_t *ed) { return ecex_uncomment_region(ed); } +static int cmd_toggle_line_numbers(ecex_t *ed) { if (!ed) return ECEX_ERR; ed->theme.line_numbers_enabled = !ed->theme.line_numbers_enabled; return ECEX_OK; } +static int cmd_toggle_current_line(ecex_t *ed) { if (!ed) return ECEX_ERR; ed->theme.current_line_enabled = !ed->theme.current_line_enabled; return ECEX_OK; } +static int cmd_isearch_forward(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_ISEARCH_FORWARD, "I-search: "); return ECEX_OK; } +static int cmd_isearch_backward(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_ISEARCH_BACKWARD, "I-search backward: "); return ECEX_OK; } + +static int cmd_find_file(ecex_t *ed) { + ecex_request_prompt(ed, ECEX_PROMPT_FIND_FILE, "Find file: "); + return ECEX_OK; +} + +static int cmd_write_file(ecex_t *ed) { + ecex_request_prompt(ed, ECEX_PROMPT_WRITE_FILE, "Write file: "); + return ECEX_OK; +} + +static int cmd_save_buffer(ecex_t *ed) { + if (!ed) return ECEX_ERR; + + buffer_t *buf = ecex_current_buffer(ed); + if (!buf) return ECEX_ERR; + + if (!buf->path) { + ecex_request_prompt(ed, ECEX_PROMPT_WRITE_FILE, "Write file: "); + return ECEX_OK; + } + + return ecex_save_current_buffer(ed); +} + +static int cmd_eval_buffer(ecex_t *ed) { + return ecex_eval_current_buffer(ed); +} + +static int cmd_eval_line(ecex_t *ed) { + return ecex_eval_current_line(ed); +} + +static int cmd_eval_region(ecex_t *ed) { + return ecex_eval_current_region(ed); +} + +static int cmd_eval_file(ecex_t *ed) { + ecex_request_prompt(ed, ECEX_PROMPT_EVAL_FILE, "Eval file: "); + return ECEX_OK; +} + +static int cmd_eval_rerun_last(ecex_t *ed) { + return ecex_eval_rerun_last(ed); +} + +static int cmd_quit_window(ecex_t *ed) { + if (!ed) return ECEX_ERR; + if (ecex_window_count(ed) > 1) return ecex_delete_window(ed); + return ecex_previous_buffer(ed); +} + +static int cmd_reload_config(ecex_t *ed) { + return ecex_reload_config(ed); +} + + +/* Built-in file browser -------------------------------------------------- */ + +typedef struct ecex_file_entry { + char *name; + char *path; + int is_dir; + int is_image; + long long size; +} ecex_file_entry_t; + +static char ecex_fb_cwd[4096] = "."; +static char ecex_fb_history[64][4096]; +static size_t ecex_fb_history_count = 0; +static size_t ecex_fb_history_index = 0; +static int ecex_fb_preview_expanded = 0; + +static void ecex_file_entry_free(ecex_file_entry_t *entries, size_t count) { + if (!entries) return; + for (size_t i = 0; i < count; i++) { + free(entries[i].name); + free(entries[i].path); + } + free(entries); +} + +static int ecex_file_entry_compare(const void *a, const void *b) { + const ecex_file_entry_t *ea = (const ecex_file_entry_t *)a; + const ecex_file_entry_t *eb = (const ecex_file_entry_t *)b; + if (ea->is_dir != eb->is_dir) return eb->is_dir - ea->is_dir; + return strcmp(ea->name ? ea->name : "", eb->name ? eb->name : ""); +} + +static int ecex_file_browser_collect(const char *dir, ecex_file_entry_t **out_entries, size_t *out_count) { + if (out_entries) *out_entries = NULL; + if (out_count) *out_count = 0; + if (!dir || !out_entries || !out_count) return ECEX_ERR; + + DIR *d = opendir(dir); + if (!d) return ECEX_ERR; + + size_t cap = 64; + size_t count = 0; + ecex_file_entry_t *entries = calloc(cap, sizeof(*entries)); + if (!entries) { closedir(d); return ECEX_ERR; } + + struct dirent *entry; + while ((entry = readdir(d)) != NULL) { + const char *name = entry->d_name; + if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) continue; + + if (count == cap) { + size_t new_cap = cap * 2; + ecex_file_entry_t *grown = realloc(entries, new_cap * sizeof(*entries)); + if (!grown) break; + memset(grown + cap, 0, (new_cap - cap) * sizeof(*grown)); + entries = grown; + cap = new_cap; + } + + char *path = ecex_path_join(dir, name); + if (!path) continue; + char *name_copy = ecex_strdup(name); + if (!name_copy) { free(path); continue; } + + entries[count].name = name_copy; + entries[count].path = path; + entries[count].is_dir = ecex_path_is_dir(path); + entries[count].is_image = ecex_path_is_image(path); + entries[count].size = ecex_path_file_size(path); + count++; + } + + closedir(d); + qsort(entries, count, sizeof(*entries), ecex_file_entry_compare); + *out_entries = entries; + *out_count = count; + return ECEX_OK; +} + +static void ecex_file_browser_push_history(const char *dir) { + if (!dir || !dir[0]) return; + if (ecex_fb_history_count > 0 && strcmp(ecex_fb_history[ecex_fb_history_index], dir) == 0) return; + + if (ecex_fb_history_index + 1 < ecex_fb_history_count) { + ecex_fb_history_count = ecex_fb_history_index + 1; + } + + if (ecex_fb_history_count == 64) { + memmove(ecex_fb_history, ecex_fb_history + 1, sizeof(ecex_fb_history[0]) * 63); + ecex_fb_history_count = 63; + if (ecex_fb_history_index > 0) ecex_fb_history_index--; + } + + ecex_path_copy(ecex_fb_history[ecex_fb_history_count], sizeof(ecex_fb_history[0]), dir); + ecex_fb_history_index = ecex_fb_history_count; + ecex_fb_history_count++; +} + +static int ecex_file_browser_populate(ecex_t *ed, const char *dir, int push_history); +static int ecex_file_browser_move_to_action(buffer_t *buf, size_t action_index); + +static int ecex_file_browser_open_action(ecex_t *ed, + buffer_t *buffer, + size_t line, + const char *payload, + void *userdata) { + (void)buffer; + (void)line; + (void)userdata; + if (!ed || !payload) return ECEX_ERR; + + char selected[4096]; + ecex_path_copy(selected, sizeof(selected), payload); + + if (ecex_path_is_dir(selected)) return ecex_file_browser_populate(ed, selected, 1); + if (ecex_path_is_file(selected) && ecex_path_is_media(selected)) return ecex_media_open(ed, selected); + if (ecex_path_is_file(selected)) return ecex_find_file(ed, selected); + return ECEX_ERR; +} + +static int ecex_file_browser_populate(ecex_t *ed, const char *dir, int push_history) { + if (!ed) return ECEX_ERR; + + char *normalized = ecex_path_normalize((dir && dir[0]) ? dir : ecex_fb_cwd); + if (!normalized) return ECEX_ERR; + if (!ecex_path_is_dir(normalized)) { + char *parent = ecex_path_dirname(normalized); + free(normalized); + normalized = parent; + if (!normalized) return ECEX_ERR; + } + + ecex_path_copy(ecex_fb_cwd, sizeof(ecex_fb_cwd), normalized); + if (push_history) ecex_file_browser_push_history(ecex_fb_cwd); + + buffer_t *buf = ecex_find_buffer(ed, "*file-browser*"); + if (!buf) buf = ecex_create_interactive_buffer(ed, "*file-browser*"); + if (!buf) { free(normalized); return ECEX_ERR; } + + buffer_clear(buf); + buffer_set_interactive(buf, 1); + ecex_buffer_set_major_mode_by_name(ed, buf, "file-browser-mode"); + + char line[8192]; + snprintf(line, sizeof(line), + "File browser: %s\n\nKeys: RET/l open, h parent, g refresh, b/f history, v preview, m toggle preview, q quit.\n\n", + ecex_fb_cwd); + buffer_append(buf, line); + + char *parent = ecex_path_dirname(ecex_fb_cwd); + if (parent) { + ecex_interactive_append_line(ed, buf, "[..] parent", ecex_file_browser_open_action, parent, NULL); + free(parent); + } + + ecex_file_entry_t *entries = NULL; + size_t count = 0; + if (ecex_file_browser_collect(ecex_fb_cwd, &entries, &count) != ECEX_OK) { + buffer_append(buf, "\nCould not read directory.\n"); + } else { + for (size_t i = 0; i < count; i++) { + const char *tag = entries[i].is_dir ? "[D]" : (entries[i].is_image ? "[I]" : (ecex_path_is_video(entries[i].path) ? "[V]" : " ")); + if (entries[i].is_dir) { + snprintf(line, sizeof(line), "%s %s/", tag, entries[i].name); + } else if (entries[i].size >= 0) { + snprintf(line, sizeof(line), "%s %s (%lld bytes)", tag, entries[i].name, entries[i].size); + } else { + snprintf(line, sizeof(line), "%s %s", tag, entries[i].name); + } + ecex_interactive_append_line(ed, buf, line, ecex_file_browser_open_action, entries[i].path, NULL); + } + } + ecex_file_entry_free(entries, count); + + if (ecex_fb_preview_expanded) { + buffer_append(buf, "\nPreview pane is active: move up/down to update it, v refreshes, m closes it.\n"); + } + + buf->point = 0; + buf->scroll_line = 0; + buf->scroll_col = 0; + if (buf->interactive_action_count > 0) { + /* Start on the first actual directory entry instead of the [..] parent row. + * That makes pressing m/v immediately show a useful preview. */ + ecex_file_browser_move_to_action(buf, buf->interactive_action_count > 1 ? 1 : 0); + } + buf->modified = 0; + free(normalized); + int result = ecex_switch_buffer(ed, "*file-browser*"); + if (result == ECEX_OK && ecex_fb_preview_expanded) { + ecex_file_browser_update_preview_if_enabled(ed); + } + return result; +} + +static int ecex_file_browser_current_payload(ecex_t *ed, char *out, size_t out_size) { + if (!ed || !out || out_size == 0) return ECEX_ERR; + buffer_t *buf = ecex_current_buffer(ed); + if (!buf || !buffer_is_interactive(buf)) return ECEX_ERR; + size_t line = buffer_current_line_number(buf); + if (line > 0) line--; + ecex_interactive_line_action_t *action = buffer_interactive_action_at_line(buf, line); + if ((!action || !action->payload) && buf->interactive_action_count > 0) { + action = &buf->interactive_actions[0]; + } + if (!action || !action->payload) return ECEX_ERR; + return ecex_path_copy(out, out_size, action->payload) == 0 ? ECEX_OK : ECEX_ERR; +} + +static int ecex_file_browser_move_to_action(buffer_t *buf, size_t action_index) { + if (!buf || !buffer_is_interactive(buf) || buf->interactive_action_count == 0) return ECEX_ERR; + if (action_index >= buf->interactive_action_count) action_index = buf->interactive_action_count - 1; + size_t target_line = buf->interactive_actions[action_index].line; + size_t pos = 0; + for (size_t line = 0; line < target_line && pos < buf->len; pos++) { + if (buf->data[pos] == '\n') line++; + } + buffer_set_point(buf, pos); + return ECEX_OK; +} + +static buffer_t *ecex_file_preview_buffer(ecex_t *ed) { + if (!ed) return NULL; + buffer_t *preview = ecex_find_buffer(ed, "*file-preview*"); + if (!preview) preview = ecex_create_buffer(ed, "*file-preview*", NULL, 0); + return preview; +} + +static int ecex_file_browser_show_preview_pane(ecex_t *ed, buffer_t *preview) { + if (!ed || !preview || ed->window_count == 0) return ECEX_ERR; - int result = ecex_clipboard_set(ed, text); - free(text); - if (result != ECEX_OK) return result; + size_t browser_window = ed->current_window_index; + if (ed->window_count == 1) { + if (ecex_split_window_vertically(ed) != ECEX_OK) return ECEX_ERR; + ed->windows[ed->current_window_index].buffer = preview; + ed->current_window_index = browser_window; + return ecex_sync_current_buffer(ed); + } - return buffer_delete_selection(buf); + size_t preview_window = (browser_window + 1) % ed->window_count; + if (preview_window == browser_window && ed->window_count > 1) preview_window = 1; + ed->windows[preview_window].buffer = preview; + ed->current_window_index = browser_window; + return ecex_sync_current_buffer(ed); } -static int cmd_list_commands(ecex_t *ed) { return ecex_list_commands(ed); } -static int cmd_list_buffers(ecex_t *ed) { return ecex_list_buffers(ed); } -static int cmd_switch_buffer(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_SWITCH_BUFFER, "Switch buffer: "); return ECEX_OK; } -static int cmd_kill_buffer_command(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_KILL_BUFFER, "Kill buffer: "); return ECEX_OK; } -static int cmd_compile(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_COMPILE, "Compile: "); return ECEX_OK; } -static int cmd_grep(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_GREP, "Grep: "); return ECEX_OK; } -static int cmd_recompile(ecex_t *ed) { return ecex_rerun_compile(ed); } -static int cmd_regrep(ecex_t *ed) { return ecex_rerun_grep(ed); } -static int cmd_next_error(ecex_t *ed) { return ecex_next_interactive_action(ed); } -static int cmd_previous_error(ecex_t *ed) { return ecex_previous_interactive_action(ed); } -static int cmd_comment_region(ecex_t *ed) { return ecex_comment_region(ed); } -static int cmd_uncomment_region(ecex_t *ed) { return ecex_uncomment_region(ed); } -static int cmd_toggle_line_numbers(ecex_t *ed) { if (!ed) return ECEX_ERR; ed->theme.line_numbers_enabled = !ed->theme.line_numbers_enabled; return ECEX_OK; } -static int cmd_toggle_current_line(ecex_t *ed) { if (!ed) return ECEX_ERR; ed->theme.current_line_enabled = !ed->theme.current_line_enabled; return ECEX_OK; } -static int cmd_isearch_forward(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_ISEARCH_FORWARD, "I-search: "); return ECEX_OK; } -static int cmd_isearch_backward(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_ISEARCH_BACKWARD, "I-search backward: "); return ECEX_OK; } +static int ecex_file_browser_close_preview_pane(ecex_t *ed) { + if (!ed || ed->window_count <= 1) return ECEX_OK; + buffer_t *preview = ecex_find_buffer(ed, "*file-preview*"); + if (!preview) return ECEX_OK; -static int cmd_find_file(ecex_t *ed) { - ecex_request_prompt(ed, ECEX_PROMPT_FIND_FILE, "Find file: "); + for (size_t i = 0; i < ed->window_count; i++) { + if (ed->windows[i].buffer != preview) continue; + memmove(&ed->windows[i], &ed->windows[i + 1], (ed->window_count - i - 1) * sizeof(ed->windows[0])); + ed->window_count--; + if (ed->current_window_index >= ed->window_count) ed->current_window_index = ed->window_count - 1; + ecex_balance_windows(ed); + return ecex_sync_current_buffer(ed); + } return ECEX_OK; } -static int cmd_write_file(ecex_t *ed) { - ecex_request_prompt(ed, ECEX_PROMPT_WRITE_FILE, "Write file: "); +static int ecex_file_browser_fill_preview(ecex_t *ed, buffer_t *preview, const char *path) { + if (!ed || !preview || !path) return ECEX_ERR; + + if (ecex_path_is_media(path)) { + return ecex_media_load_into_buffer(ed, path, preview); + } + + ecex_media_buffer_clear(preview); + ecex_buffer_set_major_mode_by_name(ed, preview, "special-mode"); + preview->read_only = 0; + buffer_clear(preview); + char line[8192]; + snprintf(line, sizeof(line), "Preview: %s\n\n", path); + buffer_append(preview, line); + + if (ecex_path_is_dir(path)) { + buffer_append(preview, "Directory. Press RET/l in *file-browser* to open it.\n"); + } else if (ecex_path_is_file(path)) { + long long size = ecex_path_file_size(path); + snprintf(line, sizeof(line), "File size: %lld bytes.\n\n", size); + buffer_append(preview, line); + FILE *f = fopen(path, "rb"); + if (f) { + char chunk[8193]; + size_t n = fread(chunk, 1, sizeof(chunk) - 1, f); + fclose(f); + chunk[n] = '\0'; + int binary = 0; + for (size_t i = 0; i < n; i++) { + unsigned char c = (unsigned char)chunk[i]; + if (c == 0 || (c < 8) || (c > 13 && c < 32)) { binary = 1; break; } + } + if (binary) buffer_append(preview, "Binary file; text preview suppressed.\n"); + else buffer_append(preview, chunk); + } + } + + preview->modified = 0; + preview->read_only = 1; return ECEX_OK; } -static int cmd_save_buffer(ecex_t *ed) { - if (!ed) return ECEX_ERR; - - buffer_t *buf = ecex_current_buffer(ed); - if (!buf) return ECEX_ERR; +static int ecex_file_browser_preview_current(ecex_t *ed) { + char path[4096]; + if (ecex_file_browser_current_payload(ed, path, sizeof(path)) != ECEX_OK) return ECEX_ERR; - if (!buf->path) { - ecex_request_prompt(ed, ECEX_PROMPT_WRITE_FILE, "Write file: "); - return ECEX_OK; - } + buffer_t *preview = ecex_file_preview_buffer(ed); + if (!preview) return ECEX_ERR; - return ecex_save_current_buffer(ed); + int result = ecex_file_browser_fill_preview(ed, preview, path); + ecex_file_browser_show_preview_pane(ed, preview); + return result; } -static int cmd_eval_buffer(ecex_t *ed) { - return ecex_eval_current_buffer(ed); +static int ecex_file_browser_update_preview_if_enabled(ecex_t *ed) { + if (!ed || !ecex_fb_preview_expanded) return ECEX_OK; + buffer_t *buf = ecex_current_buffer(ed); + const char *mode = buf ? ecex_buffer_major_mode_name(ed, buf) : NULL; + if (!mode || strcmp(mode, "file-browser-mode") != 0) return ECEX_OK; + return ecex_file_browser_preview_current(ed); } -static int cmd_eval_line(ecex_t *ed) { - return ecex_eval_current_line(ed); +static int cmd_file_browser(ecex_t *ed) { + char cwd[4096]; + ecex_path_cwd(cwd, sizeof(cwd)); + return ecex_file_browser_populate(ed, cwd, 1); } -static int cmd_eval_region(ecex_t *ed) { - return ecex_eval_current_region(ed); +static int cmd_file_browser_here(ecex_t *ed) { + buffer_t *buf = ecex_current_buffer(ed); + if (buf && buf->path && buf->path[0]) { + char *dir = ecex_path_dirname(buf->path); + if (!dir) return ECEX_ERR; + int result = ecex_file_browser_populate(ed, dir, 1); + free(dir); + return result; + } + return cmd_file_browser(ed); } -static int cmd_eval_file(ecex_t *ed) { - ecex_request_prompt(ed, ECEX_PROMPT_EVAL_FILE, "Eval file: "); - return ECEX_OK; +static int cmd_file_browser_refresh(ecex_t *ed) { return ecex_file_browser_populate(ed, ecex_fb_cwd, 0); } + +static int cmd_file_browser_parent(ecex_t *ed) { + char *parent = ecex_path_dirname(ecex_fb_cwd); + if (!parent) return ECEX_ERR; + int result = ecex_file_browser_populate(ed, parent, 1); + free(parent); + return result; } -static int cmd_eval_rerun_last(ecex_t *ed) { - return ecex_eval_rerun_last(ed); +static int cmd_file_browser_open(ecex_t *ed) { return ecex_interactive_activate_current_line(ed); } +static int cmd_file_browser_preview(ecex_t *ed) { return ecex_file_browser_preview_current(ed); } + +static int cmd_file_browser_toggle_preview(ecex_t *ed) { + ecex_fb_preview_expanded = !ecex_fb_preview_expanded; + if (!ecex_fb_preview_expanded) { + ecex_file_browser_close_preview_pane(ed); + return ecex_file_browser_populate(ed, ecex_fb_cwd, 0); + } + int result = ecex_file_browser_populate(ed, ecex_fb_cwd, 0); + if (result == ECEX_OK) ecex_file_browser_preview_current(ed); + return result; } -static int cmd_quit_window(ecex_t *ed) { - if (!ed) return ECEX_ERR; - if (ecex_window_count(ed) > 1) return ecex_delete_window(ed); - return ecex_previous_buffer(ed); +static int cmd_file_browser_history_back(ecex_t *ed) { + if (ecex_fb_history_count == 0 || ecex_fb_history_index == 0) return ECEX_ERR; + ecex_fb_history_index--; + return ecex_file_browser_populate(ed, ecex_fb_history[ecex_fb_history_index], 0); } -static int cmd_reload_config(ecex_t *ed) { - return ecex_reload_config(ed); +static int cmd_file_browser_history_forward(ecex_t *ed) { + if (ecex_fb_history_count == 0 || ecex_fb_history_index + 1 >= ecex_fb_history_count) return ECEX_ERR; + ecex_fb_history_index++; + return ecex_file_browser_populate(ed, ecex_fb_history[ecex_fb_history_index], 0); } +static int cmd_media_play_pause(ecex_t *ed) { return ecex_media_toggle_playback(ed); } + static int ecex_register_builtins(ecex_t *ed) { ECEX_COMMAND("quit", cmd_quit); + ECEX_COMMAND("force-quit", cmd_force_quit); ECEX_COMMAND("find-file", cmd_find_file); + ECEX_COMMAND("file-browser", cmd_file_browser); + ECEX_COMMAND("file-browser-here", cmd_file_browser_here); + ECEX_COMMAND("file-browser-refresh", cmd_file_browser_refresh); + ECEX_COMMAND("file-browser-parent", cmd_file_browser_parent); + ECEX_COMMAND("file-browser-open", cmd_file_browser_open); + ECEX_COMMAND("file-browser-preview", cmd_file_browser_preview); + ECEX_COMMAND("file-browser-toggle-preview", cmd_file_browser_toggle_preview); + ECEX_COMMAND("file-browser-history-back", cmd_file_browser_history_back); + ECEX_COMMAND("file-browser-history-forward", cmd_file_browser_history_forward); + ECEX_COMMAND("media-play-pause", cmd_media_play_pause); ECEX_COMMAND("save-buffer", cmd_save_buffer); ECEX_COMMAND("write-file", cmd_write_file); ECEX_COMMAND("eval-buffer", cmd_eval_buffer); @@ -245,6 +1296,7 @@ static int ecex_register_builtins(ecex_t *ed) { ECEX_COMMAND("list-buffers", cmd_list_buffers); ECEX_COMMAND("switch-buffer", cmd_switch_buffer); ECEX_COMMAND("kill-buffer", cmd_kill_buffer_command); + ECEX_COMMAND("force-kill-buffer", cmd_force_kill_buffer_command); ECEX_COMMAND("compile", cmd_compile); ECEX_COMMAND("recompile", cmd_recompile); ECEX_COMMAND("grep", cmd_grep); @@ -320,11 +1372,15 @@ static int ecex_register_builtins(ecex_t *ed) { ECEX_BIND("C-S-z", "redo"); ECEX_BIND("C-x C-f", "find-file"); + ECEX_BIND("C-x f", "find-file"); + ECEX_BIND("C-x d", "file-browser"); + ECEX_BIND("C-x C-d", "file-browser-here"); ECEX_BIND("C-x C-s", "save-buffer"); ECEX_BIND("C-x C-w", "write-file"); ECEX_BIND("C-x C-b", "list-buffers"); ECEX_BIND("C-x b", "switch-buffer"); ECEX_BIND("C-x k", "kill-buffer"); + ECEX_BIND("C-x K", "force-kill-buffer"); ECEX_BIND("C-x 2", "split-window-below"); ECEX_BIND("C-x 3", "split-window-right"); ECEX_BIND("C-x o", "other-window"); @@ -351,10 +1407,26 @@ static int ecex_register_builtins(ecex_t *ed) { ecex_define_major_mode(ed, "c-mode"); ecex_define_major_mode(ed, "eval-output-mode"); ecex_define_major_mode(ed, "special-mode"); + ecex_define_major_mode(ed, "file-browser-mode"); + ecex_define_major_mode(ed, "media-preview-mode"); + ecex_define_major_mode(ed, "markdown-mode"); ecex_bind_mode_key(ed, "eval-output-mode", "g", "eval-rerun-last"); ecex_bind_mode_key(ed, "eval-output-mode", "q", "quit-window"); ecex_bind_mode_key(ed, "eval-output-mode", "r", "eval-rerun-last"); ecex_bind_mode_key(ed, "special-mode", "q", "quit-window"); + + ecex_bind_mode_key(ed, "file-browser-mode", "g", "file-browser-refresh"); + ecex_bind_mode_key(ed, "file-browser-mode", "r", "file-browser-refresh"); + ecex_bind_mode_key(ed, "file-browser-mode", "h", "file-browser-parent"); + ecex_bind_mode_key(ed, "file-browser-mode", "l", "file-browser-open"); + ecex_bind_mode_key(ed, "file-browser-mode", "v", "file-browser-preview"); + ecex_bind_mode_key(ed, "file-browser-mode", "m", "file-browser-toggle-preview"); + ecex_bind_mode_key(ed, "file-browser-mode", "b", "file-browser-history-back"); + ecex_bind_mode_key(ed, "file-browser-mode", "f", "file-browser-history-forward"); + ecex_bind_mode_key(ed, "file-browser-mode", "q", "quit-window"); + ecex_bind_mode_key(ed, "media-preview-mode", "SPC", "media-play-pause"); + ecex_bind_mode_key(ed, "media-preview-mode", "p", "media-play-pause"); + ecex_bind_mode_key(ed, "media-preview-mode", "q", "quit-window"); ecex_bind_mode_key(ed, "special-mode", "g", "recompile"); ecex_bind_mode_key(ed, "special-mode", "n", "next-error"); ecex_bind_mode_key(ed, "special-mode", "p", "previous-error"); @@ -410,14 +1482,18 @@ ecex_t *ecex_new(void) { void ecex_free(ecex_t *ed) { if (!ed) return; - for (size_t i = 0; i < ed->jit_module_count; i++) { - ccdjit_module_free((ccdjit_module *)ed->jit_modules[i]); - } - + /* Buffers may hold renderer/animation callbacks and userdata destructors + * compiled by CCDJIT config modules. Run those destructors while their JIT + * modules are still alive; freeing modules first leaves callback pointers + * dangling and can segfault during buffer teardown. */ for (size_t i = 0; i < ed->buffer_count; i++) { buffer_free(ed->buffers[i]); } + for (size_t i = 0; i < ed->jit_module_count; i++) { + ccdjit_module_free((ccdjit_module *)ed->jit_modules[i]); + } + for (size_t i = 0; i < ed->command_count; i++) { free(ed->commands[i].name); } @@ -436,6 +1512,22 @@ void ecex_free(ecex_t *ed) { free(ed->major_modes[i].name); } + for (size_t i = 0; i < ed->var_count; i++) { + ecex_var_entry_clear(&ed->vars[i]); + } + + for (size_t i = 0; i < ed->text_count; i++) { + ecex_text_entry_clear(&ed->texts[i]); + } + + for (size_t i = 0; i < ed->file_handler_count; i++) { + free(ed->file_handlers[i].extension); + } + + for (size_t i = 0; i < ed->object_count; i++) { + free(ed->objects[i].ptr); + } + free(ed->jit_modules); free(ed->windows); free(ed->buffers); @@ -443,6 +1535,10 @@ void ecex_free(ecex_t *ed) { free(ed->keybinds); free(ed->mode_keybinds); free(ed->major_modes); + free(ed->vars); + free(ed->texts); + free(ed->file_handlers); + free(ed->objects); free(ed->last_eval_source); free(ed->last_eval_filename); free(ed->last_compile_command); @@ -513,10 +1609,27 @@ buffer_t *ecex_find_buffer(ecex_t *ed, const char *name) { } +static int ecex_buffer_index_of(ecex_t *ed, buffer_t *buffer, size_t *out_index) { + if (!ed || !buffer) return ECEX_ERR; + for (size_t i = 0; i < ed->buffer_count; i++) { + if (ed->buffers[i] == buffer) { + if (out_index) *out_index = i; + return ECEX_OK; + } + } + return ECEX_ERR; +} + static int ecex_set_current_buffer_index(ecex_t *ed, size_t index) { if (!ed || index >= ed->buffer_count) return ECEX_ERR; + + buffer_t *next = ed->buffers[index]; + if (ed->current_buffer && ed->current_buffer != next) { + ed->previous_buffer = ed->current_buffer; + } + ed->current_buffer_index = index; - ed->current_buffer = ed->buffers[index]; + ed->current_buffer = next; ecex_window_t *win = ecex_current_window(ed); if (win) win->buffer = ed->current_buffer; return ECEX_OK; @@ -527,9 +1640,14 @@ int ecex_sync_current_buffer(ecex_t *ed) { ecex_window_t *win = ecex_current_window(ed); if (!win || !win->buffer) return ECEX_ERR; - ed->current_buffer = win->buffer; + buffer_t *next = win->buffer; + if (ed->current_buffer && ed->current_buffer != next) { + ed->previous_buffer = ed->current_buffer; + } + + ed->current_buffer = next; for (size_t i = 0; i < ed->buffer_count; i++) { - if (ed->buffers[i] == win->buffer) { + if (ed->buffers[i] == next) { ed->current_buffer_index = i; return ECEX_OK; } @@ -538,7 +1656,14 @@ int ecex_sync_current_buffer(ecex_t *ed) { } int ecex_switch_buffer(ecex_t *ed, const char *name) { - if (!ed || !name) return ECEX_ERR; + if (!ed) return ECEX_ERR; + + if (!name || name[0] == '\0') { + buffer_t *other = ecex_other_buffer(ed); + size_t index = 0; + if (!other || ecex_buffer_index_of(ed, other, &index) != ECEX_OK) return ECEX_ERR; + return ecex_set_current_buffer_index(ed, index); + } for (size_t i = 0; i < ed->buffer_count; i++) { if (strcmp(ed->buffers[i]->name, name) == 0) { @@ -556,6 +1681,22 @@ buffer_t *ecex_current_buffer(ecex_t *ed) { return ed->current_buffer; } +buffer_t *ecex_other_buffer(ecex_t *ed) { + if (!ed || ed->buffer_count == 0) return NULL; + + buffer_t *current = ecex_current_buffer(ed); + if (ed->previous_buffer && ed->previous_buffer != current && + ecex_buffer_index_of(ed, ed->previous_buffer, NULL) == ECEX_OK) { + return ed->previous_buffer; + } + + for (size_t i = 0; i < ed->buffer_count; i++) { + if (ed->buffers[i] && ed->buffers[i] != current) return ed->buffers[i]; + } + + return current; +} + ecex_window_t *ecex_current_window(ecex_t *ed) { if (!ed || ed->window_count == 0 || ed->current_window_index >= ed->window_count) return NULL; return &ed->windows[ed->current_window_index]; @@ -688,32 +1829,40 @@ int ecex_previous_buffer(ecex_t *ed) { return ecex_set_current_buffer_index(ed, ed->current_buffer_index); } -int ecex_kill_buffer(ecex_t *ed, const char *name) { - if (!ed || !name || ed->buffer_count == 0) return ECEX_ERR; +static int ecex_kill_buffer_impl(ecex_t *ed, const char *name, int force) { + if (!ed || ed->buffer_count == 0) return ECEX_ERR; size_t index = ed->buffer_count; - for (size_t i = 0; i < ed->buffer_count; i++) { - if (strcmp(ed->buffers[i]->name, name) == 0) { - index = i; - break; + if (!name || name[0] == '\0') { + buffer_t *current = ecex_current_buffer(ed); + if (!current || ecex_buffer_index_of(ed, current, &index) != ECEX_OK) return ECEX_ERR; + } else { + for (size_t i = 0; i < ed->buffer_count; i++) { + if (strcmp(ed->buffers[i]->name, name) == 0) { + index = i; + break; + } } } if (index == ed->buffer_count) return ECEX_ERR; buffer_t *victim = ed->buffers[index]; - - for (size_t i = index; i + 1 < ed->buffer_count; i++) { - ed->buffers[i] = ed->buffers[i + 1]; + if (ed->previous_buffer == victim) ed->previous_buffer = NULL; + if (!force && victim->modified && !victim->read_only) { + fprintf(stderr, + "ecex: refusing to kill modified buffer '%s'; save it or use force-kill-buffer.\n", + victim->name ? victim->name : ""); + return ECEX_ERR; } + for (size_t i = index; i + 1 < ed->buffer_count; i++) ed->buffers[i] = ed->buffers[i + 1]; ed->buffer_count--; - buffer_t *fallback = ed->buffer_count > 0 ? ed->buffers[0] : NULL; + buffer_t *fallback = ecex_other_buffer(ed); + if (fallback == victim) fallback = ed->buffer_count > 0 ? ed->buffers[0] : NULL; for (size_t i = 0; i < ed->window_count; i++) { - if (ed->windows[i].buffer == victim) { - ed->windows[i].buffer = fallback; - } + if (ed->windows[i].buffer == victim) ed->windows[i].buffer = fallback; } buffer_free(victim); @@ -733,6 +1882,23 @@ int ecex_kill_buffer(ecex_t *ed, const char *name) { return ecex_sync_current_buffer(ed); } +int ecex_kill_buffer(ecex_t *ed, const char *name) { + return ecex_kill_buffer_impl(ed, name, 0); +} + +int ecex_kill_buffer_force(ecex_t *ed, const char *name) { + return ecex_kill_buffer_impl(ed, name, 1); +} + +int ecex_has_modified_buffers(ecex_t *ed) { + if (!ed) return 0; + for (size_t i = 0; i < ed->buffer_count; i++) { + buffer_t *buf = ed->buffers[i]; + if (buf && buf->modified && !buf->read_only) return 1; + } + return 0; +} + int ecex_keep_jit_module(ecex_t *ed, void *module) { if (!ed || !module) return ECEX_ERR; @@ -795,6 +1961,129 @@ static void ecex_clear_mode_keybinds(ecex_t *ed) { ed->mode_keybind_count = 0; } +typedef struct ecex_binding_snapshot { + ecex_command_t *commands; + size_t command_count; + size_t command_cap; + ecex_keybind_t *keybinds; + size_t keybind_count; + size_t keybind_cap; + ecex_mode_keybind_t *mode_keybinds; + size_t mode_keybind_count; + size_t mode_keybind_cap; + ecex_theme_t theme; +} ecex_binding_snapshot_t; + +static void ecex_theme_snapshot_free(ecex_theme_t *theme) { + if (!theme) return; + free(theme->font_path); + theme->font_path = NULL; +} + +static int ecex_theme_clone(ecex_theme_t *out, const ecex_theme_t *in) { + if (!out || !in) return ECEX_ERR; + *out = *in; + out->font_path = NULL; + if (in->font_path) { + out->font_path = ecex_strdup(in->font_path); + if (!out->font_path) return ECEX_ERR; + } + return ECEX_OK; +} + +static void ecex_snapshot_free(ecex_binding_snapshot_t *snap) { + if (!snap) return; + for (size_t i = 0; i < snap->command_count; i++) free(snap->commands[i].name); + for (size_t i = 0; i < snap->keybind_count; i++) { + free(snap->keybinds[i].key); + free(snap->keybinds[i].command); + } + for (size_t i = 0; i < snap->mode_keybind_count; i++) { + free(snap->mode_keybinds[i].key); + free(snap->mode_keybinds[i].command); + } + free(snap->commands); + free(snap->keybinds); + free(snap->mode_keybinds); + ecex_theme_snapshot_free(&snap->theme); + memset(snap, 0, sizeof(*snap)); +} + +static int ecex_snapshot_clone(ecex_binding_snapshot_t *snap, ecex_t *ed) { + if (!snap || !ed) return ECEX_ERR; + memset(snap, 0, sizeof(*snap)); + + if (ecex_theme_clone(&snap->theme, &ed->theme) != ECEX_OK) return ECEX_ERR; + + if (ed->command_cap) { + snap->commands = calloc(ed->command_cap, sizeof(*snap->commands)); + if (!snap->commands) goto fail; + snap->command_cap = ed->command_cap; + snap->command_count = ed->command_count; + for (size_t i = 0; i < ed->command_count; i++) { + snap->commands[i].fn = ed->commands[i].fn; + snap->commands[i].name = ecex_strdup(ed->commands[i].name); + if (!snap->commands[i].name) goto fail; + } + } + + if (ed->keybind_cap) { + snap->keybinds = calloc(ed->keybind_cap, sizeof(*snap->keybinds)); + if (!snap->keybinds) goto fail; + snap->keybind_cap = ed->keybind_cap; + snap->keybind_count = ed->keybind_count; + for (size_t i = 0; i < ed->keybind_count; i++) { + snap->keybinds[i].key = ecex_strdup(ed->keybinds[i].key); + snap->keybinds[i].command = ecex_strdup(ed->keybinds[i].command); + if (!snap->keybinds[i].key || !snap->keybinds[i].command) goto fail; + } + } + + if (ed->mode_keybind_cap) { + snap->mode_keybinds = calloc(ed->mode_keybind_cap, sizeof(*snap->mode_keybinds)); + if (!snap->mode_keybinds) goto fail; + snap->mode_keybind_cap = ed->mode_keybind_cap; + snap->mode_keybind_count = ed->mode_keybind_count; + for (size_t i = 0; i < ed->mode_keybind_count; i++) { + snap->mode_keybinds[i].mode = ed->mode_keybinds[i].mode; + snap->mode_keybinds[i].key = ecex_strdup(ed->mode_keybinds[i].key); + snap->mode_keybinds[i].command = ecex_strdup(ed->mode_keybinds[i].command); + if (!snap->mode_keybinds[i].key || !snap->mode_keybinds[i].command) goto fail; + } + } + + return ECEX_OK; + +fail: + ecex_snapshot_free(snap); + return ECEX_ERR; +} + +static void ecex_restore_snapshot(ecex_t *ed, ecex_binding_snapshot_t *snap) { + if (!ed || !snap) return; + + ecex_clear_commands(ed); + ecex_clear_keybinds(ed); + ecex_clear_mode_keybinds(ed); + free(ed->commands); + free(ed->keybinds); + free(ed->mode_keybinds); + free(ed->theme.font_path); + + ed->commands = snap->commands; + ed->command_count = snap->command_count; + ed->command_cap = snap->command_cap; + ed->keybinds = snap->keybinds; + ed->keybind_count = snap->keybind_count; + ed->keybind_cap = snap->keybind_cap; + ed->mode_keybinds = snap->mode_keybinds; + ed->mode_keybind_count = snap->mode_keybind_count; + ed->mode_keybind_cap = snap->mode_keybind_cap; + ed->theme = snap->theme; + + memset(snap, 0, sizeof(*snap)); +} + int ecex_reload_config(ecex_t *ed) { if (!ed || !ed->config_path || !ed->config_path[0]) { fprintf(stderr, "ecex: no config file to reload; start with --config path/to/ecexrc.c\n"); @@ -804,25 +2093,89 @@ int ecex_reload_config(ecex_t *ed) { char *path = ecex_strdup(ed->config_path); if (!path) return ECEX_ERR; + ecex_binding_snapshot_t snapshot; + if (ecex_snapshot_clone(&snapshot, ed) != ECEX_OK) { + free(path); + fprintf(stderr, "ecex: failed to snapshot config state before reload\n"); + return ECEX_ERR; + } + ecex_clear_commands(ed); ecex_clear_keybinds(ed); ecex_clear_mode_keybinds(ed); - if (ecex_register_builtins(ed) != ECEX_OK) { - free(path); - return ECEX_ERR; + int result = ECEX_ERR; + if (ecex_register_builtins(ed) == ECEX_OK) { + result = ecex_load_c_config(ed, path); + if (result == ECEX_OK) result = ecex_validate_bindings(ed); + } + + if (result != ECEX_OK) { + fprintf(stderr, "ecex: config reload failed; keeping previous config active\n"); + ecex_restore_snapshot(ed, &snapshot); + } else { + ecex_snapshot_free(&snapshot); } - int result = ecex_load_c_config(ed, path); free(path); return result; } + +int ecex_config_register_commands(ecex_t *ed, const ecex_config_command_t *commands, size_t count) { + if (!ed || (!commands && count != 0)) return ECEX_ERR; + for (size_t i = 0; i < count; i++) { + if (!commands[i].name || !commands[i].fn) return ECEX_ERR; + if (ecex_register_command(ed, commands[i].name, commands[i].fn) != ECEX_OK) return ECEX_ERR; + } + return ECEX_OK; +} + +int ecex_config_bind_keys(ecex_t *ed, const ecex_config_keybind_t *bindings, size_t count) { + if (!ed || (!bindings && count != 0)) return ECEX_ERR; + for (size_t i = 0; i < count; i++) { + if (!bindings[i].key || !bindings[i].command) return ECEX_ERR; + if (ecex_bind_key(ed, bindings[i].key, bindings[i].command) != ECEX_OK) return ECEX_ERR; + } + return ECEX_OK; +} + +int ecex_config_bind_mode_keys(ecex_t *ed, const ecex_config_mode_keybind_t *bindings, size_t count) { + if (!ed || (!bindings && count != 0)) return ECEX_ERR; + for (size_t i = 0; i < count; i++) { + if (!bindings[i].mode || !bindings[i].key || !bindings[i].command) return ECEX_ERR; + if (ecex_bind_mode_key(ed, bindings[i].mode, bindings[i].key, bindings[i].command) != ECEX_OK) return ECEX_ERR; + } + return ECEX_OK; +} + +int ecex_config_define_modes(ecex_t *ed, const char *const *modes, size_t count) { + if (!ed || (!modes && count != 0)) return ECEX_ERR; + for (size_t i = 0; i < count; i++) { + if (!modes[i] || !ecex_define_major_mode(ed, modes[i])) return ECEX_ERR; + } + return ECEX_OK; +} + +int ecex_apply_theme(ecex_t *ed, const ecex_theme_t *theme) { + if (!ed || !theme) return ECEX_ERR; + char *font = NULL; + if (theme->font_path) { + font = ecex_strdup(theme->font_path); + if (!font) return ECEX_ERR; + } + free(ed->theme.font_path); + ed->theme = *theme; + ed->theme.font_path = font; + return ECEX_OK; +} + int ecex_register_command(ecex_t *ed, const char *name, ecex_command_fn fn) { if (!ed || !name || !fn) return ECEX_ERR; for (size_t i = 0; i < ed->command_count; i++) { if (strcmp(ed->commands[i].name, name) == 0) { + fprintf(stderr, "ecex: command warning: replacing existing command '%s'\n", name); ed->commands[i].fn = fn; return ECEX_OK; } @@ -877,11 +2230,66 @@ int ecex_clipboard_set(ecex_t *ed, const char *text) { return ECEX_OK; } +static int ecex_command_exists(ecex_t *ed, const char *name) { + if (!ed || !name) return 0; + for (size_t i = 0; i < ed->command_count; i++) { + if (ed->commands[i].name && strcmp(ed->commands[i].name, name) == 0) return 1; + } + return 0; +} + +static int ecex_key_is_prefix_of(const char *prefix, const char *key) { + if (!prefix || !key) return 0; + size_t n = strlen(prefix); + return strncmp(prefix, key, n) == 0 && key[n] == ' '; +} + +static void ecex_warn_keybind_issue(const char *scope, const char *key, const char *detail) { + if (scope && scope[0]) { + fprintf(stderr, + "ecex: keybind warning [%s]: %s%s%s\n", + scope, + key ? key : "", + detail && detail[0] ? " " : "", + detail ? detail : ""); + } else { + fprintf(stderr, + "ecex: keybind warning: %s%s%s\n", + key ? key : "", + detail && detail[0] ? " " : "", + detail ? detail : ""); + } +} + +static void ecex_warn_keybind_conflicts(ecex_t *ed, const char *scope, const char *key, int mode) { + if (!ed || !key) return; + + for (size_t i = 0; i < ed->keybind_count; i++) { + if (mode != 0) break; + const char *existing = ed->keybinds[i].key; + if (existing && ecex_key_is_prefix_of(key, existing)) ecex_warn_keybind_issue(scope, key, "is a prefix of an existing binding"); + if (existing && ecex_key_is_prefix_of(existing, key)) ecex_warn_keybind_issue(scope, key, "extends an existing complete binding"); + } + + for (size_t i = 0; i < ed->mode_keybind_count; i++) { + if (mode != ed->mode_keybinds[i].mode) continue; + const char *existing = ed->mode_keybinds[i].key; + if (existing && ecex_key_is_prefix_of(key, existing)) ecex_warn_keybind_issue(scope, key, "is a prefix of an existing mode binding"); + if (existing && ecex_key_is_prefix_of(existing, key)) ecex_warn_keybind_issue(scope, key, "extends an existing complete mode binding"); + } +} + int ecex_bind_key(ecex_t *ed, const char *key, const char *command) { if (!ed || !key || !command) return ECEX_ERR; + if (!ecex_command_exists(ed, command)) { + ecex_warn_keybind_issue("global", key, "targets a command that is not registered yet"); + } + ecex_warn_keybind_conflicts(ed, "global", key, 0); + for (size_t i = 0; i < ed->keybind_count; i++) { if (strcmp(ed->keybinds[i].key, key) == 0) { + fprintf(stderr, "ecex: keybind warning [global]: replacing %s from %s to %s\n", key, ed->keybinds[i].command, command); char *new_command = ecex_strdup(command); if (!new_command) return ECEX_ERR; @@ -990,8 +2398,14 @@ int ecex_bind_mode_key(ecex_t *ed, const char *mode_name, const char *key, const int mode = ecex_define_major_mode(ed, mode_name); if (!mode) return ECEX_ERR; + if (!ecex_command_exists(ed, command)) { + ecex_warn_keybind_issue(mode_name, key, "targets a command that is not registered yet"); + } + ecex_warn_keybind_conflicts(ed, mode_name, key, mode); + for (size_t i = 0; i < ed->mode_keybind_count; i++) { if (ed->mode_keybinds[i].mode == mode && strcmp(ed->mode_keybinds[i].key, key) == 0) { + fprintf(stderr, "ecex: keybind warning [%s]: replacing %s from %s to %s\n", mode_name, key, ed->mode_keybinds[i].command, command); char *new_command = ecex_strdup(command); if (!new_command) return ECEX_ERR; free(ed->mode_keybinds[i].command); @@ -1022,6 +2436,43 @@ int ecex_bind_mode_key(ecex_t *ed, const char *mode_name, const char *key, const return ECEX_OK; } +int ecex_validate_bindings(ecex_t *ed) { + if (!ed) return ECEX_ERR; + int ok = ECEX_OK; + + for (size_t i = 0; i < ed->keybind_count; i++) { + const char *command = ed->keybinds[i].command; + if (!ecex_command_exists(ed, command)) { + ecex_warn_keybind_issue("global", ed->keybinds[i].key, "targets a missing command"); + ok = ECEX_ERR; + } + for (size_t j = i + 1; j < ed->keybind_count; j++) { + if (ecex_key_is_prefix_of(ed->keybinds[i].key, ed->keybinds[j].key) || + ecex_key_is_prefix_of(ed->keybinds[j].key, ed->keybinds[i].key)) { + ecex_warn_keybind_issue("global", ed->keybinds[i].key, "has a prefix conflict"); + } + } + } + + for (size_t i = 0; i < ed->mode_keybind_count; i++) { + const char *command = ed->mode_keybinds[i].command; + const char *mode = ecex_major_mode_name(ed, ed->mode_keybinds[i].mode); + if (!ecex_command_exists(ed, command)) { + ecex_warn_keybind_issue(mode, ed->mode_keybinds[i].key, "targets a missing command"); + ok = ECEX_ERR; + } + for (size_t j = i + 1; j < ed->mode_keybind_count; j++) { + if (ed->mode_keybinds[i].mode != ed->mode_keybinds[j].mode) continue; + if (ecex_key_is_prefix_of(ed->mode_keybinds[i].key, ed->mode_keybinds[j].key) || + ecex_key_is_prefix_of(ed->mode_keybinds[j].key, ed->mode_keybinds[i].key)) { + ecex_warn_keybind_issue(mode, ed->mode_keybinds[i].key, "has a mode prefix conflict"); + } + } + } + + return ok; +} + const char *ecex_lookup_key_for_buffer(ecex_t *ed, buffer_t *buffer, const char *key) { if (!ed || !key) return NULL; @@ -1243,15 +2694,6 @@ int ecex_interactive_activate_current_line(ecex_t *ed) { return action->fn(ed, buffer, line, action->payload, action->userdata); } -static const char *ecex_basename(const char *path) { - if (!path || !path[0]) return "untitled"; - - const char *last_slash = strrchr(path, '/'); - if (last_slash && last_slash[1]) return last_slash + 1; - if (last_slash && last_slash == path) return "/"; - return path; -} - static int ecex_buffer_path_equal(buffer_t *buffer, const char *path) { return buffer && buffer->path && path && strcmp(buffer->path, path) == 0; } @@ -1259,35 +2701,55 @@ static int ecex_buffer_path_equal(buffer_t *buffer, const char *path) { int ecex_find_file(ecex_t *ed, const char *path) { if (!ed || !path || !path[0]) return ECEX_ERR; + char *normal_path = ecex_path_normalize(path); + if (!normal_path) return ECEX_ERR; + + if (ecex_path_is_dir(normal_path)) { + int result = ecex_file_browser_populate(ed, normal_path, 1); + free(normal_path); + return result; + } + + if (ecex_path_is_file(normal_path) && ecex_path_is_media(normal_path)) { + int result = ecex_media_open(ed, normal_path); + free(normal_path); + return result; + } + for (size_t i = 0; i < ed->buffer_count; i++) { - if (ecex_buffer_path_equal(ed->buffers[i], path)) { - return ecex_set_current_buffer_index(ed, i); + if (ecex_buffer_path_equal(ed->buffers[i], normal_path)) { + int result; + free(normal_path); + result = ecex_set_current_buffer_index(ed, i); + if (result == ECEX_OK && !ecex_buffer_has_renderer(ed->buffers[i])) ecex_run_file_handlers(ed, ed->buffers[i]); + return result; } } - const char *name = ecex_basename(path); + char *name = ecex_path_basename_dup(normal_path); + if (!name) { free(normal_path); return ECEX_ERR; } buffer_t *buf = ecex_create_buffer(ed, name, NULL, 0); - if (!buf) return ECEX_ERR; + free(name); + if (!buf) { free(normal_path); return ECEX_ERR; } - if (ecex_file_exists(path)) { - if (buffer_load_file(buf, path) != ECEX_OK) { - ecex_kill_buffer(ed, buf->name); + if (ecex_file_exists(normal_path)) { + if (buffer_load_file(buf, normal_path) != ECEX_OK) { + ecex_kill_buffer_force(ed, buf->name); + free(normal_path); return ECEX_ERR; } } else { - char *new_path = ecex_strdup(path); - if (!new_path) { - ecex_kill_buffer(ed, buf->name); - return ECEX_ERR; - } - free(buf->path); - buf->path = new_path; + buf->path = normal_path; + normal_path = NULL; buf->modified = 0; } + free(normal_path); ecex_auto_set_major_mode(ed, buf); - return ecex_set_current_buffer_index(ed, ed->buffer_count ? ed->buffer_count - 1 : 0); + int switch_result = ecex_set_current_buffer_index(ed, ed->buffer_count ? ed->buffer_count - 1 : 0); + if (switch_result == ECEX_OK) ecex_run_file_handlers(ed, buf); + return switch_result; } int ecex_save_current_buffer(ecex_t *ed) { @@ -1299,7 +2761,10 @@ int ecex_save_current_buffer(ecex_t *ed) { int ecex_write_current_buffer(ecex_t *ed, const char *path) { buffer_t *buf = ecex_current_buffer(ed); if (!buf || !path || !path[0]) return ECEX_ERR; - int result = buffer_save_as(buf, path); + char *normal_path = ecex_path_normalize(path); + if (!normal_path) return ECEX_ERR; + int result = buffer_save_as(buf, normal_path); + free(normal_path); if (result == ECEX_OK) ecex_auto_set_major_mode(ed, buf); return result; } -- cgit v1.2.3