esc
Externally Scriptable Editor
git clone git://mccd.space/esc
| Log | Files | Refs | README |
commit f9ff11d6c910dc2551fb92ab0e66656b7a782a00 parent 46f5c673e7eba7f5bec9a4f7c55ef753607282dc Author: Marc Coquand <marc@coquand.email> Date: Mon, 23 Feb 2026 16:29:50 +0100 * Diffstat:
| M | fuse_ipc.c | | | 251 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------- |
| M | main.c | | | 13 | +++++++++++++ |
2 files changed, 225 insertions(+), 39 deletions(-)
diff --git a/fuse_ipc.c b/fuse_ipc.c
@@ -20,9 +20,22 @@ typedef struct {
size_t cap;
} WriteBuffer;
+/*
+ * In-memory temp file slot. Used for the create→write→rename workflow
+ * that tools like `sed -i` perform. Slots are identified by FUSE path;
+ * an empty path[0] means the slot is free.
+ */
+#define MAX_TEMP_FILES 8
+typedef struct {
+ char path[256];
+ char *data;
+ size_t len;
+} TempFile;
+
/* FUSE private data passed as private_data to fuse_new */
typedef struct {
- Editor *ed;
+ Editor *ed;
+ TempFile temps[MAX_TEMP_FILES];
} FuseCtx;
struct FuseIPC {
@@ -38,6 +51,49 @@ static FuseCtx *get_ctx(void)
return (FuseCtx *)fuse_get_context()->private_data;
}
+/* ---- temp-file helpers ------------------------------------------------ */
+
+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)
+ return &ctx->temps[i];
+ }
+ return NULL;
+}
+
+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;
+ return &ctx->temps[i];
+ }
+ }
+ return NULL;
+}
+
+static void free_temp(TempFile *tf)
+{
+ free(tf->data);
+ 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)
+{
+ SDL_Event ev;
+ memset(&ev, 0, sizeof(ev));
+ ev.type = SDL_EVENT_USER;
+ SDL_PushEvent(&ev);
+}
+
/* ---- path helpers ---------------------------------------------------- */
static int is_dir(const char *path)
@@ -121,31 +177,42 @@ static int op_getattr(const char *path, struct stat *st)
return 0;
}
- if (!is_file(path))
- return -ENOENT;
+ if (is_file(path)) {
+ st->st_mode = S_IFREG | 0644;
+ st->st_nlink = 1;
- st->st_mode = S_IFREG | 0644;
- st->st_nlink = 1;
+ editor_lock(ed);
- 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);
+ }
- 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;
}
- editor_unlock(ed);
- return 0;
+ /* In-flight or committed temp files */
+ TempFile *tf = find_temp(ctx, path);
+ if (tf) {
+ st->st_mode = S_IFREG | 0644;
+ st->st_nlink = 1;
+ st->st_size = (off_t)tf->len;
+ return 0;
+ }
+
+ return -ENOENT;
}
static int op_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
@@ -179,8 +246,13 @@ static int op_open(const char *path, struct fuse_file_info *fi)
{
fi->fh = 0;
- if (!is_file(path))
+ if (!is_file(path)) {
+ /* Allow opening an existing temp file for reading */
+ FuseCtx *ctx = get_ctx();
+ if (find_temp(ctx, path))
+ return 0;
return -ENOENT;
+ }
if (!is_writable(path)) {
if ((fi->flags & O_ACCMODE) != O_RDONLY)
@@ -199,6 +271,37 @@ static int op_open(const char *path, struct fuse_file_info *fi)
return 0;
}
+/*
+ * 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)
+{
+ (void)mode;
+ FuseCtx *ctx = get_ctx();
+
+ if (is_file(path) || is_dir(path))
+ return -EEXIST;
+
+ if (strncmp(path, "/buffer/0/", 10) != 0)
+ return -EACCES;
+
+ TempFile *tf = find_temp(ctx, path);
+ if (!tf) {
+ tf = alloc_temp(ctx, path);
+ if (!tf)
+ return -ENOSPC;
+ }
+
+ 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)
{
@@ -305,6 +408,10 @@ static int op_truncate(const char *path, off_t size)
/* Accept truncate on writable files; we reset on open anyway */
if (is_writable(path))
return 0;
+ /* Also accept on temp files */
+ FuseCtx *ctx = get_ctx();
+ if (find_temp(ctx, path))
+ return 0;
return -EACCES;
}
@@ -320,26 +427,47 @@ 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)
+{
+ editor_lock(ed);
+ 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->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)
{
FuseCtx *ctx = get_ctx();
Editor *ed = ctx->ed;
if (!fi->fh)
- return 0; /* read-only open — nothing to apply */
+ return 0;
WriteBuffer *wb = (WriteBuffer *)(uintptr_t)fi->fh;
+ fi->fh = 0;
+
+ /*
+ * Temp file: save the accumulated data into the TempFile slot so
+ * op_rename can pick it up. Do NOT apply to the editor yet.
+ */
+ TempFile *tf = find_temp(ctx, path);
+ if (tf) {
+ free(tf->data);
+ tf->data = wb->data; /* steal */
+ tf->len = wb->len;
+ wb->data = NULL;
+ free(wb);
+ return 0;
+ }
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);
+ apply_body(ed, wb->data, wb->len);
} else if (strcmp(path, "/cursor") == 0 &&
wb->data && wb->len > 0) {
@@ -355,11 +483,40 @@ static int op_release(const char *path, struct fuse_file_info *fi)
ed->selection_anchor = anchor;
ed->cursor_idx = cursor_pos;
editor_unlock(ed);
+ wake_render();
}
free(wb->data);
free(wb);
- fi->fh = 0;
+ 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)
+{
+ FuseCtx *ctx = get_ctx();
+ Editor *ed = ctx->ed;
+
+ TempFile *tf = find_temp(ctx, from);
+ if (!tf)
+ return -ENOENT;
+
+ if (strcmp(to, "/buffer/0/body") == 0)
+ apply_body(ed, tf->data, tf->len);
+
+ free_temp(tf);
+ return 0;
+}
+
+static int op_unlink(const char *path)
+{
+ FuseCtx *ctx = get_ctx();
+ TempFile *tf = find_temp(ctx, path);
+ if (!tf)
+ return -ENOENT;
+ free_temp(tf);
return 0;
}
@@ -377,11 +534,14 @@ 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,
};
/* ---- Public API ------------------------------------------------------ */
@@ -409,12 +569,25 @@ FuseIPC *fuse_ipc_start(Editor *ed)
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;
+
+ if (mkdir(ipc->mountpoint, 0700) != 0) {
+ if (errno == EEXIST) {
+ /*
+ * Stale mount from a previous crash — detach it so
+ * we can reuse the directory.
+ */
+ char cmd[512];
+ snprintf(cmd, sizeof(cmd),
+ "fusermount -u -- %s 2>/dev/null",
+ ipc->mountpoint);
+ system(cmd); /* ignore errors */
+ } else {
+ 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);
diff --git a/main.c b/main.c
@@ -12,6 +12,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
+#include <signal.h>
#include <unistd.h>
void handle_events(Editor *ed, SDL_Renderer *renderer, float *scroll_x,
@@ -508,11 +509,23 @@ void render_editor(SDL_Renderer *renderer, Editor *ed, TTF_Font *font,
SDL_RenderPresent(renderer);
}
+static void sig_quit(int sig)
+{
+ (void)sig;
+ SDL_Event ev;
+ memset(&ev, 0, sizeof(ev));
+ ev.type = SDL_EVENT_QUIT;
+ SDL_PushEvent(&ev);
+}
+
int main(int argc, char *argv[]) {
// Otherwise wayland just wouldn't load.
SDL_SetHintWithPriority(SDL_HINT_VIDEO_DRIVER, "wayland",
SDL_HINT_OVERRIDE);
+ signal(SIGTERM, sig_quit);
+ signal(SIGINT, sig_quit);
+
SDL_Init(SDL_INIT_VIDEO);
TTF_Init();