esc

Externally Scriptable Editor

git clone git://mccd.space/esc

commit a88cbab0d1f560f8336cee6e055e003285381acf
parent f9ff11d6c910dc2551fb92ab0e66656b7a782a00
Author: Marc Coquand <marc@coquand.email>
Date:   Mon, 23 Feb 2026 17:12:43 +0100

*

Diffstat:
Mfuse_ipc.c | 250+++++++++++++++++++++++++++++++++++++++----------------------------------------
1 file changed, 122 insertions(+), 128 deletions(-)
diff --git a/fuse_ipc.c b/fuse_ipc.c
@@ -15,7 +15,7 @@
 
 /* Per-open write accumulator (stored in fi->fh cast to/from uintptr_t) */
 typedef struct {
-	char  *data;
+	char *data;
 	size_t len;
 	size_t cap;
 } WriteBuffer;
@@ -27,34 +27,32 @@ typedef struct {
  */
 #define MAX_TEMP_FILES 8
 typedef struct {
-	char   path[256];
-	char  *data;
+	char path[256];
+	char *data;
 	size_t len;
 } TempFile;
 
 /* FUSE private data passed as private_data to fuse_new */
 typedef struct {
-	Editor   *ed;
-	TempFile  temps[MAX_TEMP_FILES];
+	Editor *ed;
+	TempFile temps[MAX_TEMP_FILES];
 } FuseCtx;
 
 struct FuseIPC {
-	struct fuse	*fuse;
-	struct fuse_chan	*chan;
-	SDL_Thread	*thread;
-	char		 mountpoint[256];
-	FuseCtx		*ctx;
+	struct fuse *fuse;
+	struct fuse_chan *chan;
+	SDL_Thread *thread;
+	char mountpoint[256];
+	FuseCtx *ctx;
 };
 
-static FuseCtx *get_ctx(void)
-{
+static FuseCtx *get_ctx(void) {
 	return (FuseCtx *)fuse_get_context()->private_data;
 }
 
 /* ---- temp-file helpers ------------------------------------------------ */
 
-static TempFile *find_temp(FuseCtx *ctx, const char *path)
-{
+static TempFile *find_temp(FuseCtx *ctx, const char *path) {
 	for (int i = 0; i < MAX_TEMP_FILES; i++) {
 		if (ctx->temps[i].path[0] &&
 		    strcmp(ctx->temps[i].path, path) == 0)
@@ -63,31 +61,28 @@ static TempFile *find_temp(FuseCtx *ctx, const char *path)
 	return NULL;
 }
 
-static TempFile *alloc_temp(FuseCtx *ctx, const char *path)
-{
+static TempFile *alloc_temp(FuseCtx *ctx, const char *path) {
 	for (int i = 0; i < MAX_TEMP_FILES; i++) {
 		if (!ctx->temps[i].path[0]) {
 			strncpy(ctx->temps[i].path, path,
 				sizeof(ctx->temps[i].path) - 1);
 			ctx->temps[i].data = NULL;
-			ctx->temps[i].len  = 0;
+			ctx->temps[i].len = 0;
 			return &ctx->temps[i];
 		}
 	}
 	return NULL;
 }
 
-static void free_temp(TempFile *tf)
-{
+static void free_temp(TempFile *tf) {
 	free(tf->data);
-	tf->data   = NULL;
-	tf->len    = 0;
+	tf->data = NULL;
+	tf->len = 0;
 	tf->path[0] = '\0';
 }
 
 /* Push a wakeup event so SDL_WaitEvent unblocks and the frame is redrawn. */
-static void wake_render(void)
-{
+static void wake_render(void) {
 	SDL_Event ev;
 	memset(&ev, 0, sizeof(ev));
 	ev.type = SDL_EVENT_USER;
@@ -96,23 +91,23 @@ static void wake_render(void)
 
 /* ---- path helpers ---------------------------------------------------- */
 
-static int is_dir(const char *path)
-{
-	return strcmp(path, "/") == 0 ||
-	       strcmp(path, "/buffer") == 0 ||
+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)
-{
+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;
 }
 
-static int is_writable(const char *path)
-{
+static int is_writable(const char *path) {
 	return strcmp(path, "/cursor") == 0 ||
 	       strcmp(path, "/buffer/0/body") == 0;
 }
@@ -121,24 +116,23 @@ static int is_writable(const char *path)
 
 /* Build a malloc'd text representation of ed->ranges.
  * Caller must hold editor lock. */
-static char *build_ranges(Editor *ed, size_t *out_len)
-{
+static char *build_ranges(Editor *ed, size_t *out_len) {
 	size_t total = 0;
-	char   tmp[128];
+	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);
+			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);
+			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);
@@ -150,51 +144,51 @@ static char *build_ranges(Editor *ed, size_t *out_len)
 		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);
+				       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);
+				       r->start_byte, r->end_byte,
+				       r->data.replacement.visual_cols);
 	}
 	buf[pos] = '\0';
-	*out_len  = pos;
+	*out_len = pos;
 	return buf;
 }
 
 /* ---- FUSE callbacks -------------------------------------------------- */
 
-static int op_getattr(const char *path, struct stat *st)
-{
+static int op_getattr(const char *path, struct stat *st) {
 	memset(st, 0, sizeof(*st));
 	FuseCtx *ctx = get_ctx();
-	Editor  *ed  = ctx->ed;
+	Editor *ed = ctx->ed;
 
 	if (is_dir(path)) {
-		st->st_mode  = S_IFDIR | 0755;
+		st->st_mode = S_IFDIR | 0755;
 		st->st_nlink = 2;
 		return 0;
 	}
 
 	if (is_file(path)) {
-		st->st_mode  = S_IFREG | 0644;
+		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);
+			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;
+			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);
+			char *tmp = build_ranges(ed, &sz);
 			st->st_size = (off_t)sz;
 			free(tmp);
 		}
@@ -203,12 +197,21 @@ 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;
+	}
+
 	/* In-flight or committed temp files */
 	TempFile *tf = find_temp(ctx, path);
 	if (tf) {
-		st->st_mode  = S_IFREG | 0644;
+		st->st_mode = S_IFREG | 0644;
 		st->st_nlink = 1;
-		st->st_size  = (off_t)tf->len;
+		st->st_size = (off_t)tf->len;
 		return 0;
 	}
 
@@ -216,8 +219,7 @@ static int op_getattr(const char *path, struct stat *st)
 }
 
 static int op_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
-		      off_t offset, struct fuse_file_info *fi)
-{
+		      off_t offset, struct fuse_file_info *fi) {
 	(void)offset;
 	(void)fi;
 
@@ -226,6 +228,7 @@ static int op_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
 
 	if (strcmp(path, "/") == 0) {
 		filler(buf, "cursor", NULL, 0);
+		filler(buf, "cwd", NULL, 0);
 		filler(buf, "buffer", NULL, 0);
 		return 0;
 	}
@@ -242,8 +245,14 @@ static int op_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
 	return -ENOENT;
 }
 
-static int op_open(const char *path, struct fuse_file_info *fi)
-{
+static int op_readlink(const char *path, char *buf, size_t size) {
+	if (!is_symlink(path))
+		return -ENOENT;
+	snprintf(buf, size, "/proc/%d/cwd", (int)getpid());
+	return 0;
+}
+
+static int op_open(const char *path, struct fuse_file_info *fi) {
 	fi->fh = 0;
 
 	if (!is_file(path)) {
@@ -275,9 +284,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.
  */
-static int op_create(const char *path, mode_t mode,
-		     struct fuse_file_info *fi)
-{
+static int op_create(const char *path, mode_t mode, struct fuse_file_info *fi) {
 	(void)mode;
 	FuseCtx *ctx = get_ctx();
 
@@ -303,17 +310,16 @@ static int op_create(const char *path, mode_t mode,
 }
 
 static int op_read(const char *path, char *buf, size_t size, off_t offset,
-		   struct fuse_file_info *fi)
-{
+		   struct fuse_file_info *fi) {
 	(void)fi;
 	FuseCtx *ctx = get_ctx();
-	Editor  *ed  = ctx->ed;
+	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);
+		int n = snprintf(tmp, sizeof(tmp), "%d %d\n", ed->cursor_idx,
+				 ed->selection_anchor);
 		editor_unlock(ed);
 
 		if (offset >= (off_t)n)
@@ -357,7 +363,7 @@ static int op_read(const char *path, char *buf, size_t size, off_t offset,
 	if (strcmp(path, "/buffer/0/ranges") == 0) {
 		editor_lock(ed);
 		size_t rlen;
-		char  *rbuf = build_ranges(ed, &rlen);
+		char *rbuf = build_ranges(ed, &rlen);
 		editor_unlock(ed);
 
 		if (!rbuf)
@@ -378,22 +384,21 @@ static int op_read(const char *path, char *buf, size_t size, off_t offset,
 }
 
 static int op_write(const char *path, const char *buf, size_t size,
-		    off_t offset, struct fuse_file_info *fi)
-{
+		    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;
+	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);
+		char *nd = realloc(wb->data, new_cap);
 		if (!nd)
 			return -ENOMEM;
 		wb->data = nd;
-		wb->cap  = new_cap;
+		wb->cap = new_cap;
 	}
 
 	memcpy(wb->data + offset, buf, size);
@@ -402,8 +407,7 @@ static int op_write(const char *path, const char *buf, size_t size,
 	return (int)size;
 }
 
-static int op_truncate(const char *path, off_t 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))
@@ -416,8 +420,7 @@ static int op_truncate(const char *path, off_t size)
 }
 
 static int op_ftruncate(const char *path, off_t size,
-			struct fuse_file_info *fi)
-{
+			struct fuse_file_info *fi) {
 	(void)path;
 	if (!fi->fh)
 		return 0;
@@ -427,24 +430,23 @@ 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. */
-static void apply_body(Editor *ed, const char *data, size_t len)
-{
+/* Apply content to the editor and wake the render loop. Caller holds no lock.
+ */
+static void apply_body(Editor *ed, const char *data, size_t len) {
 	editor_lock(ed);
-	ed->ranges_count     = 0;
+	ed->ranges_count = 0;
 	strbuf_delete(&ed->text, 0, ed->text.len);
 	strbuf_insert(&ed->text, 0, data ? data : "", len);
-	ed->cursor_idx       = 0;
+	ed->cursor_idx = 0;
 	ed->selection_anchor = 0;
 	editor_unlock(ed);
 	editor_parse_ansi_codes(ed);
 	wake_render();
 }
 
-static int op_release(const char *path, struct fuse_file_info *fi)
-{
+static int op_release(const char *path, struct fuse_file_info *fi) {
 	FuseCtx *ctx = get_ctx();
-	Editor  *ed  = ctx->ed;
+	Editor *ed = ctx->ed;
 
 	if (!fi->fh)
 		return 0;
@@ -460,7 +462,7 @@ static int op_release(const char *path, struct fuse_file_info *fi)
 	if (tf) {
 		free(tf->data);
 		tf->data = wb->data; /* steal */
-		tf->len  = wb->len;
+		tf->len = wb->len;
 		wb->data = NULL;
 		free(wb);
 		return 0;
@@ -469,10 +471,9 @@ static int op_release(const char *path, struct fuse_file_info *fi)
 	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) {
+	} else if (strcmp(path, "/cursor") == 0 && wb->data && wb->len > 0) {
 		char *tmp = malloc(wb->len + 1);
-		int   anchor = 0, cursor_pos = 0;
+		int anchor = 0, cursor_pos = 0;
 		if (tmp) {
 			memcpy(tmp, wb->data, wb->len);
 			tmp[wb->len] = '\0';
@@ -481,7 +482,7 @@ static int op_release(const char *path, struct fuse_file_info *fi)
 		}
 		editor_lock(ed);
 		ed->selection_anchor = anchor;
-		ed->cursor_idx       = cursor_pos;
+		ed->cursor_idx = cursor_pos;
 		editor_unlock(ed);
 		wake_render();
 	}
@@ -491,13 +492,9 @@ static int op_release(const char *path, struct fuse_file_info *fi)
 	return 0;
 }
 
-/*
- * op_rename — the final step of `sed -i`: rename the temp file over body.
- */
-static int op_rename(const char *from, const char *to)
-{
+static int op_rename(const char *from, const char *to) {
 	FuseCtx *ctx = get_ctx();
-	Editor  *ed  = ctx->ed;
+	Editor *ed = ctx->ed;
 
 	TempFile *tf = find_temp(ctx, from);
 	if (!tf)
@@ -510,8 +507,7 @@ static int op_rename(const char *from, const char *to)
 	return 0;
 }
 
-static int op_unlink(const char *path)
-{
+static int op_unlink(const char *path) {
 	FuseCtx *ctx = get_ctx();
 	TempFile *tf = find_temp(ctx, path);
 	if (!tf)
@@ -522,8 +518,7 @@ static int op_unlink(const char *path)
 
 /* ---- FUSE thread ----------------------------------------------------- */
 
-static int fuse_thread_fn(void *arg)
-{
+static int fuse_thread_fn(void *arg) {
 	fuse_loop((struct fuse *)arg);
 	return 0;
 }
@@ -531,23 +526,23 @@ static int fuse_thread_fn(void *arg)
 /* ---- operations table ------------------------------------------------ */
 
 static const struct fuse_operations ops = {
-	.getattr   = op_getattr,
-	.readdir   = op_readdir,
-	.open      = op_open,
-	.create    = op_create,
-	.read      = op_read,
-	.write     = op_write,
-	.truncate  = op_truncate,
-	.ftruncate = op_ftruncate,
-	.release   = op_release,
-	.rename    = op_rename,
-	.unlink    = op_unlink,
+    .getattr = op_getattr,
+    .readdir = op_readdir,
+    .readlink = op_readlink,
+    .open = op_open,
+    .create = op_create,
+    .read = op_read,
+    .write = op_write,
+    .truncate = op_truncate,
+    .ftruncate = op_ftruncate,
+    .release = op_release,
+    .rename = op_rename,
+    .unlink = op_unlink,
 };
 
 /* ---- Public API ------------------------------------------------------ */
 
-FuseIPC *fuse_ipc_start(Editor *ed)
-{
+FuseIPC *fuse_ipc_start(Editor *ed) {
 	FuseIPC *ipc = calloc(1, sizeof(FuseIPC));
 	if (!ipc)
 		return NULL;
@@ -557,18 +552,18 @@ FuseIPC *fuse_ipc_start(Editor *ed)
 		free(ipc);
 		return NULL;
 	}
-	ctx->ed  = ed;
+	ctx->ed = ed;
 	ipc->ctx = ctx;
 
-	const char *xdg  = getenv("XDG_RUNTIME_DIR");
+	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());
+	snprintf(ipc->mountpoint, sizeof(ipc->mountpoint), "%s/esc/%d", base,
+		 (int)getpid());
 
 	if (mkdir(ipc->mountpoint, 0700) != 0) {
 		if (errno == EEXIST) {
@@ -578,8 +573,8 @@ FuseIPC *fuse_ipc_start(Editor *ed)
 			 */
 			char cmd[512];
 			snprintf(cmd, sizeof(cmd),
-				"fusermount -u -- %s 2>/dev/null",
-				ipc->mountpoint);
+				 "fusermount -u -- %s 2>/dev/null",
+				 ipc->mountpoint);
 			system(cmd); /* ignore errors */
 		} else {
 			SDL_Log("fuse_ipc: mkdir %s failed: %s",
@@ -626,8 +621,7 @@ FuseIPC *fuse_ipc_start(Editor *ed)
 	return ipc;
 }
 
-void fuse_ipc_stop(FuseIPC *ipc)
-{
+void fuse_ipc_stop(FuseIPC *ipc) {
 	if (!ipc)
 		return;
 	/* Unmount first: tears down the kernel-side channel, causing