aboutsummaryrefslogtreecommitdiff
path: root/src/ecex.c
diff options
context:
space:
mode:
authorDavid Moc <personal@cdatgoose.org>2026-05-30 21:53:05 +0200
committerDavid Moc <personal@cdatgoose.org>2026-05-30 21:53:05 +0200
commite930cc6bdc7f62befac063d7d9d016ffb0a64f1a (patch)
tree52118a1e990ae88f5f0410c8caea129609e22e19 /src/ecex.c
Added the old repo, refactored it, added the C jit.
Diffstat (limited to 'src/ecex.c')
-rw-r--r--src/ecex.c1630
1 files changed, 1630 insertions, 0 deletions
diff --git a/src/ecex.c b/src/ecex.c
new file mode 100644
index 0000000..2f4c962
--- /dev/null
+++ b/src/ecex.c
@@ -0,0 +1,1630 @@
+#include "ecex.h"
+
+#include "ccdjit.h"
+#include "common.h"
+#include "config.h"
+#include "util.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+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);
+
+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;
+ 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); 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_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_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);
+}
+
+static int ecex_register_builtins(ecex_t *ed) {
+ ECEX_COMMAND("quit", cmd_quit);
+ ECEX_COMMAND("find-file", cmd_find_file);
+ 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("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 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 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_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, "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;
+
+ 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->buffer_count; i++) {
+ buffer_free(ed->buffers[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);
+ }
+
+ 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->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_set_current_buffer_index(ecex_t *ed, size_t index) {
+ if (!ed || index >= ed->buffer_count) return ECEX_ERR;
+ ed->current_buffer_index = index;
+ ed->current_buffer = ed->buffers[index];
+ 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;
+
+ ed->current_buffer = win->buffer;
+ for (size_t i = 0; i < ed->buffer_count; i++) {
+ if (ed->buffers[i] == win->buffer) {
+ ed->current_buffer_index = i;
+ return ECEX_OK;
+ }
+ }
+ return ECEX_ERR;
+}
+
+int ecex_switch_buffer(ecex_t *ed, const char *name) {
+ if (!ed || !name) return ECEX_ERR;
+
+ 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;
+}
+
+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);
+}
+
+int ecex_kill_buffer(ecex_t *ed, const char *name) {
+ if (!ed || !name || 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 (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];
+ }
+
+ ed->buffer_count--;
+
+ buffer_t *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_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;
+}
+
+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_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_load_c_config(ed, path);
+ free(path);
+ return result;
+}
+
+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) {
+ 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;
+}
+
+int ecex_bind_key(ecex_t *ed, const char *key, const char *command) {
+ if (!ed || !key || !command) return ECEX_ERR;
+
+ for (size_t i = 0; i < ed->keybind_count; i++) {
+ if (strcmp(ed->keybinds[i].key, key) == 0) {
+ 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;
+
+ 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) {
+ 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;
+}
+
+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 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;
+}
+
+int ecex_find_file(ecex_t *ed, const char *path) {
+ if (!ed || !path || !path[0]) return ECEX_ERR;
+
+ 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);
+ }
+ }
+
+ const char *name = ecex_basename(path);
+ buffer_t *buf = ecex_create_buffer(ed, name, NULL, 0);
+ if (!buf) return ECEX_ERR;
+
+ if (ecex_file_exists(path)) {
+ if (buffer_load_file(buf, path) != ECEX_OK) {
+ ecex_kill_buffer(ed, buf->name);
+ 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->modified = 0;
+ }
+
+ ecex_auto_set_major_mode(ed, buf);
+ return ecex_set_current_buffer_index(ed, ed->buffer_count ? ed->buffer_count - 1 : 0);
+}
+
+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;
+ int result = buffer_save_as(buf, 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; }