esc
Externally Scriptable Editor
git clone git://mccd.space/esc
| Log | Files | Refs | README |
commit 80f74a421b2da7299dca7e9e43244097a4296f05 parent d25eaacabce71e577e9c80c3673ab138cb746b45 Author: Marc Coquand <marc@coquand.email> Date: Fri, 27 Feb 2026 11:17:21 +0100 Add multi buffer edits Diffstat:
| M | editor.c | | | 213 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------- |
| M | editor.h | | | 28 | ++++++++++++++++++++++------ |
| M | fuse_ipc.c | | | 412 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------ |
| M | main.c | | | 7 | ++++--- |
| M | renderer.c | | | 16 | ++++++++++++++++ |
5 files changed, 548 insertions(+), 128 deletions(-)
diff --git a/editor.c b/editor.c
@@ -6,6 +6,10 @@
#include <stdlib.h>
#include <string.h>
+/* Forward declarations for file slot helpers (defined after lifecycle code) */
+static void slots_on_insert(Editor *ed, int pos, int len);
+static void slots_on_delete(Editor *ed, int pos, int len);
+
/* ---------- undo helpers (called with lock held) ----------------------- */
static void push_undo_delta(Editor *ed, UndoDelta d) {
@@ -54,6 +58,7 @@ static void delete_range_raw(Editor *ed, int start, int end) {
ed->ranges[new_count++] = r;
}
ed->ranges_count = new_count;
+ slots_on_delete(ed, start, len);
strbuf_delete(&ed->text, start, len);
ed->cursor_idx = start;
ed->selection_anchor = start;
@@ -128,7 +133,10 @@ Editor *editor_create(int char_width, float line_height) {
ed->ranges = NULL;
ed->ranges_count = 0;
ed->ranges_capacity = 0;
- ed->filename = NULL;
+
+ ed->files = NULL;
+ ed->files_count = 0;
+ ed->files_cap = 0;
ed->undo_stack = malloc(UNDO_MAX * sizeof(UndoDelta));
ed->undo_len = 0;
@@ -141,8 +149,9 @@ Editor *editor_create(int char_width, float line_height) {
}
void editor_destroy(Editor *ed) {
- if (ed->filename)
- free(ed->filename);
+ for (int i = 0; i < ed->files_count; i++)
+ free(ed->files[i].path);
+ free(ed->files);
strbuf_free(&ed->text);
if (ed->mutex)
SDL_DestroyMutex(ed->mutex);
@@ -172,45 +181,176 @@ void editor_clear_ranges(Editor *ed) {
editor_unlock(ed);
}
-bool editor_load_file(Editor *ed, const char *filename) {
+/* ---------- file slot helpers ------------------------------------------ */
+
+static void files_grow(Editor *ed) {
+ if (ed->files_count >= ed->files_cap) {
+ int new_cap = ed->files_cap == 0 ? 4 : ed->files_cap * 2;
+ ed->files = realloc(ed->files, new_cap * sizeof(FileSlot));
+ ed->files_cap = new_cap;
+ }
+}
+
+/* Shift all slot boundaries after a text insertion at pos. */
+static void slots_on_insert(Editor *ed, int pos, int len) {
+ for (int i = 0; i < ed->files_count; i++) {
+ FileSlot *s = &ed->files[i];
+ if (s->buf_start <= pos && pos <= s->buf_end) {
+ /* Cursor is inside this slot */
+ s->buf_end += len;
+ if (s->has_range)
+ s->range_end += len;
+ s->dirty = true;
+ } else if (s->buf_start > pos) {
+ s->buf_start += len;
+ s->buf_end += len;
+ }
+ }
+}
+
+/* Shift all slot boundaries after a text deletion of [pos, pos+len). */
+static void slots_on_delete(Editor *ed, int pos, int len) {
+ int del_end = pos + len;
+ for (int i = 0; i < ed->files_count; i++) {
+ FileSlot *s = &ed->files[i];
+ if (s->buf_end <= pos) {
+ /* slot entirely before deletion: no change */
+ } else if (s->buf_start >= del_end) {
+ /* slot entirely after deletion */
+ s->buf_start -= len;
+ s->buf_end -= len;
+ } else {
+ /* overlapping */
+ int overlap_start = s->buf_start > pos ? s->buf_start : pos;
+ int overlap_end = s->buf_end < del_end ? s->buf_end : del_end;
+ int removed = overlap_end - overlap_start;
+ s->buf_end -= removed;
+ if (s->has_range)
+ s->range_end -= removed;
+ if (s->buf_start > pos)
+ s->buf_start = pos;
+ s->dirty = true;
+ }
+ }
+}
+
+int editor_file_slot_at(Editor *ed, int byte_pos) {
+ for (int i = 0; i < ed->files_count; i++) {
+ if (byte_pos >= ed->files[i].buf_start &&
+ byte_pos <= ed->files[i].buf_end)
+ return i;
+ }
+ return ed->files_count - 1;
+}
+
+bool editor_open_file(Editor *ed, const char *filename) {
editor_lock(ed);
- if (!strbuf_read_file(&ed->text, filename)) {
+ /* Read file into a temporary strbuf */
+ StrBuf tmp;
+ strbuf_init(&tmp, 1024);
+ if (!strbuf_read_file(&tmp, filename)) {
+ strbuf_free(&tmp);
editor_unlock(ed);
return false;
}
- if (ed->filename)
- free(ed->filename);
- ed->filename = strdup(filename);
- ed->cursor_idx = 0;
- ed->selection_anchor = 0;
- ed->ranges_count = 0;
- for (int i = 0; i < ed->undo_len; i++)
- free(ed->undo_stack[i].reinsert);
- ed->undo_len = 0;
- for (int i = 0; i < ed->redo_len; i++)
- free(ed->redo_stack[i].reinsert);
- ed->redo_len = 0;
+ int insert_pos = (int)ed->text.len;
+ /* Append a newline separator between slots (except for the first) */
+ if (ed->files_count > 0 && insert_pos > 0 &&
+ ed->text.data[insert_pos - 1] != '\n') {
+ strbuf_insert(&ed->text, insert_pos, "\n", 1);
+ slots_on_insert(ed, insert_pos, 1);
+ insert_pos++;
+ }
+ files_grow(ed);
+ FileSlot *s = &ed->files[ed->files_count];
+ s->path = filename ? strdup(filename) : NULL;
+ s->buf_start = insert_pos;
+ s->dirty = false;
+ s->has_range = false;
+ s->range_start = 0;
+ s->range_end = 0;
+
+ strbuf_insert(&ed->text, insert_pos, tmp.data, tmp.len);
+ s->buf_end = insert_pos + (int)tmp.len;
+ ed->files_count++;
+
+ strbuf_free(&tmp);
editor_unlock(ed);
return true;
}
-bool editor_write_file(Editor *ed, const char *filename) {
+bool editor_add_file_slot(Editor *ed, const char *path, const char *data, int len) {
+ editor_lock(ed);
+
+ int insert_pos = (int)ed->text.len;
+ if (ed->files_count > 0 && insert_pos > 0 &&
+ ed->text.data[insert_pos - 1] != '\n') {
+ strbuf_insert(&ed->text, insert_pos, "\n", 1);
+ slots_on_insert(ed, insert_pos, 1);
+ insert_pos++;
+ }
+
+ files_grow(ed);
+ FileSlot *s = &ed->files[ed->files_count];
+ s->path = path ? strdup(path) : NULL;
+ s->buf_start = insert_pos;
+ s->dirty = false;
+ s->has_range = false;
+ s->range_start = 0;
+ s->range_end = 0;
+
+ if (data && len > 0)
+ strbuf_insert(&ed->text, insert_pos, data, len);
+ s->buf_end = insert_pos + (len > 0 ? len : 0);
+ ed->files_count++;
+
+ editor_unlock(ed);
+ return true;
+}
+void editor_set_file_slot_range(Editor *ed, int idx, int start, int end) {
editor_lock(ed);
- const char *to_write;
- if (filename)
- to_write = filename;
- else if (ed->filename) {
- filename = ed->filename;
- } else {
- return false;
+ if (idx >= 0 && idx < ed->files_count) {
+ ed->files[idx].has_range = true;
+ ed->files[idx].range_start = start;
+ ed->files[idx].range_end = end;
}
- bool res = strbuf_write_file(&ed->text, filename);
editor_unlock(ed);
- return res;
+}
+
+bool editor_write_file(Editor *ed, const char *filename) {
+ editor_lock(ed);
+
+ bool any = false;
+ if (filename) {
+ /* Write full buffer to explicit path (legacy / single-file) */
+ bool res = strbuf_write_file(&ed->text, filename);
+ editor_unlock(ed);
+ return res;
+ }
+
+ /* Save all dirty slots */
+ for (int i = 0; i < ed->files_count; i++) {
+ FileSlot *s = &ed->files[i];
+ if (!s->dirty || !s->path)
+ continue;
+ int slot_len = s->buf_end - s->buf_start;
+ /* Write only the slot's bytes */
+ StrBuf tmp;
+ strbuf_init(&tmp, slot_len + 1);
+ strbuf_insert(&tmp, 0, ed->text.data + s->buf_start, slot_len);
+ if (strbuf_write_file(&tmp, s->path)) {
+ s->dirty = false;
+ any = true;
+ }
+ strbuf_free(&tmp);
+ }
+
+ editor_unlock(ed);
+ return any;
}
static int utf8_char_to_byte_idx(const char *text, int char_idx) {
@@ -406,6 +546,9 @@ void editor_insert_text(Editor *ed, const char *text, bool replace) {
}
}
+ // Shift file slot boundaries
+ slots_on_insert(ed, ed->cursor_idx, (int)input_len);
+
editor_unlock(ed);
strbuf_insert(&ed->text, ed->cursor_idx, text, input_len);
@@ -456,6 +599,13 @@ 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) {
+ /* Hard boundary: cannot backspace across a file slot boundary */
+ for (int i = 1; i < ed->files_count; i++) {
+ if (ed->cursor_idx == ed->files[i].buf_start) {
+ editor_unlock(ed);
+ return;
+ }
+ }
int prev = ed->cursor_idx;
do {
prev--;
@@ -471,7 +621,14 @@ 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) {
+ } else if (ed->cursor_idx < (int)ed->text.len) {
+ /* Hard boundary: cannot delete-forward across a file slot boundary */
+ for (int i = 0; i < ed->files_count - 1; i++) {
+ if (ed->cursor_idx == ed->files[i].buf_end) {
+ editor_unlock(ed);
+ return;
+ }
+ }
const char *ptr = ed->text.data + ed->cursor_idx;
const char *next = ptr;
SDL_StepUTF8(&next, NULL);
diff --git a/editor.h b/editor.h
@@ -19,7 +19,7 @@ typedef struct {
typedef enum { RANGE_REPLACEMENT, RANGE_FORMAT } EditorRangeType;
typedef struct {
- int visual_cols;
+ int visual_cols;
} RangeReplacement;
typedef struct {
@@ -38,16 +38,27 @@ typedef struct {
} EditorRange;
typedef struct {
+ char *path; // Heap-allocated file path (NULL if unnamed)
+ int buf_start; // Byte offset in text where this slot starts
+ int buf_end; // Byte offset in text where this slot ends (exclusive)
+ bool dirty; // Unsaved changes in this slot
+ bool has_range; // Whether an external range has been set
+ int range_start; // Fixed: original-file byte where visible region starts
+ int range_end; // Dynamic: original-file byte where visible region ends
+} FileSlot;
+
+typedef struct {
StrBuf text;
int cursor_idx;
int selection_anchor; // Where the selection started
- char *filename;
EditorRange *ranges;
size_t ranges_count;
size_t ranges_capacity;
- bool dirty;
+ FileSlot *files;
+ int files_count;
+ int files_cap;
UndoDelta *undo_stack;
int undo_len;
@@ -74,17 +85,22 @@ void editor_delete_back(Editor *ed);
void editor_delete_forward(Editor *ed);
void editor_clear_selection(Editor *ed);
void editor_delete_range(Editor *ed, int start, int end);
-bool editor_load_file(Editor *ed, const char *filename);
+bool editor_open_file(Editor *ed, const char *filename);
bool editor_write_file(Editor *ed, const char *filename);
void editor_replace_body(Editor *ed, const char *data, size_t len);
void editor_undo(Editor *ed);
void editor_redo(Editor *ed);
void editor_add_formatting_range(Editor *ed, int start, int end, RangeFormat format);
-void editor_add_replace_range(Editor *ed, int start, int end, int visual_cols);
-EditorRange* editor_get_replacement_at(Editor *ed, int byte_idx);
+void editor_add_replace_range(Editor *ed, int start, int end, int visual_cols);
+EditorRange* editor_get_replacement_at(Editor *ed, int byte_idx);
RangeFormat editor_get_format_at(Editor *ed, int byte_idx);
void editor_parse_ansi_codes(Editor *ed);
+// File slot management
+bool editor_add_file_slot(Editor *ed, const char *path, const char *data, int len);
+void editor_set_file_slot_range(Editor *ed, int idx, int start, int end);
+int editor_file_slot_at(Editor *ed, int byte_pos);
+
// Cursor Movement
void editor_cursor_left(Editor *ed);
void editor_cursor_right(Editor *ed);
diff --git a/fuse_ipc.c b/fuse_ipc.c
@@ -91,46 +91,101 @@ static void wake_render(void) {
/* ---- path helpers ---------------------------------------------------- */
+/*
+ * Parse /buffer/N/sub paths.
+ * Returns slot index (>=0) and sets *sub to the sub-file name.
+ * Returns -2 for /buffer itself.
+ * Returns -3 for /buffer/N (slot directory).
+ * Returns -1 if not a /buffer/... path.
+ */
+static int parse_buffer_path(const char *path, const char **sub) {
+ if (strcmp(path, "/buffer") == 0) {
+ if (sub) *sub = NULL;
+ return -2;
+ }
+ if (strncmp(path, "/buffer/", 8) != 0)
+ return -1;
+ const char *rest = path + 8;
+ /* rest should be N or N/sub */
+ char *slash = strchr(rest, '/');
+ int idx = atoi(rest);
+ if (idx < 0)
+ return -1;
+ /* Validate that 'rest' is really a number */
+ const char *p = rest;
+ if (*p == '-') p++;
+ if (*p < '0' || *p > '9')
+ return -1;
+ if (!slash) {
+ if (sub) *sub = NULL;
+ return -3; /* slot directory */
+ }
+ if (sub) *sub = slash + 1;
+ return idx;
+}
+
static int is_dir(const char *path) {
- return strcmp(path, "/") == 0 || strcmp(path, "/buffer") == 0 ||
- strcmp(path, "/buffer/0") == 0;
+ if (strcmp(path, "/") == 0 || strcmp(path, "/buffer") == 0)
+ return 1;
+ const char *sub = NULL;
+ int r = parse_buffer_path(path, &sub);
+ return r == -3; /* /buffer/N with no sub-file */
}
static int is_symlink(const char *path) { return strcmp(path, "/cwd") == 0; }
static int is_file(const char *path) {
- return strcmp(path, "/cursor") == 0 ||
- strcmp(path, "/buffer/0/body") == 0 ||
- strcmp(path, "/buffer/0/path") == 0 ||
- strcmp(path, "/buffer/0/ranges") == 0;
+ if (strcmp(path, "/cursor") == 0)
+ return 1;
+ if (strcmp(path, "/buffer/count") == 0)
+ return 1;
+ const char *sub = NULL;
+ int idx = parse_buffer_path(path, &sub);
+ if (idx < 0 || !sub)
+ return 0;
+ return strcmp(sub, "body") == 0 || strcmp(sub, "path") == 0 ||
+ strcmp(sub, "ranges") == 0 || strcmp(sub, "range") == 0;
}
static int is_writable(const char *path) {
- return strcmp(path, "/cursor") == 0 ||
- strcmp(path, "/buffer/0/body") == 0;
+ if (strcmp(path, "/cursor") == 0)
+ return 1;
+ const char *sub = NULL;
+ int idx = parse_buffer_path(path, &sub);
+ if (idx < 0 || !sub)
+ return 0;
+ return strcmp(sub, "body") == 0 || strcmp(sub, "path") == 0 ||
+ strcmp(sub, "range") == 0;
}
/* ---- ranges serialisation -------------------------------------------- */
-/* Build a malloc'd text representation of ed->ranges.
- * Caller must hold editor lock. */
-static char *build_ranges(Editor *ed, size_t *out_len) {
+/*
+ * Build a malloc'd text representation of ed->ranges filtered to
+ * [slot_start, slot_end). Pass slot_start=0, slot_end=INT_MAX for all.
+ * Offsets in output are relative to slot_start.
+ * Caller must hold editor lock.
+ */
+static char *build_ranges_filtered(Editor *ed, int slot_start, int slot_end,
+ size_t *out_len) {
size_t total = 0;
char tmp[128];
for (size_t i = 0; i < ed->ranges_count; i++) {
EditorRange *r = &ed->ranges[i];
+ if (r->end_byte <= slot_start || r->start_byte >= slot_end)
+ continue;
+ int s = r->start_byte - slot_start;
+ int e = r->end_byte - slot_start;
if (r->type == RANGE_FORMAT)
- total +=
- snprintf(tmp, sizeof(tmp), "format %d %d %d %d\n",
- r->start_byte, r->end_byte,
- r->data.format.bold ? 1 : 0,
- r->data.format.italic ? 1 : 0);
+ total += snprintf(tmp, sizeof(tmp),
+ "format %d %d %d %d\n", s, e,
+ r->data.format.bold ? 1 : 0,
+ r->data.format.italic ? 1 : 0);
else
- total +=
- snprintf(tmp, sizeof(tmp), "replacement %d %d %d\n",
- r->start_byte, r->end_byte,
- r->data.replacement.visual_cols);
+ total += snprintf(tmp, sizeof(tmp),
+ "replacement %d %d %d\n", s, e,
+ r->data.replacement.visual_cols);
}
char *buf = malloc(total + 1);
@@ -140,14 +195,17 @@ static char *build_ranges(Editor *ed, size_t *out_len) {
size_t pos = 0;
for (size_t i = 0; i < ed->ranges_count; i++) {
EditorRange *r = &ed->ranges[i];
+ if (r->end_byte <= slot_start || r->start_byte >= slot_end)
+ continue;
+ int s = r->start_byte - slot_start;
+ int e = r->end_byte - slot_start;
if (r->type == RANGE_FORMAT)
- pos += sprintf(buf + pos, "format %d %d %d %d\n",
- r->start_byte, r->end_byte,
+ pos += sprintf(buf + pos, "format %d %d %d %d\n", s, e,
r->data.format.bold ? 1 : 0,
r->data.format.italic ? 1 : 0);
else
pos += sprintf(buf + pos, "replacement %d %d %d\n",
- r->start_byte, r->end_byte,
+ s, e,
r->data.replacement.visual_cols);
}
buf[pos] = '\0';
@@ -155,6 +213,10 @@ static char *build_ranges(Editor *ed, size_t *out_len) {
return buf;
}
+static char *build_ranges(Editor *ed, size_t *out_len) {
+ return build_ranges_filtered(ed, 0, (int)ed->text.len + 1, out_len);
+}
+
/* ---- FUSE callbacks -------------------------------------------------- */
static int op_getattr(const char *path, struct stat *st) {
@@ -168,6 +230,15 @@ static int op_getattr(const char *path, struct stat *st) {
return 0;
}
+ if (is_symlink(path)) {
+ char tmp[64];
+ st->st_mode = S_IFLNK | 0777;
+ st->st_nlink = 1;
+ st->st_size =
+ snprintf(tmp, sizeof(tmp), "/proc/%d/cwd", (int)getpid());
+ return 0;
+ }
+
if (is_file(path)) {
st->st_mode = S_IFREG | 0644;
st->st_nlink = 1;
@@ -176,34 +247,48 @@ static int op_getattr(const char *path, struct stat *st) {
if (strcmp(path, "/cursor") == 0) {
char tmp[64];
+ st->st_size = snprintf(tmp, sizeof(tmp), "%d %d\n",
+ ed->cursor_idx,
+ ed->selection_anchor);
+ } else if (strcmp(path, "/buffer/count") == 0) {
+ char tmp[32];
st->st_size =
- snprintf(tmp, sizeof(tmp), "%d\n%d", ed->cursor_idx,
- ed->selection_anchor);
- } else if (strcmp(path, "/buffer/0/body") == 0) {
- st->st_size = (off_t)ed->text.len;
- } else if (strcmp(path, "/buffer/0/path") == 0) {
- st->st_size =
- ed->filename ? (off_t)strlen(ed->filename) : 0;
- } else if (strcmp(path, "/buffer/0/ranges") == 0) {
- size_t sz;
- char *tmp = build_ranges(ed, &sz);
- st->st_size = (off_t)sz;
- free(tmp);
+ snprintf(tmp, sizeof(tmp), "%d\n", ed->files_count);
+ } else {
+ const char *sub = NULL;
+ int idx = parse_buffer_path(path, &sub);
+ if (idx >= 0 && idx < ed->files_count && sub) {
+ FileSlot *s = &ed->files[idx];
+ if (strcmp(sub, "body") == 0) {
+ st->st_size =
+ (off_t)(s->buf_end - s->buf_start);
+ } else if (strcmp(sub, "path") == 0) {
+ st->st_size = s->path
+ ? (off_t)strlen(s->path)
+ : 0;
+ } else if (strcmp(sub, "ranges") == 0) {
+ size_t sz;
+ char *tmp = build_ranges_filtered(
+ ed, s->buf_start, s->buf_end, &sz);
+ st->st_size = (off_t)sz;
+ free(tmp);
+ } else if (strcmp(sub, "range") == 0) {
+ if (!s->has_range) {
+ editor_unlock(ed);
+ return -ENOENT;
+ }
+ char tmp[64];
+ st->st_size = snprintf(
+ tmp, sizeof(tmp), "%d %d\n",
+ s->range_start, s->range_end);
+ }
+ }
}
editor_unlock(ed);
return 0;
}
- if (is_symlink(path)) {
- char tmp[64];
- st->st_mode = S_IFLNK | 0777;
- st->st_nlink = 1;
- st->st_size =
- snprintf(tmp, sizeof(tmp), "/proc/%d/cwd", (int)getpid());
- return 0;
- }
-
/* In-flight or committed temp files */
TempFile *tf = find_temp(ctx, path);
if (tf) {
@@ -220,6 +305,8 @@ static int op_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
off_t offset, struct fuse_file_info *fi) {
(void)offset;
(void)fi;
+ FuseCtx *ctx = get_ctx();
+ Editor *ed = ctx->ed;
filler(buf, ".", NULL, 0);
filler(buf, "..", NULL, 0);
@@ -231,13 +318,36 @@ static int op_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
return 0;
}
if (strcmp(path, "/buffer") == 0) {
- filler(buf, "0", NULL, 0);
+ filler(buf, "count", NULL, 0);
+ editor_lock(ed);
+ int n = ed->files_count;
+ editor_unlock(ed);
+ char tmp[32];
+ for (int i = 0; i < n; i++) {
+ snprintf(tmp, sizeof(tmp), "%d", i);
+ filler(buf, tmp, NULL, 0);
+ }
return 0;
}
- if (strcmp(path, "/buffer/0") == 0) {
+ /* /buffer/N */
+ const char *sub = NULL;
+ int idx = parse_buffer_path(path, &sub);
+ if (idx == -3) {
filler(buf, "body", NULL, 0);
filler(buf, "path", NULL, 0);
filler(buf, "ranges", NULL, 0);
+ /* range only shown when has_range */
+ editor_lock(ed);
+ int slot_idx = -parse_buffer_path(path, NULL) - 3;
+ /* re-parse to get numeric index */
+ const char *rest = path + 8;
+ int sidx = atoi(rest);
+ bool hr = (sidx >= 0 && sidx < ed->files_count &&
+ ed->files[sidx].has_range);
+ editor_unlock(ed);
+ if (hr)
+ filler(buf, "range", NULL, 0);
+ (void)slot_idx;
return 0;
}
return -ENOENT;
@@ -280,7 +390,7 @@ static int op_open(const char *path, struct fuse_file_info *fi) {
/*
* op_create — called when a new file is created (e.g. sed -i's temp file).
- * We only allow creation inside /buffer/0/ and reject known permanent names.
+ * Allow creation inside any /buffer/N/ prefix.
*/
static int op_create(const char *path, mode_t mode, struct fuse_file_info *fi) {
(void)mode;
@@ -289,7 +399,10 @@ static int op_create(const char *path, mode_t mode, struct fuse_file_info *fi) {
if (is_file(path) || is_dir(path))
return -EEXIST;
- if (strncmp(path, "/buffer/0/", 10) != 0)
+ /* Must be under /buffer/N/ */
+ const char *sub = NULL;
+ int idx = parse_buffer_path(path, &sub);
+ if (idx < 0 || !sub)
return -EACCES;
TempFile *tf = find_temp(ctx, path);
@@ -329,52 +442,97 @@ static int op_read(const char *path, char *buf, size_t size, off_t offset,
return (int)to_copy;
}
- if (strcmp(path, "/buffer/0/body") == 0) {
+ if (strcmp(path, "/buffer/count") == 0) {
editor_lock(ed);
- if (offset >= (off_t)ed->text.len) {
- editor_unlock(ed);
- return 0;
- }
- size_t to_copy = ed->text.len - (size_t)offset;
- if (to_copy > size)
- to_copy = size;
- memcpy(buf, ed->text.data + offset, to_copy);
+ char tmp[32];
+ int n = snprintf(tmp, sizeof(tmp), "%d\n", ed->files_count);
editor_unlock(ed);
+ if (offset >= (off_t)n)
+ return 0;
+ size_t to_copy = (size_t)(n - offset);
+ if (to_copy > size) to_copy = size;
+ memcpy(buf, tmp + offset, to_copy);
return (int)to_copy;
}
- if (strcmp(path, "/buffer/0/path") == 0) {
+ /* /buffer/N/... */
+ const char *sub = NULL;
+ int idx = parse_buffer_path(path, &sub);
+ if (idx >= 0 && sub) {
editor_lock(ed);
- size_t flen = ed->filename ? strlen(ed->filename) : 0;
- if (offset >= (off_t)flen) {
+ if (idx >= ed->files_count) {
editor_unlock(ed);
- return 0;
+ return -ENOENT;
+ }
+ FileSlot *s = &ed->files[idx];
+
+ if (strcmp(sub, "body") == 0) {
+ int slot_len = s->buf_end - s->buf_start;
+ if (offset >= (off_t)slot_len) {
+ editor_unlock(ed);
+ return 0;
+ }
+ size_t to_copy = (size_t)(slot_len - offset);
+ if (to_copy > size) to_copy = size;
+ memcpy(buf, ed->text.data + s->buf_start + offset, to_copy);
+ editor_unlock(ed);
+ return (int)to_copy;
}
- size_t to_copy = flen - (size_t)offset;
- if (to_copy > size)
- to_copy = size;
- memcpy(buf, ed->filename + offset, to_copy);
- editor_unlock(ed);
- return (int)to_copy;
- }
- if (strcmp(path, "/buffer/0/ranges") == 0) {
- editor_lock(ed);
- size_t rlen;
- char *rbuf = build_ranges(ed, &rlen);
- editor_unlock(ed);
+ if (strcmp(sub, "path") == 0) {
+ size_t flen = s->path ? strlen(s->path) : 0;
+ if (offset >= (off_t)flen) {
+ editor_unlock(ed);
+ return 0;
+ }
+ size_t to_copy = flen - (size_t)offset;
+ if (to_copy > size) to_copy = size;
+ memcpy(buf, s->path + offset, to_copy);
+ editor_unlock(ed);
+ return (int)to_copy;
+ }
- if (!rbuf)
- return -ENOMEM;
- if (offset >= (off_t)rlen) {
+ if (strcmp(sub, "ranges") == 0) {
+ size_t rlen;
+ char *rbuf = build_ranges_filtered(ed, s->buf_start,
+ s->buf_end, &rlen);
+ editor_unlock(ed);
+ if (!rbuf) return -ENOMEM;
+ if (offset >= (off_t)rlen) { free(rbuf); return 0; }
+ size_t to_copy = rlen - (size_t)offset;
+ if (to_copy > size) to_copy = size;
+ memcpy(buf, rbuf + offset, to_copy);
free(rbuf);
- return 0;
+ return (int)to_copy;
}
- size_t to_copy = rlen - (size_t)offset;
- if (to_copy > size)
- to_copy = size;
- memcpy(buf, rbuf + offset, to_copy);
- free(rbuf);
+
+ if (strcmp(sub, "range") == 0) {
+ if (!s->has_range) {
+ editor_unlock(ed);
+ return -ENOENT;
+ }
+ char tmp[64];
+ int n = snprintf(tmp, sizeof(tmp), "%d %d\n",
+ s->range_start, s->range_end);
+ editor_unlock(ed);
+ if (offset >= (off_t)n) return 0;
+ size_t to_copy = (size_t)(n - offset);
+ if (to_copy > size) to_copy = size;
+ memcpy(buf, tmp + offset, to_copy);
+ return (int)to_copy;
+ }
+
+ editor_unlock(ed);
+ return -ENOENT;
+ }
+
+ /* temp files */
+ TempFile *tf = find_temp(ctx, path);
+ if (tf) {
+ if (offset >= (off_t)tf->len) return 0;
+ size_t to_copy = tf->len - (size_t)offset;
+ if (to_copy > size) to_copy = size;
+ memcpy(buf, tf->data + offset, to_copy);
return (int)to_copy;
}
@@ -428,13 +586,55 @@ static int op_ftruncate(const char *path, off_t size,
return 0;
}
-/* Apply content to the editor and wake the render loop. Caller holds no lock.
- */
+/* Apply content to the editor body (slot 0, legacy) and wake render loop. */
static void apply_body(Editor *ed, const char *data, size_t len) {
editor_replace_body(ed, data, len);
wake_render();
}
+/* Apply content to a specific slot by index. Creates slot if needed. */
+static void apply_slot_body(Editor *ed, int idx, const char *data, size_t len) {
+ editor_lock(ed);
+ if (idx == ed->files_count) {
+ /* Create a new slot */
+ editor_unlock(ed);
+ editor_add_file_slot(ed, NULL, data, (int)len);
+ } else if (idx < ed->files_count) {
+ /* Replace existing slot content */
+ FileSlot *s = &ed->files[idx];
+ int old_len = s->buf_end - s->buf_start;
+ /* Delete old content */
+ strbuf_delete(&ed->text, s->buf_start, old_len);
+ /* Adjust all slot boundaries as if a delete happened */
+ for (int i = 0; i < ed->files_count; i++) {
+ if (i == idx) continue;
+ if (ed->files[i].buf_start >= s->buf_end)
+ ed->files[i].buf_start -= old_len;
+ if (ed->files[i].buf_end > s->buf_start)
+ ed->files[i].buf_end -= old_len;
+ }
+ s->buf_end = s->buf_start;
+ /* Insert new content */
+ if (data && len > 0) {
+ strbuf_insert(&ed->text, s->buf_start, data, len);
+ /* Adjust boundaries after insert */
+ s->buf_end = s->buf_start + (int)len;
+ for (int i = 0; i < ed->files_count; i++) {
+ if (i == idx) continue;
+ if (ed->files[i].buf_start >= s->buf_start)
+ ed->files[i].buf_start += (int)len;
+ if (ed->files[i].buf_end > s->buf_start)
+ ed->files[i].buf_end += (int)len;
+ }
+ }
+ s->dirty = true;
+ editor_unlock(ed);
+ } else {
+ editor_unlock(ed);
+ }
+ wake_render();
+}
+
static int op_release(const char *path, struct fuse_file_info *fi) {
FuseCtx *ctx = get_ctx();
Editor *ed = ctx->ed;
@@ -459,10 +659,7 @@ static int op_release(const char *path, struct fuse_file_info *fi) {
return 0;
}
- if (strcmp(path, "/buffer/0/body") == 0) {
- apply_body(ed, wb->data, wb->len);
-
- } else if (strcmp(path, "/cursor") == 0 && wb->data && wb->len > 0) {
+ if (strcmp(path, "/cursor") == 0 && wb->data && wb->len > 0) {
char *tmp = malloc(wb->len + 1);
int cidx = 0, sidx = 0;
if (tmp) {
@@ -472,11 +669,42 @@ static int op_release(const char *path, struct fuse_file_info *fi) {
free(tmp);
}
editor_lock(ed);
- // XXX HANDLE OVERFLOW
ed->cursor_idx = cidx;
ed->selection_anchor = sidx;
editor_unlock(ed);
wake_render();
+ } else {
+ const char *sub = NULL;
+ int idx = parse_buffer_path(path, &sub);
+ if (idx >= 0 && sub) {
+ if (strcmp(sub, "body") == 0) {
+ apply_slot_body(ed, idx, wb->data, wb->len);
+ } else if (strcmp(sub, "path") == 0 && wb->data) {
+ char *p = malloc(wb->len + 1);
+ if (p) {
+ memcpy(p, wb->data, wb->len);
+ p[wb->len] = '\0';
+ editor_lock(ed);
+ if (idx < ed->files_count) {
+ free(ed->files[idx].path);
+ ed->files[idx].path = p;
+ } else {
+ free(p);
+ }
+ editor_unlock(ed);
+ }
+ } else if (strcmp(sub, "range") == 0 && wb->data) {
+ char *tmp = malloc(wb->len + 1);
+ if (tmp) {
+ memcpy(tmp, wb->data, wb->len);
+ tmp[wb->len] = '\0';
+ int rs = 0, re = 0;
+ sscanf(tmp, "%d %d", &rs, &re);
+ free(tmp);
+ editor_set_file_slot_range(ed, idx, rs, re);
+ }
+ }
+ }
}
free(wb->data);
@@ -492,8 +720,10 @@ static int op_rename(const char *from, const char *to) {
if (!tf)
return -ENOENT;
- if (strcmp(to, "/buffer/0/body") == 0)
- apply_body(ed, tf->data, tf->len);
+ const char *sub = NULL;
+ int idx = parse_buffer_path(to, &sub);
+ if (idx >= 0 && sub && strcmp(sub, "body") == 0)
+ apply_slot_body(ed, idx, tf->data, tf->len);
free_temp(tf);
return 0;
diff --git a/main.c b/main.c
@@ -418,8 +418,8 @@ int main(int argc, char *argv[]) {
float line_height = (float)TTF_GetFontHeight(font) * 1.5;
float cursor_height = (float)TTF_GetFontHeight(font);
Editor *ed = editor_create(char_width, line_height);
- if (argc > 1) {
- editor_load_file(ed, argv[1]);
+ for (int i = 1; i < argc; i++) {
+ editor_open_file(ed, argv[i]);
editor_parse_ansi_codes(ed);
}
FuseIPC *ipc = fuse_ipc_start(ed);
@@ -435,7 +435,8 @@ int main(int argc, char *argv[]) {
SelectionState sel = { .last_click_byte = -1, .stack_len = 0 };
#ifdef HAVE_TREESITTER
- sel.ts = ed->filename ? ts_state_create(ed->filename) : NULL;
+ sel.ts = (ed->files_count > 0 && ed->files[0].path)
+ ? ts_state_create(ed->files[0].path) : NULL;
#endif
SDL_StartTextInput(window);
diff --git a/renderer.c b/renderer.c
@@ -265,12 +265,28 @@ void render_ctx_init(RenderCtx *ctx, SDL_Renderer *renderer, TTF_Font *font,
ctx->margin = 20.0f;
}
+static void render_separators(RenderCtx *ctx, const Editor *ed)
+{
+ for (int i = 0; i < ed->files_count - 1; i++) {
+ int boundary_byte = ed->files[i].buf_end;
+ VisualPos vp = editor_byte_to_visual_pos(ed, boundary_byte);
+ float y = ctx->margin + vp.row * ctx->line_height
+ - ctx->scroll_y + ctx->line_height / 2.0f;
+ if (y >= 0 && y < (float)ctx->render_h) {
+ SDL_SetRenderDrawColor(ctx->renderer, 80, 80, 80, 255);
+ SDL_RenderLine(ctx->renderer, 0, y,
+ (float)ctx->render_w, y);
+ }
+ }
+}
+
void render_editor(RenderCtx *ctx, const Editor *ed)
{
SDL_GetRenderOutputSize(ctx->renderer, &ctx->render_w, &ctx->render_h);
SDL_SetRenderDrawColor(ctx->renderer, 255, 255, 255, 255);
SDL_RenderClear(ctx->renderer);
render_selection(ctx, ed);
+ render_separators(ctx, ed);
render_text(ctx, ed);
render_cursor(ctx, ed);
SDL_RenderPresent(ctx->renderer);