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:
+122
-4
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user