diff --git a/src/hqt/cli.py b/src/hqt/cli.py index cd056b7..9e99188 100644 --- a/src/hqt/cli.py +++ b/src/hqt/cli.py @@ -169,6 +169,68 @@ def tool_cmd(tool, window): raise SystemExit(1) +PALETTE_ENTRIES = ["nvim", "lazygit", "shell", "clone"] + +# fzf colors matching the Catppuccin Frappé status bar / the Alt+o switcher. +_PALETTE_FZF_COLORS = ( + "bg:#292c3c,bg+:#414559,fg:#c6d0f5,fg+:#c6d0f5,hl:#ef9f76,hl+:#ef9f76," + "pointer:#ef9f76,prompt:#8caaee,info:#838ba7,border:#838ba7" +) + + +def _palette_popup_command(window: str) -> str: + """Shell pipeline for the fzf popup; `window` is baked in as a literal. + + display-popup does NOT format-expand its command, so the window name must be + concrete here (run-shell already expanded #{window_name} before `hqt palette` + ran). The selected entry runs `hqt tool `. + """ + import shlex + + entries = "\\n".join(PALETTE_ENTRIES) + "\\n" + return ( + f"printf '{entries}' | " + f"fzf --reverse --no-info --prompt='tool ' --pointer='▌' " + f"--color='{_PALETTE_FZF_COLORS}' | " + f"xargs -r -I{{}} hqt tool {{}} {shlex.quote(window)}" + ) + + +@main.command(name="palette") +@click.argument("window") +def palette_cmd(window): + """Pop an fzf tool palette for the session in tmux WINDOW (bound to M-p).""" + from hqt.config import get_settings + + settings = get_settings() + svc = _build_session_service() + if svc.session_id_for_window(window) is None: + subprocess.run( + [ + settings.tmux_path, + "display-message", + "hqt: open the tool palette from a harness window", + ] + ) + return + subprocess.run( + [ + settings.tmux_path, + "display-popup", + "-E", + "-w", + "40%", + "-h", + "30%", + "-T", + " open tool ", + "-S", + "fg=#838ba7", + _palette_popup_command(window), + ] + ) + + @main.command(name="list") def list_cmd(): """List projects and sessions.""" diff --git a/tests/test_cli.py b/tests/test_cli.py index 652116b..7472b75 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -63,3 +63,47 @@ def test_tool_cmd_reports_service_error(monkeypatch, tmp_path): assert result.exit_code == 1 assert "not an hqt session window" in result.output + + +def test_palette_cmd_shows_popup_for_session_window(monkeypatch, tmp_path): + monkeypatch.setattr( + "hqt.config.get_settings", lambda: Settings(db_path=tmp_path / "t.db") + ) + monkeypatch.setattr( + "hqt.sessions.service.SessionService.session_id_for_window", + lambda self, window: 5, + ) + runs = [] + monkeypatch.setattr(cli.subprocess, "run", lambda argv, **kw: runs.append(argv)) + + result = CliRunner().invoke(cli.main, ["palette", "hqt-5"]) + + assert result.exit_code == 0, result.output + assert len(runs) == 1 + argv = runs[0] + assert "display-popup" in argv + popup_cmd = argv[-1] + assert "fzf" in popup_cmd + assert "nvim" in popup_cmd and "clone" in popup_cmd + # the window is baked into the command for `hqt tool {} ` + assert "hqt-5" in popup_cmd + + +def test_palette_cmd_hints_for_non_session_window(monkeypatch, tmp_path): + monkeypatch.setattr( + "hqt.config.get_settings", lambda: Settings(db_path=tmp_path / "t.db") + ) + monkeypatch.setattr( + "hqt.sessions.service.SessionService.session_id_for_window", + lambda self, window: None, + ) + runs = [] + monkeypatch.setattr(cli.subprocess, "run", lambda argv, **kw: runs.append(argv)) + + result = CliRunner().invoke(cli.main, ["palette", "nvim"]) + + assert result.exit_code == 0, result.output + assert len(runs) == 1 + argv = runs[0] + assert "display-message" in argv + assert "display-popup" not in argv