esc

Externally Scriptable Editor

git clone git://mccd.space/esc

commit 4464a7a4c89da905009dd867cd42c55e7ffa4ef8
parent 3bdbc2bce5fe2e46613b7b70c4d97e09964e5f72
Author: Marc Coquand <marc@coquand.email>
Date:   Sun, 22 Feb 2026 12:46:13 +0100

strbuf

Diffstat:
MREADME.md | 2+-
Meditor.c | 139+++++++++++++++++++++++++++----------------------------------------------------
Meditor.h | 5++---
Mmain.c | 33++++++++++++++++-----------------
Astrbuf.c | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Astrbuf.h | 22++++++++++++++++++++++
6 files changed, 164 insertions(+), 113 deletions(-)
diff --git a/README.md b/README.md
@@ -18,7 +18,7 @@ Esc (**E**xternally **Sc**riptable Editor) is an extensible text editor written 
 ## Compiling
 
 ```
-cc main.c unix_utils.* editor.* -o esc $(pkg-config --cflags --libs sdl3 sdl3-ttf)
+cc main.c unix_utils.* editor.* strbuf.* -o esc $(pkg-config --cflags --libs sdl3 sdl3-ttf)
 ```
 
 ## Inspiration
diff --git a/editor.c b/editor.c
@@ -1,4 +1,5 @@
 #include "editor.h"
+#include "strbuf.h"
 #include <ctype.h>
 #include <stdio.h>
 #include <stdlib.h>
@@ -9,10 +10,7 @@ Editor *editor_create(int char_width, float line_height) {
 	if (!ed)
 		return NULL;
 
-	ed->capacity = 1024;
-	ed->buffer = malloc(ed->capacity);
-	ed->buffer[0] = '\0';
-	ed->length = 0;
+	strbuf_init(&ed->text, 1024);
 	ed->cursor_idx = 0;
 	ed->selection_anchor = 0;
 
@@ -30,36 +28,15 @@ Editor *editor_create(int char_width, float line_height) {
 void editor_destroy(Editor *ed) {
 	if (ed->filename)
 		free(ed->filename);
-	free(ed->buffer);
+	strbuf_free(&ed->text);
 	free(ed);
 }
 
 void editor_clear_ranges(Editor *ed) { ed->ranges_count = 0; }
 
 bool editor_load_file(Editor *ed, const char *filename) {
-	FILE *f = fopen(filename, "rb");
-	if (!f)
+	if (!strbuf_read_file(&ed->text, filename))
 		return false;
-
-	fseek(f, 0, SEEK_END);
-	size_t size = ftell(f);
-	fseek(f, 0, SEEK_SET);
-
-	if (size + 1 > ed->capacity) {
-		ed->capacity = size + 1024;
-		char *new_buf = realloc(ed->buffer, ed->capacity);
-		if (!new_buf) {
-			fclose(f);
-			return false;
-		}
-		ed->buffer = new_buf;
-	}
-
-	fread(ed->buffer, 1, size, f);
-	ed->buffer[size] = '\0';
-	ed->length = size;
-	fclose(f);
-
 	if (ed->filename)
 		free(ed->filename);
 	ed->filename = strdup(filename);
@@ -78,12 +55,7 @@ bool editor_write_file(Editor *ed, const char *filename) {
 	} else {
 		return false;
 	}
-	FILE *f = fopen(ed->filename, "wb");
-	if (!f)
-		return false;
-	fwrite(ed->buffer, 1, ed->length, f);
-	fclose(f);
-	return true;
+	return strbuf_write_file(&ed->text, filename);
 }
 
 void editor_add_range(Editor *ed, int start, int end, int visual_cols) {
@@ -147,8 +119,8 @@ EditorRange *editor_get_range_ending_at(Editor *ed, int byte_idx) {
 }
 
 void editor_goto_pos(Editor *ed, int pos) {
-	if (pos > ed->length) {
-		ed->cursor_idx = (int)ed->length;
+	if (pos > ed->text.len) {
+		ed->cursor_idx = (int)ed->text.len;
 	} else {
 		ed->cursor_idx = pos;
 	}
@@ -156,7 +128,7 @@ void editor_goto_pos(Editor *ed, int pos) {
 
 void editor_select_all(Editor *ed) {
 	ed->selection_anchor = 0;
-	editor_goto_pos(ed, ed->length);
+	editor_goto_pos(ed, ed->text.len);
 }
 
 void editor_insert_text(Editor *ed, const char *text, bool replace) {
@@ -165,18 +137,6 @@ void editor_insert_text(Editor *ed, const char *text, bool replace) {
 	}
 
 	size_t input_len = strlen(text);
-	size_t required_size = ed->length + input_len + 1;
-
-	if (required_size > ed->capacity) {
-		// Keep doubling until we can fit the new text
-		while (required_size > ed->capacity) {
-			ed->capacity *= 2;
-		}
-		char *new_buf = realloc(ed->buffer, ed->capacity);
-		if (!new_buf)
-			return; // Handle OOM appropriately
-		ed->buffer = new_buf;
-	}
 
 	// Shift ranges affected by insertion
 	for (size_t i = 0; i < ed->ranges_count; i++) {
@@ -189,11 +149,7 @@ void editor_insert_text(Editor *ed, const char *text, bool replace) {
 		}
 	}
 
-	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;
+	strbuf_insert(&ed->text, ed->cursor_idx, text, input_len);
 	ed->cursor_idx += (int)input_len;
 	ed->selection_anchor = ed->cursor_idx;
 }
@@ -242,8 +198,7 @@ void editor_delete_range(Editor *ed, int start, int end) {
 	}
 	ed->ranges_count = new_count;
 
-	memmove(&ed->buffer[start], &ed->buffer[end], ed->length - end + 1);
-	ed->length -= (end - start);
+	strbuf_delete(&ed->text, start, len);
 	ed->cursor_idx = start;
 	ed->selection_anchor = start;
 }
@@ -255,7 +210,7 @@ void editor_delete_back(Editor *ed) {
 		int prev = ed->cursor_idx;
 		do {
 			prev--;
-		} while (prev > 0 && (ed->buffer[prev] & 0xC0) == 0x80);
+		} while (prev > 0 && (ed->text.data[prev] & 0xC0) == 0x80);
 		editor_delete_range(ed, prev, ed->cursor_idx);
 	}
 }
@@ -263,12 +218,12 @@ void editor_delete_back(Editor *ed) {
 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;
+	} else if (ed->cursor_idx < ed->text.len) {
+		const char *ptr = ed->text.data + ed->cursor_idx;
 		const char *next = ptr;
 		SDL_StepUTF8(&next, NULL);
 		editor_delete_range(ed, ed->cursor_idx,
-				    (int)(next - ed->buffer));
+				    (int)(next - ed->text.data));
 	}
 }
 
@@ -276,16 +231,16 @@ 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->text.data[ed->cursor_idx] & 0xC0) == 0x80)
 			ed->cursor_idx--;
 	}
 }
 
 void editor_cursor_right(Editor *ed) {
-	if (ed->cursor_idx < (int)ed->length) {
-		const char *next = ed->buffer + ed->cursor_idx;
+	if (ed->cursor_idx < (int)ed->text.len) {
+		const char *next = ed->text.data + ed->cursor_idx;
 		SDL_StepUTF8(&next, NULL);
-		ed->cursor_idx = (int)(next - ed->buffer);
+		ed->cursor_idx = (int)(next - ed->text.data);
 	}
 }
 
@@ -295,13 +250,13 @@ void editor_clear_selection(Editor *ed) {
 
 void editor_cursor_up(Editor *ed) {
 	int line_start = ed->cursor_idx;
-	while (line_start > 0 && ed->buffer[line_start - 1] != '\n')
+	while (line_start > 0 && ed->text.data[line_start - 1] != '\n')
 		line_start--;
 
 	int visual_col = 0;
-	const char *p = ed->buffer + line_start;
+	const char *p = ed->text.data + line_start;
 
-	while (p < ed->buffer + ed->cursor_idx) {
+	while (p < ed->text.data + ed->cursor_idx) {
 		if (*p == '\t') {
 			visual_col += TAB_SIZE - (visual_col % TAB_SIZE);
 			SDL_StepUTF8(&p, NULL);
@@ -315,25 +270,25 @@ void editor_cursor_up(Editor *ed) {
 		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')
+		       ed->text.data[prev_line_start - 1] != '\n')
 			prev_line_start--;
 
-		const char *ptr = ed->buffer + prev_line_start;
+		const char *ptr = ed->text.data + 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->cursor_idx = (int)(ptr - ed->text.data);
 	}
 }
 
 void editor_cursor_down(Editor *ed) {
 	int line_start = ed->cursor_idx;
-	while (line_start > 0 && ed->buffer[line_start - 1] != '\n')
+	while (line_start > 0 && ed->text.data[line_start - 1] != '\n')
 		line_start--;
 
 	int visual_col = 0;
-	const char *p = ed->buffer + line_start;
-	while (p < ed->buffer + ed->cursor_idx) {
+	const char *p = ed->text.data + line_start;
+	while (p < ed->text.data + ed->cursor_idx) {
 		if (*p == '\t') {
 			visual_col += TAB_SIZE - (visual_col % TAB_SIZE);
 			SDL_StepUTF8(&p, NULL);
@@ -343,16 +298,16 @@ void editor_cursor_down(Editor *ed) {
 		}
 	}
 
-	const char *next_line = strchr(ed->buffer + ed->cursor_idx, '\n');
+	const char *next_line = strchr(ed->text.data + ed->cursor_idx, '\n');
 	if (next_line) {
-		int target_idx = (int)(next_line - ed->buffer) + 1;
-		const char *ptr = ed->buffer + target_idx;
+		int target_idx = (int)(next_line - ed->text.data) + 1;
+		const char *ptr = ed->text.data + 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->cursor_idx = (int)(ptr - ed->text.data);
 	}
 }
 
@@ -364,11 +319,11 @@ void editor_set_cursor_from_coords(Editor *ed, float mx, float my,
 	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;
+	const char *ptr = ed->text.data;
+	const char *last_ptr = ed->text.data;
 
 	while (*ptr != '\0') {
-		int current_idx = (int)(ptr - ed->buffer);
+		int current_idx = (int)(ptr - ed->text.data);
 		if (cur_r == target_row && cur_c >= target_col)
 			break;
 
@@ -378,7 +333,7 @@ void editor_set_cursor_from_coords(Editor *ed, float mx, float my,
 		EditorRange *r = editor_get_range_at(ed, current_idx);
 		if (r) {
 			cur_c += r->visual_cols;
-			ptr = ed->buffer + r->end_byte; // Skip pointer ahead
+			ptr = ed->text.data + r->end_byte; // Skip pointer ahead
 			continue;
 		}
 
@@ -402,7 +357,7 @@ void editor_set_cursor_from_coords(Editor *ed, float mx, float my,
 			break;
 		}
 	}
-	ed->cursor_idx = (int)(ptr - ed->buffer);
+	ed->cursor_idx = (int)(ptr - ed->text.data);
 }
 
 static bool is_word_char(char c) {
@@ -413,46 +368,46 @@ static bool is_word_char(char c) {
 }
 
 void editor_goto_word_start(Editor *ed) {
-	if (ed->length == 0)
+	if (ed->text.len == 0)
 		return;
 
 	int start = ed->cursor_idx;
-	if (start > 0 && !is_word_char(ed->buffer[start])) {
-		if (is_word_char(ed->buffer[start - 1])) {
+	if (start > 0 && !is_word_char(ed->text.data[start])) {
+		if (is_word_char(ed->text.data[start - 1])) {
 			start--;
 		}
 	}
-	while (start > 0 && is_word_char(ed->buffer[start - 1])) {
+	while (start > 0 && is_word_char(ed->text.data[start - 1])) {
 		start--;
 	}
 	ed->cursor_idx = start;
 }
 
 void editor_select_word(Editor *ed) {
-	if (ed->length == 0)
+	if (ed->text.len == 0)
 		return;
 
 	int start = ed->cursor_idx;
 	int end = ed->cursor_idx;
 
-	if (start > 0 && !is_word_char(ed->buffer[start])) {
-		if (is_word_char(ed->buffer[start - 1])) {
+	if (start > 0 && !is_word_char(ed->text.data[start])) {
+		if (is_word_char(ed->text.data[start - 1])) {
 			start--;
 			end--;
 		}
 	}
 
 	// Only proceed if we are actually on a word character
-	if (!is_word_char(ed->buffer[start]))
+	if (!is_word_char(ed->text.data[start]))
 		return;
 
 	// Expand left
-	while (start > 0 && is_word_char(ed->buffer[start - 1])) {
+	while (start > 0 && is_word_char(ed->text.data[start - 1])) {
 		start--;
 	}
 
 	// Expand right
-	while (end < (int)ed->length && is_word_char(ed->buffer[end])) {
+	while (end < (int)ed->text.len && is_word_char(ed->text.data[end])) {
 		end++;
 	}
 
@@ -480,7 +435,7 @@ char *editor_get_selection(Editor *ed) {
 	if (!result)
 		return NULL;
 
-	memcpy(result, &ed->buffer[start], len);
+	memcpy(result, &ed->text.data[start], len);
 	result[len] = '\0';
 	return result;
 }
diff --git a/editor.h b/editor.h
@@ -4,6 +4,7 @@
 
 #include <SDL3/SDL.h>
 #include <stdbool.h>
+#include "strbuf.h"
 
 typedef struct {
 	int start_byte;
@@ -12,9 +13,7 @@ typedef struct {
 } EditorRange;
 
 typedef struct {
-	char *buffer;
-	size_t capacity;
-	size_t length;
+    	StrBuf text;
 	int cursor_idx;
 	int selection_anchor; // Where the selection started
 
diff --git a/main.c b/main.c
@@ -230,9 +230,9 @@ void render_editor(SDL_Renderer *renderer, Editor *ed, TTF_Font *font,
 				       255); // Light Blue
 
 		int cur_row = 0, cur_col = 0;
-		const char *p = ed->buffer;
-		while (p < ed->buffer + ed->length) {
-			int start_of_char_idx = (int)(p - ed->buffer);
+		const char *p = ed->text.data;
+		while (p < ed->text.data + ed->text.len) {
+			int start_of_char_idx = (int)(p - ed->text.data);
 
 			EditorRange *r =
 			    editor_get_range_at(ed, start_of_char_idx);
@@ -250,8 +250,8 @@ void render_editor(SDL_Renderer *renderer, Editor *ed, TTF_Font *font,
 					SDL_RenderFillRect(renderer, &sel_rect);
 				}
 				cur_col += r->visual_cols;
-				p = ed->buffer + r->end_byte;
-				if ((int)(p - ed->buffer) >= sel_max)
+				p = ed->text.data + r->end_byte;
+				if ((int)(p - ed->text.data) >= sel_max)
 					break;
 				continue;
 			}
@@ -279,16 +279,16 @@ void render_editor(SDL_Renderer *renderer, Editor *ed, TTF_Font *font,
 				cur_col += cols_for_char;
 			}
 
-			if ((int)(p - ed->buffer) >= sel_max)
+			if ((int)(p - ed->text.data) >= sel_max)
 				break;
 		}
 	}
 
-	if (font && ed->length > 0) {
+	if (font && ed->text.len > 0) {
 		SDL_Color black = {0, 0, 0, 255};
 		float current_y = 20.0f - scroll_y;
 
-		const char *line_start = ed->buffer;
+		const char *line_start = ed->text.data;
 		while (line_start != NULL && *line_start != '\0') {
 			const char *line_end = strchr(line_start, '\n');
 			size_t len = line_end ? (size_t)(line_end - line_start)
@@ -304,7 +304,7 @@ void render_editor(SDL_Renderer *renderer, Editor *ed, TTF_Font *font,
 
 				for (size_t i = 0; i < len; i++) {
 					size_t abs_idx =
-					    (line_start - ed->buffer) + i;
+					    (line_start - ed->text.data) + i;
 					EditorRange *r = editor_get_range_at(
 					    ed, (int)abs_idx);
 
@@ -319,11 +319,11 @@ void render_editor(SDL_Renderer *renderer, Editor *ed, TTF_Font *font,
 						// Safety handling if the fold
 						// bounds across the new line
 						if (r->end_byte >
-						    (line_start - ed->buffer) +
+						    (line_start - ed->text.data) +
 							len) {
 							const char
 							    *next_line_start =
-								ed->buffer +
+								ed->text.data +
 								r->end_byte;
 							line_end =
 							    next_line_start - 1;
@@ -331,7 +331,7 @@ void render_editor(SDL_Renderer *renderer, Editor *ed, TTF_Font *font,
 						} else {
 							i = (size_t)(r->end_byte -
 								     (line_start -
-								      ed->buffer)) -
+								      ed->text.data)) -
 							    1;
 						}
 						continue;
@@ -379,14 +379,14 @@ void render_editor(SDL_Renderer *renderer, Editor *ed, TTF_Font *font,
 	}
 
 	int cur_row = 0, cur_col = 0;
-	const char *ptr = ed->buffer;
-	while (ptr < ed->buffer + ed->cursor_idx) {
-		int current_idx = (int)(ptr - ed->buffer);
+	const char *ptr = ed->text.data;
+	while (ptr < ed->text.data + ed->cursor_idx) {
+		int current_idx = (int)(ptr - ed->text.data);
 		EditorRange *r = editor_get_range_at(ed, current_idx);
 
 		if (r) {
 			cur_col += r->visual_cols;
-			ptr = ed->buffer + r->end_byte;
+			ptr = ed->text.data + r->end_byte;
 			continue;
 		}
 
@@ -435,7 +435,6 @@ int main(int argc, char *argv[]) {
 	float cursor_height = (float)TTF_GetFontHeight(font);
 	Editor *ed = editor_create(char_width, line_height);
 	if (argc > 1) {
-    		setenv("ESC_BUFFILE", argv[1], true);
 		editor_load_file(ed, argv[1]);
 	}
 
diff --git a/strbuf.c b/strbuf.c
@@ -0,0 +1,76 @@
+#include "strbuf.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+void strbuf_init(StrBuf *sb, size_t init_cap) {
+	sb->cap = init_cap > 0 ? init_cap : 1024;
+	sb->data = malloc(sb->cap);
+	if (sb->data) sb->data[0] = '\0';
+	sb->len = 0;
+}
+
+void strbuf_free(StrBuf *sb) {
+	free(sb->data);
+	sb->data = NULL;
+	sb->len = 0;
+	sb->cap = 0;
+}
+
+static bool strbuf_ensure_cap(StrBuf *sb, size_t req_cap) {
+	if (req_cap <= sb->cap) return true;
+	size_t new_cap = sb->cap * 2;
+	while (new_cap < req_cap) new_cap *= 2;
+	
+	char *new_data = realloc(sb->data, new_cap);
+	if (!new_data) return false;
+	
+	sb->data = new_data;
+	sb->cap = new_cap;
+	return true;
+}
+
+bool strbuf_insert(StrBuf *sb, size_t pos, const char *text, size_t text_len) {
+	if (pos > sb->len) pos = sb->len;
+	if (!strbuf_ensure_cap(sb, sb->len + text_len + 1)) return false;
+	
+	memmove(sb->data + pos + text_len, sb->data + pos, sb->len - pos + 1);
+	memcpy(sb->data + pos, text, text_len);
+	sb->len += text_len;
+	return true;
+}
+
+void strbuf_delete(StrBuf *sb, size_t pos, size_t len) {
+	if (pos >= sb->len) return;
+	if (pos + len > sb->len) len = sb->len - pos;
+	
+	memmove(sb->data + pos, sb->data + pos + len, sb->len - pos - len + 1);
+	sb->len -= len;
+}
+
+bool strbuf_read_file(StrBuf *sb, const char *filename) {
+	FILE *f = fopen(filename, "rb");
+	if (!f) return false;
+	
+	fseek(f, 0, SEEK_END);
+	size_t size = ftell(f);
+	fseek(f, 0, SEEK_SET);
+	
+	if (!strbuf_ensure_cap(sb, size + 1)) {
+		fclose(f);
+		return false;
+	}
+	fread(sb->data, 1, size, f);
+	sb->data[size] = '\0';
+	sb->len = size;
+	fclose(f);
+	return true;
+}
+
+bool strbuf_write_file(const StrBuf *sb, const char *filename) {
+	FILE *f = fopen(filename, "wb");
+	if (!f) return false;
+	fwrite(sb->data, 1, sb->len, f);
+	fclose(f);
+	return true;
+}
diff --git a/strbuf.h b/strbuf.h
@@ -0,0 +1,22 @@
+#ifndef STRBUF_H
+#define STRBUF_H
+
+#include <stddef.h>
+#include <stdbool.h>
+
+typedef struct {
+	char *data;
+	size_t len;
+	size_t cap;
+} StrBuf;
+
+void strbuf_init(StrBuf *sb, size_t init_cap);
+void strbuf_free(StrBuf *sb);
+
+bool strbuf_insert(StrBuf *sb, size_t pos, const char *text, size_t text_len);
+void strbuf_delete(StrBuf *sb, size_t pos, size_t len);
+
+bool strbuf_read_file(StrBuf *sb, const char *filename);
+bool strbuf_write_file(const StrBuf *sb, const char *filename);
+
+#endif