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