diff options
| author | user <user@clank> | 2026-09-29 14:26:18 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-09-29 14:26:18 +0200 |
| commit | 43fa063445821470cbf55f85aa8116c11470396f (patch) | |
| tree | 1612fa68b3aa70bf75a9ec28a76cbd8d05f9264d /src | |
| parent | panes: $SLOSH_PANE, so a program knows which pane it is in (diff) | |
| download | slosh-43fa063445821470cbf55f85aa8116c11470396f.tar.gz slosh-43fa063445821470cbf55f85aa8116c11470396f.tar.xz slosh-43fa063445821470cbf55f85aa8116c11470396f.zip | |
panes: read and write a pane by id, without taking focus
`raw` wrote into the focused pane and `snapshot` composited the active tab,
so driving a pane anywhere else meant focusing it or selecting its tab
first. Both of those move the view of whoever is watching, and both lose to
anybody doing the same thing at the same time.
`raw` now takes an `id` (0 is the focused pane, as before), and `capture`
answers one pane's viewport as text -- from the pane's own terminal, so it
works for a pane in a background tab, changes no focus, and leaves the
human's selection alone. It builds its selection as a snapshot rather than
setting one on the terminal, because a read with a visible side effect is
not a read.
`capture` tells "no such pane" apart from "that pane is empty": a caller
polling a dead id would otherwise wait forever on a pane that is quiet
because it is gone.
Diffstat (limited to 'src')
| -rw-r--r-- | src/app_layout.c | 32 | ||||
| -rw-r--r-- | src/cmd.c | 32 | ||||
| -rw-r--r-- | src/pane.c | 49 |
3 files changed, 112 insertions, 1 deletions
diff --git a/src/app_layout.c b/src/app_layout.c index 4de1bf3..7979b08 100644 --- a/src/app_layout.c +++ b/src/app_layout.c @@ -559,6 +559,31 @@ void app_write_focused(app_t *a, const void *buf, size_t len) { if (cur(a)->focus) pane_write(cur(a)->focus->pane, buf, len); } +/* The same, into a pane named by id -- the one a caller can use without first + * stealing focus from whoever has it. `id` 0 means the focused pane, as + * everywhere. False when there is no such pane, so a script that names one + * that has gone hears about it instead of writing into a stranger's shell. */ +bool app_write_pane(app_t *a, uint32_t id, const void *buf, size_t len) { + if (!id) { + if (!cur(a)->focus) return false; + pane_write(cur(a)->focus->pane, buf, len); + return true; + } + node_t *n = pane_by_id(a, id); + if (!n || n->kind != NODE_LEAF) return false; + pane_write(n->pane, buf, len); + return true; +} + +/* One pane's viewport as text, by id (0 = focused). NULL when there is no such + * pane or nothing to read; caller frees. Reads the pane's own terminal, so a + * pane in a tab nobody is looking at answers exactly as one on screen does. */ +char *app_pane_text(app_t *a, uint32_t id) { + node_t *n = id ? pane_by_id(a, id) : cur(a)->focus; + if (!n || n->kind != NODE_LEAF) return NULL; + return pane_viewport_text(n->pane); +} + void app_resize(app_t *a, uint16_t cols, uint16_t rows) { a->cols = cols; a->rows = rows; @@ -1174,6 +1199,13 @@ bool app_pane_floating(app_t *a, uint32_t id) { return n && n->floating; } +/* Is there a pane with this id at all, in any tab. So a reply can tell "gone" + * apart from "empty", which are the same answer to a caller reading text. */ +bool app_pane_exists(app_t *a, uint32_t id) { + node_t *n = id ? pane_by_id(a, id) : (a->ntabs ? cur(a)->focus : NULL); + return n && n->kind == NODE_LEAF; +} + /* Float a pane *at* a rect: the script's version of the drag. Already * floating, it is re-placed without a toggle — a placement is a statement * about where, not about whether — and the intent is kept exactly as asked @@ -153,10 +153,40 @@ static char *cmd_json(app_t *a, screen_t *s, input_parser_t *in, return jok_int(NULL, 0); } if (strcmp(cmd, "raw") == 0) { + /* `id` names the pane to write into; 0 (the default) is the focused one, + * which is what this verb always did. Naming a pane is the difference + * between driving a program and interfering with a person: without it a + * caller had to focus the pane first, which moves the view of whoever is + * watching and loses a race with anybody else doing the same. */ const char *data = jv_gets(req, "data", ""); - app_write_focused(a, data, strlen(data)); + uint32_t id = (uint32_t)jv_geti(req, "id", 0); + if (!app_write_pane(a, id, data, strlen(data))) return jerr("no such pane"); return jok_int(NULL, 0); } + /* One pane's visible text, by id (0 = focused), from the pane's own terminal. + * `snapshot` is the composited screen and composites only the active tab, so + * this is the verb for reading a pane that is somewhere else -- with no + * select-tab, no focus change and no effect on the human's selection. */ + if (strcmp(cmd, "capture") == 0) { + app_compose(a, s); /* so a pane that has just printed is up to date */ + uint32_t id = (uint32_t)jv_geti(req, "id", 0); + char *txt = app_pane_text(a, id); + if (!txt) { + /* Tell "no such pane" apart from "that pane is empty": a caller polling + * for output would otherwise read a dead id as a quiet pane forever. */ + if (id && !app_pane_exists(a, id)) return jerr("no such pane"); + txt = calloc(1, 1); + } + json_t j; + json_init(&j); + json_obj_open(&j, NULL); + json_bool(&j, "ok", true); + json_int(&j, "id", (long long)(id ? id : app_focused_pane_id(a))); + json_str(&j, "text", txt, strlen(txt)); + json_obj_close(&j); + free(txt); + return j.buf; + } if (strcmp(cmd, "resize") == 0) { uint16_t c = (uint16_t)jv_geti(req, "cols", s->cols); uint16_t r = (uint16_t)jv_geti(req, "rows", s->rows); @@ -1264,6 +1264,55 @@ char *pane_selection_text(pane_t *p) { return out; } +/* The pane's whole viewport as text: what somebody looking at this pane would + * read, for a pane in any tab, without touching anything. + * + * A snapshot of the *session* is the composited screen and only the active tab + * is composited, so reading one pane used to mean bringing its tab up, slicing + * its rect out of the frame and putting the tab back -- which moves the view + * of whoever is watching, and races anybody else doing the same. This asks the + * pane's own terminal instead. + * + * The selection is built here and handed to the formatter as a snapshot rather + * than set on the terminal: formatting the *terminal's* selection would mean + * overwriting whatever the human had selected, so a read would have a visible + * side effect. `unwrap` is off, unlike a copy: this answers "what is on + * screen", and a row is a line there even when it is the tail of a longer one. + * + * NULL when there is nothing (a pane whose terminal has no viewport yet). + * Caller frees. */ +char *pane_viewport_text(pane_t *p) { + if (!p || !p->term || !p->cols || !p->rows_n) return NULL; + GhosttyGridRef a, b; + if (!grid_ref_at(p, 0, 0, &a) || + !grid_ref_at(p, (uint16_t)(p->cols - 1), (uint16_t)(p->rows_n - 1), &b)) + return NULL; + + GhosttySelection sel = GHOSTTY_INIT_SIZED(GhosttySelection); + sel.start = a; + sel.end = b; + sel.rectangle = false; + + GhosttyTerminalSelectionFormatOptions opts = + GHOSTTY_INIT_SIZED(GhosttyTerminalSelectionFormatOptions); + opts.emit = GHOSTTY_FORMATTER_FORMAT_PLAIN; + opts.unwrap = false; + opts.trim = true; + opts.selection = &sel; + + uint8_t *ptr = NULL; + size_t len = 0; + if (ghostty_terminal_selection_format_alloc(p->term, NULL, opts, &ptr, + &len) != GHOSTTY_SUCCESS || + !len) + return NULL; + char *out = malloc(len + 1); + memcpy(out, ptr, len); + out[len] = 0; + ghostty_free(NULL, ptr, len); + return out; +} + /* ---- scrollback --------------------------------------------------------- */ void pane_scroll(pane_t *p, int delta) { |
