aboutsummaryrefslogtreecommitdiff
path: root/src/ecex.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/ecex.c')
-rw-r--r--src/ecex.c1569
1 files changed, 1517 insertions, 52 deletions
diff --git a/src/ecex.c b/src/ecex.c
index 2f4c962..564d05f 100644
--- a/src/ecex.c
+++ b/src/ecex.c
@@ -1,13 +1,18 @@
#include "ecex.h"
+#include "media.h"
#include "ccdjit.h"
#include "common.h"
#include "config.h"
#include "util.h"
+#include "path.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
+#include <dirent.h>
+#include <sys/stat.h>
+#include <sys/time.h>
extern FILE *popen(const char *command, const char *type);
extern int pclose(FILE *stream);
@@ -21,6 +26,639 @@ extern int pclose(FILE *stream);
ecex_window_t *ecex_current_window(ecex_t *ed);
+
+void *ecex_config_alloc(size_t size) {
+ return malloc(size ? size : 1);
+}
+
+void *ecex_config_calloc(size_t count, size_t size) {
+ if (count == 0) count = 1;
+ if (size == 0) size = 1;
+ return calloc(count, size);
+}
+
+void ecex_config_free(void *ptr) {
+ free(ptr);
+}
+
+double ecex_time_seconds(void) {
+ struct timeval tv;
+ gettimeofday(&tv, NULL);
+ return (double)tv.tv_sec + (double)tv.tv_usec / 1000000.0;
+}
+
+static int ecex_env_enabled(const char *name) {
+ const char *v;
+ if (!name) return 0;
+ v = getenv(name);
+ return v && v[0] && v[0] != '0';
+}
+
+static int ecex_log_enabled(void) {
+ return ecex_env_enabled("ECEX_LOG") ||
+ ecex_env_enabled("ECEX_TRACE_CALLBACKS") ||
+ ecex_env_enabled("ECEX_TRACE_TETRIS");
+}
+
+void ecex_log(const char *message) {
+ if (!ecex_log_enabled()) return;
+ fprintf(stderr, "ecex-log: %s\n", message ? message : "(null)");
+ fflush(stderr);
+}
+
+void ecex_log_int(const char *message, int value) {
+ if (!ecex_log_enabled()) return;
+ fprintf(stderr, "ecex-log: %s%d\n", message ? message : "", value);
+ fflush(stderr);
+}
+
+void ecex_log_double(const char *message, double value) {
+ if (!ecex_log_enabled()) return;
+ fprintf(stderr, "ecex-log: %s%.6f\n", message ? message : "", value);
+ fflush(stderr);
+}
+
+void ecex_log_ptr(const char *message, const void *ptr) {
+ if (!ecex_log_enabled()) return;
+ fprintf(stderr, "ecex-log: %s%p\n", message ? message : "", ptr);
+ fflush(stderr);
+}
+
+void ecex_mem_zero(void *ptr, size_t size) {
+ if (!ptr || size == 0) return;
+ memset(ptr, 0, size);
+}
+
+
+static ecex_object_entry_t *ecex_object_find_entry(ecex_t *ed, void *object) {
+ if (!ed || !object) return NULL;
+ for (size_t i = 0; i < ed->object_count; ++i) {
+ if (ed->objects[i].ptr == object) return &ed->objects[i];
+ }
+ return NULL;
+}
+
+static int ecex_reserve_objects(ecex_t *ed, size_t needed) {
+ ecex_object_entry_t *new_objects;
+ size_t new_cap;
+ if (!ed) return ECEX_ERR;
+ if (needed <= ed->object_cap) return ECEX_OK;
+ new_cap = ed->object_cap ? ed->object_cap * 2 : 16;
+ while (new_cap < needed) new_cap *= 2;
+ new_objects = realloc(ed->objects, new_cap * sizeof(*new_objects));
+ if (!new_objects) return ECEX_ERR;
+ memset(new_objects + ed->object_cap, 0, (new_cap - ed->object_cap) * sizeof(*new_objects));
+ ed->objects = new_objects;
+ ed->object_cap = new_cap;
+ return ECEX_OK;
+}
+
+void *ecex_object_alloc(ecex_t *ed, size_t size) {
+ void *ptr;
+ if (!ed) return NULL;
+ if (size == 0) size = 1;
+ if (ecex_reserve_objects(ed, ed->object_count + 1) != ECEX_OK) return NULL;
+ ptr = malloc(size);
+ if (!ptr) return NULL;
+ ed->objects[ed->object_count].ptr = ptr;
+ ed->objects[ed->object_count].size = size;
+ ed->object_count++;
+ return ptr;
+}
+
+void *ecex_object_calloc(ecex_t *ed, size_t count, size_t size) {
+ void *ptr;
+ size_t total;
+ if (!ed) return NULL;
+ if (count == 0) count = 1;
+ if (size == 0) size = 1;
+ if (count > ((size_t)-1) / size) return NULL;
+ total = count * size;
+ ptr = ecex_object_alloc(ed, total);
+ if (!ptr) return NULL;
+ memset(ptr, 0, total);
+ return ptr;
+}
+
+int ecex_object_free(ecex_t *ed, void *object) {
+ if (!ed || !object) return ECEX_ERR;
+ for (size_t i = 0; i < ed->object_count; ++i) {
+ if (ed->objects[i].ptr == object) {
+ free(ed->objects[i].ptr);
+ if (i + 1 < ed->object_count) {
+ memmove(&ed->objects[i], &ed->objects[i + 1], (ed->object_count - i - 1) * sizeof(ed->objects[i]));
+ }
+ --ed->object_count;
+ if (ed->object_count < ed->object_cap) memset(&ed->objects[ed->object_count], 0, sizeof(ed->objects[ed->object_count]));
+ return ECEX_OK;
+ }
+ }
+ return ECEX_ERR;
+}
+
+int ecex_object_valid(ecex_t *ed, void *object) {
+ return ecex_object_find_entry(ed, object) ? 1 : 0;
+}
+
+int ecex_object_i32_get(ecex_t *ed, void *object, size_t byte_offset, int fallback) {
+ ecex_object_entry_t *o = ecex_object_find_entry(ed, object);
+ int value;
+ if (!o || !o->ptr || byte_offset > o->size || o->size - byte_offset < sizeof(int)) return fallback;
+ memcpy(&value, (const char *)o->ptr + byte_offset, sizeof(value));
+ return value;
+}
+
+int ecex_object_i32_set(ecex_t *ed, void *object, size_t byte_offset, int value) {
+ ecex_object_entry_t *o = ecex_object_find_entry(ed, object);
+ if (!o || !o->ptr || byte_offset > o->size || o->size - byte_offset < sizeof(int)) return ECEX_ERR;
+ memcpy((char *)o->ptr + byte_offset, &value, sizeof(value));
+ return ECEX_OK;
+}
+
+void *ecex_object_ptr_get(ecex_t *ed, void *object, size_t byte_offset) {
+ ecex_object_entry_t *o = ecex_object_find_entry(ed, object);
+ void *value = NULL;
+ if (!o || !o->ptr || byte_offset > o->size || o->size - byte_offset < sizeof(void *)) return NULL;
+ memcpy(&value, (const char *)o->ptr + byte_offset, sizeof(value));
+ return value;
+}
+
+int ecex_object_ptr_set(ecex_t *ed, void *object, size_t byte_offset, void *value) {
+ ecex_object_entry_t *o = ecex_object_find_entry(ed, object);
+ if (!o || !o->ptr || byte_offset > o->size || o->size - byte_offset < sizeof(void *)) return ECEX_ERR;
+ memcpy((char *)o->ptr + byte_offset, &value, sizeof(value));
+ return ECEX_OK;
+}
+
+int ecex_i32_get(const int *items, size_t index) {
+ if (!items) return 0;
+ return items[index];
+}
+
+void ecex_i32_set(int *items, size_t index, int value) {
+ if (!items) return;
+ items[index] = value;
+}
+
+static char *ecex_var_strdup(const char *s) {
+ size_t n;
+ char *out;
+ if (!s) s = "";
+ n = strlen(s) + 1;
+ out = malloc(n);
+ if (!out) return NULL;
+ memcpy(out, s, n);
+ return out;
+}
+
+static int ecex_var_name_eq(const char *a, const char *b) {
+ if (!a) a = "";
+ if (!b) b = "";
+ return strcmp(a, b) == 0;
+}
+
+static ecex_var_entry_t *ecex_var_find_entry(ecex_t *ed, void *owner, const char *name) {
+ size_t i;
+ if (!ed || !name) return NULL;
+ for (i = 0; i < ed->var_count; ++i) {
+ ecex_var_entry_t *v = &ed->vars[i];
+ if (v->owner == owner && ecex_var_name_eq(v->name, name)) return v;
+ }
+ return NULL;
+}
+
+static int ecex_reserve_vars(ecex_t *ed, size_t needed) {
+ ecex_var_entry_t *new_vars;
+ size_t new_cap;
+ if (!ed) return ECEX_ERR;
+ if (needed <= ed->var_cap) return ECEX_OK;
+ new_cap = ed->var_cap ? ed->var_cap * 2 : 16;
+ while (new_cap < needed) new_cap *= 2;
+ new_vars = realloc(ed->vars, new_cap * sizeof(*new_vars));
+ if (!new_vars) return ECEX_ERR;
+ memset(new_vars + ed->var_cap, 0, (new_cap - ed->var_cap) * sizeof(*new_vars));
+ ed->vars = new_vars;
+ ed->var_cap = new_cap;
+ return ECEX_OK;
+}
+
+void *ecex_var_get(ecex_t *ed, void *owner, const char *name) {
+ ecex_var_entry_t *v = ecex_var_find_entry(ed, owner, name);
+ return v ? v->data : NULL;
+}
+
+void *ecex_var_get_or_alloc(ecex_t *ed, void *owner, const char *name, size_t count, size_t elem_size) {
+ ecex_var_entry_t *v;
+ void *data;
+ if (!ed || !name) return NULL;
+ if (count == 0) count = 1;
+ if (elem_size == 0) elem_size = 1;
+
+ v = ecex_var_find_entry(ed, owner, name);
+ if (v) {
+ if (v->elem_size != elem_size) {
+ ecex_log("ecex_var_get_or_alloc: existing slot element-size mismatch");
+ return NULL;
+ }
+ if (v->count < count) {
+ void *new_data;
+ if (!v->dynamic) {
+ ecex_log("ecex_var_get_or_alloc: static slot too small");
+ return NULL;
+ }
+ new_data = realloc(v->data, count * elem_size);
+ if (!new_data) return NULL;
+ memset((char *)new_data + v->count * elem_size, 0, (count - v->count) * elem_size);
+ v->data = new_data;
+ v->count = count;
+ }
+ return v->data;
+ }
+
+ if (ecex_reserve_vars(ed, ed->var_count + 1) != ECEX_OK) return NULL;
+ data = calloc(count, elem_size);
+ if (!data) return NULL;
+
+ v = &ed->vars[ed->var_count++];
+ memset(v, 0, sizeof(*v));
+ v->owner = owner;
+ v->name = ecex_var_strdup(name);
+ if (!v->name) {
+ free(data);
+ --ed->var_count;
+ return NULL;
+ }
+ v->data = data;
+ v->elem_size = elem_size;
+ v->count = count;
+ v->kind = (elem_size == sizeof(int)) ? ECEX_VAR_I32 : ECEX_VAR_BYTES;
+ v->dynamic = 1;
+ return data;
+}
+
+int ecex_var_bind_static(ecex_t *ed, void *owner, const char *name, void *data, size_t count, size_t elem_size) {
+ ecex_var_entry_t *v;
+ if (!ed || !name || !data) return ECEX_ERR;
+ if (count == 0) count = 1;
+ if (elem_size == 0) elem_size = 1;
+
+ v = ecex_var_find_entry(ed, owner, name);
+ if (!v) {
+ if (ecex_reserve_vars(ed, ed->var_count + 1) != ECEX_OK) return ECEX_ERR;
+ v = &ed->vars[ed->var_count++];
+ memset(v, 0, sizeof(*v));
+ v->name = ecex_var_strdup(name);
+ if (!v->name) {
+ --ed->var_count;
+ return ECEX_ERR;
+ }
+ } else if (v->dynamic) {
+ free(v->data);
+ }
+
+ v->owner = owner;
+ v->data = data;
+ v->elem_size = elem_size;
+ v->count = count;
+ v->kind = (elem_size == sizeof(int)) ? ECEX_VAR_I32 : ECEX_VAR_BYTES;
+ v->dynamic = 0;
+ return ECEX_OK;
+}
+
+static void ecex_var_entry_clear(ecex_var_entry_t *v) {
+ if (!v) return;
+ if (v->dynamic) free(v->data);
+ free(v->name);
+ memset(v, 0, sizeof(*v));
+}
+
+int ecex_var_free(ecex_t *ed, void *owner, const char *name) {
+ size_t i;
+ if (!ed || !name) return ECEX_ERR;
+ for (i = 0; i < ed->var_count; ++i) {
+ if (ed->vars[i].owner == owner && ecex_var_name_eq(ed->vars[i].name, name)) {
+ ecex_var_entry_clear(&ed->vars[i]);
+ if (i + 1 < ed->var_count) {
+ memmove(&ed->vars[i], &ed->vars[i + 1], (ed->var_count - i - 1) * sizeof(ed->vars[i]));
+ }
+ --ed->var_count;
+ if (ed->var_count < ed->var_cap) memset(&ed->vars[ed->var_count], 0, sizeof(ed->vars[ed->var_count]));
+ return ECEX_OK;
+ }
+ }
+ return ECEX_ERR;
+}
+
+int ecex_var_free_owner(ecex_t *ed, void *owner) {
+ size_t i;
+ if (!ed) return ECEX_ERR;
+ i = 0;
+ while (i < ed->var_count) {
+ if (ed->vars[i].owner == owner) {
+ ecex_var_entry_clear(&ed->vars[i]);
+ if (i + 1 < ed->var_count) {
+ memmove(&ed->vars[i], &ed->vars[i + 1], (ed->var_count - i - 1) * sizeof(ed->vars[i]));
+ }
+ --ed->var_count;
+ if (ed->var_count < ed->var_cap) memset(&ed->vars[ed->var_count], 0, sizeof(ed->vars[ed->var_count]));
+ continue;
+ }
+ ++i;
+ }
+ return ECEX_OK;
+}
+
+int ecex_var_i32_get(ecex_t *ed, void *owner, const char *name, size_t index, int fallback) {
+ ecex_var_entry_t *v = ecex_var_find_entry(ed, owner, name);
+ if (!v || !v->data || v->elem_size != sizeof(int) || index >= v->count) return fallback;
+ return ((int *)v->data)[index];
+}
+
+int ecex_var_i32_set(ecex_t *ed, void *owner, const char *name, size_t index, int value) {
+ int *data;
+ if (!ed || !name) return ECEX_ERR;
+ data = (int *)ecex_var_get_or_alloc(ed, owner, name, index + 1, sizeof(int));
+ if (!data) return ECEX_ERR;
+ data[index] = value;
+ return ECEX_OK;
+}
+
+int ecex_var_i32(ecex_t *ed, void *owner, const char *name, int fallback) {
+ return ecex_var_i32_get(ed, owner, name, 0, fallback);
+}
+
+int ecex_var_i32_set_scalar(ecex_t *ed, void *owner, const char *name, int value) {
+ return ecex_var_i32_set(ed, owner, name, 0, value);
+}
+
+
+
+static ecex_text_entry_t *ecex_text_find_entry(ecex_t *ed, void *owner, int id) {
+ if (!ed) return NULL;
+ for (size_t i = 0; i < ed->text_count; ++i) {
+ ecex_text_entry_t *t = &ed->texts[i];
+ if (t->owner == owner && t->id == id) return t;
+ }
+ return NULL;
+}
+
+static int ecex_reserve_texts(ecex_t *ed, size_t needed) {
+ ecex_text_entry_t *new_texts;
+ size_t new_cap;
+ if (!ed) return ECEX_ERR;
+ if (needed <= ed->text_cap) return ECEX_OK;
+ new_cap = ed->text_cap ? ed->text_cap * 2 : 32;
+ while (new_cap < needed) new_cap *= 2;
+ new_texts = realloc(ed->texts, new_cap * sizeof(*new_texts));
+ if (!new_texts) return ECEX_ERR;
+ memset(new_texts + ed->text_cap, 0, (new_cap - ed->text_cap) * sizeof(*new_texts));
+ ed->texts = new_texts;
+ ed->text_cap = new_cap;
+ return ECEX_OK;
+}
+
+static void ecex_text_entry_clear(ecex_text_entry_t *t) {
+ if (!t) return;
+ free(t->text);
+ memset(t, 0, sizeof(*t));
+}
+
+int ecex_text_set(ecex_t *ed, void *owner, int id, const char *text, int len) {
+ ecex_text_entry_t *t;
+ char *copy;
+ size_t n;
+ if (!ed || id < 0) return ECEX_ERR;
+ if (!text) text = "";
+ if (len < 0) n = strlen(text);
+ else n = (size_t)len;
+ copy = malloc(n + 1);
+ if (!copy) return ECEX_ERR;
+ if (n) memcpy(copy, text, n);
+ copy[n] = '\0';
+
+ t = ecex_text_find_entry(ed, owner, id);
+ if (!t) {
+ if (ecex_reserve_texts(ed, ed->text_count + 1) != ECEX_OK) {
+ free(copy);
+ return ECEX_ERR;
+ }
+ t = &ed->texts[ed->text_count++];
+ memset(t, 0, sizeof(*t));
+ t->owner = owner;
+ t->id = id;
+ }
+ free(t->text);
+ t->text = copy;
+ t->len = n;
+ return ECEX_OK;
+}
+
+int ecex_text_set_buffer_title(ecex_t *ed, void *owner, int id, buffer_t *buffer) {
+ const char *title = NULL;
+ if (buffer) title = buffer->path ? buffer->path : buffer->name;
+ return ecex_text_set(ed, owner, id, title ? title : "(unnamed)", -1);
+}
+
+int ecex_text_free(ecex_t *ed, void *owner, int id) {
+ if (!ed) return ECEX_ERR;
+ for (size_t i = 0; i < ed->text_count; ++i) {
+ if (ed->texts[i].owner == owner && ed->texts[i].id == id) {
+ ecex_text_entry_clear(&ed->texts[i]);
+ if (i + 1 < ed->text_count) {
+ memmove(&ed->texts[i], &ed->texts[i + 1], (ed->text_count - i - 1) * sizeof(ed->texts[i]));
+ }
+ --ed->text_count;
+ if (ed->text_count < ed->text_cap) memset(&ed->texts[ed->text_count], 0, sizeof(ed->texts[ed->text_count]));
+ return ECEX_OK;
+ }
+ }
+ return ECEX_ERR;
+}
+
+int ecex_text_free_owner(ecex_t *ed, void *owner) {
+ if (!ed) return ECEX_ERR;
+ for (size_t i = 0; i < ed->text_count;) {
+ if (ed->texts[i].owner == owner) {
+ ecex_text_entry_clear(&ed->texts[i]);
+ if (i + 1 < ed->text_count) {
+ memmove(&ed->texts[i], &ed->texts[i + 1], (ed->text_count - i - 1) * sizeof(ed->texts[i]));
+ }
+ --ed->text_count;
+ if (ed->text_count < ed->text_cap) memset(&ed->texts[ed->text_count], 0, sizeof(ed->texts[ed->text_count]));
+ continue;
+ }
+ ++i;
+ }
+ return ECEX_OK;
+}
+
+const char *ecex_text_get_for_draw(ecex_t *ed, void *owner, int id) {
+ ecex_text_entry_t *t = ecex_text_find_entry(ed, owner, id);
+ return t && t->text ? t->text : "";
+}
+
+static int ecex_ascii_equal_ci(const char *a, const char *b) {
+ unsigned char ca;
+ unsigned char cb;
+ if (!a || !b) return 0;
+ while (*a && *b) {
+ ca = (unsigned char)*a;
+ cb = (unsigned char)*b;
+ if (ca >= 'A' && ca <= 'Z') ca = (unsigned char)(ca - 'A' + 'a');
+ if (cb >= 'A' && cb <= 'Z') cb = (unsigned char)(cb - 'A' + 'a');
+ if (ca != cb) return 0;
+ ++a;
+ ++b;
+ }
+ return *a == '\0' && *b == '\0';
+}
+
+static const char *ecex_path_extension(const char *path) {
+ const char *dot;
+ if (!path) return NULL;
+ dot = strrchr(path, '.');
+ return dot && dot[0] ? dot : NULL;
+}
+
+static int ecex_reserve_file_handlers(ecex_t *ed, size_t needed) {
+ ecex_file_handler_t *new_handlers;
+ size_t new_cap;
+ if (!ed) return ECEX_ERR;
+ if (needed <= ed->file_handler_cap) return ECEX_OK;
+ new_cap = ed->file_handler_cap ? ed->file_handler_cap * 2 : 8;
+ while (new_cap < needed) new_cap *= 2;
+ new_handlers = realloc(ed->file_handlers, new_cap * sizeof(*new_handlers));
+ if (!new_handlers) return ECEX_ERR;
+ memset(new_handlers + ed->file_handler_cap, 0, (new_cap - ed->file_handler_cap) * sizeof(*new_handlers));
+ ed->file_handlers = new_handlers;
+ ed->file_handler_cap = new_cap;
+ return ECEX_OK;
+}
+
+int ecex_register_file_handler(ecex_t *ed, const char *extension, ecex_file_handler_fn fn) {
+ char *copy;
+ if (!ed || !extension || !extension[0] || !fn) return ECEX_ERR;
+ for (size_t i = 0; i < ed->file_handler_count; ++i) {
+ if (ecex_ascii_equal_ci(ed->file_handlers[i].extension, extension)) {
+ ed->file_handlers[i].fn = fn;
+ return ECEX_OK;
+ }
+ }
+ if (ecex_reserve_file_handlers(ed, ed->file_handler_count + 1) != ECEX_OK) return ECEX_ERR;
+ copy = ecex_var_strdup(extension);
+ if (!copy) return ECEX_ERR;
+ ed->file_handlers[ed->file_handler_count].extension = copy;
+ ed->file_handlers[ed->file_handler_count].fn = fn;
+ ed->file_handler_count++;
+ return ECEX_OK;
+}
+
+int ecex_run_file_handlers(ecex_t *ed, buffer_t *buffer) {
+ const char *ext;
+ const char *path;
+ if (!ed || !buffer) return ECEX_ERR;
+ path = buffer->path ? buffer->path : buffer->name;
+ ext = ecex_path_extension(path);
+ if (!ext) return ECEX_OK;
+ for (size_t i = 0; i < ed->file_handler_count; ++i) {
+ if (ecex_ascii_equal_ci(ed->file_handlers[i].extension, ext)) {
+ return ed->file_handlers[i].fn(ed, buffer);
+ }
+ }
+ return ECEX_OK;
+}
+
+int ecex_prng_next_bounded(unsigned int *state, int bound) {
+ unsigned int x;
+ unsigned int limit;
+
+ if (!state || bound <= 0) return 0;
+
+ x = *state;
+ if (x == 0u) x = 0x6d2b79f5u;
+
+ /* Xorshift32. Keep this host-side so CCDJIT plugins do not depend on
+ * unsigned multiply/divide lowering details for game logic randomness. */
+ x ^= x << 13;
+ x ^= x >> 17;
+ x ^= x << 5;
+ if (x == 0u) x = 0xa5a5a5a5u;
+ *state = x;
+
+ /* Avoid modulo bias enough for tiny bounds without libc. */
+ limit = 0xffffffffu - (0xffffffffu % (unsigned int)bound);
+ while (x >= limit) {
+ x ^= x << 13;
+ x ^= x >> 17;
+ x ^= x << 5;
+ if (x == 0u) x = 0x9e3779b9u;
+ *state = x;
+ }
+
+ return (int)(x % (unsigned int)bound);
+}
+
+
+int ecex_random_bounded(int bound) {
+ static unsigned int state = 0x7f4a7c15u;
+ unsigned int mix;
+
+ if (bound <= 0) return 0;
+ /* Host-owned PRNG for plugins that should not pass writable state pointers
+ * through the JIT ABI. Stir in a stack address so separate launches differ
+ * enough for games/demos without requiring libc time calls from plugins. */
+ mix = (unsigned int)(size_t)&bound;
+ state ^= mix + 0x9e3779b9u + (state << 6) + (state >> 2);
+ return ecex_prng_next_bounded(&state, bound);
+}
+
+int ecex_tetris_shape_cell(int piece, int rot, int col, int row) {
+ int p = piece % 7;
+ int r = rot & 3;
+ if (p < 0) p += 7;
+ if (col < 0 || col >= 4 || row < 0 || row >= 4) return 0;
+
+ /* I */
+ if (p == 0) {
+ if ((r & 1) == 0) return row == 1;
+ return col == 1;
+ }
+ /* O */
+ if (p == 1) return (row == 1 || row == 2) && (col == 1 || col == 2);
+ /* T */
+ if (p == 2) {
+ if (r == 0) return (row == 1 && col >= 0 && col <= 2) || (row == 2 && col == 1);
+ if (r == 1) return (col == 1 && row >= 0 && row <= 2) || (row == 1 && col == 2);
+ if (r == 2) return (row == 1 && col >= 0 && col <= 2) || (row == 0 && col == 1);
+ return (col == 1 && row >= 0 && row <= 2) || (row == 1 && col == 0);
+ }
+ /* S */
+ if (p == 3) {
+ if ((r & 1) == 0) return (row == 1 && (col == 1 || col == 2)) || (row == 2 && (col == 0 || col == 1));
+ return (col == 1 && (row == 0 || row == 1)) || (col == 2 && (row == 1 || row == 2));
+ }
+ /* Z */
+ if (p == 4) {
+ if ((r & 1) == 0) return (row == 1 && (col == 0 || col == 1)) || (row == 2 && (col == 1 || col == 2));
+ return (col == 2 && (row == 0 || row == 1)) || (col == 1 && (row == 1 || row == 2));
+ }
+ /* J */
+ if (p == 5) {
+ if (r == 0) return (row == 1 && col >= 0 && col <= 2) || (row == 0 && col == 0);
+ if (r == 1) return (col == 1 && row >= 0 && row <= 2) || (row == 0 && col == 2);
+ if (r == 2) return (row == 1 && col >= 0 && col <= 2) || (row == 2 && col == 2);
+ return (col == 1 && row >= 0 && row <= 2) || (row == 2 && col == 0);
+ }
+ /* L */
+ if (r == 0) return (row == 1 && col >= 0 && col <= 2) || (row == 0 && col == 2);
+ if (r == 1) return (col == 1 && row >= 0 && row <= 2) || (row == 2 && col == 2);
+ if (r == 2) return (row == 1 && col >= 0 && col <= 2) || (row == 2 && col == 0);
+ return (col == 1 && row >= 0 && row <= 2) || (row == 0 && col == 0);
+}
+
+static int ecex_file_browser_preview_current(ecex_t *ed);
+static int ecex_file_browser_update_preview_if_enabled(ecex_t *ed);
+
static ecex_color_t ecex_color(float r, float g, float b) {
ecex_color_t c = {r, g, b};
return c;
@@ -72,6 +710,16 @@ static void ecex_theme_set_defaults(ecex_t *ed) {
static int cmd_quit(ecex_t *ed) {
if (!ed) return ECEX_ERR;
+ if (ecex_has_modified_buffers(ed)) {
+ fprintf(stderr, "ecex: refusing to quit; modified buffers exist. Save or use force-quit.\n");
+ return ECEX_ERR;
+ }
+ ed->should_quit = 1;
+ return ECEX_OK;
+}
+
+static int cmd_force_quit(ecex_t *ed) {
+ if (!ed) return ECEX_ERR;
ed->should_quit = 1;
return ECEX_OK;
}
@@ -88,8 +736,18 @@ static int cmd_balance_windows(ecex_t *ed) { return ecex_balance_windows(ed); }
static int cmd_move_left(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); buffer_move_left(buf); return ECEX_OK; }
static int cmd_move_right(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); buffer_move_right(buf); return ECEX_OK; }
-static int cmd_move_up(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); buffer_move_up(buf); return ECEX_OK; }
-static int cmd_move_down(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); buffer_move_down(buf); return ECEX_OK; }
+static int cmd_move_up(ecex_t *ed) {
+ CURRENT_BUFFER_OR_ERR(ed);
+ buffer_move_up(buf);
+ ecex_file_browser_update_preview_if_enabled(ed);
+ return ECEX_OK;
+}
+static int cmd_move_down(ecex_t *ed) {
+ CURRENT_BUFFER_OR_ERR(ed);
+ buffer_move_down(buf);
+ ecex_file_browser_update_preview_if_enabled(ed);
+ return ECEX_OK;
+}
static int cmd_move_word_left(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); buffer_move_word_left(buf); return ECEX_OK; }
static int cmd_move_word_right(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); buffer_move_word_right(buf); return ECEX_OK; }
static int cmd_beginning_of_line(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); buffer_move_beginning_of_line(buf); return ECEX_OK; }
@@ -159,6 +817,7 @@ 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); }
@@ -227,9 +886,401 @@ 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);
@@ -245,6 +1296,7 @@ static int ecex_register_builtins(ecex_t *ed) {
ECEX_COMMAND("list-buffers", cmd_list_buffers);
ECEX_COMMAND("switch-buffer", cmd_switch_buffer);
ECEX_COMMAND("kill-buffer", cmd_kill_buffer_command);
+ ECEX_COMMAND("force-kill-buffer", cmd_force_kill_buffer_command);
ECEX_COMMAND("compile", cmd_compile);
ECEX_COMMAND("recompile", cmd_recompile);
ECEX_COMMAND("grep", cmd_grep);
@@ -320,11 +1372,15 @@ static int ecex_register_builtins(ecex_t *ed) {
ECEX_BIND("C-S-z", "redo");
ECEX_BIND("C-x C-f", "find-file");
+ ECEX_BIND("C-x f", "find-file");
+ ECEX_BIND("C-x d", "file-browser");
+ ECEX_BIND("C-x C-d", "file-browser-here");
ECEX_BIND("C-x C-s", "save-buffer");
ECEX_BIND("C-x C-w", "write-file");
ECEX_BIND("C-x C-b", "list-buffers");
ECEX_BIND("C-x b", "switch-buffer");
ECEX_BIND("C-x k", "kill-buffer");
+ ECEX_BIND("C-x K", "force-kill-buffer");
ECEX_BIND("C-x 2", "split-window-below");
ECEX_BIND("C-x 3", "split-window-right");
ECEX_BIND("C-x o", "other-window");
@@ -351,10 +1407,26 @@ static int ecex_register_builtins(ecex_t *ed) {
ecex_define_major_mode(ed, "c-mode");
ecex_define_major_mode(ed, "eval-output-mode");
ecex_define_major_mode(ed, "special-mode");
+ ecex_define_major_mode(ed, "file-browser-mode");
+ ecex_define_major_mode(ed, "media-preview-mode");
+ ecex_define_major_mode(ed, "markdown-mode");
ecex_bind_mode_key(ed, "eval-output-mode", "g", "eval-rerun-last");
ecex_bind_mode_key(ed, "eval-output-mode", "q", "quit-window");
ecex_bind_mode_key(ed, "eval-output-mode", "r", "eval-rerun-last");
ecex_bind_mode_key(ed, "special-mode", "q", "quit-window");
+
+ ecex_bind_mode_key(ed, "file-browser-mode", "g", "file-browser-refresh");
+ ecex_bind_mode_key(ed, "file-browser-mode", "r", "file-browser-refresh");
+ ecex_bind_mode_key(ed, "file-browser-mode", "h", "file-browser-parent");
+ ecex_bind_mode_key(ed, "file-browser-mode", "l", "file-browser-open");
+ ecex_bind_mode_key(ed, "file-browser-mode", "v", "file-browser-preview");
+ ecex_bind_mode_key(ed, "file-browser-mode", "m", "file-browser-toggle-preview");
+ ecex_bind_mode_key(ed, "file-browser-mode", "b", "file-browser-history-back");
+ ecex_bind_mode_key(ed, "file-browser-mode", "f", "file-browser-history-forward");
+ ecex_bind_mode_key(ed, "file-browser-mode", "q", "quit-window");
+ ecex_bind_mode_key(ed, "media-preview-mode", "SPC", "media-play-pause");
+ ecex_bind_mode_key(ed, "media-preview-mode", "p", "media-play-pause");
+ ecex_bind_mode_key(ed, "media-preview-mode", "q", "quit-window");
ecex_bind_mode_key(ed, "special-mode", "g", "recompile");
ecex_bind_mode_key(ed, "special-mode", "n", "next-error");
ecex_bind_mode_key(ed, "special-mode", "p", "previous-error");
@@ -410,14 +1482,18 @@ ecex_t *ecex_new(void) {
void ecex_free(ecex_t *ed) {
if (!ed) return;
- for (size_t i = 0; i < ed->jit_module_count; i++) {
- ccdjit_module_free((ccdjit_module *)ed->jit_modules[i]);
- }
-
+ /* Buffers may hold renderer/animation callbacks and userdata destructors
+ * compiled by CCDJIT config modules. Run those destructors while their JIT
+ * modules are still alive; freeing modules first leaves callback pointers
+ * dangling and can segfault during buffer teardown. */
for (size_t i = 0; i < ed->buffer_count; i++) {
buffer_free(ed->buffers[i]);
}
+ for (size_t i = 0; i < ed->jit_module_count; i++) {
+ ccdjit_module_free((ccdjit_module *)ed->jit_modules[i]);
+ }
+
for (size_t i = 0; i < ed->command_count; i++) {
free(ed->commands[i].name);
}
@@ -436,6 +1512,22 @@ void ecex_free(ecex_t *ed) {
free(ed->major_modes[i].name);
}
+ for (size_t i = 0; i < ed->var_count; i++) {
+ ecex_var_entry_clear(&ed->vars[i]);
+ }
+
+ for (size_t i = 0; i < ed->text_count; i++) {
+ ecex_text_entry_clear(&ed->texts[i]);
+ }
+
+ for (size_t i = 0; i < ed->file_handler_count; i++) {
+ free(ed->file_handlers[i].extension);
+ }
+
+ for (size_t i = 0; i < ed->object_count; i++) {
+ free(ed->objects[i].ptr);
+ }
+
free(ed->jit_modules);
free(ed->windows);
free(ed->buffers);
@@ -443,6 +1535,10 @@ void ecex_free(ecex_t *ed) {
free(ed->keybinds);
free(ed->mode_keybinds);
free(ed->major_modes);
+ free(ed->vars);
+ free(ed->texts);
+ free(ed->file_handlers);
+ free(ed->objects);
free(ed->last_eval_source);
free(ed->last_eval_filename);
free(ed->last_compile_command);
@@ -513,10 +1609,27 @@ buffer_t *ecex_find_buffer(ecex_t *ed, const char *name) {
}
+static int ecex_buffer_index_of(ecex_t *ed, buffer_t *buffer, size_t *out_index) {
+ if (!ed || !buffer) return ECEX_ERR;
+ for (size_t i = 0; i < ed->buffer_count; i++) {
+ if (ed->buffers[i] == buffer) {
+ if (out_index) *out_index = i;
+ return ECEX_OK;
+ }
+ }
+ return ECEX_ERR;
+}
+
static int ecex_set_current_buffer_index(ecex_t *ed, size_t index) {
if (!ed || index >= ed->buffer_count) return ECEX_ERR;
+
+ buffer_t *next = ed->buffers[index];
+ if (ed->current_buffer && ed->current_buffer != next) {
+ ed->previous_buffer = ed->current_buffer;
+ }
+
ed->current_buffer_index = index;
- ed->current_buffer = ed->buffers[index];
+ ed->current_buffer = next;
ecex_window_t *win = ecex_current_window(ed);
if (win) win->buffer = ed->current_buffer;
return ECEX_OK;
@@ -527,9 +1640,14 @@ int ecex_sync_current_buffer(ecex_t *ed) {
ecex_window_t *win = ecex_current_window(ed);
if (!win || !win->buffer) return ECEX_ERR;
- ed->current_buffer = win->buffer;
+ buffer_t *next = win->buffer;
+ if (ed->current_buffer && ed->current_buffer != next) {
+ ed->previous_buffer = ed->current_buffer;
+ }
+
+ ed->current_buffer = next;
for (size_t i = 0; i < ed->buffer_count; i++) {
- if (ed->buffers[i] == win->buffer) {
+ if (ed->buffers[i] == next) {
ed->current_buffer_index = i;
return ECEX_OK;
}
@@ -538,7 +1656,14 @@ int ecex_sync_current_buffer(ecex_t *ed) {
}
int ecex_switch_buffer(ecex_t *ed, const char *name) {
- if (!ed || !name) return ECEX_ERR;
+ if (!ed) return ECEX_ERR;
+
+ if (!name || name[0] == '\0') {
+ buffer_t *other = ecex_other_buffer(ed);
+ size_t index = 0;
+ if (!other || ecex_buffer_index_of(ed, other, &index) != ECEX_OK) return ECEX_ERR;
+ return ecex_set_current_buffer_index(ed, index);
+ }
for (size_t i = 0; i < ed->buffer_count; i++) {
if (strcmp(ed->buffers[i]->name, name) == 0) {
@@ -556,6 +1681,22 @@ buffer_t *ecex_current_buffer(ecex_t *ed) {
return ed->current_buffer;
}
+buffer_t *ecex_other_buffer(ecex_t *ed) {
+ if (!ed || ed->buffer_count == 0) return NULL;
+
+ buffer_t *current = ecex_current_buffer(ed);
+ if (ed->previous_buffer && ed->previous_buffer != current &&
+ ecex_buffer_index_of(ed, ed->previous_buffer, NULL) == ECEX_OK) {
+ return ed->previous_buffer;
+ }
+
+ for (size_t i = 0; i < ed->buffer_count; i++) {
+ if (ed->buffers[i] && ed->buffers[i] != current) return ed->buffers[i];
+ }
+
+ return current;
+}
+
ecex_window_t *ecex_current_window(ecex_t *ed) {
if (!ed || ed->window_count == 0 || ed->current_window_index >= ed->window_count) return NULL;
return &ed->windows[ed->current_window_index];
@@ -688,32 +1829,40 @@ int ecex_previous_buffer(ecex_t *ed) {
return ecex_set_current_buffer_index(ed, ed->current_buffer_index);
}
-int ecex_kill_buffer(ecex_t *ed, const char *name) {
- if (!ed || !name || ed->buffer_count == 0) return ECEX_ERR;
+static int ecex_kill_buffer_impl(ecex_t *ed, const char *name, int force) {
+ if (!ed || ed->buffer_count == 0) return ECEX_ERR;
size_t index = ed->buffer_count;
- for (size_t i = 0; i < ed->buffer_count; i++) {
- if (strcmp(ed->buffers[i]->name, name) == 0) {
- index = i;
- break;
+ if (!name || name[0] == '\0') {
+ buffer_t *current = ecex_current_buffer(ed);
+ if (!current || ecex_buffer_index_of(ed, current, &index) != ECEX_OK) return ECEX_ERR;
+ } else {
+ for (size_t i = 0; i < ed->buffer_count; i++) {
+ if (strcmp(ed->buffers[i]->name, name) == 0) {
+ index = i;
+ break;
+ }
}
}
if (index == ed->buffer_count) return ECEX_ERR;
buffer_t *victim = ed->buffers[index];
-
- for (size_t i = index; i + 1 < ed->buffer_count; i++) {
- ed->buffers[i] = ed->buffers[i + 1];
+ if (ed->previous_buffer == victim) ed->previous_buffer = NULL;
+ if (!force && victim->modified && !victim->read_only) {
+ fprintf(stderr,
+ "ecex: refusing to kill modified buffer '%s'; save it or use force-kill-buffer.\n",
+ victim->name ? victim->name : "<unnamed>");
+ return ECEX_ERR;
}
+ for (size_t i = index; i + 1 < ed->buffer_count; i++) ed->buffers[i] = ed->buffers[i + 1];
ed->buffer_count--;
- buffer_t *fallback = ed->buffer_count > 0 ? ed->buffers[0] : NULL;
+ buffer_t *fallback = ecex_other_buffer(ed);
+ if (fallback == victim) fallback = ed->buffer_count > 0 ? ed->buffers[0] : NULL;
for (size_t i = 0; i < ed->window_count; i++) {
- if (ed->windows[i].buffer == victim) {
- ed->windows[i].buffer = fallback;
- }
+ if (ed->windows[i].buffer == victim) ed->windows[i].buffer = fallback;
}
buffer_free(victim);
@@ -733,6 +1882,23 @@ int ecex_kill_buffer(ecex_t *ed, const char *name) {
return ecex_sync_current_buffer(ed);
}
+int ecex_kill_buffer(ecex_t *ed, const char *name) {
+ return ecex_kill_buffer_impl(ed, name, 0);
+}
+
+int ecex_kill_buffer_force(ecex_t *ed, const char *name) {
+ return ecex_kill_buffer_impl(ed, name, 1);
+}
+
+int ecex_has_modified_buffers(ecex_t *ed) {
+ if (!ed) return 0;
+ for (size_t i = 0; i < ed->buffer_count; i++) {
+ buffer_t *buf = ed->buffers[i];
+ if (buf && buf->modified && !buf->read_only) return 1;
+ }
+ return 0;
+}
+
int ecex_keep_jit_module(ecex_t *ed, void *module) {
if (!ed || !module) return ECEX_ERR;
@@ -795,6 +1961,129 @@ static void ecex_clear_mode_keybinds(ecex_t *ed) {
ed->mode_keybind_count = 0;
}
+typedef struct ecex_binding_snapshot {
+ ecex_command_t *commands;
+ size_t command_count;
+ size_t command_cap;
+ ecex_keybind_t *keybinds;
+ size_t keybind_count;
+ size_t keybind_cap;
+ ecex_mode_keybind_t *mode_keybinds;
+ size_t mode_keybind_count;
+ size_t mode_keybind_cap;
+ ecex_theme_t theme;
+} ecex_binding_snapshot_t;
+
+static void ecex_theme_snapshot_free(ecex_theme_t *theme) {
+ if (!theme) return;
+ free(theme->font_path);
+ theme->font_path = NULL;
+}
+
+static int ecex_theme_clone(ecex_theme_t *out, const ecex_theme_t *in) {
+ if (!out || !in) return ECEX_ERR;
+ *out = *in;
+ out->font_path = NULL;
+ if (in->font_path) {
+ out->font_path = ecex_strdup(in->font_path);
+ if (!out->font_path) return ECEX_ERR;
+ }
+ return ECEX_OK;
+}
+
+static void ecex_snapshot_free(ecex_binding_snapshot_t *snap) {
+ if (!snap) return;
+ for (size_t i = 0; i < snap->command_count; i++) free(snap->commands[i].name);
+ for (size_t i = 0; i < snap->keybind_count; i++) {
+ free(snap->keybinds[i].key);
+ free(snap->keybinds[i].command);
+ }
+ for (size_t i = 0; i < snap->mode_keybind_count; i++) {
+ free(snap->mode_keybinds[i].key);
+ free(snap->mode_keybinds[i].command);
+ }
+ free(snap->commands);
+ free(snap->keybinds);
+ free(snap->mode_keybinds);
+ ecex_theme_snapshot_free(&snap->theme);
+ memset(snap, 0, sizeof(*snap));
+}
+
+static int ecex_snapshot_clone(ecex_binding_snapshot_t *snap, ecex_t *ed) {
+ if (!snap || !ed) return ECEX_ERR;
+ memset(snap, 0, sizeof(*snap));
+
+ if (ecex_theme_clone(&snap->theme, &ed->theme) != ECEX_OK) return ECEX_ERR;
+
+ if (ed->command_cap) {
+ snap->commands = calloc(ed->command_cap, sizeof(*snap->commands));
+ if (!snap->commands) goto fail;
+ snap->command_cap = ed->command_cap;
+ snap->command_count = ed->command_count;
+ for (size_t i = 0; i < ed->command_count; i++) {
+ snap->commands[i].fn = ed->commands[i].fn;
+ snap->commands[i].name = ecex_strdup(ed->commands[i].name);
+ if (!snap->commands[i].name) goto fail;
+ }
+ }
+
+ if (ed->keybind_cap) {
+ snap->keybinds = calloc(ed->keybind_cap, sizeof(*snap->keybinds));
+ if (!snap->keybinds) goto fail;
+ snap->keybind_cap = ed->keybind_cap;
+ snap->keybind_count = ed->keybind_count;
+ for (size_t i = 0; i < ed->keybind_count; i++) {
+ snap->keybinds[i].key = ecex_strdup(ed->keybinds[i].key);
+ snap->keybinds[i].command = ecex_strdup(ed->keybinds[i].command);
+ if (!snap->keybinds[i].key || !snap->keybinds[i].command) goto fail;
+ }
+ }
+
+ if (ed->mode_keybind_cap) {
+ snap->mode_keybinds = calloc(ed->mode_keybind_cap, sizeof(*snap->mode_keybinds));
+ if (!snap->mode_keybinds) goto fail;
+ snap->mode_keybind_cap = ed->mode_keybind_cap;
+ snap->mode_keybind_count = ed->mode_keybind_count;
+ for (size_t i = 0; i < ed->mode_keybind_count; i++) {
+ snap->mode_keybinds[i].mode = ed->mode_keybinds[i].mode;
+ snap->mode_keybinds[i].key = ecex_strdup(ed->mode_keybinds[i].key);
+ snap->mode_keybinds[i].command = ecex_strdup(ed->mode_keybinds[i].command);
+ if (!snap->mode_keybinds[i].key || !snap->mode_keybinds[i].command) goto fail;
+ }
+ }
+
+ return ECEX_OK;
+
+fail:
+ ecex_snapshot_free(snap);
+ return ECEX_ERR;
+}
+
+static void ecex_restore_snapshot(ecex_t *ed, ecex_binding_snapshot_t *snap) {
+ if (!ed || !snap) return;
+
+ ecex_clear_commands(ed);
+ ecex_clear_keybinds(ed);
+ ecex_clear_mode_keybinds(ed);
+ free(ed->commands);
+ free(ed->keybinds);
+ free(ed->mode_keybinds);
+ free(ed->theme.font_path);
+
+ ed->commands = snap->commands;
+ ed->command_count = snap->command_count;
+ ed->command_cap = snap->command_cap;
+ ed->keybinds = snap->keybinds;
+ ed->keybind_count = snap->keybind_count;
+ ed->keybind_cap = snap->keybind_cap;
+ ed->mode_keybinds = snap->mode_keybinds;
+ ed->mode_keybind_count = snap->mode_keybind_count;
+ ed->mode_keybind_cap = snap->mode_keybind_cap;
+ ed->theme = snap->theme;
+
+ memset(snap, 0, sizeof(*snap));
+}
+
int ecex_reload_config(ecex_t *ed) {
if (!ed || !ed->config_path || !ed->config_path[0]) {
fprintf(stderr, "ecex: no config file to reload; start with --config path/to/ecexrc.c\n");
@@ -804,25 +2093,89 @@ int ecex_reload_config(ecex_t *ed) {
char *path = ecex_strdup(ed->config_path);
if (!path) return ECEX_ERR;
+ ecex_binding_snapshot_t snapshot;
+ if (ecex_snapshot_clone(&snapshot, ed) != ECEX_OK) {
+ free(path);
+ fprintf(stderr, "ecex: failed to snapshot config state before reload\n");
+ return ECEX_ERR;
+ }
+
ecex_clear_commands(ed);
ecex_clear_keybinds(ed);
ecex_clear_mode_keybinds(ed);
- if (ecex_register_builtins(ed) != ECEX_OK) {
- free(path);
- return ECEX_ERR;
+ int result = ECEX_ERR;
+ if (ecex_register_builtins(ed) == ECEX_OK) {
+ result = ecex_load_c_config(ed, path);
+ if (result == ECEX_OK) result = ecex_validate_bindings(ed);
+ }
+
+ if (result != ECEX_OK) {
+ fprintf(stderr, "ecex: config reload failed; keeping previous config active\n");
+ ecex_restore_snapshot(ed, &snapshot);
+ } else {
+ ecex_snapshot_free(&snapshot);
}
- int result = ecex_load_c_config(ed, path);
free(path);
return result;
}
+
+int ecex_config_register_commands(ecex_t *ed, const ecex_config_command_t *commands, size_t count) {
+ if (!ed || (!commands && count != 0)) return ECEX_ERR;
+ for (size_t i = 0; i < count; i++) {
+ if (!commands[i].name || !commands[i].fn) return ECEX_ERR;
+ if (ecex_register_command(ed, commands[i].name, commands[i].fn) != ECEX_OK) return ECEX_ERR;
+ }
+ return ECEX_OK;
+}
+
+int ecex_config_bind_keys(ecex_t *ed, const ecex_config_keybind_t *bindings, size_t count) {
+ if (!ed || (!bindings && count != 0)) return ECEX_ERR;
+ for (size_t i = 0; i < count; i++) {
+ if (!bindings[i].key || !bindings[i].command) return ECEX_ERR;
+ if (ecex_bind_key(ed, bindings[i].key, bindings[i].command) != ECEX_OK) return ECEX_ERR;
+ }
+ return ECEX_OK;
+}
+
+int ecex_config_bind_mode_keys(ecex_t *ed, const ecex_config_mode_keybind_t *bindings, size_t count) {
+ if (!ed || (!bindings && count != 0)) return ECEX_ERR;
+ for (size_t i = 0; i < count; i++) {
+ if (!bindings[i].mode || !bindings[i].key || !bindings[i].command) return ECEX_ERR;
+ if (ecex_bind_mode_key(ed, bindings[i].mode, bindings[i].key, bindings[i].command) != ECEX_OK) return ECEX_ERR;
+ }
+ return ECEX_OK;
+}
+
+int ecex_config_define_modes(ecex_t *ed, const char *const *modes, size_t count) {
+ if (!ed || (!modes && count != 0)) return ECEX_ERR;
+ for (size_t i = 0; i < count; i++) {
+ if (!modes[i] || !ecex_define_major_mode(ed, modes[i])) return ECEX_ERR;
+ }
+ return ECEX_OK;
+}
+
+int ecex_apply_theme(ecex_t *ed, const ecex_theme_t *theme) {
+ if (!ed || !theme) return ECEX_ERR;
+ char *font = NULL;
+ if (theme->font_path) {
+ font = ecex_strdup(theme->font_path);
+ if (!font) return ECEX_ERR;
+ }
+ free(ed->theme.font_path);
+ ed->theme = *theme;
+ ed->theme.font_path = font;
+ return ECEX_OK;
+}
+
int ecex_register_command(ecex_t *ed, const char *name, ecex_command_fn fn) {
if (!ed || !name || !fn) return ECEX_ERR;
for (size_t i = 0; i < ed->command_count; i++) {
if (strcmp(ed->commands[i].name, name) == 0) {
+ fprintf(stderr, "ecex: command warning: replacing existing command '%s'\n", name);
ed->commands[i].fn = fn;
return ECEX_OK;
}
@@ -877,11 +2230,66 @@ int ecex_clipboard_set(ecex_t *ed, const char *text) {
return ECEX_OK;
}
+static int ecex_command_exists(ecex_t *ed, const char *name) {
+ if (!ed || !name) return 0;
+ for (size_t i = 0; i < ed->command_count; i++) {
+ if (ed->commands[i].name && strcmp(ed->commands[i].name, name) == 0) return 1;
+ }
+ return 0;
+}
+
+static int ecex_key_is_prefix_of(const char *prefix, const char *key) {
+ if (!prefix || !key) return 0;
+ size_t n = strlen(prefix);
+ return strncmp(prefix, key, n) == 0 && key[n] == ' ';
+}
+
+static void ecex_warn_keybind_issue(const char *scope, const char *key, const char *detail) {
+ if (scope && scope[0]) {
+ fprintf(stderr,
+ "ecex: keybind warning [%s]: %s%s%s\n",
+ scope,
+ key ? key : "<null>",
+ detail && detail[0] ? " " : "",
+ detail ? detail : "");
+ } else {
+ fprintf(stderr,
+ "ecex: keybind warning: %s%s%s\n",
+ key ? key : "<null>",
+ detail && detail[0] ? " " : "",
+ detail ? detail : "");
+ }
+}
+
+static void ecex_warn_keybind_conflicts(ecex_t *ed, const char *scope, const char *key, int mode) {
+ if (!ed || !key) return;
+
+ for (size_t i = 0; i < ed->keybind_count; i++) {
+ if (mode != 0) break;
+ const char *existing = ed->keybinds[i].key;
+ if (existing && ecex_key_is_prefix_of(key, existing)) ecex_warn_keybind_issue(scope, key, "is a prefix of an existing binding");
+ if (existing && ecex_key_is_prefix_of(existing, key)) ecex_warn_keybind_issue(scope, key, "extends an existing complete binding");
+ }
+
+ for (size_t i = 0; i < ed->mode_keybind_count; i++) {
+ if (mode != ed->mode_keybinds[i].mode) continue;
+ const char *existing = ed->mode_keybinds[i].key;
+ if (existing && ecex_key_is_prefix_of(key, existing)) ecex_warn_keybind_issue(scope, key, "is a prefix of an existing mode binding");
+ if (existing && ecex_key_is_prefix_of(existing, key)) ecex_warn_keybind_issue(scope, key, "extends an existing complete mode binding");
+ }
+}
+
int ecex_bind_key(ecex_t *ed, const char *key, const char *command) {
if (!ed || !key || !command) return ECEX_ERR;
+ if (!ecex_command_exists(ed, command)) {
+ ecex_warn_keybind_issue("global", key, "targets a command that is not registered yet");
+ }
+ ecex_warn_keybind_conflicts(ed, "global", key, 0);
+
for (size_t i = 0; i < ed->keybind_count; i++) {
if (strcmp(ed->keybinds[i].key, key) == 0) {
+ fprintf(stderr, "ecex: keybind warning [global]: replacing %s from %s to %s\n", key, ed->keybinds[i].command, command);
char *new_command = ecex_strdup(command);
if (!new_command) return ECEX_ERR;
@@ -990,8 +2398,14 @@ int ecex_bind_mode_key(ecex_t *ed, const char *mode_name, const char *key, const
int mode = ecex_define_major_mode(ed, mode_name);
if (!mode) return ECEX_ERR;
+ if (!ecex_command_exists(ed, command)) {
+ ecex_warn_keybind_issue(mode_name, key, "targets a command that is not registered yet");
+ }
+ ecex_warn_keybind_conflicts(ed, mode_name, key, mode);
+
for (size_t i = 0; i < ed->mode_keybind_count; i++) {
if (ed->mode_keybinds[i].mode == mode && strcmp(ed->mode_keybinds[i].key, key) == 0) {
+ fprintf(stderr, "ecex: keybind warning [%s]: replacing %s from %s to %s\n", mode_name, key, ed->mode_keybinds[i].command, command);
char *new_command = ecex_strdup(command);
if (!new_command) return ECEX_ERR;
free(ed->mode_keybinds[i].command);
@@ -1022,6 +2436,43 @@ int ecex_bind_mode_key(ecex_t *ed, const char *mode_name, const char *key, const
return ECEX_OK;
}
+int ecex_validate_bindings(ecex_t *ed) {
+ if (!ed) return ECEX_ERR;
+ int ok = ECEX_OK;
+
+ for (size_t i = 0; i < ed->keybind_count; i++) {
+ const char *command = ed->keybinds[i].command;
+ if (!ecex_command_exists(ed, command)) {
+ ecex_warn_keybind_issue("global", ed->keybinds[i].key, "targets a missing command");
+ ok = ECEX_ERR;
+ }
+ for (size_t j = i + 1; j < ed->keybind_count; j++) {
+ if (ecex_key_is_prefix_of(ed->keybinds[i].key, ed->keybinds[j].key) ||
+ ecex_key_is_prefix_of(ed->keybinds[j].key, ed->keybinds[i].key)) {
+ ecex_warn_keybind_issue("global", ed->keybinds[i].key, "has a prefix conflict");
+ }
+ }
+ }
+
+ for (size_t i = 0; i < ed->mode_keybind_count; i++) {
+ const char *command = ed->mode_keybinds[i].command;
+ const char *mode = ecex_major_mode_name(ed, ed->mode_keybinds[i].mode);
+ if (!ecex_command_exists(ed, command)) {
+ ecex_warn_keybind_issue(mode, ed->mode_keybinds[i].key, "targets a missing command");
+ ok = ECEX_ERR;
+ }
+ for (size_t j = i + 1; j < ed->mode_keybind_count; j++) {
+ if (ed->mode_keybinds[i].mode != ed->mode_keybinds[j].mode) continue;
+ if (ecex_key_is_prefix_of(ed->mode_keybinds[i].key, ed->mode_keybinds[j].key) ||
+ ecex_key_is_prefix_of(ed->mode_keybinds[j].key, ed->mode_keybinds[i].key)) {
+ ecex_warn_keybind_issue(mode, ed->mode_keybinds[i].key, "has a mode prefix conflict");
+ }
+ }
+ }
+
+ return ok;
+}
+
const char *ecex_lookup_key_for_buffer(ecex_t *ed, buffer_t *buffer, const char *key) {
if (!ed || !key) return NULL;
@@ -1243,15 +2694,6 @@ int ecex_interactive_activate_current_line(ecex_t *ed) {
return action->fn(ed, buffer, line, action->payload, action->userdata);
}
-static const char *ecex_basename(const char *path) {
- if (!path || !path[0]) return "untitled";
-
- const char *last_slash = strrchr(path, '/');
- if (last_slash && last_slash[1]) return last_slash + 1;
- if (last_slash && last_slash == path) return "/";
- return path;
-}
-
static int ecex_buffer_path_equal(buffer_t *buffer, const char *path) {
return buffer && buffer->path && path && strcmp(buffer->path, path) == 0;
}
@@ -1259,35 +2701,55 @@ static int ecex_buffer_path_equal(buffer_t *buffer, const char *path) {
int ecex_find_file(ecex_t *ed, const char *path) {
if (!ed || !path || !path[0]) return ECEX_ERR;
+ char *normal_path = ecex_path_normalize(path);
+ if (!normal_path) return ECEX_ERR;
+
+ if (ecex_path_is_dir(normal_path)) {
+ int result = ecex_file_browser_populate(ed, normal_path, 1);
+ free(normal_path);
+ return result;
+ }
+
+ if (ecex_path_is_file(normal_path) && ecex_path_is_media(normal_path)) {
+ int result = ecex_media_open(ed, normal_path);
+ free(normal_path);
+ return result;
+ }
+
for (size_t i = 0; i < ed->buffer_count; i++) {
- if (ecex_buffer_path_equal(ed->buffers[i], path)) {
- return ecex_set_current_buffer_index(ed, i);
+ if (ecex_buffer_path_equal(ed->buffers[i], normal_path)) {
+ int result;
+ free(normal_path);
+ result = ecex_set_current_buffer_index(ed, i);
+ if (result == ECEX_OK && !ecex_buffer_has_renderer(ed->buffers[i])) ecex_run_file_handlers(ed, ed->buffers[i]);
+ return result;
}
}
- const char *name = ecex_basename(path);
+ char *name = ecex_path_basename_dup(normal_path);
+ if (!name) { free(normal_path); return ECEX_ERR; }
buffer_t *buf = ecex_create_buffer(ed, name, NULL, 0);
- if (!buf) return ECEX_ERR;
+ free(name);
+ if (!buf) { free(normal_path); return ECEX_ERR; }
- if (ecex_file_exists(path)) {
- if (buffer_load_file(buf, path) != ECEX_OK) {
- ecex_kill_buffer(ed, buf->name);
+ if (ecex_file_exists(normal_path)) {
+ if (buffer_load_file(buf, normal_path) != ECEX_OK) {
+ ecex_kill_buffer_force(ed, buf->name);
+ free(normal_path);
return ECEX_ERR;
}
} else {
- char *new_path = ecex_strdup(path);
- if (!new_path) {
- ecex_kill_buffer(ed, buf->name);
- return ECEX_ERR;
- }
-
free(buf->path);
- buf->path = new_path;
+ buf->path = normal_path;
+ normal_path = NULL;
buf->modified = 0;
}
+ free(normal_path);
ecex_auto_set_major_mode(ed, buf);
- return ecex_set_current_buffer_index(ed, ed->buffer_count ? ed->buffer_count - 1 : 0);
+ int switch_result = ecex_set_current_buffer_index(ed, ed->buffer_count ? ed->buffer_count - 1 : 0);
+ if (switch_result == ECEX_OK) ecex_run_file_handlers(ed, buf);
+ return switch_result;
}
int ecex_save_current_buffer(ecex_t *ed) {
@@ -1299,7 +2761,10 @@ int ecex_save_current_buffer(ecex_t *ed) {
int ecex_write_current_buffer(ecex_t *ed, const char *path) {
buffer_t *buf = ecex_current_buffer(ed);
if (!buf || !path || !path[0]) return ECEX_ERR;
- int result = buffer_save_as(buf, path);
+ char *normal_path = ecex_path_normalize(path);
+ if (!normal_path) return ECEX_ERR;
+ int result = buffer_save_as(buf, normal_path);
+ free(normal_path);
if (result == ECEX_OK) ecex_auto_set_major_mode(ed, buf);
return result;
}