diff --git a/.claude/skills/test-forestui/SKILL.md b/.claude/skills/test-forestui/SKILL.md index 90460d5..0cf1853 100644 --- a/.claude/skills/test-forestui/SKILL.md +++ b/.claude/skills/test-forestui/SKILL.md @@ -1,6 +1,6 @@ --- name: test-forestui -description: Use tu to visually drive and test forestui in headless terminals. Invoke when developing forestui features, debugging UI issues, or verifying changes — lets you see and interact with the running app yourself. +description: Use tu to visually drive and test forestui in headless terminals. Invoke PROACTIVELY when fixing behavioral/visual bugs or developing new features — don't wait to be asked. Lint and typecheck can't catch UI regressions, wrong window names, or broken interactions. allowed-tools: Bash, Read argument-hint: "[what to test or verify]" --- @@ -35,9 +35,10 @@ If no `.tmux.conf`: defaults are prefix `Ctrl+B`, next `Ctrl+B n`, prev `Ctrl+B ## How to launch forestui **NEVER run `uv run forestui` or any tmux command without `TMUX_TMPDIR` isolation.** -The user is likely running their own tmux/forestui session right now. If you -connect to the default tmux server you will interfere with their live session — -creating windows, switching their active view, or corrupting their session state. +**NEVER call `tmux` directly from Bash** — not even with `TMUX_TMPDIR` set. The +user is likely running their own tmux/forestui session right now. Any direct +`tmux` call risks connecting to (and corrupting) their live session. ALL +interaction with the test tmux must go through `tu` commands. ```bash FUI_TEST_DIR=$(mktemp -d) @@ -48,6 +49,32 @@ tu run --name fui --env TMUX_TMPDIR=$FUI_TEST_DIR \ tu wait --name fui --text "forestui" --timeout 15000 ``` +### When you need to detach and reattach + +If your test involves detaching from tmux and re-running `forestui` (e.g., +testing reattach behavior), do NOT launch forestui directly as the `tu` process. +Instead, launch a **bash shell** inside `tu` and run forestui from it. This way, +when tmux detaches, bash gets its prompt back and you can run forestui again: + +```bash +tu run --name fui --env TMUX_TMPDIR=$FUI_TEST_DIR \ + --cwd -- env -u TMUX bash -l + +# Wait for bash prompt, then start forestui +tu wait --name fui --text "\\$" --timeout 5000 +tu type --name fui "uv run forestui" +tu press --name fui Enter + +# ... do your test, detach with Ctrl+A d ... +# After detach, bash prompt returns. Run forestui again: +tu wait --name fui --text "\\$" --timeout 5000 +tu type --name fui "uv run forestui" +tu press --name fui Enter +``` + +Why: forestui calls `os.execvp("tmux", ...)` to enter tmux. If forestui IS the +`tu` process, detaching kills the tu session (no shell to return to). + Need multiple terminals (e.g., testing grouped sessions)? Use the same `TMUX_TMPDIR` so they share the same isolated tmux server: @@ -85,6 +112,11 @@ each session is viewing. Use the bindings from `.tmux.conf`. Translate to `tu press` yourself. +**Gotcha — renaming windows.** Check `.tmux.conf` for the rename-window binding. +If it uses `command-prompt -I "#W"`, the prompt is pre-filled with the current +window name — press `Ctrl+U` to clear before typing. If it uses +`command-prompt` without `-I`, the prompt starts empty and you can type directly. + ### Text input ```bash diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..bc8c745 --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,24 @@ +name: Check + +on: + push: + branches: [main] + pull_request: + +jobs: + check: + name: Lint & Typecheck + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@v6 + + - name: Install uv + uses: astral-sh/setup-uv@v7 + + - name: Set up Python + run: uv python install + + - name: Run checks + run: make check diff --git a/CLAUDE.md b/CLAUDE.md index 6faf046..10c445f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -153,7 +153,14 @@ Each forest has its own `.forestui-config.json` state file. ## Testing -Currently no test suite. When adding tests: +**Visual/behavioral verification:** forestui is a TUI app — lint and typecheck +alone cannot catch visual bugs, wrong window names, broken interactions, or +UI regressions. When fixing behavioral/visual bugs or developing new features, +use the `test-forestui` skill to launch forestui in a headless terminal and +visually verify the fix or feature works. Do this proactively, not only when +asked. + +Currently no unit test suite. When adding tests: - Use pytest - Place tests in `tests/` directory - Add pytest to dev dependencies @@ -165,7 +172,8 @@ Version is `0.0.0` in source code. Actual versions are derived from git tags at ### Development 1. Create a branch, make changes -2. Run `make check` and test locally +2. **Always run `make check` before committing, pushing, or opening a PR.** Do not + push code that fails lint or typecheck — fix issues first. 3. Open a PR and merge to `main` Running from source (`uv run forestui`) shows version `0.0.0` and auto-enables dev mode. diff --git a/Makefile b/Makefile index 5cd0cc5..6843330 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: lint typecheck check format install dev clean +.PHONY: lint typecheck format-check check format install dev clean # Run ruff linter lint: @@ -8,8 +8,12 @@ lint: typecheck: uv run mypy forestui/ -# Run all checks (lint + typecheck) -check: lint typecheck +# Check formatting without modifying files +format-check: + uv run ruff format --check forestui/ + +# Run all checks (lint + typecheck + format) +check: lint typecheck format-check # Format code with ruff format: diff --git a/forestui/cli.py b/forestui/cli.py index a794944..4868629 100644 --- a/forestui/cli.py +++ b/forestui/cli.py @@ -112,7 +112,7 @@ def ensure_tmux( "-t", f"={session_name}", "-n", - "forestui", + get_window_name(dev_mode), forestui_cmd, ], ) diff --git a/forestui/services/tmux.py b/forestui/services/tmux.py index 17ea528..06db049 100644 --- a/forestui/services/tmux.py +++ b/forestui/services/tmux.py @@ -108,13 +108,26 @@ def session(self) -> Session | None: @property def current_window(self) -> Window | None: - """Get the current tmux window.""" - if self.session is None: + """Get the tmux window this process is running in. + + Uses the TMUX_PANE environment variable to find our own window, + rather than the session's active window — which may be a different + window when forestui is starting up in a background window. + """ + if self.server is None: + return None + pane_id = os.environ.get("TMUX_PANE") + if not pane_id: return None try: - return self.session.active_window + for sess in self.server.sessions: + for window in sess.windows: + for pane in window.panes: + if pane.pane_id == pane_id: + return window except LibTmuxException: return None + return None def rename_window(self, name: str) -> bool: """Rename the current tmux window."""