esc
Externally Scriptable Editor
git clone git://mccd.space/esc
| Log | Files | Refs | README |
commit 1746c4ef0b62cf10fd5521e760246b0acb6880fe parent dec7fd5d28f3b78b405271ac978d1fb4975f54b4 Author: Marc Coquand <marc@coquand.email> Date: Mon, 23 Feb 2026 15:41:53 +0100 * Diffstat:
| M | editor.c | | | 253 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------- |
| M | editor.h | | | 4 | ++++ |
| M | main.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);