From 6aeaa171dc1ca43392f53cbd02097f76e1b1c5a0 Mon Sep 17 00:00:00 2001 From: David Moc Date: Sun, 31 May 2026 03:47:04 +0200 Subject: Hardened API, tetris, MD-View --- src/app.c | 511 +++++++++++------ src/buffers.c | 320 +++++++++++ src/completion.c | 97 ++++ src/config.c | 115 +++- src/ecex.c | 1675 ++++++++++++++++++++++++++++++++++++++++++++++++++---- src/eval.c | 2 +- src/main.c | 8 +- src/media.c | 378 ++++++++++++ src/path.c | 208 +++++++ src/render.c | 813 +++++++++++++++++++++++++- 10 files changed, 3836 insertions(+), 291 deletions(-) create mode 100644 src/completion.c create mode 100644 src/media.c create mode 100644 src/path.c (limited to 'src') diff --git a/src/app.c b/src/app.c index 3ae368c..9a38e40 100644 --- a/src/app.c +++ b/src/app.c @@ -1,5 +1,7 @@ #include "app.h" #include "common.h" +#include "completion.h" +#include "path.h" #include #include @@ -60,140 +62,12 @@ static void app_set_prompt_input(app_t *app, const char *text) { app->dirty = 1; } -static char *app_expand_user_path(const char *path) { - if (!path) return NULL; - - if (path[0] != '~' || (path[1] != '\0' && path[1] != '/')) { - char *copy = malloc(strlen(path) + 1); - if (!copy) return NULL; - strcpy(copy, path); - return copy; - } - - const char *home = getenv("HOME"); - if (!home || !home[0]) { - char *copy = malloc(strlen(path) + 1); - if (!copy) return NULL; - strcpy(copy, path); - return copy; - } - - size_t home_len = strlen(home); - size_t rest_len = strlen(path + 1); - - char *expanded = malloc(home_len + rest_len + 1); - if (!expanded) return NULL; - - memcpy(expanded, home, home_len); - memcpy(expanded + home_len, path + 1, rest_len + 1); - return expanded; -} - typedef struct command_candidate { const char *name; int score; size_t order; } command_candidate_t; -static int ascii_lower(int c) { - if (c >= 'A' && c <= 'Z') return c - 'A' + 'a'; - return c; -} - -static int ascii_strncasecmp_local(const char *a, const char *b, size_t n) { - for (size_t i = 0; i < n; i++) { - int ac = ascii_lower((unsigned char)a[i]); - int bc = ascii_lower((unsigned char)b[i]); - - if (ac != bc || ac == '\0' || bc == '\0') { - return ac - bc; - } - } - - return 0; -} - -static int ascii_contains_ci(const char *haystack, const char *needle) { - if (!haystack || !needle) return 0; - if (needle[0] == '\0') return 1; - - size_t needle_len = strlen(needle); - - for (size_t i = 0; haystack[i]; i++) { - if (ascii_strncasecmp_local(haystack + i, needle, needle_len) == 0) { - return 1; - } - } - - return 0; -} - -static int command_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; - } - - if (ascii_contains_ci(candidate, query)) { - score += 75; - } - - return score; -} - static int compare_command_candidates(const void *a, const void *b) { const command_candidate_t *ca = (const command_candidate_t *)a; const command_candidate_t *cb = (const command_candidate_t *)b; @@ -220,7 +94,7 @@ static command_candidate_t *collect_command_candidates(ecex_t *ed, for (size_t i = 0; i < ed->command_count; i++) { const char *name = ed->commands[i].name; - int score = command_fuzzy_score(name, query); + int score = ecex_fuzzy_score(name, query); if (score >= 0) { items[count].name = name; @@ -280,14 +154,14 @@ static int compare_path_candidates(const void *a, const void *b) { const path_candidate_t *pa = (const path_candidate_t *)a; const path_candidate_t *pb = (const path_candidate_t *)b; - if (pa->score != pb->score) { - return pb->score - pa->score; - } - if (pa->is_dir != pb->is_dir) { return pb->is_dir - pa->is_dir; } + if (pa->score != pb->score) { + return pb->score - pa->score; + } + int cmp = strcmp(pa->path, pb->path); if (cmp != 0) return cmp; @@ -323,7 +197,7 @@ static int split_path_query(const char *query, if (display_dir[0] == '\0') { snprintf(fs_dir, fs_dir_size, "."); } else { - char *expanded = app_expand_user_path(display_dir); + char *expanded = ecex_path_expand_user(display_dir); if (!expanded) return -1; snprintf(fs_dir, fs_dir_size, "%s", expanded); @@ -393,7 +267,7 @@ static path_candidate_t *collect_path_candidates(const char *query, continue; } - int score = command_fuzzy_score(name, needle); + int score = ecex_fuzzy_score(name, needle); if (score < 0) { order++; continue; @@ -446,35 +320,151 @@ static path_candidate_t *collect_path_candidates(const char *query, return items; } -static const char *path_candidate_at(const char *query, - size_t index, - size_t *out_count) { - static char candidate[1024]; +typedef ecex_completion_item_t prompt_candidate_t; + +static void free_prompt_candidates(prompt_candidate_t *items, size_t count) { + ecex_completion_items_free(items, count); +} + +static int compare_prompt_candidates(const void *a, const void *b) { + return ecex_completion_item_compare(a, b); +} + +static int prompt_kind_uses_path_completion(ecex_prompt_request_t kind) { + return kind == ECEX_PROMPT_FIND_FILE || + kind == ECEX_PROMPT_WRITE_FILE || + kind == ECEX_PROMPT_EVAL_FILE; +} + +static int prompt_kind_uses_buffer_completion(ecex_prompt_request_t kind) { + return kind == ECEX_PROMPT_SWITCH_BUFFER || + kind == ECEX_PROMPT_KILL_BUFFER || + kind == ECEX_PROMPT_FORCE_KILL_BUFFER; +} + +static prompt_candidate_t *collect_buffer_candidates(ecex_t *ed, + const char *query, + size_t *out_count) { + if (out_count) *out_count = 0; + if (!ed || !query || ed->buffer_count == 0) return NULL; + + prompt_candidate_t *items = calloc(ed->buffer_count, sizeof(*items)); + if (!items) return NULL; size_t count = 0; - path_candidate_t *items = collect_path_candidates(query, &count); - if (!items || count == 0) { - free_path_candidates(items, count); + for (size_t i = 0; i < ed->buffer_count; i++) { + buffer_t *buf = ed->buffers[i]; + if (!buf || !buf->name) continue; + + int score = ecex_fuzzy_score(buf->name, query); + if (score < 0) continue; + + items[count].value = malloc(strlen(buf->name) + 1); + if (!items[count].value) continue; + strcpy(items[count].value, buf->name); + items[count].score = score; + items[count].is_dir = 0; + items[count].order = i; + count++; + } + + if (count == 0) { + free(items); return NULL; } - snprintf(candidate, sizeof(candidate), "%s", items[index % count].path); + qsort(items, count, sizeof(*items), compare_prompt_candidates); + if (out_count) *out_count = count; + return items; +} - if (out_count) { - *out_count = count; +static prompt_candidate_t *collect_prompt_candidates(app_t *app, + const char *query, + size_t *out_count) { + if (out_count) *out_count = 0; + if (!app || !query) return NULL; + + if (prompt_kind_uses_path_completion(app->prompt_kind)) { + size_t path_count = 0; + path_candidate_t *paths = collect_path_candidates(query, &path_count); + if (!paths || path_count == 0) { + free_path_candidates(paths, path_count); + return NULL; + } + + prompt_candidate_t *items = calloc(path_count, sizeof(*items)); + if (!items) { + free_path_candidates(paths, path_count); + return NULL; + } + + size_t count = 0; + for (size_t i = 0; i < path_count; i++) { + items[count].value = malloc(strlen(paths[i].path) + 1); + if (!items[count].value) continue; + strcpy(items[count].value, paths[i].path); + items[count].score = paths[i].score; + items[count].is_dir = paths[i].is_dir; + items[count].order = paths[i].order; + count++; + } + + free_path_candidates(paths, path_count); + if (count == 0) { + free(items); + return NULL; + } + if (out_count) *out_count = count; + return items; + } + + if (prompt_kind_uses_buffer_completion(app->prompt_kind)) { + return collect_buffer_candidates(app->ed, query, out_count); + } + + return NULL; +} + +static const char *prompt_candidate_at(app_t *app, + const char *query, + size_t index, + size_t *out_count) { + static char candidate[1024]; + + size_t count = 0; + prompt_candidate_t *items = collect_prompt_candidates(app, query, &count); + if (!items || count == 0) { + free_prompt_candidates(items, count); + return NULL; } - free_path_candidates(items, count); + snprintf(candidate, sizeof(candidate), "%s", items[index % count].value); + if (out_count) *out_count = count; + + free_prompt_candidates(items, count); return candidate; } +static int app_prompt_input_is_directory(app_t *app) { + if (!app || !prompt_kind_uses_path_completion(app->prompt_kind)) return 0; + if (app->prompt_input_len == 0) return 0; + + char *expanded = ecex_path_expand_user(app->prompt_input); + if (!expanded) return 0; + + struct stat st; + int is_dir = stat(expanded, &st) == 0 && S_ISDIR(st.st_mode); + free(expanded); + return is_dir; +} + static void app_update_prompt_completion_preview(app_t *app) { if (!app || !app->prompt_completion_active) return; size_t count = 0; - path_candidate_t *items = collect_path_candidates(app->prompt_completion_query, &count); + prompt_candidate_t *items = collect_prompt_candidates(app, app->prompt_completion_query, &count); if (!items || count == 0) { - free_path_candidates(items, count); + free_prompt_candidates(items, count); app->prompt_completion_preview_start = 0; app->prompt_completion_preview_count = 0; return; @@ -495,11 +485,11 @@ static void app_update_prompt_completion_preview(app_t *app) { snprintf(app->prompt_completion_preview[i], sizeof(app->prompt_completion_preview[i]), "%s", - items[start + i].path); + items[start + i].value); app->prompt_completion_preview_count++; } - free_path_candidates(items, count); + free_prompt_candidates(items, count); } static void app_begin_prompt_completion(app_t *app, int direction) { @@ -511,12 +501,12 @@ static void app_begin_prompt_completion(app_t *app, int direction) { app->prompt_input); size_t count = 0; - path_candidate_t *items = collect_path_candidates(app->prompt_completion_query, &count); + prompt_candidate_t *items = collect_prompt_candidates(app, app->prompt_completion_query, &count); if (!items || count == 0) { - free_path_candidates(items, count); + free_prompt_candidates(items, count); app_reset_prompt_completion(app); - app_message(app, "No file completions"); + app_message(app, prompt_kind_uses_buffer_completion(app->prompt_kind) ? "No buffer completions" : "No file completions"); app->mode = APP_MODE_PROMPT; return; } @@ -525,8 +515,8 @@ static void app_begin_prompt_completion(app_t *app, int direction) { app->prompt_completion_count = count; app->prompt_completion_index = direction < 0 ? count - 1 : 0; - app_set_prompt_input(app, items[app->prompt_completion_index].path); - free_path_candidates(items, count); + app_set_prompt_input(app, items[app->prompt_completion_index].value); + free_prompt_candidates(items, count); app_update_prompt_completion_preview(app); } @@ -548,9 +538,10 @@ static void app_cycle_prompt_completion(app_t *app, int delta) { } size_t count = 0; - const char *candidate = path_candidate_at(app->prompt_completion_query, - app->prompt_completion_index, - &count); + const char *candidate = prompt_candidate_at(app, + app->prompt_completion_query, + app->prompt_completion_index, + &count); if (!candidate || count == 0) { app_reset_prompt_completion(app); @@ -562,6 +553,18 @@ static void app_cycle_prompt_completion(app_t *app, int delta) { app_update_prompt_completion_preview(app); } +static void app_complete_prompt(app_t *app) { + if (!app) return; + + if (app->prompt_completion_active && app_prompt_input_is_directory(app)) { + app_reset_prompt_completion(app); + app_begin_prompt_completion(app, 1); + return; + } + + app_cycle_prompt_completion(app, 1); +} + static void app_begin_completion(app_t *app, int direction) { if (!app) return; @@ -819,25 +822,47 @@ static void app_cancel_prompt(app_t *app) { app_message(app, "Prompt cancelled"); } +static const char *app_default_prompt_input(app_t *app, ecex_prompt_request_t kind) { + if (!app || !app->ed) return NULL; + + if (kind == ECEX_PROMPT_SWITCH_BUFFER) { + buffer_t *other = ecex_other_buffer(app->ed); + return other ? other->name : NULL; + } + + if (kind == ECEX_PROMPT_KILL_BUFFER || + kind == ECEX_PROMPT_FORCE_KILL_BUFFER) { + buffer_t *current = ecex_current_buffer(app->ed); + return current ? current->name : NULL; + } + + return NULL; +} + static void app_submit_prompt(app_t *app) { if (!app || !app->ed) return; - if (app->prompt_input_len == 0 && app->prompt_kind != ECEX_PROMPT_COMPILE) { + ecex_prompt_request_t kind = app->prompt_kind; + const char *default_input = app_default_prompt_input(app, kind); + + if (app->prompt_input_len == 0 && + kind != ECEX_PROMPT_COMPILE && + (!default_input || default_input[0] == '\0')) { app_cancel_prompt(app); return; } char input[sizeof(app->prompt_input)]; - snprintf(input, sizeof(input), "%s", app->prompt_input); + snprintf(input, sizeof(input), "%s", + app->prompt_input_len > 0 ? app->prompt_input : (default_input ? default_input : "")); - ecex_prompt_request_t kind = app->prompt_kind; app->mode = APP_MODE_EDIT; app->prompt_kind = ECEX_PROMPT_NONE; app->prompt_label[0] = '\0'; app->prompt_input[0] = '\0'; app->prompt_input_len = 0; - char *expanded_input = app_expand_user_path(input); + char *expanded_input = ecex_path_expand_user(input); const char *path = expanded_input ? expanded_input : input; int result = -1; @@ -869,6 +894,11 @@ static void app_submit_prompt(app_t *app) { verb = "Killed"; break; + case ECEX_PROMPT_FORCE_KILL_BUFFER: + result = ecex_kill_buffer_force(app->ed, input); + verb = "Force killed"; + break; + case ECEX_PROMPT_COMPILE: result = ecex_compile(app->ed, input[0] ? input : "make"); verb = "Compiled"; @@ -1049,6 +1079,11 @@ static float app_mono_cell_width(app_t *app) { return w > 1.0f ? w : app->font.size_px * 0.6f; } +static int app_trace_callbacks_enabled(void) { + const char *v = getenv("ECEX_TRACE_CALLBACKS"); + return v && v[0] && v[0] != '0'; +} + static float app_status_height(app_t *app) { float line_h = app->font.line_height > 1.0f ? app->font.line_height : app->font.size_px * 1.2f; float pad_y = app->font.size_px * 0.35f; @@ -1117,15 +1152,151 @@ static void app_move_point_to_pixel(app_t *app, double px, double py) { app->dirty = 1; } +static int app_window_rect_at(app_t *app, + double px, + double py, + size_t *out_index, + float *out_x, + float *out_y, + float *out_w, + float *out_h) { + if (!app || !app->ed || app->ed->window_count == 0) return 0; + float editor_h = (float)app->height - app_status_height(app); + if (app_minibuffer_visible(app)) editor_h -= app_minibuffer_height(app); + if (editor_h < 1.0f) editor_h = 1.0f; + for (size_t i = 0; i < app->ed->window_count; i++) { + ecex_window_t *w = &app->ed->windows[i]; + float x = w->x * (float)app->width; + float y = w->y * editor_h; + float ww = w->w * (float)app->width; + float hh = w->h * editor_h; + if (px >= x && px < x + ww && py >= y && py < y + hh) { + if (out_index) *out_index = i; + if (out_x) *out_x = x; + if (out_y) *out_y = y; + if (out_w) *out_w = ww; + if (out_h) *out_h = hh; + return 1; + } + } + return 0; +} + +static int app_dispatch_buffer_mouse(app_t *app, int event, double px, double py, int button) { + if (!app || !app->ed) return 0; + + buffer_t *buf = NULL; + float wx = 0.0f; + float wy = 0.0f; + float ww = 0.0f; + float wh = 0.0f; + + if (app->mouse_capture_active && app->mouse_capture_buffer) { + buf = app->mouse_capture_buffer; + for (size_t i = 0; i < app->ed->window_count; i++) { + if (app->ed->windows[i].buffer != buf) continue; + float editor_h = (float)app->height - app_status_height(app); + if (app_minibuffer_visible(app)) editor_h -= app_minibuffer_height(app); + if (editor_h < 1.0f) editor_h = 1.0f; + wx = app->ed->windows[i].x * (float)app->width; + wy = app->ed->windows[i].y * editor_h; + ww = app->ed->windows[i].w * (float)app->width; + wh = app->ed->windows[i].h * editor_h; + break; + } + } else { + size_t wi = 0; + if (!app_window_rect_at(app, px, py, &wi, &wx, &wy, &ww, &wh)) return 0; + if (wi >= app->ed->window_count) return 0; + buf = app->ed->windows[wi].buffer; + } + + (void)ww; + (void)wh; + if (!buf || !buf->mouse_fn) return 0; + + int local_x = (int)(px - (double)wx); + int local_y = (int)(py - (double)wy); + if (local_x < -32768) local_x = -32768; + if (local_y < -32768) local_y = -32768; + + if (app_trace_callbacks_enabled()) { + fprintf(stderr, "ecex-log: mouse_callback buffer=%p fn=%p event=%d x=%d y=%d button=%d userdata=%p\n", + (void *)buf, (void *)buf->mouse_fn, event, local_x, local_y, button, buf->mouse_userdata); + fflush(stderr); + } + + int result = buf->mouse_fn(app->ed, buf, event, local_x, local_y, button, buf->mouse_userdata); + if (result) app->dirty = 1; + return result != 0; +} + +static int app_mouse_button_id(int glfw_button) { + if (glfw_button == GLFW_MOUSE_BUTTON_LEFT) return ECEX_MOUSE_BUTTON_LEFT; + if (glfw_button == GLFW_MOUSE_BUTTON_RIGHT) return ECEX_MOUSE_BUTTON_RIGHT; + if (glfw_button == GLFW_MOUSE_BUTTON_MIDDLE) return ECEX_MOUSE_BUTTON_MIDDLE; + return glfw_button; +} + static void mouse_button_callback(GLFWwindow *window, int button, int action, int mods) { (void)mods; - if (button != GLFW_MOUSE_BUTTON_LEFT || action != GLFW_PRESS) return; app_t *app = glfwGetWindowUserPointer(window); if (!app) return; glfwGetFramebufferSize(window, &app->width, &app->height); double x = 0.0, y = 0.0; glfwGetCursorPos(window, &x, &y); - app_move_point_to_pixel(app, x, y); + int ebutton = app_mouse_button_id(button); + + if (action == GLFW_PRESS) { + size_t capture_wi = 0; + float capture_wx = 0.0f, capture_wy = 0.0f, capture_ww = 0.0f, capture_wh = 0.0f; + int have_capture_window = app_window_rect_at(app, x, y, &capture_wi, + &capture_wx, &capture_wy, &capture_ww, &capture_wh); + + if (app_dispatch_buffer_mouse(app, ECEX_MOUSE_PRESS, x, y, ebutton)) { + /* Start capture from the window resolved before dispatch. Do not + * re-hit-test after the plugin callback; the callback may mark the + * app dirty or mutate buffer/window state, and a second lookup can + * fail or select a different target. Without capture, GLFW cursor + * motion is delivered as plain ECEX_MOUSE_MOVE events, so rendered + * widgets never receive ECEX_MOUSE_DRAG. */ + if (have_capture_window && capture_wi < app->ed->window_count) { + app->mouse_capture_buffer = app->ed->windows[capture_wi].buffer; + app->mouse_capture_button = ebutton; + app->mouse_capture_active = 1; + if (app_trace_callbacks_enabled()) { + fprintf(stderr, "ecex-log: mouse_capture_start buffer=%p button=%d\n", + (void *)app->mouse_capture_buffer, ebutton); + fflush(stderr); + } + } + return; + } + if (button == GLFW_MOUSE_BUTTON_LEFT) app_move_point_to_pixel(app, x, y); + } else if (action == GLFW_RELEASE) { + app_dispatch_buffer_mouse(app, ECEX_MOUSE_RELEASE, x, y, ebutton); + if (!app->mouse_capture_active || app->mouse_capture_button == ebutton) { + if (app_trace_callbacks_enabled() && app->mouse_capture_active) { + fprintf(stderr, "ecex-log: mouse_capture_end buffer=%p button=%d\n", + (void *)app->mouse_capture_buffer, ebutton); + fflush(stderr); + } + app->mouse_capture_active = 0; + app->mouse_capture_buffer = NULL; + app->mouse_capture_button = 0; + } + } +} + +static void cursor_pos_callback(GLFWwindow *window, double x, double y) { + app_t *app = glfwGetWindowUserPointer(window); + if (!app) return; + glfwGetFramebufferSize(window, &app->width, &app->height); + if (app->mouse_capture_active) { + app_dispatch_buffer_mouse(app, ECEX_MOUSE_DRAG, x, y, app->mouse_capture_button); + } else { + app_dispatch_buffer_mouse(app, ECEX_MOUSE_MOVE, x, y, ECEX_MOUSE_BUTTON_LEFT); + } } static void scroll_callback(GLFWwindow *window, double xoffset, double yoffset) { @@ -1329,6 +1500,7 @@ static void char_callback(GLFWwindow *window, unsigned int codepoint) { if (app->mode == APP_MODE_PROMPT) { if (codepoint >= 32 && codepoint <= 126) { if (app->prompt_input_len + 1 < sizeof(app->prompt_input)) { + app_reset_prompt_completion(app); app->prompt_input[app->prompt_input_len++] = (char)codepoint; app->prompt_input[app->prompt_input_len] = '\0'; app->dirty = 1; @@ -1470,7 +1642,7 @@ static void key_callback(GLFWwindow *window, return; case GLFW_KEY_TAB: - app_cycle_prompt_completion(app, 1); + app_complete_prompt(app); return; case GLFW_KEY_DOWN: @@ -1531,6 +1703,14 @@ static void key_callback(GLFWwindow *window, try_execute_keybind(app, key_name); return; } + + if (key_sequence_has_prefix(app, key_name)) { + if (key_produces_text(key, scancode)) { + app->suppress_next_char = 1; + } + app_enter_prefix(app, key_name); + return; + } } if (key == GLFW_KEY_ESCAPE) { @@ -1574,5 +1754,6 @@ void app_install_callbacks(app_t *app) { glfwSetFramebufferSizeCallback(app->window, framebuffer_size_callback); glfwSetWindowRefreshCallback(app->window, window_refresh_callback); glfwSetMouseButtonCallback(app->window, mouse_button_callback); + glfwSetCursorPosCallback(app->window, cursor_pos_callback); glfwSetScrollCallback(app->window, scroll_callback); } diff --git a/src/buffers.c b/src/buffers.c index e8c1d23..a932712 100644 --- a/src/buffers.c +++ b/src/buffers.c @@ -8,8 +8,15 @@ #include #include +extern int pclose(FILE *stream); + #define BUFFER_INITIAL_CAP 64 +static int ecex_trace_callbacks_enabled(void) { + const char *v = getenv("ECEX_TRACE_CALLBACKS"); + return v && v[0] && v[0] != '0'; +} + static int buffer_is_word_char(char c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || @@ -116,6 +123,12 @@ void buffer_free(buffer_t *buffer) { if (!buffer) return; buffer_clear_interactive_actions(buffer); + ecex_buffer_clear_animation(buffer); + ecex_buffer_clear_mouse_handler(buffer); + ecex_buffer_clear_renderer(buffer); + if (buffer->media_pipe) pclose((FILE *)buffer->media_pipe); + free(buffer->media_path); + free(buffer->media_pixels); buffer_undo_stack_clear(buffer->undo_stack, buffer->undo_count); buffer_undo_stack_clear(buffer->redo_stack, buffer->redo_count); free(buffer->undo_stack); @@ -699,3 +712,310 @@ ecex_interactive_line_action_t *buffer_interactive_action_at_line(buffer_t *buff return NULL; } + +int ecex_buffer_set_renderer(buffer_t *buffer, + ecex_buffer_render_fn fn, + void *userdata, + ecex_buffer_userdata_free_fn free_fn, + int flags) { + if (ecex_trace_callbacks_enabled()) { + fprintf(stderr, "ecex-log: buffer_set_renderer buffer=%p fn=%p userdata=%p free=%p flags=%d\n", + (void *)buffer, (void *)fn, userdata, (void *)free_fn, flags); + fflush(stderr); + } + if (!buffer) return ECEX_ERR; + if (buffer->render_userdata_free && buffer->render_userdata && buffer->render_userdata != userdata) { + buffer->render_userdata_free(buffer->render_userdata); + } + buffer->render_fn = fn; + buffer->render_userdata = userdata; + buffer->render_userdata_free = free_fn; + buffer->render_flags = flags; + return ECEX_OK; +} + +int ecex_buffer_clear_renderer(buffer_t *buffer) { + if (ecex_trace_callbacks_enabled()) { + fprintf(stderr, "ecex-log: buffer_clear_renderer buffer=%p userdata=%p free=%p\n", + (void *)buffer, buffer ? buffer->render_userdata : NULL, + buffer ? (void *)buffer->render_userdata_free : NULL); + fflush(stderr); + } + if (!buffer) return ECEX_ERR; + if (buffer->render_userdata_free && buffer->render_userdata) { + buffer->render_userdata_free(buffer->render_userdata); + } + buffer->render_fn = NULL; + buffer->render_userdata = NULL; + buffer->render_userdata_free = NULL; + buffer->render_flags = 0; + return ECEX_OK; +} + +int ecex_buffer_has_renderer(buffer_t *buffer) { + return buffer && buffer->render_fn; +} + +void *ecex_buffer_renderer_userdata(buffer_t *buffer) { + return buffer ? buffer->render_userdata : NULL; +} + + +int ecex_buffer_set_mouse_handler(buffer_t *buffer, + ecex_buffer_mouse_fn fn, + void *userdata, + ecex_buffer_userdata_free_fn free_fn) { + if (ecex_trace_callbacks_enabled()) { + fprintf(stderr, "ecex-log: buffer_set_mouse_handler buffer=%p fn=%p userdata=%p free=%p\n", + (void *)buffer, (void *)fn, userdata, (void *)free_fn); + fflush(stderr); + } + if (!buffer || !fn) return ECEX_ERR; + if (buffer->mouse_userdata_free && buffer->mouse_userdata && buffer->mouse_userdata != userdata) { + buffer->mouse_userdata_free(buffer->mouse_userdata); + } + buffer->mouse_fn = fn; + buffer->mouse_userdata = userdata; + buffer->mouse_userdata_free = free_fn; + return ECEX_OK; +} + +int ecex_buffer_clear_mouse_handler(buffer_t *buffer) { + if (ecex_trace_callbacks_enabled()) { + fprintf(stderr, "ecex-log: buffer_clear_mouse_handler buffer=%p userdata=%p free=%p\n", + (void *)buffer, buffer ? buffer->mouse_userdata : NULL, + buffer ? (void *)buffer->mouse_userdata_free : NULL); + fflush(stderr); + } + if (!buffer) return ECEX_ERR; + if (buffer->mouse_userdata_free && buffer->mouse_userdata) { + buffer->mouse_userdata_free(buffer->mouse_userdata); + } + buffer->mouse_fn = NULL; + buffer->mouse_userdata = NULL; + buffer->mouse_userdata_free = NULL; + return ECEX_OK; +} + +int ecex_buffer_has_mouse_handler(buffer_t *buffer) { + return buffer && buffer->mouse_fn; +} + +void *ecex_buffer_mouse_userdata(buffer_t *buffer) { + return buffer ? buffer->mouse_userdata : NULL; +} + +int ecex_buffer_set_animation(buffer_t *buffer, + ecex_buffer_tick_fn fn, + void *userdata, + ecex_buffer_userdata_free_fn free_fn, + double fps) { + if (ecex_trace_callbacks_enabled()) { + fprintf(stderr, "ecex-log: buffer_set_animation buffer=%p fn=%p userdata=%p free=%p fps=%.3f\n", + (void *)buffer, (void *)fn, userdata, (void *)free_fn, fps); + fflush(stderr); + } + if (!buffer || !fn) return ECEX_ERR; + + if (buffer->tick_userdata_free && buffer->tick_userdata && buffer->tick_userdata != userdata) { + buffer->tick_userdata_free(buffer->tick_userdata); + } + + if (fps <= 0.0) fps = 60.0; + if (fps > 240.0) fps = 240.0; + + buffer->tick_fn = fn; + buffer->tick_ms_fn = NULL; + buffer->tick_userdata = userdata; + buffer->tick_userdata_free = free_fn; + buffer->tick_interval = 1.0 / fps; + buffer->tick_last_time = 0.0; + buffer->tick_enabled = 1; + buffer->tick_uses_ms = 0; + return ECEX_OK; +} + +int ecex_buffer_set_animation_ms(buffer_t *buffer, + ecex_buffer_tick_ms_fn fn, + void *userdata, + ecex_buffer_userdata_free_fn free_fn, + int fps) { + if (ecex_trace_callbacks_enabled()) { + fprintf(stderr, "ecex-log: buffer_set_animation_ms buffer=%p fn=%p userdata=%p free=%p fps=%d\n", + (void *)buffer, (void *)fn, userdata, (void *)free_fn, fps); + fflush(stderr); + } + if (!buffer || !fn) return ECEX_ERR; + + if (buffer->tick_userdata_free && buffer->tick_userdata && buffer->tick_userdata != userdata) { + buffer->tick_userdata_free(buffer->tick_userdata); + } + + if (fps <= 0) fps = 60; + if (fps > 240) fps = 240; + + buffer->tick_fn = NULL; + buffer->tick_ms_fn = fn; + buffer->tick_userdata = userdata; + buffer->tick_userdata_free = free_fn; + buffer->tick_interval = 1.0 / (double)fps; + buffer->tick_last_time = 0.0; + buffer->tick_enabled = 1; + buffer->tick_uses_ms = 1; + return ECEX_OK; +} + +int ecex_buffer_clear_animation(buffer_t *buffer) { + if (ecex_trace_callbacks_enabled()) { + fprintf(stderr, "ecex-log: buffer_clear_animation buffer=%p userdata=%p free=%p\n", + (void *)buffer, buffer ? buffer->tick_userdata : NULL, + buffer ? (void *)buffer->tick_userdata_free : NULL); + fflush(stderr); + } + if (!buffer) return ECEX_ERR; + if (buffer->tick_userdata_free && buffer->tick_userdata) { + buffer->tick_userdata_free(buffer->tick_userdata); + } + buffer->tick_fn = NULL; + buffer->tick_ms_fn = NULL; + buffer->tick_userdata = NULL; + buffer->tick_userdata_free = NULL; + buffer->tick_interval = 0.0; + buffer->tick_last_time = 0.0; + buffer->tick_enabled = 0; + buffer->tick_uses_ms = 0; + return ECEX_OK; +} + +int ecex_buffer_is_animating(buffer_t *buffer) { + return buffer && buffer->tick_enabled && (buffer->tick_fn || buffer->tick_ms_fn); +} + +void *ecex_buffer_animation_userdata(buffer_t *buffer) { + return buffer ? buffer->tick_userdata : NULL; +} + +int ecex_tick_animations(ecex_t *ed, double now_seconds) { + if (!ed) return 0; + + int dirty = 0; + for (size_t i = 0; i < ed->buffer_count; i++) { + buffer_t *buffer = ed->buffers[i]; + if (!buffer || !buffer->tick_enabled) continue; + if (buffer->tick_uses_ms) { + if (!buffer->tick_ms_fn) { + if (ecex_trace_callbacks_enabled()) { + fprintf(stderr, "ecex-log: animation_tick_skip_ms_null buffer=%p userdata=%p\n", + (void *)buffer, buffer->tick_userdata); + fflush(stderr); + } + buffer->tick_enabled = 0; + continue; + } + } else { + if (!buffer->tick_fn) { + if (ecex_trace_callbacks_enabled()) { + fprintf(stderr, "ecex-log: animation_tick_skip_null buffer=%p userdata=%p\n", + (void *)buffer, buffer->tick_userdata); + fflush(stderr); + } + buffer->tick_enabled = 0; + continue; + } + } + + if (buffer->tick_last_time <= 0.0) { + buffer->tick_last_time = now_seconds; + } + + double interval = buffer->tick_interval > 0.0 ? buffer->tick_interval : (1.0 / 60.0); + if (now_seconds - buffer->tick_last_time + 0.000001 < interval) { + continue; + } + + buffer->tick_last_time = now_seconds; + int tick_dirty = 0; + if (buffer->tick_uses_ms) { + int now_ms = (int)(now_seconds * 1000.0); + if (ecex_trace_callbacks_enabled()) { + fprintf(stderr, "ecex-log: animation_tick_ms_enter buffer=%p fn=%p userdata=%p now_ms=%d\n", + (void *)buffer, (void *)buffer->tick_ms_fn, buffer->tick_userdata, now_ms); + fflush(stderr); + } + tick_dirty = buffer->tick_ms_fn(ed, buffer, now_ms, buffer->tick_userdata); + if (ecex_trace_callbacks_enabled()) { + fprintf(stderr, "ecex-log: animation_tick_ms_leave buffer=%p result=%d\n", + (void *)buffer, tick_dirty); + fflush(stderr); + } + } else { + if (ecex_trace_callbacks_enabled()) { + fprintf(stderr, "ecex-log: animation_tick_enter buffer=%p fn=%p userdata=%p now=%.6f\n", + (void *)buffer, (void *)buffer->tick_fn, buffer->tick_userdata, now_seconds); + fflush(stderr); + } + tick_dirty = buffer->tick_fn(ed, buffer, now_seconds, buffer->tick_userdata); + if (ecex_trace_callbacks_enabled()) { + fprintf(stderr, "ecex-log: animation_tick_leave buffer=%p result=%d\n", + (void *)buffer, tick_dirty); + fflush(stderr); + } + } + if (tick_dirty != 0) { + dirty = 1; + } + } + + return dirty; +} + + +int ecex_buffer_replace_text(buffer_t *buffer, const char *text) { + return buffer_set_text(buffer, text ? text : ""); +} + +void ecex_buffer_set_modified(buffer_t *buffer, int modified) { + if (buffer) buffer->modified = modified ? 1 : 0; +} + +int ecex_buffer_text_len(buffer_t *buffer) { + if (!buffer) return 0; + return buffer->len > (size_t)2147483647 ? 2147483647 : (int)buffer->len; +} + +int ecex_buffer_scroll_line(buffer_t *buffer) { + if (!buffer) return 0; + return buffer->scroll_line > (size_t)2147483647 ? 2147483647 : (int)buffer->scroll_line; +} + +int ecex_buffer_line_count_i(buffer_t *buffer) { + size_t n = buffer_line_count(buffer); + return n > (size_t)2147483647 ? 2147483647 : (int)n; +} + +int ecex_buffer_line_copy(buffer_t *buffer, int line, char *out, int out_cap) { + size_t current = 0; + size_t pos = 0; + size_t start; + size_t end; + size_t n; + + if (!out || out_cap <= 0) return ECEX_ERR; + out[0] = '\0'; + if (!buffer || !buffer->data || line < 0) return ECEX_ERR; + + while (current < (size_t)line && pos < buffer->len) { + if (buffer->data[pos++] == '\n') current++; + } + if (current != (size_t)line) return ECEX_ERR; + + start = pos; + while (pos < buffer->len && buffer->data[pos] != '\n') pos++; + end = pos; + while (end > start && (buffer->data[end - 1] == '\n' || buffer->data[end - 1] == '\r')) end--; + n = end - start; + if (n >= (size_t)out_cap) n = (size_t)out_cap - 1; + if (n > 0) memcpy(out, buffer->data + start, n); + out[n] = '\0'; + return (int)n; +} diff --git a/src/completion.c b/src/completion.c new file mode 100644 index 0000000..6f19530 --- /dev/null +++ b/src/completion.c @@ -0,0 +1,97 @@ +#include "completion.h" + +#include +#include + +static int ecex_ascii_lower(int c) { + if (c >= 'A' && c <= 'Z') return c - 'A' + 'a'; + return c; +} + +int ecex_ascii_strncasecmp(const char *a, const char *b, size_t n) { + if (!a || !b) return a == b ? 0 : (a ? 1 : -1); + + for (size_t i = 0; i < n; i++) { + int ac = ecex_ascii_lower((unsigned char)a[i]); + int bc = ecex_ascii_lower((unsigned char)b[i]); + + if (ac != bc || ac == '\0' || bc == '\0') return ac - bc; + } + + return 0; +} + +int ecex_ascii_contains_ci(const char *haystack, const char *needle) { + if (!haystack || !needle) return 0; + if (needle[0] == '\0') return 1; + + size_t needle_len = strlen(needle); + for (size_t i = 0; haystack[i]; i++) { + if (ecex_ascii_strncasecmp(haystack + i, needle, needle_len) == 0) return 1; + } + + return 0; +} + +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; + if (ecex_ascii_contains_ci(candidate, query)) score += 75; + return score; +} + +int ecex_completion_item_compare(const void *a, const void *b) { + const ecex_completion_item_t *pa = (const ecex_completion_item_t *)a; + const ecex_completion_item_t *pb = (const ecex_completion_item_t *)b; + + if (pa->score != pb->score) return pb->score - pa->score; + if (pa->is_dir != pb->is_dir) return pb->is_dir - pa->is_dir; + + int cmp = strcmp(pa->value ? pa->value : "", pb->value ? pb->value : ""); + if (cmp != 0) return cmp; + if (pa->order < pb->order) return -1; + if (pa->order > pb->order) return 1; + return 0; +} + +void ecex_completion_items_free(ecex_completion_item_t *items, size_t count) { + if (!items) return; + for (size_t i = 0; i < count; i++) free(items[i].value); + free(items); +} diff --git a/src/config.c b/src/config.c index b416d7a..1b0b5ed 100644 --- a/src/config.c +++ b/src/config.c @@ -6,6 +6,7 @@ #include "ecex.h" #include "eval.h" #include "util.h" +#include "path.h" #include #include @@ -56,7 +57,60 @@ static const host_symbol_t host_symbols[] = { HOST_SYMBOL(ecex_delete_window), HOST_SYMBOL(ecex_delete_other_windows), HOST_SYMBOL(ecex_kill_buffer), - + HOST_SYMBOL(ecex_kill_buffer_force), + HOST_SYMBOL(ecex_has_modified_buffers), + HOST_SYMBOL(ecex_validate_bindings), + HOST_SYMBOL(ecex_config_alloc), + HOST_SYMBOL(ecex_config_calloc), + HOST_SYMBOL(ecex_config_free), + HOST_SYMBOL(ecex_time_seconds), + HOST_SYMBOL(ecex_log), + HOST_SYMBOL(ecex_log_int), + HOST_SYMBOL(ecex_log_double), + HOST_SYMBOL(ecex_log_ptr), + HOST_SYMBOL(ecex_mem_zero), + HOST_SYMBOL(ecex_i32_get), + HOST_SYMBOL(ecex_i32_set), + HOST_SYMBOL(ecex_prng_next_bounded), + HOST_SYMBOL(ecex_random_bounded), + HOST_SYMBOL(ecex_tetris_shape_cell), + HOST_SYMBOL(ecex_var_get), + HOST_SYMBOL(ecex_var_get_or_alloc), + HOST_SYMBOL(ecex_var_bind_static), + HOST_SYMBOL(ecex_var_free), + HOST_SYMBOL(ecex_var_free_owner), + HOST_SYMBOL(ecex_var_i32_get), + HOST_SYMBOL(ecex_var_i32_set), + HOST_SYMBOL(ecex_var_i32), + HOST_SYMBOL(ecex_var_i32_set_scalar), + HOST_SYMBOL(ecex_object_alloc), + HOST_SYMBOL(ecex_object_calloc), + HOST_SYMBOL(ecex_object_free), + HOST_SYMBOL(ecex_object_valid), + HOST_SYMBOL(ecex_object_i32_get), + HOST_SYMBOL(ecex_object_i32_set), + HOST_SYMBOL(ecex_object_ptr_get), + HOST_SYMBOL(ecex_object_ptr_set), + HOST_SYMBOL(ecex_text_set), + HOST_SYMBOL(ecex_text_set_buffer_title), + HOST_SYMBOL(ecex_text_free), + HOST_SYMBOL(ecex_text_free_owner), + HOST_SYMBOL(ecex_buffer_text_len), + HOST_SYMBOL(ecex_buffer_scroll_line), + HOST_SYMBOL(ecex_buffer_line_count_i), + HOST_SYMBOL(ecex_buffer_line_copy), + HOST_SYMBOL(ecex_markdown_draw_line_from_buffer_i), + HOST_SYMBOL(ecex_markdown_body_y_i), + HOST_SYMBOL(ecex_draw_context_height_i), + HOST_SYMBOL(ecex_draw_context_line_height_i), + HOST_SYMBOL(ecex_register_file_handler), + HOST_SYMBOL(ecex_run_file_handlers), + + HOST_SYMBOL(ecex_config_register_commands), + HOST_SYMBOL(ecex_config_bind_keys), + HOST_SYMBOL(ecex_config_bind_mode_keys), + HOST_SYMBOL(ecex_config_define_modes), + HOST_SYMBOL(ecex_apply_theme), HOST_SYMBOL(ecex_register_command), HOST_SYMBOL(ecex_execute_command), HOST_SYMBOL(ecex_bind_key), @@ -76,12 +130,67 @@ static const host_symbol_t host_symbols[] = { HOST_SYMBOL(ecex_create_interactive_buffer), HOST_SYMBOL(ecex_interactive_append_line), HOST_SYMBOL(ecex_interactive_activate_current_line), + HOST_SYMBOL(ecex_buffer_set_renderer), + HOST_SYMBOL(ecex_buffer_clear_renderer), + HOST_SYMBOL(ecex_buffer_has_renderer), + HOST_SYMBOL(ecex_buffer_renderer_userdata), + HOST_SYMBOL(ecex_buffer_set_mouse_handler), + HOST_SYMBOL(ecex_buffer_clear_mouse_handler), + HOST_SYMBOL(ecex_buffer_has_mouse_handler), + HOST_SYMBOL(ecex_buffer_mouse_userdata), + HOST_SYMBOL(ecex_buffer_set_animation), + HOST_SYMBOL(ecex_buffer_set_animation_ms), + HOST_SYMBOL(ecex_buffer_clear_animation), + HOST_SYMBOL(ecex_buffer_is_animating), + HOST_SYMBOL(ecex_buffer_animation_userdata), + HOST_SYMBOL(ecex_tick_animations), + HOST_SYMBOL(ecex_buffer_replace_text), + HOST_SYMBOL(ecex_buffer_set_modified), + HOST_SYMBOL(ecex_draw_set_color), + HOST_SYMBOL(ecex_draw_rect), + HOST_SYMBOL(ecex_draw_rect_outline), + HOST_SYMBOL(ecex_draw_line), + HOST_SYMBOL(ecex_draw_text), + HOST_SYMBOL(ecex_draw_text_aligned), + HOST_SYMBOL(ecex_draw_text_width), + HOST_SYMBOL(ecex_draw_color_rgba8), + HOST_SYMBOL(ecex_draw_rect_i), + HOST_SYMBOL(ecex_draw_rect_outline_i), + HOST_SYMBOL(ecex_draw_line_i), + HOST_SYMBOL(ecex_draw_text_i), + HOST_SYMBOL(ecex_draw_text_id_i), + HOST_SYMBOL(ecex_draw_markdown_canvas_i), + HOST_SYMBOL(ecex_draw_markdown_text_i), + HOST_SYMBOL(ecex_draw_markdown_canvas_auto_i), + HOST_SYMBOL(ecex_draw_markdown_line_auto_i), + HOST_SYMBOL(ecex_draw_label_i), + HOST_SYMBOL(ecex_draw_stat_i), + HOST_SYMBOL(ecex_draw_tetris_preview_i), + HOST_SYMBOL(ecex_draw_rgba), HOST_SYMBOL(ecex_find_file), HOST_SYMBOL(ecex_save_current_buffer), HOST_SYMBOL(ecex_write_current_buffer), HOST_SYMBOL(ecex_request_prompt), HOST_SYMBOL(ecex_clear_prompt_request), HOST_SYMBOL(ecex_complete_command), + + HOST_SYMBOL(ecex_path_copy), + HOST_SYMBOL(ecex_path_expand_user), + HOST_SYMBOL(ecex_path_join), + HOST_SYMBOL(ecex_path_dirname), + HOST_SYMBOL(ecex_path_basename_dup), + HOST_SYMBOL(ecex_path_normalize), + HOST_SYMBOL(ecex_path_is_dir), + HOST_SYMBOL(ecex_path_is_file), + HOST_SYMBOL(ecex_path_exists), + HOST_SYMBOL(ecex_path_file_size), + HOST_SYMBOL(ecex_path_is_image), + HOST_SYMBOL(ecex_path_is_previewable_image), + HOST_SYMBOL(ecex_path_is_video), + HOST_SYMBOL(ecex_path_is_media), + HOST_SYMBOL(ecex_path_cwd), + HOST_SYMBOL(ecex_media_open), + HOST_SYMBOL(ecex_media_toggle_playback), HOST_SYMBOL(ecex_eval_source), HOST_SYMBOL(ecex_eval_current_buffer), HOST_SYMBOL(ecex_eval_current_line), @@ -106,6 +215,10 @@ static const host_symbol_t host_symbols[] = { HOST_SYMBOL(ecex_set_completion_enabled), HOST_SYMBOL(ecex_set_interactive_highlight_bg_color), HOST_SYMBOL(ecex_set_interactive_highlight_fg_color), + HOST_SYMBOL(ecex_set_current_line_bg_color), + HOST_SYMBOL(ecex_set_search_bg_color), + HOST_SYMBOL(ecex_set_line_numbers_enabled), + HOST_SYMBOL(ecex_set_current_line_enabled), HOST_SYMBOL(buffer_clear), HOST_SYMBOL(buffer_set_text), diff --git a/src/ecex.c b/src/ecex.c index 2f4c962..564d05f 100644 --- a/src/ecex.c +++ b/src/ecex.c @@ -1,13 +1,18 @@ #include "ecex.h" +#include "media.h" #include "ccdjit.h" #include "common.h" #include "config.h" #include "util.h" +#include "path.h" #include #include #include +#include +#include +#include extern FILE *popen(const char *command, const char *type); extern int pclose(FILE *stream); @@ -21,6 +26,639 @@ extern int pclose(FILE *stream); ecex_window_t *ecex_current_window(ecex_t *ed); + +void *ecex_config_alloc(size_t size) { + return malloc(size ? size : 1); +} + +void *ecex_config_calloc(size_t count, size_t size) { + if (count == 0) count = 1; + if (size == 0) size = 1; + return calloc(count, size); +} + +void ecex_config_free(void *ptr) { + free(ptr); +} + +double ecex_time_seconds(void) { + struct timeval tv; + gettimeofday(&tv, NULL); + return (double)tv.tv_sec + (double)tv.tv_usec / 1000000.0; +} + +static int ecex_env_enabled(const char *name) { + const char *v; + if (!name) return 0; + v = getenv(name); + return v && v[0] && v[0] != '0'; +} + +static int ecex_log_enabled(void) { + return ecex_env_enabled("ECEX_LOG") || + ecex_env_enabled("ECEX_TRACE_CALLBACKS") || + ecex_env_enabled("ECEX_TRACE_TETRIS"); +} + +void ecex_log(const char *message) { + if (!ecex_log_enabled()) return; + fprintf(stderr, "ecex-log: %s\n", message ? message : "(null)"); + fflush(stderr); +} + +void ecex_log_int(const char *message, int value) { + if (!ecex_log_enabled()) return; + fprintf(stderr, "ecex-log: %s%d\n", message ? message : "", value); + fflush(stderr); +} + +void ecex_log_double(const char *message, double value) { + if (!ecex_log_enabled()) return; + fprintf(stderr, "ecex-log: %s%.6f\n", message ? message : "", value); + fflush(stderr); +} + +void ecex_log_ptr(const char *message, const void *ptr) { + if (!ecex_log_enabled()) return; + fprintf(stderr, "ecex-log: %s%p\n", message ? message : "", ptr); + fflush(stderr); +} + +void ecex_mem_zero(void *ptr, size_t size) { + if (!ptr || size == 0) return; + memset(ptr, 0, size); +} + + +static ecex_object_entry_t *ecex_object_find_entry(ecex_t *ed, void *object) { + if (!ed || !object) return NULL; + for (size_t i = 0; i < ed->object_count; ++i) { + if (ed->objects[i].ptr == object) return &ed->objects[i]; + } + return NULL; +} + +static int ecex_reserve_objects(ecex_t *ed, size_t needed) { + ecex_object_entry_t *new_objects; + size_t new_cap; + if (!ed) return ECEX_ERR; + if (needed <= ed->object_cap) return ECEX_OK; + new_cap = ed->object_cap ? ed->object_cap * 2 : 16; + while (new_cap < needed) new_cap *= 2; + new_objects = realloc(ed->objects, new_cap * sizeof(*new_objects)); + if (!new_objects) return ECEX_ERR; + memset(new_objects + ed->object_cap, 0, (new_cap - ed->object_cap) * sizeof(*new_objects)); + ed->objects = new_objects; + ed->object_cap = new_cap; + return ECEX_OK; +} + +void *ecex_object_alloc(ecex_t *ed, size_t size) { + void *ptr; + if (!ed) return NULL; + if (size == 0) size = 1; + if (ecex_reserve_objects(ed, ed->object_count + 1) != ECEX_OK) return NULL; + ptr = malloc(size); + if (!ptr) return NULL; + ed->objects[ed->object_count].ptr = ptr; + ed->objects[ed->object_count].size = size; + ed->object_count++; + return ptr; +} + +void *ecex_object_calloc(ecex_t *ed, size_t count, size_t size) { + void *ptr; + size_t total; + if (!ed) return NULL; + if (count == 0) count = 1; + if (size == 0) size = 1; + if (count > ((size_t)-1) / size) return NULL; + total = count * size; + ptr = ecex_object_alloc(ed, total); + if (!ptr) return NULL; + memset(ptr, 0, total); + return ptr; +} + +int ecex_object_free(ecex_t *ed, void *object) { + if (!ed || !object) return ECEX_ERR; + for (size_t i = 0; i < ed->object_count; ++i) { + if (ed->objects[i].ptr == object) { + free(ed->objects[i].ptr); + if (i + 1 < ed->object_count) { + memmove(&ed->objects[i], &ed->objects[i + 1], (ed->object_count - i - 1) * sizeof(ed->objects[i])); + } + --ed->object_count; + if (ed->object_count < ed->object_cap) memset(&ed->objects[ed->object_count], 0, sizeof(ed->objects[ed->object_count])); + return ECEX_OK; + } + } + return ECEX_ERR; +} + +int ecex_object_valid(ecex_t *ed, void *object) { + return ecex_object_find_entry(ed, object) ? 1 : 0; +} + +int ecex_object_i32_get(ecex_t *ed, void *object, size_t byte_offset, int fallback) { + ecex_object_entry_t *o = ecex_object_find_entry(ed, object); + int value; + if (!o || !o->ptr || byte_offset > o->size || o->size - byte_offset < sizeof(int)) return fallback; + memcpy(&value, (const char *)o->ptr + byte_offset, sizeof(value)); + return value; +} + +int ecex_object_i32_set(ecex_t *ed, void *object, size_t byte_offset, int value) { + ecex_object_entry_t *o = ecex_object_find_entry(ed, object); + if (!o || !o->ptr || byte_offset > o->size || o->size - byte_offset < sizeof(int)) return ECEX_ERR; + memcpy((char *)o->ptr + byte_offset, &value, sizeof(value)); + return ECEX_OK; +} + +void *ecex_object_ptr_get(ecex_t *ed, void *object, size_t byte_offset) { + ecex_object_entry_t *o = ecex_object_find_entry(ed, object); + void *value = NULL; + if (!o || !o->ptr || byte_offset > o->size || o->size - byte_offset < sizeof(void *)) return NULL; + memcpy(&value, (const char *)o->ptr + byte_offset, sizeof(value)); + return value; +} + +int ecex_object_ptr_set(ecex_t *ed, void *object, size_t byte_offset, void *value) { + ecex_object_entry_t *o = ecex_object_find_entry(ed, object); + if (!o || !o->ptr || byte_offset > o->size || o->size - byte_offset < sizeof(void *)) return ECEX_ERR; + memcpy((char *)o->ptr + byte_offset, &value, sizeof(value)); + return ECEX_OK; +} + +int ecex_i32_get(const int *items, size_t index) { + if (!items) return 0; + return items[index]; +} + +void ecex_i32_set(int *items, size_t index, int value) { + if (!items) return; + items[index] = value; +} + +static char *ecex_var_strdup(const char *s) { + size_t n; + char *out; + if (!s) s = ""; + n = strlen(s) + 1; + out = malloc(n); + if (!out) return NULL; + memcpy(out, s, n); + return out; +} + +static int ecex_var_name_eq(const char *a, const char *b) { + if (!a) a = ""; + if (!b) b = ""; + return strcmp(a, b) == 0; +} + +static ecex_var_entry_t *ecex_var_find_entry(ecex_t *ed, void *owner, const char *name) { + size_t i; + if (!ed || !name) return NULL; + for (i = 0; i < ed->var_count; ++i) { + ecex_var_entry_t *v = &ed->vars[i]; + if (v->owner == owner && ecex_var_name_eq(v->name, name)) return v; + } + return NULL; +} + +static int ecex_reserve_vars(ecex_t *ed, size_t needed) { + ecex_var_entry_t *new_vars; + size_t new_cap; + if (!ed) return ECEX_ERR; + if (needed <= ed->var_cap) return ECEX_OK; + new_cap = ed->var_cap ? ed->var_cap * 2 : 16; + while (new_cap < needed) new_cap *= 2; + new_vars = realloc(ed->vars, new_cap * sizeof(*new_vars)); + if (!new_vars) return ECEX_ERR; + memset(new_vars + ed->var_cap, 0, (new_cap - ed->var_cap) * sizeof(*new_vars)); + ed->vars = new_vars; + ed->var_cap = new_cap; + return ECEX_OK; +} + +void *ecex_var_get(ecex_t *ed, void *owner, const char *name) { + ecex_var_entry_t *v = ecex_var_find_entry(ed, owner, name); + return v ? v->data : NULL; +} + +void *ecex_var_get_or_alloc(ecex_t *ed, void *owner, const char *name, size_t count, size_t elem_size) { + ecex_var_entry_t *v; + void *data; + if (!ed || !name) return NULL; + if (count == 0) count = 1; + if (elem_size == 0) elem_size = 1; + + v = ecex_var_find_entry(ed, owner, name); + if (v) { + if (v->elem_size != elem_size) { + ecex_log("ecex_var_get_or_alloc: existing slot element-size mismatch"); + return NULL; + } + if (v->count < count) { + void *new_data; + if (!v->dynamic) { + ecex_log("ecex_var_get_or_alloc: static slot too small"); + return NULL; + } + new_data = realloc(v->data, count * elem_size); + if (!new_data) return NULL; + memset((char *)new_data + v->count * elem_size, 0, (count - v->count) * elem_size); + v->data = new_data; + v->count = count; + } + return v->data; + } + + if (ecex_reserve_vars(ed, ed->var_count + 1) != ECEX_OK) return NULL; + data = calloc(count, elem_size); + if (!data) return NULL; + + v = &ed->vars[ed->var_count++]; + memset(v, 0, sizeof(*v)); + v->owner = owner; + v->name = ecex_var_strdup(name); + if (!v->name) { + free(data); + --ed->var_count; + return NULL; + } + v->data = data; + v->elem_size = elem_size; + v->count = count; + v->kind = (elem_size == sizeof(int)) ? ECEX_VAR_I32 : ECEX_VAR_BYTES; + v->dynamic = 1; + return data; +} + +int ecex_var_bind_static(ecex_t *ed, void *owner, const char *name, void *data, size_t count, size_t elem_size) { + ecex_var_entry_t *v; + if (!ed || !name || !data) return ECEX_ERR; + if (count == 0) count = 1; + if (elem_size == 0) elem_size = 1; + + v = ecex_var_find_entry(ed, owner, name); + if (!v) { + if (ecex_reserve_vars(ed, ed->var_count + 1) != ECEX_OK) return ECEX_ERR; + v = &ed->vars[ed->var_count++]; + memset(v, 0, sizeof(*v)); + v->name = ecex_var_strdup(name); + if (!v->name) { + --ed->var_count; + return ECEX_ERR; + } + } else if (v->dynamic) { + free(v->data); + } + + v->owner = owner; + v->data = data; + v->elem_size = elem_size; + v->count = count; + v->kind = (elem_size == sizeof(int)) ? ECEX_VAR_I32 : ECEX_VAR_BYTES; + v->dynamic = 0; + return ECEX_OK; +} + +static void ecex_var_entry_clear(ecex_var_entry_t *v) { + if (!v) return; + if (v->dynamic) free(v->data); + free(v->name); + memset(v, 0, sizeof(*v)); +} + +int ecex_var_free(ecex_t *ed, void *owner, const char *name) { + size_t i; + if (!ed || !name) return ECEX_ERR; + for (i = 0; i < ed->var_count; ++i) { + if (ed->vars[i].owner == owner && ecex_var_name_eq(ed->vars[i].name, name)) { + ecex_var_entry_clear(&ed->vars[i]); + if (i + 1 < ed->var_count) { + memmove(&ed->vars[i], &ed->vars[i + 1], (ed->var_count - i - 1) * sizeof(ed->vars[i])); + } + --ed->var_count; + if (ed->var_count < ed->var_cap) memset(&ed->vars[ed->var_count], 0, sizeof(ed->vars[ed->var_count])); + return ECEX_OK; + } + } + return ECEX_ERR; +} + +int ecex_var_free_owner(ecex_t *ed, void *owner) { + size_t i; + if (!ed) return ECEX_ERR; + i = 0; + while (i < ed->var_count) { + if (ed->vars[i].owner == owner) { + ecex_var_entry_clear(&ed->vars[i]); + if (i + 1 < ed->var_count) { + memmove(&ed->vars[i], &ed->vars[i + 1], (ed->var_count - i - 1) * sizeof(ed->vars[i])); + } + --ed->var_count; + if (ed->var_count < ed->var_cap) memset(&ed->vars[ed->var_count], 0, sizeof(ed->vars[ed->var_count])); + continue; + } + ++i; + } + return ECEX_OK; +} + +int ecex_var_i32_get(ecex_t *ed, void *owner, const char *name, size_t index, int fallback) { + ecex_var_entry_t *v = ecex_var_find_entry(ed, owner, name); + if (!v || !v->data || v->elem_size != sizeof(int) || index >= v->count) return fallback; + return ((int *)v->data)[index]; +} + +int ecex_var_i32_set(ecex_t *ed, void *owner, const char *name, size_t index, int value) { + int *data; + if (!ed || !name) return ECEX_ERR; + data = (int *)ecex_var_get_or_alloc(ed, owner, name, index + 1, sizeof(int)); + if (!data) return ECEX_ERR; + data[index] = value; + return ECEX_OK; +} + +int ecex_var_i32(ecex_t *ed, void *owner, const char *name, int fallback) { + return ecex_var_i32_get(ed, owner, name, 0, fallback); +} + +int ecex_var_i32_set_scalar(ecex_t *ed, void *owner, const char *name, int value) { + return ecex_var_i32_set(ed, owner, name, 0, value); +} + + + +static ecex_text_entry_t *ecex_text_find_entry(ecex_t *ed, void *owner, int id) { + if (!ed) return NULL; + for (size_t i = 0; i < ed->text_count; ++i) { + ecex_text_entry_t *t = &ed->texts[i]; + if (t->owner == owner && t->id == id) return t; + } + return NULL; +} + +static int ecex_reserve_texts(ecex_t *ed, size_t needed) { + ecex_text_entry_t *new_texts; + size_t new_cap; + if (!ed) return ECEX_ERR; + if (needed <= ed->text_cap) return ECEX_OK; + new_cap = ed->text_cap ? ed->text_cap * 2 : 32; + while (new_cap < needed) new_cap *= 2; + new_texts = realloc(ed->texts, new_cap * sizeof(*new_texts)); + if (!new_texts) return ECEX_ERR; + memset(new_texts + ed->text_cap, 0, (new_cap - ed->text_cap) * sizeof(*new_texts)); + ed->texts = new_texts; + ed->text_cap = new_cap; + return ECEX_OK; +} + +static void ecex_text_entry_clear(ecex_text_entry_t *t) { + if (!t) return; + free(t->text); + memset(t, 0, sizeof(*t)); +} + +int ecex_text_set(ecex_t *ed, void *owner, int id, const char *text, int len) { + ecex_text_entry_t *t; + char *copy; + size_t n; + if (!ed || id < 0) return ECEX_ERR; + if (!text) text = ""; + if (len < 0) n = strlen(text); + else n = (size_t)len; + copy = malloc(n + 1); + if (!copy) return ECEX_ERR; + if (n) memcpy(copy, text, n); + copy[n] = '\0'; + + t = ecex_text_find_entry(ed, owner, id); + if (!t) { + if (ecex_reserve_texts(ed, ed->text_count + 1) != ECEX_OK) { + free(copy); + return ECEX_ERR; + } + t = &ed->texts[ed->text_count++]; + memset(t, 0, sizeof(*t)); + t->owner = owner; + t->id = id; + } + free(t->text); + t->text = copy; + t->len = n; + return ECEX_OK; +} + +int ecex_text_set_buffer_title(ecex_t *ed, void *owner, int id, buffer_t *buffer) { + const char *title = NULL; + if (buffer) title = buffer->path ? buffer->path : buffer->name; + return ecex_text_set(ed, owner, id, title ? title : "(unnamed)", -1); +} + +int ecex_text_free(ecex_t *ed, void *owner, int id) { + if (!ed) return ECEX_ERR; + for (size_t i = 0; i < ed->text_count; ++i) { + if (ed->texts[i].owner == owner && ed->texts[i].id == id) { + ecex_text_entry_clear(&ed->texts[i]); + if (i + 1 < ed->text_count) { + memmove(&ed->texts[i], &ed->texts[i + 1], (ed->text_count - i - 1) * sizeof(ed->texts[i])); + } + --ed->text_count; + if (ed->text_count < ed->text_cap) memset(&ed->texts[ed->text_count], 0, sizeof(ed->texts[ed->text_count])); + return ECEX_OK; + } + } + return ECEX_ERR; +} + +int ecex_text_free_owner(ecex_t *ed, void *owner) { + if (!ed) return ECEX_ERR; + for (size_t i = 0; i < ed->text_count;) { + if (ed->texts[i].owner == owner) { + ecex_text_entry_clear(&ed->texts[i]); + if (i + 1 < ed->text_count) { + memmove(&ed->texts[i], &ed->texts[i + 1], (ed->text_count - i - 1) * sizeof(ed->texts[i])); + } + --ed->text_count; + if (ed->text_count < ed->text_cap) memset(&ed->texts[ed->text_count], 0, sizeof(ed->texts[ed->text_count])); + continue; + } + ++i; + } + return ECEX_OK; +} + +const char *ecex_text_get_for_draw(ecex_t *ed, void *owner, int id) { + ecex_text_entry_t *t = ecex_text_find_entry(ed, owner, id); + return t && t->text ? t->text : ""; +} + +static int ecex_ascii_equal_ci(const char *a, const char *b) { + unsigned char ca; + unsigned char cb; + if (!a || !b) return 0; + while (*a && *b) { + ca = (unsigned char)*a; + cb = (unsigned char)*b; + if (ca >= 'A' && ca <= 'Z') ca = (unsigned char)(ca - 'A' + 'a'); + if (cb >= 'A' && cb <= 'Z') cb = (unsigned char)(cb - 'A' + 'a'); + if (ca != cb) return 0; + ++a; + ++b; + } + return *a == '\0' && *b == '\0'; +} + +static const char *ecex_path_extension(const char *path) { + const char *dot; + if (!path) return NULL; + dot = strrchr(path, '.'); + return dot && dot[0] ? dot : NULL; +} + +static int ecex_reserve_file_handlers(ecex_t *ed, size_t needed) { + ecex_file_handler_t *new_handlers; + size_t new_cap; + if (!ed) return ECEX_ERR; + if (needed <= ed->file_handler_cap) return ECEX_OK; + new_cap = ed->file_handler_cap ? ed->file_handler_cap * 2 : 8; + while (new_cap < needed) new_cap *= 2; + new_handlers = realloc(ed->file_handlers, new_cap * sizeof(*new_handlers)); + if (!new_handlers) return ECEX_ERR; + memset(new_handlers + ed->file_handler_cap, 0, (new_cap - ed->file_handler_cap) * sizeof(*new_handlers)); + ed->file_handlers = new_handlers; + ed->file_handler_cap = new_cap; + return ECEX_OK; +} + +int ecex_register_file_handler(ecex_t *ed, const char *extension, ecex_file_handler_fn fn) { + char *copy; + if (!ed || !extension || !extension[0] || !fn) return ECEX_ERR; + for (size_t i = 0; i < ed->file_handler_count; ++i) { + if (ecex_ascii_equal_ci(ed->file_handlers[i].extension, extension)) { + ed->file_handlers[i].fn = fn; + return ECEX_OK; + } + } + if (ecex_reserve_file_handlers(ed, ed->file_handler_count + 1) != ECEX_OK) return ECEX_ERR; + copy = ecex_var_strdup(extension); + if (!copy) return ECEX_ERR; + ed->file_handlers[ed->file_handler_count].extension = copy; + ed->file_handlers[ed->file_handler_count].fn = fn; + ed->file_handler_count++; + return ECEX_OK; +} + +int ecex_run_file_handlers(ecex_t *ed, buffer_t *buffer) { + const char *ext; + const char *path; + if (!ed || !buffer) return ECEX_ERR; + path = buffer->path ? buffer->path : buffer->name; + ext = ecex_path_extension(path); + if (!ext) return ECEX_OK; + for (size_t i = 0; i < ed->file_handler_count; ++i) { + if (ecex_ascii_equal_ci(ed->file_handlers[i].extension, ext)) { + return ed->file_handlers[i].fn(ed, buffer); + } + } + return ECEX_OK; +} + +int ecex_prng_next_bounded(unsigned int *state, int bound) { + unsigned int x; + unsigned int limit; + + if (!state || bound <= 0) return 0; + + x = *state; + if (x == 0u) x = 0x6d2b79f5u; + + /* Xorshift32. Keep this host-side so CCDJIT plugins do not depend on + * unsigned multiply/divide lowering details for game logic randomness. */ + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + if (x == 0u) x = 0xa5a5a5a5u; + *state = x; + + /* Avoid modulo bias enough for tiny bounds without libc. */ + limit = 0xffffffffu - (0xffffffffu % (unsigned int)bound); + while (x >= limit) { + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + if (x == 0u) x = 0x9e3779b9u; + *state = x; + } + + return (int)(x % (unsigned int)bound); +} + + +int ecex_random_bounded(int bound) { + static unsigned int state = 0x7f4a7c15u; + unsigned int mix; + + if (bound <= 0) return 0; + /* Host-owned PRNG for plugins that should not pass writable state pointers + * through the JIT ABI. Stir in a stack address so separate launches differ + * enough for games/demos without requiring libc time calls from plugins. */ + mix = (unsigned int)(size_t)&bound; + state ^= mix + 0x9e3779b9u + (state << 6) + (state >> 2); + return ecex_prng_next_bounded(&state, bound); +} + +int ecex_tetris_shape_cell(int piece, int rot, int col, int row) { + int p = piece % 7; + int r = rot & 3; + if (p < 0) p += 7; + if (col < 0 || col >= 4 || row < 0 || row >= 4) return 0; + + /* I */ + if (p == 0) { + if ((r & 1) == 0) return row == 1; + return col == 1; + } + /* O */ + if (p == 1) return (row == 1 || row == 2) && (col == 1 || col == 2); + /* T */ + if (p == 2) { + if (r == 0) return (row == 1 && col >= 0 && col <= 2) || (row == 2 && col == 1); + if (r == 1) return (col == 1 && row >= 0 && row <= 2) || (row == 1 && col == 2); + if (r == 2) return (row == 1 && col >= 0 && col <= 2) || (row == 0 && col == 1); + return (col == 1 && row >= 0 && row <= 2) || (row == 1 && col == 0); + } + /* S */ + if (p == 3) { + if ((r & 1) == 0) return (row == 1 && (col == 1 || col == 2)) || (row == 2 && (col == 0 || col == 1)); + return (col == 1 && (row == 0 || row == 1)) || (col == 2 && (row == 1 || row == 2)); + } + /* Z */ + if (p == 4) { + if ((r & 1) == 0) return (row == 1 && (col == 0 || col == 1)) || (row == 2 && (col == 1 || col == 2)); + return (col == 2 && (row == 0 || row == 1)) || (col == 1 && (row == 1 || row == 2)); + } + /* J */ + if (p == 5) { + if (r == 0) return (row == 1 && col >= 0 && col <= 2) || (row == 0 && col == 0); + if (r == 1) return (col == 1 && row >= 0 && row <= 2) || (row == 0 && col == 2); + if (r == 2) return (row == 1 && col >= 0 && col <= 2) || (row == 2 && col == 2); + return (col == 1 && row >= 0 && row <= 2) || (row == 2 && col == 0); + } + /* L */ + if (r == 0) return (row == 1 && col >= 0 && col <= 2) || (row == 0 && col == 2); + if (r == 1) return (col == 1 && row >= 0 && row <= 2) || (row == 2 && col == 2); + if (r == 2) return (row == 1 && col >= 0 && col <= 2) || (row == 2 && col == 0); + return (col == 1 && row >= 0 && row <= 2) || (row == 0 && col == 0); +} + +static int ecex_file_browser_preview_current(ecex_t *ed); +static int ecex_file_browser_update_preview_if_enabled(ecex_t *ed); + static ecex_color_t ecex_color(float r, float g, float b) { ecex_color_t c = {r, g, b}; return c; @@ -71,6 +709,16 @@ static void ecex_theme_set_defaults(ecex_t *ed) { if (!buf) return ECEX_ERR static int cmd_quit(ecex_t *ed) { + if (!ed) return ECEX_ERR; + if (ecex_has_modified_buffers(ed)) { + fprintf(stderr, "ecex: refusing to quit; modified buffers exist. Save or use force-quit.\n"); + return ECEX_ERR; + } + ed->should_quit = 1; + return ECEX_OK; +} + +static int cmd_force_quit(ecex_t *ed) { if (!ed) return ECEX_ERR; ed->should_quit = 1; return ECEX_OK; @@ -88,8 +736,18 @@ static int cmd_balance_windows(ecex_t *ed) { return ecex_balance_windows(ed); } static int cmd_move_left(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); buffer_move_left(buf); return ECEX_OK; } static int cmd_move_right(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); buffer_move_right(buf); return ECEX_OK; } -static int cmd_move_up(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); buffer_move_up(buf); return ECEX_OK; } -static int cmd_move_down(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); buffer_move_down(buf); return ECEX_OK; } +static int cmd_move_up(ecex_t *ed) { + CURRENT_BUFFER_OR_ERR(ed); + buffer_move_up(buf); + ecex_file_browser_update_preview_if_enabled(ed); + return ECEX_OK; +} +static int cmd_move_down(ecex_t *ed) { + CURRENT_BUFFER_OR_ERR(ed); + buffer_move_down(buf); + ecex_file_browser_update_preview_if_enabled(ed); + return ECEX_OK; +} static int cmd_move_word_left(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); buffer_move_word_left(buf); return ECEX_OK; } static int cmd_move_word_right(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); buffer_move_word_right(buf); return ECEX_OK; } static int cmd_beginning_of_line(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); buffer_move_beginning_of_line(buf); return ECEX_OK; } @@ -145,91 +803,484 @@ static int cmd_kill_region(ecex_t *ed) { size_t end = 0; buffer_selection_range(buf, &start, &end); - char *text = buffer_substring(buf, start, end); - if (!text) return ECEX_ERR; + char *text = buffer_substring(buf, start, end); + if (!text) return ECEX_ERR; + + int result = ecex_clipboard_set(ed, text); + free(text); + if (result != ECEX_OK) return result; + + return buffer_delete_selection(buf); +} + +static int cmd_list_commands(ecex_t *ed) { return ecex_list_commands(ed); } +static int cmd_list_buffers(ecex_t *ed) { return ecex_list_buffers(ed); } +static int cmd_switch_buffer(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_SWITCH_BUFFER, "Switch buffer: "); return ECEX_OK; } +static int cmd_kill_buffer_command(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_KILL_BUFFER, "Kill buffer: "); return ECEX_OK; } +static int cmd_force_kill_buffer_command(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_FORCE_KILL_BUFFER, "Force kill buffer: "); return ECEX_OK; } +static int cmd_compile(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_COMPILE, "Compile: "); return ECEX_OK; } +static int cmd_grep(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_GREP, "Grep: "); return ECEX_OK; } +static int cmd_recompile(ecex_t *ed) { return ecex_rerun_compile(ed); } +static int cmd_regrep(ecex_t *ed) { return ecex_rerun_grep(ed); } +static int cmd_next_error(ecex_t *ed) { return ecex_next_interactive_action(ed); } +static int cmd_previous_error(ecex_t *ed) { return ecex_previous_interactive_action(ed); } +static int cmd_comment_region(ecex_t *ed) { return ecex_comment_region(ed); } +static int cmd_uncomment_region(ecex_t *ed) { return ecex_uncomment_region(ed); } +static int cmd_toggle_line_numbers(ecex_t *ed) { if (!ed) return ECEX_ERR; ed->theme.line_numbers_enabled = !ed->theme.line_numbers_enabled; return ECEX_OK; } +static int cmd_toggle_current_line(ecex_t *ed) { if (!ed) return ECEX_ERR; ed->theme.current_line_enabled = !ed->theme.current_line_enabled; return ECEX_OK; } +static int cmd_isearch_forward(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_ISEARCH_FORWARD, "I-search: "); return ECEX_OK; } +static int cmd_isearch_backward(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_ISEARCH_BACKWARD, "I-search backward: "); return ECEX_OK; } + +static int cmd_find_file(ecex_t *ed) { + ecex_request_prompt(ed, ECEX_PROMPT_FIND_FILE, "Find file: "); + return ECEX_OK; +} + +static int cmd_write_file(ecex_t *ed) { + ecex_request_prompt(ed, ECEX_PROMPT_WRITE_FILE, "Write file: "); + return ECEX_OK; +} + +static int cmd_save_buffer(ecex_t *ed) { + if (!ed) return ECEX_ERR; + + buffer_t *buf = ecex_current_buffer(ed); + if (!buf) return ECEX_ERR; + + if (!buf->path) { + ecex_request_prompt(ed, ECEX_PROMPT_WRITE_FILE, "Write file: "); + return ECEX_OK; + } + + return ecex_save_current_buffer(ed); +} + +static int cmd_eval_buffer(ecex_t *ed) { + return ecex_eval_current_buffer(ed); +} + +static int cmd_eval_line(ecex_t *ed) { + return ecex_eval_current_line(ed); +} + +static int cmd_eval_region(ecex_t *ed) { + return ecex_eval_current_region(ed); +} + +static int cmd_eval_file(ecex_t *ed) { + ecex_request_prompt(ed, ECEX_PROMPT_EVAL_FILE, "Eval file: "); + return ECEX_OK; +} + +static int cmd_eval_rerun_last(ecex_t *ed) { + return ecex_eval_rerun_last(ed); +} + +static int cmd_quit_window(ecex_t *ed) { + if (!ed) return ECEX_ERR; + if (ecex_window_count(ed) > 1) return ecex_delete_window(ed); + return ecex_previous_buffer(ed); +} + +static int cmd_reload_config(ecex_t *ed) { + return ecex_reload_config(ed); +} + + +/* Built-in file browser -------------------------------------------------- */ + +typedef struct ecex_file_entry { + char *name; + char *path; + int is_dir; + int is_image; + long long size; +} ecex_file_entry_t; + +static char ecex_fb_cwd[4096] = "."; +static char ecex_fb_history[64][4096]; +static size_t ecex_fb_history_count = 0; +static size_t ecex_fb_history_index = 0; +static int ecex_fb_preview_expanded = 0; + +static void ecex_file_entry_free(ecex_file_entry_t *entries, size_t count) { + if (!entries) return; + for (size_t i = 0; i < count; i++) { + free(entries[i].name); + free(entries[i].path); + } + free(entries); +} + +static int ecex_file_entry_compare(const void *a, const void *b) { + const ecex_file_entry_t *ea = (const ecex_file_entry_t *)a; + const ecex_file_entry_t *eb = (const ecex_file_entry_t *)b; + if (ea->is_dir != eb->is_dir) return eb->is_dir - ea->is_dir; + return strcmp(ea->name ? ea->name : "", eb->name ? eb->name : ""); +} + +static int ecex_file_browser_collect(const char *dir, ecex_file_entry_t **out_entries, size_t *out_count) { + if (out_entries) *out_entries = NULL; + if (out_count) *out_count = 0; + if (!dir || !out_entries || !out_count) return ECEX_ERR; + + DIR *d = opendir(dir); + if (!d) return ECEX_ERR; + + size_t cap = 64; + size_t count = 0; + ecex_file_entry_t *entries = calloc(cap, sizeof(*entries)); + if (!entries) { closedir(d); return ECEX_ERR; } + + struct dirent *entry; + while ((entry = readdir(d)) != NULL) { + const char *name = entry->d_name; + if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) continue; + + if (count == cap) { + size_t new_cap = cap * 2; + ecex_file_entry_t *grown = realloc(entries, new_cap * sizeof(*entries)); + if (!grown) break; + memset(grown + cap, 0, (new_cap - cap) * sizeof(*grown)); + entries = grown; + cap = new_cap; + } + + char *path = ecex_path_join(dir, name); + if (!path) continue; + char *name_copy = ecex_strdup(name); + if (!name_copy) { free(path); continue; } + + entries[count].name = name_copy; + entries[count].path = path; + entries[count].is_dir = ecex_path_is_dir(path); + entries[count].is_image = ecex_path_is_image(path); + entries[count].size = ecex_path_file_size(path); + count++; + } + + closedir(d); + qsort(entries, count, sizeof(*entries), ecex_file_entry_compare); + *out_entries = entries; + *out_count = count; + return ECEX_OK; +} + +static void ecex_file_browser_push_history(const char *dir) { + if (!dir || !dir[0]) return; + if (ecex_fb_history_count > 0 && strcmp(ecex_fb_history[ecex_fb_history_index], dir) == 0) return; + + if (ecex_fb_history_index + 1 < ecex_fb_history_count) { + ecex_fb_history_count = ecex_fb_history_index + 1; + } + + if (ecex_fb_history_count == 64) { + memmove(ecex_fb_history, ecex_fb_history + 1, sizeof(ecex_fb_history[0]) * 63); + ecex_fb_history_count = 63; + if (ecex_fb_history_index > 0) ecex_fb_history_index--; + } + + ecex_path_copy(ecex_fb_history[ecex_fb_history_count], sizeof(ecex_fb_history[0]), dir); + ecex_fb_history_index = ecex_fb_history_count; + ecex_fb_history_count++; +} + +static int ecex_file_browser_populate(ecex_t *ed, const char *dir, int push_history); +static int ecex_file_browser_move_to_action(buffer_t *buf, size_t action_index); + +static int ecex_file_browser_open_action(ecex_t *ed, + buffer_t *buffer, + size_t line, + const char *payload, + void *userdata) { + (void)buffer; + (void)line; + (void)userdata; + if (!ed || !payload) return ECEX_ERR; + + char selected[4096]; + ecex_path_copy(selected, sizeof(selected), payload); + + if (ecex_path_is_dir(selected)) return ecex_file_browser_populate(ed, selected, 1); + if (ecex_path_is_file(selected) && ecex_path_is_media(selected)) return ecex_media_open(ed, selected); + if (ecex_path_is_file(selected)) return ecex_find_file(ed, selected); + return ECEX_ERR; +} + +static int ecex_file_browser_populate(ecex_t *ed, const char *dir, int push_history) { + if (!ed) return ECEX_ERR; + + char *normalized = ecex_path_normalize((dir && dir[0]) ? dir : ecex_fb_cwd); + if (!normalized) return ECEX_ERR; + if (!ecex_path_is_dir(normalized)) { + char *parent = ecex_path_dirname(normalized); + free(normalized); + normalized = parent; + if (!normalized) return ECEX_ERR; + } + + ecex_path_copy(ecex_fb_cwd, sizeof(ecex_fb_cwd), normalized); + if (push_history) ecex_file_browser_push_history(ecex_fb_cwd); + + buffer_t *buf = ecex_find_buffer(ed, "*file-browser*"); + if (!buf) buf = ecex_create_interactive_buffer(ed, "*file-browser*"); + if (!buf) { free(normalized); return ECEX_ERR; } + + buffer_clear(buf); + buffer_set_interactive(buf, 1); + ecex_buffer_set_major_mode_by_name(ed, buf, "file-browser-mode"); + + char line[8192]; + snprintf(line, sizeof(line), + "File browser: %s\n\nKeys: RET/l open, h parent, g refresh, b/f history, v preview, m toggle preview, q quit.\n\n", + ecex_fb_cwd); + buffer_append(buf, line); + + char *parent = ecex_path_dirname(ecex_fb_cwd); + if (parent) { + ecex_interactive_append_line(ed, buf, "[..] parent", ecex_file_browser_open_action, parent, NULL); + free(parent); + } + + ecex_file_entry_t *entries = NULL; + size_t count = 0; + if (ecex_file_browser_collect(ecex_fb_cwd, &entries, &count) != ECEX_OK) { + buffer_append(buf, "\nCould not read directory.\n"); + } else { + for (size_t i = 0; i < count; i++) { + const char *tag = entries[i].is_dir ? "[D]" : (entries[i].is_image ? "[I]" : (ecex_path_is_video(entries[i].path) ? "[V]" : " ")); + if (entries[i].is_dir) { + snprintf(line, sizeof(line), "%s %s/", tag, entries[i].name); + } else if (entries[i].size >= 0) { + snprintf(line, sizeof(line), "%s %s (%lld bytes)", tag, entries[i].name, entries[i].size); + } else { + snprintf(line, sizeof(line), "%s %s", tag, entries[i].name); + } + ecex_interactive_append_line(ed, buf, line, ecex_file_browser_open_action, entries[i].path, NULL); + } + } + ecex_file_entry_free(entries, count); + + if (ecex_fb_preview_expanded) { + buffer_append(buf, "\nPreview pane is active: move up/down to update it, v refreshes, m closes it.\n"); + } + + buf->point = 0; + buf->scroll_line = 0; + buf->scroll_col = 0; + if (buf->interactive_action_count > 0) { + /* Start on the first actual directory entry instead of the [..] parent row. + * That makes pressing m/v immediately show a useful preview. */ + ecex_file_browser_move_to_action(buf, buf->interactive_action_count > 1 ? 1 : 0); + } + buf->modified = 0; + free(normalized); + int result = ecex_switch_buffer(ed, "*file-browser*"); + if (result == ECEX_OK && ecex_fb_preview_expanded) { + ecex_file_browser_update_preview_if_enabled(ed); + } + return result; +} + +static int ecex_file_browser_current_payload(ecex_t *ed, char *out, size_t out_size) { + if (!ed || !out || out_size == 0) return ECEX_ERR; + buffer_t *buf = ecex_current_buffer(ed); + if (!buf || !buffer_is_interactive(buf)) return ECEX_ERR; + size_t line = buffer_current_line_number(buf); + if (line > 0) line--; + ecex_interactive_line_action_t *action = buffer_interactive_action_at_line(buf, line); + if ((!action || !action->payload) && buf->interactive_action_count > 0) { + action = &buf->interactive_actions[0]; + } + if (!action || !action->payload) return ECEX_ERR; + return ecex_path_copy(out, out_size, action->payload) == 0 ? ECEX_OK : ECEX_ERR; +} + +static int ecex_file_browser_move_to_action(buffer_t *buf, size_t action_index) { + if (!buf || !buffer_is_interactive(buf) || buf->interactive_action_count == 0) return ECEX_ERR; + if (action_index >= buf->interactive_action_count) action_index = buf->interactive_action_count - 1; + size_t target_line = buf->interactive_actions[action_index].line; + size_t pos = 0; + for (size_t line = 0; line < target_line && pos < buf->len; pos++) { + if (buf->data[pos] == '\n') line++; + } + buffer_set_point(buf, pos); + return ECEX_OK; +} + +static buffer_t *ecex_file_preview_buffer(ecex_t *ed) { + if (!ed) return NULL; + buffer_t *preview = ecex_find_buffer(ed, "*file-preview*"); + if (!preview) preview = ecex_create_buffer(ed, "*file-preview*", NULL, 0); + return preview; +} + +static int ecex_file_browser_show_preview_pane(ecex_t *ed, buffer_t *preview) { + if (!ed || !preview || ed->window_count == 0) return ECEX_ERR; - int result = ecex_clipboard_set(ed, text); - free(text); - if (result != ECEX_OK) return result; + size_t browser_window = ed->current_window_index; + if (ed->window_count == 1) { + if (ecex_split_window_vertically(ed) != ECEX_OK) return ECEX_ERR; + ed->windows[ed->current_window_index].buffer = preview; + ed->current_window_index = browser_window; + return ecex_sync_current_buffer(ed); + } - return buffer_delete_selection(buf); + size_t preview_window = (browser_window + 1) % ed->window_count; + if (preview_window == browser_window && ed->window_count > 1) preview_window = 1; + ed->windows[preview_window].buffer = preview; + ed->current_window_index = browser_window; + return ecex_sync_current_buffer(ed); } -static int cmd_list_commands(ecex_t *ed) { return ecex_list_commands(ed); } -static int cmd_list_buffers(ecex_t *ed) { return ecex_list_buffers(ed); } -static int cmd_switch_buffer(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_SWITCH_BUFFER, "Switch buffer: "); return ECEX_OK; } -static int cmd_kill_buffer_command(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_KILL_BUFFER, "Kill buffer: "); return ECEX_OK; } -static int cmd_compile(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_COMPILE, "Compile: "); return ECEX_OK; } -static int cmd_grep(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_GREP, "Grep: "); return ECEX_OK; } -static int cmd_recompile(ecex_t *ed) { return ecex_rerun_compile(ed); } -static int cmd_regrep(ecex_t *ed) { return ecex_rerun_grep(ed); } -static int cmd_next_error(ecex_t *ed) { return ecex_next_interactive_action(ed); } -static int cmd_previous_error(ecex_t *ed) { return ecex_previous_interactive_action(ed); } -static int cmd_comment_region(ecex_t *ed) { return ecex_comment_region(ed); } -static int cmd_uncomment_region(ecex_t *ed) { return ecex_uncomment_region(ed); } -static int cmd_toggle_line_numbers(ecex_t *ed) { if (!ed) return ECEX_ERR; ed->theme.line_numbers_enabled = !ed->theme.line_numbers_enabled; return ECEX_OK; } -static int cmd_toggle_current_line(ecex_t *ed) { if (!ed) return ECEX_ERR; ed->theme.current_line_enabled = !ed->theme.current_line_enabled; return ECEX_OK; } -static int cmd_isearch_forward(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_ISEARCH_FORWARD, "I-search: "); return ECEX_OK; } -static int cmd_isearch_backward(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_ISEARCH_BACKWARD, "I-search backward: "); return ECEX_OK; } +static int ecex_file_browser_close_preview_pane(ecex_t *ed) { + if (!ed || ed->window_count <= 1) return ECEX_OK; + buffer_t *preview = ecex_find_buffer(ed, "*file-preview*"); + if (!preview) return ECEX_OK; -static int cmd_find_file(ecex_t *ed) { - ecex_request_prompt(ed, ECEX_PROMPT_FIND_FILE, "Find file: "); + for (size_t i = 0; i < ed->window_count; i++) { + if (ed->windows[i].buffer != preview) continue; + memmove(&ed->windows[i], &ed->windows[i + 1], (ed->window_count - i - 1) * sizeof(ed->windows[0])); + ed->window_count--; + if (ed->current_window_index >= ed->window_count) ed->current_window_index = ed->window_count - 1; + ecex_balance_windows(ed); + return ecex_sync_current_buffer(ed); + } return ECEX_OK; } -static int cmd_write_file(ecex_t *ed) { - ecex_request_prompt(ed, ECEX_PROMPT_WRITE_FILE, "Write file: "); +static int ecex_file_browser_fill_preview(ecex_t *ed, buffer_t *preview, const char *path) { + if (!ed || !preview || !path) return ECEX_ERR; + + if (ecex_path_is_media(path)) { + return ecex_media_load_into_buffer(ed, path, preview); + } + + ecex_media_buffer_clear(preview); + ecex_buffer_set_major_mode_by_name(ed, preview, "special-mode"); + preview->read_only = 0; + buffer_clear(preview); + char line[8192]; + snprintf(line, sizeof(line), "Preview: %s\n\n", path); + buffer_append(preview, line); + + if (ecex_path_is_dir(path)) { + buffer_append(preview, "Directory. Press RET/l in *file-browser* to open it.\n"); + } else if (ecex_path_is_file(path)) { + long long size = ecex_path_file_size(path); + snprintf(line, sizeof(line), "File size: %lld bytes.\n\n", size); + buffer_append(preview, line); + FILE *f = fopen(path, "rb"); + if (f) { + char chunk[8193]; + size_t n = fread(chunk, 1, sizeof(chunk) - 1, f); + fclose(f); + chunk[n] = '\0'; + int binary = 0; + for (size_t i = 0; i < n; i++) { + unsigned char c = (unsigned char)chunk[i]; + if (c == 0 || (c < 8) || (c > 13 && c < 32)) { binary = 1; break; } + } + if (binary) buffer_append(preview, "Binary file; text preview suppressed.\n"); + else buffer_append(preview, chunk); + } + } + + preview->modified = 0; + preview->read_only = 1; return ECEX_OK; } -static int cmd_save_buffer(ecex_t *ed) { - if (!ed) return ECEX_ERR; - - buffer_t *buf = ecex_current_buffer(ed); - if (!buf) return ECEX_ERR; +static int ecex_file_browser_preview_current(ecex_t *ed) { + char path[4096]; + if (ecex_file_browser_current_payload(ed, path, sizeof(path)) != ECEX_OK) return ECEX_ERR; - if (!buf->path) { - ecex_request_prompt(ed, ECEX_PROMPT_WRITE_FILE, "Write file: "); - return ECEX_OK; - } + buffer_t *preview = ecex_file_preview_buffer(ed); + if (!preview) return ECEX_ERR; - return ecex_save_current_buffer(ed); + int result = ecex_file_browser_fill_preview(ed, preview, path); + ecex_file_browser_show_preview_pane(ed, preview); + return result; } -static int cmd_eval_buffer(ecex_t *ed) { - return ecex_eval_current_buffer(ed); +static int ecex_file_browser_update_preview_if_enabled(ecex_t *ed) { + if (!ed || !ecex_fb_preview_expanded) return ECEX_OK; + buffer_t *buf = ecex_current_buffer(ed); + const char *mode = buf ? ecex_buffer_major_mode_name(ed, buf) : NULL; + if (!mode || strcmp(mode, "file-browser-mode") != 0) return ECEX_OK; + return ecex_file_browser_preview_current(ed); } -static int cmd_eval_line(ecex_t *ed) { - return ecex_eval_current_line(ed); +static int cmd_file_browser(ecex_t *ed) { + char cwd[4096]; + ecex_path_cwd(cwd, sizeof(cwd)); + return ecex_file_browser_populate(ed, cwd, 1); } -static int cmd_eval_region(ecex_t *ed) { - return ecex_eval_current_region(ed); +static int cmd_file_browser_here(ecex_t *ed) { + buffer_t *buf = ecex_current_buffer(ed); + if (buf && buf->path && buf->path[0]) { + char *dir = ecex_path_dirname(buf->path); + if (!dir) return ECEX_ERR; + int result = ecex_file_browser_populate(ed, dir, 1); + free(dir); + return result; + } + return cmd_file_browser(ed); } -static int cmd_eval_file(ecex_t *ed) { - ecex_request_prompt(ed, ECEX_PROMPT_EVAL_FILE, "Eval file: "); - return ECEX_OK; +static int cmd_file_browser_refresh(ecex_t *ed) { return ecex_file_browser_populate(ed, ecex_fb_cwd, 0); } + +static int cmd_file_browser_parent(ecex_t *ed) { + char *parent = ecex_path_dirname(ecex_fb_cwd); + if (!parent) return ECEX_ERR; + int result = ecex_file_browser_populate(ed, parent, 1); + free(parent); + return result; } -static int cmd_eval_rerun_last(ecex_t *ed) { - return ecex_eval_rerun_last(ed); +static int cmd_file_browser_open(ecex_t *ed) { return ecex_interactive_activate_current_line(ed); } +static int cmd_file_browser_preview(ecex_t *ed) { return ecex_file_browser_preview_current(ed); } + +static int cmd_file_browser_toggle_preview(ecex_t *ed) { + ecex_fb_preview_expanded = !ecex_fb_preview_expanded; + if (!ecex_fb_preview_expanded) { + ecex_file_browser_close_preview_pane(ed); + return ecex_file_browser_populate(ed, ecex_fb_cwd, 0); + } + int result = ecex_file_browser_populate(ed, ecex_fb_cwd, 0); + if (result == ECEX_OK) ecex_file_browser_preview_current(ed); + return result; } -static int cmd_quit_window(ecex_t *ed) { - if (!ed) return ECEX_ERR; - if (ecex_window_count(ed) > 1) return ecex_delete_window(ed); - return ecex_previous_buffer(ed); +static int cmd_file_browser_history_back(ecex_t *ed) { + if (ecex_fb_history_count == 0 || ecex_fb_history_index == 0) return ECEX_ERR; + ecex_fb_history_index--; + return ecex_file_browser_populate(ed, ecex_fb_history[ecex_fb_history_index], 0); } -static int cmd_reload_config(ecex_t *ed) { - return ecex_reload_config(ed); +static int cmd_file_browser_history_forward(ecex_t *ed) { + if (ecex_fb_history_count == 0 || ecex_fb_history_index + 1 >= ecex_fb_history_count) return ECEX_ERR; + ecex_fb_history_index++; + return ecex_file_browser_populate(ed, ecex_fb_history[ecex_fb_history_index], 0); } +static int cmd_media_play_pause(ecex_t *ed) { return ecex_media_toggle_playback(ed); } + static int ecex_register_builtins(ecex_t *ed) { ECEX_COMMAND("quit", cmd_quit); + ECEX_COMMAND("force-quit", cmd_force_quit); ECEX_COMMAND("find-file", cmd_find_file); + ECEX_COMMAND("file-browser", cmd_file_browser); + ECEX_COMMAND("file-browser-here", cmd_file_browser_here); + ECEX_COMMAND("file-browser-refresh", cmd_file_browser_refresh); + ECEX_COMMAND("file-browser-parent", cmd_file_browser_parent); + ECEX_COMMAND("file-browser-open", cmd_file_browser_open); + ECEX_COMMAND("file-browser-preview", cmd_file_browser_preview); + ECEX_COMMAND("file-browser-toggle-preview", cmd_file_browser_toggle_preview); + ECEX_COMMAND("file-browser-history-back", cmd_file_browser_history_back); + ECEX_COMMAND("file-browser-history-forward", cmd_file_browser_history_forward); + ECEX_COMMAND("media-play-pause", cmd_media_play_pause); ECEX_COMMAND("save-buffer", cmd_save_buffer); ECEX_COMMAND("write-file", cmd_write_file); ECEX_COMMAND("eval-buffer", cmd_eval_buffer); @@ -245,6 +1296,7 @@ static int ecex_register_builtins(ecex_t *ed) { ECEX_COMMAND("list-buffers", cmd_list_buffers); ECEX_COMMAND("switch-buffer", cmd_switch_buffer); ECEX_COMMAND("kill-buffer", cmd_kill_buffer_command); + ECEX_COMMAND("force-kill-buffer", cmd_force_kill_buffer_command); ECEX_COMMAND("compile", cmd_compile); ECEX_COMMAND("recompile", cmd_recompile); ECEX_COMMAND("grep", cmd_grep); @@ -320,11 +1372,15 @@ static int ecex_register_builtins(ecex_t *ed) { ECEX_BIND("C-S-z", "redo"); ECEX_BIND("C-x C-f", "find-file"); + ECEX_BIND("C-x f", "find-file"); + ECEX_BIND("C-x d", "file-browser"); + ECEX_BIND("C-x C-d", "file-browser-here"); ECEX_BIND("C-x C-s", "save-buffer"); ECEX_BIND("C-x C-w", "write-file"); ECEX_BIND("C-x C-b", "list-buffers"); ECEX_BIND("C-x b", "switch-buffer"); ECEX_BIND("C-x k", "kill-buffer"); + ECEX_BIND("C-x K", "force-kill-buffer"); ECEX_BIND("C-x 2", "split-window-below"); ECEX_BIND("C-x 3", "split-window-right"); ECEX_BIND("C-x o", "other-window"); @@ -351,10 +1407,26 @@ static int ecex_register_builtins(ecex_t *ed) { ecex_define_major_mode(ed, "c-mode"); ecex_define_major_mode(ed, "eval-output-mode"); ecex_define_major_mode(ed, "special-mode"); + ecex_define_major_mode(ed, "file-browser-mode"); + ecex_define_major_mode(ed, "media-preview-mode"); + ecex_define_major_mode(ed, "markdown-mode"); ecex_bind_mode_key(ed, "eval-output-mode", "g", "eval-rerun-last"); ecex_bind_mode_key(ed, "eval-output-mode", "q", "quit-window"); ecex_bind_mode_key(ed, "eval-output-mode", "r", "eval-rerun-last"); ecex_bind_mode_key(ed, "special-mode", "q", "quit-window"); + + ecex_bind_mode_key(ed, "file-browser-mode", "g", "file-browser-refresh"); + ecex_bind_mode_key(ed, "file-browser-mode", "r", "file-browser-refresh"); + ecex_bind_mode_key(ed, "file-browser-mode", "h", "file-browser-parent"); + ecex_bind_mode_key(ed, "file-browser-mode", "l", "file-browser-open"); + ecex_bind_mode_key(ed, "file-browser-mode", "v", "file-browser-preview"); + ecex_bind_mode_key(ed, "file-browser-mode", "m", "file-browser-toggle-preview"); + ecex_bind_mode_key(ed, "file-browser-mode", "b", "file-browser-history-back"); + ecex_bind_mode_key(ed, "file-browser-mode", "f", "file-browser-history-forward"); + ecex_bind_mode_key(ed, "file-browser-mode", "q", "quit-window"); + ecex_bind_mode_key(ed, "media-preview-mode", "SPC", "media-play-pause"); + ecex_bind_mode_key(ed, "media-preview-mode", "p", "media-play-pause"); + ecex_bind_mode_key(ed, "media-preview-mode", "q", "quit-window"); ecex_bind_mode_key(ed, "special-mode", "g", "recompile"); ecex_bind_mode_key(ed, "special-mode", "n", "next-error"); ecex_bind_mode_key(ed, "special-mode", "p", "previous-error"); @@ -410,14 +1482,18 @@ ecex_t *ecex_new(void) { void ecex_free(ecex_t *ed) { if (!ed) return; - for (size_t i = 0; i < ed->jit_module_count; i++) { - ccdjit_module_free((ccdjit_module *)ed->jit_modules[i]); - } - + /* Buffers may hold renderer/animation callbacks and userdata destructors + * compiled by CCDJIT config modules. Run those destructors while their JIT + * modules are still alive; freeing modules first leaves callback pointers + * dangling and can segfault during buffer teardown. */ for (size_t i = 0; i < ed->buffer_count; i++) { buffer_free(ed->buffers[i]); } + for (size_t i = 0; i < ed->jit_module_count; i++) { + ccdjit_module_free((ccdjit_module *)ed->jit_modules[i]); + } + for (size_t i = 0; i < ed->command_count; i++) { free(ed->commands[i].name); } @@ -436,6 +1512,22 @@ void ecex_free(ecex_t *ed) { free(ed->major_modes[i].name); } + for (size_t i = 0; i < ed->var_count; i++) { + ecex_var_entry_clear(&ed->vars[i]); + } + + for (size_t i = 0; i < ed->text_count; i++) { + ecex_text_entry_clear(&ed->texts[i]); + } + + for (size_t i = 0; i < ed->file_handler_count; i++) { + free(ed->file_handlers[i].extension); + } + + for (size_t i = 0; i < ed->object_count; i++) { + free(ed->objects[i].ptr); + } + free(ed->jit_modules); free(ed->windows); free(ed->buffers); @@ -443,6 +1535,10 @@ void ecex_free(ecex_t *ed) { free(ed->keybinds); free(ed->mode_keybinds); free(ed->major_modes); + free(ed->vars); + free(ed->texts); + free(ed->file_handlers); + free(ed->objects); free(ed->last_eval_source); free(ed->last_eval_filename); free(ed->last_compile_command); @@ -513,10 +1609,27 @@ buffer_t *ecex_find_buffer(ecex_t *ed, const char *name) { } +static int ecex_buffer_index_of(ecex_t *ed, buffer_t *buffer, size_t *out_index) { + if (!ed || !buffer) return ECEX_ERR; + for (size_t i = 0; i < ed->buffer_count; i++) { + if (ed->buffers[i] == buffer) { + if (out_index) *out_index = i; + return ECEX_OK; + } + } + return ECEX_ERR; +} + static int ecex_set_current_buffer_index(ecex_t *ed, size_t index) { if (!ed || index >= ed->buffer_count) return ECEX_ERR; + + buffer_t *next = ed->buffers[index]; + if (ed->current_buffer && ed->current_buffer != next) { + ed->previous_buffer = ed->current_buffer; + } + ed->current_buffer_index = index; - ed->current_buffer = ed->buffers[index]; + ed->current_buffer = next; ecex_window_t *win = ecex_current_window(ed); if (win) win->buffer = ed->current_buffer; return ECEX_OK; @@ -527,9 +1640,14 @@ int ecex_sync_current_buffer(ecex_t *ed) { ecex_window_t *win = ecex_current_window(ed); if (!win || !win->buffer) return ECEX_ERR; - ed->current_buffer = win->buffer; + buffer_t *next = win->buffer; + if (ed->current_buffer && ed->current_buffer != next) { + ed->previous_buffer = ed->current_buffer; + } + + ed->current_buffer = next; for (size_t i = 0; i < ed->buffer_count; i++) { - if (ed->buffers[i] == win->buffer) { + if (ed->buffers[i] == next) { ed->current_buffer_index = i; return ECEX_OK; } @@ -538,7 +1656,14 @@ int ecex_sync_current_buffer(ecex_t *ed) { } int ecex_switch_buffer(ecex_t *ed, const char *name) { - if (!ed || !name) return ECEX_ERR; + if (!ed) return ECEX_ERR; + + if (!name || name[0] == '\0') { + buffer_t *other = ecex_other_buffer(ed); + size_t index = 0; + if (!other || ecex_buffer_index_of(ed, other, &index) != ECEX_OK) return ECEX_ERR; + return ecex_set_current_buffer_index(ed, index); + } for (size_t i = 0; i < ed->buffer_count; i++) { if (strcmp(ed->buffers[i]->name, name) == 0) { @@ -556,6 +1681,22 @@ buffer_t *ecex_current_buffer(ecex_t *ed) { return ed->current_buffer; } +buffer_t *ecex_other_buffer(ecex_t *ed) { + if (!ed || ed->buffer_count == 0) return NULL; + + buffer_t *current = ecex_current_buffer(ed); + if (ed->previous_buffer && ed->previous_buffer != current && + ecex_buffer_index_of(ed, ed->previous_buffer, NULL) == ECEX_OK) { + return ed->previous_buffer; + } + + for (size_t i = 0; i < ed->buffer_count; i++) { + if (ed->buffers[i] && ed->buffers[i] != current) return ed->buffers[i]; + } + + return current; +} + ecex_window_t *ecex_current_window(ecex_t *ed) { if (!ed || ed->window_count == 0 || ed->current_window_index >= ed->window_count) return NULL; return &ed->windows[ed->current_window_index]; @@ -688,32 +1829,40 @@ int ecex_previous_buffer(ecex_t *ed) { return ecex_set_current_buffer_index(ed, ed->current_buffer_index); } -int ecex_kill_buffer(ecex_t *ed, const char *name) { - if (!ed || !name || ed->buffer_count == 0) return ECEX_ERR; +static int ecex_kill_buffer_impl(ecex_t *ed, const char *name, int force) { + if (!ed || ed->buffer_count == 0) return ECEX_ERR; size_t index = ed->buffer_count; - for (size_t i = 0; i < ed->buffer_count; i++) { - if (strcmp(ed->buffers[i]->name, name) == 0) { - index = i; - break; + if (!name || name[0] == '\0') { + buffer_t *current = ecex_current_buffer(ed); + if (!current || ecex_buffer_index_of(ed, current, &index) != ECEX_OK) return ECEX_ERR; + } else { + for (size_t i = 0; i < ed->buffer_count; i++) { + if (strcmp(ed->buffers[i]->name, name) == 0) { + index = i; + break; + } } } if (index == ed->buffer_count) return ECEX_ERR; buffer_t *victim = ed->buffers[index]; - - for (size_t i = index; i + 1 < ed->buffer_count; i++) { - ed->buffers[i] = ed->buffers[i + 1]; + if (ed->previous_buffer == victim) ed->previous_buffer = NULL; + if (!force && victim->modified && !victim->read_only) { + fprintf(stderr, + "ecex: refusing to kill modified buffer '%s'; save it or use force-kill-buffer.\n", + victim->name ? victim->name : ""); + return ECEX_ERR; } + for (size_t i = index; i + 1 < ed->buffer_count; i++) ed->buffers[i] = ed->buffers[i + 1]; ed->buffer_count--; - buffer_t *fallback = ed->buffer_count > 0 ? ed->buffers[0] : NULL; + buffer_t *fallback = ecex_other_buffer(ed); + if (fallback == victim) fallback = ed->buffer_count > 0 ? ed->buffers[0] : NULL; for (size_t i = 0; i < ed->window_count; i++) { - if (ed->windows[i].buffer == victim) { - ed->windows[i].buffer = fallback; - } + if (ed->windows[i].buffer == victim) ed->windows[i].buffer = fallback; } buffer_free(victim); @@ -733,6 +1882,23 @@ int ecex_kill_buffer(ecex_t *ed, const char *name) { return ecex_sync_current_buffer(ed); } +int ecex_kill_buffer(ecex_t *ed, const char *name) { + return ecex_kill_buffer_impl(ed, name, 0); +} + +int ecex_kill_buffer_force(ecex_t *ed, const char *name) { + return ecex_kill_buffer_impl(ed, name, 1); +} + +int ecex_has_modified_buffers(ecex_t *ed) { + if (!ed) return 0; + for (size_t i = 0; i < ed->buffer_count; i++) { + buffer_t *buf = ed->buffers[i]; + if (buf && buf->modified && !buf->read_only) return 1; + } + return 0; +} + int ecex_keep_jit_module(ecex_t *ed, void *module) { if (!ed || !module) return ECEX_ERR; @@ -795,6 +1961,129 @@ static void ecex_clear_mode_keybinds(ecex_t *ed) { ed->mode_keybind_count = 0; } +typedef struct ecex_binding_snapshot { + ecex_command_t *commands; + size_t command_count; + size_t command_cap; + ecex_keybind_t *keybinds; + size_t keybind_count; + size_t keybind_cap; + ecex_mode_keybind_t *mode_keybinds; + size_t mode_keybind_count; + size_t mode_keybind_cap; + ecex_theme_t theme; +} ecex_binding_snapshot_t; + +static void ecex_theme_snapshot_free(ecex_theme_t *theme) { + if (!theme) return; + free(theme->font_path); + theme->font_path = NULL; +} + +static int ecex_theme_clone(ecex_theme_t *out, const ecex_theme_t *in) { + if (!out || !in) return ECEX_ERR; + *out = *in; + out->font_path = NULL; + if (in->font_path) { + out->font_path = ecex_strdup(in->font_path); + if (!out->font_path) return ECEX_ERR; + } + return ECEX_OK; +} + +static void ecex_snapshot_free(ecex_binding_snapshot_t *snap) { + if (!snap) return; + for (size_t i = 0; i < snap->command_count; i++) free(snap->commands[i].name); + for (size_t i = 0; i < snap->keybind_count; i++) { + free(snap->keybinds[i].key); + free(snap->keybinds[i].command); + } + for (size_t i = 0; i < snap->mode_keybind_count; i++) { + free(snap->mode_keybinds[i].key); + free(snap->mode_keybinds[i].command); + } + free(snap->commands); + free(snap->keybinds); + free(snap->mode_keybinds); + ecex_theme_snapshot_free(&snap->theme); + memset(snap, 0, sizeof(*snap)); +} + +static int ecex_snapshot_clone(ecex_binding_snapshot_t *snap, ecex_t *ed) { + if (!snap || !ed) return ECEX_ERR; + memset(snap, 0, sizeof(*snap)); + + if (ecex_theme_clone(&snap->theme, &ed->theme) != ECEX_OK) return ECEX_ERR; + + if (ed->command_cap) { + snap->commands = calloc(ed->command_cap, sizeof(*snap->commands)); + if (!snap->commands) goto fail; + snap->command_cap = ed->command_cap; + snap->command_count = ed->command_count; + for (size_t i = 0; i < ed->command_count; i++) { + snap->commands[i].fn = ed->commands[i].fn; + snap->commands[i].name = ecex_strdup(ed->commands[i].name); + if (!snap->commands[i].name) goto fail; + } + } + + if (ed->keybind_cap) { + snap->keybinds = calloc(ed->keybind_cap, sizeof(*snap->keybinds)); + if (!snap->keybinds) goto fail; + snap->keybind_cap = ed->keybind_cap; + snap->keybind_count = ed->keybind_count; + for (size_t i = 0; i < ed->keybind_count; i++) { + snap->keybinds[i].key = ecex_strdup(ed->keybinds[i].key); + snap->keybinds[i].command = ecex_strdup(ed->keybinds[i].command); + if (!snap->keybinds[i].key || !snap->keybinds[i].command) goto fail; + } + } + + if (ed->mode_keybind_cap) { + snap->mode_keybinds = calloc(ed->mode_keybind_cap, sizeof(*snap->mode_keybinds)); + if (!snap->mode_keybinds) goto fail; + snap->mode_keybind_cap = ed->mode_keybind_cap; + snap->mode_keybind_count = ed->mode_keybind_count; + for (size_t i = 0; i < ed->mode_keybind_count; i++) { + snap->mode_keybinds[i].mode = ed->mode_keybinds[i].mode; + snap->mode_keybinds[i].key = ecex_strdup(ed->mode_keybinds[i].key); + snap->mode_keybinds[i].command = ecex_strdup(ed->mode_keybinds[i].command); + if (!snap->mode_keybinds[i].key || !snap->mode_keybinds[i].command) goto fail; + } + } + + return ECEX_OK; + +fail: + ecex_snapshot_free(snap); + return ECEX_ERR; +} + +static void ecex_restore_snapshot(ecex_t *ed, ecex_binding_snapshot_t *snap) { + if (!ed || !snap) return; + + ecex_clear_commands(ed); + ecex_clear_keybinds(ed); + ecex_clear_mode_keybinds(ed); + free(ed->commands); + free(ed->keybinds); + free(ed->mode_keybinds); + free(ed->theme.font_path); + + ed->commands = snap->commands; + ed->command_count = snap->command_count; + ed->command_cap = snap->command_cap; + ed->keybinds = snap->keybinds; + ed->keybind_count = snap->keybind_count; + ed->keybind_cap = snap->keybind_cap; + ed->mode_keybinds = snap->mode_keybinds; + ed->mode_keybind_count = snap->mode_keybind_count; + ed->mode_keybind_cap = snap->mode_keybind_cap; + ed->theme = snap->theme; + + memset(snap, 0, sizeof(*snap)); +} + int ecex_reload_config(ecex_t *ed) { if (!ed || !ed->config_path || !ed->config_path[0]) { fprintf(stderr, "ecex: no config file to reload; start with --config path/to/ecexrc.c\n"); @@ -804,25 +2093,89 @@ int ecex_reload_config(ecex_t *ed) { char *path = ecex_strdup(ed->config_path); if (!path) return ECEX_ERR; + ecex_binding_snapshot_t snapshot; + if (ecex_snapshot_clone(&snapshot, ed) != ECEX_OK) { + free(path); + fprintf(stderr, "ecex: failed to snapshot config state before reload\n"); + return ECEX_ERR; + } + ecex_clear_commands(ed); ecex_clear_keybinds(ed); ecex_clear_mode_keybinds(ed); - if (ecex_register_builtins(ed) != ECEX_OK) { - free(path); - return ECEX_ERR; + int result = ECEX_ERR; + if (ecex_register_builtins(ed) == ECEX_OK) { + result = ecex_load_c_config(ed, path); + if (result == ECEX_OK) result = ecex_validate_bindings(ed); + } + + if (result != ECEX_OK) { + fprintf(stderr, "ecex: config reload failed; keeping previous config active\n"); + ecex_restore_snapshot(ed, &snapshot); + } else { + ecex_snapshot_free(&snapshot); } - int result = ecex_load_c_config(ed, path); free(path); return result; } + +int ecex_config_register_commands(ecex_t *ed, const ecex_config_command_t *commands, size_t count) { + if (!ed || (!commands && count != 0)) return ECEX_ERR; + for (size_t i = 0; i < count; i++) { + if (!commands[i].name || !commands[i].fn) return ECEX_ERR; + if (ecex_register_command(ed, commands[i].name, commands[i].fn) != ECEX_OK) return ECEX_ERR; + } + return ECEX_OK; +} + +int ecex_config_bind_keys(ecex_t *ed, const ecex_config_keybind_t *bindings, size_t count) { + if (!ed || (!bindings && count != 0)) return ECEX_ERR; + for (size_t i = 0; i < count; i++) { + if (!bindings[i].key || !bindings[i].command) return ECEX_ERR; + if (ecex_bind_key(ed, bindings[i].key, bindings[i].command) != ECEX_OK) return ECEX_ERR; + } + return ECEX_OK; +} + +int ecex_config_bind_mode_keys(ecex_t *ed, const ecex_config_mode_keybind_t *bindings, size_t count) { + if (!ed || (!bindings && count != 0)) return ECEX_ERR; + for (size_t i = 0; i < count; i++) { + if (!bindings[i].mode || !bindings[i].key || !bindings[i].command) return ECEX_ERR; + if (ecex_bind_mode_key(ed, bindings[i].mode, bindings[i].key, bindings[i].command) != ECEX_OK) return ECEX_ERR; + } + return ECEX_OK; +} + +int ecex_config_define_modes(ecex_t *ed, const char *const *modes, size_t count) { + if (!ed || (!modes && count != 0)) return ECEX_ERR; + for (size_t i = 0; i < count; i++) { + if (!modes[i] || !ecex_define_major_mode(ed, modes[i])) return ECEX_ERR; + } + return ECEX_OK; +} + +int ecex_apply_theme(ecex_t *ed, const ecex_theme_t *theme) { + if (!ed || !theme) return ECEX_ERR; + char *font = NULL; + if (theme->font_path) { + font = ecex_strdup(theme->font_path); + if (!font) return ECEX_ERR; + } + free(ed->theme.font_path); + ed->theme = *theme; + ed->theme.font_path = font; + return ECEX_OK; +} + int ecex_register_command(ecex_t *ed, const char *name, ecex_command_fn fn) { if (!ed || !name || !fn) return ECEX_ERR; for (size_t i = 0; i < ed->command_count; i++) { if (strcmp(ed->commands[i].name, name) == 0) { + fprintf(stderr, "ecex: command warning: replacing existing command '%s'\n", name); ed->commands[i].fn = fn; return ECEX_OK; } @@ -877,11 +2230,66 @@ int ecex_clipboard_set(ecex_t *ed, const char *text) { return ECEX_OK; } +static int ecex_command_exists(ecex_t *ed, const char *name) { + if (!ed || !name) return 0; + for (size_t i = 0; i < ed->command_count; i++) { + if (ed->commands[i].name && strcmp(ed->commands[i].name, name) == 0) return 1; + } + return 0; +} + +static int ecex_key_is_prefix_of(const char *prefix, const char *key) { + if (!prefix || !key) return 0; + size_t n = strlen(prefix); + return strncmp(prefix, key, n) == 0 && key[n] == ' '; +} + +static void ecex_warn_keybind_issue(const char *scope, const char *key, const char *detail) { + if (scope && scope[0]) { + fprintf(stderr, + "ecex: keybind warning [%s]: %s%s%s\n", + scope, + key ? key : "", + detail && detail[0] ? " " : "", + detail ? detail : ""); + } else { + fprintf(stderr, + "ecex: keybind warning: %s%s%s\n", + key ? key : "", + detail && detail[0] ? " " : "", + detail ? detail : ""); + } +} + +static void ecex_warn_keybind_conflicts(ecex_t *ed, const char *scope, const char *key, int mode) { + if (!ed || !key) return; + + for (size_t i = 0; i < ed->keybind_count; i++) { + if (mode != 0) break; + const char *existing = ed->keybinds[i].key; + if (existing && ecex_key_is_prefix_of(key, existing)) ecex_warn_keybind_issue(scope, key, "is a prefix of an existing binding"); + if (existing && ecex_key_is_prefix_of(existing, key)) ecex_warn_keybind_issue(scope, key, "extends an existing complete binding"); + } + + for (size_t i = 0; i < ed->mode_keybind_count; i++) { + if (mode != ed->mode_keybinds[i].mode) continue; + const char *existing = ed->mode_keybinds[i].key; + if (existing && ecex_key_is_prefix_of(key, existing)) ecex_warn_keybind_issue(scope, key, "is a prefix of an existing mode binding"); + if (existing && ecex_key_is_prefix_of(existing, key)) ecex_warn_keybind_issue(scope, key, "extends an existing complete mode binding"); + } +} + int ecex_bind_key(ecex_t *ed, const char *key, const char *command) { if (!ed || !key || !command) return ECEX_ERR; + if (!ecex_command_exists(ed, command)) { + ecex_warn_keybind_issue("global", key, "targets a command that is not registered yet"); + } + ecex_warn_keybind_conflicts(ed, "global", key, 0); + for (size_t i = 0; i < ed->keybind_count; i++) { if (strcmp(ed->keybinds[i].key, key) == 0) { + fprintf(stderr, "ecex: keybind warning [global]: replacing %s from %s to %s\n", key, ed->keybinds[i].command, command); char *new_command = ecex_strdup(command); if (!new_command) return ECEX_ERR; @@ -990,8 +2398,14 @@ int ecex_bind_mode_key(ecex_t *ed, const char *mode_name, const char *key, const int mode = ecex_define_major_mode(ed, mode_name); if (!mode) return ECEX_ERR; + if (!ecex_command_exists(ed, command)) { + ecex_warn_keybind_issue(mode_name, key, "targets a command that is not registered yet"); + } + ecex_warn_keybind_conflicts(ed, mode_name, key, mode); + for (size_t i = 0; i < ed->mode_keybind_count; i++) { if (ed->mode_keybinds[i].mode == mode && strcmp(ed->mode_keybinds[i].key, key) == 0) { + fprintf(stderr, "ecex: keybind warning [%s]: replacing %s from %s to %s\n", mode_name, key, ed->mode_keybinds[i].command, command); char *new_command = ecex_strdup(command); if (!new_command) return ECEX_ERR; free(ed->mode_keybinds[i].command); @@ -1022,6 +2436,43 @@ int ecex_bind_mode_key(ecex_t *ed, const char *mode_name, const char *key, const return ECEX_OK; } +int ecex_validate_bindings(ecex_t *ed) { + if (!ed) return ECEX_ERR; + int ok = ECEX_OK; + + for (size_t i = 0; i < ed->keybind_count; i++) { + const char *command = ed->keybinds[i].command; + if (!ecex_command_exists(ed, command)) { + ecex_warn_keybind_issue("global", ed->keybinds[i].key, "targets a missing command"); + ok = ECEX_ERR; + } + for (size_t j = i + 1; j < ed->keybind_count; j++) { + if (ecex_key_is_prefix_of(ed->keybinds[i].key, ed->keybinds[j].key) || + ecex_key_is_prefix_of(ed->keybinds[j].key, ed->keybinds[i].key)) { + ecex_warn_keybind_issue("global", ed->keybinds[i].key, "has a prefix conflict"); + } + } + } + + for (size_t i = 0; i < ed->mode_keybind_count; i++) { + const char *command = ed->mode_keybinds[i].command; + const char *mode = ecex_major_mode_name(ed, ed->mode_keybinds[i].mode); + if (!ecex_command_exists(ed, command)) { + ecex_warn_keybind_issue(mode, ed->mode_keybinds[i].key, "targets a missing command"); + ok = ECEX_ERR; + } + for (size_t j = i + 1; j < ed->mode_keybind_count; j++) { + if (ed->mode_keybinds[i].mode != ed->mode_keybinds[j].mode) continue; + if (ecex_key_is_prefix_of(ed->mode_keybinds[i].key, ed->mode_keybinds[j].key) || + ecex_key_is_prefix_of(ed->mode_keybinds[j].key, ed->mode_keybinds[i].key)) { + ecex_warn_keybind_issue(mode, ed->mode_keybinds[i].key, "has a mode prefix conflict"); + } + } + } + + return ok; +} + const char *ecex_lookup_key_for_buffer(ecex_t *ed, buffer_t *buffer, const char *key) { if (!ed || !key) return NULL; @@ -1243,15 +2694,6 @@ int ecex_interactive_activate_current_line(ecex_t *ed) { return action->fn(ed, buffer, line, action->payload, action->userdata); } -static const char *ecex_basename(const char *path) { - if (!path || !path[0]) return "untitled"; - - const char *last_slash = strrchr(path, '/'); - if (last_slash && last_slash[1]) return last_slash + 1; - if (last_slash && last_slash == path) return "/"; - return path; -} - static int ecex_buffer_path_equal(buffer_t *buffer, const char *path) { return buffer && buffer->path && path && strcmp(buffer->path, path) == 0; } @@ -1259,35 +2701,55 @@ static int ecex_buffer_path_equal(buffer_t *buffer, const char *path) { int ecex_find_file(ecex_t *ed, const char *path) { if (!ed || !path || !path[0]) return ECEX_ERR; + char *normal_path = ecex_path_normalize(path); + if (!normal_path) return ECEX_ERR; + + if (ecex_path_is_dir(normal_path)) { + int result = ecex_file_browser_populate(ed, normal_path, 1); + free(normal_path); + return result; + } + + if (ecex_path_is_file(normal_path) && ecex_path_is_media(normal_path)) { + int result = ecex_media_open(ed, normal_path); + free(normal_path); + return result; + } + for (size_t i = 0; i < ed->buffer_count; i++) { - if (ecex_buffer_path_equal(ed->buffers[i], path)) { - return ecex_set_current_buffer_index(ed, i); + if (ecex_buffer_path_equal(ed->buffers[i], normal_path)) { + int result; + free(normal_path); + result = ecex_set_current_buffer_index(ed, i); + if (result == ECEX_OK && !ecex_buffer_has_renderer(ed->buffers[i])) ecex_run_file_handlers(ed, ed->buffers[i]); + return result; } } - const char *name = ecex_basename(path); + char *name = ecex_path_basename_dup(normal_path); + if (!name) { free(normal_path); return ECEX_ERR; } buffer_t *buf = ecex_create_buffer(ed, name, NULL, 0); - if (!buf) return ECEX_ERR; + free(name); + if (!buf) { free(normal_path); return ECEX_ERR; } - if (ecex_file_exists(path)) { - if (buffer_load_file(buf, path) != ECEX_OK) { - ecex_kill_buffer(ed, buf->name); + if (ecex_file_exists(normal_path)) { + if (buffer_load_file(buf, normal_path) != ECEX_OK) { + ecex_kill_buffer_force(ed, buf->name); + free(normal_path); return ECEX_ERR; } } else { - char *new_path = ecex_strdup(path); - if (!new_path) { - ecex_kill_buffer(ed, buf->name); - return ECEX_ERR; - } - free(buf->path); - buf->path = new_path; + buf->path = normal_path; + normal_path = NULL; buf->modified = 0; } + free(normal_path); ecex_auto_set_major_mode(ed, buf); - return ecex_set_current_buffer_index(ed, ed->buffer_count ? ed->buffer_count - 1 : 0); + int switch_result = ecex_set_current_buffer_index(ed, ed->buffer_count ? ed->buffer_count - 1 : 0); + if (switch_result == ECEX_OK) ecex_run_file_handlers(ed, buf); + return switch_result; } int ecex_save_current_buffer(ecex_t *ed) { @@ -1299,7 +2761,10 @@ int ecex_save_current_buffer(ecex_t *ed) { int ecex_write_current_buffer(ecex_t *ed, const char *path) { buffer_t *buf = ecex_current_buffer(ed); if (!buf || !path || !path[0]) return ECEX_ERR; - int result = buffer_save_as(buf, path); + char *normal_path = ecex_path_normalize(path); + if (!normal_path) return ECEX_ERR; + int result = buffer_save_as(buf, normal_path); + free(normal_path); if (result == ECEX_OK) ecex_auto_set_major_mode(ed, buf); return result; } diff --git a/src/eval.c b/src/eval.c index 316d6d8..e5f0e44 100644 --- a/src/eval.c +++ b/src/eval.c @@ -300,7 +300,7 @@ int ecex_eval_source(ecex_t *ed, buffer_set_interactive(out, 1); ecex_buffer_set_major_mode_by_name(ed, out, "eval-output-mode"); buffer_append(out, "Eval output (g: re-eval, q: quit window, ENTER: follow line if available)\n"); - buffer_append(out, "────────────────────────────────────────────────────────────────\n\n"); + buffer_append(out, "-------------------------------------------------------------------------\n\n"); buffer_append(out, "Eval: "); buffer_append(out, filename ? filename : ""); buffer_append(out, "\n\n"); diff --git a/src/main.c b/src/main.c index 9f388a0..94a08fa 100644 --- a/src/main.c +++ b/src/main.c @@ -1,6 +1,7 @@ #include "app.h" #include "config.h" #include "font.h" +#include "media.h" #include "render.h" #include @@ -145,13 +146,18 @@ int main(int argc, char **argv) { app_message(&app, "F1 for M-x. Tab completes commands."); while (!glfwWindowShouldClose(window) && !ed->should_quit) { + double now = glfwGetTime(); + if (ecex_media_tick(ed, now) || ecex_tick_animations(ed, now)) { + app.dirty = 1; + } + if (app.dirty) { render(&app); glfwSwapBuffers(window); app.dirty = 0; } - glfwWaitEvents(); + glfwWaitEventsTimeout(1.0 / 60.0); } font_free(&app.font); diff --git a/src/media.c b/src/media.c new file mode 100644 index 0000000..6c7a989 --- /dev/null +++ b/src/media.c @@ -0,0 +1,378 @@ +#include "media.h" + +#include "buffers.h" +#include "common.h" +#include "ecex.h" +#include "path.h" +#include "util.h" + +#include +#include +#include +#include +#include + +extern FILE *popen(const char *command, const char *type); +extern int pclose(FILE *stream); +extern int mkstemp(char *template); + +static char *shell_quote(const char *path) { + if (!path) return NULL; + size_t len = 2; /* quotes */ + for (const char *p = path; *p; p++) len += (*p == '\'') ? 4 : 1; + char *out = malloc(len + 1); + if (!out) return NULL; + char *w = out; + *w++ = '\''; + for (const char *p = path; *p; p++) { + if (*p == '\'') { + memcpy(w, "'\\''", 4); + w += 4; + } else { + *w++ = *p; + } + } + *w++ = '\''; + *w = '\0'; + return out; +} + +static int ppm_skip_ws_and_comments(FILE *f) { + int c = 0; + do { + c = fgetc(f); + if (c == '#') { + while (c != '\n' && c != EOF) c = fgetc(f); + } + } while (c != EOF && isspace((unsigned char)c)); + if (c == EOF) return EOF; + ungetc(c, f); + return 0; +} + +static int ppm_read_int(FILE *f, int *out) { + if (!f || !out) return ECEX_ERR; + if (ppm_skip_ws_and_comments(f) == EOF) return ECEX_ERR; + int c = fgetc(f); + if (c == EOF || !isdigit((unsigned char)c)) return ECEX_ERR; + int value = 0; + while (c != EOF && isdigit((unsigned char)c)) { + value = value * 10 + (c - '0'); + c = fgetc(f); + } + if (c != EOF) ungetc(c, f); + *out = value; + return ECEX_OK; +} + +static int read_ppm_frame(FILE *f, int *out_w, int *out_h, unsigned char **out_rgba) { + if (!f || !out_w || !out_h || !out_rgba) return ECEX_ERR; + int p = fgetc(f); + int six = fgetc(f); + if (p == EOF || six == EOF) return ECEX_ERR; + if (p != 'P' || six != '6') return ECEX_ERR; + + int w = 0, h = 0, maxv = 0; + if (ppm_read_int(f, &w) != ECEX_OK || ppm_read_int(f, &h) != ECEX_OK || ppm_read_int(f, &maxv) != ECEX_OK) { + return ECEX_ERR; + } + if (w <= 0 || h <= 0 || maxv <= 0 || maxv > 255) return ECEX_ERR; + + int c = fgetc(f); + if (c == EOF) return ECEX_ERR; + if (!isspace((unsigned char)c)) ungetc(c, f); + + size_t rgb_len = (size_t)w * (size_t)h * 3u; + size_t rgba_len = (size_t)w * (size_t)h * 4u; + if (w > 8192 || h > 8192 || rgb_len / 3u != (size_t)w * (size_t)h) return ECEX_ERR; + + unsigned char *rgb = malloc(rgb_len); + unsigned char *rgba = malloc(rgba_len); + if (!rgb || !rgba) { + free(rgb); + free(rgba); + return ECEX_ERR; + } + + size_t got = fread(rgb, 1, rgb_len, f); + if (got != rgb_len) { + free(rgb); + free(rgba); + return ECEX_ERR; + } + + for (size_t i = 0, j = 0; i < rgb_len; i += 3, j += 4) { + rgba[j + 0] = rgb[i + 0]; + rgba[j + 1] = rgb[i + 1]; + rgba[j + 2] = rgb[i + 2]; + rgba[j + 3] = 255; + } + + free(rgb); + *out_w = w; + *out_h = h; + *out_rgba = rgba; + return ECEX_OK; +} + +static int set_buffer_pixels(buffer_t *buffer, int w, int h, unsigned char *rgba) { + if (!buffer || w <= 0 || h <= 0 || !rgba) { + free(rgba); + return ECEX_ERR; + } + free(buffer->media_pixels); + buffer->media_pixels = rgba; + buffer->media_width = w; + buffer->media_height = h; + buffer->media_dirty = 1; + return ECEX_OK; +} + +static char *make_temp_log_path(void) { + char tmpl[] = "/tmp/ecex-ffmpeg-XXXXXX"; + int fd = mkstemp(tmpl); + if (fd < 0) return NULL; + close(fd); + return ecex_strdup(tmpl); +} + +static char *read_decode_log_status(const char *log_path, const char *fallback) { + FILE *f = log_path ? fopen(log_path, "rb") : NULL; + if (!f) return ecex_strdup(fallback ? fallback : "ffmpeg did not decode a preview frame."); + + char body[1400]; + size_t n = fread(body, 1, sizeof(body) - 1, f); + fclose(f); + body[n] = '\0'; + + if (n == 0) return ecex_strdup(fallback ? fallback : "ffmpeg did not decode a preview frame."); + + char *out = malloc(n + 128); + if (!out) return ecex_strdup(fallback ? fallback : "ffmpeg did not decode a preview frame."); + snprintf(out, n + 128, "ffmpeg did not decode a preview frame.\n\nffmpeg stderr:\n%s", body); + return out; +} + +static char *make_decode_command(const char *path, int video, char **out_log_path) { + char *q = shell_quote(path); + if (!q) return NULL; + char *log_path = make_temp_log_path(); + char *qlog = log_path ? shell_quote(log_path) : NULL; + const char *ffmpeg = getenv("ECEX_FFMPEG"); + if (!ffmpeg || !*ffmpeg) ffmpeg = "ffmpeg"; + /* + * Important: ffmpeg's filtergraph parser treats ',' as a filter + * separator unless it is escaped, even when the expression is inside + * quotes. The previous command used min(%d,iw), which makes many + * ffmpeg builds fail before producing a single PPM frame. Keep the + * comma escaped in the command string that reaches ffmpeg. + * + * -nostdin also prevents ffmpeg from accidentally consuming editor + * terminal input when ecex is launched from a shell. + */ + const char *fmt = video + ? "%s -nostdin -hide_banner -loglevel error -i %s -an -vf \"fps=60,scale='min(%d\\,iw)':-2\" -f image2pipe -vcodec ppm - %s%s" + : "%s -nostdin -hide_banner -loglevel error -i %s -frames:v 1 -vf \"scale='min(%d\\,iw)':-2\" -f image2pipe -vcodec ppm - %s%s"; + int need = snprintf(NULL, + 0, + fmt, + ffmpeg, + q, + ECEX_MEDIA_MAX_DIMENSION, + qlog ? "2>" : "", + qlog ? qlog : ""); + char *cmd = malloc((size_t)need + 1); + if (cmd) { + snprintf(cmd, + (size_t)need + 1, + fmt, + ffmpeg, + q, + ECEX_MEDIA_MAX_DIMENSION, + qlog ? "2>" : "", + qlog ? qlog : ""); + } + if (out_log_path) { + *out_log_path = log_path; + log_path = NULL; + } + free(qlog); + free(log_path); + free(q); + return cmd; +} + +static void buffer_set_media_text(buffer_t *buffer, const char *path, int video, const char *status) { + if (!buffer) return; + buffer->read_only = 0; + buffer_clear(buffer); + char text[2048]; + snprintf(text, + sizeof(text), + "%s preview: %s\n\n%s\n\nRequirements:\n install ffmpeg/ffprobe in PATH for broad image and video decoding.\n\nControls:\n Space / p play-pause video\n q quit window\n", + video ? "Video" : "Image", + path ? path : "(unknown)", + status ? status : "No decoded frame available."); + buffer_append(buffer, text); + buffer->modified = 0; + buffer->read_only = 1; +} + +void ecex_media_buffer_clear(buffer_t *buffer) { + if (!buffer) return; + if (buffer->media_pipe) { + pclose((FILE *)buffer->media_pipe); + buffer->media_pipe = NULL; + } + free(buffer->media_path); + buffer->media_path = NULL; + free(buffer->media_pixels); + buffer->media_pixels = NULL; + buffer->media_kind = ECEX_MEDIA_NONE; + buffer->media_width = 0; + buffer->media_height = 0; + buffer->media_dirty = 0; + buffer->media_texture_width = 0; + buffer->media_texture_height = 0; + buffer->media_last_frame_time = 0.0; + buffer->media_playing = 0; + buffer->media_status[0] = '\0'; +} + +int ecex_media_buffer_has_pixels(buffer_t *buffer) { + return buffer && buffer->media_pixels && buffer->media_width > 0 && buffer->media_height > 0; +} + +static buffer_t *media_buffer_for_path(ecex_t *ed, const char *path, int video) { + if (!ed || !path) return NULL; + char *base = ecex_path_basename_dup(path); + if (!base) return NULL; + char name[1024]; + snprintf(name, sizeof(name), "*%s-preview:%s*", video ? "video" : "image", base); + free(base); + + buffer_t *buffer = ecex_find_buffer(ed, name); + if (!buffer) buffer = ecex_create_buffer(ed, name, path, 1); + if (!buffer) return NULL; + return buffer; +} + +int ecex_media_load_into_buffer(ecex_t *ed, const char *path, buffer_t *buffer) { + if (!ed || !path || !buffer || !ecex_path_is_media(path)) return ECEX_ERR; + int video = ecex_path_is_video(path); + + ecex_media_buffer_clear(buffer); + buffer->media_kind = video ? ECEX_MEDIA_VIDEO : ECEX_MEDIA_IMAGE; + ecex_buffer_set_major_mode_by_name(ed, buffer, "media-preview-mode"); + buffer->media_path = ecex_path_normalize(path); + if (!buffer->media_path) buffer->media_path = ecex_strdup(path); + buffer->read_only = 0; + buffer_clear(buffer); + buffer_append(buffer, video ? "Loading video preview...\n" : "Loading image preview...\n"); + buffer->modified = 0; + buffer->read_only = 1; + + char *log_path = NULL; + char *cmd = make_decode_command(buffer->media_path ? buffer->media_path : path, video, &log_path); + if (!cmd) { + buffer_set_media_text(buffer, path, video, "Could not allocate ffmpeg command."); + free(log_path); + return ECEX_ERR; + } + + FILE *pipe = popen(cmd, "r"); + free(cmd); + if (!pipe) { + buffer_set_media_text(buffer, path, video, "Could not launch ffmpeg."); + if (log_path) unlink(log_path); + free(log_path); + return ECEX_ERR; + } + + int w = 0, h = 0; + unsigned char *rgba = NULL; + if (read_ppm_frame(pipe, &w, &h, &rgba) != ECEX_OK) { + pclose(pipe); + char *status = read_decode_log_status(log_path, "ffmpeg did not decode a preview frame."); + buffer_set_media_text(buffer, path, video, status); + free(status); + if (log_path) unlink(log_path); + free(log_path); + return ECEX_ERR; + } + + if (log_path) unlink(log_path); + free(log_path); + + if (video) { + buffer->media_pipe = pipe; + buffer->media_playing = 1; + snprintf(buffer->media_status, sizeof(buffer->media_status), "Playing at decoded 60fps stream"); + } else { + pclose(pipe); + snprintf(buffer->media_status, sizeof(buffer->media_status), "Image decoded via ffmpeg"); + } + + set_buffer_pixels(buffer, w, h, rgba); + buffer->read_only = 0; + buffer_clear(buffer); + char info[1024]; + snprintf(info, + sizeof(info), + "%s preview: %s\n%d x %d\n%s\n\n%s\n", + video ? "Video" : "Image", + buffer->media_path ? buffer->media_path : path, + w, + h, + buffer->media_status, + video ? "Space/p toggles playback. q quits window." : "q quits window."); + buffer_append(buffer, info); + buffer->modified = 0; + buffer->read_only = 1; + return ECEX_OK; +} + +int ecex_media_open(ecex_t *ed, const char *path) { + if (!ed || !path || !ecex_path_is_media(path)) return ECEX_ERR; + int video = ecex_path_is_video(path); + buffer_t *buffer = media_buffer_for_path(ed, path, video); + if (!buffer) return ECEX_ERR; + ecex_media_load_into_buffer(ed, path, buffer); + return ecex_switch_buffer(ed, buffer->name); +} + +int ecex_media_toggle_playback(ecex_t *ed) { + buffer_t *buffer = ecex_current_buffer(ed); + if (!buffer || buffer->media_kind != ECEX_MEDIA_VIDEO) return ECEX_ERR; + buffer->media_playing = !buffer->media_playing; + snprintf(buffer->media_status, + sizeof(buffer->media_status), + "%s", + buffer->media_playing ? "Playing" : "Paused"); + return ECEX_OK; +} + +int ecex_media_tick(ecex_t *ed, double now_seconds) { + if (!ed) return 0; + int dirty = 0; + for (size_t i = 0; i < ed->buffer_count; i++) { + buffer_t *buffer = ed->buffers[i]; + if (!buffer || buffer->media_kind != ECEX_MEDIA_VIDEO || !buffer->media_pipe || !buffer->media_playing) continue; + if (buffer->media_last_frame_time > 0.0 && now_seconds - buffer->media_last_frame_time < (1.0 / 60.0)) continue; + + int w = 0, h = 0; + unsigned char *rgba = NULL; + if (read_ppm_frame((FILE *)buffer->media_pipe, &w, &h, &rgba) == ECEX_OK) { + set_buffer_pixels(buffer, w, h, rgba); + buffer->media_last_frame_time = now_seconds; + dirty = 1; + } else { + pclose((FILE *)buffer->media_pipe); + buffer->media_pipe = NULL; + buffer->media_playing = 0; + snprintf(buffer->media_status, sizeof(buffer->media_status), "Playback finished"); + dirty = 1; + } + } + return dirty; +} diff --git a/src/path.c b/src/path.c new file mode 100644 index 0000000..1e002d7 --- /dev/null +++ b/src/path.c @@ -0,0 +1,208 @@ +#include "path.h" + +#include +#include +#include +#include +#include +#include + +extern char *realpath(const char *restrict path, char *restrict resolved_path); + +int ecex_path_copy(char *out, size_t out_size, const char *text) { + if (!out || out_size == 0) return -1; + if (!text) text = ""; + size_t len = strlen(text); + if (len >= out_size) { + memcpy(out, text, out_size - 1); + out[out_size - 1] = '\0'; + return -1; + } + memcpy(out, text, len + 1); + return 0; +} + +char *ecex_path_expand_user(const char *path) { + if (!path) return NULL; + if (path[0] != '~' || (path[1] != '\0' && path[1] != '/')) { + char *copy = malloc(strlen(path) + 1); + if (copy) strcpy(copy, path); + return copy; + } + + const char *home = getenv("HOME"); + if (!home || !home[0]) { + char *copy = malloc(strlen(path) + 1); + if (copy) strcpy(copy, path); + return copy; + } + + size_t home_len = strlen(home); + size_t rest_len = strlen(path + 1); + char *expanded = malloc(home_len + rest_len + 1); + if (!expanded) return NULL; + memcpy(expanded, home, home_len); + memcpy(expanded + home_len, path + 1, rest_len + 1); + return expanded; +} + +char *ecex_path_join(const char *dir, const char *name) { + if (!name) return NULL; + if (name[0] == '/') { + char *copy = malloc(strlen(name) + 1); + if (copy) strcpy(copy, name); + return copy; + } + if (!dir || !dir[0]) dir = "."; + + size_t dl = strlen(dir); + size_t nl = strlen(name); + int need_slash = dl > 0 && dir[dl - 1] != '/'; + char *out = malloc(dl + (size_t)need_slash + nl + 1); + if (!out) return NULL; + memcpy(out, dir, dl); + if (need_slash) out[dl++] = '/'; + memcpy(out + dl, name, nl + 1); + return out; +} + +char *ecex_path_dirname(const char *path) { + if (!path || !path[0]) { + char *dot = malloc(2); + if (dot) strcpy(dot, "."); + return dot; + } + + char *expanded = ecex_path_expand_user(path); + if (!expanded) return NULL; + + size_t len = strlen(expanded); + while (len > 1 && expanded[len - 1] == '/') expanded[--len] = '\0'; + + char *slash = strrchr(expanded, '/'); + if (!slash) { + strcpy(expanded, "."); + } else if (slash == expanded) { + expanded[1] = '\0'; + } else { + *slash = '\0'; + } + return expanded; +} + +char *ecex_path_basename_dup(const char *path) { + const char *base = "untitled"; + if (path && path[0]) { + const char *slash = strrchr(path, '/'); + if (slash && slash[1]) base = slash + 1; + else if (slash && slash == path) base = "/"; + else base = path; + } + char *out = malloc(strlen(base) + 1); + if (out) strcpy(out, base); + return out; +} + +char *ecex_path_normalize(const char *path) { + char *expanded = ecex_path_expand_user(path); + if (!expanded) return NULL; + + char *resolved = realpath(expanded, NULL); + if (resolved) { + free(expanded); + return resolved; + } + + if (expanded[0] == '/') return expanded; + + char cwd[4096]; + if (!getcwd(cwd, sizeof(cwd))) return expanded; + char *joined = ecex_path_join(cwd, expanded); + free(expanded); + return joined; +} + +int ecex_path_exists(const char *path) { + if (!path) return 0; + char *expanded = ecex_path_expand_user(path); + if (!expanded) return 0; + struct stat st; + int ok = stat(expanded, &st) == 0; + free(expanded); + return ok; +} + +int ecex_path_is_dir(const char *path) { + if (!path) return 0; + char *expanded = ecex_path_expand_user(path); + if (!expanded) return 0; + struct stat st; + int ok = stat(expanded, &st) == 0 && S_ISDIR(st.st_mode); + free(expanded); + return ok; +} + +int ecex_path_is_file(const char *path) { + if (!path) return 0; + char *expanded = ecex_path_expand_user(path); + if (!expanded) return 0; + struct stat st; + int ok = stat(expanded, &st) == 0 && S_ISREG(st.st_mode); + free(expanded); + return ok; +} + +long long ecex_path_file_size(const char *path) { + if (!path) return -1; + char *expanded = ecex_path_expand_user(path); + if (!expanded) return -1; + struct stat st; + long long size = (stat(expanded, &st) == 0 && S_ISREG(st.st_mode)) ? (long long)st.st_size : -1; + free(expanded); + return size; +} + +static int ext_eq(const char *path, const char *ext) { + if (!path || !ext) return 0; + size_t pl = strlen(path), el = strlen(ext); + if (pl < el) return 0; + const char *p = path + pl - el; + for (size_t i = 0; i < el; i++) { + if (tolower((unsigned char)p[i]) != tolower((unsigned char)ext[i])) return 0; + } + return 1; +} + +int ecex_path_is_image(const char *path) { + return ext_eq(path, ".png") || ext_eq(path, ".jpg") || ext_eq(path, ".jpeg") || + ext_eq(path, ".bmp") || ext_eq(path, ".ppm") || ext_eq(path, ".pnm") || + ext_eq(path, ".pgm") || ext_eq(path, ".gif") || ext_eq(path, ".webp") || + ext_eq(path, ".tif") || ext_eq(path, ".tiff") || ext_eq(path, ".avif") || + ext_eq(path, ".heic") || ext_eq(path, ".heif") || ext_eq(path, ".jxl") || + ext_eq(path, ".qoi") || ext_eq(path, ".tga") || ext_eq(path, ".dds") || + ext_eq(path, ".exr") || ext_eq(path, ".hdr") || ext_eq(path, ".ico"); +} + +int ecex_path_is_previewable_image(const char *path) { + return ecex_path_is_image(path); +} + +int ecex_path_is_video(const char *path) { + return ext_eq(path, ".mp4") || ext_eq(path, ".m4v") || ext_eq(path, ".mov") || + ext_eq(path, ".mkv") || ext_eq(path, ".webm") || ext_eq(path, ".avi") || + ext_eq(path, ".wmv") || ext_eq(path, ".flv") || ext_eq(path, ".gif") || + ext_eq(path, ".ogv") || ext_eq(path, ".mpeg") || ext_eq(path, ".mpg"); +} + +int ecex_path_is_media(const char *path) { + return ecex_path_is_image(path) || ecex_path_is_video(path); +} + +int ecex_path_cwd(char *out, size_t out_size) { + if (!out || out_size == 0) return -1; + if (!getcwd(out, out_size)) { + ecex_path_copy(out, out_size, "."); + return -1; + } + return 0; +} diff --git a/src/render.c b/src/render.c index 64a85f1..86bc535 100644 --- a/src/render.c +++ b/src/render.c @@ -1,6 +1,7 @@ #include "render.h" #include "common.h" +#include "media.h" #include #include @@ -474,6 +475,68 @@ static void draw_buffer_line(app_t *app, free(line); } + +static void draw_media_buffer(app_t *app, buffer_t *buf, view_rect_t rect) { + if (!app || !buf || !ecex_media_buffer_has_pixels(buf)) return; + + if (buf->media_texture == 0) { + glGenTextures(1, &buf->media_texture); + buf->media_texture_width = 0; + buf->media_texture_height = 0; + } + + glEnable(GL_TEXTURE_2D); + glBindTexture(GL_TEXTURE_2D, buf->media_texture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); + + if (buf->media_dirty || + buf->media_texture_width != buf->media_width || + buf->media_texture_height != buf->media_height) { + glTexImage2D(GL_TEXTURE_2D, + 0, + GL_RGBA, + buf->media_width, + buf->media_height, + 0, + GL_RGBA, + GL_UNSIGNED_BYTE, + buf->media_pixels); + buf->media_texture_width = buf->media_width; + buf->media_texture_height = buf->media_height; + buf->media_dirty = 0; + } + + ui_metrics_t m = ui_metrics(app); + float avail_x = rect.x + m.content_x; + float avail_y = rect.y + m.content_top + app->font.line_height * 4.0f; + float avail_w = rect.w - m.content_x * 2.0f; + float avail_h = rect.h - (avail_y - rect.y) - m.content_bottom_pad; + if (avail_w < 8.0f || avail_h < 8.0f) return; + + float iw = (float)buf->media_width; + float ih = (float)buf->media_height; + float scale = avail_w / iw; + if (ih * scale > avail_h) scale = avail_h / ih; + if (scale <= 0.0f) return; + + float draw_w = iw * scale; + float draw_h = ih * scale; + float x = avail_x + (avail_w - draw_w) * 0.5f; + float y = avail_y + (avail_h - draw_h) * 0.5f; + + glColor3f(1.0f, 1.0f, 1.0f); + glBegin(GL_QUADS); + glTexCoord2f(0.0f, 0.0f); glVertex2f(x, y); + glTexCoord2f(1.0f, 0.0f); glVertex2f(x + draw_w, y); + glTexCoord2f(1.0f, 1.0f); glVertex2f(x + draw_w, y + draw_h); + glTexCoord2f(0.0f, 1.0f); glVertex2f(x, y + draw_h); + glEnd(); + glDisable(GL_TEXTURE_2D); +} + static void render_cursor(app_t *app, buffer_t *buf, view_rect_t rect) { ui_metrics_t m = ui_metrics(app); @@ -522,6 +585,708 @@ static void draw_window_border(app_t *app, view_rect_t rect, int active) { draw_rect(rect.x + rect.w - 1.0f, rect.y, 1.0f, rect.h); } + +static app_t *draw_context_app(ecex_draw_context_t *ctx) { + return ctx ? (app_t *)ctx->internal : NULL; +} + +static void draw_context_vertex(ecex_draw_context_t *ctx, float x, float y) { + glVertex2f(ctx->x + x, ctx->y + y); +} + +void ecex_draw_set_color(ecex_draw_context_t *ctx, float r, float g, float b, float a) { + (void)ctx; + glColor4f(r, g, b, a); +} + +void ecex_draw_rect(ecex_draw_context_t *ctx, float x, float y, float w, float h) { + if (!ctx || w <= 0.0f || h <= 0.0f) return; + glDisable(GL_TEXTURE_2D); + glBegin(GL_QUADS); + draw_context_vertex(ctx, x, y); + draw_context_vertex(ctx, x + w, y); + draw_context_vertex(ctx, x + w, y + h); + draw_context_vertex(ctx, x, y + h); + glEnd(); +} + +void ecex_draw_rect_outline(ecex_draw_context_t *ctx, float x, float y, float w, float h, float thickness) { + if (!ctx || w <= 0.0f || h <= 0.0f) return; + if (thickness <= 0.0f) thickness = 1.0f; + ecex_draw_rect(ctx, x, y, w, thickness); + ecex_draw_rect(ctx, x, y + h - thickness, w, thickness); + ecex_draw_rect(ctx, x, y, thickness, h); + ecex_draw_rect(ctx, x + w - thickness, y, thickness, h); +} + +void ecex_draw_line(ecex_draw_context_t *ctx, float x1, float y1, float x2, float y2, float thickness) { + if (!ctx) return; + if (thickness <= 0.0f) thickness = 1.0f; + glDisable(GL_TEXTURE_2D); + glLineWidth(thickness); + glBegin(GL_LINES); + draw_context_vertex(ctx, x1, y1); + draw_context_vertex(ctx, x2, y2); + glEnd(); + glLineWidth(1.0f); +} + +void ecex_draw_text(ecex_draw_context_t *ctx, float x, float y, const char *text) { + app_t *app = draw_context_app(ctx); + if (!ctx || !app || !text) return; + float baseline = y + (app->font.ascent_px > 1.0f ? app->font.ascent_px : app->font.size_px * 0.80f); + draw_text(&app->font, ctx->x + x, ctx->y + baseline, text); +} + +float ecex_draw_text_width(ecex_draw_context_t *ctx, const char *text) { + app_t *app = draw_context_app(ctx); + if (!ctx || !app || !text) return 0.0f; + return text_width(&app->font, text); +} + +void ecex_draw_text_aligned(ecex_draw_context_t *ctx, float x, float y, float w, const char *text, int align) { + if (!ctx || !text) return; + float tw = ecex_draw_text_width(ctx, text); + float tx = x; + if (align == ECEX_TEXT_ALIGN_CENTER) tx = x + (w - tw) * 0.5f; + else if (align == ECEX_TEXT_ALIGN_RIGHT) tx = x + w - tw; + ecex_draw_text(ctx, tx, y, text); +} + +void ecex_draw_color_rgba8(ecex_draw_context_t *ctx, int r, int g, int b, int a) { + if (!ctx) return; + if (r < 0) r = 0; if (r > 255) r = 255; + if (g < 0) g = 0; if (g > 255) g = 255; + if (b < 0) b = 0; if (b > 255) b = 255; + if (a < 0) a = 0; if (a > 255) a = 255; + ecex_draw_set_color(ctx, (float)r / 255.0f, (float)g / 255.0f, (float)b / 255.0f, (float)a / 255.0f); +} + +void ecex_draw_rect_i(ecex_draw_context_t *ctx, int x, int y, int w, int h) { + ecex_draw_rect(ctx, (float)x, (float)y, (float)w, (float)h); +} + +void ecex_draw_rect_outline_i(ecex_draw_context_t *ctx, int x, int y, int w, int h, int thickness) { + ecex_draw_rect_outline(ctx, (float)x, (float)y, (float)w, (float)h, (float)thickness); +} + +void ecex_draw_line_i(ecex_draw_context_t *ctx, int x1, int y1, int x2, int y2, int thickness) { + ecex_draw_line(ctx, (float)x1, (float)y1, (float)x2, (float)y2, (float)thickness); +} + +void ecex_draw_text_i(ecex_draw_context_t *ctx, int x, int y, const char *text) { + ecex_draw_text(ctx, (float)x, (float)y, text); +} + + +static const char *ecex_draw_label_text(int label_id) { + switch (label_id) { + case 1: return "Tetris"; + case 2: return "score: "; + case 3: return "lines: "; + case 4: return "level: "; + case 5: return "next:"; + case 6: return "keys:"; + case 7: return "h/l or left/right move"; + case 8: return "j or down soft drop"; + case 9: return "k/up rotate"; + case 10: return "space hard drop"; + case 11: return "p pause, n new, q quit"; + case 12: return "GAME OVER - press n"; + case 13: return "PAUSED"; + case 20: return "Render demo"; + case 21: return "host vars and mouse input"; + case 22: return "integer safe drawing"; + case 23: return "smooth click animation"; + case 24: return "position: "; + case 25: return "click box to move cube"; + default: return ""; + } +} + +static unsigned char ecex_ascii5x7_bits(char ch, int row) { + /* Tiny host-side pixel font used by CCDJIT-safe label helpers. This avoids + * routing plugin labels through the normal stb/font text renderer, which is + * useful while plugin callback ABI issues are being isolated. Bits are + * returned left-to-right in the low 5 bits. */ + if (row < 0 || row >= 7) return 0; + switch (ch) { + case '0': { static const unsigned char b[7] = {14,17,19,21,25,17,14}; return b[row]; } + case '1': { static const unsigned char b[7] = {4,12,4,4,4,4,14}; return b[row]; } + case '2': { static const unsigned char b[7] = {14,17,1,2,4,8,31}; return b[row]; } + case '3': { static const unsigned char b[7] = {30,1,1,14,1,1,30}; return b[row]; } + case '4': { static const unsigned char b[7] = {2,6,10,18,31,2,2}; return b[row]; } + case '5': { static const unsigned char b[7] = {31,16,16,30,1,1,30}; return b[row]; } + case '6': { static const unsigned char b[7] = {14,16,16,30,17,17,14}; return b[row]; } + case '7': { static const unsigned char b[7] = {31,1,2,4,8,8,8}; return b[row]; } + case '8': { static const unsigned char b[7] = {14,17,17,14,17,17,14}; return b[row]; } + case '9': { static const unsigned char b[7] = {14,17,17,15,1,1,14}; return b[row]; } + case 'A': case 'a': { static const unsigned char b[7] = {14,17,17,31,17,17,17}; return b[row]; } + case 'B': case 'b': { static const unsigned char b[7] = {30,17,17,30,17,17,30}; return b[row]; } + case 'C': case 'c': { static const unsigned char b[7] = {14,17,16,16,16,17,14}; return b[row]; } + case 'D': case 'd': { static const unsigned char b[7] = {30,17,17,17,17,17,30}; return b[row]; } + case 'E': case 'e': { static const unsigned char b[7] = {31,16,16,30,16,16,31}; return b[row]; } + case 'F': case 'f': { static const unsigned char b[7] = {31,16,16,30,16,16,16}; return b[row]; } + case 'G': case 'g': { static const unsigned char b[7] = {14,17,16,23,17,17,15}; return b[row]; } + case 'H': case 'h': { static const unsigned char b[7] = {17,17,17,31,17,17,17}; return b[row]; } + case 'I': case 'i': { static const unsigned char b[7] = {14,4,4,4,4,4,14}; return b[row]; } + case 'J': case 'j': { static const unsigned char b[7] = {7,2,2,2,2,18,12}; return b[row]; } + case 'K': case 'k': { static const unsigned char b[7] = {17,18,20,24,20,18,17}; return b[row]; } + case 'L': case 'l': { static const unsigned char b[7] = {16,16,16,16,16,16,31}; return b[row]; } + case 'M': case 'm': { static const unsigned char b[7] = {17,27,21,21,17,17,17}; return b[row]; } + case 'N': case 'n': { static const unsigned char b[7] = {17,25,21,19,17,17,17}; return b[row]; } + case 'O': case 'o': { static const unsigned char b[7] = {14,17,17,17,17,17,14}; return b[row]; } + case 'P': case 'p': { static const unsigned char b[7] = {30,17,17,30,16,16,16}; return b[row]; } + case 'Q': case 'q': { static const unsigned char b[7] = {14,17,17,17,21,18,13}; return b[row]; } + case 'R': case 'r': { static const unsigned char b[7] = {30,17,17,30,20,18,17}; return b[row]; } + case 'S': case 's': { static const unsigned char b[7] = {15,16,16,14,1,1,30}; return b[row]; } + case 'T': case 't': { static const unsigned char b[7] = {31,4,4,4,4,4,4}; return b[row]; } + case 'U': case 'u': { static const unsigned char b[7] = {17,17,17,17,17,17,14}; return b[row]; } + case 'V': case 'v': { static const unsigned char b[7] = {17,17,17,17,17,10,4}; return b[row]; } + case 'W': case 'w': { static const unsigned char b[7] = {17,17,17,21,21,21,10}; return b[row]; } + case 'X': case 'x': { static const unsigned char b[7] = {17,17,10,4,10,17,17}; return b[row]; } + case 'Y': case 'y': { static const unsigned char b[7] = {17,17,10,4,4,4,4}; return b[row]; } + case 'Z': case 'z': { static const unsigned char b[7] = {31,1,2,4,8,16,31}; return b[row]; } + case ':': { static const unsigned char b[7] = {0,4,4,0,4,4,0}; return b[row]; } + case '-': { static const unsigned char b[7] = {0,0,0,14,0,0,0}; return b[row]; } + case '/': { static const unsigned char b[7] = {1,1,2,4,8,16,16}; return b[row]; } + case ',': { static const unsigned char b[7] = {0,0,0,0,4,4,8}; return b[row]; } + case '.': { static const unsigned char b[7] = {0,0,0,0,0,12,12}; return b[row]; } + case ' ': default: return 0; + } +} + +static void ecex_draw_mini_text_i(ecex_draw_context_t *ctx, int x, int y, const char *text) { + int scale = 2; + int advance = 12; + int cx = x; + const char *p = text; + if (!ctx || !text) return; + while (*p) { + int row; + for (row = 0; row < 7; ++row) { + unsigned char bits = ecex_ascii5x7_bits(*p, row); + int col; + for (col = 0; col < 5; ++col) { + if (bits & (1u << (4 - col))) { + ecex_draw_rect_i(ctx, cx + col * scale, y + row * scale, scale, scale); + } + } + } + cx += advance; + ++p; + } +} + + +extern const char *ecex_text_get_for_draw(ecex_t *ed, void *owner, int id); + +void ecex_draw_text_id_i(ecex_draw_context_t *ctx, void *owner, int id, int x, int y) { + app_t *app; + const char *text; + if (!ctx || !ctx->internal) return; + app = (app_t *)ctx->internal; + if (!app || !app->ed) return; + text = ecex_text_get_for_draw(app->ed, owner, id); + if (!text) text = ""; + + /* + * Plugin-safe real-font path: plugin code passes only owner/id and integer + * coordinates. The string itself lives in the host text registry, and the + * actual font renderer is called here on the host side, not directly from + * JIT-owned stack/string memory. Fixed labels may still use the mini-font + * helpers, but arbitrary plugin text such as Markdown should render with + * the loaded editor font. + */ + ecex_draw_text(ctx, (float)x, (float)y, text); +} + + + +int ecex_draw_context_height_i(ecex_draw_context_t *ctx) { + if (!ctx) return 0; + return (int)ctx->h; +} + +int ecex_draw_context_line_height_i(ecex_draw_context_t *ctx) { + int line_h; + if (!ctx) return 18; + line_h = (int)ctx->line_height; + return line_h < 18 ? 18 : line_h; +} + +int ecex_markdown_body_y_i(ecex_draw_context_t *ctx) { + int line_h; + if (!ctx) return 36; + line_h = ecex_draw_context_line_height_i(ctx); + return (int)ctx->content_y + line_h * 2; +} + +static int md_host_strlen(const char *s) { + int n = 0; + if (!s) return 0; + while (s[n]) ++n; + return n; +} + +static int md_host_is_digit(char c) { + return c >= '0' && c <= '9'; +} + +static const char *md_host_skip_indent(const char *line) { + if (!line) return ""; + while (*line == ' ' || *line == '\t') ++line; + return line; +} + +static int md_host_line_is_fence(const char *line) { + const char *p = md_host_skip_indent(line); + return (p[0] == '`' && p[1] == '`' && p[2] == '`') || + (p[0] == '~' && p[1] == '~' && p[2] == '~'); +} + +static int md_host_line_is_hr(const char *line) { + const char *p = md_host_skip_indent(line); + char mark = 0; + int count = 0; + while (*p) { + if (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n') { ++p; continue; } + if (*p != '-' && *p != '*' && *p != '_') return 0; + if (!mark) mark = *p; + if (*p != mark) return 0; + ++count; + ++p; + } + return count >= 3; +} + +static int md_host_heading_level(const char *line, const char **out_text) { + const char *p = md_host_skip_indent(line); + int level = 0; + while (p[level] == '#' && level < 6) ++level; + if (level > 0 && (p[level] == ' ' || p[level] == '\t' || p[level] == '\0')) { + p += level; + while (*p == ' ' || *p == '\t') ++p; + if (out_text) *out_text = p; + return level; + } + return 0; +} + +static const char *md_host_list_text(const char *line) { + const char *p = md_host_skip_indent(line); + if ((p[0] == '-' || p[0] == '*' || p[0] == '+') && (p[1] == ' ' || p[1] == '\t')) { + p += 2; + while (*p == ' ' || *p == '\t') ++p; + return p; + } + if (md_host_is_digit(p[0])) { + const char *q = p; + while (md_host_is_digit(*q)) ++q; + if (*q == '.' && (q[1] == ' ' || q[1] == '\t')) { + q += 2; + while (*q == ' ' || *q == '\t') ++q; + return q; + } + } + return NULL; +} + +static const char *md_host_quote_text(const char *line) { + const char *p = md_host_skip_indent(line); + if (*p != '>') return NULL; + ++p; + if (*p == ' ') ++p; + return p; +} + +static const char *md_host_trim_start(const char *text) { + if (!text) return ""; + while (*text == ' ' || *text == '\t') ++text; + return text; +} + +static int md_host_trim_len(const char *text) { + int n; + if (!text) return 0; + while (*text == ' ' || *text == '\t') ++text; + n = md_host_strlen(text); + while (n > 0 && (text[n - 1] == ' ' || text[n - 1] == '\t')) --n; + if (n > 224) n = 224; + return n; +} + +static void md_host_set_and_draw(ecex_draw_context_t *ctx, void *owner, int y, int style, const char *text) { + app_t *app; + const char *start; + int len; + if (!ctx || !ctx->internal) return; + app = (app_t *)ctx->internal; + if (!app || !app->ed) return; + start = md_host_trim_start(text); + len = md_host_trim_len(start); + if (ecex_text_set(app->ed, owner, 2, start, len) == 0) { + ecex_draw_markdown_line_auto_i(ctx, owner, 2, y, style); + } +} + +int ecex_markdown_draw_line_from_buffer_i(ecex_draw_context_t *ctx, void *owner, buffer_t *buffer, int line_index, int y, int in_code) { + char line[512]; + const char *text = NULL; + int copied; + int line_h; + int heading; + int next_in_code = in_code ? 1 : 0; + int advance; + app_t *app; + + if (!ctx || !ctx->internal || !buffer) return 18; + app = (app_t *)ctx->internal; + line_h = ecex_draw_context_line_height_i(ctx); + copied = ecex_buffer_line_copy(buffer, line_index, line, (int)sizeof(line)); + if (line_index < 4) { + ecex_log_int("markdown_host_line: index=", line_index); + ecex_log_int("markdown_host_line: copied=", copied); + } + if (copied < 0) return line_h; + line[sizeof(line) - 1] = '\0'; + + if (md_host_line_is_fence(line)) { + next_in_code = !next_in_code; + if (app && app->ed) ecex_text_set(app->ed, owner, 2, next_in_code ? "code" : "end code", -1); + ecex_draw_markdown_line_auto_i(ctx, owner, 2, y, 5); + advance = line_h + 8; + return advance | (next_in_code ? 0x10000 : 0); + } + + if (next_in_code) { + if (app && app->ed) ecex_text_set(app->ed, owner, 2, line, copied > 224 ? 224 : copied); + ecex_draw_markdown_line_auto_i(ctx, owner, 2, y, 3); + return line_h | 0x10000; + } + + heading = md_host_heading_level(line, &text); + if (heading) { + md_host_set_and_draw(ctx, owner, y, 1, text); + advance = line_h + (7 - heading) * 3 + 8; + return advance; + } + + if (md_host_line_is_hr(line)) { + if (app && app->ed) ecex_text_set(app->ed, owner, 2, "", 0); + ecex_draw_markdown_line_auto_i(ctx, owner, 2, y, 6); + return line_h; + } + + text = md_host_quote_text(line); + if (text) { + md_host_set_and_draw(ctx, owner, y, 2, text); + return line_h; + } + + text = md_host_list_text(line); + if (text) { + md_host_set_and_draw(ctx, owner, y, 4, text); + return line_h; + } + + if (line[0] == '\0') return line_h / 2; + + md_host_set_and_draw(ctx, owner, y, 0, line); + return line_h; +} + +void ecex_draw_markdown_canvas_i(ecex_draw_context_t *ctx, void *owner, int title_id, int x, int y, int w, int line_h) { + app_t *app; + const char *title; + int full_w; + int full_h; + if (!ctx || !ctx->internal) return; + app = (app_t *)ctx->internal; + full_w = (int)ctx->w; + full_h = (int)ctx->h; + if (w < 1) w = full_w - x * 2; + if (w < 1) w = 1; + if (line_h < 18) line_h = 18; + ecex_draw_color_rgba8(ctx, 29, 32, 33, 255); + ecex_draw_rect_i(ctx, 0, 0, full_w, full_h); + ecex_draw_color_rgba8(ctx, 250, 241, 199, 255); + title = (app && app->ed) ? ecex_text_get_for_draw(app->ed, owner, title_id) : ""; + if (!title) title = ""; + ecex_draw_text(ctx, (float)x, (float)y, title); + ecex_draw_color_rgba8(ctx, 80, 73, 69, 255); + ecex_draw_line_i(ctx, x, y + line_h + 8, x + w, y + line_h + 8, 1); +} + +void ecex_draw_markdown_text_i(ecex_draw_context_t *ctx, void *owner, int text_id, int x, int y, int w, int line_h, int style) { + app_t *app; + const char *text; + int h; + if (!ctx || !ctx->internal) return; + app = (app_t *)ctx->internal; + text = (app && app->ed) ? ecex_text_get_for_draw(app->ed, owner, text_id) : ""; + if (!text) text = ""; + if (w < 1) w = 1; + if (line_h < 18) line_h = 18; + h = line_h + 4; + switch (style) { + case 1: /* heading */ + ecex_draw_color_rgba8(ctx, 69, 84, 96, 255); + ecex_draw_rect_i(ctx, x, y - 5, w, h + 6); + ecex_draw_color_rgba8(ctx, 250, 189, 47, 255); + ecex_draw_text(ctx, (float)(x + 10), (float)y, text); + break; + case 2: /* quote */ + ecex_draw_color_rgba8(ctx, 131, 165, 152, 255); + ecex_draw_rect_i(ctx, x, y - 2, 4, h); + ecex_draw_color_rgba8(ctx, 213, 196, 161, 255); + ecex_draw_text(ctx, (float)(x + 14), (float)y, text); + break; + case 3: /* code */ + ecex_draw_color_rgba8(ctx, 40, 40, 40, 255); + ecex_draw_rect_i(ctx, x, y - 2, w, h); + ecex_draw_color_rgba8(ctx, 213, 196, 161, 255); + ecex_draw_text(ctx, (float)(x + 10), (float)y, text); + break; + case 4: /* list */ + ecex_draw_color_rgba8(ctx, 184, 187, 38, 255); + ecex_draw_rect_i(ctx, x + 6, y + line_h / 2 - 3, 6, 6); + ecex_draw_color_rgba8(ctx, 235, 219, 178, 255); + ecex_draw_text(ctx, (float)(x + 24), (float)y, text); + break; + case 5: /* fence */ + ecex_draw_color_rgba8(ctx, 80, 73, 69, 255); + ecex_draw_rect_i(ctx, x, y - 3, w, line_h + 6); + ecex_draw_color_rgba8(ctx, 142, 192, 124, 255); + ecex_draw_text(ctx, (float)(x + 10), (float)y, text); + break; + case 6: /* hr */ + ecex_draw_color_rgba8(ctx, 80, 73, 69, 255); + ecex_draw_line_i(ctx, x, y + line_h / 2, x + w, y + line_h / 2, 2); + break; + case 0: + default: + ecex_draw_color_rgba8(ctx, 235, 219, 178, 255); + ecex_draw_text(ctx, (float)x, (float)y, text); + break; + } +} + +void ecex_draw_markdown_canvas_auto_i(ecex_draw_context_t *ctx, void *owner, int title_id) { + int x; + int y; + int w; + int line_h; + if (!ctx) return; + ecex_log("draw_markdown_canvas_auto: enter"); + x = (int)ctx->content_x; + y = (int)ctx->content_y; + w = (int)ctx->content_w; + line_h = (int)ctx->line_height; + if (line_h < 18) line_h = 18; + if (w < 1) w = (int)ctx->w - x * 2; + if (w < 1) w = 1; + ecex_log("draw_markdown_canvas_auto: dispatch"); + ecex_draw_markdown_canvas_i(ctx, owner, title_id, x, y, w, line_h); + ecex_log("draw_markdown_canvas_auto: leave"); +} + +void ecex_draw_markdown_line_auto_i(ecex_draw_context_t *ctx, void *owner, int text_id, int y, int style) { + int x; + int w; + int line_h; + if (!ctx) return; + x = (int)ctx->content_x; + w = (int)ctx->content_w; + line_h = (int)ctx->line_height; + if (line_h < 18) line_h = 18; + if (w < 1) w = (int)ctx->w - x * 2; + if (w < 1) w = 1; + ecex_draw_markdown_text_i(ctx, owner, text_id, x, y, w, line_h, style); +} + +void ecex_draw_label_i(ecex_draw_context_t *ctx, int x, int y, int label_id) { + ecex_draw_mini_text_i(ctx, x, y, ecex_draw_label_text(label_id)); +} + +static void ecex_i32_to_ascii(int value, char *buf, size_t cap) { + char tmp[16]; + size_t n = 0; + size_t out = 0; + unsigned int v; + if (!buf || cap == 0) return; + if (value < 0) { + if (out + 1 < cap) buf[out++] = '-'; + v = (unsigned int)(-value); + } else { + v = (unsigned int)value; + } + do { + tmp[n++] = (char)('0' + (v % 10u)); + v /= 10u; + } while (v && n < sizeof(tmp)); + while (n && out + 1 < cap) buf[out++] = tmp[--n]; + buf[out] = '\0'; +} + +void ecex_draw_stat_i(ecex_draw_context_t *ctx, int x, int y, int label_id, int value) { + char num[24]; + const char *prefix = ecex_draw_label_text(label_id); + if (!ctx || !prefix) return; + ecex_draw_mini_text_i(ctx, x, y, prefix); + ecex_i32_to_ascii(value, num, sizeof(num)); + ecex_draw_mini_text_i(ctx, x + (int)strlen(prefix) * 12, y, num); +} + + +static int ecex_tetris_preview_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; + + if (p == 0) { + if ((r & 1) == 0) return row == 1; + return col == 1; + } + if (p == 1) return (row == 1 || row == 2) && (col == 1 || col == 2); + 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); + } + 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)); + } + 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)); + } + 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); + } + 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 void ecex_draw_tetris_preview_color(ecex_draw_context_t *ctx, int piece, int alpha) { + if (piece == 0) ecex_draw_color_rgba8(ctx, 51, 191, 242, alpha); + else if (piece == 1) ecex_draw_color_rgba8(ctx, 242, 217, 51, alpha); + else if (piece == 2) ecex_draw_color_rgba8(ctx, 179, 89, 242, alpha); + else if (piece == 3) ecex_draw_color_rgba8(ctx, 77, 217, 89, alpha); + else if (piece == 4) ecex_draw_color_rgba8(ctx, 242, 64, 64, alpha); + else if (piece == 5) ecex_draw_color_rgba8(ctx, 64, 102, 242, alpha); + else ecex_draw_color_rgba8(ctx, 242, 140, 51, alpha); +} + +void ecex_draw_tetris_preview_i(ecex_draw_context_t *ctx, int piece, int x, int y, int cell, int alpha) { + int r; + int c; + int p = piece % 7; + if (!ctx) return; + if (p < 0) p += 7; + if (cell < 3) cell = 3; + if (alpha < 0) alpha = 0; + if (alpha > 255) alpha = 255; + + /* Clear a small preview box first so the old preview shape cannot linger + * when the new piece occupies fewer cells than the previous I piece. */ + ecex_draw_color_rgba8(ctx, 20, 23, 28, 255); + ecex_draw_rect_i(ctx, x - 2, y - 2, cell * 4 + 4, cell * 4 + 4); + + for (r = 0; r < 4; ++r) { + for (c = 0; c < 4; ++c) { + if (!ecex_tetris_preview_shape_cell(p, 0, c, r)) continue; + ecex_draw_tetris_preview_color(ctx, p, alpha); + ecex_draw_rect_i(ctx, x + c * cell + 1, y + r * cell + 1, cell - 2, cell - 2); + } + } +} + +void ecex_draw_rgba(ecex_draw_context_t *ctx, + float x, + float y, + float w, + float h, + const unsigned char *rgba, + int image_w, + int image_h) { + if (!ctx || !rgba || image_w <= 0 || image_h <= 0 || w <= 0.0f || h <= 0.0f) return; + + GLuint tex = 0; + glGenTextures(1, &tex); + if (tex == 0) return; + + glEnable(GL_TEXTURE_2D); + glBindTexture(GL_TEXTURE_2D, tex); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image_w, image_h, 0, GL_RGBA, GL_UNSIGNED_BYTE, rgba); + + glColor4f(1.0f, 1.0f, 1.0f, 1.0f); + glBegin(GL_QUADS); + glTexCoord2f(0.0f, 0.0f); draw_context_vertex(ctx, x, y); + glTexCoord2f(1.0f, 0.0f); draw_context_vertex(ctx, x + w, y); + glTexCoord2f(1.0f, 1.0f); draw_context_vertex(ctx, x + w, y + h); + glTexCoord2f(0.0f, 1.0f); draw_context_vertex(ctx, x, y + h); + glEnd(); + + glBindTexture(GL_TEXTURE_2D, 0); + glDeleteTextures(1, &tex); + glDisable(GL_TEXTURE_2D); +} + +static ecex_draw_context_t make_draw_context(app_t *app, view_rect_t rect, size_t index) { + ui_metrics_t m = ui_metrics(app); + ecex_draw_context_t ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.x = rect.x; + ctx.y = rect.y; + ctx.w = rect.w; + ctx.h = rect.h; + ctx.content_x = m.content_x; + ctx.content_y = m.content_top; + ctx.content_w = rect.w - m.content_x * 2.0f; + ctx.content_h = rect.h - m.content_top - m.content_bottom_pad; + if (ctx.content_w < 0.0f) ctx.content_w = 0.0f; + if (ctx.content_h < 0.0f) ctx.content_h = 0.0f; + ctx.font_size = app->font.size_px; + ctx.line_height = app->font.line_height; + ctx.char_width = mono_cell_width(app); + ctx.window_index = index; + ctx.active = index == app->ed->current_window_index; + ctx.internal = app; + return ctx; +} + +static void render_custom_buffer(app_t *app, buffer_t *buf, view_rect_t rect, size_t index) { + if (!app || !buf || !buf->render_fn) return; + ecex_draw_context_t ctx = make_draw_context(app, rect, index); + int trace_callbacks = 0; + const char *trace_env = getenv("ECEX_TRACE_CALLBACKS"); + trace_callbacks = trace_env && trace_env[0] && trace_env[0] != '0'; + if (trace_callbacks) { + fprintf(stderr, "ecex-log: render_callback_enter buffer=%p fn=%p userdata=%p window=%zu %.1fx%.1f\n", + (void *)buf, (void *)buf->render_fn, buf->render_userdata, index, rect.w, rect.h); + fflush(stderr); + } + int result = buf->render_fn(app->ed, buf, &ctx, buf->render_userdata); + if (trace_callbacks) { + fprintf(stderr, "ecex-log: render_callback_leave buffer=%p result=%d\n", + (void *)buf, result); + fflush(stderr); + } +} + static void render_buffer_window(app_t *app, ecex_window_t *win, size_t index, float editor_h) { if (!app || !win || !win->buffer) return; @@ -549,29 +1314,41 @@ static void render_buffer_window(app_t *app, ecex_window_t *win, size_t index, f ensure_cursor_visible(app, buf, rect); } - size_t rows = visible_row_count_for_rect(app, rect); - size_t pos = offset_for_line(buf, buf->scroll_line); - float line_top = rect.y + m.content_top; + int replace_content = buf->render_fn && (buf->render_flags & ECEX_RENDER_REPLACE_CONTENT); + + if (!replace_content) { + size_t rows = visible_row_count_for_rect(app, rect); + size_t pos = offset_for_line(buf, buf->scroll_line); + float line_top = rect.y + m.content_top; + + for (size_t row = 0; row < rows; row++) { + size_t line_start = pos; + size_t line_end = buffer_line_end_at(buf, line_start); - for (size_t row = 0; row < rows; row++) { - size_t line_start = pos; - size_t line_end = buffer_line_end_at(buf, line_start); + draw_buffer_line(app, + buf, + rect, + buf->scroll_line + row, + line_start, + line_end, + line_top); - draw_buffer_line(app, - buf, - rect, - buf->scroll_line + row, - line_start, - line_end, - line_top); + if (line_end >= buf->len) break; + pos = line_end + 1; + line_top += app->font.line_height; + } + + if (buf->media_kind != ECEX_MEDIA_NONE) { + draw_media_buffer(app, buf, rect); + } - if (line_end >= buf->len) break; - pos = line_end + 1; - line_top += app->font.line_height; + if (index == app->ed->current_window_index && buf->media_kind == ECEX_MEDIA_NONE) { + render_cursor(app, buf, rect); + } } - if (index == app->ed->current_window_index) { - render_cursor(app, buf, rect); + if (buf->render_fn) { + render_custom_buffer(app, buf, rect, index); } glDisable(GL_SCISSOR_TEST); -- cgit v1.2.3