Add TmuxRunner.new_aux_window for styled tool windows
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
import shlex
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
@@ -315,6 +316,68 @@ class TmuxRunner:
|
|||||||
|
|
||||||
return window_id
|
return window_id
|
||||||
|
|
||||||
|
async def new_aux_window(
|
||||||
|
self, name: str, cwd: str, command: list[str], label: str
|
||||||
|
) -> str | None:
|
||||||
|
"""Create an auxiliary (non-session) tool window and switch to it.
|
||||||
|
|
||||||
|
Appends at the next free index, then styles by window_id — not name — so
|
||||||
|
duplicate names (tool windows are spawned fresh every time) stay
|
||||||
|
unambiguous. Deliberately does NOT set remain-on-exit: the window closes
|
||||||
|
when the tool exits (a quit shell closes its window too). The window is
|
||||||
|
never tracked by hqt; it lives purely as a tmux window.
|
||||||
|
|
||||||
|
Returns the window_id, or None on failure (the half-created window is
|
||||||
|
cleaned up).
|
||||||
|
"""
|
||||||
|
idx = await self._next_window_index()
|
||||||
|
args = [
|
||||||
|
"new-window",
|
||||||
|
"-t",
|
||||||
|
f"{self.session_name}:{idx}",
|
||||||
|
"-n",
|
||||||
|
name,
|
||||||
|
"-c",
|
||||||
|
cwd,
|
||||||
|
"-P",
|
||||||
|
"-F",
|
||||||
|
"#{window_id}",
|
||||||
|
]
|
||||||
|
if command:
|
||||||
|
args.append(shlex.join(command))
|
||||||
|
rc, stdout, err = await self._exec(*args)
|
||||||
|
if rc != 0:
|
||||||
|
log.error("new-window (aux) failed: %s", err)
|
||||||
|
return None
|
||||||
|
window_id = stdout.strip()
|
||||||
|
|
||||||
|
# Style by window_id: automatic-rename off, the @hqt_label, then the
|
||||||
|
# Frappé per-window theme — one atomic invocation (";" argv separators).
|
||||||
|
rc, _, err = await self._exec(
|
||||||
|
"set-option",
|
||||||
|
"-w",
|
||||||
|
"-t",
|
||||||
|
window_id,
|
||||||
|
"automatic-rename",
|
||||||
|
"off",
|
||||||
|
";",
|
||||||
|
"set-option",
|
||||||
|
"-w",
|
||||||
|
"-t",
|
||||||
|
window_id,
|
||||||
|
"@hqt_label",
|
||||||
|
label,
|
||||||
|
";",
|
||||||
|
*_window_theme_args(window_id),
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
log.error("set-option (aux window) failed for %s: %s", name, err)
|
||||||
|
await self._exec("kill-window", "-t", window_id)
|
||||||
|
return None
|
||||||
|
|
||||||
|
await self._exec("select-window", "-t", window_id)
|
||||||
|
return window_id
|
||||||
|
|
||||||
async def has_window(self, window_name: str) -> bool:
|
async def has_window(self, window_name: str) -> bool:
|
||||||
"""Check if a window with this name exists."""
|
"""Check if a window with this name exists."""
|
||||||
rc, stdout, _ = await self._exec(
|
rc, stdout, _ = await self._exec(
|
||||||
|
|||||||
@@ -1009,3 +1009,63 @@ async def test_poll_info_empty_names(runner):
|
|||||||
result = await mgr.poll_info([])
|
result = await mgr.poll_info([])
|
||||||
assert result == {}
|
assert result == {}
|
||||||
assert runner._exec.call_count == 1
|
assert runner._exec.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Task 2: new_aux_window — styled tool windows (never tracked)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_new_aux_window_spawns_styles_and_selects(runner):
|
||||||
|
runner._exec.side_effect = [
|
||||||
|
(0, "0\n", ""), # _next_window_index: indices [0] -> next index 1
|
||||||
|
(0, "@7\n", ""), # new-window -P -F '#{window_id}'
|
||||||
|
(0, "", ""), # set-option (automatic-rename + @hqt_label + theme)
|
||||||
|
(0, "", ""), # select-window
|
||||||
|
]
|
||||||
|
wid = await runner.new_aux_window("lazygit", "/proj", ["lazygit"], "lazygit · proj")
|
||||||
|
assert wid == "@7"
|
||||||
|
|
||||||
|
calls = runner._exec.call_args_list
|
||||||
|
# new-window: next free index, name, cwd, print window_id, then the command.
|
||||||
|
assert calls[1].args == (
|
||||||
|
"new-window", "-t", "hqt-main:1", "-n", "lazygit",
|
||||||
|
"-c", "/proj", "-P", "-F", "#{window_id}", "lazygit",
|
||||||
|
)
|
||||||
|
# post-creation options target the window_id and NEVER set remain-on-exit.
|
||||||
|
assert calls[2].args[:6] == (
|
||||||
|
"set-option", "-w", "-t", "@7", "automatic-rename", "off",
|
||||||
|
)
|
||||||
|
assert "@hqt_label" in calls[2].args
|
||||||
|
assert "lazygit · proj" in calls[2].args
|
||||||
|
assert "remain-on-exit" not in calls[2].args
|
||||||
|
# switch to it.
|
||||||
|
assert calls[3].args == ("select-window", "-t", "@7")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_new_aux_window_shell_omits_command_arg(runner):
|
||||||
|
runner._exec.side_effect = [
|
||||||
|
(0, "2\n", ""), # indices [2] -> next index 3
|
||||||
|
(0, "@9\n", ""),
|
||||||
|
(0, "", ""),
|
||||||
|
(0, "", ""),
|
||||||
|
]
|
||||||
|
wid = await runner.new_aux_window("shell", "/proj", [], "shell · proj")
|
||||||
|
assert wid == "@9"
|
||||||
|
calls = runner._exec.call_args_list
|
||||||
|
assert calls[1].args == (
|
||||||
|
"new-window", "-t", "hqt-main:3", "-n", "shell",
|
||||||
|
"-c", "/proj", "-P", "-F", "#{window_id}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_new_aux_window_returns_none_when_new_window_fails(runner):
|
||||||
|
runner._exec.side_effect = [
|
||||||
|
(0, "0\n", ""), # next index
|
||||||
|
(1, "", "boom"), # new-window fails
|
||||||
|
]
|
||||||
|
wid = await runner.new_aux_window("nvim", "/p", ["nvim"], "nvim · p")
|
||||||
|
assert wid is None
|
||||||
|
|||||||
Reference in New Issue
Block a user