fix(tui): advertise the key that actually detaches, and stop tmux painting it green

Two things the attach status bar got wrong, both found in a beta test.

The bar read `Ctrl+B D`. tmux key tables are case-sensitive: lowercase `d` is
`detach-client`, capital `D` is `choose-client`. Pressing what the bar said
opened a client chooser and left the tester attached, with the way out on
screen and inert. The key is now READ from `list-keys -T prefix` the same way
the prefix already was, rather than hardcoded, so a rebound tmux is followed
too and the label cannot drift from the binding again. It never goes through
formatPrefixKey(), which uppercases.

The bar also rendered as a full-width bright green slab. Only `status-format[0]`
was styled, so tmux's stock `status-style` (`bg=green,fg=black`) stayed
underneath it and won; `#[reverse]` on top could not undo it. `status-style` is
now set explicitly to `bg=default,fg=default` and snapshotted/restored with the
rest, so the bar sits on the terminal's own background and reads as a hint
line.

Tests pin both: that the chord ends in lowercase `d` and never ` D`, that a
rebound key prints verbatim, that `status-style` is part of the banner, and
that parseDetachKey() picks `d` out of verbatim tmux 3.4 `list-keys` output
while ignoring `detach-client -a`/`-P`, which act on other clients.
This commit is contained in:
Codeman maintainer
2026-08-20 00:28:11 +02:00
parent 61d84ac319
commit 1fea9d1d94
4 changed files with 150 additions and 19 deletions
+42
View File
@@ -22,6 +22,7 @@ import {
parseEnvFile,
arrayOptionBase,
parseSessionOptions,
parseDetachKey,
parseTmuxSessionList,
parseWindowSizing,
readCodemanCredentials,
@@ -599,6 +600,47 @@ describe('attach window sizing', () => {
});
});
describe('parseDetachKey', () => {
// Verbatim from `tmux -L codeman list-keys -T prefix` on tmux 3.4, trimmed to
// the two lines that matter. They differ only by case, which is the whole
// point: `D` is choose-client and advertising it leaves the tester attached.
const REAL_TMUX_34 = [
'bind-key -T prefix C-b send-prefix',
'bind-key -T prefix d detach-client',
'bind-key -T prefix D choose-client -Z',
'bind-key -T prefix x confirm-before -p "kill-pane #P? (y/n)" kill-pane',
].join('\n');
it('picks the lowercase detach-client binding out of real tmux output', () => {
expect(parseDetachKey(REAL_TMUX_34)).toBe('d');
});
it('follows a rebound detach key rather than assuming d', () => {
expect(parseDetachKey('bind-key -T prefix Q detach-client')).toBe('Q');
});
it('tolerates the -r repeat flag ahead of -T', () => {
expect(parseDetachKey('bind-key -r -T prefix d detach-client')).toBe('d');
});
it('ignores detach-client bindings that carry arguments', () => {
// `-a` detaches OTHER clients and `-P` kills the pane's process; neither is
// what the bar promises, so a socket with only those reports nothing.
expect(parseDetachKey('bind-key -T prefix X detach-client -a')).toBeNull();
expect(parseDetachKey('bind-key -T prefix Y detach-client -P')).toBeNull();
});
it('prefers a single-character binding over a named key', () => {
const both = 'bind-key -T prefix F1 detach-client\nbind-key -T prefix d detach-client';
expect(parseDetachKey(both)).toBe('d');
});
it('is null when nothing detaches, so the caller can fall back', () => {
expect(parseDetachKey('')).toBeNull();
expect(parseDetachKey('bind-key -T prefix D choose-client -Z')).toBeNull();
});
});
describe('attach status bar options', () => {
function optionsClient(exec: TuiExecFile): TuiClient {
return new TuiClient({ baseUrl: BASE_URL, socket: 'codeman-beta', exec });