aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoruser <user@clank>2026-09-10 15:42:47 +0200
committeruser <user@clank>2026-09-10 15:42:47 +0200
commite06abc0e1e3f4c3ba4630241fc4a77b5c745112e (patch)
tree3977a9df14468c1082d01c3c7c6ba0eba9b5e924
parentsidebar: what each tab's panes are saying, said under the tab (tab_bar_status) (diff)
downloadslosh-e06abc0e1e3f4c3ba4630241fc4a77b5c745112e.tar.gz
slosh-e06abc0e1e3f4c3ba4630241fc4a77b5c745112e.tar.xz
slosh-e06abc0e1e3f4c3ba4630241fc4a77b5c745112e.zip
tabs: an unnamed tab borrows the directory its focused pane is in
name, then purpose, then the basename of the focused pane's cwd, then the bare number. The kernel's answer (live_cwd, the same question save-workspace already asks), so a cd moves the label with you; home shows as ~. The focused pane rather than any consensus of the tab's panes: when they sit in different directories, the one you are typing into is what the tab is about, and a rule anyone can predict beats a cleverer one nobody can. Derived at draw time and never stored, so a real name wins the moment one exists, dumps and the tabs JSON stay unpolluted, and renaming starts from a genuinely empty name.
-rw-r--r--src/app_draw.c24
-rw-r--r--tests/test_tab_bar.py31
2 files changed, 55 insertions, 0 deletions
diff --git a/src/app_draw.c b/src/app_draw.c
index 5d358ad..b61d330 100644
--- a/src/app_draw.c
+++ b/src/app_draw.c
@@ -4,6 +4,7 @@
#include <ghostty/vt.h>
#include <ctype.h>
+#include <limits.h>
#include <stdarg.h>
#include <time.h>
#include <sys/stat.h>
@@ -1793,6 +1794,29 @@ static uint16_t draw_tab_cell(app_t *a, screen_t *s, size_t i, uint16_t x,
tab_t *t = &a->tabs[i];
char label[96];
const char *nm = t->name[0] ? t->name : (t->purpose[0] ? t->purpose : "");
+ /* An unnamed tab borrows the name of the directory its focused pane is in
+ * -- the kernel's answer, so a `cd` moves the label with you. The focused
+ * pane rather than any consensus of the tab's panes: when they disagree,
+ * the one you are typing into is what the tab is about, and a rule anyone
+ * can predict beats a cleverer one nobody can. Derived at draw time and
+ * never stored, so a real name or a purpose wins the moment one exists. */
+ char derived[80];
+ if (!nm[0]) {
+ node_t *f = t->focus ? t->focus : first_leaf_of(t->root);
+ char buf[PATH_MAX];
+ const char *cwd = f && f->pane ? live_cwd(f->pane, buf, sizeof buf) : NULL;
+ if (cwd && cwd[0]) {
+ const char *home = getenv("HOME");
+ if (home && home[0] && strcmp(cwd, home) == 0) {
+ nm = "~";
+ } else {
+ const char *base = strrchr(cwd, '/');
+ snprintf(derived, sizeof derived, "%s",
+ base && base[1] ? base + 1 : cwd);
+ nm = derived;
+ }
+ }
+ }
/* A pane that rang in a tab you are not looking at is invisible without
* this, and that is the case the whole indicator exists for. */
bool rang = CFG.bell_indicator && tab_has_bell(t);
diff --git a/tests/test_tab_bar.py b/tests/test_tab_bar.py
index 9342905..47feba6 100644
--- a/tests/test_tab_bar.py
+++ b/tests/test_tab_bar.py
@@ -204,6 +204,34 @@ def test_a_dead_pane_reports_its_exit_instead():
check("how it died replaces what it last said", "status 3" in row, repr(row))
+def test_an_unnamed_tab_borrows_its_directory():
+ cd = ["/bin/sh", "-c", "cd /tmp && stty raw -echo; cat"]
+ with Session(cd, cols=90, rows=20, config=LEFT) as s:
+ s.settle(60)
+ row = s.snapshot().line(2)[:W]
+ check("the label is the focused pane's dir", "1:tmp" in row, repr(row))
+
+
+def test_a_real_name_beats_the_derived_one():
+ lay = tempfile.NamedTemporaryFile("w", suffix=".layout", delete=False)
+ lay.write('layout {\n tab name="api" {\n pane\n }\n}\n')
+ lay.close()
+ with Session(SH, cols=90, rows=20, config=LEFT, layout=lay.name) as s:
+ s.settle(60)
+ row = s.snapshot().line(2)[:W]
+ check("a declared name is not second-guessed", "1:api" in row, repr(row))
+
+
+def test_the_borrowed_name_follows_a_cd():
+ loop = ["/bin/sh", "-c", 'cd /tmp && while IFS= read -r l; do eval "$l"; done']
+ with Session(loop, cols=90, rows=20, config=LEFT) as s:
+ s.until(lambda snap: "1:tmp" in snap.line(2))
+ s.send(r"cd /home\n")
+ s.until(lambda snap: "1:home" in snap.line(2))
+ row = s.snapshot().line(2)[:W]
+ check("the kernel's answer moves the label", "1:home" in row, repr(row))
+
+
def test_narrow_terminal_falls_back_to_top():
# 40 < width + min_pane cols + 4: the sidebar would leave no room for the
# pane it is chrome for, so the strip goes back to the top row.
@@ -242,6 +270,9 @@ if __name__ == "__main__":
test_status_cap_spends_its_last_row_on_the_ellipsis()
test_a_long_status_is_cut_and_says_so()
test_a_dead_pane_reports_its_exit_instead()
+ test_an_unnamed_tab_borrows_its_directory()
+ test_a_real_name_beats_the_derived_one()
+ test_the_borrowed_name_follows_a_cd()
test_narrow_terminal_falls_back_to_top()
test_growing_back_restores_the_sidebar()
sys.exit(report())