diff options
| author | user <user@clank> | 2026-08-15 16:13:46 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-15 16:13:46 +0200 |
| commit | ee6288a7eadeb43c83a5ab78f6c65cffa4748a53 (patch) | |
| tree | 8fc137687f47a7f20f0a2766bca17cd53e0400dc /src | |
| parent | make coverage: a number, cheaply (diff) | |
| download | slosh-ee6288a7eadeb43c83a5ab78f6c65cffa4748a53.tar.gz slosh-ee6288a7eadeb43c83a5ab78f6c65cffa4748a53.tar.xz slosh-ee6288a7eadeb43c83a5ab78f6c65cffa4748a53.zip | |
sl0ppty: C-a e opens the config, in a pane, in your editor
The config watcher's whole point is the loop -- edit, save, watch the session
change under you, edit again -- and the loop had a hole in the middle of it:
you had to leave the session, or remember where the file lives, to start.
So: one keystroke opens `$EDITOR <config>` in a new pane, down rather than
across because a config file is lines and half the height beats half the
width for reading them. `editor` in the config beats $EDITOR, and vi is the
fallback, being the one editor a POSIX system must have -- a wrong guess you
can see and change beats a pane that opens empty.
The interesting part is what kind of pane it is. Panes given a command keep
their corpse (D14), which is right for a dev server and wrong for this: quit
the editor and you would be left dismissing a dead pane you opened thirty
seconds ago. Rather than gaming that policy by giving the pane no command --
which would be a lie in the one field dump-layout and re-run read -- panes can
now be *ephemeral*: a task rather than part of the session's shape. An
ephemeral pane keeps no corpse and is not written into a dumped layout, so a
restart does not reopen somebody's editor on a file they had finished with.
That concept is the reusable half of this commit; the keybinding is the part
you notice.
Diffstat (limited to 'src')
| -rw-r--r-- | src/app.c | 67 | ||||
| -rw-r--r-- | src/app.h | 3 | ||||
| -rw-r--r-- | src/cmd.c | 4 | ||||
| -rw-r--r-- | src/config.c | 9 | ||||
| -rw-r--r-- | src/config.h | 3 | ||||
| -rw-r--r-- | src/pane.c | 3 | ||||
| -rw-r--r-- | src/sl0ppty.h | 6 |
7 files changed, 90 insertions, 5 deletions
@@ -1294,10 +1294,22 @@ static void replace_child(node_t *parent, node_t *old, node_t *new_) { } } +/* The pane a split makes. NULL argv means "whatever this session runs", which + * is the ordinary case; a command makes it a task pane instead. */ +static node_t *split_node_with(app_t *a, node_t *leaf, split_dir_t dir, + bool before, const char *const *argv, + const char *label); + static void split_node(app_t *a, node_t *leaf, split_dir_t dir, bool before) { - if (!leaf) return; - node_t *fresh = leaf_new(a); - if (!fresh) return; + split_node_with(a, leaf, dir, before, NULL, NULL); +} + +static node_t *split_node_with(app_t *a, node_t *leaf, split_dir_t dir, + bool before, const char *const *argv, + const char *label) { + if (!leaf) return NULL; + node_t *fresh = argv ? leaf_new_ex(a, argv, NULL, false, label) : leaf_new(a); + if (!fresh) return NULL; /* Growing an existing split in the same direction keeps the tree flat, so * three vertical splits are three equal columns rather than 1/2 + 1/4 + 1/4. */ @@ -1332,6 +1344,7 @@ static void split_node(app_t *a, node_t *leaf, split_dir_t dir, bool before) { cur(a)->focus = fresh; layout(a); + return fresh; } /* Would splitting this pane produce two panes worth having? @@ -1448,6 +1461,10 @@ struct reap { * empty for a pane that is just a shell -- which is the whole distinction, and * it happens to already be recorded. */ static bool keep_corpse(const pane_t *p) { + /* A pane opened to do one thing is done when that thing ends, whatever the + * policy says about commands: you asked for an editor, not for a record of + * having had one. */ + if (pane_ephemeral(p)) return false; switch (CFG.keep_dead) { case KEEP_DEAD_ALL: return true; case KEEP_DEAD_NONE: return false; @@ -1690,6 +1707,40 @@ bool app_close_pane(app_t *a, uint32_t id) { * the run that ended stays above the new one in its scrollback. A suspended * pane is started rather than restarted, because "run the thing this pane is * for" is the same request either way. */ +/* Open the config in $EDITOR, in a pane of its own. + * + * The whole point is the loop: edit, save, watch the session change under you, + * edit again. Anything that makes you leave the session to do it -- another + * terminal, or remembering where the file lives -- is friction in the middle + * of the one workflow the config watcher exists for. + * + * The pane is ephemeral: quit the editor and it is gone. You asked for an + * editor, not for a record of having had one. */ +bool app_edit_config(app_t *a) { + node_t *n = a->ntabs ? cur(a)->focus : NULL; + if (!n) return false; + + const char *editor = CFG.editor && *CFG.editor ? CFG.editor : getenv("EDITOR"); + /* vi is the one editor a POSIX system is required to have. Better a wrong + * guess you can see and change than a pane that opens empty. */ + if (!editor || !*editor) editor = "vi"; + + char cmd[1024]; + snprintf(cmd, sizeof cmd, "%s '%s'", editor, config_default_path()); + const char *argv[] = {"/bin/sh", "-c", cmd, NULL}; + + /* Down rather than across: a config file is lines, and half the width of a + * pane is a worse place to read them than half the height. */ + if (!split_fits(n, SPLIT_ROWS)) { + app_toast(a, "no room to split down"); + return false; + } + node_t *fresh = split_node_with(a, n, SPLIT_ROWS, false, argv, cmd); + if (!fresh) return false; + pane_set_ephemeral(fresh->pane, true); + return true; +} + bool app_rerun_pane(app_t *a, uint32_t id) { node_t *n = id ? pane_by_id(a, id) : (a->ntabs ? cur(a)->focus : NULL); if (!n) return false; @@ -4013,6 +4064,9 @@ static bool prefix_command(app_t *a, const input_event_t *ev) { a->query[0] = 0; a->sel = 0; return true; + case ACT_EDIT_CONFIG: + app_edit_config(a); + return true; case ACT_HELP: a->help = !a->help; return true; @@ -4336,8 +4390,11 @@ static void dump_node(app_t *a, node_t *n, strbuf_t *b, int depth) { char cwdbuf[4096]; sb_quoted(b, "cwd", live_cwd(n->pane, cwdbuf, sizeof cwdbuf)); /* The label is the command a layout gave it; a pane running the session's - * default shell has none, and gets none back. */ - sb_quoted(b, "command", pane_label(n->pane)); + * default shell has none, and gets none back. An ephemeral pane is a task + * that happened to be open when the dump was taken — restoring it would + * reopen somebody's editor on a file they finished with. */ + if (!pane_ephemeral(n->pane)) + sb_quoted(b, "command", pane_label(n->pane)); sb_quoted(b, "purpose", n->purpose); if (pane_suspended(n->pane)) sb_add(b, " suspended=true"); if (n == cur(a)->focus) sb_add(b, " focus=true"); @@ -114,6 +114,9 @@ bool app_close_pane(app_t *a, uint32_t id); /* Run a dead (or not-yet-started) pane's command again, in the same pane and * on top of the same scrollback. 0 means the focused one. */ bool app_rerun_pane(app_t *a, uint32_t id); +/* Open the config file in $EDITOR, in a pane of its own, so the edit/save/ + * watch loop happens without leaving the session. The pane is ephemeral. */ +bool app_edit_config(app_t *a); uint32_t app_focused_pane_id(app_t *a); uint32_t app_current_tab_id(app_t *a); @@ -146,6 +146,10 @@ static char *cmd_json(app_t *a, screen_t *s, input_parser_t *in, return jerr("no such pane"); return jok_int("id", app_focused_pane_id(a)); } + if (strcmp(cmd, "edit-config") == 0) { + return app_edit_config(a) ? jok_int("id", app_focused_pane_id(a)) + : jerr("no room for another pane"); + } if (strcmp(cmd, "rerun") == 0) { if (!app_rerun_pane(a, (uint32_t)jv_geti(req, "id", 0))) return jerr("cannot run it again"); diff --git a/src/config.c b/src/config.c index f4076b7..877b225 100644 --- a/src/config.c +++ b/src/config.c @@ -30,6 +30,7 @@ static const struct { {"detach", ACT_DETACH}, {"quit", ACT_QUIT}, {"literal-prefix", ACT_LITERAL_PREFIX}, {"help", ACT_HELP}, + {"edit-config", ACT_EDIT_CONFIG}, }; static const struct { @@ -393,6 +394,7 @@ void config_defaults(config_t *c) { bind_add(c, GHOSTTY_KEY_SLASH, 0, ACT_HELP, false); bind_add(c, GHOSTTY_KEY_R, 0, ACT_RERUN, false); bind_add(c, GHOSTTY_KEY_Z, 0, ACT_ZOOM, false); + bind_add(c, GHOSTTY_KEY_E, 0, ACT_EDIT_CONFIG, false); bind_add(c, GHOSTTY_KEY_M, 0, ACT_MINIMIZE, false); bind_add(c, GHOSTTY_KEY_H, 0, ACT_FOCUS_LEFT, false); bind_add(c, GHOSTTY_KEY_L, 0, ACT_FOCUS_RIGHT, false); @@ -431,6 +433,7 @@ void config_free(config_t *c) { free(c->exprs); free(c->binds); free(c->shell); + free(c->editor); free(c->shader_dir); memset(c, 0, sizeof *c); } @@ -478,6 +481,7 @@ static const struct { {ACT_SCROLL_BOTTOM, "scroll", "back to the present"}, {ACT_HELP, "session", "this list"}, + {ACT_EDIT_CONFIG, "session", "edit the config"}, {ACT_DETACH, "session", "detach, leave it running"}, {ACT_QUIT, "session", "quit the session"}, {ACT_LITERAL_PREFIX, "session", "send the prefix itself"}, @@ -709,6 +713,11 @@ bool config_load(config_t *c, const char *path, char *err, size_t errcap) { free(c->shell); c->shell = strdup(sh); } + const char *ed = kdl_arg(kdl_child(root, "editor"), 0, NULL); + if (ed) { + free(c->editor); + c->editor = strdup(ed); + } /* Shader plugins, before any shader is named below: a `shaders` block may * use what one of these adds, and a name is looked up as it is parsed. The diff --git a/src/config.h b/src/config.h index 882b8b0..33ece43 100644 --- a/src/config.h +++ b/src/config.h @@ -64,6 +64,7 @@ typedef enum { ACT_DETACH, ACT_QUIT, ACT_HELP, + ACT_EDIT_CONFIG, ACT_LITERAL_PREFIX, ACT_SELECT_TAB_1, /* .. +8, so digits stay one entry each */ } action_t; @@ -282,6 +283,8 @@ typedef struct { /* what a new pane runs; NULL means $SHELL */ char *shell; + /* What `edit-config` opens the file with; NULL means $EDITOR, then vi. */ + char *editor; /* Where to look for shader plugins (`*.so`). NULL means the default: a * `shaders` directory beside the config file. Read before the `shaders` and @@ -58,6 +58,7 @@ struct pane { * a dead pane needs exactly what it was started with, and by then the pane * is the only thing that still knows. */ bool suspended; + bool ephemeral; char **argv; char *cwd; char label[128]; @@ -224,6 +225,8 @@ static void on_title_changed(GhosttyTerminal t, void *ud) { } bool pane_suspended(const pane_t *p) { return p->suspended; } +void pane_set_ephemeral(pane_t *p, bool yes) { p->ephemeral = yes; } +bool pane_ephemeral(const pane_t *p) { return p->ephemeral; } const char *pane_label(const pane_t *p) { return p->label; } /* Spawn what a suspended pane was created to run. */ diff --git a/src/sl0ppty.h b/src/sl0ppty.h index e117908..38d5f18 100644 --- a/src/sl0ppty.h +++ b/src/sl0ppty.h @@ -119,6 +119,12 @@ pane_t *pane_new(const char *const argv[], uint16_t cols, uint16_t rows, pane_t *pane_new_ex(const char *const argv[], uint16_t cols, uint16_t rows, const char *cwd, bool suspended, const char *label); bool pane_suspended(const pane_t *p); +/* A pane opened to do one thing, rather than one the session is made of: an + * editor you popped open, not the dev server a layout declared. It runs a + * command like any other, but when that command ends it is finished — so it + * keeps no corpse and is not written into a dumped layout. */ +void pane_set_ephemeral(pane_t *p, bool yes); +bool pane_ephemeral(const pane_t *p); const char *pane_label(const pane_t *p); /* The pid of the program in the pane, or -1. Used to ask the kernel where a * shell has got to, which is the only honest answer to "what is this pane's |
