From 6aeaa171dc1ca43392f53cbd02097f76e1b1c5a0 Mon Sep 17 00:00:00 2001 From: David Moc Date: Sun, 31 May 2026 03:47:04 +0200 Subject: Hardened API, tetris, MD-View --- Makefile | 23 +- config/ecexrc.c | 55 +- config/filebrowser_plugin.c | 20 + config/markdown_plugin.c | 126 ++++ config/render_demo.c | 302 ++++++++ config/tetris.c | 682 ++++++++++++++++++ include/app.h | 8 +- include/buffers.h | 36 + include/completion.h | 19 + include/ecex.h | 219 ++++++ include/media.h | 21 + include/path.h | 22 + include/types.h | 124 ++++ src/app.c | 511 ++++++++----- src/buffers.c | 320 +++++++++ src/completion.c | 97 +++ src/config.c | 115 ++- src/ecex.c | 1675 ++++++++++++++++++++++++++++++++++++++++--- src/eval.c | 2 +- src/main.c | 8 +- src/media.c | 378 ++++++++++ src/path.c | 208 ++++++ src/render.c | 813 ++++++++++++++++++++- 23 files changed, 5465 insertions(+), 319 deletions(-) create mode 100644 config/filebrowser_plugin.c create mode 100644 config/markdown_plugin.c create mode 100644 config/render_demo.c create mode 100644 config/tetris.c create mode 100644 include/completion.h create mode 100644 include/media.h create mode 100644 include/path.h create mode 100644 src/completion.c create mode 100644 src/media.c create mode 100644 src/path.c diff --git a/Makefile b/Makefile index 1b15d60..8fa967e 100644 --- a/Makefile +++ b/Makefile @@ -3,16 +3,16 @@ CC = clang STATIC_LIB = include/libccdjit.a SHARED_LIB = include/libccdjit.so -SRC = src/main.c src/app.c src/render.c src/font.c src/util.c src/buffers.c src/ecex.c src/config.c src/eval.c +SRC = src/main.c src/app.c src/completion.c src/path.c src/media.c src/render.c src/font.c src/util.c src/buffers.c src/ecex.c src/config.c src/eval.c BIN = bin/ecex PKG_CFLAGS = $(shell pkg-config --cflags glfw3) PKG_LIBS = $(shell pkg-config --libs glfw3) -CFLAGS = -std=c11 -Wall -Wextra -pedantic -Iinclude $(PKG_CFLAGS) +CFLAGS ?= -std=c11 -Wall -Wextra -pedantic -Iinclude $(PKG_CFLAGS) LDLIBS = $(PKG_LIBS) -lGL -lm -.PHONY: all static shared clean run +.PHONY: all static shared clean run check sanitize debug release all: static @@ -27,5 +27,20 @@ shared: $(SRC) $(SHARED_LIB) run: static LD_LIBRARY_PATH=include ./$(BIN) +debug: CFLAGS += -O0 -g3 -DDEBUG +debug: static + +release: CFLAGS += -O2 -DNDEBUG +release: static + +sanitize: CFLAGS += -O1 -g -fsanitize=address,undefined -fno-omit-frame-pointer +sanitize: LDLIBS += -fsanitize=address,undefined +sanitize: static + +check: + @mkdir -p bin + $(CC) -std=c11 -Wall -Wextra -pedantic -Iinclude tests/test_core.c src/buffers.c src/completion.c src/path.c src/util.c -o bin/ecex-tests + ./bin/ecex-tests + clean: - rm -f $(BIN) + rm -f $(BIN) bin/ecex-tests diff --git a/config/ecexrc.c b/config/ecexrc.c index db4b6b9..1d13f18 100644 --- a/config/ecexrc.c +++ b/config/ecexrc.c @@ -1,8 +1,13 @@ #include "ecex.h" #include "buffers.h" -#define C(x) ((float)(x) / 255.0f) -#define RGB(r, g, b) C(r), C(g), C(b) +#define ECEX_NO_STANDALONE_CONFIG +#include "render_demo.c" +#include "tetris.c" +#include "markdown_plugin.c" +#undef ECEX_NO_STANDALONE_CONFIG + +#define RGB(r, g, b) ECEX_RGB8(r, g, b) /* Gruvbox dark palette */ #define GB_DARK0_HARD RGB(0x1d, 0x20, 0x21) @@ -93,7 +98,23 @@ static int demo_interactive_command(ecex_t *ed) { return 0; } -int ecex_config_init(ecex_t *ed) { +static const ecex_config_command_t config_commands[] = { + {"hello", hello_command}, + {"make-notes", make_notes_command}, + {"demo-interactive", demo_interactive_command}, +}; + +static const ecex_config_keybind_t config_binds[] = { + {"C-h", "hello"}, + {"C-m", "make-notes"}, + {"F2", "list-commands"}, + {"F3", "list-buffers"}, + {"F4", "demo-interactive"}, + {"C-q", "quit"}, + {"C-x h", "beginning-of-buffer"}, +}; + +ECEX_CONFIG_BEGIN ecex_set_font_size(ed, 20.0f); ecex_set_bg_color(ed, GB_DARK0_HARD); @@ -115,28 +136,18 @@ int ecex_config_init(ecex_t *ed) { ecex_set_cursor_color(ed, GB_AQUA); ecex_set_region_bg_color(ed, GB_DARK2); - ecex_register_command(ed, "hello", hello_command); - ecex_register_command(ed, "make-notes", make_notes_command); - ecex_register_command(ed, "demo-interactive", demo_interactive_command); - - ecex_bind_key(ed, "C-h", "hello"); - ecex_bind_key(ed, "C-m", "make-notes"); - ecex_bind_key(ed, "F2", "list-commands"); - ecex_bind_key(ed, "F3", "list-buffers"); - ecex_bind_key(ed, "F4", "demo-interactive"); - ecex_bind_key(ed, "C-q", "quit"); - ecex_bind_key(ed, "C-x h", "beginning-of-buffer"); - - ecex_bind_key(ed, "C-x b n", "next-buffer"); - ecex_bind_key(ed, "C-x b p", "previous-buffer"); + ECEX_CONFIG_COMMANDS(config_commands); + ECEX_CONFIG_BINDS(config_binds); + ECEX_CONFIG_INCLUDE(ecex_render_demo_plugin); + ECEX_CONFIG_INCLUDE(ecex_tetris_plugin); + ECEX_CONFIG_INCLUDE(ecex_markdown_plugin); buffer_t *scratch = ecex_current_buffer(ed); - buffer_insert(scratch, "ecex config loaded with Gruvbox colors!\n"); - buffer_insert(scratch, "Try F1 M-x, C-x C-f, C-x C-s, F2 commands, F3 buffers, F4 demo menu, C-x b n/p.\n"); - buffer_insert(scratch, "Eval live C snippets with C-x C-e (line), C-x e r (marked region), C-x e b (buffer), C-x e f (file).\n"); + buffer_insert(scratch, "ecex config loaded !\n"); + buffer_insert(scratch, "Try F1, C-x C-f, C-x C-s, C-x d file browser, F2 commands, F3 buffers, F4 demo menu, C-x b.\n"); + buffer_insert(scratch, "Run M-x render-demo for a custom renderer demo, or M-x tetris for Tetris.\n"); buffer_insert(scratch, "Example line to edit then C-x C-e: ecex_set_font_size(ed, 28.0f);\n"); buffer_insert(scratch, "Example relative change: ecex_adjust_font_size(ed, 2.0f);\n\n"); - return 0; -} +ECEX_CONFIG_END diff --git a/config/filebrowser_plugin.c b/config/filebrowser_plugin.c new file mode 100644 index 0000000..fb92cef --- /dev/null +++ b/config/filebrowser_plugin.c @@ -0,0 +1,20 @@ +/* Compatibility shim. + + The file browser is now built into the editor core and registered during + startup as these commands: + - file-browser + - file-browser-here + - file-browser-refresh + - file-browser-open + - file-browser-preview + - file-browser-toggle-preview + + Old configs that still include this file can keep doing so safely. */ +#ifndef ECEX_FILEBROWSER_PLUGIN_C +#define ECEX_FILEBROWSER_PLUGIN_C +#include "ecex.h" +static int ecex_filebrowser_plugin_init(ecex_t *ed) { + (void)ed; + return 0; +} +#endif diff --git a/config/markdown_plugin.c b/config/markdown_plugin.c new file mode 100644 index 0000000..ded0e1c --- /dev/null +++ b/config/markdown_plugin.c @@ -0,0 +1,126 @@ +#include "ecex.h" + +#define MD_TEXT_TITLE 1 + +typedef struct md_state { + ecex_t *ed; +} md_state_t; + +static int md_render(ecex_t *ed, buffer_t *buffer, ecex_draw_context_t *ctx, void *userdata) { + md_state_t *state = (md_state_t *)userdata; + int draw_count; + int line_count; + int scroll; + int y; + int h; + int line_h; + int in_code = 0; + int i; + + if (!ed || !buffer || !ctx || !state) return 0; + + draw_count = ecex_var_i32(ed, state, "draw_count", 0) + 1; + ecex_var_i32_set_scalar(ed, state, "draw_count", draw_count); + if (draw_count <= 2 || (draw_count % 120) == 0) { + ecex_log_int("markdown_plugin_draw: count=", draw_count); + } + + line_count = ecex_buffer_line_count_i(buffer); + ecex_log_int("markdown_plugin_draw: line_count=", line_count); + if (line_count < 0) line_count = 0; + + ecex_text_set_buffer_title(ed, state, MD_TEXT_TITLE, buffer); + ecex_draw_markdown_canvas_auto_i(ctx, state, MD_TEXT_TITLE); + + y = ecex_markdown_body_y_i(ctx); + h = ecex_draw_context_height_i(ctx); + line_h = ecex_draw_context_line_height_i(ctx); + scroll = ecex_buffer_scroll_line(buffer); + if (scroll < 0) scroll = 0; + if (scroll > line_count) scroll = line_count; + + for (i = scroll; i < line_count && y < h - line_h; ++i) { + int packed; + int advance; + if (i < scroll + 4) ecex_log_int("markdown_plugin_draw: host line=", i); + packed = ecex_markdown_draw_line_from_buffer_i(ctx, state, buffer, i, y, in_code); + in_code = (packed & 0x10000) ? 1 : 0; + advance = packed & 0xffff; + if (advance <= 0) advance = line_h; + y += advance; + } + + return 0; +} + +static void md_free_state(void *userdata) { + md_state_t *state = (md_state_t *)userdata; + ecex_t *ed; + if (!state) return; + ed = state->ed; + ecex_text_free_owner(ed, state); + ecex_var_free_owner(ed, state); + ecex_object_free(ed, state); +} + +static int md_view_buffer(ecex_t *ed, buffer_t *buffer) { + md_state_t *state; + if (!ed || !buffer) return -1; + if (ecex_buffer_has_renderer(buffer)) ecex_buffer_clear_renderer(buffer); + + state = (md_state_t *)ecex_object_calloc(ed, 1, sizeof(*state)); + if (!state) return -1; + state->ed = ed; + + if (ecex_buffer_set_renderer(buffer, md_render, state, md_free_state, ECEX_RENDER_REPLACE_CONTENT) != 0) { + ecex_object_free(ed, state); + return -1; + } + ecex_buffer_set_major_mode_by_name(ed, buffer, "markdown-mode"); + return 0; +} + +static int md_file_handler(ecex_t *ed, buffer_t *buffer) { + if (!buffer) return -1; + if (ecex_buffer_has_renderer(buffer)) return 0; + return md_view_buffer(ed, buffer); +} + +static int cmd_markdown_view(ecex_t *ed) { + return md_view_buffer(ed, ecex_current_buffer(ed)); +} + +static int cmd_markdown_source(ecex_t *ed) { + buffer_t *buffer = ecex_current_buffer(ed); + if (!buffer) return -1; + if (ecex_buffer_has_renderer(buffer)) ecex_buffer_clear_renderer(buffer); + ecex_buffer_set_major_mode_by_name(ed, buffer, "markdown-mode"); + return 0; +} + +static int cmd_markdown_toggle(ecex_t *ed) { + buffer_t *buffer = ecex_current_buffer(ed); + if (!buffer) return -1; + if (ecex_buffer_has_renderer(buffer)) return cmd_markdown_source(ed); + return md_view_buffer(ed, buffer); +} + +int ecex_markdown_plugin(ecex_t *ed) { + ECEX_CONFIG_MODE("markdown-mode"); + ECEX_CONFIG_COMMAND("markdown-view", cmd_markdown_view); + ECEX_CONFIG_COMMAND("markdown-source", cmd_markdown_source); + ECEX_CONFIG_COMMAND("markdown-toggle", cmd_markdown_toggle); + ECEX_CONFIG_MODE_BIND("markdown-mode", "C-c C-c", "markdown-toggle"); + ECEX_CONFIG_MODE_BIND("markdown-mode", "C-c C-s", "markdown-source"); + ecex_register_file_handler(ed, ".md", md_file_handler); + ecex_register_file_handler(ed, ".markdown", md_file_handler); + ecex_register_file_handler(ed, ".mdown", md_file_handler); + ecex_register_file_handler(ed, ".mkd", md_file_handler); + return 0; +} + +#ifndef ECEX_NO_STANDALONE_CONFIG +ECEX_CONFIG_BEGIN + ECEX_CONFIG_INCLUDE(ecex_markdown_plugin); +ECEX_CONFIG_END +#endif diff --git a/config/render_demo.c b/config/render_demo.c new file mode 100644 index 0000000..761b552 --- /dev/null +++ b/config/render_demo.c @@ -0,0 +1,302 @@ +#include "ecex.h" + +#define RENDER_DEMO_BUF "*render-demo*" +#define RENDER_DEMO_VAR_X "x_milli" +#define RENDER_DEMO_VAR_Y "y_milli" +#define RENDER_DEMO_VAR_TARGET_X "target_x_milli" +#define RENDER_DEMO_VAR_TARGET_Y "target_y_milli" +#define RENDER_DEMO_VAR_MOVING "moving" +#define RENDER_DEMO_VAR_BOX_X "box_x" +#define RENDER_DEMO_VAR_BOX_Y "box_y" +#define RENDER_DEMO_VAR_BOX_SIZE "box_size" +#define RENDER_DEMO_VAR_TRAVEL_X "travel_x" +#define RENDER_DEMO_VAR_TRAVEL_Y "travel_y" +#define RENDER_DEMO_VAR_AREA_X "area_x" +#define RENDER_DEMO_VAR_AREA_Y "area_y" + +#define RENDER_DEMO_LABEL_TITLE 20 +#define RENDER_DEMO_LABEL_SUBTITLE 21 +#define RENDER_DEMO_LABEL_SAFE_DRAW 22 +#define RENDER_DEMO_LABEL_ANIM 23 +#define RENDER_DEMO_LABEL_POSITION 24 +#define RENDER_DEMO_LABEL_CLICK 25 + +typedef struct render_demo_state { + ecex_t *ed; + int log_draw_count; +} render_demo_state_t; + +static int render_demo_get(ecex_t *ed, render_demo_state_t *s, const char *name, int fallback) { + if (!ed || !s || !name) return fallback; + return ecex_var_i32(ed, s, name, fallback); +} + +static void render_demo_set(ecex_t *ed, render_demo_state_t *s, const char *name, int value) { + if (!ed || !s || !name) return; + ecex_var_i32_set_scalar(ed, s, name, value); +} + +static void render_demo_reset(ecex_t *ed, render_demo_state_t *s) { + ecex_log_ptr("render_demo_reset: state=", s); + if (!ed || !s) return; + + /* Store mutable demo state in the host variable registry. This mirrors the + * Tetris plugin flow and avoids relying on CCDJIT struct-field writes for + * values that must survive renderer, animation, and mouse callbacks. + * Values are fixed-point integers: 0..1000 represents the available travel + * range inside the demo frame. */ + render_demo_set(ed, s, RENDER_DEMO_VAR_X, 0); + render_demo_set(ed, s, RENDER_DEMO_VAR_Y, 500); + render_demo_set(ed, s, RENDER_DEMO_VAR_TARGET_X, 0); + render_demo_set(ed, s, RENDER_DEMO_VAR_TARGET_Y, 500); + render_demo_set(ed, s, RENDER_DEMO_VAR_MOVING, 0); + s->log_draw_count = 0; +} + +static int render_demo_draw(ecex_t *ed, buffer_t *buffer, ecex_draw_context_t *ctx, void *userdata) { + render_demo_state_t *s = (render_demo_state_t *)userdata; + int w; + int h; + int cx; + int cy; + int cw; + int ch; + int line_h; + int box; + int travel; + int x_milli; + int y_milli; + int box_x; + int box_y; + int travel_y; + int area_x; + int area_y; + + (void)buffer; + if (!ed || !ctx || !s) return 0; + + s->log_draw_count++; + if (s->log_draw_count <= 4 || (s->log_draw_count % 60) == 0) { + ecex_log_int("render_demo_draw: count=", s->log_draw_count); + } + + w = (int)ctx->w; + h = (int)ctx->h; + cx = (int)ctx->content_x; + cy = (int)ctx->content_y; + cw = (int)ctx->content_w; + ch = (int)ctx->content_h; + line_h = (int)ctx->line_height; + if (line_h < 18) line_h = 18; + if (cw < 1) cw = w - cx * 2; + if (ch < 1) ch = h - cy * 2; + if (cw < 1) cw = 1; + if (ch < 1) ch = 1; + + ecex_draw_color_rgba8(ctx, 26, 28, 36, 255); + ecex_draw_rect_i(ctx, 0, 0, w, h); + + ecex_draw_color_rgba8(ctx, 48, 96, 180, 255); + ecex_draw_rect_i(ctx, cx, cy, (cw * 55) / 100, line_h * 2); + + ecex_draw_color_rgba8(ctx, 242, 230, 191, 255); + ecex_draw_rect_outline_i(ctx, cx, cy, cw, ch, 2); + ecex_draw_label_i(ctx, cx + 12, cy + 10, RENDER_DEMO_LABEL_TITLE); + + ecex_draw_color_rgba8(ctx, 229, 89, 64, 255); + ecex_draw_line_i(ctx, cx, cy + ch, cx + cw, cy, 3); + + ecex_draw_color_rgba8(ctx, 160, 230, 180, 255); + ecex_draw_label_i(ctx, cx + 12, cy + line_h * 3, RENDER_DEMO_LABEL_SUBTITLE); + ecex_draw_label_i(ctx, cx + 12, cy + line_h * 4, RENDER_DEMO_LABEL_SAFE_DRAW); + ecex_draw_label_i(ctx, cx + 12, cy + line_h * 5, RENDER_DEMO_LABEL_ANIM); + ecex_draw_label_i(ctx, cx + 12, cy + line_h * 6, RENDER_DEMO_LABEL_CLICK); + + x_milli = render_demo_get(ed, s, RENDER_DEMO_VAR_X, 0); + y_milli = render_demo_get(ed, s, RENDER_DEMO_VAR_Y, 500); + ecex_draw_stat_i(ctx, cx + 12, cy + line_h * 8, RENDER_DEMO_LABEL_POSITION, x_milli); + + box = line_h * 2; + if (box < 36) box = 36; + if (box > 72) box = 72; + travel = cw - box - 24; + if (travel < 0) travel = 0; + area_x = cx + 12; + area_y = cy + line_h * 10; + travel_y = ch - (area_y - cy) - box - 12; + if (travel_y < 0) travel_y = 0; + if (x_milli < 0) x_milli = 0; + if (x_milli > 1000) x_milli = 1000; + if (y_milli < 0) y_milli = 0; + if (y_milli > 1000) y_milli = 1000; + box_x = area_x + (travel * x_milli) / 1000; + box_y = area_y + (travel_y * y_milli) / 1000; + + render_demo_set(ed, s, RENDER_DEMO_VAR_BOX_X, box_x); + render_demo_set(ed, s, RENDER_DEMO_VAR_BOX_Y, box_y); + render_demo_set(ed, s, RENDER_DEMO_VAR_BOX_SIZE, box); + render_demo_set(ed, s, RENDER_DEMO_VAR_TRAVEL_X, travel); + render_demo_set(ed, s, RENDER_DEMO_VAR_TRAVEL_Y, travel_y); + render_demo_set(ed, s, RENDER_DEMO_VAR_AREA_X, area_x); + render_demo_set(ed, s, RENDER_DEMO_VAR_AREA_Y, area_y); + + ecex_draw_color_rgba8(ctx, 70, 76, 90, 255); + ecex_draw_rect_outline_i(ctx, area_x, area_y, travel + box, travel_y + box, 1); + + ecex_draw_color_rgba8(ctx, 89, 242, 140, 255); + ecex_draw_rect_i(ctx, box_x, box_y, box, box); + ecex_draw_color_rgba8(ctx, 20, 24, 30, 255); + ecex_draw_rect_outline_i(ctx, box_x, box_y, box, box, 2); + + return 0; +} + + +static int render_demo_clamp_milli(int value) { + if (value < 0) return 0; + if (value > 1000) return 1000; + return value; +} + +static int render_demo_delta_step(int current, int target) { + int delta = target - current; + int step; + if (delta < 0) { + if (delta >= -4) return target; + step = delta / 5; + if (step == 0) step = -1; + return current + step; + } + if (delta <= 4) return target; + step = delta / 5; + if (step == 0) step = 1; + return current + step; +} + +static int render_demo_tick(ecex_t *ed, buffer_t *buffer, int now_ms, void *userdata) { + render_demo_state_t *s = (render_demo_state_t *)userdata; + int x; + int y; + int target_x; + int target_y; + int next_x; + int next_y; + + (void)buffer; + (void)now_ms; + if (!ed || !s) return 0; + if (!render_demo_get(ed, s, RENDER_DEMO_VAR_MOVING, 0)) return 0; + + x = render_demo_get(ed, s, RENDER_DEMO_VAR_X, 0); + y = render_demo_get(ed, s, RENDER_DEMO_VAR_Y, 500); + target_x = render_demo_get(ed, s, RENDER_DEMO_VAR_TARGET_X, x); + target_y = render_demo_get(ed, s, RENDER_DEMO_VAR_TARGET_Y, y); + + next_x = render_demo_delta_step(x, target_x); + next_y = render_demo_delta_step(y, target_y); + render_demo_set(ed, s, RENDER_DEMO_VAR_X, render_demo_clamp_milli(next_x)); + render_demo_set(ed, s, RENDER_DEMO_VAR_Y, render_demo_clamp_milli(next_y)); + + if (next_x == target_x && next_y == target_y) { + render_demo_set(ed, s, RENDER_DEMO_VAR_MOVING, 0); + ecex_log("render_demo_tick: target reached"); + } + return 1; +} + +static int render_demo_mouse(ecex_t *ed, buffer_t *buffer, int event, int x, int y, int button, void *userdata) { + render_demo_state_t *s = (render_demo_state_t *)userdata; + int box; + int area_x; + int area_y; + int travel_x; + int travel_y; + int nx; + int ny; + + (void)buffer; + if (!ed || !s || button != ECEX_MOUSE_BUTTON_LEFT) return 0; + if (event != ECEX_MOUSE_PRESS) return 0; + + box = render_demo_get(ed, s, RENDER_DEMO_VAR_BOX_SIZE, 0); + area_x = render_demo_get(ed, s, RENDER_DEMO_VAR_AREA_X, 0); + area_y = render_demo_get(ed, s, RENDER_DEMO_VAR_AREA_Y, 0); + travel_x = render_demo_get(ed, s, RENDER_DEMO_VAR_TRAVEL_X, 0); + travel_y = render_demo_get(ed, s, RENDER_DEMO_VAR_TRAVEL_Y, 0); + if (box <= 0) return 0; + + if (x < area_x || x >= area_x + travel_x + box || + y < area_y || y >= area_y + travel_y + box) { + return 0; + } + + nx = x - area_x - box / 2; + ny = y - area_y - box / 2; + if (travel_x <= 0) render_demo_set(ed, s, RENDER_DEMO_VAR_TARGET_X, 0); + else render_demo_set(ed, s, RENDER_DEMO_VAR_TARGET_X, render_demo_clamp_milli((nx * 1000) / travel_x)); + if (travel_y <= 0) render_demo_set(ed, s, RENDER_DEMO_VAR_TARGET_Y, 0); + else render_demo_set(ed, s, RENDER_DEMO_VAR_TARGET_Y, render_demo_clamp_milli((ny * 1000) / travel_y)); + + render_demo_set(ed, s, RENDER_DEMO_VAR_MOVING, 1); + ecex_log("render_demo_mouse: target set"); + return 1; +} + +static void render_demo_free_state(void *userdata) { + render_demo_state_t *s = (render_demo_state_t *)userdata; + if (!s) return; + ecex_var_free_owner(s->ed, s); + ecex_config_free(s); +} + +static int cmd_render_demo(ecex_t *ed) { + buffer_t *buffer; + render_demo_state_t *s; + + ecex_log("cmd_render_demo: enter"); + if (!ed) return -1; + + buffer = ecex_find_buffer(ed, RENDER_DEMO_BUF); + if (!buffer) buffer = ecex_create_interactive_buffer(ed, RENDER_DEMO_BUF); + if (!buffer) return -1; + + s = (render_demo_state_t *)ecex_buffer_renderer_userdata(buffer); + if (!s) { + s = (render_demo_state_t *)ecex_config_calloc(1, sizeof(*s)); + if (!s) return -1; + s->ed = ed; + render_demo_reset(ed, s); + + if (ecex_buffer_set_renderer(buffer, render_demo_draw, s, render_demo_free_state, ECEX_RENDER_REPLACE_CONTENT) != 0) { + render_demo_free_state(s); + return -1; + } + if (ecex_buffer_set_mouse_handler(buffer, render_demo_mouse, s, 0) != 0) { + ecex_buffer_clear_renderer(buffer); + return -1; + } + if (ecex_buffer_set_animation_ms(buffer, render_demo_tick, s, 0, 60) != 0) { + ecex_buffer_clear_mouse_handler(buffer); + ecex_buffer_clear_renderer(buffer); + return -1; + } + } + + if (ecex_buffer_replace_text(buffer, "Render demo. The renderer replaces normal buffer content.\n") != 0) { + return -1; + } + ecex_buffer_set_modified(buffer, 0); + + return ecex_switch_buffer(ed, RENDER_DEMO_BUF); +} + +int ecex_render_demo_plugin(ecex_t *ed) { + ECEX_CONFIG_COMMAND("render-demo", cmd_render_demo); + return 0; +} + +#ifndef ECEX_NO_STANDALONE_CONFIG +ECEX_CONFIG_BEGIN + ECEX_CONFIG_INCLUDE(ecex_render_demo_plugin); +ECEX_CONFIG_END +#endif diff --git a/config/tetris.c b/config/tetris.c new file mode 100644 index 0000000..542963a --- /dev/null +++ b/config/tetris.c @@ -0,0 +1,682 @@ +#include "ecex.h" + +#define TETRIS_W 10 +#define TETRIS_H 20 +#define TETRIS_CELLS (TETRIS_W * TETRIS_H) +#define TETRIS_BUF "*tetris*" +#define TETRIS_MODE "tetris-mode" +#define TETRIS_VAR_BOARD "board" +#define TETRIS_VAR_NEXT "next_piece" + +typedef struct tetris_state { + ecex_t *ed; + int *board; + int piece; + int rot; + int x; + int y; + int next_piece; + unsigned int rng; + int score; + int lines; + int level; + int game_over; + int paused; + int last_drop_ms; + int drop_interval_ms; + int log_tick_count; + int log_draw_count; + int log_state_id; +} tetris_state_t; + +static int tetris_idx(int x, int y) { + return y * TETRIS_W + x; +} + +static int tetris_shape_cell(int piece, int rot, int col, int row) { + return ecex_tetris_shape_cell(piece, rot, col, row); +} + +static int *tetris_board(ecex_t *ed, tetris_state_t *s) { + if (!ed || !s) return 0; + return (int *)ecex_var_get_or_alloc(ed, s, TETRIS_VAR_BOARD, (size_t)TETRIS_CELLS, sizeof(int)); +} + +static int tetris_next_piece(ecex_t *ed, tetris_state_t *s) { + return ecex_var_i32(ed, s, TETRIS_VAR_NEXT, 0); +} + +static void tetris_set_next_piece(ecex_t *ed, tetris_state_t *s, int piece) { + if (piece < 0) piece = 0; + if (piece > 6) piece = piece % 7; + ecex_var_i32_set_scalar(ed, s, TETRIS_VAR_NEXT, piece); +} + +static void tetris_clear_board(ecex_t *ed, tetris_state_t *s) { + int *board; + if (!s) { ecex_log("tetris_clear_board: null state"); return; } + board = tetris_board(ed, s); + ecex_log_ptr("tetris_clear_board: board=", board); + ecex_mem_zero(board, (size_t)TETRIS_CELLS * sizeof(int)); + ecex_log("tetris_clear_board: done"); +} + +static int tetris_pick(tetris_state_t *s) { + int piece; + if (!s) { ecex_log("tetris_pick: null state"); return 0; } + piece = ecex_random_bounded(7); + if (piece < 0) piece = 0; + if (piece > 6) piece = piece % 7; + ecex_log_int("tetris_pick: piece=", piece); + return piece; +} + +static int tetris_collides(ecex_t *ed, tetris_state_t *s, int piece, int rot, int px, int py) { + int r; + int c; + + int *board = tetris_board(ed, s); + if (!s || !board) return 1; + for (r = 0; r < 4; ++r) { + for (c = 0; c < 4; ++c) { + int x; + int y; + if (!tetris_shape_cell(piece, rot, c, r)) continue; + + x = px + c; + y = py + r; + if (x < 0 || x >= TETRIS_W || y >= TETRIS_H) return 1; + if (y >= 0 && ecex_i32_get(board, (size_t)tetris_idx(x, y))) return 1; + } + } + + return 0; +} + +static void tetris_spawn(ecex_t *ed, tetris_state_t *s) { + int queued; + + ecex_log_ptr("tetris_spawn: state=", s); + if (!s) return; + + /* The sidebar must show the piece that will become active on the next + * spawn. Keep that queued value in the host variable registry, then consume + * it here and immediately replace it with a freshly-picked preview. */ + queued = tetris_next_piece(ed, s); + if (queued < 0 || queued > 6) queued = tetris_pick(s); + s->piece = queued; + tetris_set_next_piece(ed, s, tetris_pick(s)); + + ecex_log_int("tetris_spawn: current_piece=", s->piece); + ecex_log_int("tetris_spawn: preview_piece=", tetris_next_piece(ed, s)); + + s->rot = 0; + s->x = 3; + s->y = -1; + s->last_drop_ms = 0; + + if (tetris_collides(ed, s, s->piece, s->rot, s->x, s->y)) { + ecex_log("tetris_spawn: immediate collision -> game over"); + s->game_over = 1; + } +} + +static void tetris_reset(ecex_t *ed, tetris_state_t *s) { + ecex_log_ptr("tetris_reset: state=", s); + if (!s) return; + + tetris_clear_board(ed, s); + ecex_log("tetris_reset: after clear"); + /* Avoid calling libc/time-returning host functions from CCDJIT plugin code. + * Seed from stable host-owned addresses instead; good enough for a toy game + * and much safer for the tiny JIT ABI. */ + s->rng = 0x9e3779b9u ^ (unsigned int)(size_t)s ^ (unsigned int)(size_t)tetris_board(ed, s); + ecex_log_int("tetris_reset: rng=", (int)s->rng); + s->score = 0; + s->lines = 0; + s->level = 1; + s->game_over = 0; + s->paused = 0; + s->last_drop_ms = 0; + s->drop_interval_ms = 650; + tetris_set_next_piece(ed, s, tetris_pick(s)); + s->log_tick_count = 0; + s->log_draw_count = 0; + ecex_log("tetris_reset: spawning first piece"); + tetris_spawn(ed, s); + ecex_log("tetris_reset: done"); +} + +static void tetris_lock(ecex_t *ed, tetris_state_t *s) { + ecex_log_ptr("tetris_lock: state=", s); + int r; + int c; + int y; + int cleared = 0; + int *board; + + if (!s) return; + board = tetris_board(ed, s); + if (!board) return; + + for (r = 0; r < 4; ++r) { + for (c = 0; c < 4; ++c) { + int x; + int yy; + if (!tetris_shape_cell(s->piece, s->rot, c, r)) continue; + + x = s->x + c; + yy = s->y + r; + if (x >= 0 && x < TETRIS_W && yy >= 0 && yy < TETRIS_H) { + ecex_i32_set(board, (size_t)tetris_idx(x, yy), s->piece + 1); + } + } + } + + for (y = TETRIS_H - 1; y >= 0; --y) { + int x; + int full = 1; + for (x = 0; x < TETRIS_W; ++x) { + if (!ecex_i32_get(board, (size_t)tetris_idx(x, y))) { + full = 0; + break; + } + } + + if (!full) continue; + ++cleared; + + { + int yy; + for (yy = y; yy > 0; --yy) { + for (x = 0; x < TETRIS_W; ++x) { + ecex_i32_set(board, (size_t)tetris_idx(x, yy), ecex_i32_get(board, (size_t)tetris_idx(x, yy - 1))); + } + } + } + + for (x = 0; x < TETRIS_W; ++x) ecex_i32_set(board, (size_t)tetris_idx(x, 0), 0); + ++y; + } + + if (cleared) { + int points = 0; + if (cleared == 1) points = 100; + else if (cleared == 2) points = 300; + else if (cleared == 3) points = 500; + else points = 800; + + s->score += points * s->level; + s->lines += cleared; + s->level = 1 + s->lines / 10; + s->drop_interval_ms = 650 - (s->level - 1) * 45; + if (s->drop_interval_ms < 80) s->drop_interval_ms = 80; + } + + ecex_log_int("tetris_lock: cleared=", cleared); + tetris_spawn(ed, s); +} + + +static int tetris_piece_has_visible_cells(tetris_state_t *s) { + int r; + int c; + + if (!s) return 0; + for (r = 0; r < 4; ++r) { + for (c = 0; c < 4; ++c) { + int y; + if (!tetris_shape_cell(s->piece, s->rot, c, r)) continue; + y = s->y + r; + if (y >= 0) return 1; + } + } + return 0; +} + +static int tetris_top_out_if_hidden(tetris_state_t *s, const char *where) { + if (!s) return 1; + if (tetris_piece_has_visible_cells(s)) return 0; + ecex_log(where); + ecex_log_int("tetris_top_out: piece=", s->piece); + ecex_log_int("tetris_top_out: y=", s->y); + s->game_over = 1; + return 1; +} + +static int tetris_soft_drop(ecex_t *ed, tetris_state_t *s) { + if (!s) { ecex_log("tetris_soft_drop: null state"); return 0; } + if (s->game_over || s->paused) return 0; + + if (!tetris_collides(ed, s, s->piece, s->rot, s->x, s->y + 1)) { + ++s->y; + return 1; + } + + ecex_log_int("tetris_soft_drop: locking piece=", s->piece); + ecex_log_int("tetris_soft_drop: y=", s->y); + if (tetris_top_out_if_hidden(s, "tetris_soft_drop: hidden lock -> game over")) return 1; + tetris_lock(ed, s); + return 0; +} + + +static int tetris_move_horizontal(ecex_t *ed, tetris_state_t *s, int dx) { + int old_x; + int old_y; + int old_rot; + + if (!s) { ecex_log("tetris_move_horizontal: null state"); return 0; } + if (s->game_over || s->paused) return 0; + + old_x = s->x; + old_y = s->y; + old_rot = s->rot; + + if (!tetris_collides(ed, s, s->piece, s->rot, old_x + dx, old_y)) { + s->x = old_x + dx; + /* Horizontal movement must never vertical-kick the active piece. + * Keep these assignments explicit because plugin/JIT callback bugs are + * otherwise very hard to distinguish from game movement. */ + s->y = old_y; + s->rot = old_rot; + + /* Restart the gravity phase after manual lateral input. This avoids the + * visual "bop" where a key repeat lands on the same frame as gravity + * and appears to couple side movement with a vertical step. */ + s->last_drop_ms = 0; + ecex_log_int("tetris_move_horizontal: x=", s->x); + ecex_log_int("tetris_move_horizontal: y=", s->y); + return 1; + } + + s->x = old_x; + s->y = old_y; + s->rot = old_rot; + return 0; +} + +static void tetris_hard_drop(ecex_t *ed, tetris_state_t *s) { + int moved = 0; + if (!s || s->game_over || s->paused) return; + + while (!tetris_collides(ed, s, s->piece, s->rot, s->x, s->y + 1)) { + ++s->y; + ++moved; + } + + s->score += moved * 2; + if (tetris_top_out_if_hidden(s, "tetris_hard_drop: hidden lock -> game over")) return; + tetris_lock(ed, s); +} + +static tetris_state_t *tetris_state_for_ed(ecex_t *ed) { + buffer_t *buffer; + tetris_state_t *s; + ecex_log("tetris_state_for_ed: enter"); + buffer = ecex_find_buffer(ed, TETRIS_BUF); + ecex_log_ptr("tetris_state_for_ed: buffer=", buffer); + if (!buffer) return 0; + s = (tetris_state_t *)ecex_buffer_renderer_userdata(buffer); + ecex_log_ptr("tetris_state_for_ed: state=", s); + return s; +} + +static int tetris_alpha8(int alpha) { + if (alpha < 0) return 0; + if (alpha > 255) return 255; + return alpha; +} + +static void tetris_color(ecex_draw_context_t *ctx, int cell, int alpha) { + alpha = tetris_alpha8(alpha); + if (cell == 1) ecex_draw_color_rgba8(ctx, 51, 191, 242, alpha); + else if (cell == 2) ecex_draw_color_rgba8(ctx, 242, 217, 51, alpha); + else if (cell == 3) ecex_draw_color_rgba8(ctx, 179, 89, 242, alpha); + else if (cell == 4) ecex_draw_color_rgba8(ctx, 77, 217, 89, alpha); + else if (cell == 5) ecex_draw_color_rgba8(ctx, 242, 64, 64, alpha); + else if (cell == 6) ecex_draw_color_rgba8(ctx, 64, 102, 242, alpha); + else if (cell == 7) ecex_draw_color_rgba8(ctx, 242, 140, 51, alpha); + else ecex_draw_color_rgba8(ctx, 41, 41, 46, alpha); +} + +static void tetris_draw_piece(ecex_draw_context_t *ctx, + int piece, + int rot, + int px, + int py, + int ox, + int oy, + int cell, + int alpha) { + int r; + int c; + + for (r = 0; r < 4; ++r) { + for (c = 0; c < 4; ++c) { + int bx; + int by; + if (!tetris_shape_cell(piece, rot, c, r)) continue; + + bx = px + c; + by = py + r; + if (by < 0) continue; + tetris_color(ctx, piece + 1, alpha); + ecex_draw_rect_i(ctx, ox + bx * cell + 1, oy + by * cell + 1, cell - 2, cell - 2); + } + } +} + +static int tetris_tick(ecex_t *ed, buffer_t *buffer, int now_ms, void *userdata) { + tetris_state_t *s = (tetris_state_t *)userdata; + (void)ed; + (void)buffer; + + if (!s) { ecex_log("tetris_tick: null state"); return 0; } + s->log_tick_count += 1; + if (s->log_tick_count <= 8 || (s->log_tick_count % 60) == 0) { + ecex_log_int("tetris_tick: count=", s->log_tick_count); + ecex_log_int("tetris_tick: now_ms=", now_ms); + } + + if (s->last_drop_ms <= 0) { + s->last_drop_ms = now_ms; + return 1; + } + + if (!s->game_over && !s->paused && now_ms - s->last_drop_ms >= s->drop_interval_ms) { + ecex_log("tetris_tick: dropping piece"); + tetris_soft_drop(ed, s); + s->last_drop_ms = now_ms; + return 1; + } + + return 0; +} + +static int tetris_draw(ecex_t *ed, buffer_t *buffer, ecex_draw_context_t *ctx, void *userdata) { + tetris_state_t *s = (tetris_state_t *)userdata; + int max_board_w; + int max_board_h; + int cell; + int board_w; + int board_h; + int ox; + int oy; + int sx; + int sy; + int x; + int y; + int *board; + (void)ed; + (void)buffer; + + if (!s) { ecex_log("tetris_draw: null state"); return 0; } + if (!ctx) { ecex_log("tetris_draw: null ctx"); return 0; } + board = tetris_board(ed, s); + if (!board) { ecex_log("tetris_draw: no registry board"); return 0; } + s->log_draw_count += 1; + if (s->log_draw_count <= 8 || (s->log_draw_count % 60) == 0) { + ecex_log_int("tetris_draw: count=", s->log_draw_count); + ecex_log_int("tetris_draw: ctx_w=", (int)ctx->w); + ecex_log_int("tetris_draw: ctx_h=", (int)ctx->h); + } + + if (s->log_draw_count <= 3) ecex_log("tetris_draw: background"); + ecex_draw_color_rgba8(ctx, 20, 23, 28, 255); + ecex_draw_rect_i(ctx, 0, 0, (int)ctx->w, (int)ctx->h); + + max_board_w = ((int)ctx->content_w * 62) / 100; + max_board_h = (int)ctx->content_h - 24; + cell = max_board_w / TETRIS_W; + if (cell * TETRIS_H > max_board_h) cell = max_board_h / TETRIS_H; + if (cell < 6) cell = 6; + + board_w = cell * TETRIS_W; + board_h = cell * TETRIS_H; + ox = (int)ctx->content_x + 12; + oy = (int)ctx->content_y + 12; + + if (s->log_draw_count <= 3) ecex_log("tetris_draw: board frame"); + ecex_draw_color_rgba8(ctx, 8, 9, 12, 255); + ecex_draw_rect_i(ctx, ox - 6, oy - 6, board_w + 12, board_h + 12); + ecex_draw_color_rgba8(ctx, 97, 102, 115, 255); + ecex_draw_rect_outline_i(ctx, ox - 6, oy - 6, board_w + 12, board_h + 12, 2); + + if (s->log_draw_count <= 3) ecex_log("tetris_draw: cells"); + for (y = 0; y < TETRIS_H; ++y) { + for (x = 0; x < TETRIS_W; ++x) { + int cell_value = ecex_i32_get(board, (size_t)tetris_idx(x, y)); + tetris_color(ctx, cell_value, cell_value ? 255 : 140); + ecex_draw_rect_i(ctx, ox + x * cell + 1, oy + y * cell + 1, cell - 2, cell - 2); + } + } + + if (s->log_draw_count <= 3) ecex_log("tetris_draw: active piece"); + tetris_draw_piece(ctx, s->piece, s->rot, s->x, s->y, ox, oy, cell, 255); + + sx = ox + board_w + 28; + sy = oy; + + if (s->log_draw_count <= 3) ecex_log("tetris_draw: sidebar"); + ecex_draw_color_rgba8(ctx, 235, 235, 214, 255); + ecex_log("tetris_draw: sidebar title"); + ecex_draw_label_i(ctx, sx, sy, 1); + sy += ((int)ctx->line_height * 16) / 10; + + ecex_draw_stat_i(ctx, sx, sy, 2, s->score); + sy += (int)ctx->line_height; + + ecex_draw_stat_i(ctx, sx, sy, 3, s->lines); + sy += (int)ctx->line_height; + + ecex_draw_stat_i(ctx, sx, sy, 4, s->level); + sy += ((int)ctx->line_height * 15) / 10; + + ecex_draw_label_i(ctx, sx, sy, 5); + sy += ((int)ctx->line_height * 8) / 10; + ecex_log_int("tetris_draw: preview_piece=", tetris_next_piece(ed, s)); + ecex_draw_tetris_preview_i(ctx, tetris_next_piece(ed, s), sx, sy, (cell * 72) / 100, 255); + sy += cell * 4; + + ecex_draw_color_rgba8(ctx, 184, 189, 199, 255); + ecex_log("tetris_draw: sidebar help"); + ecex_draw_label_i(ctx, sx, sy, 6); sy += (int)ctx->line_height; + ecex_draw_label_i(ctx, sx, sy, 7); sy += (int)ctx->line_height; + ecex_draw_label_i(ctx, sx, sy, 8); sy += (int)ctx->line_height; + ecex_draw_label_i(ctx, sx, sy, 9); sy += (int)ctx->line_height; + ecex_draw_label_i(ctx, sx, sy, 10); sy += (int)ctx->line_height; + ecex_draw_label_i(ctx, sx, sy, 11); + + if (s->paused || s->game_over) { + ecex_draw_color_rgba8(ctx, 0, 0, 0, 174); + ecex_draw_rect_i(ctx, ox, oy + (board_h * 43) / 100, board_w, ((int)ctx->line_height * 22) / 10); + ecex_draw_color_rgba8(ctx, 255, 235, 89, 255); + ecex_draw_label_i(ctx, ox + cell, oy + (board_h * 43) / 100 + ((int)ctx->line_height * 55) / 100, s->game_over ? 12 : 13); + } + + if (s->log_draw_count <= 3) ecex_log("tetris_draw: done"); + return 0; +} + +static void tetris_free_state(void *userdata) { + tetris_state_t *s = (tetris_state_t *)userdata; + if (!s) return; + ecex_var_free_owner(s->ed, s); + ecex_config_free(s); +} + +static int cmd_tetris(ecex_t *ed) { + buffer_t *buffer; + tetris_state_t *s; + + ecex_log("cmd_tetris: enter"); + ecex_log_ptr("cmd_tetris: ed=", ed); + if (!ed) { ecex_log("cmd_tetris: no editor"); return -1; } + + buffer = ecex_find_buffer(ed, TETRIS_BUF); + ecex_log_ptr("cmd_tetris: existing buffer=", buffer); + if (!buffer) { + ecex_log("cmd_tetris: creating interactive buffer"); + buffer = ecex_create_interactive_buffer(ed, TETRIS_BUF); + } + ecex_log_ptr("cmd_tetris: buffer=", buffer); + if (!buffer) { ecex_log("cmd_tetris: buffer creation failed"); return -1; } + + s = (tetris_state_t *)ecex_buffer_renderer_userdata(buffer); + ecex_log_ptr("cmd_tetris: existing state=", s); + if (!s) { + ecex_log_int("cmd_tetris: allocating state bytes=", (int)sizeof(*s)); + s = (tetris_state_t *)ecex_config_calloc(1, sizeof(*s)); + ecex_log_ptr("cmd_tetris: allocated state=", s); + if (!s) { ecex_log("cmd_tetris: allocation failed"); return -1; } + s->ed = ed; + ecex_log_int("cmd_tetris: registry board bytes=", (int)((size_t)TETRIS_CELLS * sizeof(int))); + ecex_log_ptr("cmd_tetris: registry board=", tetris_board(ed, s)); + if (!tetris_board(ed, s)) { + ecex_log("cmd_tetris: registry board allocation failed"); + ecex_var_free_owner(ed, s); + ecex_config_free(s); + return -1; + } + + tetris_reset(ed, s); + + ecex_log("cmd_tetris: setting renderer"); + if (ecex_buffer_set_renderer(buffer, tetris_draw, s, tetris_free_state, ECEX_RENDER_REPLACE_CONTENT) != 0) { + ecex_log("cmd_tetris: set_renderer failed"); + ecex_config_free(s); + return -1; + } + + ecex_log("cmd_tetris: setting animation"); + if (ecex_buffer_set_animation_ms(buffer, tetris_tick, s, 0, 60) != 0) { + ecex_log("cmd_tetris: set_animation failed"); + ecex_buffer_clear_renderer(buffer); + return -1; + } + } + + ecex_log("cmd_tetris: replacing text"); + if (ecex_buffer_replace_text(buffer, "Tetris renderer. Press n for a new game, q to quit the window.\n") != 0) { + ecex_log("cmd_tetris: replace text failed"); + return -1; + } + + ecex_log("cmd_tetris: setting modified false"); + ecex_buffer_set_modified(buffer, 0); + ecex_log("cmd_tetris: setting major mode"); + if (ecex_buffer_set_major_mode_by_name(ed, buffer, TETRIS_MODE) != 0) { ecex_log("cmd_tetris: set mode failed"); return -1; } + ecex_log("cmd_tetris: switching buffer"); + if (ecex_switch_buffer(ed, TETRIS_BUF) != 0) { ecex_log("cmd_tetris: switch failed"); return -1; } + ecex_log("cmd_tetris: success"); + return 0; +} + +static int cmd_tetris_new(ecex_t *ed) { + ecex_log("cmd_tetris_new: enter"); + tetris_state_t *s = tetris_state_for_ed(ed); + if (!s) return cmd_tetris(ed); + tetris_reset(ed, s); + return 0; +} + +static int cmd_tetris_left(ecex_t *ed) { + ecex_log("cmd_tetris_left: enter"); + tetris_move_horizontal(ed, tetris_state_for_ed(ed), -1); + return 0; +} + +static int cmd_tetris_right(ecex_t *ed) { + ecex_log("cmd_tetris_right: enter"); + tetris_move_horizontal(ed, tetris_state_for_ed(ed), 1); + return 0; +} + +static int cmd_tetris_down(ecex_t *ed) { + ecex_log("cmd_tetris_down: enter"); + tetris_state_t *s = tetris_state_for_ed(ed); + if (s) { + if (tetris_soft_drop(ed, s)) s->score += 1; + s->last_drop_ms = 0; + } + return 0; +} + +static int cmd_tetris_rotate(ecex_t *ed) { + ecex_log("cmd_tetris_rotate: enter"); + tetris_state_t *s = tetris_state_for_ed(ed); + int nr; + + if (!s || s->game_over || s->paused) return 0; + + nr = (s->rot + 1) & 3; + if (!tetris_collides(ed, s, s->piece, nr, s->x, s->y)) { + s->rot = nr; + } else if (!tetris_collides(ed, s, s->piece, nr, s->x - 1, s->y)) { + --s->x; + s->rot = nr; + } else if (!tetris_collides(ed, s, s->piece, nr, s->x + 1, s->y)) { + ++s->x; + s->rot = nr; + } + + return 0; +} + +static int cmd_tetris_drop(ecex_t *ed) { + ecex_log("cmd_tetris_drop: enter"); + tetris_hard_drop(ed, tetris_state_for_ed(ed)); + return 0; +} + +static int cmd_tetris_pause(ecex_t *ed) { + ecex_log("cmd_tetris_pause: enter"); + tetris_state_t *s = tetris_state_for_ed(ed); + if (s && !s->game_over) { + s->paused = !s->paused; + s->last_drop_ms = 0; + } + return 0; +} + +static int cmd_tetris_quit(ecex_t *ed) { + ecex_log("cmd_tetris_quit: enter"); + return ecex_execute_command(ed, "quit-window"); +} + +int ecex_tetris_plugin(ecex_t *ed) { + ecex_log("ecex_tetris_plugin: enter"); + ECEX_CONFIG_MODE(TETRIS_MODE); + ECEX_CONFIG_COMMAND("tetris", cmd_tetris); + ECEX_CONFIG_COMMAND("tetris-new", cmd_tetris_new); + ECEX_CONFIG_COMMAND("tetris-left", cmd_tetris_left); + ECEX_CONFIG_COMMAND("tetris-right", cmd_tetris_right); + ECEX_CONFIG_COMMAND("tetris-down", cmd_tetris_down); + ECEX_CONFIG_COMMAND("tetris-rotate", cmd_tetris_rotate); + ECEX_CONFIG_COMMAND("tetris-drop", cmd_tetris_drop); + ECEX_CONFIG_COMMAND("tetris-pause", cmd_tetris_pause); + ECEX_CONFIG_COMMAND("tetris-quit", cmd_tetris_quit); + + ECEX_CONFIG_MODE_BIND(TETRIS_MODE, "LEFT", "tetris-left"); + ECEX_CONFIG_MODE_BIND(TETRIS_MODE, "h", "tetris-left"); + ECEX_CONFIG_MODE_BIND(TETRIS_MODE, "RIGHT", "tetris-right"); + ECEX_CONFIG_MODE_BIND(TETRIS_MODE, "l", "tetris-right"); + ECEX_CONFIG_MODE_BIND(TETRIS_MODE, "DOWN", "tetris-down"); + ECEX_CONFIG_MODE_BIND(TETRIS_MODE, "j", "tetris-down"); + ECEX_CONFIG_MODE_BIND(TETRIS_MODE, "UP", "tetris-rotate"); + ECEX_CONFIG_MODE_BIND(TETRIS_MODE, "k", "tetris-rotate"); + ECEX_CONFIG_MODE_BIND(TETRIS_MODE, "SPC", "tetris-drop"); + ECEX_CONFIG_MODE_BIND(TETRIS_MODE, "p", "tetris-pause"); + ECEX_CONFIG_MODE_BIND(TETRIS_MODE, "n", "tetris-new"); + ECEX_CONFIG_MODE_BIND(TETRIS_MODE, "q", "tetris-quit"); + + ecex_log("ecex_tetris_plugin: registered all commands and keybinds"); + return 0; +} + +#ifndef ECEX_NO_STANDALONE_CONFIG +ECEX_CONFIG_BEGIN + ECEX_CONFIG_INCLUDE(ecex_tetris_plugin); +ECEX_CONFIG_END +#endif diff --git a/include/app.h b/include/app.h index cb09b04..a420f00 100644 --- a/include/app.h +++ b/include/app.h @@ -46,8 +46,8 @@ typedef struct app { size_t prompt_input_len; /* - * File/path prompt completion state for find-file and write-file. - * prompt_completion_query stores the original typed path/query while + * Prompt completion state for path prompts and buffer prompts. + * prompt_completion_query stores the original typed query while * prompt_input is temporarily replaced with the selected candidate. */ int prompt_completion_active; @@ -75,6 +75,10 @@ typedef struct app { int current_mods; + buffer_t *mouse_capture_buffer; + int mouse_capture_button; + int mouse_capture_active; + /* * GLFW sends key events and text events separately. When a printable * key is consumed as part of a command sequence, suppress the matching diff --git a/include/buffers.h b/include/buffers.h index 503dfb8..148d718 100644 --- a/include/buffers.h +++ b/include/buffers.h @@ -75,4 +75,40 @@ int buffer_add_interactive_action(buffer_t *buffer, ecex_interactive_line_action_t *buffer_interactive_action_at_line(buffer_t *buffer, size_t line); +int ecex_buffer_set_renderer(buffer_t *buffer, + ecex_buffer_render_fn fn, + void *userdata, + ecex_buffer_userdata_free_fn free_fn, + int flags); +int ecex_buffer_clear_renderer(buffer_t *buffer); +int ecex_buffer_has_renderer(buffer_t *buffer); +void *ecex_buffer_renderer_userdata(buffer_t *buffer); +int ecex_buffer_set_mouse_handler(buffer_t *buffer, + ecex_buffer_mouse_fn fn, + void *userdata, + ecex_buffer_userdata_free_fn free_fn); +int ecex_buffer_clear_mouse_handler(buffer_t *buffer); +int ecex_buffer_has_mouse_handler(buffer_t *buffer); +void *ecex_buffer_mouse_userdata(buffer_t *buffer); +int ecex_buffer_set_animation(buffer_t *buffer, + ecex_buffer_tick_fn fn, + void *userdata, + ecex_buffer_userdata_free_fn free_fn, + double fps); +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); +int ecex_buffer_clear_animation(buffer_t *buffer); +int ecex_buffer_is_animating(buffer_t *buffer); +void *ecex_buffer_animation_userdata(buffer_t *buffer); +int ecex_tick_animations(ecex_t *ed, double now_seconds); +int ecex_buffer_replace_text(buffer_t *buffer, const char *text); +void ecex_buffer_set_modified(buffer_t *buffer, int modified); +int ecex_buffer_text_len(buffer_t *buffer); +int ecex_buffer_scroll_line(buffer_t *buffer); +int ecex_buffer_line_count_i(buffer_t *buffer); +int ecex_buffer_line_copy(buffer_t *buffer, int line, char *out, int out_cap); + #endif diff --git a/include/completion.h b/include/completion.h new file mode 100644 index 0000000..86c41f0 --- /dev/null +++ b/include/completion.h @@ -0,0 +1,19 @@ +#ifndef ECEX_COMPLETION_H +#define ECEX_COMPLETION_H + +#include + +typedef struct ecex_completion_item { + char *value; + int score; + int is_dir; + size_t order; +} ecex_completion_item_t; + +int ecex_ascii_strncasecmp(const char *a, const char *b, size_t n); +int ecex_ascii_contains_ci(const char *haystack, const char *needle); +int ecex_fuzzy_score(const char *candidate, const char *query); +int ecex_completion_item_compare(const void *a, const void *b); +void ecex_completion_items_free(ecex_completion_item_t *items, size_t count); + +#endif diff --git a/include/ecex.h b/include/ecex.h index cefee9b..efcbe5a 100644 --- a/include/ecex.h +++ b/include/ecex.h @@ -4,10 +4,93 @@ #include "types.h" #include "buffers.h" #include "eval.h" +#include "path.h" +#include "media.h" + + +typedef struct ecex_config_command { + const char *name; + ecex_command_fn fn; +} ecex_config_command_t; + +typedef struct ecex_config_keybind { + const char *key; + const char *command; +} ecex_config_keybind_t; + +typedef struct ecex_config_mode_keybind { + const char *mode; + const char *key; + const char *command; +} ecex_config_mode_keybind_t; ecex_t *ecex_new(void); void ecex_free(ecex_t *ed); +/* Small runtime services exported to JIT configs/plugins. Prefer these over + * calling libc directly from config code; the tiny JIT resolver is intentionally + * conservative and host-exported wrappers are more reliable. */ +void *ecex_config_alloc(size_t size); +void *ecex_config_calloc(size_t count, size_t size); +void ecex_config_free(void *ptr); +double ecex_time_seconds(void); +void ecex_log(const char *message); +void ecex_log_int(const char *message, int value); +void ecex_log_double(const char *message, double value); +void ecex_log_ptr(const char *message, const void *ptr); +void ecex_mem_zero(void *ptr, size_t size); +int ecex_i32_get(const int *items, size_t index); +void ecex_i32_set(int *items, size_t index, int value); +int ecex_prng_next_bounded(unsigned int *state, int bound); +int ecex_random_bounded(int bound); +int ecex_tetris_shape_cell(int piece, int rot, int col, int row); + +/* Host-owned variable registry for JIT plugins. Plugins can keep persistent + * scalar values, dynamic arrays, or static host buffers behind a stable owner + * pointer and a name instead of relying on fragile JIT struct/array storage. */ +void *ecex_var_get(ecex_t *ed, void *owner, const char *name); +void *ecex_var_get_or_alloc(ecex_t *ed, void *owner, const char *name, size_t count, size_t elem_size); +int ecex_var_bind_static(ecex_t *ed, void *owner, const char *name, void *data, size_t count, size_t elem_size); +int ecex_var_free(ecex_t *ed, void *owner, const char *name); +int ecex_var_free_owner(ecex_t *ed, void *owner); +int ecex_var_i32_get(ecex_t *ed, void *owner, const char *name, size_t index, int fallback); +int ecex_var_i32_set(ecex_t *ed, void *owner, const char *name, size_t index, int value); +int ecex_var_i32(ecex_t *ed, void *owner, const char *name, int fallback); +int ecex_var_i32_set_scalar(ecex_t *ed, void *owner, const char *name, int value); + +/* Host-owned object allocator for plugin state. Returned pointers are tracked + * by the editor and can be freed through the API. Prefer this for callback + * userdata over plugin/local allocations when the object has callback lifetime. + */ +void *ecex_object_alloc(ecex_t *ed, size_t size); +void *ecex_object_calloc(ecex_t *ed, size_t count, size_t size); +int ecex_object_free(ecex_t *ed, void *object); +int ecex_object_valid(ecex_t *ed, void *object); +int ecex_object_i32_get(ecex_t *ed, void *object, size_t byte_offset, int fallback); +int ecex_object_i32_set(ecex_t *ed, void *object, size_t byte_offset, int value); +void *ecex_object_ptr_get(ecex_t *ed, void *object, size_t byte_offset); +int ecex_object_ptr_set(ecex_t *ed, void *object, size_t byte_offset, void *value); + +/* Host-owned text registry for plugins that need to render arbitrary text. + * Plugins copy text into host storage, then draw by owner/id. This avoids + * passing JIT-owned literals or stack strings into the normal text renderer. */ +int ecex_text_set(ecex_t *ed, void *owner, int id, const char *text, int len); +int ecex_text_set_buffer_title(ecex_t *ed, void *owner, int id, buffer_t *buffer); +int ecex_text_free(ecex_t *ed, void *owner, int id); +int ecex_text_free_owner(ecex_t *ed, void *owner); + +int ecex_buffer_text_len(buffer_t *buffer); +int ecex_buffer_scroll_line(buffer_t *buffer); +int ecex_buffer_line_count_i(buffer_t *buffer); +int ecex_buffer_line_copy(buffer_t *buffer, int line, char *out, int out_cap); +int ecex_markdown_draw_line_from_buffer_i(ecex_draw_context_t *ctx, void *owner, buffer_t *buffer, int line, int y, int in_code); +int ecex_markdown_body_y_i(ecex_draw_context_t *ctx); +int ecex_draw_context_height_i(ecex_draw_context_t *ctx); +int ecex_draw_context_line_height_i(ecex_draw_context_t *ctx); + +int ecex_register_file_handler(ecex_t *ed, const char *extension, ecex_file_handler_fn fn); +int ecex_run_file_handlers(ecex_t *ed, buffer_t *buffer); + int ecex_reserve_buffers(ecex_t *ed, size_t needed); int ecex_add_buffer(ecex_t *ed, buffer_t *buffer); @@ -20,6 +103,7 @@ buffer_t *ecex_find_buffer(ecex_t *ed, const char *name); int ecex_switch_buffer(ecex_t *ed, const char *name); buffer_t *ecex_current_buffer(ecex_t *ed); +buffer_t *ecex_other_buffer(ecex_t *ed); ecex_window_t *ecex_current_window(ecex_t *ed); size_t ecex_window_count(ecex_t *ed); int ecex_sync_current_buffer(ecex_t *ed); @@ -35,6 +119,9 @@ int ecex_balance_windows(ecex_t *ed); int ecex_next_buffer(ecex_t *ed); int ecex_previous_buffer(ecex_t *ed); int ecex_kill_buffer(ecex_t *ed, const char *name); +int ecex_kill_buffer_force(ecex_t *ed, const char *name); +int ecex_has_modified_buffers(ecex_t *ed); +int ecex_validate_bindings(ecex_t *ed); int ecex_keep_jit_module(ecex_t *ed, void *module); @@ -42,6 +129,12 @@ int ecex_set_config_path(ecex_t *ed, const char *path); const char *ecex_config_path(ecex_t *ed); int ecex_reload_config(ecex_t *ed); +int ecex_config_register_commands(ecex_t *ed, const ecex_config_command_t *commands, size_t count); +int ecex_config_bind_keys(ecex_t *ed, const ecex_config_keybind_t *bindings, size_t count); +int ecex_config_bind_mode_keys(ecex_t *ed, const ecex_config_mode_keybind_t *bindings, size_t count); +int ecex_config_define_modes(ecex_t *ed, const char *const *modes, size_t count); +int ecex_apply_theme(ecex_t *ed, const ecex_theme_t *theme); + int ecex_register_command(ecex_t *ed, const char *name, ecex_command_fn fn); int ecex_execute_command(ecex_t *ed, const char *name); void ecex_set_clipboard_callbacks(ecex_t *ed, @@ -77,6 +170,100 @@ int ecex_interactive_append_line(ecex_t *ed, void *userdata); int ecex_interactive_activate_current_line(ecex_t *ed); +/* + * Attach an immediate-mode renderer to a buffer. The callback runs while the + * buffer's window is being drawn and may call ecex_draw_* functions with + * coordinates local to that window. Pass ECEX_RENDER_REPLACE_CONTENT to draw a + * fully custom buffer; pass ECEX_RENDER_OVERLAY to draw on top of normal text. + */ +int ecex_buffer_set_renderer(buffer_t *buffer, + ecex_buffer_render_fn fn, + void *userdata, + ecex_buffer_userdata_free_fn free_fn, + int flags); +int ecex_buffer_clear_renderer(buffer_t *buffer); +int ecex_buffer_has_renderer(buffer_t *buffer); +void *ecex_buffer_renderer_userdata(buffer_t *buffer); + +/* + * Optional mouse/pointer input for rendered buffers. Coordinates are integer + * pixels local to the buffer window. Return nonzero from the callback when the + * event changes state and should trigger a redraw. Prefer this host-dispatched + * path over reading windowing-system globals from a plugin. + */ +int ecex_buffer_set_mouse_handler(buffer_t *buffer, + ecex_buffer_mouse_fn fn, + void *userdata, + ecex_buffer_userdata_free_fn free_fn); +int ecex_buffer_clear_mouse_handler(buffer_t *buffer); +int ecex_buffer_has_mouse_handler(buffer_t *buffer); +void *ecex_buffer_mouse_userdata(buffer_t *buffer); + +/* + * Host-driven animation for rendered buffers. The editor calls tick_fn at up to + * fps while the main loop is alive; returning nonzero marks the frame dirty and + * triggers a redraw. Use this instead of timers, sleep, clock calls, or threads + * in CCDJIT plugins. fps <= 0 defaults to 60; values above 240 are clamped. + */ +int ecex_buffer_set_animation(buffer_t *buffer, + ecex_buffer_tick_fn fn, + void *userdata, + ecex_buffer_userdata_free_fn free_fn, + double fps); +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); +int ecex_buffer_clear_animation(buffer_t *buffer); +int ecex_buffer_is_animating(buffer_t *buffer); +void *ecex_buffer_animation_userdata(buffer_t *buffer); +int ecex_tick_animations(ecex_t *ed, double now_seconds); + +int ecex_buffer_replace_text(buffer_t *buffer, const char *text); +void ecex_buffer_set_modified(buffer_t *buffer, int modified); + +/* Drawing helpers. x/y are local to the buffer window; ecex_draw_text uses + * top-left coordinates. ecex_draw_rgba expects tightly packed RGBA8 pixels. */ +void ecex_draw_set_color(ecex_draw_context_t *ctx, float r, float g, float b, float a); +void ecex_draw_rect(ecex_draw_context_t *ctx, float x, float y, float w, float h); +void ecex_draw_rect_outline(ecex_draw_context_t *ctx, float x, float y, float w, float h, float thickness); +void ecex_draw_line(ecex_draw_context_t *ctx, float x1, float y1, float x2, float y2, float thickness); +void ecex_draw_text(ecex_draw_context_t *ctx, float x, float y, const char *text); +void ecex_draw_text_aligned(ecex_draw_context_t *ctx, float x, float y, float w, const char *text, int align); +float ecex_draw_text_width(ecex_draw_context_t *ctx, const char *text); +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); + +/* CCDJIT-safe integer drawing wrappers. Prefer these from plugins when the + * JIT ABI is known to be fragile with float/double arguments. Colors are + * 0..255 RGBA and coordinates are integer pixels local to the buffer window. */ +void ecex_draw_color_rgba8(ecex_draw_context_t *ctx, int r, int g, int b, int a); +void ecex_draw_rect_i(ecex_draw_context_t *ctx, int x, int y, int w, int h); +void ecex_draw_rect_outline_i(ecex_draw_context_t *ctx, int x, int y, int w, int h, int thickness); +void ecex_draw_line_i(ecex_draw_context_t *ctx, int x1, int y1, int x2, int y2, int thickness); +void ecex_draw_text_i(ecex_draw_context_t *ctx, int x, int y, const char *text); +void ecex_draw_text_id_i(ecex_draw_context_t *ctx, void *owner, int id, int x, int y); + +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); +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); +/* Lower-arity Markdown helpers preferred by CCDJIT plugins. Some JIT call + * paths are fragile with 7+ arguments, so these derive layout from ctx. */ +void ecex_draw_markdown_canvas_auto_i(ecex_draw_context_t *ctx, void *owner, int title_id); +void ecex_draw_markdown_line_auto_i(ecex_draw_context_t *ctx, void *owner, int text_id, int y, int style); + +/* CCDJIT-safe host-owned text helpers. Plugins pass integer ids instead of + * pointers to JIT-owned string literals/stack buffers. */ +void ecex_draw_label_i(ecex_draw_context_t *ctx, int x, int y, int label_id); +void ecex_draw_stat_i(ecex_draw_context_t *ctx, int x, int y, int label_id, int value); +void ecex_draw_tetris_preview_i(ecex_draw_context_t *ctx, int piece, int x, int y, int cell, int alpha); + int ecex_find_file(ecex_t *ed, const char *path); int ecex_save_current_buffer(ecex_t *ed); int ecex_write_current_buffer(ecex_t *ed, const char *path); @@ -121,4 +308,36 @@ void ecex_set_search_bg_color(ecex_t *ed, float r, float g, float b); void ecex_set_line_numbers_enabled(ecex_t *ed, int enabled); void ecex_set_current_line_enabled(ecex_t *ed, int enabled); + +/* + * Small config/plugin convenience layer. + * + * Config files are still plain C and must export ecex_config_init(ecex_t *ed), + * but these macros make them safer to write: every registration/bind operation + * is checked and failures abort config loading instead of being ignored. + */ +#define ECEX_ARRAY_COUNT(a) (sizeof(a) / sizeof((a)[0])) +#define ECEX_RGB8(r, g, b) ((float)(r) / 255.0f), ((float)(g) / 255.0f), ((float)(b) / 255.0f) +#define ECEX_CONFIG_BEGIN int ecex_config_init(ecex_t *ed) { +#define ECEX_CONFIG_END return 0; } +#define ECEX_PLUGIN_BEGIN(name) int name(ecex_t *ed) { +#define ECEX_PLUGIN_END return 0; } +#define ECEX_CONFIG_TRY(expr) \ + do { \ + if ((expr) != 0) return -1; \ + } while (0) +#define ECEX_CONFIG_COMMAND(name, fn) ECEX_CONFIG_TRY(ecex_register_command(ed, (name), (fn))) +#define ECEX_CONFIG_BIND(key, command) ECEX_CONFIG_TRY(ecex_bind_key(ed, (key), (command))) +#define ECEX_CONFIG_MODE(name) \ + do { \ + if (ecex_define_major_mode(ed, (name)) == 0) return -1; \ + } while (0) +#define ECEX_CONFIG_MODE_BIND(mode, key, command) ECEX_CONFIG_TRY(ecex_bind_mode_key(ed, (mode), (key), (command))) +#define ECEX_CONFIG_COMMANDS(commands) ECEX_CONFIG_TRY(ecex_config_register_commands(ed, (commands), ECEX_ARRAY_COUNT(commands))) +#define ECEX_CONFIG_BINDS(bindings) ECEX_CONFIG_TRY(ecex_config_bind_keys(ed, (bindings), ECEX_ARRAY_COUNT(bindings))) +#define ECEX_CONFIG_MODE_BINDS(bindings) ECEX_CONFIG_TRY(ecex_config_bind_mode_keys(ed, (bindings), ECEX_ARRAY_COUNT(bindings))) +#define ECEX_CONFIG_MODES(modes) ECEX_CONFIG_TRY(ecex_config_define_modes(ed, (modes), ECEX_ARRAY_COUNT(modes))) +#define ECEX_CONFIG_THEME(theme_ptr) ECEX_CONFIG_TRY(ecex_apply_theme(ed, (theme_ptr))) +#define ECEX_CONFIG_INCLUDE(plugin_init_fn) ECEX_CONFIG_TRY((plugin_init_fn)(ed)) + #endif diff --git a/include/media.h b/include/media.h new file mode 100644 index 0000000..1b8f88d --- /dev/null +++ b/include/media.h @@ -0,0 +1,21 @@ +#ifndef ECEX_MEDIA_H +#define ECEX_MEDIA_H + +#include "types.h" + +#define ECEX_MEDIA_MAX_DIMENSION 1600 + +typedef enum ecex_media_kind { + ECEX_MEDIA_NONE = 0, + ECEX_MEDIA_IMAGE = 1, + ECEX_MEDIA_VIDEO = 2, +} ecex_media_kind_t; + +int ecex_media_open(ecex_t *ed, const char *path); +int ecex_media_load_into_buffer(ecex_t *ed, const char *path, buffer_t *buffer); +int ecex_media_toggle_playback(ecex_t *ed); +int ecex_media_tick(ecex_t *ed, double now_seconds); +void ecex_media_buffer_clear(buffer_t *buffer); +int ecex_media_buffer_has_pixels(buffer_t *buffer); + +#endif diff --git a/include/path.h b/include/path.h new file mode 100644 index 0000000..e76a654 --- /dev/null +++ b/include/path.h @@ -0,0 +1,22 @@ +#ifndef ECEX_PATH_H +#define ECEX_PATH_H + +#include + +int ecex_path_copy(char *out, size_t out_size, const char *text); +char *ecex_path_expand_user(const char *path); +char *ecex_path_join(const char *dir, const char *name); +char *ecex_path_dirname(const char *path); +char *ecex_path_basename_dup(const char *path); +char *ecex_path_normalize(const char *path); +int ecex_path_is_dir(const char *path); +int ecex_path_is_file(const char *path); +int ecex_path_exists(const char *path); +long long ecex_path_file_size(const char *path); +int ecex_path_is_image(const char *path); +int ecex_path_is_previewable_image(const char *path); +int ecex_path_is_video(const char *path); +int ecex_path_is_media(const char *path); +int ecex_path_cwd(char *out, size_t out_size); + +#endif diff --git a/include/types.h b/include/types.h index 1dc2158..800c3d6 100644 --- a/include/types.h +++ b/include/types.h @@ -6,11 +6,18 @@ typedef struct ecex ecex_t; typedef struct buffer buffer_t; typedef struct ecex_window ecex_window_t; +typedef struct ecex_draw_context ecex_draw_context_t; typedef int (*ecex_command_fn)(ecex_t *ed); typedef int (*ecex_interactive_line_fn)(ecex_t *ed, buffer_t *buffer, size_t line, const char *payload, void *userdata); typedef const char *(*ecex_clipboard_get_fn)(void *userdata); typedef void (*ecex_clipboard_set_fn)(void *userdata, const char *text); +typedef int (*ecex_buffer_render_fn)(ecex_t *ed, buffer_t *buffer, ecex_draw_context_t *ctx, void *userdata); +typedef int (*ecex_buffer_tick_fn)(ecex_t *ed, buffer_t *buffer, double now_seconds, void *userdata); +typedef int (*ecex_buffer_tick_ms_fn)(ecex_t *ed, buffer_t *buffer, int now_ms, void *userdata); +typedef int (*ecex_buffer_mouse_fn)(ecex_t *ed, buffer_t *buffer, int event, int x, int y, int button, void *userdata); +typedef int (*ecex_file_handler_fn)(ecex_t *ed, buffer_t *buffer); +typedef void (*ecex_buffer_userdata_free_fn)(void *userdata); typedef enum ecex_prompt_request { ECEX_PROMPT_NONE = 0, @@ -19,12 +26,48 @@ typedef enum ecex_prompt_request { ECEX_PROMPT_EVAL_FILE, ECEX_PROMPT_SWITCH_BUFFER, ECEX_PROMPT_KILL_BUFFER, + ECEX_PROMPT_FORCE_KILL_BUFFER, ECEX_PROMPT_COMPILE, ECEX_PROMPT_GREP, ECEX_PROMPT_ISEARCH_FORWARD, ECEX_PROMPT_ISEARCH_BACKWARD, } ecex_prompt_request_t; + +#define ECEX_RENDER_OVERLAY 0 +#define ECEX_RENDER_REPLACE_CONTENT 1 + +#define ECEX_MOUSE_MOVE 0 +#define ECEX_MOUSE_PRESS 1 +#define ECEX_MOUSE_RELEASE 2 +#define ECEX_MOUSE_DRAG 3 +#define ECEX_MOUSE_BUTTON_LEFT 0 +#define ECEX_MOUSE_BUTTON_RIGHT 1 +#define ECEX_MOUSE_BUTTON_MIDDLE 2 + +typedef enum ecex_draw_text_align { + ECEX_TEXT_ALIGN_LEFT = 0, + ECEX_TEXT_ALIGN_CENTER = 1, + ECEX_TEXT_ALIGN_RIGHT = 2, +} ecex_draw_text_align_t; + +struct ecex_draw_context { + float x; + float y; + float w; + float h; + float content_x; + float content_y; + float content_w; + float content_h; + float font_size; + float line_height; + float char_width; + size_t window_index; + int active; + void *internal; +}; + typedef struct ecex_undo_entry { char *data; size_t len; @@ -98,6 +141,38 @@ struct buffer { struct ecex_interactive_line_action *interactive_actions; size_t interactive_action_cap; size_t interactive_action_count; + + int media_kind; + char *media_path; + int media_width; + int media_height; + unsigned char *media_pixels; + int media_dirty; + unsigned int media_texture; + int media_texture_width; + int media_texture_height; + void *media_pipe; + double media_last_frame_time; + int media_playing; + char media_status[256]; + + ecex_buffer_render_fn render_fn; + void *render_userdata; + ecex_buffer_userdata_free_fn render_userdata_free; + int render_flags; + + ecex_buffer_mouse_fn mouse_fn; + void *mouse_userdata; + ecex_buffer_userdata_free_fn mouse_userdata_free; + + ecex_buffer_tick_fn tick_fn; + ecex_buffer_tick_ms_fn tick_ms_fn; + void *tick_userdata; + ecex_buffer_userdata_free_fn tick_userdata_free; + double tick_interval; + double tick_last_time; + int tick_enabled; + int tick_uses_ms; }; typedef struct ecex_interactive_line_action { @@ -128,6 +203,38 @@ typedef struct ecex_major_mode { char *name; } ecex_major_mode_t; +typedef enum ecex_var_kind { + ECEX_VAR_BYTES = 0, + ECEX_VAR_I32 = 1, +} ecex_var_kind_t; + +typedef struct ecex_var_entry { + void *owner; + char *name; + void *data; + size_t elem_size; + size_t count; + int kind; + int dynamic; +} ecex_var_entry_t; + +typedef struct ecex_text_entry { + void *owner; + int id; + char *text; + size_t len; +} ecex_text_entry_t; + +typedef struct ecex_file_handler { + char *extension; + ecex_file_handler_fn fn; +} ecex_file_handler_t; + +typedef struct ecex_object_entry { + void *ptr; + size_t size; +} ecex_object_entry_t; + struct ecex_window { buffer_t *buffer; float x; @@ -143,6 +250,7 @@ struct ecex { size_t current_buffer_index; buffer_t *current_buffer; + buffer_t *previous_buffer; ecex_window_t *windows; size_t window_cap; @@ -170,6 +278,22 @@ struct ecex { size_t major_mode_count; int next_major_mode_id; + ecex_var_entry_t *vars; + size_t var_cap; + size_t var_count; + + ecex_text_entry_t *texts; + size_t text_cap; + size_t text_count; + + ecex_file_handler_t *file_handlers; + size_t file_handler_cap; + size_t file_handler_count; + + ecex_object_entry_t *objects; + size_t object_cap; + size_t object_count; + char *last_eval_source; char *last_eval_filename; int last_eval_wrap_as_statements; diff --git a/src/app.c b/src/app.c index 3ae368c..9a38e40 100644 --- a/src/app.c +++ b/src/app.c @@ -1,5 +1,7 @@ #include "app.h" #include "common.h" +#include "completion.h" +#include "path.h" #include #include @@ -60,140 +62,12 @@ static void app_set_prompt_input(app_t *app, const char *text) { app->dirty = 1; } -static char *app_expand_user_path(const char *path) { - if (!path) return NULL; - - if (path[0] != '~' || (path[1] != '\0' && path[1] != '/')) { - char *copy = malloc(strlen(path) + 1); - if (!copy) return NULL; - strcpy(copy, path); - return copy; - } - - const char *home = getenv("HOME"); - if (!home || !home[0]) { - char *copy = malloc(strlen(path) + 1); - if (!copy) return NULL; - strcpy(copy, path); - return copy; - } - - size_t home_len = strlen(home); - size_t rest_len = strlen(path + 1); - - char *expanded = malloc(home_len + rest_len + 1); - if (!expanded) return NULL; - - memcpy(expanded, home, home_len); - memcpy(expanded + home_len, path + 1, rest_len + 1); - return expanded; -} - typedef struct command_candidate { const char *name; int score; size_t order; } command_candidate_t; -static int ascii_lower(int c) { - if (c >= 'A' && c <= 'Z') return c - 'A' + 'a'; - return c; -} - -static int ascii_strncasecmp_local(const char *a, const char *b, size_t n) { - for (size_t i = 0; i < n; i++) { - int ac = ascii_lower((unsigned char)a[i]); - int bc = ascii_lower((unsigned char)b[i]); - - if (ac != bc || ac == '\0' || bc == '\0') { - return ac - bc; - } - } - - return 0; -} - -static int ascii_contains_ci(const char *haystack, const char *needle) { - if (!haystack || !needle) return 0; - if (needle[0] == '\0') return 1; - - size_t needle_len = strlen(needle); - - for (size_t i = 0; haystack[i]; i++) { - if (ascii_strncasecmp_local(haystack + i, needle, needle_len) == 0) { - return 1; - } - } - - return 0; -} - -static int command_fuzzy_score(const char *candidate, const char *query) { - if (!candidate || !query) return -1; - - if (query[0] == '\0') { - return 0; - } - - int score = 0; - int consecutive = 0; - int last_match = -1; - - size_t ci = 0; - size_t qi = 0; - - while (candidate[ci] && query[qi]) { - char c = candidate[ci]; - char q = query[qi]; - - if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a'); - if (q >= 'A' && q <= 'Z') q = (char)(q - 'A' + 'a'); - - if (c == q) { - score += 10; - - if ((int)ci == last_match + 1) { - consecutive++; - score += 5 * consecutive; - } else { - consecutive = 0; - } - - if (ci == 0) { - score += 20; - } - - if (ci > 0 && - (candidate[ci - 1] == '-' || - candidate[ci - 1] == '_' || - candidate[ci - 1] == ' ')) { - score += 15; - } - - last_match = (int)ci; - qi++; - } - - ci++; - } - - if (query[qi] != '\0') { - return -1; - } - - score -= (int)strlen(candidate); - - if (strncmp(candidate, query, strlen(query)) == 0) { - score += 100; - } - - if (ascii_contains_ci(candidate, query)) { - score += 75; - } - - return score; -} - static int compare_command_candidates(const void *a, const void *b) { const command_candidate_t *ca = (const command_candidate_t *)a; const command_candidate_t *cb = (const command_candidate_t *)b; @@ -220,7 +94,7 @@ static command_candidate_t *collect_command_candidates(ecex_t *ed, for (size_t i = 0; i < ed->command_count; i++) { const char *name = ed->commands[i].name; - int score = command_fuzzy_score(name, query); + int score = ecex_fuzzy_score(name, query); if (score >= 0) { items[count].name = name; @@ -280,14 +154,14 @@ static int compare_path_candidates(const void *a, const void *b) { const path_candidate_t *pa = (const path_candidate_t *)a; const path_candidate_t *pb = (const path_candidate_t *)b; - if (pa->score != pb->score) { - return pb->score - pa->score; - } - if (pa->is_dir != pb->is_dir) { return pb->is_dir - pa->is_dir; } + if (pa->score != pb->score) { + return pb->score - pa->score; + } + int cmp = strcmp(pa->path, pb->path); if (cmp != 0) return cmp; @@ -323,7 +197,7 @@ static int split_path_query(const char *query, if (display_dir[0] == '\0') { snprintf(fs_dir, fs_dir_size, "."); } else { - char *expanded = app_expand_user_path(display_dir); + char *expanded = ecex_path_expand_user(display_dir); if (!expanded) return -1; snprintf(fs_dir, fs_dir_size, "%s", expanded); @@ -393,7 +267,7 @@ static path_candidate_t *collect_path_candidates(const char *query, continue; } - int score = command_fuzzy_score(name, needle); + int score = ecex_fuzzy_score(name, needle); if (score < 0) { order++; continue; @@ -446,35 +320,151 @@ static path_candidate_t *collect_path_candidates(const char *query, return items; } -static const char *path_candidate_at(const char *query, - size_t index, - size_t *out_count) { - static char candidate[1024]; +typedef ecex_completion_item_t prompt_candidate_t; + +static void free_prompt_candidates(prompt_candidate_t *items, size_t count) { + ecex_completion_items_free(items, count); +} + +static int compare_prompt_candidates(const void *a, const void *b) { + return ecex_completion_item_compare(a, b); +} + +static int prompt_kind_uses_path_completion(ecex_prompt_request_t kind) { + return kind == ECEX_PROMPT_FIND_FILE || + kind == ECEX_PROMPT_WRITE_FILE || + kind == ECEX_PROMPT_EVAL_FILE; +} + +static int prompt_kind_uses_buffer_completion(ecex_prompt_request_t kind) { + return kind == ECEX_PROMPT_SWITCH_BUFFER || + kind == ECEX_PROMPT_KILL_BUFFER || + kind == ECEX_PROMPT_FORCE_KILL_BUFFER; +} + +static prompt_candidate_t *collect_buffer_candidates(ecex_t *ed, + const char *query, + size_t *out_count) { + if (out_count) *out_count = 0; + if (!ed || !query || ed->buffer_count == 0) return NULL; + + prompt_candidate_t *items = calloc(ed->buffer_count, sizeof(*items)); + if (!items) return NULL; size_t count = 0; - path_candidate_t *items = collect_path_candidates(query, &count); - if (!items || count == 0) { - free_path_candidates(items, count); + for (size_t i = 0; i < ed->buffer_count; i++) { + buffer_t *buf = ed->buffers[i]; + if (!buf || !buf->name) continue; + + int score = ecex_fuzzy_score(buf->name, query); + if (score < 0) continue; + + items[count].value = malloc(strlen(buf->name) + 1); + if (!items[count].value) continue; + strcpy(items[count].value, buf->name); + items[count].score = score; + items[count].is_dir = 0; + items[count].order = i; + count++; + } + + if (count == 0) { + free(items); return NULL; } - snprintf(candidate, sizeof(candidate), "%s", items[index % count].path); + qsort(items, count, sizeof(*items), compare_prompt_candidates); + if (out_count) *out_count = count; + return items; +} - if (out_count) { - *out_count = count; +static prompt_candidate_t *collect_prompt_candidates(app_t *app, + const char *query, + size_t *out_count) { + if (out_count) *out_count = 0; + if (!app || !query) return NULL; + + if (prompt_kind_uses_path_completion(app->prompt_kind)) { + size_t path_count = 0; + path_candidate_t *paths = collect_path_candidates(query, &path_count); + if (!paths || path_count == 0) { + free_path_candidates(paths, path_count); + return NULL; + } + + prompt_candidate_t *items = calloc(path_count, sizeof(*items)); + if (!items) { + free_path_candidates(paths, path_count); + return NULL; + } + + size_t count = 0; + for (size_t i = 0; i < path_count; i++) { + items[count].value = malloc(strlen(paths[i].path) + 1); + if (!items[count].value) continue; + strcpy(items[count].value, paths[i].path); + items[count].score = paths[i].score; + items[count].is_dir = paths[i].is_dir; + items[count].order = paths[i].order; + count++; + } + + free_path_candidates(paths, path_count); + if (count == 0) { + free(items); + return NULL; + } + if (out_count) *out_count = count; + return items; + } + + if (prompt_kind_uses_buffer_completion(app->prompt_kind)) { + return collect_buffer_candidates(app->ed, query, out_count); + } + + return NULL; +} + +static const char *prompt_candidate_at(app_t *app, + const char *query, + size_t index, + size_t *out_count) { + static char candidate[1024]; + + size_t count = 0; + prompt_candidate_t *items = collect_prompt_candidates(app, query, &count); + if (!items || count == 0) { + free_prompt_candidates(items, count); + return NULL; } - free_path_candidates(items, count); + snprintf(candidate, sizeof(candidate), "%s", items[index % count].value); + if (out_count) *out_count = count; + + free_prompt_candidates(items, count); return candidate; } +static int app_prompt_input_is_directory(app_t *app) { + if (!app || !prompt_kind_uses_path_completion(app->prompt_kind)) return 0; + if (app->prompt_input_len == 0) return 0; + + char *expanded = ecex_path_expand_user(app->prompt_input); + if (!expanded) return 0; + + struct stat st; + int is_dir = stat(expanded, &st) == 0 && S_ISDIR(st.st_mode); + free(expanded); + return is_dir; +} + static void app_update_prompt_completion_preview(app_t *app) { if (!app || !app->prompt_completion_active) return; size_t count = 0; - path_candidate_t *items = collect_path_candidates(app->prompt_completion_query, &count); + prompt_candidate_t *items = collect_prompt_candidates(app, app->prompt_completion_query, &count); if (!items || count == 0) { - free_path_candidates(items, count); + free_prompt_candidates(items, count); app->prompt_completion_preview_start = 0; app->prompt_completion_preview_count = 0; return; @@ -495,11 +485,11 @@ static void app_update_prompt_completion_preview(app_t *app) { snprintf(app->prompt_completion_preview[i], sizeof(app->prompt_completion_preview[i]), "%s", - items[start + i].path); + items[start + i].value); app->prompt_completion_preview_count++; } - free_path_candidates(items, count); + free_prompt_candidates(items, count); } static void app_begin_prompt_completion(app_t *app, int direction) { @@ -511,12 +501,12 @@ static void app_begin_prompt_completion(app_t *app, int direction) { app->prompt_input); size_t count = 0; - path_candidate_t *items = collect_path_candidates(app->prompt_completion_query, &count); + prompt_candidate_t *items = collect_prompt_candidates(app, app->prompt_completion_query, &count); if (!items || count == 0) { - free_path_candidates(items, count); + free_prompt_candidates(items, count); app_reset_prompt_completion(app); - app_message(app, "No file completions"); + app_message(app, prompt_kind_uses_buffer_completion(app->prompt_kind) ? "No buffer completions" : "No file completions"); app->mode = APP_MODE_PROMPT; return; } @@ -525,8 +515,8 @@ static void app_begin_prompt_completion(app_t *app, int direction) { app->prompt_completion_count = count; app->prompt_completion_index = direction < 0 ? count - 1 : 0; - app_set_prompt_input(app, items[app->prompt_completion_index].path); - free_path_candidates(items, count); + app_set_prompt_input(app, items[app->prompt_completion_index].value); + free_prompt_candidates(items, count); app_update_prompt_completion_preview(app); } @@ -548,9 +538,10 @@ static void app_cycle_prompt_completion(app_t *app, int delta) { } size_t count = 0; - const char *candidate = path_candidate_at(app->prompt_completion_query, - app->prompt_completion_index, - &count); + const char *candidate = prompt_candidate_at(app, + app->prompt_completion_query, + app->prompt_completion_index, + &count); if (!candidate || count == 0) { app_reset_prompt_completion(app); @@ -562,6 +553,18 @@ static void app_cycle_prompt_completion(app_t *app, int delta) { app_update_prompt_completion_preview(app); } +static void app_complete_prompt(app_t *app) { + if (!app) return; + + if (app->prompt_completion_active && app_prompt_input_is_directory(app)) { + app_reset_prompt_completion(app); + app_begin_prompt_completion(app, 1); + return; + } + + app_cycle_prompt_completion(app, 1); +} + static void app_begin_completion(app_t *app, int direction) { if (!app) return; @@ -819,25 +822,47 @@ static void app_cancel_prompt(app_t *app) { app_message(app, "Prompt cancelled"); } +static const char *app_default_prompt_input(app_t *app, ecex_prompt_request_t kind) { + if (!app || !app->ed) return NULL; + + if (kind == ECEX_PROMPT_SWITCH_BUFFER) { + buffer_t *other = ecex_other_buffer(app->ed); + return other ? other->name : NULL; + } + + if (kind == ECEX_PROMPT_KILL_BUFFER || + kind == ECEX_PROMPT_FORCE_KILL_BUFFER) { + buffer_t *current = ecex_current_buffer(app->ed); + return current ? current->name : NULL; + } + + return NULL; +} + static void app_submit_prompt(app_t *app) { if (!app || !app->ed) return; - if (app->prompt_input_len == 0 && app->prompt_kind != ECEX_PROMPT_COMPILE) { + ecex_prompt_request_t kind = app->prompt_kind; + const char *default_input = app_default_prompt_input(app, kind); + + if (app->prompt_input_len == 0 && + kind != ECEX_PROMPT_COMPILE && + (!default_input || default_input[0] == '\0')) { app_cancel_prompt(app); return; } char input[sizeof(app->prompt_input)]; - snprintf(input, sizeof(input), "%s", app->prompt_input); + snprintf(input, sizeof(input), "%s", + app->prompt_input_len > 0 ? app->prompt_input : (default_input ? default_input : "")); - ecex_prompt_request_t kind = app->prompt_kind; app->mode = APP_MODE_EDIT; app->prompt_kind = ECEX_PROMPT_NONE; app->prompt_label[0] = '\0'; app->prompt_input[0] = '\0'; app->prompt_input_len = 0; - char *expanded_input = app_expand_user_path(input); + char *expanded_input = ecex_path_expand_user(input); const char *path = expanded_input ? expanded_input : input; int result = -1; @@ -869,6 +894,11 @@ static void app_submit_prompt(app_t *app) { verb = "Killed"; break; + case ECEX_PROMPT_FORCE_KILL_BUFFER: + result = ecex_kill_buffer_force(app->ed, input); + verb = "Force killed"; + break; + case ECEX_PROMPT_COMPILE: result = ecex_compile(app->ed, input[0] ? input : "make"); verb = "Compiled"; @@ -1049,6 +1079,11 @@ static float app_mono_cell_width(app_t *app) { return w > 1.0f ? w : app->font.size_px * 0.6f; } +static int app_trace_callbacks_enabled(void) { + const char *v = getenv("ECEX_TRACE_CALLBACKS"); + return v && v[0] && v[0] != '0'; +} + static float app_status_height(app_t *app) { float line_h = app->font.line_height > 1.0f ? app->font.line_height : app->font.size_px * 1.2f; float pad_y = app->font.size_px * 0.35f; @@ -1117,15 +1152,151 @@ static void app_move_point_to_pixel(app_t *app, double px, double py) { app->dirty = 1; } +static int app_window_rect_at(app_t *app, + double px, + double py, + size_t *out_index, + float *out_x, + float *out_y, + float *out_w, + float *out_h) { + if (!app || !app->ed || app->ed->window_count == 0) return 0; + float editor_h = (float)app->height - app_status_height(app); + if (app_minibuffer_visible(app)) editor_h -= app_minibuffer_height(app); + if (editor_h < 1.0f) editor_h = 1.0f; + for (size_t i = 0; i < app->ed->window_count; i++) { + ecex_window_t *w = &app->ed->windows[i]; + float x = w->x * (float)app->width; + float y = w->y * editor_h; + float ww = w->w * (float)app->width; + float hh = w->h * editor_h; + if (px >= x && px < x + ww && py >= y && py < y + hh) { + if (out_index) *out_index = i; + if (out_x) *out_x = x; + if (out_y) *out_y = y; + if (out_w) *out_w = ww; + if (out_h) *out_h = hh; + return 1; + } + } + return 0; +} + +static int app_dispatch_buffer_mouse(app_t *app, int event, double px, double py, int button) { + if (!app || !app->ed) return 0; + + buffer_t *buf = NULL; + float wx = 0.0f; + float wy = 0.0f; + float ww = 0.0f; + float wh = 0.0f; + + if (app->mouse_capture_active && app->mouse_capture_buffer) { + buf = app->mouse_capture_buffer; + for (size_t i = 0; i < app->ed->window_count; i++) { + if (app->ed->windows[i].buffer != buf) continue; + float editor_h = (float)app->height - app_status_height(app); + if (app_minibuffer_visible(app)) editor_h -= app_minibuffer_height(app); + if (editor_h < 1.0f) editor_h = 1.0f; + wx = app->ed->windows[i].x * (float)app->width; + wy = app->ed->windows[i].y * editor_h; + ww = app->ed->windows[i].w * (float)app->width; + wh = app->ed->windows[i].h * editor_h; + break; + } + } else { + size_t wi = 0; + if (!app_window_rect_at(app, px, py, &wi, &wx, &wy, &ww, &wh)) return 0; + if (wi >= app->ed->window_count) return 0; + buf = app->ed->windows[wi].buffer; + } + + (void)ww; + (void)wh; + if (!buf || !buf->mouse_fn) return 0; + + int local_x = (int)(px - (double)wx); + int local_y = (int)(py - (double)wy); + if (local_x < -32768) local_x = -32768; + if (local_y < -32768) local_y = -32768; + + if (app_trace_callbacks_enabled()) { + fprintf(stderr, "ecex-log: mouse_callback buffer=%p fn=%p event=%d x=%d y=%d button=%d userdata=%p\n", + (void *)buf, (void *)buf->mouse_fn, event, local_x, local_y, button, buf->mouse_userdata); + fflush(stderr); + } + + int result = buf->mouse_fn(app->ed, buf, event, local_x, local_y, button, buf->mouse_userdata); + if (result) app->dirty = 1; + return result != 0; +} + +static int app_mouse_button_id(int glfw_button) { + if (glfw_button == GLFW_MOUSE_BUTTON_LEFT) return ECEX_MOUSE_BUTTON_LEFT; + if (glfw_button == GLFW_MOUSE_BUTTON_RIGHT) return ECEX_MOUSE_BUTTON_RIGHT; + if (glfw_button == GLFW_MOUSE_BUTTON_MIDDLE) return ECEX_MOUSE_BUTTON_MIDDLE; + return glfw_button; +} + static void mouse_button_callback(GLFWwindow *window, int button, int action, int mods) { (void)mods; - if (button != GLFW_MOUSE_BUTTON_LEFT || action != GLFW_PRESS) return; app_t *app = glfwGetWindowUserPointer(window); if (!app) return; glfwGetFramebufferSize(window, &app->width, &app->height); double x = 0.0, y = 0.0; glfwGetCursorPos(window, &x, &y); - app_move_point_to_pixel(app, x, y); + int ebutton = app_mouse_button_id(button); + + if (action == GLFW_PRESS) { + size_t capture_wi = 0; + float capture_wx = 0.0f, capture_wy = 0.0f, capture_ww = 0.0f, capture_wh = 0.0f; + int have_capture_window = app_window_rect_at(app, x, y, &capture_wi, + &capture_wx, &capture_wy, &capture_ww, &capture_wh); + + if (app_dispatch_buffer_mouse(app, ECEX_MOUSE_PRESS, x, y, ebutton)) { + /* Start capture from the window resolved before dispatch. Do not + * re-hit-test after the plugin callback; the callback may mark the + * app dirty or mutate buffer/window state, and a second lookup can + * fail or select a different target. Without capture, GLFW cursor + * motion is delivered as plain ECEX_MOUSE_MOVE events, so rendered + * widgets never receive ECEX_MOUSE_DRAG. */ + if (have_capture_window && capture_wi < app->ed->window_count) { + app->mouse_capture_buffer = app->ed->windows[capture_wi].buffer; + app->mouse_capture_button = ebutton; + app->mouse_capture_active = 1; + if (app_trace_callbacks_enabled()) { + fprintf(stderr, "ecex-log: mouse_capture_start buffer=%p button=%d\n", + (void *)app->mouse_capture_buffer, ebutton); + fflush(stderr); + } + } + return; + } + if (button == GLFW_MOUSE_BUTTON_LEFT) app_move_point_to_pixel(app, x, y); + } else if (action == GLFW_RELEASE) { + app_dispatch_buffer_mouse(app, ECEX_MOUSE_RELEASE, x, y, ebutton); + if (!app->mouse_capture_active || app->mouse_capture_button == ebutton) { + if (app_trace_callbacks_enabled() && app->mouse_capture_active) { + fprintf(stderr, "ecex-log: mouse_capture_end buffer=%p button=%d\n", + (void *)app->mouse_capture_buffer, ebutton); + fflush(stderr); + } + app->mouse_capture_active = 0; + app->mouse_capture_buffer = NULL; + app->mouse_capture_button = 0; + } + } +} + +static void cursor_pos_callback(GLFWwindow *window, double x, double y) { + app_t *app = glfwGetWindowUserPointer(window); + if (!app) return; + glfwGetFramebufferSize(window, &app->width, &app->height); + if (app->mouse_capture_active) { + app_dispatch_buffer_mouse(app, ECEX_MOUSE_DRAG, x, y, app->mouse_capture_button); + } else { + app_dispatch_buffer_mouse(app, ECEX_MOUSE_MOVE, x, y, ECEX_MOUSE_BUTTON_LEFT); + } } static void scroll_callback(GLFWwindow *window, double xoffset, double yoffset) { @@ -1329,6 +1500,7 @@ static void char_callback(GLFWwindow *window, unsigned int codepoint) { if (app->mode == APP_MODE_PROMPT) { if (codepoint >= 32 && codepoint <= 126) { if (app->prompt_input_len + 1 < sizeof(app->prompt_input)) { + app_reset_prompt_completion(app); app->prompt_input[app->prompt_input_len++] = (char)codepoint; app->prompt_input[app->prompt_input_len] = '\0'; app->dirty = 1; @@ -1470,7 +1642,7 @@ static void key_callback(GLFWwindow *window, return; case GLFW_KEY_TAB: - app_cycle_prompt_completion(app, 1); + app_complete_prompt(app); return; case GLFW_KEY_DOWN: @@ -1531,6 +1703,14 @@ static void key_callback(GLFWwindow *window, try_execute_keybind(app, key_name); return; } + + if (key_sequence_has_prefix(app, key_name)) { + if (key_produces_text(key, scancode)) { + app->suppress_next_char = 1; + } + app_enter_prefix(app, key_name); + return; + } } if (key == GLFW_KEY_ESCAPE) { @@ -1574,5 +1754,6 @@ void app_install_callbacks(app_t *app) { glfwSetFramebufferSizeCallback(app->window, framebuffer_size_callback); glfwSetWindowRefreshCallback(app->window, window_refresh_callback); glfwSetMouseButtonCallback(app->window, mouse_button_callback); + glfwSetCursorPosCallback(app->window, cursor_pos_callback); glfwSetScrollCallback(app->window, scroll_callback); } diff --git a/src/buffers.c b/src/buffers.c index e8c1d23..a932712 100644 --- a/src/buffers.c +++ b/src/buffers.c @@ -8,8 +8,15 @@ #include #include +extern int pclose(FILE *stream); + #define BUFFER_INITIAL_CAP 64 +static int ecex_trace_callbacks_enabled(void) { + const char *v = getenv("ECEX_TRACE_CALLBACKS"); + return v && v[0] && v[0] != '0'; +} + static int buffer_is_word_char(char c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || @@ -116,6 +123,12 @@ void buffer_free(buffer_t *buffer) { if (!buffer) return; buffer_clear_interactive_actions(buffer); + ecex_buffer_clear_animation(buffer); + ecex_buffer_clear_mouse_handler(buffer); + ecex_buffer_clear_renderer(buffer); + if (buffer->media_pipe) pclose((FILE *)buffer->media_pipe); + free(buffer->media_path); + free(buffer->media_pixels); buffer_undo_stack_clear(buffer->undo_stack, buffer->undo_count); buffer_undo_stack_clear(buffer->redo_stack, buffer->redo_count); free(buffer->undo_stack); @@ -699,3 +712,310 @@ ecex_interactive_line_action_t *buffer_interactive_action_at_line(buffer_t *buff return NULL; } + +int ecex_buffer_set_renderer(buffer_t *buffer, + ecex_buffer_render_fn fn, + void *userdata, + ecex_buffer_userdata_free_fn free_fn, + int flags) { + if (ecex_trace_callbacks_enabled()) { + fprintf(stderr, "ecex-log: buffer_set_renderer buffer=%p fn=%p userdata=%p free=%p flags=%d\n", + (void *)buffer, (void *)fn, userdata, (void *)free_fn, flags); + fflush(stderr); + } + if (!buffer) return ECEX_ERR; + if (buffer->render_userdata_free && buffer->render_userdata && buffer->render_userdata != userdata) { + buffer->render_userdata_free(buffer->render_userdata); + } + buffer->render_fn = fn; + buffer->render_userdata = userdata; + buffer->render_userdata_free = free_fn; + buffer->render_flags = flags; + return ECEX_OK; +} + +int ecex_buffer_clear_renderer(buffer_t *buffer) { + if (ecex_trace_callbacks_enabled()) { + fprintf(stderr, "ecex-log: buffer_clear_renderer buffer=%p userdata=%p free=%p\n", + (void *)buffer, buffer ? buffer->render_userdata : NULL, + buffer ? (void *)buffer->render_userdata_free : NULL); + fflush(stderr); + } + if (!buffer) return ECEX_ERR; + if (buffer->render_userdata_free && buffer->render_userdata) { + buffer->render_userdata_free(buffer->render_userdata); + } + buffer->render_fn = NULL; + buffer->render_userdata = NULL; + buffer->render_userdata_free = NULL; + buffer->render_flags = 0; + return ECEX_OK; +} + +int ecex_buffer_has_renderer(buffer_t *buffer) { + return buffer && buffer->render_fn; +} + +void *ecex_buffer_renderer_userdata(buffer_t *buffer) { + return buffer ? buffer->render_userdata : NULL; +} + + +int ecex_buffer_set_mouse_handler(buffer_t *buffer, + ecex_buffer_mouse_fn fn, + void *userdata, + ecex_buffer_userdata_free_fn free_fn) { + if (ecex_trace_callbacks_enabled()) { + fprintf(stderr, "ecex-log: buffer_set_mouse_handler buffer=%p fn=%p userdata=%p free=%p\n", + (void *)buffer, (void *)fn, userdata, (void *)free_fn); + fflush(stderr); + } + if (!buffer || !fn) return ECEX_ERR; + if (buffer->mouse_userdata_free && buffer->mouse_userdata && buffer->mouse_userdata != userdata) { + buffer->mouse_userdata_free(buffer->mouse_userdata); + } + buffer->mouse_fn = fn; + buffer->mouse_userdata = userdata; + buffer->mouse_userdata_free = free_fn; + return ECEX_OK; +} + +int ecex_buffer_clear_mouse_handler(buffer_t *buffer) { + if (ecex_trace_callbacks_enabled()) { + fprintf(stderr, "ecex-log: buffer_clear_mouse_handler buffer=%p userdata=%p free=%p\n", + (void *)buffer, buffer ? buffer->mouse_userdata : NULL, + buffer ? (void *)buffer->mouse_userdata_free : NULL); + fflush(stderr); + } + if (!buffer) return ECEX_ERR; + if (buffer->mouse_userdata_free && buffer->mouse_userdata) { + buffer->mouse_userdata_free(buffer->mouse_userdata); + } + buffer->mouse_fn = NULL; + buffer->mouse_userdata = NULL; + buffer->mouse_userdata_free = NULL; + return ECEX_OK; +} + +int ecex_buffer_has_mouse_handler(buffer_t *buffer) { + return buffer && buffer->mouse_fn; +} + +void *ecex_buffer_mouse_userdata(buffer_t *buffer) { + return buffer ? buffer->mouse_userdata : NULL; +} + +int ecex_buffer_set_animation(buffer_t *buffer, + ecex_buffer_tick_fn fn, + void *userdata, + ecex_buffer_userdata_free_fn free_fn, + double fps) { + if (ecex_trace_callbacks_enabled()) { + fprintf(stderr, "ecex-log: buffer_set_animation buffer=%p fn=%p userdata=%p free=%p fps=%.3f\n", + (void *)buffer, (void *)fn, userdata, (void *)free_fn, fps); + fflush(stderr); + } + if (!buffer || !fn) return ECEX_ERR; + + if (buffer->tick_userdata_free && buffer->tick_userdata && buffer->tick_userdata != userdata) { + buffer->tick_userdata_free(buffer->tick_userdata); + } + + if (fps <= 0.0) fps = 60.0; + if (fps > 240.0) fps = 240.0; + + buffer->tick_fn = fn; + buffer->tick_ms_fn = NULL; + buffer->tick_userdata = userdata; + buffer->tick_userdata_free = free_fn; + buffer->tick_interval = 1.0 / fps; + buffer->tick_last_time = 0.0; + buffer->tick_enabled = 1; + buffer->tick_uses_ms = 0; + return ECEX_OK; +} + +int ecex_buffer_set_animation_ms(buffer_t *buffer, + ecex_buffer_tick_ms_fn fn, + void *userdata, + ecex_buffer_userdata_free_fn free_fn, + int fps) { + if (ecex_trace_callbacks_enabled()) { + fprintf(stderr, "ecex-log: buffer_set_animation_ms buffer=%p fn=%p userdata=%p free=%p fps=%d\n", + (void *)buffer, (void *)fn, userdata, (void *)free_fn, fps); + fflush(stderr); + } + if (!buffer || !fn) return ECEX_ERR; + + if (buffer->tick_userdata_free && buffer->tick_userdata && buffer->tick_userdata != userdata) { + buffer->tick_userdata_free(buffer->tick_userdata); + } + + if (fps <= 0) fps = 60; + if (fps > 240) fps = 240; + + buffer->tick_fn = NULL; + buffer->tick_ms_fn = fn; + buffer->tick_userdata = userdata; + buffer->tick_userdata_free = free_fn; + buffer->tick_interval = 1.0 / (double)fps; + buffer->tick_last_time = 0.0; + buffer->tick_enabled = 1; + buffer->tick_uses_ms = 1; + return ECEX_OK; +} + +int ecex_buffer_clear_animation(buffer_t *buffer) { + if (ecex_trace_callbacks_enabled()) { + fprintf(stderr, "ecex-log: buffer_clear_animation buffer=%p userdata=%p free=%p\n", + (void *)buffer, buffer ? buffer->tick_userdata : NULL, + buffer ? (void *)buffer->tick_userdata_free : NULL); + fflush(stderr); + } + if (!buffer) return ECEX_ERR; + if (buffer->tick_userdata_free && buffer->tick_userdata) { + buffer->tick_userdata_free(buffer->tick_userdata); + } + buffer->tick_fn = NULL; + buffer->tick_ms_fn = NULL; + buffer->tick_userdata = NULL; + buffer->tick_userdata_free = NULL; + buffer->tick_interval = 0.0; + buffer->tick_last_time = 0.0; + buffer->tick_enabled = 0; + buffer->tick_uses_ms = 0; + return ECEX_OK; +} + +int ecex_buffer_is_animating(buffer_t *buffer) { + return buffer && buffer->tick_enabled && (buffer->tick_fn || buffer->tick_ms_fn); +} + +void *ecex_buffer_animation_userdata(buffer_t *buffer) { + return buffer ? buffer->tick_userdata : NULL; +} + +int ecex_tick_animations(ecex_t *ed, double now_seconds) { + if (!ed) return 0; + + int dirty = 0; + for (size_t i = 0; i < ed->buffer_count; i++) { + buffer_t *buffer = ed->buffers[i]; + if (!buffer || !buffer->tick_enabled) continue; + if (buffer->tick_uses_ms) { + if (!buffer->tick_ms_fn) { + if (ecex_trace_callbacks_enabled()) { + fprintf(stderr, "ecex-log: animation_tick_skip_ms_null buffer=%p userdata=%p\n", + (void *)buffer, buffer->tick_userdata); + fflush(stderr); + } + buffer->tick_enabled = 0; + continue; + } + } else { + if (!buffer->tick_fn) { + if (ecex_trace_callbacks_enabled()) { + fprintf(stderr, "ecex-log: animation_tick_skip_null buffer=%p userdata=%p\n", + (void *)buffer, buffer->tick_userdata); + fflush(stderr); + } + buffer->tick_enabled = 0; + continue; + } + } + + if (buffer->tick_last_time <= 0.0) { + buffer->tick_last_time = now_seconds; + } + + double interval = buffer->tick_interval > 0.0 ? buffer->tick_interval : (1.0 / 60.0); + if (now_seconds - buffer->tick_last_time + 0.000001 < interval) { + continue; + } + + buffer->tick_last_time = now_seconds; + int tick_dirty = 0; + if (buffer->tick_uses_ms) { + int now_ms = (int)(now_seconds * 1000.0); + if (ecex_trace_callbacks_enabled()) { + fprintf(stderr, "ecex-log: animation_tick_ms_enter buffer=%p fn=%p userdata=%p now_ms=%d\n", + (void *)buffer, (void *)buffer->tick_ms_fn, buffer->tick_userdata, now_ms); + fflush(stderr); + } + tick_dirty = buffer->tick_ms_fn(ed, buffer, now_ms, buffer->tick_userdata); + if (ecex_trace_callbacks_enabled()) { + fprintf(stderr, "ecex-log: animation_tick_ms_leave buffer=%p result=%d\n", + (void *)buffer, tick_dirty); + fflush(stderr); + } + } else { + if (ecex_trace_callbacks_enabled()) { + fprintf(stderr, "ecex-log: animation_tick_enter buffer=%p fn=%p userdata=%p now=%.6f\n", + (void *)buffer, (void *)buffer->tick_fn, buffer->tick_userdata, now_seconds); + fflush(stderr); + } + tick_dirty = buffer->tick_fn(ed, buffer, now_seconds, buffer->tick_userdata); + if (ecex_trace_callbacks_enabled()) { + fprintf(stderr, "ecex-log: animation_tick_leave buffer=%p result=%d\n", + (void *)buffer, tick_dirty); + fflush(stderr); + } + } + if (tick_dirty != 0) { + dirty = 1; + } + } + + return dirty; +} + + +int ecex_buffer_replace_text(buffer_t *buffer, const char *text) { + return buffer_set_text(buffer, text ? text : ""); +} + +void ecex_buffer_set_modified(buffer_t *buffer, int modified) { + if (buffer) buffer->modified = modified ? 1 : 0; +} + +int ecex_buffer_text_len(buffer_t *buffer) { + if (!buffer) return 0; + return buffer->len > (size_t)2147483647 ? 2147483647 : (int)buffer->len; +} + +int ecex_buffer_scroll_line(buffer_t *buffer) { + if (!buffer) return 0; + return buffer->scroll_line > (size_t)2147483647 ? 2147483647 : (int)buffer->scroll_line; +} + +int ecex_buffer_line_count_i(buffer_t *buffer) { + size_t n = buffer_line_count(buffer); + return n > (size_t)2147483647 ? 2147483647 : (int)n; +} + +int ecex_buffer_line_copy(buffer_t *buffer, int line, char *out, int out_cap) { + size_t current = 0; + size_t pos = 0; + size_t start; + size_t end; + size_t n; + + if (!out || out_cap <= 0) return ECEX_ERR; + out[0] = '\0'; + if (!buffer || !buffer->data || line < 0) return ECEX_ERR; + + while (current < (size_t)line && pos < buffer->len) { + if (buffer->data[pos++] == '\n') current++; + } + if (current != (size_t)line) return ECEX_ERR; + + start = pos; + while (pos < buffer->len && buffer->data[pos] != '\n') pos++; + end = pos; + while (end > start && (buffer->data[end - 1] == '\n' || buffer->data[end - 1] == '\r')) end--; + n = end - start; + if (n >= (size_t)out_cap) n = (size_t)out_cap - 1; + if (n > 0) memcpy(out, buffer->data + start, n); + out[n] = '\0'; + return (int)n; +} diff --git a/src/completion.c b/src/completion.c new file mode 100644 index 0000000..6f19530 --- /dev/null +++ b/src/completion.c @@ -0,0 +1,97 @@ +#include "completion.h" + +#include +#include + +static int ecex_ascii_lower(int c) { + if (c >= 'A' && c <= 'Z') return c - 'A' + 'a'; + return c; +} + +int ecex_ascii_strncasecmp(const char *a, const char *b, size_t n) { + if (!a || !b) return a == b ? 0 : (a ? 1 : -1); + + for (size_t i = 0; i < n; i++) { + int ac = ecex_ascii_lower((unsigned char)a[i]); + int bc = ecex_ascii_lower((unsigned char)b[i]); + + if (ac != bc || ac == '\0' || bc == '\0') return ac - bc; + } + + return 0; +} + +int ecex_ascii_contains_ci(const char *haystack, const char *needle) { + if (!haystack || !needle) return 0; + if (needle[0] == '\0') return 1; + + size_t needle_len = strlen(needle); + for (size_t i = 0; haystack[i]; i++) { + if (ecex_ascii_strncasecmp(haystack + i, needle, needle_len) == 0) return 1; + } + + return 0; +} + +int ecex_fuzzy_score(const char *candidate, const char *query) { + if (!candidate || !query) return -1; + if (query[0] == '\0') return 0; + + int score = 0; + int consecutive = 0; + int last_match = -1; + size_t ci = 0; + size_t qi = 0; + + while (candidate[ci] && query[qi]) { + char c = candidate[ci]; + char q = query[qi]; + + if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a'); + if (q >= 'A' && q <= 'Z') q = (char)(q - 'A' + 'a'); + + if (c == q) { + score += 10; + if ((int)ci == last_match + 1) { + consecutive++; + score += 5 * consecutive; + } else { + consecutive = 0; + } + if (ci == 0) score += 20; + if (ci > 0 && (candidate[ci - 1] == '-' || candidate[ci - 1] == '_' || candidate[ci - 1] == ' ')) { + score += 15; + } + last_match = (int)ci; + qi++; + } + ci++; + } + + if (query[qi] != '\0') return -1; + + score -= (int)strlen(candidate); + if (strncmp(candidate, query, strlen(query)) == 0) score += 100; + if (ecex_ascii_contains_ci(candidate, query)) score += 75; + return score; +} + +int ecex_completion_item_compare(const void *a, const void *b) { + const ecex_completion_item_t *pa = (const ecex_completion_item_t *)a; + const ecex_completion_item_t *pb = (const ecex_completion_item_t *)b; + + if (pa->score != pb->score) return pb->score - pa->score; + if (pa->is_dir != pb->is_dir) return pb->is_dir - pa->is_dir; + + int cmp = strcmp(pa->value ? pa->value : "", pb->value ? pb->value : ""); + if (cmp != 0) return cmp; + if (pa->order < pb->order) return -1; + if (pa->order > pb->order) return 1; + return 0; +} + +void ecex_completion_items_free(ecex_completion_item_t *items, size_t count) { + if (!items) return; + for (size_t i = 0; i < count; i++) free(items[i].value); + free(items); +} diff --git a/src/config.c b/src/config.c index b416d7a..1b0b5ed 100644 --- a/src/config.c +++ b/src/config.c @@ -6,6 +6,7 @@ #include "ecex.h" #include "eval.h" #include "util.h" +#include "path.h" #include #include @@ -56,7 +57,60 @@ static const host_symbol_t host_symbols[] = { HOST_SYMBOL(ecex_delete_window), HOST_SYMBOL(ecex_delete_other_windows), HOST_SYMBOL(ecex_kill_buffer), - + HOST_SYMBOL(ecex_kill_buffer_force), + HOST_SYMBOL(ecex_has_modified_buffers), + HOST_SYMBOL(ecex_validate_bindings), + HOST_SYMBOL(ecex_config_alloc), + HOST_SYMBOL(ecex_config_calloc), + HOST_SYMBOL(ecex_config_free), + HOST_SYMBOL(ecex_time_seconds), + HOST_SYMBOL(ecex_log), + HOST_SYMBOL(ecex_log_int), + HOST_SYMBOL(ecex_log_double), + HOST_SYMBOL(ecex_log_ptr), + HOST_SYMBOL(ecex_mem_zero), + HOST_SYMBOL(ecex_i32_get), + HOST_SYMBOL(ecex_i32_set), + HOST_SYMBOL(ecex_prng_next_bounded), + HOST_SYMBOL(ecex_random_bounded), + HOST_SYMBOL(ecex_tetris_shape_cell), + HOST_SYMBOL(ecex_var_get), + HOST_SYMBOL(ecex_var_get_or_alloc), + HOST_SYMBOL(ecex_var_bind_static), + HOST_SYMBOL(ecex_var_free), + HOST_SYMBOL(ecex_var_free_owner), + HOST_SYMBOL(ecex_var_i32_get), + HOST_SYMBOL(ecex_var_i32_set), + HOST_SYMBOL(ecex_var_i32), + HOST_SYMBOL(ecex_var_i32_set_scalar), + HOST_SYMBOL(ecex_object_alloc), + HOST_SYMBOL(ecex_object_calloc), + HOST_SYMBOL(ecex_object_free), + HOST_SYMBOL(ecex_object_valid), + HOST_SYMBOL(ecex_object_i32_get), + HOST_SYMBOL(ecex_object_i32_set), + HOST_SYMBOL(ecex_object_ptr_get), + HOST_SYMBOL(ecex_object_ptr_set), + HOST_SYMBOL(ecex_text_set), + HOST_SYMBOL(ecex_text_set_buffer_title), + HOST_SYMBOL(ecex_text_free), + HOST_SYMBOL(ecex_text_free_owner), + HOST_SYMBOL(ecex_buffer_text_len), + HOST_SYMBOL(ecex_buffer_scroll_line), + HOST_SYMBOL(ecex_buffer_line_count_i), + HOST_SYMBOL(ecex_buffer_line_copy), + HOST_SYMBOL(ecex_markdown_draw_line_from_buffer_i), + HOST_SYMBOL(ecex_markdown_body_y_i), + HOST_SYMBOL(ecex_draw_context_height_i), + HOST_SYMBOL(ecex_draw_context_line_height_i), + HOST_SYMBOL(ecex_register_file_handler), + HOST_SYMBOL(ecex_run_file_handlers), + + HOST_SYMBOL(ecex_config_register_commands), + HOST_SYMBOL(ecex_config_bind_keys), + HOST_SYMBOL(ecex_config_bind_mode_keys), + HOST_SYMBOL(ecex_config_define_modes), + HOST_SYMBOL(ecex_apply_theme), HOST_SYMBOL(ecex_register_command), HOST_SYMBOL(ecex_execute_command), HOST_SYMBOL(ecex_bind_key), @@ -76,12 +130,67 @@ static const host_symbol_t host_symbols[] = { HOST_SYMBOL(ecex_create_interactive_buffer), HOST_SYMBOL(ecex_interactive_append_line), HOST_SYMBOL(ecex_interactive_activate_current_line), + HOST_SYMBOL(ecex_buffer_set_renderer), + HOST_SYMBOL(ecex_buffer_clear_renderer), + HOST_SYMBOL(ecex_buffer_has_renderer), + HOST_SYMBOL(ecex_buffer_renderer_userdata), + HOST_SYMBOL(ecex_buffer_set_mouse_handler), + HOST_SYMBOL(ecex_buffer_clear_mouse_handler), + HOST_SYMBOL(ecex_buffer_has_mouse_handler), + HOST_SYMBOL(ecex_buffer_mouse_userdata), + HOST_SYMBOL(ecex_buffer_set_animation), + HOST_SYMBOL(ecex_buffer_set_animation_ms), + HOST_SYMBOL(ecex_buffer_clear_animation), + HOST_SYMBOL(ecex_buffer_is_animating), + HOST_SYMBOL(ecex_buffer_animation_userdata), + HOST_SYMBOL(ecex_tick_animations), + HOST_SYMBOL(ecex_buffer_replace_text), + HOST_SYMBOL(ecex_buffer_set_modified), + HOST_SYMBOL(ecex_draw_set_color), + HOST_SYMBOL(ecex_draw_rect), + HOST_SYMBOL(ecex_draw_rect_outline), + HOST_SYMBOL(ecex_draw_line), + HOST_SYMBOL(ecex_draw_text), + HOST_SYMBOL(ecex_draw_text_aligned), + HOST_SYMBOL(ecex_draw_text_width), + HOST_SYMBOL(ecex_draw_color_rgba8), + HOST_SYMBOL(ecex_draw_rect_i), + HOST_SYMBOL(ecex_draw_rect_outline_i), + HOST_SYMBOL(ecex_draw_line_i), + HOST_SYMBOL(ecex_draw_text_i), + HOST_SYMBOL(ecex_draw_text_id_i), + HOST_SYMBOL(ecex_draw_markdown_canvas_i), + HOST_SYMBOL(ecex_draw_markdown_text_i), + HOST_SYMBOL(ecex_draw_markdown_canvas_auto_i), + HOST_SYMBOL(ecex_draw_markdown_line_auto_i), + HOST_SYMBOL(ecex_draw_label_i), + HOST_SYMBOL(ecex_draw_stat_i), + HOST_SYMBOL(ecex_draw_tetris_preview_i), + HOST_SYMBOL(ecex_draw_rgba), HOST_SYMBOL(ecex_find_file), HOST_SYMBOL(ecex_save_current_buffer), HOST_SYMBOL(ecex_write_current_buffer), HOST_SYMBOL(ecex_request_prompt), HOST_SYMBOL(ecex_clear_prompt_request), HOST_SYMBOL(ecex_complete_command), + + HOST_SYMBOL(ecex_path_copy), + HOST_SYMBOL(ecex_path_expand_user), + HOST_SYMBOL(ecex_path_join), + HOST_SYMBOL(ecex_path_dirname), + HOST_SYMBOL(ecex_path_basename_dup), + HOST_SYMBOL(ecex_path_normalize), + HOST_SYMBOL(ecex_path_is_dir), + HOST_SYMBOL(ecex_path_is_file), + HOST_SYMBOL(ecex_path_exists), + HOST_SYMBOL(ecex_path_file_size), + HOST_SYMBOL(ecex_path_is_image), + HOST_SYMBOL(ecex_path_is_previewable_image), + HOST_SYMBOL(ecex_path_is_video), + HOST_SYMBOL(ecex_path_is_media), + HOST_SYMBOL(ecex_path_cwd), + HOST_SYMBOL(ecex_media_open), + HOST_SYMBOL(ecex_media_toggle_playback), HOST_SYMBOL(ecex_eval_source), HOST_SYMBOL(ecex_eval_current_buffer), HOST_SYMBOL(ecex_eval_current_line), @@ -106,6 +215,10 @@ static const host_symbol_t host_symbols[] = { HOST_SYMBOL(ecex_set_completion_enabled), HOST_SYMBOL(ecex_set_interactive_highlight_bg_color), HOST_SYMBOL(ecex_set_interactive_highlight_fg_color), + HOST_SYMBOL(ecex_set_current_line_bg_color), + HOST_SYMBOL(ecex_set_search_bg_color), + HOST_SYMBOL(ecex_set_line_numbers_enabled), + HOST_SYMBOL(ecex_set_current_line_enabled), HOST_SYMBOL(buffer_clear), HOST_SYMBOL(buffer_set_text), diff --git a/src/ecex.c b/src/ecex.c index 2f4c962..564d05f 100644 --- a/src/ecex.c +++ b/src/ecex.c @@ -1,13 +1,18 @@ #include "ecex.h" +#include "media.h" #include "ccdjit.h" #include "common.h" #include "config.h" #include "util.h" +#include "path.h" #include #include #include +#include +#include +#include extern FILE *popen(const char *command, const char *type); extern int pclose(FILE *stream); @@ -21,6 +26,639 @@ extern int pclose(FILE *stream); ecex_window_t *ecex_current_window(ecex_t *ed); + +void *ecex_config_alloc(size_t size) { + return malloc(size ? size : 1); +} + +void *ecex_config_calloc(size_t count, size_t size) { + if (count == 0) count = 1; + if (size == 0) size = 1; + return calloc(count, size); +} + +void ecex_config_free(void *ptr) { + free(ptr); +} + +double ecex_time_seconds(void) { + struct timeval tv; + gettimeofday(&tv, NULL); + return (double)tv.tv_sec + (double)tv.tv_usec / 1000000.0; +} + +static int ecex_env_enabled(const char *name) { + const char *v; + if (!name) return 0; + v = getenv(name); + return v && v[0] && v[0] != '0'; +} + +static int ecex_log_enabled(void) { + return ecex_env_enabled("ECEX_LOG") || + ecex_env_enabled("ECEX_TRACE_CALLBACKS") || + ecex_env_enabled("ECEX_TRACE_TETRIS"); +} + +void ecex_log(const char *message) { + if (!ecex_log_enabled()) return; + fprintf(stderr, "ecex-log: %s\n", message ? message : "(null)"); + fflush(stderr); +} + +void ecex_log_int(const char *message, int value) { + if (!ecex_log_enabled()) return; + fprintf(stderr, "ecex-log: %s%d\n", message ? message : "", value); + fflush(stderr); +} + +void ecex_log_double(const char *message, double value) { + if (!ecex_log_enabled()) return; + fprintf(stderr, "ecex-log: %s%.6f\n", message ? message : "", value); + fflush(stderr); +} + +void ecex_log_ptr(const char *message, const void *ptr) { + if (!ecex_log_enabled()) return; + fprintf(stderr, "ecex-log: %s%p\n", message ? message : "", ptr); + fflush(stderr); +} + +void ecex_mem_zero(void *ptr, size_t size) { + if (!ptr || size == 0) return; + memset(ptr, 0, size); +} + + +static ecex_object_entry_t *ecex_object_find_entry(ecex_t *ed, void *object) { + if (!ed || !object) return NULL; + for (size_t i = 0; i < ed->object_count; ++i) { + if (ed->objects[i].ptr == object) return &ed->objects[i]; + } + return NULL; +} + +static int ecex_reserve_objects(ecex_t *ed, size_t needed) { + ecex_object_entry_t *new_objects; + size_t new_cap; + if (!ed) return ECEX_ERR; + if (needed <= ed->object_cap) return ECEX_OK; + new_cap = ed->object_cap ? ed->object_cap * 2 : 16; + while (new_cap < needed) new_cap *= 2; + new_objects = realloc(ed->objects, new_cap * sizeof(*new_objects)); + if (!new_objects) return ECEX_ERR; + memset(new_objects + ed->object_cap, 0, (new_cap - ed->object_cap) * sizeof(*new_objects)); + ed->objects = new_objects; + ed->object_cap = new_cap; + return ECEX_OK; +} + +void *ecex_object_alloc(ecex_t *ed, size_t size) { + void *ptr; + if (!ed) return NULL; + if (size == 0) size = 1; + if (ecex_reserve_objects(ed, ed->object_count + 1) != ECEX_OK) return NULL; + ptr = malloc(size); + if (!ptr) return NULL; + ed->objects[ed->object_count].ptr = ptr; + ed->objects[ed->object_count].size = size; + ed->object_count++; + return ptr; +} + +void *ecex_object_calloc(ecex_t *ed, size_t count, size_t size) { + void *ptr; + size_t total; + if (!ed) return NULL; + if (count == 0) count = 1; + if (size == 0) size = 1; + if (count > ((size_t)-1) / size) return NULL; + total = count * size; + ptr = ecex_object_alloc(ed, total); + if (!ptr) return NULL; + memset(ptr, 0, total); + return ptr; +} + +int ecex_object_free(ecex_t *ed, void *object) { + if (!ed || !object) return ECEX_ERR; + for (size_t i = 0; i < ed->object_count; ++i) { + if (ed->objects[i].ptr == object) { + free(ed->objects[i].ptr); + if (i + 1 < ed->object_count) { + memmove(&ed->objects[i], &ed->objects[i + 1], (ed->object_count - i - 1) * sizeof(ed->objects[i])); + } + --ed->object_count; + if (ed->object_count < ed->object_cap) memset(&ed->objects[ed->object_count], 0, sizeof(ed->objects[ed->object_count])); + return ECEX_OK; + } + } + return ECEX_ERR; +} + +int ecex_object_valid(ecex_t *ed, void *object) { + return ecex_object_find_entry(ed, object) ? 1 : 0; +} + +int ecex_object_i32_get(ecex_t *ed, void *object, size_t byte_offset, int fallback) { + ecex_object_entry_t *o = ecex_object_find_entry(ed, object); + int value; + if (!o || !o->ptr || byte_offset > o->size || o->size - byte_offset < sizeof(int)) return fallback; + memcpy(&value, (const char *)o->ptr + byte_offset, sizeof(value)); + return value; +} + +int ecex_object_i32_set(ecex_t *ed, void *object, size_t byte_offset, int value) { + ecex_object_entry_t *o = ecex_object_find_entry(ed, object); + if (!o || !o->ptr || byte_offset > o->size || o->size - byte_offset < sizeof(int)) return ECEX_ERR; + memcpy((char *)o->ptr + byte_offset, &value, sizeof(value)); + return ECEX_OK; +} + +void *ecex_object_ptr_get(ecex_t *ed, void *object, size_t byte_offset) { + ecex_object_entry_t *o = ecex_object_find_entry(ed, object); + void *value = NULL; + if (!o || !o->ptr || byte_offset > o->size || o->size - byte_offset < sizeof(void *)) return NULL; + memcpy(&value, (const char *)o->ptr + byte_offset, sizeof(value)); + return value; +} + +int ecex_object_ptr_set(ecex_t *ed, void *object, size_t byte_offset, void *value) { + ecex_object_entry_t *o = ecex_object_find_entry(ed, object); + if (!o || !o->ptr || byte_offset > o->size || o->size - byte_offset < sizeof(void *)) return ECEX_ERR; + memcpy((char *)o->ptr + byte_offset, &value, sizeof(value)); + return ECEX_OK; +} + +int ecex_i32_get(const int *items, size_t index) { + if (!items) return 0; + return items[index]; +} + +void ecex_i32_set(int *items, size_t index, int value) { + if (!items) return; + items[index] = value; +} + +static char *ecex_var_strdup(const char *s) { + size_t n; + char *out; + if (!s) s = ""; + n = strlen(s) + 1; + out = malloc(n); + if (!out) return NULL; + memcpy(out, s, n); + return out; +} + +static int ecex_var_name_eq(const char *a, const char *b) { + if (!a) a = ""; + if (!b) b = ""; + return strcmp(a, b) == 0; +} + +static ecex_var_entry_t *ecex_var_find_entry(ecex_t *ed, void *owner, const char *name) { + size_t i; + if (!ed || !name) return NULL; + for (i = 0; i < ed->var_count; ++i) { + ecex_var_entry_t *v = &ed->vars[i]; + if (v->owner == owner && ecex_var_name_eq(v->name, name)) return v; + } + return NULL; +} + +static int ecex_reserve_vars(ecex_t *ed, size_t needed) { + ecex_var_entry_t *new_vars; + size_t new_cap; + if (!ed) return ECEX_ERR; + if (needed <= ed->var_cap) return ECEX_OK; + new_cap = ed->var_cap ? ed->var_cap * 2 : 16; + while (new_cap < needed) new_cap *= 2; + new_vars = realloc(ed->vars, new_cap * sizeof(*new_vars)); + if (!new_vars) return ECEX_ERR; + memset(new_vars + ed->var_cap, 0, (new_cap - ed->var_cap) * sizeof(*new_vars)); + ed->vars = new_vars; + ed->var_cap = new_cap; + return ECEX_OK; +} + +void *ecex_var_get(ecex_t *ed, void *owner, const char *name) { + ecex_var_entry_t *v = ecex_var_find_entry(ed, owner, name); + return v ? v->data : NULL; +} + +void *ecex_var_get_or_alloc(ecex_t *ed, void *owner, const char *name, size_t count, size_t elem_size) { + ecex_var_entry_t *v; + void *data; + if (!ed || !name) return NULL; + if (count == 0) count = 1; + if (elem_size == 0) elem_size = 1; + + v = ecex_var_find_entry(ed, owner, name); + if (v) { + if (v->elem_size != elem_size) { + ecex_log("ecex_var_get_or_alloc: existing slot element-size mismatch"); + return NULL; + } + if (v->count < count) { + void *new_data; + if (!v->dynamic) { + ecex_log("ecex_var_get_or_alloc: static slot too small"); + return NULL; + } + new_data = realloc(v->data, count * elem_size); + if (!new_data) return NULL; + memset((char *)new_data + v->count * elem_size, 0, (count - v->count) * elem_size); + v->data = new_data; + v->count = count; + } + return v->data; + } + + if (ecex_reserve_vars(ed, ed->var_count + 1) != ECEX_OK) return NULL; + data = calloc(count, elem_size); + if (!data) return NULL; + + v = &ed->vars[ed->var_count++]; + memset(v, 0, sizeof(*v)); + v->owner = owner; + v->name = ecex_var_strdup(name); + if (!v->name) { + free(data); + --ed->var_count; + return NULL; + } + v->data = data; + v->elem_size = elem_size; + v->count = count; + v->kind = (elem_size == sizeof(int)) ? ECEX_VAR_I32 : ECEX_VAR_BYTES; + v->dynamic = 1; + return data; +} + +int ecex_var_bind_static(ecex_t *ed, void *owner, const char *name, void *data, size_t count, size_t elem_size) { + ecex_var_entry_t *v; + if (!ed || !name || !data) return ECEX_ERR; + if (count == 0) count = 1; + if (elem_size == 0) elem_size = 1; + + v = ecex_var_find_entry(ed, owner, name); + if (!v) { + if (ecex_reserve_vars(ed, ed->var_count + 1) != ECEX_OK) return ECEX_ERR; + v = &ed->vars[ed->var_count++]; + memset(v, 0, sizeof(*v)); + v->name = ecex_var_strdup(name); + if (!v->name) { + --ed->var_count; + return ECEX_ERR; + } + } else if (v->dynamic) { + free(v->data); + } + + v->owner = owner; + v->data = data; + v->elem_size = elem_size; + v->count = count; + v->kind = (elem_size == sizeof(int)) ? ECEX_VAR_I32 : ECEX_VAR_BYTES; + v->dynamic = 0; + return ECEX_OK; +} + +static void ecex_var_entry_clear(ecex_var_entry_t *v) { + if (!v) return; + if (v->dynamic) free(v->data); + free(v->name); + memset(v, 0, sizeof(*v)); +} + +int ecex_var_free(ecex_t *ed, void *owner, const char *name) { + size_t i; + if (!ed || !name) return ECEX_ERR; + for (i = 0; i < ed->var_count; ++i) { + if (ed->vars[i].owner == owner && ecex_var_name_eq(ed->vars[i].name, name)) { + ecex_var_entry_clear(&ed->vars[i]); + if (i + 1 < ed->var_count) { + memmove(&ed->vars[i], &ed->vars[i + 1], (ed->var_count - i - 1) * sizeof(ed->vars[i])); + } + --ed->var_count; + if (ed->var_count < ed->var_cap) memset(&ed->vars[ed->var_count], 0, sizeof(ed->vars[ed->var_count])); + return ECEX_OK; + } + } + return ECEX_ERR; +} + +int ecex_var_free_owner(ecex_t *ed, void *owner) { + size_t i; + if (!ed) return ECEX_ERR; + i = 0; + while (i < ed->var_count) { + if (ed->vars[i].owner == owner) { + ecex_var_entry_clear(&ed->vars[i]); + if (i + 1 < ed->var_count) { + memmove(&ed->vars[i], &ed->vars[i + 1], (ed->var_count - i - 1) * sizeof(ed->vars[i])); + } + --ed->var_count; + if (ed->var_count < ed->var_cap) memset(&ed->vars[ed->var_count], 0, sizeof(ed->vars[ed->var_count])); + continue; + } + ++i; + } + return ECEX_OK; +} + +int ecex_var_i32_get(ecex_t *ed, void *owner, const char *name, size_t index, int fallback) { + ecex_var_entry_t *v = ecex_var_find_entry(ed, owner, name); + if (!v || !v->data || v->elem_size != sizeof(int) || index >= v->count) return fallback; + return ((int *)v->data)[index]; +} + +int ecex_var_i32_set(ecex_t *ed, void *owner, const char *name, size_t index, int value) { + int *data; + if (!ed || !name) return ECEX_ERR; + data = (int *)ecex_var_get_or_alloc(ed, owner, name, index + 1, sizeof(int)); + if (!data) return ECEX_ERR; + data[index] = value; + return ECEX_OK; +} + +int ecex_var_i32(ecex_t *ed, void *owner, const char *name, int fallback) { + return ecex_var_i32_get(ed, owner, name, 0, fallback); +} + +int ecex_var_i32_set_scalar(ecex_t *ed, void *owner, const char *name, int value) { + return ecex_var_i32_set(ed, owner, name, 0, value); +} + + + +static ecex_text_entry_t *ecex_text_find_entry(ecex_t *ed, void *owner, int id) { + if (!ed) return NULL; + for (size_t i = 0; i < ed->text_count; ++i) { + ecex_text_entry_t *t = &ed->texts[i]; + if (t->owner == owner && t->id == id) return t; + } + return NULL; +} + +static int ecex_reserve_texts(ecex_t *ed, size_t needed) { + ecex_text_entry_t *new_texts; + size_t new_cap; + if (!ed) return ECEX_ERR; + if (needed <= ed->text_cap) return ECEX_OK; + new_cap = ed->text_cap ? ed->text_cap * 2 : 32; + while (new_cap < needed) new_cap *= 2; + new_texts = realloc(ed->texts, new_cap * sizeof(*new_texts)); + if (!new_texts) return ECEX_ERR; + memset(new_texts + ed->text_cap, 0, (new_cap - ed->text_cap) * sizeof(*new_texts)); + ed->texts = new_texts; + ed->text_cap = new_cap; + return ECEX_OK; +} + +static void ecex_text_entry_clear(ecex_text_entry_t *t) { + if (!t) return; + free(t->text); + memset(t, 0, sizeof(*t)); +} + +int ecex_text_set(ecex_t *ed, void *owner, int id, const char *text, int len) { + ecex_text_entry_t *t; + char *copy; + size_t n; + if (!ed || id < 0) return ECEX_ERR; + if (!text) text = ""; + if (len < 0) n = strlen(text); + else n = (size_t)len; + copy = malloc(n + 1); + if (!copy) return ECEX_ERR; + if (n) memcpy(copy, text, n); + copy[n] = '\0'; + + t = ecex_text_find_entry(ed, owner, id); + if (!t) { + if (ecex_reserve_texts(ed, ed->text_count + 1) != ECEX_OK) { + free(copy); + return ECEX_ERR; + } + t = &ed->texts[ed->text_count++]; + memset(t, 0, sizeof(*t)); + t->owner = owner; + t->id = id; + } + free(t->text); + t->text = copy; + t->len = n; + return ECEX_OK; +} + +int ecex_text_set_buffer_title(ecex_t *ed, void *owner, int id, buffer_t *buffer) { + const char *title = NULL; + if (buffer) title = buffer->path ? buffer->path : buffer->name; + return ecex_text_set(ed, owner, id, title ? title : "(unnamed)", -1); +} + +int ecex_text_free(ecex_t *ed, void *owner, int id) { + if (!ed) return ECEX_ERR; + for (size_t i = 0; i < ed->text_count; ++i) { + if (ed->texts[i].owner == owner && ed->texts[i].id == id) { + ecex_text_entry_clear(&ed->texts[i]); + if (i + 1 < ed->text_count) { + memmove(&ed->texts[i], &ed->texts[i + 1], (ed->text_count - i - 1) * sizeof(ed->texts[i])); + } + --ed->text_count; + if (ed->text_count < ed->text_cap) memset(&ed->texts[ed->text_count], 0, sizeof(ed->texts[ed->text_count])); + return ECEX_OK; + } + } + return ECEX_ERR; +} + +int ecex_text_free_owner(ecex_t *ed, void *owner) { + if (!ed) return ECEX_ERR; + for (size_t i = 0; i < ed->text_count;) { + if (ed->texts[i].owner == owner) { + ecex_text_entry_clear(&ed->texts[i]); + if (i + 1 < ed->text_count) { + memmove(&ed->texts[i], &ed->texts[i + 1], (ed->text_count - i - 1) * sizeof(ed->texts[i])); + } + --ed->text_count; + if (ed->text_count < ed->text_cap) memset(&ed->texts[ed->text_count], 0, sizeof(ed->texts[ed->text_count])); + continue; + } + ++i; + } + return ECEX_OK; +} + +const char *ecex_text_get_for_draw(ecex_t *ed, void *owner, int id) { + ecex_text_entry_t *t = ecex_text_find_entry(ed, owner, id); + return t && t->text ? t->text : ""; +} + +static int ecex_ascii_equal_ci(const char *a, const char *b) { + unsigned char ca; + unsigned char cb; + if (!a || !b) return 0; + while (*a && *b) { + ca = (unsigned char)*a; + cb = (unsigned char)*b; + if (ca >= 'A' && ca <= 'Z') ca = (unsigned char)(ca - 'A' + 'a'); + if (cb >= 'A' && cb <= 'Z') cb = (unsigned char)(cb - 'A' + 'a'); + if (ca != cb) return 0; + ++a; + ++b; + } + return *a == '\0' && *b == '\0'; +} + +static const char *ecex_path_extension(const char *path) { + const char *dot; + if (!path) return NULL; + dot = strrchr(path, '.'); + return dot && dot[0] ? dot : NULL; +} + +static int ecex_reserve_file_handlers(ecex_t *ed, size_t needed) { + ecex_file_handler_t *new_handlers; + size_t new_cap; + if (!ed) return ECEX_ERR; + if (needed <= ed->file_handler_cap) return ECEX_OK; + new_cap = ed->file_handler_cap ? ed->file_handler_cap * 2 : 8; + while (new_cap < needed) new_cap *= 2; + new_handlers = realloc(ed->file_handlers, new_cap * sizeof(*new_handlers)); + if (!new_handlers) return ECEX_ERR; + memset(new_handlers + ed->file_handler_cap, 0, (new_cap - ed->file_handler_cap) * sizeof(*new_handlers)); + ed->file_handlers = new_handlers; + ed->file_handler_cap = new_cap; + return ECEX_OK; +} + +int ecex_register_file_handler(ecex_t *ed, const char *extension, ecex_file_handler_fn fn) { + char *copy; + if (!ed || !extension || !extension[0] || !fn) return ECEX_ERR; + for (size_t i = 0; i < ed->file_handler_count; ++i) { + if (ecex_ascii_equal_ci(ed->file_handlers[i].extension, extension)) { + ed->file_handlers[i].fn = fn; + return ECEX_OK; + } + } + if (ecex_reserve_file_handlers(ed, ed->file_handler_count + 1) != ECEX_OK) return ECEX_ERR; + copy = ecex_var_strdup(extension); + if (!copy) return ECEX_ERR; + ed->file_handlers[ed->file_handler_count].extension = copy; + ed->file_handlers[ed->file_handler_count].fn = fn; + ed->file_handler_count++; + return ECEX_OK; +} + +int ecex_run_file_handlers(ecex_t *ed, buffer_t *buffer) { + const char *ext; + const char *path; + if (!ed || !buffer) return ECEX_ERR; + path = buffer->path ? buffer->path : buffer->name; + ext = ecex_path_extension(path); + if (!ext) return ECEX_OK; + for (size_t i = 0; i < ed->file_handler_count; ++i) { + if (ecex_ascii_equal_ci(ed->file_handlers[i].extension, ext)) { + return ed->file_handlers[i].fn(ed, buffer); + } + } + return ECEX_OK; +} + +int ecex_prng_next_bounded(unsigned int *state, int bound) { + unsigned int x; + unsigned int limit; + + if (!state || bound <= 0) return 0; + + x = *state; + if (x == 0u) x = 0x6d2b79f5u; + + /* Xorshift32. Keep this host-side so CCDJIT plugins do not depend on + * unsigned multiply/divide lowering details for game logic randomness. */ + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + if (x == 0u) x = 0xa5a5a5a5u; + *state = x; + + /* Avoid modulo bias enough for tiny bounds without libc. */ + limit = 0xffffffffu - (0xffffffffu % (unsigned int)bound); + while (x >= limit) { + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + if (x == 0u) x = 0x9e3779b9u; + *state = x; + } + + return (int)(x % (unsigned int)bound); +} + + +int ecex_random_bounded(int bound) { + static unsigned int state = 0x7f4a7c15u; + unsigned int mix; + + if (bound <= 0) return 0; + /* Host-owned PRNG for plugins that should not pass writable state pointers + * through the JIT ABI. Stir in a stack address so separate launches differ + * enough for games/demos without requiring libc time calls from plugins. */ + mix = (unsigned int)(size_t)&bound; + state ^= mix + 0x9e3779b9u + (state << 6) + (state >> 2); + return ecex_prng_next_bounded(&state, bound); +} + +int ecex_tetris_shape_cell(int piece, int rot, int col, int row) { + int p = piece % 7; + int r = rot & 3; + if (p < 0) p += 7; + if (col < 0 || col >= 4 || row < 0 || row >= 4) return 0; + + /* I */ + if (p == 0) { + if ((r & 1) == 0) return row == 1; + return col == 1; + } + /* O */ + if (p == 1) return (row == 1 || row == 2) && (col == 1 || col == 2); + /* T */ + if (p == 2) { + if (r == 0) return (row == 1 && col >= 0 && col <= 2) || (row == 2 && col == 1); + if (r == 1) return (col == 1 && row >= 0 && row <= 2) || (row == 1 && col == 2); + if (r == 2) return (row == 1 && col >= 0 && col <= 2) || (row == 0 && col == 1); + return (col == 1 && row >= 0 && row <= 2) || (row == 1 && col == 0); + } + /* S */ + if (p == 3) { + if ((r & 1) == 0) return (row == 1 && (col == 1 || col == 2)) || (row == 2 && (col == 0 || col == 1)); + return (col == 1 && (row == 0 || row == 1)) || (col == 2 && (row == 1 || row == 2)); + } + /* Z */ + if (p == 4) { + if ((r & 1) == 0) return (row == 1 && (col == 0 || col == 1)) || (row == 2 && (col == 1 || col == 2)); + return (col == 2 && (row == 0 || row == 1)) || (col == 1 && (row == 1 || row == 2)); + } + /* J */ + if (p == 5) { + if (r == 0) return (row == 1 && col >= 0 && col <= 2) || (row == 0 && col == 0); + if (r == 1) return (col == 1 && row >= 0 && row <= 2) || (row == 0 && col == 2); + if (r == 2) return (row == 1 && col >= 0 && col <= 2) || (row == 2 && col == 2); + return (col == 1 && row >= 0 && row <= 2) || (row == 2 && col == 0); + } + /* L */ + if (r == 0) return (row == 1 && col >= 0 && col <= 2) || (row == 0 && col == 2); + if (r == 1) return (col == 1 && row >= 0 && row <= 2) || (row == 2 && col == 2); + if (r == 2) return (row == 1 && col >= 0 && col <= 2) || (row == 2 && col == 0); + return (col == 1 && row >= 0 && row <= 2) || (row == 0 && col == 0); +} + +static int ecex_file_browser_preview_current(ecex_t *ed); +static int ecex_file_browser_update_preview_if_enabled(ecex_t *ed); + static ecex_color_t ecex_color(float r, float g, float b) { ecex_color_t c = {r, g, b}; return c; @@ -71,6 +709,16 @@ static void ecex_theme_set_defaults(ecex_t *ed) { if (!buf) return ECEX_ERR static int cmd_quit(ecex_t *ed) { + if (!ed) return ECEX_ERR; + if (ecex_has_modified_buffers(ed)) { + fprintf(stderr, "ecex: refusing to quit; modified buffers exist. Save or use force-quit.\n"); + return ECEX_ERR; + } + ed->should_quit = 1; + return ECEX_OK; +} + +static int cmd_force_quit(ecex_t *ed) { if (!ed) return ECEX_ERR; ed->should_quit = 1; return ECEX_OK; @@ -88,8 +736,18 @@ static int cmd_balance_windows(ecex_t *ed) { return ecex_balance_windows(ed); } static int cmd_move_left(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); buffer_move_left(buf); return ECEX_OK; } static int cmd_move_right(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); buffer_move_right(buf); return ECEX_OK; } -static int cmd_move_up(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); buffer_move_up(buf); return ECEX_OK; } -static int cmd_move_down(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); buffer_move_down(buf); return ECEX_OK; } +static int cmd_move_up(ecex_t *ed) { + CURRENT_BUFFER_OR_ERR(ed); + buffer_move_up(buf); + ecex_file_browser_update_preview_if_enabled(ed); + return ECEX_OK; +} +static int cmd_move_down(ecex_t *ed) { + CURRENT_BUFFER_OR_ERR(ed); + buffer_move_down(buf); + ecex_file_browser_update_preview_if_enabled(ed); + return ECEX_OK; +} static int cmd_move_word_left(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); buffer_move_word_left(buf); return ECEX_OK; } static int cmd_move_word_right(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); buffer_move_word_right(buf); return ECEX_OK; } static int cmd_beginning_of_line(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); buffer_move_beginning_of_line(buf); return ECEX_OK; } @@ -145,91 +803,484 @@ static int cmd_kill_region(ecex_t *ed) { size_t end = 0; buffer_selection_range(buf, &start, &end); - char *text = buffer_substring(buf, start, end); - if (!text) return ECEX_ERR; + char *text = buffer_substring(buf, start, end); + if (!text) return ECEX_ERR; + + int result = ecex_clipboard_set(ed, text); + free(text); + if (result != ECEX_OK) return result; + + return buffer_delete_selection(buf); +} + +static int cmd_list_commands(ecex_t *ed) { return ecex_list_commands(ed); } +static int cmd_list_buffers(ecex_t *ed) { return ecex_list_buffers(ed); } +static int cmd_switch_buffer(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_SWITCH_BUFFER, "Switch buffer: "); return ECEX_OK; } +static int cmd_kill_buffer_command(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_KILL_BUFFER, "Kill buffer: "); return ECEX_OK; } +static int cmd_force_kill_buffer_command(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_FORCE_KILL_BUFFER, "Force kill buffer: "); return ECEX_OK; } +static int cmd_compile(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_COMPILE, "Compile: "); return ECEX_OK; } +static int cmd_grep(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_GREP, "Grep: "); return ECEX_OK; } +static int cmd_recompile(ecex_t *ed) { return ecex_rerun_compile(ed); } +static int cmd_regrep(ecex_t *ed) { return ecex_rerun_grep(ed); } +static int cmd_next_error(ecex_t *ed) { return ecex_next_interactive_action(ed); } +static int cmd_previous_error(ecex_t *ed) { return ecex_previous_interactive_action(ed); } +static int cmd_comment_region(ecex_t *ed) { return ecex_comment_region(ed); } +static int cmd_uncomment_region(ecex_t *ed) { return ecex_uncomment_region(ed); } +static int cmd_toggle_line_numbers(ecex_t *ed) { if (!ed) return ECEX_ERR; ed->theme.line_numbers_enabled = !ed->theme.line_numbers_enabled; return ECEX_OK; } +static int cmd_toggle_current_line(ecex_t *ed) { if (!ed) return ECEX_ERR; ed->theme.current_line_enabled = !ed->theme.current_line_enabled; return ECEX_OK; } +static int cmd_isearch_forward(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_ISEARCH_FORWARD, "I-search: "); return ECEX_OK; } +static int cmd_isearch_backward(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_ISEARCH_BACKWARD, "I-search backward: "); return ECEX_OK; } + +static int cmd_find_file(ecex_t *ed) { + ecex_request_prompt(ed, ECEX_PROMPT_FIND_FILE, "Find file: "); + return ECEX_OK; +} + +static int cmd_write_file(ecex_t *ed) { + ecex_request_prompt(ed, ECEX_PROMPT_WRITE_FILE, "Write file: "); + return ECEX_OK; +} + +static int cmd_save_buffer(ecex_t *ed) { + if (!ed) return ECEX_ERR; + + buffer_t *buf = ecex_current_buffer(ed); + if (!buf) return ECEX_ERR; + + if (!buf->path) { + ecex_request_prompt(ed, ECEX_PROMPT_WRITE_FILE, "Write file: "); + return ECEX_OK; + } + + return ecex_save_current_buffer(ed); +} + +static int cmd_eval_buffer(ecex_t *ed) { + return ecex_eval_current_buffer(ed); +} + +static int cmd_eval_line(ecex_t *ed) { + return ecex_eval_current_line(ed); +} + +static int cmd_eval_region(ecex_t *ed) { + return ecex_eval_current_region(ed); +} + +static int cmd_eval_file(ecex_t *ed) { + ecex_request_prompt(ed, ECEX_PROMPT_EVAL_FILE, "Eval file: "); + return ECEX_OK; +} + +static int cmd_eval_rerun_last(ecex_t *ed) { + return ecex_eval_rerun_last(ed); +} + +static int cmd_quit_window(ecex_t *ed) { + if (!ed) return ECEX_ERR; + if (ecex_window_count(ed) > 1) return ecex_delete_window(ed); + return ecex_previous_buffer(ed); +} + +static int cmd_reload_config(ecex_t *ed) { + return ecex_reload_config(ed); +} + + +/* Built-in file browser -------------------------------------------------- */ + +typedef struct ecex_file_entry { + char *name; + char *path; + int is_dir; + int is_image; + long long size; +} ecex_file_entry_t; + +static char ecex_fb_cwd[4096] = "."; +static char ecex_fb_history[64][4096]; +static size_t ecex_fb_history_count = 0; +static size_t ecex_fb_history_index = 0; +static int ecex_fb_preview_expanded = 0; + +static void ecex_file_entry_free(ecex_file_entry_t *entries, size_t count) { + if (!entries) return; + for (size_t i = 0; i < count; i++) { + free(entries[i].name); + free(entries[i].path); + } + free(entries); +} + +static int ecex_file_entry_compare(const void *a, const void *b) { + const ecex_file_entry_t *ea = (const ecex_file_entry_t *)a; + const ecex_file_entry_t *eb = (const ecex_file_entry_t *)b; + if (ea->is_dir != eb->is_dir) return eb->is_dir - ea->is_dir; + return strcmp(ea->name ? ea->name : "", eb->name ? eb->name : ""); +} + +static int ecex_file_browser_collect(const char *dir, ecex_file_entry_t **out_entries, size_t *out_count) { + if (out_entries) *out_entries = NULL; + if (out_count) *out_count = 0; + if (!dir || !out_entries || !out_count) return ECEX_ERR; + + DIR *d = opendir(dir); + if (!d) return ECEX_ERR; + + size_t cap = 64; + size_t count = 0; + ecex_file_entry_t *entries = calloc(cap, sizeof(*entries)); + if (!entries) { closedir(d); return ECEX_ERR; } + + struct dirent *entry; + while ((entry = readdir(d)) != NULL) { + const char *name = entry->d_name; + if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) continue; + + if (count == cap) { + size_t new_cap = cap * 2; + ecex_file_entry_t *grown = realloc(entries, new_cap * sizeof(*entries)); + if (!grown) break; + memset(grown + cap, 0, (new_cap - cap) * sizeof(*grown)); + entries = grown; + cap = new_cap; + } + + char *path = ecex_path_join(dir, name); + if (!path) continue; + char *name_copy = ecex_strdup(name); + if (!name_copy) { free(path); continue; } + + entries[count].name = name_copy; + entries[count].path = path; + entries[count].is_dir = ecex_path_is_dir(path); + entries[count].is_image = ecex_path_is_image(path); + entries[count].size = ecex_path_file_size(path); + count++; + } + + closedir(d); + qsort(entries, count, sizeof(*entries), ecex_file_entry_compare); + *out_entries = entries; + *out_count = count; + return ECEX_OK; +} + +static void ecex_file_browser_push_history(const char *dir) { + if (!dir || !dir[0]) return; + if (ecex_fb_history_count > 0 && strcmp(ecex_fb_history[ecex_fb_history_index], dir) == 0) return; + + if (ecex_fb_history_index + 1 < ecex_fb_history_count) { + ecex_fb_history_count = ecex_fb_history_index + 1; + } + + if (ecex_fb_history_count == 64) { + memmove(ecex_fb_history, ecex_fb_history + 1, sizeof(ecex_fb_history[0]) * 63); + ecex_fb_history_count = 63; + if (ecex_fb_history_index > 0) ecex_fb_history_index--; + } + + ecex_path_copy(ecex_fb_history[ecex_fb_history_count], sizeof(ecex_fb_history[0]), dir); + ecex_fb_history_index = ecex_fb_history_count; + ecex_fb_history_count++; +} + +static int ecex_file_browser_populate(ecex_t *ed, const char *dir, int push_history); +static int ecex_file_browser_move_to_action(buffer_t *buf, size_t action_index); + +static int ecex_file_browser_open_action(ecex_t *ed, + buffer_t *buffer, + size_t line, + const char *payload, + void *userdata) { + (void)buffer; + (void)line; + (void)userdata; + if (!ed || !payload) return ECEX_ERR; + + char selected[4096]; + ecex_path_copy(selected, sizeof(selected), payload); + + if (ecex_path_is_dir(selected)) return ecex_file_browser_populate(ed, selected, 1); + if (ecex_path_is_file(selected) && ecex_path_is_media(selected)) return ecex_media_open(ed, selected); + if (ecex_path_is_file(selected)) return ecex_find_file(ed, selected); + return ECEX_ERR; +} + +static int ecex_file_browser_populate(ecex_t *ed, const char *dir, int push_history) { + if (!ed) return ECEX_ERR; + + char *normalized = ecex_path_normalize((dir && dir[0]) ? dir : ecex_fb_cwd); + if (!normalized) return ECEX_ERR; + if (!ecex_path_is_dir(normalized)) { + char *parent = ecex_path_dirname(normalized); + free(normalized); + normalized = parent; + if (!normalized) return ECEX_ERR; + } + + ecex_path_copy(ecex_fb_cwd, sizeof(ecex_fb_cwd), normalized); + if (push_history) ecex_file_browser_push_history(ecex_fb_cwd); + + buffer_t *buf = ecex_find_buffer(ed, "*file-browser*"); + if (!buf) buf = ecex_create_interactive_buffer(ed, "*file-browser*"); + if (!buf) { free(normalized); return ECEX_ERR; } + + buffer_clear(buf); + buffer_set_interactive(buf, 1); + ecex_buffer_set_major_mode_by_name(ed, buf, "file-browser-mode"); + + char line[8192]; + snprintf(line, sizeof(line), + "File browser: %s\n\nKeys: RET/l open, h parent, g refresh, b/f history, v preview, m toggle preview, q quit.\n\n", + ecex_fb_cwd); + buffer_append(buf, line); + + char *parent = ecex_path_dirname(ecex_fb_cwd); + if (parent) { + ecex_interactive_append_line(ed, buf, "[..] parent", ecex_file_browser_open_action, parent, NULL); + free(parent); + } + + ecex_file_entry_t *entries = NULL; + size_t count = 0; + if (ecex_file_browser_collect(ecex_fb_cwd, &entries, &count) != ECEX_OK) { + buffer_append(buf, "\nCould not read directory.\n"); + } else { + for (size_t i = 0; i < count; i++) { + const char *tag = entries[i].is_dir ? "[D]" : (entries[i].is_image ? "[I]" : (ecex_path_is_video(entries[i].path) ? "[V]" : " ")); + if (entries[i].is_dir) { + snprintf(line, sizeof(line), "%s %s/", tag, entries[i].name); + } else if (entries[i].size >= 0) { + snprintf(line, sizeof(line), "%s %s (%lld bytes)", tag, entries[i].name, entries[i].size); + } else { + snprintf(line, sizeof(line), "%s %s", tag, entries[i].name); + } + ecex_interactive_append_line(ed, buf, line, ecex_file_browser_open_action, entries[i].path, NULL); + } + } + ecex_file_entry_free(entries, count); + + if (ecex_fb_preview_expanded) { + buffer_append(buf, "\nPreview pane is active: move up/down to update it, v refreshes, m closes it.\n"); + } + + buf->point = 0; + buf->scroll_line = 0; + buf->scroll_col = 0; + if (buf->interactive_action_count > 0) { + /* Start on the first actual directory entry instead of the [..] parent row. + * That makes pressing m/v immediately show a useful preview. */ + ecex_file_browser_move_to_action(buf, buf->interactive_action_count > 1 ? 1 : 0); + } + buf->modified = 0; + free(normalized); + int result = ecex_switch_buffer(ed, "*file-browser*"); + if (result == ECEX_OK && ecex_fb_preview_expanded) { + ecex_file_browser_update_preview_if_enabled(ed); + } + return result; +} + +static int ecex_file_browser_current_payload(ecex_t *ed, char *out, size_t out_size) { + if (!ed || !out || out_size == 0) return ECEX_ERR; + buffer_t *buf = ecex_current_buffer(ed); + if (!buf || !buffer_is_interactive(buf)) return ECEX_ERR; + size_t line = buffer_current_line_number(buf); + if (line > 0) line--; + ecex_interactive_line_action_t *action = buffer_interactive_action_at_line(buf, line); + if ((!action || !action->payload) && buf->interactive_action_count > 0) { + action = &buf->interactive_actions[0]; + } + if (!action || !action->payload) return ECEX_ERR; + return ecex_path_copy(out, out_size, action->payload) == 0 ? ECEX_OK : ECEX_ERR; +} + +static int ecex_file_browser_move_to_action(buffer_t *buf, size_t action_index) { + if (!buf || !buffer_is_interactive(buf) || buf->interactive_action_count == 0) return ECEX_ERR; + if (action_index >= buf->interactive_action_count) action_index = buf->interactive_action_count - 1; + size_t target_line = buf->interactive_actions[action_index].line; + size_t pos = 0; + for (size_t line = 0; line < target_line && pos < buf->len; pos++) { + if (buf->data[pos] == '\n') line++; + } + buffer_set_point(buf, pos); + return ECEX_OK; +} + +static buffer_t *ecex_file_preview_buffer(ecex_t *ed) { + if (!ed) return NULL; + buffer_t *preview = ecex_find_buffer(ed, "*file-preview*"); + if (!preview) preview = ecex_create_buffer(ed, "*file-preview*", NULL, 0); + return preview; +} + +static int ecex_file_browser_show_preview_pane(ecex_t *ed, buffer_t *preview) { + if (!ed || !preview || ed->window_count == 0) return ECEX_ERR; - int result = ecex_clipboard_set(ed, text); - free(text); - if (result != ECEX_OK) return result; + size_t browser_window = ed->current_window_index; + if (ed->window_count == 1) { + if (ecex_split_window_vertically(ed) != ECEX_OK) return ECEX_ERR; + ed->windows[ed->current_window_index].buffer = preview; + ed->current_window_index = browser_window; + return ecex_sync_current_buffer(ed); + } - return buffer_delete_selection(buf); + size_t preview_window = (browser_window + 1) % ed->window_count; + if (preview_window == browser_window && ed->window_count > 1) preview_window = 1; + ed->windows[preview_window].buffer = preview; + ed->current_window_index = browser_window; + return ecex_sync_current_buffer(ed); } -static int cmd_list_commands(ecex_t *ed) { return ecex_list_commands(ed); } -static int cmd_list_buffers(ecex_t *ed) { return ecex_list_buffers(ed); } -static int cmd_switch_buffer(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_SWITCH_BUFFER, "Switch buffer: "); return ECEX_OK; } -static int cmd_kill_buffer_command(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_KILL_BUFFER, "Kill buffer: "); return ECEX_OK; } -static int cmd_compile(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_COMPILE, "Compile: "); return ECEX_OK; } -static int cmd_grep(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_GREP, "Grep: "); return ECEX_OK; } -static int cmd_recompile(ecex_t *ed) { return ecex_rerun_compile(ed); } -static int cmd_regrep(ecex_t *ed) { return ecex_rerun_grep(ed); } -static int cmd_next_error(ecex_t *ed) { return ecex_next_interactive_action(ed); } -static int cmd_previous_error(ecex_t *ed) { return ecex_previous_interactive_action(ed); } -static int cmd_comment_region(ecex_t *ed) { return ecex_comment_region(ed); } -static int cmd_uncomment_region(ecex_t *ed) { return ecex_uncomment_region(ed); } -static int cmd_toggle_line_numbers(ecex_t *ed) { if (!ed) return ECEX_ERR; ed->theme.line_numbers_enabled = !ed->theme.line_numbers_enabled; return ECEX_OK; } -static int cmd_toggle_current_line(ecex_t *ed) { if (!ed) return ECEX_ERR; ed->theme.current_line_enabled = !ed->theme.current_line_enabled; return ECEX_OK; } -static int cmd_isearch_forward(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_ISEARCH_FORWARD, "I-search: "); return ECEX_OK; } -static int cmd_isearch_backward(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_ISEARCH_BACKWARD, "I-search backward: "); return ECEX_OK; } +static int ecex_file_browser_close_preview_pane(ecex_t *ed) { + if (!ed || ed->window_count <= 1) return ECEX_OK; + buffer_t *preview = ecex_find_buffer(ed, "*file-preview*"); + if (!preview) return ECEX_OK; -static int cmd_find_file(ecex_t *ed) { - ecex_request_prompt(ed, ECEX_PROMPT_FIND_FILE, "Find file: "); + for (size_t i = 0; i < ed->window_count; i++) { + if (ed->windows[i].buffer != preview) continue; + memmove(&ed->windows[i], &ed->windows[i + 1], (ed->window_count - i - 1) * sizeof(ed->windows[0])); + ed->window_count--; + if (ed->current_window_index >= ed->window_count) ed->current_window_index = ed->window_count - 1; + ecex_balance_windows(ed); + return ecex_sync_current_buffer(ed); + } return ECEX_OK; } -static int cmd_write_file(ecex_t *ed) { - ecex_request_prompt(ed, ECEX_PROMPT_WRITE_FILE, "Write file: "); +static int ecex_file_browser_fill_preview(ecex_t *ed, buffer_t *preview, const char *path) { + if (!ed || !preview || !path) return ECEX_ERR; + + if (ecex_path_is_media(path)) { + return ecex_media_load_into_buffer(ed, path, preview); + } + + ecex_media_buffer_clear(preview); + ecex_buffer_set_major_mode_by_name(ed, preview, "special-mode"); + preview->read_only = 0; + buffer_clear(preview); + char line[8192]; + snprintf(line, sizeof(line), "Preview: %s\n\n", path); + buffer_append(preview, line); + + if (ecex_path_is_dir(path)) { + buffer_append(preview, "Directory. Press RET/l in *file-browser* to open it.\n"); + } else if (ecex_path_is_file(path)) { + long long size = ecex_path_file_size(path); + snprintf(line, sizeof(line), "File size: %lld bytes.\n\n", size); + buffer_append(preview, line); + FILE *f = fopen(path, "rb"); + if (f) { + char chunk[8193]; + size_t n = fread(chunk, 1, sizeof(chunk) - 1, f); + fclose(f); + chunk[n] = '\0'; + int binary = 0; + for (size_t i = 0; i < n; i++) { + unsigned char c = (unsigned char)chunk[i]; + if (c == 0 || (c < 8) || (c > 13 && c < 32)) { binary = 1; break; } + } + if (binary) buffer_append(preview, "Binary file; text preview suppressed.\n"); + else buffer_append(preview, chunk); + } + } + + preview->modified = 0; + preview->read_only = 1; return ECEX_OK; } -static int cmd_save_buffer(ecex_t *ed) { - if (!ed) return ECEX_ERR; - - buffer_t *buf = ecex_current_buffer(ed); - if (!buf) return ECEX_ERR; +static int ecex_file_browser_preview_current(ecex_t *ed) { + char path[4096]; + if (ecex_file_browser_current_payload(ed, path, sizeof(path)) != ECEX_OK) return ECEX_ERR; - if (!buf->path) { - ecex_request_prompt(ed, ECEX_PROMPT_WRITE_FILE, "Write file: "); - return ECEX_OK; - } + buffer_t *preview = ecex_file_preview_buffer(ed); + if (!preview) return ECEX_ERR; - return ecex_save_current_buffer(ed); + int result = ecex_file_browser_fill_preview(ed, preview, path); + ecex_file_browser_show_preview_pane(ed, preview); + return result; } -static int cmd_eval_buffer(ecex_t *ed) { - return ecex_eval_current_buffer(ed); +static int ecex_file_browser_update_preview_if_enabled(ecex_t *ed) { + if (!ed || !ecex_fb_preview_expanded) return ECEX_OK; + buffer_t *buf = ecex_current_buffer(ed); + const char *mode = buf ? ecex_buffer_major_mode_name(ed, buf) : NULL; + if (!mode || strcmp(mode, "file-browser-mode") != 0) return ECEX_OK; + return ecex_file_browser_preview_current(ed); } -static int cmd_eval_line(ecex_t *ed) { - return ecex_eval_current_line(ed); +static int cmd_file_browser(ecex_t *ed) { + char cwd[4096]; + ecex_path_cwd(cwd, sizeof(cwd)); + return ecex_file_browser_populate(ed, cwd, 1); } -static int cmd_eval_region(ecex_t *ed) { - return ecex_eval_current_region(ed); +static int cmd_file_browser_here(ecex_t *ed) { + buffer_t *buf = ecex_current_buffer(ed); + if (buf && buf->path && buf->path[0]) { + char *dir = ecex_path_dirname(buf->path); + if (!dir) return ECEX_ERR; + int result = ecex_file_browser_populate(ed, dir, 1); + free(dir); + return result; + } + return cmd_file_browser(ed); } -static int cmd_eval_file(ecex_t *ed) { - ecex_request_prompt(ed, ECEX_PROMPT_EVAL_FILE, "Eval file: "); - return ECEX_OK; +static int cmd_file_browser_refresh(ecex_t *ed) { return ecex_file_browser_populate(ed, ecex_fb_cwd, 0); } + +static int cmd_file_browser_parent(ecex_t *ed) { + char *parent = ecex_path_dirname(ecex_fb_cwd); + if (!parent) return ECEX_ERR; + int result = ecex_file_browser_populate(ed, parent, 1); + free(parent); + return result; } -static int cmd_eval_rerun_last(ecex_t *ed) { - return ecex_eval_rerun_last(ed); +static int cmd_file_browser_open(ecex_t *ed) { return ecex_interactive_activate_current_line(ed); } +static int cmd_file_browser_preview(ecex_t *ed) { return ecex_file_browser_preview_current(ed); } + +static int cmd_file_browser_toggle_preview(ecex_t *ed) { + ecex_fb_preview_expanded = !ecex_fb_preview_expanded; + if (!ecex_fb_preview_expanded) { + ecex_file_browser_close_preview_pane(ed); + return ecex_file_browser_populate(ed, ecex_fb_cwd, 0); + } + int result = ecex_file_browser_populate(ed, ecex_fb_cwd, 0); + if (result == ECEX_OK) ecex_file_browser_preview_current(ed); + return result; } -static int cmd_quit_window(ecex_t *ed) { - if (!ed) return ECEX_ERR; - if (ecex_window_count(ed) > 1) return ecex_delete_window(ed); - return ecex_previous_buffer(ed); +static int cmd_file_browser_history_back(ecex_t *ed) { + if (ecex_fb_history_count == 0 || ecex_fb_history_index == 0) return ECEX_ERR; + ecex_fb_history_index--; + return ecex_file_browser_populate(ed, ecex_fb_history[ecex_fb_history_index], 0); } -static int cmd_reload_config(ecex_t *ed) { - return ecex_reload_config(ed); +static int cmd_file_browser_history_forward(ecex_t *ed) { + if (ecex_fb_history_count == 0 || ecex_fb_history_index + 1 >= ecex_fb_history_count) return ECEX_ERR; + ecex_fb_history_index++; + return ecex_file_browser_populate(ed, ecex_fb_history[ecex_fb_history_index], 0); } +static int cmd_media_play_pause(ecex_t *ed) { return ecex_media_toggle_playback(ed); } + static int ecex_register_builtins(ecex_t *ed) { ECEX_COMMAND("quit", cmd_quit); + ECEX_COMMAND("force-quit", cmd_force_quit); ECEX_COMMAND("find-file", cmd_find_file); + ECEX_COMMAND("file-browser", cmd_file_browser); + ECEX_COMMAND("file-browser-here", cmd_file_browser_here); + ECEX_COMMAND("file-browser-refresh", cmd_file_browser_refresh); + ECEX_COMMAND("file-browser-parent", cmd_file_browser_parent); + ECEX_COMMAND("file-browser-open", cmd_file_browser_open); + ECEX_COMMAND("file-browser-preview", cmd_file_browser_preview); + ECEX_COMMAND("file-browser-toggle-preview", cmd_file_browser_toggle_preview); + ECEX_COMMAND("file-browser-history-back", cmd_file_browser_history_back); + ECEX_COMMAND("file-browser-history-forward", cmd_file_browser_history_forward); + ECEX_COMMAND("media-play-pause", cmd_media_play_pause); ECEX_COMMAND("save-buffer", cmd_save_buffer); ECEX_COMMAND("write-file", cmd_write_file); ECEX_COMMAND("eval-buffer", cmd_eval_buffer); @@ -245,6 +1296,7 @@ static int ecex_register_builtins(ecex_t *ed) { ECEX_COMMAND("list-buffers", cmd_list_buffers); ECEX_COMMAND("switch-buffer", cmd_switch_buffer); ECEX_COMMAND("kill-buffer", cmd_kill_buffer_command); + ECEX_COMMAND("force-kill-buffer", cmd_force_kill_buffer_command); ECEX_COMMAND("compile", cmd_compile); ECEX_COMMAND("recompile", cmd_recompile); ECEX_COMMAND("grep", cmd_grep); @@ -320,11 +1372,15 @@ static int ecex_register_builtins(ecex_t *ed) { ECEX_BIND("C-S-z", "redo"); ECEX_BIND("C-x C-f", "find-file"); + ECEX_BIND("C-x f", "find-file"); + ECEX_BIND("C-x d", "file-browser"); + ECEX_BIND("C-x C-d", "file-browser-here"); ECEX_BIND("C-x C-s", "save-buffer"); ECEX_BIND("C-x C-w", "write-file"); ECEX_BIND("C-x C-b", "list-buffers"); ECEX_BIND("C-x b", "switch-buffer"); ECEX_BIND("C-x k", "kill-buffer"); + ECEX_BIND("C-x K", "force-kill-buffer"); ECEX_BIND("C-x 2", "split-window-below"); ECEX_BIND("C-x 3", "split-window-right"); ECEX_BIND("C-x o", "other-window"); @@ -351,10 +1407,26 @@ static int ecex_register_builtins(ecex_t *ed) { ecex_define_major_mode(ed, "c-mode"); ecex_define_major_mode(ed, "eval-output-mode"); ecex_define_major_mode(ed, "special-mode"); + ecex_define_major_mode(ed, "file-browser-mode"); + ecex_define_major_mode(ed, "media-preview-mode"); + ecex_define_major_mode(ed, "markdown-mode"); ecex_bind_mode_key(ed, "eval-output-mode", "g", "eval-rerun-last"); ecex_bind_mode_key(ed, "eval-output-mode", "q", "quit-window"); ecex_bind_mode_key(ed, "eval-output-mode", "r", "eval-rerun-last"); ecex_bind_mode_key(ed, "special-mode", "q", "quit-window"); + + ecex_bind_mode_key(ed, "file-browser-mode", "g", "file-browser-refresh"); + ecex_bind_mode_key(ed, "file-browser-mode", "r", "file-browser-refresh"); + ecex_bind_mode_key(ed, "file-browser-mode", "h", "file-browser-parent"); + ecex_bind_mode_key(ed, "file-browser-mode", "l", "file-browser-open"); + ecex_bind_mode_key(ed, "file-browser-mode", "v", "file-browser-preview"); + ecex_bind_mode_key(ed, "file-browser-mode", "m", "file-browser-toggle-preview"); + ecex_bind_mode_key(ed, "file-browser-mode", "b", "file-browser-history-back"); + ecex_bind_mode_key(ed, "file-browser-mode", "f", "file-browser-history-forward"); + ecex_bind_mode_key(ed, "file-browser-mode", "q", "quit-window"); + ecex_bind_mode_key(ed, "media-preview-mode", "SPC", "media-play-pause"); + ecex_bind_mode_key(ed, "media-preview-mode", "p", "media-play-pause"); + ecex_bind_mode_key(ed, "media-preview-mode", "q", "quit-window"); ecex_bind_mode_key(ed, "special-mode", "g", "recompile"); ecex_bind_mode_key(ed, "special-mode", "n", "next-error"); ecex_bind_mode_key(ed, "special-mode", "p", "previous-error"); @@ -410,14 +1482,18 @@ ecex_t *ecex_new(void) { void ecex_free(ecex_t *ed) { if (!ed) return; - for (size_t i = 0; i < ed->jit_module_count; i++) { - ccdjit_module_free((ccdjit_module *)ed->jit_modules[i]); - } - + /* Buffers may hold renderer/animation callbacks and userdata destructors + * compiled by CCDJIT config modules. Run those destructors while their JIT + * modules are still alive; freeing modules first leaves callback pointers + * dangling and can segfault during buffer teardown. */ for (size_t i = 0; i < ed->buffer_count; i++) { buffer_free(ed->buffers[i]); } + for (size_t i = 0; i < ed->jit_module_count; i++) { + ccdjit_module_free((ccdjit_module *)ed->jit_modules[i]); + } + for (size_t i = 0; i < ed->command_count; i++) { free(ed->commands[i].name); } @@ -436,6 +1512,22 @@ void ecex_free(ecex_t *ed) { free(ed->major_modes[i].name); } + for (size_t i = 0; i < ed->var_count; i++) { + ecex_var_entry_clear(&ed->vars[i]); + } + + for (size_t i = 0; i < ed->text_count; i++) { + ecex_text_entry_clear(&ed->texts[i]); + } + + for (size_t i = 0; i < ed->file_handler_count; i++) { + free(ed->file_handlers[i].extension); + } + + for (size_t i = 0; i < ed->object_count; i++) { + free(ed->objects[i].ptr); + } + free(ed->jit_modules); free(ed->windows); free(ed->buffers); @@ -443,6 +1535,10 @@ void ecex_free(ecex_t *ed) { free(ed->keybinds); free(ed->mode_keybinds); free(ed->major_modes); + free(ed->vars); + free(ed->texts); + free(ed->file_handlers); + free(ed->objects); free(ed->last_eval_source); free(ed->last_eval_filename); free(ed->last_compile_command); @@ -513,10 +1609,27 @@ buffer_t *ecex_find_buffer(ecex_t *ed, const char *name) { } +static int ecex_buffer_index_of(ecex_t *ed, buffer_t *buffer, size_t *out_index) { + if (!ed || !buffer) return ECEX_ERR; + for (size_t i = 0; i < ed->buffer_count; i++) { + if (ed->buffers[i] == buffer) { + if (out_index) *out_index = i; + return ECEX_OK; + } + } + return ECEX_ERR; +} + static int ecex_set_current_buffer_index(ecex_t *ed, size_t index) { if (!ed || index >= ed->buffer_count) return ECEX_ERR; + + buffer_t *next = ed->buffers[index]; + if (ed->current_buffer && ed->current_buffer != next) { + ed->previous_buffer = ed->current_buffer; + } + ed->current_buffer_index = index; - ed->current_buffer = ed->buffers[index]; + ed->current_buffer = next; ecex_window_t *win = ecex_current_window(ed); if (win) win->buffer = ed->current_buffer; return ECEX_OK; @@ -527,9 +1640,14 @@ int ecex_sync_current_buffer(ecex_t *ed) { ecex_window_t *win = ecex_current_window(ed); if (!win || !win->buffer) return ECEX_ERR; - ed->current_buffer = win->buffer; + buffer_t *next = win->buffer; + if (ed->current_buffer && ed->current_buffer != next) { + ed->previous_buffer = ed->current_buffer; + } + + ed->current_buffer = next; for (size_t i = 0; i < ed->buffer_count; i++) { - if (ed->buffers[i] == win->buffer) { + if (ed->buffers[i] == next) { ed->current_buffer_index = i; return ECEX_OK; } @@ -538,7 +1656,14 @@ int ecex_sync_current_buffer(ecex_t *ed) { } int ecex_switch_buffer(ecex_t *ed, const char *name) { - if (!ed || !name) return ECEX_ERR; + if (!ed) return ECEX_ERR; + + if (!name || name[0] == '\0') { + buffer_t *other = ecex_other_buffer(ed); + size_t index = 0; + if (!other || ecex_buffer_index_of(ed, other, &index) != ECEX_OK) return ECEX_ERR; + return ecex_set_current_buffer_index(ed, index); + } for (size_t i = 0; i < ed->buffer_count; i++) { if (strcmp(ed->buffers[i]->name, name) == 0) { @@ -556,6 +1681,22 @@ buffer_t *ecex_current_buffer(ecex_t *ed) { return ed->current_buffer; } +buffer_t *ecex_other_buffer(ecex_t *ed) { + if (!ed || ed->buffer_count == 0) return NULL; + + buffer_t *current = ecex_current_buffer(ed); + if (ed->previous_buffer && ed->previous_buffer != current && + ecex_buffer_index_of(ed, ed->previous_buffer, NULL) == ECEX_OK) { + return ed->previous_buffer; + } + + for (size_t i = 0; i < ed->buffer_count; i++) { + if (ed->buffers[i] && ed->buffers[i] != current) return ed->buffers[i]; + } + + return current; +} + ecex_window_t *ecex_current_window(ecex_t *ed) { if (!ed || ed->window_count == 0 || ed->current_window_index >= ed->window_count) return NULL; return &ed->windows[ed->current_window_index]; @@ -688,32 +1829,40 @@ int ecex_previous_buffer(ecex_t *ed) { return ecex_set_current_buffer_index(ed, ed->current_buffer_index); } -int ecex_kill_buffer(ecex_t *ed, const char *name) { - if (!ed || !name || ed->buffer_count == 0) return ECEX_ERR; +static int ecex_kill_buffer_impl(ecex_t *ed, const char *name, int force) { + if (!ed || ed->buffer_count == 0) return ECEX_ERR; size_t index = ed->buffer_count; - for (size_t i = 0; i < ed->buffer_count; i++) { - if (strcmp(ed->buffers[i]->name, name) == 0) { - index = i; - break; + if (!name || name[0] == '\0') { + buffer_t *current = ecex_current_buffer(ed); + if (!current || ecex_buffer_index_of(ed, current, &index) != ECEX_OK) return ECEX_ERR; + } else { + for (size_t i = 0; i < ed->buffer_count; i++) { + if (strcmp(ed->buffers[i]->name, name) == 0) { + index = i; + break; + } } } if (index == ed->buffer_count) return ECEX_ERR; buffer_t *victim = ed->buffers[index]; - - for (size_t i = index; i + 1 < ed->buffer_count; i++) { - ed->buffers[i] = ed->buffers[i + 1]; + if (ed->previous_buffer == victim) ed->previous_buffer = NULL; + if (!force && victim->modified && !victim->read_only) { + fprintf(stderr, + "ecex: refusing to kill modified buffer '%s'; save it or use force-kill-buffer.\n", + victim->name ? victim->name : ""); + return ECEX_ERR; } + for (size_t i = index; i + 1 < ed->buffer_count; i++) ed->buffers[i] = ed->buffers[i + 1]; ed->buffer_count--; - buffer_t *fallback = ed->buffer_count > 0 ? ed->buffers[0] : NULL; + buffer_t *fallback = ecex_other_buffer(ed); + if (fallback == victim) fallback = ed->buffer_count > 0 ? ed->buffers[0] : NULL; for (size_t i = 0; i < ed->window_count; i++) { - if (ed->windows[i].buffer == victim) { - ed->windows[i].buffer = fallback; - } + if (ed->windows[i].buffer == victim) ed->windows[i].buffer = fallback; } buffer_free(victim); @@ -733,6 +1882,23 @@ int ecex_kill_buffer(ecex_t *ed, const char *name) { return ecex_sync_current_buffer(ed); } +int ecex_kill_buffer(ecex_t *ed, const char *name) { + return ecex_kill_buffer_impl(ed, name, 0); +} + +int ecex_kill_buffer_force(ecex_t *ed, const char *name) { + return ecex_kill_buffer_impl(ed, name, 1); +} + +int ecex_has_modified_buffers(ecex_t *ed) { + if (!ed) return 0; + for (size_t i = 0; i < ed->buffer_count; i++) { + buffer_t *buf = ed->buffers[i]; + if (buf && buf->modified && !buf->read_only) return 1; + } + return 0; +} + int ecex_keep_jit_module(ecex_t *ed, void *module) { if (!ed || !module) return ECEX_ERR; @@ -795,6 +1961,129 @@ static void ecex_clear_mode_keybinds(ecex_t *ed) { ed->mode_keybind_count = 0; } +typedef struct ecex_binding_snapshot { + ecex_command_t *commands; + size_t command_count; + size_t command_cap; + ecex_keybind_t *keybinds; + size_t keybind_count; + size_t keybind_cap; + ecex_mode_keybind_t *mode_keybinds; + size_t mode_keybind_count; + size_t mode_keybind_cap; + ecex_theme_t theme; +} ecex_binding_snapshot_t; + +static void ecex_theme_snapshot_free(ecex_theme_t *theme) { + if (!theme) return; + free(theme->font_path); + theme->font_path = NULL; +} + +static int ecex_theme_clone(ecex_theme_t *out, const ecex_theme_t *in) { + if (!out || !in) return ECEX_ERR; + *out = *in; + out->font_path = NULL; + if (in->font_path) { + out->font_path = ecex_strdup(in->font_path); + if (!out->font_path) return ECEX_ERR; + } + return ECEX_OK; +} + +static void ecex_snapshot_free(ecex_binding_snapshot_t *snap) { + if (!snap) return; + for (size_t i = 0; i < snap->command_count; i++) free(snap->commands[i].name); + for (size_t i = 0; i < snap->keybind_count; i++) { + free(snap->keybinds[i].key); + free(snap->keybinds[i].command); + } + for (size_t i = 0; i < snap->mode_keybind_count; i++) { + free(snap->mode_keybinds[i].key); + free(snap->mode_keybinds[i].command); + } + free(snap->commands); + free(snap->keybinds); + free(snap->mode_keybinds); + ecex_theme_snapshot_free(&snap->theme); + memset(snap, 0, sizeof(*snap)); +} + +static int ecex_snapshot_clone(ecex_binding_snapshot_t *snap, ecex_t *ed) { + if (!snap || !ed) return ECEX_ERR; + memset(snap, 0, sizeof(*snap)); + + if (ecex_theme_clone(&snap->theme, &ed->theme) != ECEX_OK) return ECEX_ERR; + + if (ed->command_cap) { + snap->commands = calloc(ed->command_cap, sizeof(*snap->commands)); + if (!snap->commands) goto fail; + snap->command_cap = ed->command_cap; + snap->command_count = ed->command_count; + for (size_t i = 0; i < ed->command_count; i++) { + snap->commands[i].fn = ed->commands[i].fn; + snap->commands[i].name = ecex_strdup(ed->commands[i].name); + if (!snap->commands[i].name) goto fail; + } + } + + if (ed->keybind_cap) { + snap->keybinds = calloc(ed->keybind_cap, sizeof(*snap->keybinds)); + if (!snap->keybinds) goto fail; + snap->keybind_cap = ed->keybind_cap; + snap->keybind_count = ed->keybind_count; + for (size_t i = 0; i < ed->keybind_count; i++) { + snap->keybinds[i].key = ecex_strdup(ed->keybinds[i].key); + snap->keybinds[i].command = ecex_strdup(ed->keybinds[i].command); + if (!snap->keybinds[i].key || !snap->keybinds[i].command) goto fail; + } + } + + if (ed->mode_keybind_cap) { + snap->mode_keybinds = calloc(ed->mode_keybind_cap, sizeof(*snap->mode_keybinds)); + if (!snap->mode_keybinds) goto fail; + snap->mode_keybind_cap = ed->mode_keybind_cap; + snap->mode_keybind_count = ed->mode_keybind_count; + for (size_t i = 0; i < ed->mode_keybind_count; i++) { + snap->mode_keybinds[i].mode = ed->mode_keybinds[i].mode; + snap->mode_keybinds[i].key = ecex_strdup(ed->mode_keybinds[i].key); + snap->mode_keybinds[i].command = ecex_strdup(ed->mode_keybinds[i].command); + if (!snap->mode_keybinds[i].key || !snap->mode_keybinds[i].command) goto fail; + } + } + + return ECEX_OK; + +fail: + ecex_snapshot_free(snap); + return ECEX_ERR; +} + +static void ecex_restore_snapshot(ecex_t *ed, ecex_binding_snapshot_t *snap) { + if (!ed || !snap) return; + + ecex_clear_commands(ed); + ecex_clear_keybinds(ed); + ecex_clear_mode_keybinds(ed); + free(ed->commands); + free(ed->keybinds); + free(ed->mode_keybinds); + free(ed->theme.font_path); + + ed->commands = snap->commands; + ed->command_count = snap->command_count; + ed->command_cap = snap->command_cap; + ed->keybinds = snap->keybinds; + ed->keybind_count = snap->keybind_count; + ed->keybind_cap = snap->keybind_cap; + ed->mode_keybinds = snap->mode_keybinds; + ed->mode_keybind_count = snap->mode_keybind_count; + ed->mode_keybind_cap = snap->mode_keybind_cap; + ed->theme = snap->theme; + + memset(snap, 0, sizeof(*snap)); +} + int ecex_reload_config(ecex_t *ed) { if (!ed || !ed->config_path || !ed->config_path[0]) { fprintf(stderr, "ecex: no config file to reload; start with --config path/to/ecexrc.c\n"); @@ -804,25 +2093,89 @@ int ecex_reload_config(ecex_t *ed) { char *path = ecex_strdup(ed->config_path); if (!path) return ECEX_ERR; + ecex_binding_snapshot_t snapshot; + if (ecex_snapshot_clone(&snapshot, ed) != ECEX_OK) { + free(path); + fprintf(stderr, "ecex: failed to snapshot config state before reload\n"); + return ECEX_ERR; + } + ecex_clear_commands(ed); ecex_clear_keybinds(ed); ecex_clear_mode_keybinds(ed); - if (ecex_register_builtins(ed) != ECEX_OK) { - free(path); - return ECEX_ERR; + int result = ECEX_ERR; + if (ecex_register_builtins(ed) == ECEX_OK) { + result = ecex_load_c_config(ed, path); + if (result == ECEX_OK) result = ecex_validate_bindings(ed); + } + + if (result != ECEX_OK) { + fprintf(stderr, "ecex: config reload failed; keeping previous config active\n"); + ecex_restore_snapshot(ed, &snapshot); + } else { + ecex_snapshot_free(&snapshot); } - int result = ecex_load_c_config(ed, path); free(path); return result; } + +int ecex_config_register_commands(ecex_t *ed, const ecex_config_command_t *commands, size_t count) { + if (!ed || (!commands && count != 0)) return ECEX_ERR; + for (size_t i = 0; i < count; i++) { + if (!commands[i].name || !commands[i].fn) return ECEX_ERR; + if (ecex_register_command(ed, commands[i].name, commands[i].fn) != ECEX_OK) return ECEX_ERR; + } + return ECEX_OK; +} + +int ecex_config_bind_keys(ecex_t *ed, const ecex_config_keybind_t *bindings, size_t count) { + if (!ed || (!bindings && count != 0)) return ECEX_ERR; + for (size_t i = 0; i < count; i++) { + if (!bindings[i].key || !bindings[i].command) return ECEX_ERR; + if (ecex_bind_key(ed, bindings[i].key, bindings[i].command) != ECEX_OK) return ECEX_ERR; + } + return ECEX_OK; +} + +int ecex_config_bind_mode_keys(ecex_t *ed, const ecex_config_mode_keybind_t *bindings, size_t count) { + if (!ed || (!bindings && count != 0)) return ECEX_ERR; + for (size_t i = 0; i < count; i++) { + if (!bindings[i].mode || !bindings[i].key || !bindings[i].command) return ECEX_ERR; + if (ecex_bind_mode_key(ed, bindings[i].mode, bindings[i].key, bindings[i].command) != ECEX_OK) return ECEX_ERR; + } + return ECEX_OK; +} + +int ecex_config_define_modes(ecex_t *ed, const char *const *modes, size_t count) { + if (!ed || (!modes && count != 0)) return ECEX_ERR; + for (size_t i = 0; i < count; i++) { + if (!modes[i] || !ecex_define_major_mode(ed, modes[i])) return ECEX_ERR; + } + return ECEX_OK; +} + +int ecex_apply_theme(ecex_t *ed, const ecex_theme_t *theme) { + if (!ed || !theme) return ECEX_ERR; + char *font = NULL; + if (theme->font_path) { + font = ecex_strdup(theme->font_path); + if (!font) return ECEX_ERR; + } + free(ed->theme.font_path); + ed->theme = *theme; + ed->theme.font_path = font; + return ECEX_OK; +} + int ecex_register_command(ecex_t *ed, const char *name, ecex_command_fn fn) { if (!ed || !name || !fn) return ECEX_ERR; for (size_t i = 0; i < ed->command_count; i++) { if (strcmp(ed->commands[i].name, name) == 0) { + fprintf(stderr, "ecex: command warning: replacing existing command '%s'\n", name); ed->commands[i].fn = fn; return ECEX_OK; } @@ -877,11 +2230,66 @@ int ecex_clipboard_set(ecex_t *ed, const char *text) { return ECEX_OK; } +static int ecex_command_exists(ecex_t *ed, const char *name) { + if (!ed || !name) return 0; + for (size_t i = 0; i < ed->command_count; i++) { + if (ed->commands[i].name && strcmp(ed->commands[i].name, name) == 0) return 1; + } + return 0; +} + +static int ecex_key_is_prefix_of(const char *prefix, const char *key) { + if (!prefix || !key) return 0; + size_t n = strlen(prefix); + return strncmp(prefix, key, n) == 0 && key[n] == ' '; +} + +static void ecex_warn_keybind_issue(const char *scope, const char *key, const char *detail) { + if (scope && scope[0]) { + fprintf(stderr, + "ecex: keybind warning [%s]: %s%s%s\n", + scope, + key ? key : "", + detail && detail[0] ? " " : "", + detail ? detail : ""); + } else { + fprintf(stderr, + "ecex: keybind warning: %s%s%s\n", + key ? key : "", + detail && detail[0] ? " " : "", + detail ? detail : ""); + } +} + +static void ecex_warn_keybind_conflicts(ecex_t *ed, const char *scope, const char *key, int mode) { + if (!ed || !key) return; + + for (size_t i = 0; i < ed->keybind_count; i++) { + if (mode != 0) break; + const char *existing = ed->keybinds[i].key; + if (existing && ecex_key_is_prefix_of(key, existing)) ecex_warn_keybind_issue(scope, key, "is a prefix of an existing binding"); + if (existing && ecex_key_is_prefix_of(existing, key)) ecex_warn_keybind_issue(scope, key, "extends an existing complete binding"); + } + + for (size_t i = 0; i < ed->mode_keybind_count; i++) { + if (mode != ed->mode_keybinds[i].mode) continue; + const char *existing = ed->mode_keybinds[i].key; + if (existing && ecex_key_is_prefix_of(key, existing)) ecex_warn_keybind_issue(scope, key, "is a prefix of an existing mode binding"); + if (existing && ecex_key_is_prefix_of(existing, key)) ecex_warn_keybind_issue(scope, key, "extends an existing complete mode binding"); + } +} + int ecex_bind_key(ecex_t *ed, const char *key, const char *command) { if (!ed || !key || !command) return ECEX_ERR; + if (!ecex_command_exists(ed, command)) { + ecex_warn_keybind_issue("global", key, "targets a command that is not registered yet"); + } + ecex_warn_keybind_conflicts(ed, "global", key, 0); + for (size_t i = 0; i < ed->keybind_count; i++) { if (strcmp(ed->keybinds[i].key, key) == 0) { + fprintf(stderr, "ecex: keybind warning [global]: replacing %s from %s to %s\n", key, ed->keybinds[i].command, command); char *new_command = ecex_strdup(command); if (!new_command) return ECEX_ERR; @@ -990,8 +2398,14 @@ int ecex_bind_mode_key(ecex_t *ed, const char *mode_name, const char *key, const int mode = ecex_define_major_mode(ed, mode_name); if (!mode) return ECEX_ERR; + if (!ecex_command_exists(ed, command)) { + ecex_warn_keybind_issue(mode_name, key, "targets a command that is not registered yet"); + } + ecex_warn_keybind_conflicts(ed, mode_name, key, mode); + for (size_t i = 0; i < ed->mode_keybind_count; i++) { if (ed->mode_keybinds[i].mode == mode && strcmp(ed->mode_keybinds[i].key, key) == 0) { + fprintf(stderr, "ecex: keybind warning [%s]: replacing %s from %s to %s\n", mode_name, key, ed->mode_keybinds[i].command, command); char *new_command = ecex_strdup(command); if (!new_command) return ECEX_ERR; free(ed->mode_keybinds[i].command); @@ -1022,6 +2436,43 @@ int ecex_bind_mode_key(ecex_t *ed, const char *mode_name, const char *key, const return ECEX_OK; } +int ecex_validate_bindings(ecex_t *ed) { + if (!ed) return ECEX_ERR; + int ok = ECEX_OK; + + for (size_t i = 0; i < ed->keybind_count; i++) { + const char *command = ed->keybinds[i].command; + if (!ecex_command_exists(ed, command)) { + ecex_warn_keybind_issue("global", ed->keybinds[i].key, "targets a missing command"); + ok = ECEX_ERR; + } + for (size_t j = i + 1; j < ed->keybind_count; j++) { + if (ecex_key_is_prefix_of(ed->keybinds[i].key, ed->keybinds[j].key) || + ecex_key_is_prefix_of(ed->keybinds[j].key, ed->keybinds[i].key)) { + ecex_warn_keybind_issue("global", ed->keybinds[i].key, "has a prefix conflict"); + } + } + } + + for (size_t i = 0; i < ed->mode_keybind_count; i++) { + const char *command = ed->mode_keybinds[i].command; + const char *mode = ecex_major_mode_name(ed, ed->mode_keybinds[i].mode); + if (!ecex_command_exists(ed, command)) { + ecex_warn_keybind_issue(mode, ed->mode_keybinds[i].key, "targets a missing command"); + ok = ECEX_ERR; + } + for (size_t j = i + 1; j < ed->mode_keybind_count; j++) { + if (ed->mode_keybinds[i].mode != ed->mode_keybinds[j].mode) continue; + if (ecex_key_is_prefix_of(ed->mode_keybinds[i].key, ed->mode_keybinds[j].key) || + ecex_key_is_prefix_of(ed->mode_keybinds[j].key, ed->mode_keybinds[i].key)) { + ecex_warn_keybind_issue(mode, ed->mode_keybinds[i].key, "has a mode prefix conflict"); + } + } + } + + return ok; +} + const char *ecex_lookup_key_for_buffer(ecex_t *ed, buffer_t *buffer, const char *key) { if (!ed || !key) return NULL; @@ -1243,15 +2694,6 @@ int ecex_interactive_activate_current_line(ecex_t *ed) { return action->fn(ed, buffer, line, action->payload, action->userdata); } -static const char *ecex_basename(const char *path) { - if (!path || !path[0]) return "untitled"; - - const char *last_slash = strrchr(path, '/'); - if (last_slash && last_slash[1]) return last_slash + 1; - if (last_slash && last_slash == path) return "/"; - return path; -} - static int ecex_buffer_path_equal(buffer_t *buffer, const char *path) { return buffer && buffer->path && path && strcmp(buffer->path, path) == 0; } @@ -1259,35 +2701,55 @@ static int ecex_buffer_path_equal(buffer_t *buffer, const char *path) { int ecex_find_file(ecex_t *ed, const char *path) { if (!ed || !path || !path[0]) return ECEX_ERR; + char *normal_path = ecex_path_normalize(path); + if (!normal_path) return ECEX_ERR; + + if (ecex_path_is_dir(normal_path)) { + int result = ecex_file_browser_populate(ed, normal_path, 1); + free(normal_path); + return result; + } + + if (ecex_path_is_file(normal_path) && ecex_path_is_media(normal_path)) { + int result = ecex_media_open(ed, normal_path); + free(normal_path); + return result; + } + for (size_t i = 0; i < ed->buffer_count; i++) { - if (ecex_buffer_path_equal(ed->buffers[i], path)) { - return ecex_set_current_buffer_index(ed, i); + if (ecex_buffer_path_equal(ed->buffers[i], normal_path)) { + int result; + free(normal_path); + result = ecex_set_current_buffer_index(ed, i); + if (result == ECEX_OK && !ecex_buffer_has_renderer(ed->buffers[i])) ecex_run_file_handlers(ed, ed->buffers[i]); + return result; } } - const char *name = ecex_basename(path); + char *name = ecex_path_basename_dup(normal_path); + if (!name) { free(normal_path); return ECEX_ERR; } buffer_t *buf = ecex_create_buffer(ed, name, NULL, 0); - if (!buf) return ECEX_ERR; + free(name); + if (!buf) { free(normal_path); return ECEX_ERR; } - if (ecex_file_exists(path)) { - if (buffer_load_file(buf, path) != ECEX_OK) { - ecex_kill_buffer(ed, buf->name); + if (ecex_file_exists(normal_path)) { + if (buffer_load_file(buf, normal_path) != ECEX_OK) { + ecex_kill_buffer_force(ed, buf->name); + free(normal_path); return ECEX_ERR; } } else { - char *new_path = ecex_strdup(path); - if (!new_path) { - ecex_kill_buffer(ed, buf->name); - return ECEX_ERR; - } - free(buf->path); - buf->path = new_path; + buf->path = normal_path; + normal_path = NULL; buf->modified = 0; } + free(normal_path); ecex_auto_set_major_mode(ed, buf); - return ecex_set_current_buffer_index(ed, ed->buffer_count ? ed->buffer_count - 1 : 0); + int switch_result = ecex_set_current_buffer_index(ed, ed->buffer_count ? ed->buffer_count - 1 : 0); + if (switch_result == ECEX_OK) ecex_run_file_handlers(ed, buf); + return switch_result; } int ecex_save_current_buffer(ecex_t *ed) { @@ -1299,7 +2761,10 @@ int ecex_save_current_buffer(ecex_t *ed) { int ecex_write_current_buffer(ecex_t *ed, const char *path) { buffer_t *buf = ecex_current_buffer(ed); if (!buf || !path || !path[0]) return ECEX_ERR; - int result = buffer_save_as(buf, path); + char *normal_path = ecex_path_normalize(path); + if (!normal_path) return ECEX_ERR; + int result = buffer_save_as(buf, normal_path); + free(normal_path); if (result == ECEX_OK) ecex_auto_set_major_mode(ed, buf); return result; } diff --git a/src/eval.c b/src/eval.c index 316d6d8..e5f0e44 100644 --- a/src/eval.c +++ b/src/eval.c @@ -300,7 +300,7 @@ int ecex_eval_source(ecex_t *ed, buffer_set_interactive(out, 1); ecex_buffer_set_major_mode_by_name(ed, out, "eval-output-mode"); buffer_append(out, "Eval output (g: re-eval, q: quit window, ENTER: follow line if available)\n"); - buffer_append(out, "────────────────────────────────────────────────────────────────\n\n"); + buffer_append(out, "-------------------------------------------------------------------------\n\n"); buffer_append(out, "Eval: "); buffer_append(out, filename ? filename : ""); buffer_append(out, "\n\n"); diff --git a/src/main.c b/src/main.c index 9f388a0..94a08fa 100644 --- a/src/main.c +++ b/src/main.c @@ -1,6 +1,7 @@ #include "app.h" #include "config.h" #include "font.h" +#include "media.h" #include "render.h" #include @@ -145,13 +146,18 @@ int main(int argc, char **argv) { app_message(&app, "F1 for M-x. Tab completes commands."); while (!glfwWindowShouldClose(window) && !ed->should_quit) { + double now = glfwGetTime(); + if (ecex_media_tick(ed, now) || ecex_tick_animations(ed, now)) { + app.dirty = 1; + } + if (app.dirty) { render(&app); glfwSwapBuffers(window); app.dirty = 0; } - glfwWaitEvents(); + glfwWaitEventsTimeout(1.0 / 60.0); } font_free(&app.font); diff --git a/src/media.c b/src/media.c new file mode 100644 index 0000000..6c7a989 --- /dev/null +++ b/src/media.c @@ -0,0 +1,378 @@ +#include "media.h" + +#include "buffers.h" +#include "common.h" +#include "ecex.h" +#include "path.h" +#include "util.h" + +#include +#include +#include +#include +#include + +extern FILE *popen(const char *command, const char *type); +extern int pclose(FILE *stream); +extern int mkstemp(char *template); + +static char *shell_quote(const char *path) { + if (!path) return NULL; + size_t len = 2; /* quotes */ + for (const char *p = path; *p; p++) len += (*p == '\'') ? 4 : 1; + char *out = malloc(len + 1); + if (!out) return NULL; + char *w = out; + *w++ = '\''; + for (const char *p = path; *p; p++) { + if (*p == '\'') { + memcpy(w, "'\\''", 4); + w += 4; + } else { + *w++ = *p; + } + } + *w++ = '\''; + *w = '\0'; + return out; +} + +static int ppm_skip_ws_and_comments(FILE *f) { + int c = 0; + do { + c = fgetc(f); + if (c == '#') { + while (c != '\n' && c != EOF) c = fgetc(f); + } + } while (c != EOF && isspace((unsigned char)c)); + if (c == EOF) return EOF; + ungetc(c, f); + return 0; +} + +static int ppm_read_int(FILE *f, int *out) { + if (!f || !out) return ECEX_ERR; + if (ppm_skip_ws_and_comments(f) == EOF) return ECEX_ERR; + int c = fgetc(f); + if (c == EOF || !isdigit((unsigned char)c)) return ECEX_ERR; + int value = 0; + while (c != EOF && isdigit((unsigned char)c)) { + value = value * 10 + (c - '0'); + c = fgetc(f); + } + if (c != EOF) ungetc(c, f); + *out = value; + return ECEX_OK; +} + +static int read_ppm_frame(FILE *f, int *out_w, int *out_h, unsigned char **out_rgba) { + if (!f || !out_w || !out_h || !out_rgba) return ECEX_ERR; + int p = fgetc(f); + int six = fgetc(f); + if (p == EOF || six == EOF) return ECEX_ERR; + if (p != 'P' || six != '6') return ECEX_ERR; + + int w = 0, h = 0, maxv = 0; + if (ppm_read_int(f, &w) != ECEX_OK || ppm_read_int(f, &h) != ECEX_OK || ppm_read_int(f, &maxv) != ECEX_OK) { + return ECEX_ERR; + } + if (w <= 0 || h <= 0 || maxv <= 0 || maxv > 255) return ECEX_ERR; + + int c = fgetc(f); + if (c == EOF) return ECEX_ERR; + if (!isspace((unsigned char)c)) ungetc(c, f); + + size_t rgb_len = (size_t)w * (size_t)h * 3u; + size_t rgba_len = (size_t)w * (size_t)h * 4u; + if (w > 8192 || h > 8192 || rgb_len / 3u != (size_t)w * (size_t)h) return ECEX_ERR; + + unsigned char *rgb = malloc(rgb_len); + unsigned char *rgba = malloc(rgba_len); + if (!rgb || !rgba) { + free(rgb); + free(rgba); + return ECEX_ERR; + } + + size_t got = fread(rgb, 1, rgb_len, f); + if (got != rgb_len) { + free(rgb); + free(rgba); + return ECEX_ERR; + } + + for (size_t i = 0, j = 0; i < rgb_len; i += 3, j += 4) { + rgba[j + 0] = rgb[i + 0]; + rgba[j + 1] = rgb[i + 1]; + rgba[j + 2] = rgb[i + 2]; + rgba[j + 3] = 255; + } + + free(rgb); + *out_w = w; + *out_h = h; + *out_rgba = rgba; + return ECEX_OK; +} + +static int set_buffer_pixels(buffer_t *buffer, int w, int h, unsigned char *rgba) { + if (!buffer || w <= 0 || h <= 0 || !rgba) { + free(rgba); + return ECEX_ERR; + } + free(buffer->media_pixels); + buffer->media_pixels = rgba; + buffer->media_width = w; + buffer->media_height = h; + buffer->media_dirty = 1; + return ECEX_OK; +} + +static char *make_temp_log_path(void) { + char tmpl[] = "/tmp/ecex-ffmpeg-XXXXXX"; + int fd = mkstemp(tmpl); + if (fd < 0) return NULL; + close(fd); + return ecex_strdup(tmpl); +} + +static char *read_decode_log_status(const char *log_path, const char *fallback) { + FILE *f = log_path ? fopen(log_path, "rb") : NULL; + if (!f) return ecex_strdup(fallback ? fallback : "ffmpeg did not decode a preview frame."); + + char body[1400]; + size_t n = fread(body, 1, sizeof(body) - 1, f); + fclose(f); + body[n] = '\0'; + + if (n == 0) return ecex_strdup(fallback ? fallback : "ffmpeg did not decode a preview frame."); + + char *out = malloc(n + 128); + if (!out) return ecex_strdup(fallback ? fallback : "ffmpeg did not decode a preview frame."); + snprintf(out, n + 128, "ffmpeg did not decode a preview frame.\n\nffmpeg stderr:\n%s", body); + return out; +} + +static char *make_decode_command(const char *path, int video, char **out_log_path) { + char *q = shell_quote(path); + if (!q) return NULL; + char *log_path = make_temp_log_path(); + char *qlog = log_path ? shell_quote(log_path) : NULL; + const char *ffmpeg = getenv("ECEX_FFMPEG"); + if (!ffmpeg || !*ffmpeg) ffmpeg = "ffmpeg"; + /* + * Important: ffmpeg's filtergraph parser treats ',' as a filter + * separator unless it is escaped, even when the expression is inside + * quotes. The previous command used min(%d,iw), which makes many + * ffmpeg builds fail before producing a single PPM frame. Keep the + * comma escaped in the command string that reaches ffmpeg. + * + * -nostdin also prevents ffmpeg from accidentally consuming editor + * terminal input when ecex is launched from a shell. + */ + const char *fmt = video + ? "%s -nostdin -hide_banner -loglevel error -i %s -an -vf \"fps=60,scale='min(%d\\,iw)':-2\" -f image2pipe -vcodec ppm - %s%s" + : "%s -nostdin -hide_banner -loglevel error -i %s -frames:v 1 -vf \"scale='min(%d\\,iw)':-2\" -f image2pipe -vcodec ppm - %s%s"; + int need = snprintf(NULL, + 0, + fmt, + ffmpeg, + q, + ECEX_MEDIA_MAX_DIMENSION, + qlog ? "2>" : "", + qlog ? qlog : ""); + char *cmd = malloc((size_t)need + 1); + if (cmd) { + snprintf(cmd, + (size_t)need + 1, + fmt, + ffmpeg, + q, + ECEX_MEDIA_MAX_DIMENSION, + qlog ? "2>" : "", + qlog ? qlog : ""); + } + if (out_log_path) { + *out_log_path = log_path; + log_path = NULL; + } + free(qlog); + free(log_path); + free(q); + return cmd; +} + +static void buffer_set_media_text(buffer_t *buffer, const char *path, int video, const char *status) { + if (!buffer) return; + buffer->read_only = 0; + buffer_clear(buffer); + char text[2048]; + snprintf(text, + sizeof(text), + "%s preview: %s\n\n%s\n\nRequirements:\n install ffmpeg/ffprobe in PATH for broad image and video decoding.\n\nControls:\n Space / p play-pause video\n q quit window\n", + video ? "Video" : "Image", + path ? path : "(unknown)", + status ? status : "No decoded frame available."); + buffer_append(buffer, text); + buffer->modified = 0; + buffer->read_only = 1; +} + +void ecex_media_buffer_clear(buffer_t *buffer) { + if (!buffer) return; + if (buffer->media_pipe) { + pclose((FILE *)buffer->media_pipe); + buffer->media_pipe = NULL; + } + free(buffer->media_path); + buffer->media_path = NULL; + free(buffer->media_pixels); + buffer->media_pixels = NULL; + buffer->media_kind = ECEX_MEDIA_NONE; + buffer->media_width = 0; + buffer->media_height = 0; + buffer->media_dirty = 0; + buffer->media_texture_width = 0; + buffer->media_texture_height = 0; + buffer->media_last_frame_time = 0.0; + buffer->media_playing = 0; + buffer->media_status[0] = '\0'; +} + +int ecex_media_buffer_has_pixels(buffer_t *buffer) { + return buffer && buffer->media_pixels && buffer->media_width > 0 && buffer->media_height > 0; +} + +static buffer_t *media_buffer_for_path(ecex_t *ed, const char *path, int video) { + if (!ed || !path) return NULL; + char *base = ecex_path_basename_dup(path); + if (!base) return NULL; + char name[1024]; + snprintf(name, sizeof(name), "*%s-preview:%s*", video ? "video" : "image", base); + free(base); + + buffer_t *buffer = ecex_find_buffer(ed, name); + if (!buffer) buffer = ecex_create_buffer(ed, name, path, 1); + if (!buffer) return NULL; + return buffer; +} + +int ecex_media_load_into_buffer(ecex_t *ed, const char *path, buffer_t *buffer) { + if (!ed || !path || !buffer || !ecex_path_is_media(path)) return ECEX_ERR; + int video = ecex_path_is_video(path); + + ecex_media_buffer_clear(buffer); + buffer->media_kind = video ? ECEX_MEDIA_VIDEO : ECEX_MEDIA_IMAGE; + ecex_buffer_set_major_mode_by_name(ed, buffer, "media-preview-mode"); + buffer->media_path = ecex_path_normalize(path); + if (!buffer->media_path) buffer->media_path = ecex_strdup(path); + buffer->read_only = 0; + buffer_clear(buffer); + buffer_append(buffer, video ? "Loading video preview...\n" : "Loading image preview...\n"); + buffer->modified = 0; + buffer->read_only = 1; + + char *log_path = NULL; + char *cmd = make_decode_command(buffer->media_path ? buffer->media_path : path, video, &log_path); + if (!cmd) { + buffer_set_media_text(buffer, path, video, "Could not allocate ffmpeg command."); + free(log_path); + return ECEX_ERR; + } + + FILE *pipe = popen(cmd, "r"); + free(cmd); + if (!pipe) { + buffer_set_media_text(buffer, path, video, "Could not launch ffmpeg."); + if (log_path) unlink(log_path); + free(log_path); + return ECEX_ERR; + } + + int w = 0, h = 0; + unsigned char *rgba = NULL; + if (read_ppm_frame(pipe, &w, &h, &rgba) != ECEX_OK) { + pclose(pipe); + char *status = read_decode_log_status(log_path, "ffmpeg did not decode a preview frame."); + buffer_set_media_text(buffer, path, video, status); + free(status); + if (log_path) unlink(log_path); + free(log_path); + return ECEX_ERR; + } + + if (log_path) unlink(log_path); + free(log_path); + + if (video) { + buffer->media_pipe = pipe; + buffer->media_playing = 1; + snprintf(buffer->media_status, sizeof(buffer->media_status), "Playing at decoded 60fps stream"); + } else { + pclose(pipe); + snprintf(buffer->media_status, sizeof(buffer->media_status), "Image decoded via ffmpeg"); + } + + set_buffer_pixels(buffer, w, h, rgba); + buffer->read_only = 0; + buffer_clear(buffer); + char info[1024]; + snprintf(info, + sizeof(info), + "%s preview: %s\n%d x %d\n%s\n\n%s\n", + video ? "Video" : "Image", + buffer->media_path ? buffer->media_path : path, + w, + h, + buffer->media_status, + video ? "Space/p toggles playback. q quits window." : "q quits window."); + buffer_append(buffer, info); + buffer->modified = 0; + buffer->read_only = 1; + return ECEX_OK; +} + +int ecex_media_open(ecex_t *ed, const char *path) { + if (!ed || !path || !ecex_path_is_media(path)) return ECEX_ERR; + int video = ecex_path_is_video(path); + buffer_t *buffer = media_buffer_for_path(ed, path, video); + if (!buffer) return ECEX_ERR; + ecex_media_load_into_buffer(ed, path, buffer); + return ecex_switch_buffer(ed, buffer->name); +} + +int ecex_media_toggle_playback(ecex_t *ed) { + buffer_t *buffer = ecex_current_buffer(ed); + if (!buffer || buffer->media_kind != ECEX_MEDIA_VIDEO) return ECEX_ERR; + buffer->media_playing = !buffer->media_playing; + snprintf(buffer->media_status, + sizeof(buffer->media_status), + "%s", + buffer->media_playing ? "Playing" : "Paused"); + return ECEX_OK; +} + +int ecex_media_tick(ecex_t *ed, double now_seconds) { + if (!ed) return 0; + int dirty = 0; + for (size_t i = 0; i < ed->buffer_count; i++) { + buffer_t *buffer = ed->buffers[i]; + if (!buffer || buffer->media_kind != ECEX_MEDIA_VIDEO || !buffer->media_pipe || !buffer->media_playing) continue; + if (buffer->media_last_frame_time > 0.0 && now_seconds - buffer->media_last_frame_time < (1.0 / 60.0)) continue; + + int w = 0, h = 0; + unsigned char *rgba = NULL; + if (read_ppm_frame((FILE *)buffer->media_pipe, &w, &h, &rgba) == ECEX_OK) { + set_buffer_pixels(buffer, w, h, rgba); + buffer->media_last_frame_time = now_seconds; + dirty = 1; + } else { + pclose((FILE *)buffer->media_pipe); + buffer->media_pipe = NULL; + buffer->media_playing = 0; + snprintf(buffer->media_status, sizeof(buffer->media_status), "Playback finished"); + dirty = 1; + } + } + return dirty; +} diff --git a/src/path.c b/src/path.c new file mode 100644 index 0000000..1e002d7 --- /dev/null +++ b/src/path.c @@ -0,0 +1,208 @@ +#include "path.h" + +#include +#include +#include +#include +#include +#include + +extern char *realpath(const char *restrict path, char *restrict resolved_path); + +int ecex_path_copy(char *out, size_t out_size, const char *text) { + if (!out || out_size == 0) return -1; + if (!text) text = ""; + size_t len = strlen(text); + if (len >= out_size) { + memcpy(out, text, out_size - 1); + out[out_size - 1] = '\0'; + return -1; + } + memcpy(out, text, len + 1); + return 0; +} + +char *ecex_path_expand_user(const char *path) { + if (!path) return NULL; + if (path[0] != '~' || (path[1] != '\0' && path[1] != '/')) { + char *copy = malloc(strlen(path) + 1); + if (copy) strcpy(copy, path); + return copy; + } + + const char *home = getenv("HOME"); + if (!home || !home[0]) { + char *copy = malloc(strlen(path) + 1); + if (copy) strcpy(copy, path); + return copy; + } + + size_t home_len = strlen(home); + size_t rest_len = strlen(path + 1); + char *expanded = malloc(home_len + rest_len + 1); + if (!expanded) return NULL; + memcpy(expanded, home, home_len); + memcpy(expanded + home_len, path + 1, rest_len + 1); + return expanded; +} + +char *ecex_path_join(const char *dir, const char *name) { + if (!name) return NULL; + if (name[0] == '/') { + char *copy = malloc(strlen(name) + 1); + if (copy) strcpy(copy, name); + return copy; + } + if (!dir || !dir[0]) dir = "."; + + size_t dl = strlen(dir); + size_t nl = strlen(name); + int need_slash = dl > 0 && dir[dl - 1] != '/'; + char *out = malloc(dl + (size_t)need_slash + nl + 1); + if (!out) return NULL; + memcpy(out, dir, dl); + if (need_slash) out[dl++] = '/'; + memcpy(out + dl, name, nl + 1); + return out; +} + +char *ecex_path_dirname(const char *path) { + if (!path || !path[0]) { + char *dot = malloc(2); + if (dot) strcpy(dot, "."); + return dot; + } + + char *expanded = ecex_path_expand_user(path); + if (!expanded) return NULL; + + size_t len = strlen(expanded); + while (len > 1 && expanded[len - 1] == '/') expanded[--len] = '\0'; + + char *slash = strrchr(expanded, '/'); + if (!slash) { + strcpy(expanded, "."); + } else if (slash == expanded) { + expanded[1] = '\0'; + } else { + *slash = '\0'; + } + return expanded; +} + +char *ecex_path_basename_dup(const char *path) { + const char *base = "untitled"; + if (path && path[0]) { + const char *slash = strrchr(path, '/'); + if (slash && slash[1]) base = slash + 1; + else if (slash && slash == path) base = "/"; + else base = path; + } + char *out = malloc(strlen(base) + 1); + if (out) strcpy(out, base); + return out; +} + +char *ecex_path_normalize(const char *path) { + char *expanded = ecex_path_expand_user(path); + if (!expanded) return NULL; + + char *resolved = realpath(expanded, NULL); + if (resolved) { + free(expanded); + return resolved; + } + + if (expanded[0] == '/') return expanded; + + char cwd[4096]; + if (!getcwd(cwd, sizeof(cwd))) return expanded; + char *joined = ecex_path_join(cwd, expanded); + free(expanded); + return joined; +} + +int ecex_path_exists(const char *path) { + if (!path) return 0; + char *expanded = ecex_path_expand_user(path); + if (!expanded) return 0; + struct stat st; + int ok = stat(expanded, &st) == 0; + free(expanded); + return ok; +} + +int ecex_path_is_dir(const char *path) { + if (!path) return 0; + char *expanded = ecex_path_expand_user(path); + if (!expanded) return 0; + struct stat st; + int ok = stat(expanded, &st) == 0 && S_ISDIR(st.st_mode); + free(expanded); + return ok; +} + +int ecex_path_is_file(const char *path) { + if (!path) return 0; + char *expanded = ecex_path_expand_user(path); + if (!expanded) return 0; + struct stat st; + int ok = stat(expanded, &st) == 0 && S_ISREG(st.st_mode); + free(expanded); + return ok; +} + +long long ecex_path_file_size(const char *path) { + if (!path) return -1; + char *expanded = ecex_path_expand_user(path); + if (!expanded) return -1; + struct stat st; + long long size = (stat(expanded, &st) == 0 && S_ISREG(st.st_mode)) ? (long long)st.st_size : -1; + free(expanded); + return size; +} + +static int ext_eq(const char *path, const char *ext) { + if (!path || !ext) return 0; + size_t pl = strlen(path), el = strlen(ext); + if (pl < el) return 0; + const char *p = path + pl - el; + for (size_t i = 0; i < el; i++) { + if (tolower((unsigned char)p[i]) != tolower((unsigned char)ext[i])) return 0; + } + return 1; +} + +int ecex_path_is_image(const char *path) { + return ext_eq(path, ".png") || ext_eq(path, ".jpg") || ext_eq(path, ".jpeg") || + ext_eq(path, ".bmp") || ext_eq(path, ".ppm") || ext_eq(path, ".pnm") || + ext_eq(path, ".pgm") || ext_eq(path, ".gif") || ext_eq(path, ".webp") || + ext_eq(path, ".tif") || ext_eq(path, ".tiff") || ext_eq(path, ".avif") || + ext_eq(path, ".heic") || ext_eq(path, ".heif") || ext_eq(path, ".jxl") || + ext_eq(path, ".qoi") || ext_eq(path, ".tga") || ext_eq(path, ".dds") || + ext_eq(path, ".exr") || ext_eq(path, ".hdr") || ext_eq(path, ".ico"); +} + +int ecex_path_is_previewable_image(const char *path) { + return ecex_path_is_image(path); +} + +int ecex_path_is_video(const char *path) { + return ext_eq(path, ".mp4") || ext_eq(path, ".m4v") || ext_eq(path, ".mov") || + ext_eq(path, ".mkv") || ext_eq(path, ".webm") || ext_eq(path, ".avi") || + ext_eq(path, ".wmv") || ext_eq(path, ".flv") || ext_eq(path, ".gif") || + ext_eq(path, ".ogv") || ext_eq(path, ".mpeg") || ext_eq(path, ".mpg"); +} + +int ecex_path_is_media(const char *path) { + return ecex_path_is_image(path) || ecex_path_is_video(path); +} + +int ecex_path_cwd(char *out, size_t out_size) { + if (!out || out_size == 0) return -1; + if (!getcwd(out, out_size)) { + ecex_path_copy(out, out_size, "."); + return -1; + } + return 0; +} diff --git a/src/render.c b/src/render.c index 64a85f1..86bc535 100644 --- a/src/render.c +++ b/src/render.c @@ -1,6 +1,7 @@ #include "render.h" #include "common.h" +#include "media.h" #include #include @@ -474,6 +475,68 @@ static void draw_buffer_line(app_t *app, free(line); } + +static void draw_media_buffer(app_t *app, buffer_t *buf, view_rect_t rect) { + if (!app || !buf || !ecex_media_buffer_has_pixels(buf)) return; + + if (buf->media_texture == 0) { + glGenTextures(1, &buf->media_texture); + buf->media_texture_width = 0; + buf->media_texture_height = 0; + } + + glEnable(GL_TEXTURE_2D); + glBindTexture(GL_TEXTURE_2D, buf->media_texture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); + + if (buf->media_dirty || + buf->media_texture_width != buf->media_width || + buf->media_texture_height != buf->media_height) { + glTexImage2D(GL_TEXTURE_2D, + 0, + GL_RGBA, + buf->media_width, + buf->media_height, + 0, + GL_RGBA, + GL_UNSIGNED_BYTE, + buf->media_pixels); + buf->media_texture_width = buf->media_width; + buf->media_texture_height = buf->media_height; + buf->media_dirty = 0; + } + + ui_metrics_t m = ui_metrics(app); + float avail_x = rect.x + m.content_x; + float avail_y = rect.y + m.content_top + app->font.line_height * 4.0f; + float avail_w = rect.w - m.content_x * 2.0f; + float avail_h = rect.h - (avail_y - rect.y) - m.content_bottom_pad; + if (avail_w < 8.0f || avail_h < 8.0f) return; + + float iw = (float)buf->media_width; + float ih = (float)buf->media_height; + float scale = avail_w / iw; + if (ih * scale > avail_h) scale = avail_h / ih; + if (scale <= 0.0f) return; + + float draw_w = iw * scale; + float draw_h = ih * scale; + float x = avail_x + (avail_w - draw_w) * 0.5f; + float y = avail_y + (avail_h - draw_h) * 0.5f; + + glColor3f(1.0f, 1.0f, 1.0f); + glBegin(GL_QUADS); + glTexCoord2f(0.0f, 0.0f); glVertex2f(x, y); + glTexCoord2f(1.0f, 0.0f); glVertex2f(x + draw_w, y); + glTexCoord2f(1.0f, 1.0f); glVertex2f(x + draw_w, y + draw_h); + glTexCoord2f(0.0f, 1.0f); glVertex2f(x, y + draw_h); + glEnd(); + glDisable(GL_TEXTURE_2D); +} + static void render_cursor(app_t *app, buffer_t *buf, view_rect_t rect) { ui_metrics_t m = ui_metrics(app); @@ -522,6 +585,708 @@ static void draw_window_border(app_t *app, view_rect_t rect, int active) { draw_rect(rect.x + rect.w - 1.0f, rect.y, 1.0f, rect.h); } + +static app_t *draw_context_app(ecex_draw_context_t *ctx) { + return ctx ? (app_t *)ctx->internal : NULL; +} + +static void draw_context_vertex(ecex_draw_context_t *ctx, float x, float y) { + glVertex2f(ctx->x + x, ctx->y + y); +} + +void ecex_draw_set_color(ecex_draw_context_t *ctx, float r, float g, float b, float a) { + (void)ctx; + glColor4f(r, g, b, a); +} + +void ecex_draw_rect(ecex_draw_context_t *ctx, float x, float y, float w, float h) { + if (!ctx || w <= 0.0f || h <= 0.0f) return; + glDisable(GL_TEXTURE_2D); + glBegin(GL_QUADS); + draw_context_vertex(ctx, x, y); + draw_context_vertex(ctx, x + w, y); + draw_context_vertex(ctx, x + w, y + h); + draw_context_vertex(ctx, x, y + h); + glEnd(); +} + +void ecex_draw_rect_outline(ecex_draw_context_t *ctx, float x, float y, float w, float h, float thickness) { + if (!ctx || w <= 0.0f || h <= 0.0f) return; + if (thickness <= 0.0f) thickness = 1.0f; + ecex_draw_rect(ctx, x, y, w, thickness); + ecex_draw_rect(ctx, x, y + h - thickness, w, thickness); + ecex_draw_rect(ctx, x, y, thickness, h); + ecex_draw_rect(ctx, x + w - thickness, y, thickness, h); +} + +void ecex_draw_line(ecex_draw_context_t *ctx, float x1, float y1, float x2, float y2, float thickness) { + if (!ctx) return; + if (thickness <= 0.0f) thickness = 1.0f; + glDisable(GL_TEXTURE_2D); + glLineWidth(thickness); + glBegin(GL_LINES); + draw_context_vertex(ctx, x1, y1); + draw_context_vertex(ctx, x2, y2); + glEnd(); + glLineWidth(1.0f); +} + +void ecex_draw_text(ecex_draw_context_t *ctx, float x, float y, const char *text) { + app_t *app = draw_context_app(ctx); + if (!ctx || !app || !text) return; + float baseline = y + (app->font.ascent_px > 1.0f ? app->font.ascent_px : app->font.size_px * 0.80f); + draw_text(&app->font, ctx->x + x, ctx->y + baseline, text); +} + +float ecex_draw_text_width(ecex_draw_context_t *ctx, const char *text) { + app_t *app = draw_context_app(ctx); + if (!ctx || !app || !text) return 0.0f; + return text_width(&app->font, text); +} + +void ecex_draw_text_aligned(ecex_draw_context_t *ctx, float x, float y, float w, const char *text, int align) { + if (!ctx || !text) return; + float tw = ecex_draw_text_width(ctx, text); + float tx = x; + if (align == ECEX_TEXT_ALIGN_CENTER) tx = x + (w - tw) * 0.5f; + else if (align == ECEX_TEXT_ALIGN_RIGHT) tx = x + w - tw; + ecex_draw_text(ctx, tx, y, text); +} + +void ecex_draw_color_rgba8(ecex_draw_context_t *ctx, int r, int g, int b, int a) { + if (!ctx) return; + if (r < 0) r = 0; if (r > 255) r = 255; + if (g < 0) g = 0; if (g > 255) g = 255; + if (b < 0) b = 0; if (b > 255) b = 255; + if (a < 0) a = 0; if (a > 255) a = 255; + ecex_draw_set_color(ctx, (float)r / 255.0f, (float)g / 255.0f, (float)b / 255.0f, (float)a / 255.0f); +} + +void ecex_draw_rect_i(ecex_draw_context_t *ctx, int x, int y, int w, int h) { + ecex_draw_rect(ctx, (float)x, (float)y, (float)w, (float)h); +} + +void ecex_draw_rect_outline_i(ecex_draw_context_t *ctx, int x, int y, int w, int h, int thickness) { + ecex_draw_rect_outline(ctx, (float)x, (float)y, (float)w, (float)h, (float)thickness); +} + +void ecex_draw_line_i(ecex_draw_context_t *ctx, int x1, int y1, int x2, int y2, int thickness) { + ecex_draw_line(ctx, (float)x1, (float)y1, (float)x2, (float)y2, (float)thickness); +} + +void ecex_draw_text_i(ecex_draw_context_t *ctx, int x, int y, const char *text) { + ecex_draw_text(ctx, (float)x, (float)y, text); +} + + +static const char *ecex_draw_label_text(int label_id) { + switch (label_id) { + case 1: return "Tetris"; + case 2: return "score: "; + case 3: return "lines: "; + case 4: return "level: "; + case 5: return "next:"; + case 6: return "keys:"; + case 7: return "h/l or left/right move"; + case 8: return "j or down soft drop"; + case 9: return "k/up rotate"; + case 10: return "space hard drop"; + case 11: return "p pause, n new, q quit"; + case 12: return "GAME OVER - press n"; + case 13: return "PAUSED"; + case 20: return "Render demo"; + case 21: return "host vars and mouse input"; + case 22: return "integer safe drawing"; + case 23: return "smooth click animation"; + case 24: return "position: "; + case 25: return "click box to move cube"; + default: return ""; + } +} + +static unsigned char ecex_ascii5x7_bits(char ch, int row) { + /* Tiny host-side pixel font used by CCDJIT-safe label helpers. This avoids + * routing plugin labels through the normal stb/font text renderer, which is + * useful while plugin callback ABI issues are being isolated. Bits are + * returned left-to-right in the low 5 bits. */ + if (row < 0 || row >= 7) return 0; + switch (ch) { + case '0': { static const unsigned char b[7] = {14,17,19,21,25,17,14}; return b[row]; } + case '1': { static const unsigned char b[7] = {4,12,4,4,4,4,14}; return b[row]; } + case '2': { static const unsigned char b[7] = {14,17,1,2,4,8,31}; return b[row]; } + case '3': { static const unsigned char b[7] = {30,1,1,14,1,1,30}; return b[row]; } + case '4': { static const unsigned char b[7] = {2,6,10,18,31,2,2}; return b[row]; } + case '5': { static const unsigned char b[7] = {31,16,16,30,1,1,30}; return b[row]; } + case '6': { static const unsigned char b[7] = {14,16,16,30,17,17,14}; return b[row]; } + case '7': { static const unsigned char b[7] = {31,1,2,4,8,8,8}; return b[row]; } + case '8': { static const unsigned char b[7] = {14,17,17,14,17,17,14}; return b[row]; } + case '9': { static const unsigned char b[7] = {14,17,17,15,1,1,14}; return b[row]; } + case 'A': case 'a': { static const unsigned char b[7] = {14,17,17,31,17,17,17}; return b[row]; } + case 'B': case 'b': { static const unsigned char b[7] = {30,17,17,30,17,17,30}; return b[row]; } + case 'C': case 'c': { static const unsigned char b[7] = {14,17,16,16,16,17,14}; return b[row]; } + case 'D': case 'd': { static const unsigned char b[7] = {30,17,17,17,17,17,30}; return b[row]; } + case 'E': case 'e': { static const unsigned char b[7] = {31,16,16,30,16,16,31}; return b[row]; } + case 'F': case 'f': { static const unsigned char b[7] = {31,16,16,30,16,16,16}; return b[row]; } + case 'G': case 'g': { static const unsigned char b[7] = {14,17,16,23,17,17,15}; return b[row]; } + case 'H': case 'h': { static const unsigned char b[7] = {17,17,17,31,17,17,17}; return b[row]; } + case 'I': case 'i': { static const unsigned char b[7] = {14,4,4,4,4,4,14}; return b[row]; } + case 'J': case 'j': { static const unsigned char b[7] = {7,2,2,2,2,18,12}; return b[row]; } + case 'K': case 'k': { static const unsigned char b[7] = {17,18,20,24,20,18,17}; return b[row]; } + case 'L': case 'l': { static const unsigned char b[7] = {16,16,16,16,16,16,31}; return b[row]; } + case 'M': case 'm': { static const unsigned char b[7] = {17,27,21,21,17,17,17}; return b[row]; } + case 'N': case 'n': { static const unsigned char b[7] = {17,25,21,19,17,17,17}; return b[row]; } + case 'O': case 'o': { static const unsigned char b[7] = {14,17,17,17,17,17,14}; return b[row]; } + case 'P': case 'p': { static const unsigned char b[7] = {30,17,17,30,16,16,16}; return b[row]; } + case 'Q': case 'q': { static const unsigned char b[7] = {14,17,17,17,21,18,13}; return b[row]; } + case 'R': case 'r': { static const unsigned char b[7] = {30,17,17,30,20,18,17}; return b[row]; } + case 'S': case 's': { static const unsigned char b[7] = {15,16,16,14,1,1,30}; return b[row]; } + case 'T': case 't': { static const unsigned char b[7] = {31,4,4,4,4,4,4}; return b[row]; } + case 'U': case 'u': { static const unsigned char b[7] = {17,17,17,17,17,17,14}; return b[row]; } + case 'V': case 'v': { static const unsigned char b[7] = {17,17,17,17,17,10,4}; return b[row]; } + case 'W': case 'w': { static const unsigned char b[7] = {17,17,17,21,21,21,10}; return b[row]; } + case 'X': case 'x': { static const unsigned char b[7] = {17,17,10,4,10,17,17}; return b[row]; } + case 'Y': case 'y': { static const unsigned char b[7] = {17,17,10,4,4,4,4}; return b[row]; } + case 'Z': case 'z': { static const unsigned char b[7] = {31,1,2,4,8,16,31}; return b[row]; } + case ':': { static const unsigned char b[7] = {0,4,4,0,4,4,0}; return b[row]; } + case '-': { static const unsigned char b[7] = {0,0,0,14,0,0,0}; return b[row]; } + case '/': { static const unsigned char b[7] = {1,1,2,4,8,16,16}; return b[row]; } + case ',': { static const unsigned char b[7] = {0,0,0,0,4,4,8}; return b[row]; } + case '.': { static const unsigned char b[7] = {0,0,0,0,0,12,12}; return b[row]; } + case ' ': default: return 0; + } +} + +static void ecex_draw_mini_text_i(ecex_draw_context_t *ctx, int x, int y, const char *text) { + int scale = 2; + int advance = 12; + int cx = x; + const char *p = text; + if (!ctx || !text) return; + while (*p) { + int row; + for (row = 0; row < 7; ++row) { + unsigned char bits = ecex_ascii5x7_bits(*p, row); + int col; + for (col = 0; col < 5; ++col) { + if (bits & (1u << (4 - col))) { + ecex_draw_rect_i(ctx, cx + col * scale, y + row * scale, scale, scale); + } + } + } + cx += advance; + ++p; + } +} + + +extern const char *ecex_text_get_for_draw(ecex_t *ed, void *owner, int id); + +void ecex_draw_text_id_i(ecex_draw_context_t *ctx, void *owner, int id, int x, int y) { + app_t *app; + const char *text; + if (!ctx || !ctx->internal) return; + app = (app_t *)ctx->internal; + if (!app || !app->ed) return; + text = ecex_text_get_for_draw(app->ed, owner, id); + if (!text) text = ""; + + /* + * Plugin-safe real-font path: plugin code passes only owner/id and integer + * coordinates. The string itself lives in the host text registry, and the + * actual font renderer is called here on the host side, not directly from + * JIT-owned stack/string memory. Fixed labels may still use the mini-font + * helpers, but arbitrary plugin text such as Markdown should render with + * the loaded editor font. + */ + ecex_draw_text(ctx, (float)x, (float)y, text); +} + + + +int ecex_draw_context_height_i(ecex_draw_context_t *ctx) { + if (!ctx) return 0; + return (int)ctx->h; +} + +int ecex_draw_context_line_height_i(ecex_draw_context_t *ctx) { + int line_h; + if (!ctx) return 18; + line_h = (int)ctx->line_height; + return line_h < 18 ? 18 : line_h; +} + +int ecex_markdown_body_y_i(ecex_draw_context_t *ctx) { + int line_h; + if (!ctx) return 36; + line_h = ecex_draw_context_line_height_i(ctx); + return (int)ctx->content_y + line_h * 2; +} + +static int md_host_strlen(const char *s) { + int n = 0; + if (!s) return 0; + while (s[n]) ++n; + return n; +} + +static int md_host_is_digit(char c) { + return c >= '0' && c <= '9'; +} + +static const char *md_host_skip_indent(const char *line) { + if (!line) return ""; + while (*line == ' ' || *line == '\t') ++line; + return line; +} + +static int md_host_line_is_fence(const char *line) { + const char *p = md_host_skip_indent(line); + return (p[0] == '`' && p[1] == '`' && p[2] == '`') || + (p[0] == '~' && p[1] == '~' && p[2] == '~'); +} + +static int md_host_line_is_hr(const char *line) { + const char *p = md_host_skip_indent(line); + char mark = 0; + int count = 0; + while (*p) { + if (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n') { ++p; continue; } + if (*p != '-' && *p != '*' && *p != '_') return 0; + if (!mark) mark = *p; + if (*p != mark) return 0; + ++count; + ++p; + } + return count >= 3; +} + +static int md_host_heading_level(const char *line, const char **out_text) { + const char *p = md_host_skip_indent(line); + int level = 0; + while (p[level] == '#' && level < 6) ++level; + if (level > 0 && (p[level] == ' ' || p[level] == '\t' || p[level] == '\0')) { + p += level; + while (*p == ' ' || *p == '\t') ++p; + if (out_text) *out_text = p; + return level; + } + return 0; +} + +static const char *md_host_list_text(const char *line) { + const char *p = md_host_skip_indent(line); + if ((p[0] == '-' || p[0] == '*' || p[0] == '+') && (p[1] == ' ' || p[1] == '\t')) { + p += 2; + while (*p == ' ' || *p == '\t') ++p; + return p; + } + if (md_host_is_digit(p[0])) { + const char *q = p; + while (md_host_is_digit(*q)) ++q; + if (*q == '.' && (q[1] == ' ' || q[1] == '\t')) { + q += 2; + while (*q == ' ' || *q == '\t') ++q; + return q; + } + } + return NULL; +} + +static const char *md_host_quote_text(const char *line) { + const char *p = md_host_skip_indent(line); + if (*p != '>') return NULL; + ++p; + if (*p == ' ') ++p; + return p; +} + +static const char *md_host_trim_start(const char *text) { + if (!text) return ""; + while (*text == ' ' || *text == '\t') ++text; + return text; +} + +static int md_host_trim_len(const char *text) { + int n; + if (!text) return 0; + while (*text == ' ' || *text == '\t') ++text; + n = md_host_strlen(text); + while (n > 0 && (text[n - 1] == ' ' || text[n - 1] == '\t')) --n; + if (n > 224) n = 224; + return n; +} + +static void md_host_set_and_draw(ecex_draw_context_t *ctx, void *owner, int y, int style, const char *text) { + app_t *app; + const char *start; + int len; + if (!ctx || !ctx->internal) return; + app = (app_t *)ctx->internal; + if (!app || !app->ed) return; + start = md_host_trim_start(text); + len = md_host_trim_len(start); + if (ecex_text_set(app->ed, owner, 2, start, len) == 0) { + ecex_draw_markdown_line_auto_i(ctx, owner, 2, y, style); + } +} + +int ecex_markdown_draw_line_from_buffer_i(ecex_draw_context_t *ctx, void *owner, buffer_t *buffer, int line_index, int y, int in_code) { + char line[512]; + const char *text = NULL; + int copied; + int line_h; + int heading; + int next_in_code = in_code ? 1 : 0; + int advance; + app_t *app; + + if (!ctx || !ctx->internal || !buffer) return 18; + app = (app_t *)ctx->internal; + line_h = ecex_draw_context_line_height_i(ctx); + copied = ecex_buffer_line_copy(buffer, line_index, line, (int)sizeof(line)); + if (line_index < 4) { + ecex_log_int("markdown_host_line: index=", line_index); + ecex_log_int("markdown_host_line: copied=", copied); + } + if (copied < 0) return line_h; + line[sizeof(line) - 1] = '\0'; + + if (md_host_line_is_fence(line)) { + next_in_code = !next_in_code; + if (app && app->ed) ecex_text_set(app->ed, owner, 2, next_in_code ? "code" : "end code", -1); + ecex_draw_markdown_line_auto_i(ctx, owner, 2, y, 5); + advance = line_h + 8; + return advance | (next_in_code ? 0x10000 : 0); + } + + if (next_in_code) { + if (app && app->ed) ecex_text_set(app->ed, owner, 2, line, copied > 224 ? 224 : copied); + ecex_draw_markdown_line_auto_i(ctx, owner, 2, y, 3); + return line_h | 0x10000; + } + + heading = md_host_heading_level(line, &text); + if (heading) { + md_host_set_and_draw(ctx, owner, y, 1, text); + advance = line_h + (7 - heading) * 3 + 8; + return advance; + } + + if (md_host_line_is_hr(line)) { + if (app && app->ed) ecex_text_set(app->ed, owner, 2, "", 0); + ecex_draw_markdown_line_auto_i(ctx, owner, 2, y, 6); + return line_h; + } + + text = md_host_quote_text(line); + if (text) { + md_host_set_and_draw(ctx, owner, y, 2, text); + return line_h; + } + + text = md_host_list_text(line); + if (text) { + md_host_set_and_draw(ctx, owner, y, 4, text); + return line_h; + } + + if (line[0] == '\0') return line_h / 2; + + md_host_set_and_draw(ctx, owner, y, 0, line); + return line_h; +} + +void ecex_draw_markdown_canvas_i(ecex_draw_context_t *ctx, void *owner, int title_id, int x, int y, int w, int line_h) { + app_t *app; + const char *title; + int full_w; + int full_h; + if (!ctx || !ctx->internal) return; + app = (app_t *)ctx->internal; + full_w = (int)ctx->w; + full_h = (int)ctx->h; + if (w < 1) w = full_w - x * 2; + if (w < 1) w = 1; + if (line_h < 18) line_h = 18; + ecex_draw_color_rgba8(ctx, 29, 32, 33, 255); + ecex_draw_rect_i(ctx, 0, 0, full_w, full_h); + ecex_draw_color_rgba8(ctx, 250, 241, 199, 255); + title = (app && app->ed) ? ecex_text_get_for_draw(app->ed, owner, title_id) : ""; + if (!title) title = ""; + ecex_draw_text(ctx, (float)x, (float)y, title); + ecex_draw_color_rgba8(ctx, 80, 73, 69, 255); + ecex_draw_line_i(ctx, x, y + line_h + 8, x + w, y + line_h + 8, 1); +} + +void ecex_draw_markdown_text_i(ecex_draw_context_t *ctx, void *owner, int text_id, int x, int y, int w, int line_h, int style) { + app_t *app; + const char *text; + int h; + if (!ctx || !ctx->internal) return; + app = (app_t *)ctx->internal; + text = (app && app->ed) ? ecex_text_get_for_draw(app->ed, owner, text_id) : ""; + if (!text) text = ""; + if (w < 1) w = 1; + if (line_h < 18) line_h = 18; + h = line_h + 4; + switch (style) { + case 1: /* heading */ + ecex_draw_color_rgba8(ctx, 69, 84, 96, 255); + ecex_draw_rect_i(ctx, x, y - 5, w, h + 6); + ecex_draw_color_rgba8(ctx, 250, 189, 47, 255); + ecex_draw_text(ctx, (float)(x + 10), (float)y, text); + break; + case 2: /* quote */ + ecex_draw_color_rgba8(ctx, 131, 165, 152, 255); + ecex_draw_rect_i(ctx, x, y - 2, 4, h); + ecex_draw_color_rgba8(ctx, 213, 196, 161, 255); + ecex_draw_text(ctx, (float)(x + 14), (float)y, text); + break; + case 3: /* code */ + ecex_draw_color_rgba8(ctx, 40, 40, 40, 255); + ecex_draw_rect_i(ctx, x, y - 2, w, h); + ecex_draw_color_rgba8(ctx, 213, 196, 161, 255); + ecex_draw_text(ctx, (float)(x + 10), (float)y, text); + break; + case 4: /* list */ + ecex_draw_color_rgba8(ctx, 184, 187, 38, 255); + ecex_draw_rect_i(ctx, x + 6, y + line_h / 2 - 3, 6, 6); + ecex_draw_color_rgba8(ctx, 235, 219, 178, 255); + ecex_draw_text(ctx, (float)(x + 24), (float)y, text); + break; + case 5: /* fence */ + ecex_draw_color_rgba8(ctx, 80, 73, 69, 255); + ecex_draw_rect_i(ctx, x, y - 3, w, line_h + 6); + ecex_draw_color_rgba8(ctx, 142, 192, 124, 255); + ecex_draw_text(ctx, (float)(x + 10), (float)y, text); + break; + case 6: /* hr */ + ecex_draw_color_rgba8(ctx, 80, 73, 69, 255); + ecex_draw_line_i(ctx, x, y + line_h / 2, x + w, y + line_h / 2, 2); + break; + case 0: + default: + ecex_draw_color_rgba8(ctx, 235, 219, 178, 255); + ecex_draw_text(ctx, (float)x, (float)y, text); + break; + } +} + +void ecex_draw_markdown_canvas_auto_i(ecex_draw_context_t *ctx, void *owner, int title_id) { + int x; + int y; + int w; + int line_h; + if (!ctx) return; + ecex_log("draw_markdown_canvas_auto: enter"); + x = (int)ctx->content_x; + y = (int)ctx->content_y; + w = (int)ctx->content_w; + line_h = (int)ctx->line_height; + if (line_h < 18) line_h = 18; + if (w < 1) w = (int)ctx->w - x * 2; + if (w < 1) w = 1; + ecex_log("draw_markdown_canvas_auto: dispatch"); + ecex_draw_markdown_canvas_i(ctx, owner, title_id, x, y, w, line_h); + ecex_log("draw_markdown_canvas_auto: leave"); +} + +void ecex_draw_markdown_line_auto_i(ecex_draw_context_t *ctx, void *owner, int text_id, int y, int style) { + int x; + int w; + int line_h; + if (!ctx) return; + x = (int)ctx->content_x; + w = (int)ctx->content_w; + line_h = (int)ctx->line_height; + if (line_h < 18) line_h = 18; + if (w < 1) w = (int)ctx->w - x * 2; + if (w < 1) w = 1; + ecex_draw_markdown_text_i(ctx, owner, text_id, x, y, w, line_h, style); +} + +void ecex_draw_label_i(ecex_draw_context_t *ctx, int x, int y, int label_id) { + ecex_draw_mini_text_i(ctx, x, y, ecex_draw_label_text(label_id)); +} + +static void ecex_i32_to_ascii(int value, char *buf, size_t cap) { + char tmp[16]; + size_t n = 0; + size_t out = 0; + unsigned int v; + if (!buf || cap == 0) return; + if (value < 0) { + if (out + 1 < cap) buf[out++] = '-'; + v = (unsigned int)(-value); + } else { + v = (unsigned int)value; + } + do { + tmp[n++] = (char)('0' + (v % 10u)); + v /= 10u; + } while (v && n < sizeof(tmp)); + while (n && out + 1 < cap) buf[out++] = tmp[--n]; + buf[out] = '\0'; +} + +void ecex_draw_stat_i(ecex_draw_context_t *ctx, int x, int y, int label_id, int value) { + char num[24]; + const char *prefix = ecex_draw_label_text(label_id); + if (!ctx || !prefix) return; + ecex_draw_mini_text_i(ctx, x, y, prefix); + ecex_i32_to_ascii(value, num, sizeof(num)); + ecex_draw_mini_text_i(ctx, x + (int)strlen(prefix) * 12, y, num); +} + + +static int ecex_tetris_preview_shape_cell(int piece, int rot, int col, int row) { + int p = piece % 7; + int r = rot & 3; + if (p < 0) p += 7; + if (col < 0 || col >= 4 || row < 0 || row >= 4) return 0; + + if (p == 0) { + if ((r & 1) == 0) return row == 1; + return col == 1; + } + if (p == 1) return (row == 1 || row == 2) && (col == 1 || col == 2); + if (p == 2) { + if (r == 0) return (row == 1 && col >= 0 && col <= 2) || (row == 2 && col == 1); + if (r == 1) return (col == 1 && row >= 0 && row <= 2) || (row == 1 && col == 2); + if (r == 2) return (row == 1 && col >= 0 && col <= 2) || (row == 0 && col == 1); + return (col == 1 && row >= 0 && row <= 2) || (row == 1 && col == 0); + } + if (p == 3) { + if ((r & 1) == 0) return (row == 1 && (col == 1 || col == 2)) || (row == 2 && (col == 0 || col == 1)); + return (col == 1 && (row == 0 || row == 1)) || (col == 2 && (row == 1 || row == 2)); + } + if (p == 4) { + if ((r & 1) == 0) return (row == 1 && (col == 0 || col == 1)) || (row == 2 && (col == 1 || col == 2)); + return (col == 2 && (row == 0 || row == 1)) || (col == 1 && (row == 1 || row == 2)); + } + if (p == 5) { + if (r == 0) return (row == 1 && col >= 0 && col <= 2) || (row == 0 && col == 0); + if (r == 1) return (col == 1 && row >= 0 && row <= 2) || (row == 0 && col == 2); + if (r == 2) return (row == 1 && col >= 0 && col <= 2) || (row == 2 && col == 2); + return (col == 1 && row >= 0 && row <= 2) || (row == 2 && col == 0); + } + if (r == 0) return (row == 1 && col >= 0 && col <= 2) || (row == 0 && col == 2); + if (r == 1) return (col == 1 && row >= 0 && row <= 2) || (row == 2 && col == 2); + if (r == 2) return (row == 1 && col >= 0 && col <= 2) || (row == 2 && col == 0); + return (col == 1 && row >= 0 && row <= 2) || (row == 0 && col == 0); +} + +static void ecex_draw_tetris_preview_color(ecex_draw_context_t *ctx, int piece, int alpha) { + if (piece == 0) ecex_draw_color_rgba8(ctx, 51, 191, 242, alpha); + else if (piece == 1) ecex_draw_color_rgba8(ctx, 242, 217, 51, alpha); + else if (piece == 2) ecex_draw_color_rgba8(ctx, 179, 89, 242, alpha); + else if (piece == 3) ecex_draw_color_rgba8(ctx, 77, 217, 89, alpha); + else if (piece == 4) ecex_draw_color_rgba8(ctx, 242, 64, 64, alpha); + else if (piece == 5) ecex_draw_color_rgba8(ctx, 64, 102, 242, alpha); + else ecex_draw_color_rgba8(ctx, 242, 140, 51, alpha); +} + +void ecex_draw_tetris_preview_i(ecex_draw_context_t *ctx, int piece, int x, int y, int cell, int alpha) { + int r; + int c; + int p = piece % 7; + if (!ctx) return; + if (p < 0) p += 7; + if (cell < 3) cell = 3; + if (alpha < 0) alpha = 0; + if (alpha > 255) alpha = 255; + + /* Clear a small preview box first so the old preview shape cannot linger + * when the new piece occupies fewer cells than the previous I piece. */ + ecex_draw_color_rgba8(ctx, 20, 23, 28, 255); + ecex_draw_rect_i(ctx, x - 2, y - 2, cell * 4 + 4, cell * 4 + 4); + + for (r = 0; r < 4; ++r) { + for (c = 0; c < 4; ++c) { + if (!ecex_tetris_preview_shape_cell(p, 0, c, r)) continue; + ecex_draw_tetris_preview_color(ctx, p, alpha); + ecex_draw_rect_i(ctx, x + c * cell + 1, y + r * cell + 1, cell - 2, cell - 2); + } + } +} + +void ecex_draw_rgba(ecex_draw_context_t *ctx, + float x, + float y, + float w, + float h, + const unsigned char *rgba, + int image_w, + int image_h) { + if (!ctx || !rgba || image_w <= 0 || image_h <= 0 || w <= 0.0f || h <= 0.0f) return; + + GLuint tex = 0; + glGenTextures(1, &tex); + if (tex == 0) return; + + glEnable(GL_TEXTURE_2D); + glBindTexture(GL_TEXTURE_2D, tex); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image_w, image_h, 0, GL_RGBA, GL_UNSIGNED_BYTE, rgba); + + glColor4f(1.0f, 1.0f, 1.0f, 1.0f); + glBegin(GL_QUADS); + glTexCoord2f(0.0f, 0.0f); draw_context_vertex(ctx, x, y); + glTexCoord2f(1.0f, 0.0f); draw_context_vertex(ctx, x + w, y); + glTexCoord2f(1.0f, 1.0f); draw_context_vertex(ctx, x + w, y + h); + glTexCoord2f(0.0f, 1.0f); draw_context_vertex(ctx, x, y + h); + glEnd(); + + glBindTexture(GL_TEXTURE_2D, 0); + glDeleteTextures(1, &tex); + glDisable(GL_TEXTURE_2D); +} + +static ecex_draw_context_t make_draw_context(app_t *app, view_rect_t rect, size_t index) { + ui_metrics_t m = ui_metrics(app); + ecex_draw_context_t ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.x = rect.x; + ctx.y = rect.y; + ctx.w = rect.w; + ctx.h = rect.h; + ctx.content_x = m.content_x; + ctx.content_y = m.content_top; + ctx.content_w = rect.w - m.content_x * 2.0f; + ctx.content_h = rect.h - m.content_top - m.content_bottom_pad; + if (ctx.content_w < 0.0f) ctx.content_w = 0.0f; + if (ctx.content_h < 0.0f) ctx.content_h = 0.0f; + ctx.font_size = app->font.size_px; + ctx.line_height = app->font.line_height; + ctx.char_width = mono_cell_width(app); + ctx.window_index = index; + ctx.active = index == app->ed->current_window_index; + ctx.internal = app; + return ctx; +} + +static void render_custom_buffer(app_t *app, buffer_t *buf, view_rect_t rect, size_t index) { + if (!app || !buf || !buf->render_fn) return; + ecex_draw_context_t ctx = make_draw_context(app, rect, index); + int trace_callbacks = 0; + const char *trace_env = getenv("ECEX_TRACE_CALLBACKS"); + trace_callbacks = trace_env && trace_env[0] && trace_env[0] != '0'; + if (trace_callbacks) { + fprintf(stderr, "ecex-log: render_callback_enter buffer=%p fn=%p userdata=%p window=%zu %.1fx%.1f\n", + (void *)buf, (void *)buf->render_fn, buf->render_userdata, index, rect.w, rect.h); + fflush(stderr); + } + int result = buf->render_fn(app->ed, buf, &ctx, buf->render_userdata); + if (trace_callbacks) { + fprintf(stderr, "ecex-log: render_callback_leave buffer=%p result=%d\n", + (void *)buf, result); + fflush(stderr); + } +} + static void render_buffer_window(app_t *app, ecex_window_t *win, size_t index, float editor_h) { if (!app || !win || !win->buffer) return; @@ -549,29 +1314,41 @@ static void render_buffer_window(app_t *app, ecex_window_t *win, size_t index, f ensure_cursor_visible(app, buf, rect); } - size_t rows = visible_row_count_for_rect(app, rect); - size_t pos = offset_for_line(buf, buf->scroll_line); - float line_top = rect.y + m.content_top; + int replace_content = buf->render_fn && (buf->render_flags & ECEX_RENDER_REPLACE_CONTENT); + + if (!replace_content) { + size_t rows = visible_row_count_for_rect(app, rect); + size_t pos = offset_for_line(buf, buf->scroll_line); + float line_top = rect.y + m.content_top; + + for (size_t row = 0; row < rows; row++) { + size_t line_start = pos; + size_t line_end = buffer_line_end_at(buf, line_start); - for (size_t row = 0; row < rows; row++) { - size_t line_start = pos; - size_t line_end = buffer_line_end_at(buf, line_start); + draw_buffer_line(app, + buf, + rect, + buf->scroll_line + row, + line_start, + line_end, + line_top); - draw_buffer_line(app, - buf, - rect, - buf->scroll_line + row, - line_start, - line_end, - line_top); + if (line_end >= buf->len) break; + pos = line_end + 1; + line_top += app->font.line_height; + } + + if (buf->media_kind != ECEX_MEDIA_NONE) { + draw_media_buffer(app, buf, rect); + } - if (line_end >= buf->len) break; - pos = line_end + 1; - line_top += app->font.line_height; + if (index == app->ed->current_window_index && buf->media_kind == ECEX_MEDIA_NONE) { + render_cursor(app, buf, rect); + } } - if (index == app->ed->current_window_index) { - render_cursor(app, buf, rect); + if (buf->render_fn) { + render_custom_buffer(app, buf, rect, index); } glDisable(GL_SCISSOR_TEST); -- cgit v1.2.3