esc

Externally Scriptable Editor

git clone git://mccd.space/esc

commit 510afa756f3e66e75d3a4423a4b20addf1071a5a
parent adbfcdaeca908232daad90536e393e7e4695db67
Author: Marc Coquand <marc@coquand.email>
Date:   Thu, 19 Feb 2026 15:01:01 +0100

Migrate to editor code

Diffstat:
Aeditor.c | 147+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aeditor.h | 41+++++++++++++++++++++++++++++++++++++++++
Mmain.c | 309+++++++++++++++++++------------------------------------------------------------
3 files changed, 263 insertions(+), 234 deletions(-)
diff --git a/editor.c b/editor.c
@@ -0,0 +1,147 @@
+#include "editor.h"
+#include <stdlib.h>
+#include <string.h>
+
+Editor* editor_create(int char_width, float line_height) {
+    Editor *ed = malloc(sizeof(Editor));
+    ed->capacity = 1024;
+    ed->buffer = malloc(ed->capacity);
+    ed->buffer[0] = '\0';
+    ed->length = 0;
+    ed->cursor_idx = 0;
+    ed->selection_anchor = 0;
+    ed->char_width = char_width;
+    ed->line_height = line_height;
+    return ed;
+}
+
+void editor_destroy(Editor *ed) {
+    free(ed->buffer);
+    free(ed);
+}
+
+void editor_insert_text(Editor *ed, const char *text) {
+    size_t input_len = strlen(text);
+    if (ed->length + input_len + 1 > ed->capacity) {
+        ed->capacity *= 2;
+        ed->buffer = realloc(ed->buffer, ed->capacity);
+    }
+    memmove(&ed->buffer[ed->cursor_idx + input_len], &ed->buffer[ed->cursor_idx], ed->length - ed->cursor_idx + 1);
+    memcpy(&ed->buffer[ed->cursor_idx], text, input_len);
+    ed->length += input_len;
+    ed->cursor_idx += (int)input_len;
+    ed->selection_anchor = ed->cursor_idx;
+}
+
+void editor_newline(Editor *ed) {
+    editor_insert_text(ed, "\n");
+}
+
+void editor_delete_range(Editor *ed, int start, int end) {
+    if (start == end) return;
+    if (start > end) { int tmp = start; start = end; end = tmp; }
+    
+    memmove(&ed->buffer[start], &ed->buffer[end], ed->length - end + 1);
+    ed->length -= (end - start);
+    ed->cursor_idx = start;
+    ed->selection_anchor = start;
+}
+
+void editor_delete_back(Editor *ed) {
+    if (ed->cursor_idx != ed->selection_anchor) {
+        editor_delete_range(ed, ed->selection_anchor, ed->cursor_idx);
+    } else if (ed->cursor_idx > 0) {
+        int prev = ed->cursor_idx;
+        do { prev--; } while (prev > 0 && (ed->buffer[prev] & 0xC0) == 0x80);
+        editor_delete_range(ed, prev, ed->cursor_idx);
+    }
+}
+
+void editor_delete_forward(Editor *ed) {
+    if (ed->cursor_idx != ed->selection_anchor) {
+        editor_delete_range(ed, ed->selection_anchor, ed->cursor_idx);
+    } else if (ed->cursor_idx < (int)ed->length) {
+        const char *ptr = ed->buffer + ed->cursor_idx;
+        const char *next = ptr;
+        SDL_StepUTF8(&next, NULL);
+        editor_delete_range(ed, ed->cursor_idx, (int)(next - ed->buffer));
+    }
+}
+
+void editor_cursor_left(Editor *ed) {
+    if (ed->cursor_idx > 0) {
+        ed->cursor_idx--;
+        while (ed->cursor_idx > 0 && (ed->buffer[ed->cursor_idx] & 0xC0) == 0x80) ed->cursor_idx--;
+    }
+    ed->selection_anchor = ed->cursor_idx;
+}
+
+void editor_cursor_right(Editor *ed) {
+    if (ed->cursor_idx < (int)ed->length) {
+        const char *next = ed->buffer + ed->cursor_idx;
+        SDL_StepUTF8(&next, NULL);
+        ed->cursor_idx = (int)(next - ed->buffer);
+    }
+    ed->selection_anchor = ed->cursor_idx;
+}
+
+void editor_cursor_up(Editor *ed) {
+    int line_start = ed->cursor_idx;
+    while (line_start > 0 && ed->buffer[line_start - 1] != '\n') line_start--;
+    
+    int visual_col = 0;
+    const char *p = ed->buffer + line_start;
+    while (p < ed->buffer + ed->cursor_idx) { SDL_StepUTF8(&p, NULL); visual_col++; }
+
+    if (line_start > 0) {
+        int prev_line_end = line_start - 1;
+        int prev_line_start = prev_line_end;
+        while (prev_line_start > 0 && ed->buffer[prev_line_start - 1] != '\n') prev_line_start--;
+        
+        const char *ptr = ed->buffer + prev_line_start;
+        for (int i = 0; i < visual_col && *ptr != '\n' && *ptr != '\0'; i++) SDL_StepUTF8(&ptr, NULL);
+        ed->cursor_idx = (int)(ptr - ed->buffer);
+    }
+    ed->selection_anchor = ed->cursor_idx;
+}
+void editor_cursor_down(Editor *ed) {
+    int line_start = ed->cursor_idx;
+    while (line_start > 0 && ed->buffer[line_start - 1] != '\n') line_start--;
+    
+    int visual_col = 0;
+    const char *p = ed->buffer + line_start;
+    while (p < ed->buffer + ed->cursor_idx) { SDL_StepUTF8(&p, NULL); visual_col++; }
+
+    const char *next_line = strchr(ed->buffer + ed->cursor_idx, '\n');
+    if (next_line) {
+        int target_idx = (int)(next_line - ed->buffer) + 1;
+        const char *ptr = ed->buffer + target_idx; 
+        for(int i=0; i < visual_col; i++) {
+            if (*ptr == '\n' || *ptr == '\0') break;
+            SDL_StepUTF8(&ptr, NULL);
+        }
+        ed->cursor_idx = (int)(ptr - ed->buffer);
+    }
+
+    ed->selection_anchor = ed->cursor_idx;
+}
+void editor_set_cursor_from_coords(Editor *ed, float mx, float my, float scroll_x, float scroll_y) {
+    int target_col = (int)((mx - 20.0f + scroll_x + (ed->char_width / 2.0f)) / ed->char_width);
+    int target_row = (int)((my - 20.0f + scroll_y) / ed->line_height);
+
+    int cur_r = 0, cur_c = 0;
+    const char *ptr = ed->buffer;
+    const char *last_ptr = ed->buffer;
+
+    while (*ptr != '\0') {
+        if (cur_r == target_row && cur_c == target_col) break;
+        last_ptr = ptr;
+        Uint32 cp = SDL_StepUTF8(&ptr, NULL);
+        if (cp == '\n') {
+            if (cur_r == target_row) { ptr = last_ptr; break; }
+            cur_r++; cur_c = 0;
+        } else cur_c++;
+        if (cur_r > target_row) { ptr = last_ptr; break; }
+    }
+    ed->cursor_idx = (int)(ptr - ed->buffer);
+}
diff --git a/editor.h b/editor.h
@@ -0,0 +1,41 @@
+#ifndef EDITOR_H
+#define EDITOR_H
+
+#include <SDL3/SDL.h>
+#include <stdbool.h>
+
+typedef struct {
+    char *buffer;
+    size_t capacity;
+    size_t length;
+    int cursor_idx;
+    int selection_anchor; // Where the selection started
+    
+    // Metrics for coordinate calculations
+    int char_width;
+    float line_height;
+} Editor;
+
+// Lifecycle
+Editor* editor_create(int char_width, float line_height);
+void editor_destroy(Editor *ed);
+
+// Core Actions
+void editor_insert_text(Editor *ed, const char *text);
+void editor_newline(Editor *ed);
+void editor_delete_back(Editor *ed);
+void editor_delete_forward(Editor *ed);
+void editor_delete_range(Editor *ed, int start, int end);
+
+// Cursor Movement
+void editor_cursor_left(Editor *ed);
+void editor_cursor_right(Editor *ed);
+void editor_cursor_up(Editor *ed);
+void editor_cursor_down(Editor *ed);
+
+// Selection & Coordination
+void editor_set_cursor_from_coords(Editor *ed, float mx, float my, float scroll_x, float scroll_y);
+void editor_set_selection(Editor *ed, int anchor, int cursor);
+void editor_clear_selection(Editor *ed);
+
+#endif
diff --git a/main.c b/main.c
@@ -1,4 +1,6 @@
+#include "editor.h"
 #include <SDL3/SDL.h>
+#include <SDL3/SDL_events.h>
 #include <SDL3/SDL_main.h>
 #include <SDL3_ttf/SDL_ttf.h>
 #include <stdbool.h>
@@ -66,19 +68,15 @@ int main(int argc, char *argv[]) {
 	TTF_GetGlyphMetrics(font, 'A', NULL, NULL, NULL, NULL, &char_width);
 	float line_height = (float)TTF_GetFontHeight(font);
 
-	size_t text_capacity = 1024; // Start with 1KB
-	char *text_buffer = malloc(text_capacity);
-	text_buffer[0] = '\0'; // Initialize as empty string
-	size_t text_length = 0;
-
 	float scroll_x = 0.0f;
 	float scroll_y = 0.0f;
 	bool running = true;
-	int cursor_idx = 0;
+	bool is_dragging = false;
 
 	SDL_StartTextInput(window);
-	int selection_idx = 0;
 	bool is_selecting = false;
+	Editor* ed = editor_create(char_width, line_height);
+	ed->cursor_idx = 0;
 
 	while (running) {
 		SDL_Event event;
@@ -96,231 +94,73 @@ int main(int argc, char *argv[]) {
 						scroll_x = 0;
 					if (scroll_y < 0)
 						scroll_y = 0;
-				} else if (event.type ==
-					   SDL_EVENT_MOUSE_BUTTON_DOWN) {
+				}
+
+				switch (event.type) {
+				case SDL_EVENT_KEY_DOWN:
+					switch (event.key.key) {
+					case SDLK_BACKSPACE:
+						editor_delete_back(ed);
+						break;
+					case SDLK_DELETE:
+						editor_delete_forward(ed);
+						break;
+					case SDLK_RETURN:
+						editor_newline(ed);
+						break;
+					case SDLK_LEFT:
+						editor_cursor_left(ed);
+						break;
+					case SDLK_RIGHT:
+						editor_cursor_right(ed);
+						break;
+					case SDLK_UP:
+						editor_cursor_up(ed);
+						break;
+					case SDLK_DOWN:
+						editor_cursor_down(ed);
+						break;
+					}
+				case SDL_EVENT_MOUSE_BUTTON_DOWN:
 					if (event.button.button ==
 					    SDL_BUTTON_LEFT) {
 						float mx, my;
 						SDL_RenderCoordinatesFromWindow(
 						    renderer, event.button.x,
 						    event.button.y, &mx, &my);
-						cursor_idx =
-						    get_idx_from_coords(
-							text_buffer, mx, my,
-							scroll_x, scroll_y,
-							char_width,
-							line_height);
-						selection_idx = cursor_idx;
-
-						is_selecting = true;
-					}
-				} else if (event.type ==
-					   SDL_EVENT_MOUSE_MOTION) {
-					if (is_selecting) {
-						float mx, my;
-						SDL_RenderCoordinatesFromWindow(
-						    renderer, event.motion.x,
-						    event.motion.y, &mx, &my);
-						cursor_idx =
-						    get_idx_from_coords(
-							text_buffer, mx, my,
-							scroll_x, scroll_y,
-							char_width,
-							line_height);
+						editor_set_cursor_from_coords(
+						    ed, mx, my, scroll_x,
+						    scroll_y);
+						ed->selection_anchor =
+						    ed->cursor_idx; 
+						is_dragging = true;
 					}
-				} else if (event.type ==
-					   SDL_EVENT_MOUSE_BUTTON_UP) {
+					break;
+				case SDL_EVENT_MOUSE_BUTTON_UP:
 					if (event.button.button ==
 					    SDL_BUTTON_LEFT) {
-						is_selecting = false;
+						is_dragging = false;
 					}
-				}
-
-				else if (event.type == SDL_EVENT_KEY_DOWN) {
-					is_selecting = false;
-					SDL_Keycode key = event.key.key;
-
-					if (key == SDLK_BACKSPACE &&
-					    cursor_idx > 0) {
-						int prev_idx = cursor_idx;
-						do {
-							prev_idx--;
-						} while (
-						    prev_idx > 0 &&
-						    (text_buffer[prev_idx] &
-						     0xC0) == 0x80);
-
-						int char_size =
-						    cursor_idx - prev_idx;
-						memmove(
-						    &text_buffer[prev_idx],
-						    &text_buffer[cursor_idx],
-						    text_length - cursor_idx +
-							1);
-						text_length -= char_size;
-						cursor_idx = prev_idx;
-					} else if (key == SDLK_RETURN) {
-						memmove(
-						    &text_buffer[cursor_idx +
-								 1],
-						    &text_buffer[cursor_idx],
-						    text_length - cursor_idx +
-							1);
-						text_buffer[cursor_idx] = '\n';
-						text_length++;
-						cursor_idx++;
-						selection_idx = cursor_idx;
-
-					} else if (key == SDLK_LEFT &&
-						   cursor_idx > 0) {
-						cursor_idx--;
-
-						while (
-						    cursor_idx > 0 &&
-						    (text_buffer[cursor_idx] &
-						     0xC0) == 0x80) {
-							cursor_idx--;
-						}
-						selection_idx = cursor_idx;
-					} else if (key == SDLK_DELETE &&
-						   cursor_idx < text_length) {
-						const char *ptr =
-						    text_buffer + cursor_idx;
-						const char *next = ptr;
-						SDL_StepUTF8(&next, NULL);
-						int char_size =
-						    (int)(next - ptr);
-						memmove(
-						    &text_buffer[cursor_idx],
-						    &text_buffer[cursor_idx +
-								 char_size],
-						    text_length -
-							(cursor_idx +
-							 char_size) +
-							1);
-						text_length -= char_size;
-					}
-
-					else if (key == SDLK_UP ||
-						 key == SDLK_DOWN) {
-						// 1. Find start of current line
-						// and current visual column
-						int line_start_idx = cursor_idx;
-						while (
-						    line_start_idx > 0 &&
-						    text_buffer[line_start_idx -
-								1] != '\n') {
-							line_start_idx--;
-						}
-
-						int visual_col = 0;
-						const char *p = text_buffer +
-								line_start_idx;
-						while (p < text_buffer +
-							       cursor_idx) {
-							SDL_StepUTF8(&p, NULL);
-							visual_col++;
-						}
-
-						int target_idx = -1;
-						if (key == SDLK_UP) {
-							if (line_start_idx >
-							    0) {
-								// Find start of
-								// PREVIOUS line
-								int prev_line_end =
-								    line_start_idx -
-								    1;
-								int prev_line_start =
-								    prev_line_end;
-								while (
-								    prev_line_start >
-									0 &&
-								    text_buffer
-									    [prev_line_start -
-									     1] !=
-									'\n') {
-									prev_line_start--;
-								}
-								target_idx =
-								    prev_line_start;
-							}
-							selection_idx =
-							    cursor_idx;
-
-						} else { // SDLK_DOWN
-							const char *next_line =
-							    strchr(
-								text_buffer +
-								    cursor_idx,
-								'\n');
-							if (next_line) {
-								target_idx =
-								    (int)(next_line -
-									  text_buffer) +
-								    1;
-							}
-							selection_idx =
-							    cursor_idx;
-						}
-
-						if (target_idx != -1) {
-							const char *ptr =
-							    text_buffer +
-							    target_idx;
-							for (int i = 0;
-							     i < visual_col;
-							     i++) {
-								if (*ptr ==
-									'\n' ||
-								    *ptr ==
-									'\0')
-									break;
-								SDL_StepUTF8(
-								    &ptr, NULL);
-							}
-							cursor_idx =
-							    (int)(ptr -
-								  text_buffer);
-						}
-					} else if (key == SDLK_RIGHT &&
-						   cursor_idx < text_length) {
-						const char *next =
-						    text_buffer + cursor_idx;
-						SDL_StepUTF8(&next, NULL);
-
-						cursor_idx =
-						    (int)(next - text_buffer);
-						selection_idx = cursor_idx;
-					}
-				}
+					break;
 
-				else if (event.type == SDL_EVENT_TEXT_INPUT) {
-					size_t input_len =
-					    strlen(event.text.text);
-
-					if (text_length + input_len + 1 >
-					    text_capacity) {
-						text_capacity *= 2;
-						char *new_buffer = realloc(
-						    text_buffer, text_capacity);
-						if (new_buffer) {
-							text_buffer =
-							    new_buffer;
-						} else {
-							break;
-						}
+				case SDL_EVENT_MOUSE_MOTION:
+					if (is_dragging) {
+						float mx, my;
+						SDL_RenderCoordinatesFromWindow(
+						    renderer, event.motion.x,
+						    event.motion.y, &mx, &my);
+						editor_set_cursor_from_coords(
+						    ed, mx, my, scroll_x,
+						    scroll_y);
+						// We don't update
+						// selection_anchor here, so the
+						// range expands
 					}
+					break;
 
-					memmove(&text_buffer[cursor_idx +
-							     input_len],
-						&text_buffer[cursor_idx],
-						text_length - cursor_idx + 1);
-					memcpy(&text_buffer[cursor_idx],
-					       event.text.text, input_len);
-					text_length += input_len;
-					cursor_idx += input_len;
-					selection_idx = cursor_idx;
+				case SDL_EVENT_TEXT_INPUT:
+					editor_insert_text(ed, event.text.text);
+					break;
 				}
 			} while (SDL_PollEvent(&event));
 		}
@@ -332,21 +172,21 @@ int main(int argc, char *argv[]) {
 
 		SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
 		SDL_RenderClear(renderer);
-		if (cursor_idx != selection_idx) {
-			int sel_min = (cursor_idx < selection_idx)
-					  ? cursor_idx
-					  : selection_idx;
-			int sel_max = (cursor_idx > selection_idx)
-					  ? cursor_idx
-					  : selection_idx;
+		if (ed->cursor_idx != ed->selection_anchor) {
+			int sel_min = (ed->cursor_idx < ed->selection_anchor)
+					  ? ed->cursor_idx
+					  : ed->selection_anchor;
+			int sel_max = (ed->cursor_idx > ed->selection_anchor)
+					  ? ed->cursor_idx
+					  : ed->selection_anchor;
 
 			SDL_SetRenderDrawColor(renderer, 200, 220, 255,
 					       255); // Light Blue
 
 			int cur_row = 0, cur_col = 0;
-			const char *p = text_buffer;
-			while (p < text_buffer + text_length) {
-				int start_of_char_idx = (int)(p - text_buffer);
+			const char *p = ed->buffer;
+			while (p < ed->buffer + ed->length) {
+				int start_of_char_idx = (int)(p - ed->buffer);
 
 				if (start_of_char_idx >= sel_min &&
 				    start_of_char_idx < sel_max) {
@@ -367,17 +207,17 @@ int main(int argc, char *argv[]) {
 					SDL_StepUTF8(&p, NULL);
 					cur_col++;
 				}
-				if ((int)(p - text_buffer) > sel_max)
+				if ((int)(p - ed->buffer) > sel_max)
 					break;
 			}
 		}
 
-		if (font && text_length > 0) {
+		if (font && ed->length > 0) {
 			SDL_Color black = {0, 0, 0, 255};
 
 			float current_y = 20.0f - scroll_y;
 
-			const char *line_start = text_buffer;
+			const char *line_start = ed->buffer;
 			while (line_start != NULL && *line_start != '\0') {
 				const char *line_end = strchr(line_start, '\n');
 				size_t len =
@@ -411,8 +251,8 @@ int main(int argc, char *argv[]) {
 		}
 
 		int cur_row = 0, cur_col = 0;
-		const char *ptr = text_buffer;
-		while (ptr < text_buffer + cursor_idx) {
+		const char *ptr = ed->buffer;
+		while (ptr < ed->buffer + ed->cursor_idx) {
 			if (*ptr == '\n') {
 				cur_row++;
 				cur_col = 0;
@@ -437,6 +277,7 @@ int main(int argc, char *argv[]) {
 		SDL_RenderPresent(renderer);
 	}
 
+	editor_destroy(ed);
 	TTF_CloseFont(font);
 	SDL_DestroyRenderer(renderer);
 	SDL_DestroyWindow(window);