esc
Externally Scriptable Editor
git clone git://mccd.space/esc
| Log | Files | Refs | README |
commit 60f3c809db781e20b5e3cd4f11b2b0c54c4fef36 parent 32216b80ea485ed35f4ada77807810c02f82b04d Author: Marc Coquand <marc@coquand.email> Date: Mon, 23 Feb 2026 16:03:05 +0100 test IPC Diffstat:
| M | README.md | | | 5 | +++-- |
| A | fuse_ipc.c | | | 463 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | fuse_ipc.h | | | 8 | ++++++++ |
| M | main.c | | | 4 | ++++ |
4 files changed, 478 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
@@ -17,8 +17,9 @@ Esc (**E**xternally **Sc**riptable Editor) is an extensible text editor written
## Compiling
-```
-cc main.c unix_utils.* editor.* strbuf.* -o esc $(pkg-config --cflags --libs sdl3 sdl3-ttf)
+```sh
+cc main.c unix_utils.* editor.* strbuf.* fuse_ipc.* -o esc \
+ $(pkg-config --cflags --libs sdl3 sdl3-ttf fuse)
```
## Inspiration
diff --git a/fuse_ipc.c b/fuse_ipc.c
@@ -0,0 +1,463 @@
+#define FUSE_USE_VERSION 26
+#include <fuse.h>
+
+#include "editor.h"
+#include "fuse_ipc.h"
+#include "strbuf.h"
+#include <SDL3/SDL.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+/* Per-open write accumulator (stored in fi->fh cast to/from uintptr_t) */
+typedef struct {
+ char *data;
+ size_t len;
+ size_t cap;
+} WriteBuffer;
+
+/* FUSE private data passed as private_data to fuse_new */
+typedef struct {
+ Editor *ed;
+} FuseCtx;
+
+struct FuseIPC {
+ struct fuse *fuse;
+ struct fuse_chan *chan;
+ SDL_Thread *thread;
+ char mountpoint[256];
+ FuseCtx *ctx;
+};
+
+static FuseCtx *get_ctx(void)
+{
+ return (FuseCtx *)fuse_get_context()->private_data;
+}
+
+/* ---- path helpers ---------------------------------------------------- */
+
+static int is_dir(const char *path)
+{
+ return strcmp(path, "/") == 0 ||
+ strcmp(path, "/buffer") == 0 ||
+ strcmp(path, "/buffer/0") == 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;
+}
+
+static int is_writable(const char *path)
+{
+ return strcmp(path, "/cursor") == 0 ||
+ strcmp(path, "/buffer/0/body") == 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)
+{
+ size_t total = 0;
+ char tmp[128];
+
+ for (size_t i = 0; i < ed->ranges_count; i++) {
+ EditorRange *r = &ed->ranges[i];
+ 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);
+ else
+ total += snprintf(tmp, sizeof(tmp),
+ "replacement %d %d %d\n",
+ r->start_byte, r->end_byte,
+ r->data.replacement.visual_cols);
+ }
+
+ char *buf = malloc(total + 1);
+ if (!buf)
+ return NULL;
+
+ size_t pos = 0;
+ for (size_t i = 0; i < ed->ranges_count; i++) {
+ EditorRange *r = &ed->ranges[i];
+ if (r->type == RANGE_FORMAT)
+ pos += sprintf(buf + pos, "format %d %d %d %d\n",
+ r->start_byte, r->end_byte,
+ 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,
+ r->data.replacement.visual_cols);
+ }
+ buf[pos] = '\0';
+ *out_len = pos;
+ return buf;
+}
+
+/* ---- FUSE callbacks -------------------------------------------------- */
+
+static int op_getattr(const char *path, struct stat *st)
+{
+ memset(st, 0, sizeof(*st));
+ FuseCtx *ctx = get_ctx();
+ Editor *ed = ctx->ed;
+
+ if (is_dir(path)) {
+ st->st_mode = S_IFDIR | 0755;
+ st->st_nlink = 2;
+ return 0;
+ }
+
+ if (!is_file(path))
+ return -ENOENT;
+
+ st->st_mode = S_IFREG | 0644;
+ st->st_nlink = 1;
+
+ editor_lock(ed);
+
+ 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/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);
+ }
+
+ editor_unlock(ed);
+ return 0;
+}
+
+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;
+
+ filler(buf, ".", NULL, 0);
+ filler(buf, "..", NULL, 0);
+
+ if (strcmp(path, "/") == 0) {
+ filler(buf, "cursor", NULL, 0);
+ filler(buf, "buffer", NULL, 0);
+ return 0;
+ }
+ if (strcmp(path, "/buffer") == 0) {
+ filler(buf, "0", NULL, 0);
+ return 0;
+ }
+ if (strcmp(path, "/buffer/0") == 0) {
+ filler(buf, "body", NULL, 0);
+ filler(buf, "path", NULL, 0);
+ filler(buf, "ranges", NULL, 0);
+ return 0;
+ }
+ return -ENOENT;
+}
+
+static int op_open(const char *path, struct fuse_file_info *fi)
+{
+ fi->fh = 0;
+
+ if (!is_file(path))
+ return -ENOENT;
+
+ if (!is_writable(path)) {
+ if ((fi->flags & O_ACCMODE) != O_RDONLY)
+ return -EACCES;
+ return 0;
+ }
+
+ WriteBuffer *wb = calloc(1, sizeof(WriteBuffer));
+ if (!wb)
+ return -ENOMEM;
+ fi->fh = (uint64_t)(uintptr_t)wb;
+ return 0;
+}
+
+static int op_read(const char *path, char *buf, size_t size, off_t offset,
+ struct fuse_file_info *fi)
+{
+ (void)fi;
+ FuseCtx *ctx = get_ctx();
+ Editor *ed = ctx->ed;
+
+ if (strcmp(path, "/cursor") == 0) {
+ editor_lock(ed);
+ char tmp[64];
+ int n = snprintf(tmp, sizeof(tmp), "%d %d\n",
+ ed->cursor_idx, ed->selection_anchor);
+ 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/body") == 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);
+ editor_unlock(ed);
+ return (int)to_copy;
+ }
+
+ if (strcmp(path, "/buffer/0/path") == 0) {
+ editor_lock(ed);
+ size_t flen = ed->filename ? strlen(ed->filename) : 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, 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 (!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 (int)to_copy;
+ }
+
+ return -ENOENT;
+}
+
+static int op_write(const char *path, const char *buf, size_t size,
+ off_t offset, struct fuse_file_info *fi)
+{
+ (void)path;
+ if (!fi->fh)
+ return -EACCES;
+
+ WriteBuffer *wb = (WriteBuffer *)(uintptr_t)fi->fh;
+ size_t end = (size_t)offset + size;
+
+ if (end > wb->cap) {
+ size_t new_cap = end < 128 ? 128 : end * 2;
+ char *nd = realloc(wb->data, new_cap);
+ if (!nd)
+ return -ENOMEM;
+ wb->data = nd;
+ wb->cap = new_cap;
+ }
+
+ memcpy(wb->data + offset, buf, size);
+ if (end > wb->len)
+ wb->len = end;
+ return (int)size;
+}
+
+static int op_truncate(const char *path, off_t size)
+{
+ (void)size;
+ /* Accept truncate on writable files; we reset on open anyway */
+ if (is_writable(path))
+ return 0;
+ return -EACCES;
+}
+
+static int op_ftruncate(const char *path, off_t size,
+ struct fuse_file_info *fi)
+{
+ (void)path;
+ if (!fi->fh)
+ return 0;
+ WriteBuffer *wb = (WriteBuffer *)(uintptr_t)fi->fh;
+ if ((size_t)size < wb->len)
+ wb->len = (size_t)size;
+ return 0;
+}
+
+static int op_release(const char *path, struct fuse_file_info *fi)
+{
+ FuseCtx *ctx = get_ctx();
+ Editor *ed = ctx->ed;
+
+ if (!fi->fh)
+ return 0; /* read-only open — nothing to apply */
+
+ WriteBuffer *wb = (WriteBuffer *)(uintptr_t)fi->fh;
+
+ if (strcmp(path, "/buffer/0/body") == 0) {
+ editor_lock(ed);
+ ed->ranges_count = 0;
+ strbuf_delete(&ed->text, 0, ed->text.len);
+ strbuf_insert(&ed->text, 0,
+ wb->data ? wb->data : "", wb->len);
+ ed->cursor_idx = 0;
+ ed->selection_anchor = 0;
+ editor_unlock(ed);
+ editor_parse_ansi_codes(ed);
+
+ } else if (strcmp(path, "/cursor") == 0 &&
+ wb->data && wb->len > 0) {
+ char *tmp = malloc(wb->len + 1);
+ int anchor = 0, cursor_pos = 0;
+ if (tmp) {
+ memcpy(tmp, wb->data, wb->len);
+ tmp[wb->len] = '\0';
+ sscanf(tmp, "%d %d", &anchor, &cursor_pos);
+ free(tmp);
+ }
+ editor_lock(ed);
+ ed->selection_anchor = anchor;
+ ed->cursor_idx = cursor_pos;
+ editor_unlock(ed);
+ }
+
+ free(wb->data);
+ free(wb);
+ fi->fh = 0;
+ return 0;
+}
+
+/* ---- FUSE thread ----------------------------------------------------- */
+
+static int fuse_thread_fn(void *arg)
+{
+ fuse_loop((struct fuse *)arg);
+ return 0;
+}
+
+/* ---- operations table ------------------------------------------------ */
+
+static const struct fuse_operations ops = {
+ .getattr = op_getattr,
+ .readdir = op_readdir,
+ .open = op_open,
+ .read = op_read,
+ .write = op_write,
+ .truncate = op_truncate,
+ .ftruncate = op_ftruncate,
+ .release = op_release,
+};
+
+/* ---- Public API ------------------------------------------------------ */
+
+FuseIPC *fuse_ipc_start(Editor *ed)
+{
+ FuseIPC *ipc = calloc(1, sizeof(FuseIPC));
+ if (!ipc)
+ return NULL;
+
+ FuseCtx *ctx = calloc(1, sizeof(FuseCtx));
+ if (!ctx) {
+ free(ipc);
+ return NULL;
+ }
+ ctx->ed = ed;
+ ipc->ctx = ctx;
+
+ const char *xdg = getenv("XDG_RUNTIME_DIR");
+ const char *base = xdg ? xdg : "/tmp";
+
+ char esc_dir[240];
+ snprintf(esc_dir, sizeof(esc_dir), "%s/esc", base);
+ mkdir(esc_dir, 0700); /* ignore EEXIST */
+
+ snprintf(ipc->mountpoint, sizeof(ipc->mountpoint),
+ "%s/esc/%d", base, (int)getpid());
+ if (mkdir(ipc->mountpoint, 0700) != 0 && errno != EEXIST) {
+ SDL_Log("fuse_ipc: mkdir %s failed: %s",
+ ipc->mountpoint, strerror(errno));
+ free(ctx);
+ free(ipc);
+ return NULL;
+ }
+
+ struct fuse_args args = FUSE_ARGS_INIT(0, NULL);
+
+ ipc->chan = fuse_mount(ipc->mountpoint, &args);
+ if (!ipc->chan) {
+ SDL_Log("fuse_ipc: fuse_mount failed");
+ rmdir(ipc->mountpoint);
+ free(ctx);
+ free(ipc);
+ return NULL;
+ }
+
+ ipc->fuse = fuse_new(ipc->chan, &args, &ops, sizeof(ops), ctx);
+ if (!ipc->fuse) {
+ SDL_Log("fuse_ipc: fuse_new failed");
+ fuse_unmount(ipc->mountpoint, ipc->chan);
+ rmdir(ipc->mountpoint);
+ free(ctx);
+ free(ipc);
+ return NULL;
+ }
+
+ ipc->thread = SDL_CreateThread(fuse_thread_fn, "FuseIPC", ipc->fuse);
+ if (!ipc->thread) {
+ SDL_Log("fuse_ipc: SDL_CreateThread failed");
+ fuse_destroy(ipc->fuse);
+ fuse_unmount(ipc->mountpoint, ipc->chan);
+ rmdir(ipc->mountpoint);
+ free(ctx);
+ free(ipc);
+ return NULL;
+ }
+
+ SDL_Log("fuse_ipc: mounted at %s", ipc->mountpoint);
+ return ipc;
+}
+
+void fuse_ipc_stop(FuseIPC *ipc)
+{
+ if (!ipc)
+ return;
+ fuse_exit(ipc->fuse);
+ SDL_WaitThread(ipc->thread, NULL);
+ fuse_unmount(ipc->mountpoint, ipc->chan);
+ fuse_destroy(ipc->fuse);
+ rmdir(ipc->mountpoint);
+ free(ipc->ctx);
+ free(ipc);
+}
diff --git a/fuse_ipc.h b/fuse_ipc.h
@@ -0,0 +1,8 @@
+#ifndef FUSE_IPC_H
+#define FUSE_IPC_H
+#include "editor.h"
+
+typedef struct FuseIPC FuseIPC;
+FuseIPC *fuse_ipc_start(Editor *ed); /* returns NULL on failure (non-fatal) */
+void fuse_ipc_stop(FuseIPC *ipc);
+#endif
diff --git a/main.c b/main.c
@@ -1,4 +1,5 @@
#include "editor.h"
+#include "fuse_ipc.h"
#include "unix_utils.h"
#include <SDL3/SDL.h>
#include <SDL3/SDL_events.h>
@@ -534,6 +535,7 @@ int main(int argc, char *argv[]) {
editor_load_file(ed, argv[1]);
editor_parse_ansi_codes(ed);
}
+ FuseIPC *ipc = fuse_ipc_start(ed);
float scroll_x = 0.0f;
float scroll_y = 0.0f;
@@ -552,6 +554,8 @@ int main(int argc, char *argv[]) {
editor_unlock(ed);
}
+ if (ipc)
+ fuse_ipc_stop(ipc);
editor_destroy(ed);
TTF_CloseFont(font);
TTF_CloseFont(bold_font);