#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); #define ECEX_INITIAL_BUFFER_CAP 8 #define ECEX_INITIAL_COMMAND_CAP 32 #define ECEX_INITIAL_KEYBIND_CAP 32 #define ECEX_INITIAL_WINDOW_CAP 4 #define ECEX_INITIAL_MODE_CAP 8 #define ECEX_INITIAL_MODE_KEYBIND_CAP 16 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; } static void ecex_theme_set_defaults(ecex_t *ed) { if (!ed) return; ed->theme.font_path = NULL; ed->theme.font_size = 22.0f; ed->theme.bg = ecex_color(0.08f, 0.09f, 0.11f); ed->theme.fg = ecex_color(0.88f, 0.88f, 0.86f); ed->theme.status_bg = ecex_color(0.15f, 0.15f, 0.18f); ed->theme.status_fg = ecex_color(0.86f, 0.86f, 0.84f); ed->theme.status_border = ecex_color(0.28f, 0.28f, 0.32f); ed->theme.cursor = ecex_color(1.0f, 1.0f, 1.0f); ed->theme.region_bg = ecex_color(0.22f, 0.34f, 0.48f); ed->theme.minibuffer_bg = ecex_color(0.10f, 0.11f, 0.14f); ed->theme.minibuffer_fg = ecex_color(0.95f, 0.95f, 0.90f); ed->theme.completion_fg = ecex_color(0.45f, 0.45f, 0.45f); ed->theme.completion_enabled = 1; ed->theme.interactive_highlight_bg = ecex_color(0.18f, 0.20f, 0.24f); ed->theme.interactive_highlight_fg = ecex_color(1.0f, 1.0f, 1.0f); ed->theme.current_line_bg = ecex_color(0.12f, 0.13f, 0.16f); ed->theme.search_bg = ecex_color(0.55f, 0.42f, 0.12f); ed->theme.line_numbers_enabled = 1; ed->theme.current_line_enabled = 1; } #define ECEX_COMMAND(name, fn) \ do { \ if (ecex_register_command(ed, (name), (fn)) != ECEX_OK) return ECEX_ERR; \ } while (0) #define ECEX_BIND(key, command) \ do { \ if (ecex_bind_key(ed, (key), (command)) != ECEX_OK) return ECEX_ERR; \ } while (0) #define CURRENT_BUFFER_OR_ERR(ed) \ buffer_t *buf = ecex_current_buffer(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; } static int cmd_next_buffer(ecex_t *ed) { return ecex_next_buffer(ed); } static int cmd_previous_buffer(ecex_t *ed) { return ecex_previous_buffer(ed); } static int cmd_split_window_right(ecex_t *ed) { return ecex_split_window_vertically(ed); } static int cmd_split_window_below(ecex_t *ed) { return ecex_split_window_horizontally(ed); } static int cmd_other_window(ecex_t *ed) { return ecex_other_window(ed); } static int cmd_previous_window(ecex_t *ed) { return ecex_previous_window(ed); } static int cmd_delete_window(ecex_t *ed) { return ecex_delete_window(ed); } static int cmd_delete_other_windows(ecex_t *ed) { return ecex_delete_other_windows(ed); } 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); 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; } static int cmd_end_of_line(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); buffer_move_end_of_line(buf); return ECEX_OK; } static int cmd_beginning_of_buffer(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); buffer_move_beginning_of_buffer(buf); return ECEX_OK; } static int cmd_end_of_buffer(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); buffer_move_end_of_buffer(buf); return ECEX_OK; } static int cmd_undo(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); return buffer_undo(buf); } static int cmd_redo(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); return buffer_redo(buf); } static int cmd_backspace(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); return buffer_backspace(buf); } static int cmd_delete_forward(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); return buffer_delete_forward(buf); } static int cmd_newline(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); if (buffer_is_interactive(buf)) { return ecex_interactive_activate_current_line(ed); } return buffer_insert_char(buf, '\n'); } static int cmd_kill_line(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); return buffer_kill_line(buf); } static int cmd_clear_buffer(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); return buffer_clear(buf); } static int cmd_set_mark(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); buffer_set_mark(buf, buf->point); return ECEX_OK; } static int cmd_clear_mark(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); buffer_clear_mark(buf); return ECEX_OK; } static int cmd_yank(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); const char *text = ecex_clipboard_get(ed); if (!text || !text[0]) return ECEX_OK; return buffer_replace_selection(buf, text); } static int cmd_copy_region(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); if (!buffer_has_selection(buf)) return ECEX_OK; size_t start = 0; size_t end = 0; buffer_selection_range(buf, &start, &end); char *text = buffer_substring(buf, start, end); if (!text) return ECEX_ERR; int result = ecex_clipboard_set(ed, text); free(text); return result; } static int cmd_kill_region(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); if (!buffer_has_selection(buf)) return ECEX_OK; size_t start = 0; size_t end = 0; buffer_selection_range(buf, &start, &end); 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; 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); } 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 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; 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 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 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; buffer_t *preview = ecex_file_preview_buffer(ed); if (!preview) return ECEX_ERR; int result = ecex_file_browser_fill_preview(ed, preview, path); ecex_file_browser_show_preview_pane(ed, preview); return result; } 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_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_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_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_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_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_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); ECEX_COMMAND("eval-line", cmd_eval_line); ECEX_COMMAND("eval-region", cmd_eval_region); ECEX_COMMAND("eval-selection", cmd_eval_region); ECEX_COMMAND("eval-file", cmd_eval_file); ECEX_COMMAND("eval-rerun-last", cmd_eval_rerun_last); ECEX_COMMAND("revert-eval-buffer", cmd_eval_rerun_last); ECEX_COMMAND("quit-window", cmd_quit_window); ECEX_COMMAND("reload-config", cmd_reload_config); ECEX_COMMAND("list-commands", cmd_list_commands); 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); ECEX_COMMAND("regrep", cmd_regrep); ECEX_COMMAND("next-error", cmd_next_error); ECEX_COMMAND("previous-error", cmd_previous_error); ECEX_COMMAND("comment-region", cmd_comment_region); ECEX_COMMAND("uncomment-region", cmd_uncomment_region); ECEX_COMMAND("toggle-line-numbers", cmd_toggle_line_numbers); ECEX_COMMAND("toggle-current-line", cmd_toggle_current_line); ECEX_COMMAND("isearch-forward", cmd_isearch_forward); ECEX_COMMAND("isearch-backward", cmd_isearch_backward); ECEX_COMMAND("next-buffer", cmd_next_buffer); ECEX_COMMAND("previous-buffer", cmd_previous_buffer); ECEX_COMMAND("split-window-right", cmd_split_window_right); ECEX_COMMAND("split-window-vertically", cmd_split_window_right); ECEX_COMMAND("split-window-below", cmd_split_window_below); ECEX_COMMAND("split-window-horizontally", cmd_split_window_below); ECEX_COMMAND("other-window", cmd_other_window); ECEX_COMMAND("previous-window", cmd_previous_window); ECEX_COMMAND("delete-window", cmd_delete_window); ECEX_COMMAND("delete-other-windows", cmd_delete_other_windows); ECEX_COMMAND("balance-windows", cmd_balance_windows); ECEX_COMMAND("move-left", cmd_move_left); ECEX_COMMAND("move-right", cmd_move_right); ECEX_COMMAND("move-up", cmd_move_up); ECEX_COMMAND("move-down", cmd_move_down); ECEX_COMMAND("move-word-left", cmd_move_word_left); ECEX_COMMAND("move-word-right", cmd_move_word_right); ECEX_COMMAND("beginning-of-line", cmd_beginning_of_line); ECEX_COMMAND("end-of-line", cmd_end_of_line); ECEX_COMMAND("beginning-of-buffer", cmd_beginning_of_buffer); ECEX_COMMAND("end-of-buffer", cmd_end_of_buffer); ECEX_COMMAND("undo", cmd_undo); ECEX_COMMAND("redo", cmd_redo); ECEX_COMMAND("backspace", cmd_backspace); ECEX_COMMAND("delete-forward", cmd_delete_forward); ECEX_COMMAND("newline", cmd_newline); ECEX_COMMAND("kill-line", cmd_kill_line); ECEX_COMMAND("clear-buffer", cmd_clear_buffer); ECEX_COMMAND("set-mark", cmd_set_mark); ECEX_COMMAND("clear-mark", cmd_clear_mark); ECEX_COMMAND("yank", cmd_yank); ECEX_COMMAND("paste", cmd_yank); ECEX_COMMAND("copy-region", cmd_copy_region); ECEX_COMMAND("copy-region-as-kill", cmd_copy_region); ECEX_COMMAND("kill-region", cmd_kill_region); ECEX_BIND("LEFT", "move-left"); ECEX_BIND("RIGHT", "move-right"); ECEX_BIND("UP", "move-up"); ECEX_BIND("DOWN", "move-down"); ECEX_BIND("C-b", "move-left"); ECEX_BIND("C-f", "move-right"); ECEX_BIND("C-p", "move-up"); ECEX_BIND("C-n", "move-down"); ECEX_BIND("C-a", "beginning-of-line"); ECEX_BIND("C-e", "end-of-line"); ECEX_BIND("C-k", "kill-line"); ECEX_BIND("C-SPC", "set-mark"); ECEX_BIND("C-y", "yank"); ECEX_BIND("C-v", "paste"); ECEX_BIND("M-w", "copy-region-as-kill"); ECEX_BIND("C-w", "kill-region"); ECEX_BIND("C-s", "isearch-forward"); ECEX_BIND("C-r", "isearch-backward"); ECEX_BIND("C-/", "undo"); ECEX_BIND("C-z", "undo"); 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"); ECEX_BIND("C-x O", "previous-window"); ECEX_BIND("C-x 0", "delete-window"); ECEX_BIND("C-x 1", "delete-other-windows"); ECEX_BIND("C-x +", "balance-windows"); ECEX_BIND("C-x e b", "eval-buffer"); ECEX_BIND("C-x e l", "eval-line"); ECEX_BIND("C-x C-e", "eval-line"); ECEX_BIND("C-x e r", "eval-region"); ECEX_BIND("C-x e f", "eval-file"); ECEX_BIND("C-x C-r", "reload-config"); ECEX_BIND("F5", "reload-config"); ECEX_BIND("M-g n", "next-error"); ECEX_BIND("M-g p", "previous-error"); ECEX_BIND("M-;", "comment-region"); ECEX_BIND("BACKSPACE", "backspace"); ECEX_BIND("DELETE", "delete-forward"); ECEX_BIND("ENTER", "newline"); ecex_define_major_mode(ed, "fundamental-mode"); 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"); return ECEX_OK; } #undef ECEX_COMMAND #undef ECEX_BIND #undef CURRENT_BUFFER_OR_ERR ecex_t *ecex_new(void) { ecex_t *ed = calloc(1, sizeof(*ed)); if (!ed) return NULL; ecex_theme_set_defaults(ed); buffer_t *scratch = buffer_new("*scratch*", NULL, 0); if (!scratch || ecex_add_buffer(ed, scratch) != ECEX_OK) { buffer_free(scratch); ecex_free(ed); return NULL; } ed->current_buffer_index = 0; ed->current_buffer = scratch; ed->next_major_mode_id = 1; ed->window_cap = ECEX_INITIAL_WINDOW_CAP; ed->windows = calloc(ed->window_cap, sizeof(*ed->windows)); if (!ed->windows) { ecex_free(ed); return NULL; } ed->window_count = 1; ed->current_window_index = 0; ed->windows[0].buffer = scratch; ed->windows[0].x = 0.0f; ed->windows[0].y = 0.0f; ed->windows[0].w = 1.0f; ed->windows[0].h = 1.0f; if (ecex_register_builtins(ed) != ECEX_OK) { ecex_free(ed); return NULL; } ecex_buffer_set_major_mode_by_name(ed, scratch, "fundamental-mode"); return ed; } void ecex_free(ecex_t *ed) { if (!ed) return; /* 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); } for (size_t i = 0; i < ed->keybind_count; i++) { free(ed->keybinds[i].key); free(ed->keybinds[i].command); } for (size_t i = 0; i < ed->mode_keybind_count; i++) { free(ed->mode_keybinds[i].key); free(ed->mode_keybinds[i].command); } for (size_t i = 0; i < ed->major_mode_count; i++) { 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); free(ed->commands); 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); free(ed->last_grep_command); free(ed->config_path); free(ed->theme.font_path); free(ed); } int ecex_reserve_buffers(ecex_t *ed, size_t needed) { ECEX_RETURN_ERR_IF_NULL(ed); if (needed <= ed->buffer_cap) return ECEX_OK; while (ed->buffer_cap < needed) { if (ECEX_GROW_ARRAY(ed->buffers, ed->buffer_count, ed->buffer_cap, ECEX_INITIAL_BUFFER_CAP) != ECEX_OK) { return ECEX_ERR; } } return ECEX_OK; } int ecex_add_buffer(ecex_t *ed, buffer_t *buffer) { if (!ed || !buffer) return ECEX_ERR; if (ecex_reserve_buffers(ed, ed->buffer_count + 1) != ECEX_OK) return ECEX_ERR; ed->buffers[ed->buffer_count++] = buffer; if (!ed->current_buffer) { ed->current_buffer_index = 0; ed->current_buffer = buffer; } return ECEX_OK; } buffer_t *ecex_create_buffer(ecex_t *ed, const char *name, const char *path, int read_only) { if (!ed || !name) return NULL; buffer_t *buffer = buffer_new(name, path, read_only); if (!buffer) return NULL; if (ecex_add_buffer(ed, buffer) != ECEX_OK) { buffer_free(buffer); return NULL; } return buffer; } buffer_t *ecex_find_buffer(ecex_t *ed, const char *name) { if (!ed || !name) return NULL; for (size_t i = 0; i < ed->buffer_count; i++) { if (strcmp(ed->buffers[i]->name, name) == 0) { return ed->buffers[i]; } } return NULL; } 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 = next; ecex_window_t *win = ecex_current_window(ed); if (win) win->buffer = ed->current_buffer; return ECEX_OK; } int ecex_sync_current_buffer(ecex_t *ed) { if (!ed) return ECEX_ERR; ecex_window_t *win = ecex_current_window(ed); if (!win || !win->buffer) return ECEX_ERR; 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] == next) { ed->current_buffer_index = i; return ECEX_OK; } } return ECEX_ERR; } int ecex_switch_buffer(ecex_t *ed, const char *name) { 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) { return ecex_set_current_buffer_index(ed, i); } } return ECEX_ERR; } buffer_t *ecex_current_buffer(ecex_t *ed) { if (!ed) return NULL; ecex_window_t *win = ecex_current_window(ed); if (win && win->buffer) return win->buffer; 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]; } size_t ecex_window_count(ecex_t *ed) { return ed ? ed->window_count : 0; } static int ecex_reserve_windows(ecex_t *ed, size_t needed) { if (!ed) return ECEX_ERR; if (needed <= ed->window_cap) return ECEX_OK; while (ed->window_cap < needed) { if (ECEX_GROW_ARRAY(ed->windows, ed->window_count, ed->window_cap, ECEX_INITIAL_WINDOW_CAP) != ECEX_OK) { return ECEX_ERR; } } return ECEX_OK; } static int ecex_add_window(ecex_t *ed, ecex_window_t win) { if (!ed || !win.buffer) return ECEX_ERR; if (ecex_reserve_windows(ed, ed->window_count + 1) != ECEX_OK) return ECEX_ERR; ed->windows[ed->window_count++] = win; return ECEX_OK; } int ecex_split_window_vertically(ecex_t *ed) { ecex_window_t *active = ecex_current_window(ed); if (!active || !active->buffer || active->w < 0.08f) return ECEX_ERR; ecex_window_t new_win = *active; float half = active->w * 0.5f; active->w = half; new_win.x = active->x + half; new_win.w = half; if (ecex_add_window(ed, new_win) != ECEX_OK) return ECEX_ERR; ed->current_window_index = ed->window_count - 1; return ecex_sync_current_buffer(ed); } int ecex_split_window_horizontally(ecex_t *ed) { ecex_window_t *active = ecex_current_window(ed); if (!active || !active->buffer || active->h < 0.08f) return ECEX_ERR; ecex_window_t new_win = *active; float half = active->h * 0.5f; active->h = half; new_win.y = active->y + half; new_win.h = half; if (ecex_add_window(ed, new_win) != ECEX_OK) return ECEX_ERR; ed->current_window_index = ed->window_count - 1; return ecex_sync_current_buffer(ed); } int ecex_other_window(ecex_t *ed) { if (!ed || ed->window_count == 0) return ECEX_ERR; ed->current_window_index = (ed->current_window_index + 1) % ed->window_count; return ecex_sync_current_buffer(ed); } int ecex_previous_window(ecex_t *ed) { if (!ed || ed->window_count == 0) return ECEX_ERR; if (ed->current_window_index == 0) ed->current_window_index = ed->window_count - 1; else ed->current_window_index--; return ecex_sync_current_buffer(ed); } int ecex_delete_window(ecex_t *ed) { if (!ed || ed->window_count <= 1 || ed->current_window_index >= ed->window_count) return ECEX_ERR; size_t index = ed->current_window_index; for (size_t i = index; i + 1 < ed->window_count; i++) { ed->windows[i] = ed->windows[i + 1]; } ed->window_count--; if (ed->current_window_index >= ed->window_count) ed->current_window_index = ed->window_count - 1; return ecex_sync_current_buffer(ed); } int ecex_delete_other_windows(ecex_t *ed) { ecex_window_t *active = ecex_current_window(ed); if (!ed || !active) return ECEX_ERR; ecex_window_t keep = *active; keep.x = 0.0f; keep.y = 0.0f; keep.w = 1.0f; keep.h = 1.0f; ed->windows[0] = keep; ed->window_count = 1; ed->current_window_index = 0; return ecex_sync_current_buffer(ed); } int ecex_balance_windows(ecex_t *ed) { if (!ed || ed->window_count == 0) return ECEX_ERR; size_t n = ed->window_count; size_t cols = 1; while (cols * cols < n) cols++; size_t rows = (n + cols - 1) / cols; for (size_t i = 0; i < n; i++) { size_t row = i / cols; size_t col = i % cols; ed->windows[i].x = (float)col / (float)cols; ed->windows[i].y = (float)row / (float)rows; ed->windows[i].w = 1.0f / (float)cols; ed->windows[i].h = 1.0f / (float)rows; } return ecex_sync_current_buffer(ed); } int ecex_next_buffer(ecex_t *ed) { if (!ed || ed->buffer_count == 0) return ECEX_ERR; ecex_sync_current_buffer(ed); return ecex_set_current_buffer_index(ed, (ed->current_buffer_index + 1) % ed->buffer_count); } int ecex_previous_buffer(ecex_t *ed) { if (!ed || ed->buffer_count == 0) return ECEX_ERR; ecex_sync_current_buffer(ed); if (ed->current_buffer_index == 0) { ed->current_buffer_index = ed->buffer_count - 1; } else { ed->current_buffer_index--; } return ecex_set_current_buffer_index(ed, ed->current_buffer_index); } 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; 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]; 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 = 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; } buffer_free(victim); if (ed->buffer_count == 0) { ed->current_buffer = NULL; ed->current_buffer_index = 0; ed->window_count = 0; ed->current_window_index = 0; return ECEX_OK; } if (ed->current_window_index >= ed->window_count) { ed->current_window_index = ed->window_count ? ed->window_count - 1 : 0; } 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; if (ECEX_GROW_ARRAY(ed->jit_modules, ed->jit_module_count, ed->jit_module_cap, 8) != ECEX_OK) { return ECEX_ERR; } ed->jit_modules[ed->jit_module_count++] = module; return ECEX_OK; } int ecex_set_config_path(ecex_t *ed, const char *path) { if (!ed) return ECEX_ERR; char *copy = NULL; if (path && path[0]) { copy = ecex_strdup(path); if (!copy) return ECEX_ERR; } free(ed->config_path); ed->config_path = copy; return ECEX_OK; } const char *ecex_config_path(ecex_t *ed) { if (!ed) return NULL; return ed->config_path; } static void ecex_clear_commands(ecex_t *ed) { if (!ed) return; for (size_t i = 0; i < ed->command_count; i++) { free(ed->commands[i].name); } ed->command_count = 0; } static void ecex_clear_keybinds(ecex_t *ed) { if (!ed) return; for (size_t i = 0; i < ed->keybind_count; i++) { free(ed->keybinds[i].key); free(ed->keybinds[i].command); } ed->keybind_count = 0; } static void ecex_clear_mode_keybinds(ecex_t *ed) { if (!ed) return; for (size_t i = 0; i < ed->mode_keybind_count; i++) { free(ed->mode_keybinds[i].key); free(ed->mode_keybinds[i].command); } 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"); return ECEX_ERR; } 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); 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); } 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; } } if (ECEX_GROW_ARRAY(ed->commands, ed->command_count, ed->command_cap, ECEX_INITIAL_COMMAND_CAP) != ECEX_OK) { return ECEX_ERR; } char *copy = ecex_strdup(name); if (!copy) return ECEX_ERR; ed->commands[ed->command_count].name = copy; ed->commands[ed->command_count].fn = fn; ed->command_count++; return ECEX_OK; } int ecex_execute_command(ecex_t *ed, const char *name) { if (!ed || !name) return ECEX_ERR; for (size_t i = 0; i < ed->command_count; i++) { if (strcmp(ed->commands[i].name, name) == 0) { return ed->commands[i].fn(ed); } } return ECEX_ERR; } void ecex_set_clipboard_callbacks(ecex_t *ed, ecex_clipboard_get_fn get_fn, ecex_clipboard_set_fn set_fn, void *userdata) { if (!ed) return; ed->clipboard_get = get_fn; ed->clipboard_set = set_fn; ed->clipboard_userdata = userdata; } const char *ecex_clipboard_get(ecex_t *ed) { if (!ed || !ed->clipboard_get) return NULL; return ed->clipboard_get(ed->clipboard_userdata); } int ecex_clipboard_set(ecex_t *ed, const char *text) { if (!ed || !ed->clipboard_set || !text) return ECEX_ERR; ed->clipboard_set(ed->clipboard_userdata, 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; free(ed->keybinds[i].command); ed->keybinds[i].command = new_command; return ECEX_OK; } } if (ECEX_GROW_ARRAY(ed->keybinds, ed->keybind_count, ed->keybind_cap, ECEX_INITIAL_KEYBIND_CAP) != ECEX_OK) { return ECEX_ERR; } char *key_copy = ecex_strdup(key); char *command_copy = ecex_strdup(command); if (!key_copy || !command_copy) { free(key_copy); free(command_copy); return ECEX_ERR; } ed->keybinds[ed->keybind_count].key = key_copy; ed->keybinds[ed->keybind_count].command = command_copy; ed->keybind_count++; return ECEX_OK; } const char *ecex_lookup_key(ecex_t *ed, const char *key) { if (!ed || !key) return NULL; for (size_t i = 0; i < ed->keybind_count; i++) { if (strcmp(ed->keybinds[i].key, key) == 0) { return ed->keybinds[i].command; } } return NULL; } int ecex_define_major_mode(ecex_t *ed, const char *name) { if (!ed || !name || !name[0]) return 0; for (size_t i = 0; i < ed->major_mode_count; i++) { if (strcmp(ed->major_modes[i].name, name) == 0) { return ed->major_modes[i].id; } } if (ECEX_GROW_ARRAY(ed->major_modes, ed->major_mode_count, ed->major_mode_cap, ECEX_INITIAL_MODE_CAP) != ECEX_OK) { return 0; } char *copy = ecex_strdup(name); if (!copy) return 0; int id = ed->next_major_mode_id++; ed->major_modes[ed->major_mode_count].id = id; ed->major_modes[ed->major_mode_count].name = copy; ed->major_mode_count++; return id; } int ecex_major_mode_by_name(ecex_t *ed, const char *name) { if (!ed || !name) return 0; for (size_t i = 0; i < ed->major_mode_count; i++) { if (strcmp(ed->major_modes[i].name, name) == 0) return ed->major_modes[i].id; } return 0; } const char *ecex_major_mode_name(ecex_t *ed, int mode) { if (!ed || mode == 0) return "fundamental-mode"; for (size_t i = 0; i < ed->major_mode_count; i++) { if (ed->major_modes[i].id == mode) return ed->major_modes[i].name; } return "unknown-mode"; } int ecex_buffer_set_major_mode(buffer_t *buffer, int mode) { if (!buffer) return ECEX_ERR; buffer->major_mode = mode; return ECEX_OK; } int ecex_buffer_set_major_mode_by_name(ecex_t *ed, buffer_t *buffer, const char *name) { if (!ed || !buffer || !name) return ECEX_ERR; int mode = ecex_define_major_mode(ed, name); if (!mode) return ECEX_ERR; return ecex_buffer_set_major_mode(buffer, mode); } const char *ecex_buffer_major_mode_name(ecex_t *ed, buffer_t *buffer) { if (!buffer) return "fundamental-mode"; return ecex_major_mode_name(ed, buffer->major_mode); } int ecex_bind_mode_key(ecex_t *ed, const char *mode_name, const char *key, const char *command) { if (!ed || !mode_name || !key || !command) return ECEX_ERR; 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); ed->mode_keybinds[i].command = new_command; return ECEX_OK; } } if (ECEX_GROW_ARRAY(ed->mode_keybinds, ed->mode_keybind_count, ed->mode_keybind_cap, ECEX_INITIAL_MODE_KEYBIND_CAP) != ECEX_OK) { return ECEX_ERR; } char *key_copy = ecex_strdup(key); char *command_copy = ecex_strdup(command); if (!key_copy || !command_copy) { free(key_copy); free(command_copy); return ECEX_ERR; } ed->mode_keybinds[ed->mode_keybind_count].mode = mode; ed->mode_keybinds[ed->mode_keybind_count].key = key_copy; ed->mode_keybinds[ed->mode_keybind_count].command = command_copy; ed->mode_keybind_count++; 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; int mode = buffer ? buffer->major_mode : 0; if (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) { return ed->mode_keybinds[i].command; } } } return ecex_lookup_key(ed, key); } int ecex_key_sequence_has_prefix_for_buffer(ecex_t *ed, buffer_t *buffer, const char *prefix) { if (!ed || !prefix) return 0; size_t prefix_len = strlen(prefix); int mode = buffer ? buffer->major_mode : 0; if (mode) { for (size_t i = 0; i < ed->mode_keybind_count; i++) { const char *key = ed->mode_keybinds[i].key; if (ed->mode_keybinds[i].mode == mode && strncmp(key, prefix, prefix_len) == 0 && key[prefix_len] == ' ') { return 1; } } } for (size_t i = 0; i < ed->keybind_count; i++) { const char *key = ed->keybinds[i].key; if (strncmp(key, prefix, prefix_len) == 0 && key[prefix_len] == ' ') return 1; } return 0; } int ecex_auto_set_major_mode(ecex_t *ed, buffer_t *buffer) { if (!ed || !buffer) return ECEX_ERR; const char *name = "fundamental-mode"; const char *path = buffer->path ? buffer->path : buffer->name; const char *dot = path ? strrchr(path, '.') : NULL; if (dot && (strcmp(dot, ".c") == 0 || strcmp(dot, ".h") == 0 || strcmp(dot, ".cc") == 0 || strcmp(dot, ".cpp") == 0 || strcmp(dot, ".hpp") == 0)) { name = "c-mode"; } if (buffer_is_interactive(buffer) && buffer->name && buffer->name[0] == '*') { name = "special-mode"; } return ecex_buffer_set_major_mode_by_name(ed, buffer, name); } int ecex_list_commands(ecex_t *ed) { if (!ed) return ECEX_ERR; buffer_t *buf = ecex_find_buffer(ed, "*commands*"); if (!buf) { buf = ecex_create_buffer(ed, "*commands*", NULL, 0); if (!buf) return ECEX_ERR; } if (buffer_clear(buf) != ECEX_OK) return ECEX_ERR; buffer_append(buf, "Commands:\n\n"); for (size_t i = 0; i < ed->command_count; i++) { const char *command_name = ed->commands[i].name; int first_key = 1; buffer_append(buf, " "); buffer_append(buf, command_name); for (size_t j = 0; j < ed->keybind_count; j++) { if (strcmp(ed->keybinds[j].command, command_name) == 0) { buffer_append(buf, first_key ? " [" : ", "); buffer_append(buf, ed->keybinds[j].key); first_key = 0; } } for (size_t j = 0; j < ed->mode_keybind_count; j++) { if (strcmp(ed->mode_keybinds[j].command, command_name) == 0) { buffer_append(buf, first_key ? " [" : ", "); buffer_append(buf, ecex_major_mode_name(ed, ed->mode_keybinds[j].mode)); buffer_append(buf, ":"); buffer_append(buf, ed->mode_keybinds[j].key); first_key = 0; } } if (!first_key) buffer_append(buf, "]"); buffer_append(buf, "\n"); } buf->point = 0; buf->modified = 0; return ecex_switch_buffer(ed, "*commands*"); } static int ecex_interactive_switch_buffer_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; return ecex_switch_buffer(ed, payload); } int ecex_list_buffers(ecex_t *ed) { if (!ed) return ECEX_ERR; buffer_t *buf = ecex_find_buffer(ed, "*buffers*"); if (!buf) { buf = ecex_create_interactive_buffer(ed, "*buffers*"); if (!buf) return ECEX_ERR; } buffer_set_interactive(buf, 1); if (buffer_clear(buf) != ECEX_OK) return ECEX_ERR; buffer_set_interactive(buf, 1); buffer_append(buf, "Buffers:\n"); buffer_append(buf, "Press ENTER on a buffer to switch to it.\n\n"); for (size_t i = 0; i < ed->buffer_count; i++) { buffer_t *b = ed->buffers[i]; char line[1024]; snprintf(line, sizeof(line), "%s %s %-24s %-18s size:%zu%s%s", b == ed->current_buffer ? "*" : " ", b->modified ? "+" : " ", b->name ? b->name : "(unnamed)", ecex_buffer_major_mode_name(ed, b), b->len, b->path ? " " : "", b->path ? b->path : ""); if (ecex_interactive_append_line(ed, buf, line, ecex_interactive_switch_buffer_action, b->name, NULL) != ECEX_OK) { return ECEX_ERR; } } buf->point = 0; buf->modified = 0; return ecex_switch_buffer(ed, "*buffers*"); } buffer_t *ecex_create_interactive_buffer(ecex_t *ed, const char *name) { if (!ed || !name) return NULL; buffer_t *buffer = ecex_find_buffer(ed, name); if (!buffer) { buffer = ecex_create_buffer(ed, name, NULL, 0); if (!buffer) return NULL; } buffer_set_interactive(buffer, 1); if (buffer->major_mode == 0) ecex_buffer_set_major_mode_by_name(ed, buffer, "special-mode"); return buffer; } int ecex_interactive_append_line(ecex_t *ed, buffer_t *buffer, const char *text, ecex_interactive_line_fn fn, const char *payload, void *userdata) { (void)ed; if (!buffer || !text) return ECEX_ERR; size_t line = buffer_line_count(buffer); if (line > 0) line--; if (fn) { if (buffer_add_interactive_action(buffer, line, fn, payload, userdata) != ECEX_OK) { return ECEX_ERR; } } if (buffer_append(buffer, text) != ECEX_OK) return ECEX_ERR; if (buffer_append(buffer, "\n") != ECEX_OK) return ECEX_ERR; buffer_set_interactive(buffer, 1); return ECEX_OK; } int ecex_interactive_activate_current_line(ecex_t *ed) { if (!ed) return ECEX_ERR; buffer_t *buffer = ecex_current_buffer(ed); if (!buffer || !buffer_is_interactive(buffer)) return ECEX_ERR; size_t line = buffer_current_line_number(buffer); if (line > 0) line--; ecex_interactive_line_action_t *action = buffer_interactive_action_at_line(buffer, line); if (!action || !action->fn) return ECEX_ERR; return action->fn(ed, buffer, line, action->payload, action->userdata); } static int ecex_buffer_path_equal(buffer_t *buffer, const char *path) { return buffer && buffer->path && path && strcmp(buffer->path, path) == 0; } 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], 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; } } 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); free(name); if (!buf) { free(normal_path); return ECEX_ERR; } 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 { free(buf->path); buf->path = normal_path; normal_path = NULL; buf->modified = 0; } free(normal_path); ecex_auto_set_major_mode(ed, buf); 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) { buffer_t *buf = ecex_current_buffer(ed); if (!buf) return ECEX_ERR; return buffer_save(buf); } 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; 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; } void ecex_request_prompt(ecex_t *ed, ecex_prompt_request_t request, const char *message) { if (!ed) return; ed->prompt_request = request; snprintf(ed->prompt_message, sizeof(ed->prompt_message), "%s", message ? message : ""); } void ecex_clear_prompt_request(ecex_t *ed) { if (!ed) return; ed->prompt_request = ECEX_PROMPT_NONE; ed->prompt_message[0] = '\0'; } static int ecex_fuzzy_score(const char *candidate, const char *query) { if (!candidate || !query) return -1; if (query[0] == '\0') return 0; int score = 0; int consecutive = 0; int last_match = -1; size_t ci = 0; size_t qi = 0; while (candidate[ci] && query[qi]) { char c = candidate[ci]; char q = query[qi]; if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a'); if (q >= 'A' && q <= 'Z') q = (char)(q - 'A' + 'a'); if (c == q) { score += 10; if ((int)ci == last_match + 1) { consecutive++; score += 5 * consecutive; } else { consecutive = 0; } if (ci == 0) score += 20; if (ci > 0 && (candidate[ci - 1] == '-' || candidate[ci - 1] == '_' || candidate[ci - 1] == ' ')) { score += 15; } last_match = (int)ci; qi++; } ci++; } if (query[qi] != '\0') return -1; score -= (int)strlen(candidate); if (strncmp(candidate, query, strlen(query)) == 0) score += 100; return score; } const char *ecex_complete_command(ecex_t *ed, const char *query) { if (!ed || !query || !ed->theme.completion_enabled) return NULL; const char *best = NULL; int best_score = -1; for (size_t i = 0; i < ed->command_count; i++) { const char *name = ed->commands[i].name; int score = ecex_fuzzy_score(name, query); if (score > best_score) { best_score = score; best = name; } } return best; } static int ecex_set_owned_string(char **slot, const char *value) { if (!slot) return ECEX_ERR; char *copy = NULL; if (value && value[0]) { copy = ecex_strdup(value); if (!copy) return ECEX_ERR; } free(*slot); *slot = copy; return ECEX_OK; } static int ecex_parse_file_line(const char *text, char *path, size_t path_size, size_t *out_line) { if (!text || !path || path_size == 0 || !out_line) return ECEX_ERR; const char *colon = strchr(text, ':'); if (!colon || colon == text) return ECEX_ERR; char *end = NULL; long line = strtol(colon + 1, &end, 10); if (line <= 0 || end == colon + 1) return ECEX_ERR; size_t len = (size_t)(colon - text); if (len >= path_size) len = path_size - 1; memcpy(path, text, len); path[len] = '\0'; *out_line = (size_t)line; return ECEX_OK; } static int ecex_goto_file_line_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 path[1024]; size_t target_line = 1; if (ecex_parse_file_line(payload, path, sizeof(path), &target_line) != ECEX_OK) return ECEX_ERR; if (ecex_find_file(ed, path) != ECEX_OK) return ECEX_ERR; buffer_t *buf = ecex_current_buffer(ed); if (!buf) return ECEX_ERR; size_t pos = 0; for (size_t l = 1; l < target_line && pos < buf->len; pos++) { if (buf->data[pos] == '\n') l++; } buffer_set_point(buf, pos); return ECEX_OK; } static int ecex_append_shell_output(ecex_t *ed, const char *name, const char *command) { if (!ed || !name || !command || !command[0]) return ECEX_ERR; buffer_t *buf = ecex_create_interactive_buffer(ed, name); if (!buf) return ECEX_ERR; buffer_set_interactive(buf, 1); if (buffer_clear(buf) != ECEX_OK) return ECEX_ERR; buffer_set_interactive(buf, 1); ecex_buffer_set_major_mode_by_name(ed, buf, "special-mode"); char header[2048]; snprintf(header, sizeof(header), "%s\n\nKeys: g/r rerun, n/p next/previous result, RET jump, q quit.\n\n", command); buffer_append(buf, header); char shell_command[4096]; snprintf(shell_command, sizeof(shell_command), "%s 2>&1", command); FILE *pipe = popen(shell_command, "r"); if (!pipe) { buffer_append(buf, "Failed to start command.\n"); return ecex_switch_buffer(ed, name); } char line[4096]; while (fgets(line, sizeof(line), pipe)) { size_t len = strlen(line); while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) line[--len] = '\0'; char path[1024]; size_t target = 0; ecex_interactive_line_fn fn = NULL; char payload[1200]; payload[0] = '\0'; if (ecex_parse_file_line(line, path, sizeof(path), &target) == ECEX_OK) { snprintf(payload, sizeof(payload), "%s:%zu", path, target); fn = ecex_goto_file_line_action; } ecex_interactive_append_line(ed, buf, line, fn, payload[0] ? payload : NULL, NULL); } int status = pclose(pipe); char footer[128]; snprintf(footer, sizeof(footer), "\n[process exited: %d]\n", status); buffer_append(buf, footer); buf->point = 0; buf->modified = 0; return ecex_switch_buffer(ed, name); } int ecex_compile(ecex_t *ed, const char *command) { if (!ed || !command || !command[0]) return ECEX_ERR; if (ecex_set_owned_string(&ed->last_compile_command, command) != ECEX_OK) return ECEX_ERR; return ecex_append_shell_output(ed, "*compilation*", command); } int ecex_grep(ecex_t *ed, const char *command) { if (!ed || !command || !command[0]) return ECEX_ERR; if (ecex_set_owned_string(&ed->last_grep_command, command) != ECEX_OK) return ECEX_ERR; return ecex_append_shell_output(ed, "*grep*", command); } int ecex_rerun_compile(ecex_t *ed) { if (!ed || !ed->last_compile_command) return ECEX_ERR; return ecex_append_shell_output(ed, "*compilation*", ed->last_compile_command); } int ecex_rerun_grep(ecex_t *ed) { if (!ed || !ed->last_grep_command) return ECEX_ERR; return ecex_append_shell_output(ed, "*grep*", ed->last_grep_command); } int ecex_next_interactive_action(ecex_t *ed) { buffer_t *buf = ecex_current_buffer(ed); if (!buf || !buffer_is_interactive(buf) || buf->interactive_action_count == 0) return ECEX_ERR; size_t current = buffer_current_line_number(buf); if (current > 0) current--; size_t best = (size_t)-1; for (size_t i = 0; i < buf->interactive_action_count; i++) { size_t line = buf->interactive_actions[i].line; if (line > current && (best == (size_t)-1 || line < best)) best = line; } if (best == (size_t)-1) best = buf->interactive_actions[0].line; size_t pos = 0; for (size_t l = 0; l < best && pos < buf->len; pos++) if (buf->data[pos] == '\n') l++; buffer_set_point(buf, pos); return ECEX_OK; } int ecex_previous_interactive_action(ecex_t *ed) { buffer_t *buf = ecex_current_buffer(ed); if (!buf || !buffer_is_interactive(buf) || buf->interactive_action_count == 0) return ECEX_ERR; size_t current = buffer_current_line_number(buf); if (current > 0) current--; size_t best = (size_t)-1; for (size_t i = 0; i < buf->interactive_action_count; i++) { size_t line = buf->interactive_actions[i].line; if (line < current && (best == (size_t)-1 || line > best)) best = line; } if (best == (size_t)-1) best = buf->interactive_actions[buf->interactive_action_count - 1].line; size_t pos = 0; for (size_t l = 0; l < best && pos < buf->len; pos++) if (buf->data[pos] == '\n') l++; buffer_set_point(buf, pos); return ECEX_OK; } static int ecex_region_lines(buffer_t *buf, size_t *out_start, size_t *out_end) { if (!buf || !buffer_has_selection(buf)) return ECEX_ERR; size_t start = 0, end = 0; buffer_selection_range(buf, &start, &end); start = buffer_line_start_at(buf, start); end = buffer_line_end_at(buf, end); if (end < buf->len) end++; if (out_start) *out_start = start; if (out_end) *out_end = end; return ECEX_OK; } int ecex_comment_region(ecex_t *ed) { buffer_t *buf = ecex_current_buffer(ed); size_t start = 0, end = 0; if (!buf || ecex_region_lines(buf, &start, &end) != ECEX_OK) return ECEX_ERR; size_t pos = start; while (pos <= end && pos <= buf->len) { if (buffer_insert_at(buf, pos, "// ") != ECEX_OK) return ECEX_ERR; pos += 3; end += 3; size_t eol = buffer_line_end_at(buf, pos); if (eol >= end || eol >= buf->len) break; pos = eol + 1; } return ECEX_OK; } int ecex_uncomment_region(ecex_t *ed) { buffer_t *buf = ecex_current_buffer(ed); size_t start = 0, end = 0; if (!buf || ecex_region_lines(buf, &start, &end) != ECEX_OK) return ECEX_ERR; size_t pos = start; while (pos < end && pos < buf->len) { if (pos + 2 <= buf->len && memcmp(buf->data + pos, "//", 2) == 0) { size_t del = (pos + 3 <= buf->len && buf->data[pos + 2] == ' ') ? 3 : 2; if (buffer_delete_range(buf, pos, pos + del) != ECEX_OK) return ECEX_ERR; end = end > del ? end - del : 0; } size_t eol = buffer_line_end_at(buf, pos); if (eol >= end || eol >= buf->len) break; pos = eol + 1; } return ECEX_OK; } int ecex_set_font(ecex_t *ed, const char *path) { if (!ed || !path) return ECEX_ERR; char *copy = ecex_strdup(path); if (!copy) return ECEX_ERR; free(ed->theme.font_path); ed->theme.font_path = copy; return ECEX_OK; } float ecex_get_font_size(ecex_t *ed) { if (!ed) return 0.0f; return ed->theme.font_size; } int ecex_set_font_size(ecex_t *ed, float size) { if (!ed) return ECEX_ERR; ed->theme.font_size = ECEX_CLAMP(size, 8.0f, 96.0f); return ECEX_OK; } int ecex_adjust_font_size(ecex_t *ed, float delta) { if (!ed) return ECEX_ERR; return ecex_set_font_size(ed, ed->theme.font_size + delta); } void ecex_set_bg_color(ecex_t *ed, float r, float g, float b) { if (ed) ed->theme.bg = ecex_color(r, g, b); } void ecex_set_fg_color(ecex_t *ed, float r, float g, float b) { if (ed) ed->theme.fg = ecex_color(r, g, b); } void ecex_set_status_bg_color(ecex_t *ed, float r, float g, float b) { if (ed) ed->theme.status_bg = ecex_color(r, g, b); } void ecex_set_status_fg_color(ecex_t *ed, float r, float g, float b) { if (ed) ed->theme.status_fg = ecex_color(r, g, b); } void ecex_set_status_border_color(ecex_t *ed, float r, float g, float b) { if (ed) ed->theme.status_border = ecex_color(r, g, b); } void ecex_set_cursor_color(ecex_t *ed, float r, float g, float b) { if (ed) ed->theme.cursor = ecex_color(r, g, b); } void ecex_set_region_bg_color(ecex_t *ed, float r, float g, float b) { if (ed) ed->theme.region_bg = ecex_color(r, g, b); } void ecex_set_minibuffer_bg_color(ecex_t *ed, float r, float g, float b) { if (ed) ed->theme.minibuffer_bg = ecex_color(r, g, b); } void ecex_set_minibuffer_fg_color(ecex_t *ed, float r, float g, float b) { if (ed) ed->theme.minibuffer_fg = ecex_color(r, g, b); } void ecex_set_completion_fg_color(ecex_t *ed, float r, float g, float b) { if (ed) ed->theme.completion_fg = ecex_color(r, g, b); } void ecex_set_completion_enabled(ecex_t *ed, int enabled) { if (ed) ed->theme.completion_enabled = enabled ? 1 : 0; } void ecex_set_interactive_highlight_bg_color(ecex_t *ed, float r, float g, float b) { if (ed) ed->theme.interactive_highlight_bg = ecex_color(r, g, b); } void ecex_set_interactive_highlight_fg_color(ecex_t *ed, float r, float g, float b) { if (ed) ed->theme.interactive_highlight_fg = ecex_color(r, g, b); } void ecex_set_current_line_bg_color(ecex_t *ed, float r, float g, float b) { if (ed) ed->theme.current_line_bg = ecex_color(r, g, b); } void ecex_set_search_bg_color(ecex_t *ed, float r, float g, float b) { if (ed) ed->theme.search_bg = ecex_color(r, g, b); } void ecex_set_line_numbers_enabled(ecex_t *ed, int enabled) { if (ed) ed->theme.line_numbers_enabled = enabled ? 1 : 0; } void ecex_set_current_line_enabled(ecex_t *ed, int enabled) { if (ed) ed->theme.current_line_enabled = enabled ? 1 : 0; }