esc
Externally Scriptable Editor
git clone git://mccd.space/esc
| Log | Files | Refs | README |
commit 4464a7a4c89da905009dd867cd42c55e7ffa4ef8 parent 3bdbc2bce5fe2e46613b7b70c4d97e09964e5f72 Author: Marc Coquand <marc@coquand.email> Date: Sun, 22 Feb 2026 12:46:13 +0100 strbuf Diffstat:
| M | README.md | | | 2 | +- |
| M | editor.c | | | 139 | +++++++++++++++++++++++++++---------------------------------------------------- |
| M | editor.h | | | 5 | ++--- |
| M | main.c | | | 33 | ++++++++++++++++----------------- |
| A | strbuf.c | | | 76 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | strbuf.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