Theme tmux status bar to match Catppuccin Frappé TUI

Style the hqt session's status bar with the same Frappé palette the TUI
uses: blue session badge, Mantle background, peach current-window
highlight, muted inactive windows, styled clock/date. The TUI window
shows just its house glyph (no index/flag/text). Applied from the TUI's
on_mount since the os.execvp bootstrap can't run follow-up tmux commands.

All options are session-scoped (never -g), so the user's other tmux
sessions and personal config are untouched. Window-scoped options
(current-window highlight, pane borders, mode style) have no session
scope in tmux, so they're painted per-window on every existing window in
apply_theme and on each new harness window in new_window — otherwise a
window spawned after mount would render with no peach highlight.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 17:26:34 -04:00
parent 90944948f5
commit 5eea909029
3 changed files with 256 additions and 6 deletions
+122 -4
View File
@@ -22,13 +22,15 @@ async def test_new_window(runner):
]
result = await runner.new_window("hqt-1", "/tmp", "kiro-cli chat")
assert result == "@1"
# Step 2: combined set-option call with ";" separators
# Step 2: combined set-option call with ";" separators — the three window
# options, then the per-window Frappé theme options (see new_window).
set_call = runner._exec.call_args_list[2].args
assert set_call == (
assert set_call[:17] == (
"set-option", "-t", "hqt-main:=hqt-1", "remain-on-exit", "on",
";", "set-option", "-t", "hqt-main:=hqt-1", "allow-rename", "off",
";", "set-option", "-t", "hqt-main:=hqt-1", "automatic-rename", "off",
)
assert "window-status-current-style" in set_call
# respawn-pane runs the actual command
calls = runner._exec.call_args_list
respawn_args = calls[3].args
@@ -217,13 +219,25 @@ async def test_new_window_race_free_sequence(runner):
assert new_win_args[-1] != "kiro-cli chat"
assert "kiro-cli chat" not in new_win_args
# Step 2: combined set-option call with ";" separators for all three options
# Step 2: combined set-option call with ";" separators — the three window
# options first, then the window-scoped Frappé theme options (so a window
# created after apply_theme still gets the current-window highlight).
from hqt.tmux.runner import _FRAPPE
set_args = calls[2].args
assert set_args == (
assert set_args[:17] == (
"set-option", "-t", "hqt-main:=hqt-1", "remain-on-exit", "on",
";", "set-option", "-t", "hqt-main:=hqt-1", "allow-rename", "off",
";", "set-option", "-t", "hqt-main:=hqt-1", "automatic-rename", "off",
)
# Window-scoped theme options painted on this window, current-style = peach.
assert "window-status-current-style" in set_args
assert f"fg={_FRAPPE['crust']},bg={_FRAPPE['peach']},bold" in set_args
for i, a in enumerate(set_args):
if a == "window-status-current-style":
assert set_args[i - 1] == "hqt-main:=hqt-1"
assert set_args[i - 2] == "-t"
assert set_args[i - 3] == "-w"
# Step 3: respawn-pane runs the command
resp_args = calls[3].args
@@ -799,6 +813,110 @@ async def test_manager_set_window_label_delegates(runner):
assert "○ deadproj" in args
@pytest.mark.asyncio
async def test_apply_theme_sets_session_scoped_status_options(runner):
"""apply_theme themes the status bar in one atomic styling call.
Session-scoped options target the session by -t; window-scoped options
target a window by -w -t <session>:=<window>. Nothing is ever set globally
(-g), so the user's other tmux sessions and personal config stay untouched.
"""
from hqt.tmux.runner import _FRAPPE
# Two windows already exist when the theme is applied.
async def fake_exec(*a):
if a and a[0] == "list-windows":
return (0, "⌂ HQT\nhqt-1\n", "")
return (0, "", "")
runner._exec.side_effect = fake_exec
await runner.apply_theme()
# One read to enumerate windows + one atomic styling call.
assert runner._exec.call_count == 2
args = runner._exec.call_args.args
# Core status-bar options are present and reference the Frappé palette.
assert "status-style" in args
assert "window-status-current-style" in args
assert f"fg={_FRAPPE['crust']},bg={_FRAPPE['peach']},bold" in args
assert f"bg={_FRAPPE['mantle']},fg={_FRAPPE['text']}" in args
# Never global.
assert "-g" not in args
# Every set-option targets the hqt session — either session-scoped (-t
# hqt-main) or window-scoped (-w -t hqt-main:=<window>).
for i, a in enumerate(args):
if a == "set-option":
if args[i + 1] == "-w":
assert args[i + 2] == "-t"
assert args[i + 3].startswith("hqt-main:=")
else:
assert args[i + 1] == "-t"
assert args[i + 2] == "hqt-main"
@pytest.mark.asyncio
async def test_apply_theme_paints_window_options_on_every_window(runner):
"""Regression: window-scoped options (current-window peach highlight) must be
set per-window for EVERY existing window, not just the active one — tmux has
no session scope for window options, so windows created after a bare
session-target set would render with no highlight.
"""
from hqt.tmux.runner import _FRAPPE
async def fake_exec(*a):
if a and a[0] == "list-windows":
return (0, "⌂ HQT\nhqt-1\nhqt-2\n", "")
return (0, "", "")
runner._exec.side_effect = fake_exec
await runner.apply_theme()
args = runner._exec.call_args.args
peach = f"fg={_FRAPPE['crust']},bg={_FRAPPE['peach']},bold"
# Each window gets its own `-w -t <target> window-status-current-style <peach>`.
for window in ("⌂ HQT", "hqt-1", "hqt-2"):
target = f"hqt-main:={window}"
idxs = [
i for i, a in enumerate(args)
if a == "window-status-current-style" and args[i - 1] == target
]
assert idxs, f"no current-style set for {window}"
assert args[idxs[0] + 1] == peach
@pytest.mark.asyncio
async def test_apply_theme_failure_is_logged_not_raised(runner):
"""A non-zero tmux rc must not raise — the TUI still starts."""
runner._exec.return_value = (1, "", "no server")
await runner.apply_theme() # must not raise
def test_home_window_format_is_just_the_glyph():
"""The home window's cell shows only its first glyph (the house), padded —
no index, no flag, no text."""
from hqt.tmux.runner import _home_window_format
assert _home_window_format("⌂ HQT") == ""
# Falls back to the trimmed name if there's no leading glyph.
assert _home_window_format("") == " "
@pytest.mark.asyncio
async def test_apply_theme_overrides_home_window_cell(runner):
"""apply_theme(home_window=…) adds a per-window (-w) format override for that
window showing just the house, targeted by name."""
await runner.apply_theme(home_window="⌂ HQT")
args = runner._exec.call_args.args
# Per-window override, targeted by exact name, set to the house-only cell.
assert "-w" in args
assert "hqt-main:=⌂ HQT" in args
assert "" in args
assert args.count("window-status-format") >= 1
assert "window-status-current-format" in args
# Styling is still one atomic call; the extra exec is the window enumeration.
assert runner._exec.call_count == 2
@pytest.mark.asyncio
async def test_poll_info_empty_names(runner):
"""poll_info with empty names list returns empty dict with exactly one exec."""