esc

Externally Scriptable Editor

git clone git://mccd.space/esc

commit 60f3c809db781e20b5e3cd4f11b2b0c54c4fef36
parent 32216b80ea485ed35f4ada77807810c02f82b04d
Author: Marc Coquand <marc@coquand.email>
Date:   Mon, 23 Feb 2026 16:03:05 +0100

test IPC

Diffstat:
MREADME.md | 5+++--
Afuse_ipc.c | 463+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afuse_ipc.h | 8++++++++
Mmain.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);