esc

Externally Scriptable Editor

git clone git://mccd.space/esc

commit 1746c4ef0b62cf10fd5521e760246b0acb6880fe
parent dec7fd5d28f3b78b405271ac978d1fb4975f54b4
Author: Marc Coquand <marc@coquand.email>
Date:   Mon, 23 Feb 2026 15:41:53 +0100

*

Diffstat:
Meditor.c | 253++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Meditor.h | 4++++
Mmain.c | 5++++-
3 files changed, 196 insertions(+), 66 deletions(-)
diff --git a/editor.c b/editor.c
@@ -15,6 +15,12 @@ Editor *editor_create(int char_width, float line_height) {
 	ed->cursor_idx = 0;
 	ed->selection_anchor = 0;
 
+	ed->mutex = SDL_CreateMutex();
+	if (!ed->mutex) {
+		free(ed);
+		return NULL;
+	}
+
 	// Initialize ranges to zero/NULL
 	ed->ranges = NULL;
 	ed->ranges_count = 0;
@@ -30,12 +36,30 @@ void editor_destroy(Editor *ed) {
 	if (ed->filename)
 		free(ed->filename);
 	strbuf_free(&ed->text);
+	if (ed->mutex)
+		SDL_DestroyMutex(ed->mutex);
 	free(ed);
 }
 
-void editor_clear_ranges(Editor *ed) { ed->ranges_count = 0; }
+void editor_lock(Editor *ed) {
+	if (ed && ed->mutex)
+		SDL_LockMutex(ed->mutex);
+}
+
+void editor_unlock(Editor *ed) {
+	if (ed && ed->mutex)
+		SDL_UnlockMutex(ed->mutex);
+}
+
+void editor_clear_ranges(Editor *ed) {
+	editor_lock(ed);
+	ed->ranges_count = 0;
+	editor_unlock(ed);
+}
 
 bool editor_load_file(Editor *ed, const char *filename) {
+	editor_lock(ed);
+
 	if (!strbuf_read_file(&ed->text, filename))
 		return false;
 	if (ed->filename)
@@ -44,10 +68,14 @@ bool editor_load_file(Editor *ed, const char *filename) {
 	ed->cursor_idx = 0;
 	ed->selection_anchor = 0;
 	editor_clear_ranges(ed);
+
+	editor_unlock(ed);
 	return true;
 }
 
 bool editor_write_file(Editor *ed, const char *filename) {
+
+	editor_lock(ed);
 	const char *to_write;
 	if (filename)
 		to_write = filename;
@@ -56,9 +84,12 @@ bool editor_write_file(Editor *ed, const char *filename) {
 	} else {
 		return false;
 	}
-	return strbuf_write_file(&ed->text, filename);
+	bool res = strbuf_write_file(&ed->text, filename);
+	editor_unlock(ed);
+	return res;
 }
 static int utf8_char_to_byte_idx(const char *text, int char_idx) {
+
 	int byte_pos = 0;
 	int current_char = 0;
 	while (text[byte_pos] != '\0' && current_char < char_idx) {
@@ -77,6 +108,9 @@ static int utf8_char_to_byte_idx(const char *text, int char_idx) {
 }
 void editor_add_formatting_range(Editor *ed, int start_char, int end_char,
 				 RangeFormat data) {
+
+	editor_lock(ed);
+
 	int start = utf8_char_to_byte_idx(ed->text.data, start_char);
 	int end = utf8_char_to_byte_idx(ed->text.data, end_char);
 
@@ -99,10 +133,14 @@ void editor_add_formatting_range(Editor *ed, int start_char, int end_char,
 	}
 	ed->ranges[i] = r;
 	ed->ranges_count++;
+
+	editor_unlock(ed);
 }
 
 void editor_add_replace_range(Editor *ed, int start_char, int end_char,
 			      int visual_cols) {
+
+	editor_lock(ed);
 	int start = utf8_char_to_byte_idx(ed->text.data, start_char);
 	int end = utf8_char_to_byte_idx(ed->text.data, end_char);
 
@@ -122,6 +160,8 @@ void editor_add_replace_range(Editor *ed, int start_char, int end_char,
 	}
 	ed->ranges[i] = r;
 	ed->ranges_count++;
+
+	editor_unlock(ed);
 }
 
 EditorRange *editor_get_range_at(Editor *ed, int byte_idx) {
@@ -167,19 +207,28 @@ EditorRange *editor_get_range_ending_at(Editor *ed, int byte_idx) {
 }
 
 void editor_goto_pos(Editor *ed, int pos) {
+
+	editor_lock(ed);
 	if (pos > ed->text.len) {
 		ed->cursor_idx = (int)ed->text.len;
 	} else {
 		ed->cursor_idx = pos;
 	}
+	editor_unlock(ed);
 }
 
 void editor_select_all(Editor *ed) {
+
+	editor_lock(ed);
 	ed->selection_anchor = 0;
 	editor_goto_pos(ed, ed->text.len);
+
+	editor_unlock(ed);
 }
 
 void editor_insert_text(Editor *ed, const char *text, bool replace) {
+
+	editor_lock(ed);
 	if (replace && ed->cursor_idx != ed->selection_anchor) {
 		editor_delete_range(ed, ed->cursor_idx, ed->selection_anchor);
 	}
@@ -197,6 +246,8 @@ void editor_insert_text(Editor *ed, const char *text, bool replace) {
 		}
 	}
 
+	editor_unlock(ed);
+
 	strbuf_insert(&ed->text, ed->cursor_idx, text, input_len);
 	ed->cursor_idx += (int)input_len;
 	ed->selection_anchor = ed->cursor_idx;
@@ -205,6 +256,8 @@ void editor_insert_text(Editor *ed, const char *text, bool replace) {
 void editor_newline(Editor *ed) { editor_insert_text(ed, "\n", true); }
 
 void editor_delete_range(Editor *ed, int start, int end) {
+
+	editor_lock(ed);
 	if (start == end)
 		return;
 	if (start > end) {
@@ -249,9 +302,13 @@ void editor_delete_range(Editor *ed, int start, int end) {
 	strbuf_delete(&ed->text, start, len);
 	ed->cursor_idx = start;
 	ed->selection_anchor = start;
+
+	editor_unlock(ed);
 }
 
 void editor_delete_back(Editor *ed) {
+
+	editor_lock(ed);
 	if (ed->cursor_idx != ed->selection_anchor) {
 		editor_delete_range(ed, ed->selection_anchor, ed->cursor_idx);
 	} else if (ed->cursor_idx > 0) {
@@ -261,9 +318,13 @@ void editor_delete_back(Editor *ed) {
 		} while (prev > 0 && (ed->text.data[prev] & 0xC0) == 0x80);
 		editor_delete_range(ed, prev, ed->cursor_idx);
 	}
+
+	editor_unlock(ed);
 }
 
 void editor_delete_forward(Editor *ed) {
+
+	editor_lock(ed);
 	if (ed->cursor_idx != ed->selection_anchor) {
 		editor_delete_range(ed, ed->selection_anchor, ed->cursor_idx);
 	} else if (ed->cursor_idx < ed->text.len) {
@@ -273,9 +334,12 @@ void editor_delete_forward(Editor *ed) {
 		editor_delete_range(ed, ed->cursor_idx,
 				    (int)(next - ed->text.data));
 	}
+
+	editor_unlock(ed);
 }
 
 EditorRange *editor_get_replacement_at(Editor *ed, int byte_idx) {
+
 	for (size_t i = 0; i < ed->ranges_count; i++) {
 		EditorRange *r = &ed->ranges[i];
 		if (r->type == RANGE_REPLACEMENT && byte_idx >= r->start_byte &&
@@ -302,6 +366,8 @@ RangeFormat editor_get_format_at(Editor *ed, int byte_idx) {
 }
 
 void editor_cursor_left(Editor *ed) {
+
+	editor_lock(ed);
 	if (ed->cursor_idx > 0) {
 		// Are we standing at the end of a replacement?
 		// We need to look backwards to see if the previous byte is
@@ -317,9 +383,13 @@ void editor_cursor_left(Editor *ed) {
 			ed->cursor_idx = (int)(cur - ptr);
 		}
 	}
+
+	editor_unlock(ed);
 }
 
 void editor_cursor_right(Editor *ed) {
+
+	editor_lock(ed);
 	if (ed->cursor_idx < (int)ed->text.len) {
 		// Are we standing at the start of a replacement?
 		EditorRange *r = editor_get_replacement_at(ed, ed->cursor_idx);
@@ -331,13 +401,20 @@ void editor_cursor_right(Editor *ed) {
 			ed->cursor_idx = (int)(ptr - ed->text.data);
 		}
 	}
+	editor_unlock(ed);
 }
 
 void editor_clear_selection(Editor *ed) {
+
+	editor_lock(ed);
 	ed->selection_anchor = ed->cursor_idx;
+
+	editor_unlock(ed);
 }
 
 void editor_cursor_up(Editor *ed) {
+
+	editor_lock(ed);
 	int line_start = ed->cursor_idx;
 	while (line_start > 0 && ed->text.data[line_start - 1] != '\n')
 		line_start--;
@@ -368,9 +445,13 @@ void editor_cursor_up(Editor *ed) {
 			SDL_StepUTF8(&ptr, NULL);
 		ed->cursor_idx = (int)(ptr - ed->text.data);
 	}
+
+	editor_unlock(ed);
 }
 
 void editor_cursor_down(Editor *ed) {
+
+	editor_lock(ed);
 	int line_start = ed->cursor_idx;
 	while (line_start > 0 && ed->text.data[line_start - 1] != '\n')
 		line_start--;
@@ -398,10 +479,14 @@ void editor_cursor_down(Editor *ed) {
 		}
 		ed->cursor_idx = (int)(ptr - ed->text.data);
 	}
+
+	editor_unlock(ed);
 }
 
 void editor_set_cursor_from_coords(Editor *ed, float mx, float my,
 				   float scroll_x, float scroll_y) {
+
+	editor_lock(ed);
 	int target_col =
 	    (int)((mx - 20.0f + scroll_x + (ed->char_width / 2.0f)) /
 		  ed->char_width);
@@ -446,6 +531,8 @@ void editor_set_cursor_from_coords(Editor *ed, float mx, float my,
 		}
 	}
 	ed->cursor_idx = (int)(ptr - ed->text.data);
+
+	editor_unlock(ed);
 }
 
 static bool is_word_char(char c) {
@@ -456,6 +543,8 @@ static bool is_word_char(char c) {
 }
 
 void editor_goto_word_start(Editor *ed) {
+
+	editor_lock(ed);
 	if (ed->text.len == 0)
 		return;
 
@@ -469,9 +558,13 @@ void editor_goto_word_start(Editor *ed) {
 		start--;
 	}
 	ed->cursor_idx = start;
+
+	editor_unlock(ed);
 }
 
 void editor_select_word(Editor *ed) {
+
+	editor_lock(ed);
 	if (ed->text.len == 0)
 		return;
 
@@ -501,6 +594,8 @@ void editor_select_word(Editor *ed) {
 
 	ed->selection_anchor = start;
 	ed->cursor_idx = end;
+
+	editor_unlock(ed);
 }
 
 bool editor_has_selection(Editor *ed) {
@@ -529,70 +624,98 @@ char *editor_get_selection(Editor *ed) {
 }
 
 static int byte_to_utf8_char_idx(const char *text, int byte_limit) {
-    int char_idx = 0;
-    for (int i = 0; i < byte_limit && text[i] != '\0'; i++) {
-        if ((text[i] & 0xC0) != 0x80) char_idx++;
-    }
-    return char_idx;
+	int char_idx = 0;
+	for (int i = 0; i < byte_limit && text[i] != '\0'; i++) {
+		if ((text[i] & 0xC0) != 0x80)
+			char_idx++;
+	}
+	return char_idx;
 }
 
 void editor_parse_ansi_codes(Editor *ed) {
-    editor_clear_ranges(ed); 
-
-    const char *data = ed->text.data;
-    size_t len = ed->text.len;
-    
-    RangeFormat current_fmt = { .bold = false, .italic = false };
-    int last_segment_start_byte = 0;
-
-    for (int i = 0; i < (int)len; i++) {
-        if (data[i] == '\x1b' && i + 1 < len && data[i + 1] == '[') {
-            
-            if (i > last_segment_start_byte) {
-                if (current_fmt.bold || current_fmt.italic) {
-                    editor_add_formatting_range(ed, 
-                        byte_to_utf8_char_idx(data, last_segment_start_byte),
-                        byte_to_utf8_char_idx(data, i), 
-                        current_fmt);
-                }
-            }
-
-            int seq_start = i;
-            int seq_end = i + 2;
-            while (seq_end < len && data[seq_end] != 'm') {
-                seq_end++;
-            }
-
-            if (seq_end < len && data[seq_end] == 'm') {
-                for (int p = seq_start + 2; p < seq_end; ) {
-                    int val = atoi(&data[p]);
-                    if (val == 0) { current_fmt.bold = false; current_fmt.italic = false; }
-                    else if (val == 1) { current_fmt.bold = true; }
-                    else if (val == 3) { current_fmt.italic = true; }
-                    else if (val == 22) { current_fmt.bold = false; }
-                    else if (val == 23) { current_fmt.italic = false; }
-                    
-                    while (p < seq_end && (data[p] >= '0' && data[p] <= '9')) p++;
-                    if (p < seq_end && data[p] == ';') p++;
-                }
-
-                editor_add_replace_range(ed, 
-                    byte_to_utf8_char_idx(data, seq_start), 
-                    byte_to_utf8_char_idx(data, seq_end + 1), 
-                    0);
-
-                i = seq_end; 
-                last_segment_start_byte = i + 1;
-            }
-        }
-    }
-
-    if (last_segment_start_byte < len) {
-        if (current_fmt.bold || current_fmt.italic) {
-            editor_add_formatting_range(ed, 
-                byte_to_utf8_char_idx(data, last_segment_start_byte),
-                byte_to_utf8_char_idx(data, len), 
-                current_fmt);
-        }
-    }
+	editor_lock(ed);
+	ed->ranges_count = 0;
+
+	const char *data = ed->text.data;
+	size_t len = ed->text.len;
+
+	RangeFormat current_fmt = {.bold = false, .italic = false};
+	int last_segment_start_byte = 0;
+
+	for (int i = 0; i < (int)len; i++) {
+		if (data[i] == '\x1b' && i + 1 < len && data[i + 1] == '[') {
+
+			if (i > last_segment_start_byte) {
+				if (current_fmt.bold || current_fmt.italic) {
+					// We temporarily unlock because
+					// editor_add_formatting_range locks
+					editor_unlock(ed);
+					editor_add_formatting_range(
+					    ed,
+					    byte_to_utf8_char_idx(
+						data, last_segment_start_byte),
+					    byte_to_utf8_char_idx(data, i),
+					    current_fmt);
+					editor_lock(ed);
+					// Re-evaluate data ptr in case buffer
+					// moved (unlikely here but good
+					// practice)
+					data = ed->text.data;
+				}
+			}
+
+			int seq_start = i;
+			int seq_end = i + 2;
+			while (seq_end < len && data[seq_end] != 'm')
+				seq_end++;
+
+			if (seq_end < len && data[seq_end] == 'm') {
+				for (int p = seq_start + 2; p < seq_end;) {
+					int val = atoi(&data[p]);
+					if (val == 0) {
+						current_fmt.bold = false;
+						current_fmt.italic = false;
+					} else if (val == 1) {
+						current_fmt.bold = true;
+					} else if (val == 3) {
+						current_fmt.italic = true;
+					} else if (val == 22) {
+						current_fmt.bold = false;
+					} else if (val == 23) {
+						current_fmt.italic = false;
+					}
+
+					while (p < seq_end && (data[p] >= '0' &&
+							       data[p] <= '9'))
+						p++;
+					if (p < seq_end && data[p] == ';')
+						p++;
+				}
+
+				editor_unlock(ed);
+				editor_add_replace_range(
+				    ed, byte_to_utf8_char_idx(data, seq_start),
+				    byte_to_utf8_char_idx(data, seq_end + 1),
+				    0);
+				editor_lock(ed);
+				data = ed->text.data;
+
+				i = seq_end;
+				last_segment_start_byte = i + 1;
+			}
+		}
+	}
+
+	if (last_segment_start_byte < len) {
+		if (current_fmt.bold || current_fmt.italic) {
+			editor_unlock(ed);
+			editor_add_formatting_range(
+			    ed,
+			    byte_to_utf8_char_idx(data,
+						  last_segment_start_byte),
+			    byte_to_utf8_char_idx(data, len), current_fmt);
+			editor_lock(ed);
+		}
+	}
+	editor_unlock(ed);
 }
diff --git a/editor.h b/editor.h
@@ -40,11 +40,15 @@ typedef struct {
 	// Metrics for coordinate calculations
 	int char_width;
 	float line_height;
+
+	SDL_Mutex *mutex;
 } Editor;
 
 // Lifecycle
 Editor *editor_create(int char_width, float line_height);
 void editor_destroy(Editor *ed);
+void editor_lock(Editor *ed);
+void editor_unlock(Editor *ed);
 
 // Core Actions
 void editor_insert_text(Editor *ed, const char *text, bool replace);
diff --git a/main.c b/main.c
@@ -532,7 +532,7 @@ int main(int argc, char *argv[]) {
 	Editor *ed = editor_create(char_width, line_height);
 	if (argc > 1) {
 		editor_load_file(ed, argv[1]);
-		editor_parse_ansi_codes(ed); 
+		editor_parse_ansi_codes(ed);
 	}
 
 	float scroll_x = 0.0f;
@@ -545,8 +545,11 @@ int main(int argc, char *argv[]) {
 	while (running) {
 		handle_events(ed, renderer, &scroll_x, &scroll_y, line_height,
 			      &running, &is_dragging);
+
+		editor_lock(ed);
 		render_editor(renderer, ed, font, bold_font, char_width,
 			      line_height, cursor_height, scroll_x, scroll_y);
+		editor_unlock(ed);
 	}
 
 	editor_destroy(ed);