aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-16 19:25:52 +0200
committeruser <user@clank>2026-08-16 19:25:52 +0200
commit503b309187eaf0bc7282dd09e93c695dba11abb5 (patch)
tree420112d1b9ea8416375b7f0cd0b4777ee8137599 /src
parentsl0ppty: a pane can change tabs (diff)
downloadslosh-503b309187eaf0bc7282dd09e93c695dba11abb5.tar.gz
slosh-503b309187eaf0bc7282dd09e93c695dba11abb5.tar.xz
slosh-503b309187eaf0bc7282dd09e93c695dba11abb5.zip
sl0ppty: `C-a >` sends this pane to the next tab
C-a > push it to the tab after C-a b into a tab of its own C-a < push it to the tab before Three actions rather than three special cases in the key handler, which is what puts them in the cheatsheet and the palette without another line of code -- and the palette is where you find a thing you did not know to look for. You do not follow the pane, and a toast says where it went: "moved to tab 2 (api)". A key you might press twice needs to say what it did, and following would take you away from what you were doing. The tab's *name* is read before the move and its *number* after: emptying this tab removes it and shifts everything along, so the number read first is not the number anybody will see. When the tab you were in is the one that goes, you arrive in the survivor because there is nowhere to stay. `>` and `<` are the shifted period and comma, and the defaults bind both halves -- a terminal does not report shift for punctuation, so `>` arrives as a bare period the way `?` arrives as a bare slash. The config loader already did this for a chord you write by hand; the defaults had to do it themselves. The cost is that `.` and `,` are spoken for, which is the cost `/` already pays for `?`. `b` is for break out, the word tmux taught everybody for this. **Two things the tests caught, both mine.** `panes` reported a pane's tab as an index while every command that takes a tab wants an id -- and my own test conflated them, passing only because the two agree until a tab is removed, which is half of what these keys do. There is now a `tab_id` beside the `tab`: one is where it sits in the strip, the other is what you pass back. And three more rows overflowed the cheatsheet: `modal_frame` clamps the box to the screen, so at 56x44 the `session` group was simply not drawn. The sheet now says `+7 more, in docs/keys.md` when it cannot show everything -- counted by walking the rows it skipped, because arithmetic on two uneven columns claimed six were hidden on a screen showing all of them. A list that quietly stops is worse than a short one: the reader cannot tell that the key they wanted was on it. Reflowing to two columns when the height forces it would be better than a footnote, and is not this commit. Verified: `tests/test_move_pane.py` at 38 checks -- the keys in both directions, breaking out, the refusal when there is only one tab, pushing the last pane out and landing in the survivor, and that the sheet prints `>` rather than the bare period it also answers to while the palette finds it by label. `tests/test_help.py` gained the short-terminal case and lost an assumption about its own fixture height.
Diffstat (limited to 'src')
-rw-r--r--src/app.c75
-rw-r--r--src/config.c24
-rw-r--r--src/config.h3
3 files changed, 100 insertions, 2 deletions
diff --git a/src/app.c b/src/app.c
index af7d77f..3fa6fee 100644
--- a/src/app.c
+++ b/src/app.c
@@ -4115,7 +4115,23 @@ static void draw_help(app_t *a, screen_t *s) {
if (right_n && !rows[n - 1].label) right_n--;
uint16_t lines = (uint16_t)(left_n > right_n ? left_n : right_n);
- rect_t in = modal_frame(a, s, (uint16_t)(body + 6), (uint16_t)(lines + 6),
+ /* A terminal too short for the whole sheet gets as much of it as fits and a line
+ * saying how much it did not. `modal_frame` clamps the box to the screen, so
+ * without this the rows past the bottom are simply not drawn -- and a list that
+ * quietly stops is worse than a short one, because the reader has no way to know
+ * the key they are looking for is on it. */
+ uint16_t room = (uint16_t)(s->rows > 7 ? s->rows - 7 : 1);
+ uint16_t shown = lines > room ? room : lines;
+ /* Counted, not calculated: the two columns are not the same length and a blank
+ * spacer is not a row anybody is missing. Arithmetic on `lines` claimed six rows
+ * were hidden on a screen showing all of them. */
+ size_t hidden = 0;
+ for (size_t i = 0; i < n; i++) {
+ size_t row = (two && i >= split) ? i - split : i;
+ if (row >= shown && rows[i].label) hidden++;
+ }
+
+ rect_t in = modal_frame(a, s, (uint16_t)(body + 6), (uint16_t)(shown + 6 + (hidden ? 1 : 0)),
"keys", "closehelp");
uint16_t x1 = (uint16_t)(in.x + in.w); /* the right border */
@@ -4130,6 +4146,7 @@ static void draw_help(app_t *a, screen_t *s) {
for (size_t i = 0; i < n; i++) {
bool right = two && i >= split;
size_t row = right ? i - split : i;
+ if (row >= shown) continue; /* past the bottom: counted, not drawn */
uint16_t ry = (uint16_t)(in.y + 3 + row);
if (ry >= in.y + in.h) continue;
if (!rows[i].label) continue; /* the blank row: it occupies, it draws not */
@@ -4151,6 +4168,14 @@ static void draw_help(app_t *a, screen_t *s) {
MODAL_FG, MODAL_BG, 0);
}
+ /* Said inside the box, where the rows stopped, rather than left to be noticed. */
+ if (hidden) {
+ char more[64];
+ snprintf(more, sizeof more, "+%zu more, in docs/keys.md", hidden);
+ help_text(s, (uint16_t)(in.x + 2), (uint16_t)(in.y + 3 + shown), more, in.w,
+ HINT_C, MODAL_BG, 0);
+ }
+
const char *foot = " any key closes this ";
if (in.w > cells(foot))
screen_text(s, (uint16_t)(in.x + (in.w - cells(foot)) / 2),
@@ -4397,6 +4422,37 @@ static void draw_picker(app_t *a, screen_t *s) {
/* Defined with the other action handling, further down: a picker row that
* says "run:" means one of these, and running it is that code's business. */
+/* Push the focused pane one tab along, and say where it went.
+ *
+ * You do not follow it -- which is the right default for a key you might press
+ * twice, and useless without a word about where the thing landed. The name is read
+ * *before* the move and the index *after*: emptying this tab removes it and shifts
+ * everything after, so the number you would have printed first is not the number
+ * anybody will see. */
+static bool push_pane_a_tab(app_t *a, bool forward) {
+ if (a->ntabs < 2) {
+ app_toast(a, "only one tab to move it to");
+ return true;
+ }
+ size_t to = (a->cur + (forward ? 1 : a->ntabs - 1)) % a->ntabs;
+ uint32_t tid = a->tabs[to].id;
+ char name[64];
+ snprintf(name, sizeof name, "%s", a->tabs[to].name);
+
+ if (!app_move_pane_to_tab(a, 0, tid, false)) {
+ app_toast(a, "cannot move it there");
+ return true;
+ }
+ size_t at = 0;
+ for (size_t i = 0; i < a->ntabs; i++)
+ if (a->tabs[i].id == tid) at = i;
+ char msg[128];
+ if (name[0]) snprintf(msg, sizeof msg, "moved to tab %zu (%s)", at + 1, name);
+ else snprintf(msg, sizeof msg, "moved to tab %zu", at + 1);
+ app_toast(a, msg);
+ return true;
+}
+
static bool run_action(app_t *a, action_t act);
/* Do what a row says it does. One entry point for the keyboard and the mouse,
@@ -5099,6 +5155,15 @@ static bool run_action(app_t *a, action_t act) {
app_toast(a, app_clear_pane_shaders(a, 0) ? "shaders cleared"
: "no shaders on this pane");
return true;
+ case ACT_PANE_TO_NEXT_TAB:
+ case ACT_PANE_TO_PREV_TAB:
+ return push_pane_a_tab(a, act == ACT_PANE_TO_NEXT_TAB);
+ case ACT_PANE_TO_NEW_TAB: {
+ uint32_t made = app_move_pane_to_new_tab(a, 0, "");
+ app_toast(a, made ? "into a tab of its own"
+ : "it is the only pane in this tab");
+ return true;
+ }
case ACT_EQUALIZE:
if (!app_equalize_splits(a)) app_toast(a, "nothing to even out");
return true;
@@ -5537,7 +5602,13 @@ static void panes_cb(node_t *n, void *ud) {
}
json_str(j, "purpose", n->purpose, strlen(n->purpose));
json_bool(j, "purpose_declared", n->purpose_locked);
- json_int(j, "tab", (long long)tab_of(pj->a, n) + 1);
+ /* Both, because they answer different questions and only one of them is stable:
+ * `tab` is where it sits in the strip, which is what a person reads, and `tab_id`
+ * is what every command that takes a tab wants. They coincide until a tab is
+ * removed, which is exactly when a script that guessed would be wrong. */
+ size_t ti = tab_of(pj->a, n);
+ json_int(j, "tab", (long long)ti + 1);
+ json_int(j, "tab_id", (long long)pj->a->tabs[ti].id);
const char *t = pane_title(n->pane);
json_str(j, "title", t ? t : "", t ? strlen(t) : 0);
json_obj_close(j);
diff --git a/src/config.c b/src/config.c
index 52188ed..d0cd347 100644
--- a/src/config.c
+++ b/src/config.c
@@ -25,6 +25,9 @@ static const struct {
{"resize-up", ACT_RESIZE_UP}, {"resize-down", ACT_RESIZE_DOWN},
{"equalize", ACT_EQUALIZE},
{"clear-shaders", ACT_CLEAR_SHADERS},
+ {"pane-to-next-tab", ACT_PANE_TO_NEXT_TAB},
+ {"pane-to-prev-tab", ACT_PANE_TO_PREV_TAB},
+ {"pane-to-new-tab", ACT_PANE_TO_NEW_TAB},
{"rotate-layout", ACT_ROTATE_LAYOUT},
{"scroll-up", ACT_SCROLL_UP}, {"scroll-down", ACT_SCROLL_DOWN},
{"scroll-page-up", ACT_SCROLL_PAGE_UP},
@@ -860,6 +863,21 @@ void config_defaults(config_t *c) {
* that reaches for the right thing here. Four presses come back round, which
* is what makes a key this easy to hit the right choice rather than a hazard. */
bind_add(c, GHOSTTY_KEY_SPACE, 0, ACT_ROTATE_LAYOUT, false);
+ /* `>` and `<` are the shifted period and comma, which is how the sheet prints
+ * them and how anybody would type them. Pushing a pane one tab along is the
+ * cheap version of moving it: no picker, no drag, and the toast says where it
+ * went since you do not follow it. */
+ bind_add(c, GHOSTTY_KEY_PERIOD, MOD_SHIFT, ACT_PANE_TO_NEXT_TAB, false);
+ bind_add(c, GHOSTTY_KEY_COMMA, MOD_SHIFT, ACT_PANE_TO_PREV_TAB, false);
+ /* ...and without the shift, because a terminal does not report one for
+ * punctuation: `>` arrives as a bare period, the way `?` arrives as a bare
+ * slash. The config loader binds both halves for the same reason when you write
+ * one of these by hand; the defaults have to do it themselves. The cost is that
+ * `.` and `,` are spoken for, which is the cost `/` already pays for `?`. */
+ bind_add(c, GHOSTTY_KEY_PERIOD, 0, ACT_PANE_TO_NEXT_TAB, false);
+ bind_add(c, GHOSTTY_KEY_COMMA, 0, ACT_PANE_TO_PREV_TAB, false);
+ /* `b` for break out, which is the word tmux taught everybody for this. */
+ bind_add(c, GHOSTTY_KEY_B, 0, ACT_PANE_TO_NEW_TAB, false);
bind_add(c, GHOSTTY_KEY_C, 0, ACT_NEW_TAB, false);
/* Cycling tabs is on tab/shift+tab, not on n/p: `p` is the palette, which is
* pressed far more often than "the tab before this one" and had the only
@@ -927,6 +945,12 @@ static const struct {
/* Under `panes`, not `size`: it undoes what the program in the pane did to
* the pane, which is a fact about that pane and not about the layout. */
{ACT_CLEAR_SHADERS, "panes", "clear this pane's shaders"},
+ /* Under `panes`: what moves is the pane, and the tab it lands in is where it
+ * lands. A reader looking for "how do I get this thing out of here" looks up
+ * the thing, not the place. */
+ {ACT_PANE_TO_NEXT_TAB, "panes", "push it to the tab after"},
+ {ACT_PANE_TO_PREV_TAB, "panes", "push it to the tab before"},
+ {ACT_PANE_TO_NEW_TAB, "panes", "into a tab of its own"},
{ACT_ROTATE_LAYOUT, "size", "turn the layout a quarter"},
{ACT_NEW_TAB, "tabs", "new tab"},
diff --git a/src/config.h b/src/config.h
index 062291e..16c4218 100644
--- a/src/config.h
+++ b/src/config.h
@@ -71,6 +71,9 @@ typedef enum {
ACT_RESIZE_DOWN,
ACT_EQUALIZE,
ACT_CLEAR_SHADERS,
+ ACT_PANE_TO_NEXT_TAB,
+ ACT_PANE_TO_PREV_TAB,
+ ACT_PANE_TO_NEW_TAB,
ACT_DETACH,
ACT_QUIT,
ACT_HELP,