From 9ef058e41255bb2a82b09d9822a5ae5c237dc954 Mon Sep 17 00:00:00 2001 From: Christopher Boone Date: Sun, 8 Feb 2026 21:08:38 -0500 Subject: [PATCH 1/3] build: add Makefile with test target --- .claude/CLAUDE.md | 4 +++- Makefile | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 Makefile diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index ff6de3f..103b13e 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -9,9 +9,11 @@ tmux-binding-help is a tmux plugin that displays an interactive popup showing al Tests use [Scrut](https://github.com/facebookincubator/scrut), a snapshot testing tool: ```sh -scrut test -w . tests +make test ``` +This runs `scrut test -w . tests` via the Makefile. + There is no build or lint step. ### Installation (for manual testing) diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..274fe9e --- /dev/null +++ b/Makefile @@ -0,0 +1,4 @@ +.PHONY: test + +test: + scrut test -w . tests From def3ef4d92846bc7491d4ebd10c01dedd6a14f9a Mon Sep 17 00:00:00 2001 From: Christopher Boone Date: Sun, 8 Feb 2026 21:40:07 -0500 Subject: [PATCH 2/3] feat: close popup on click outside content area Detect out-of-bounds mouse coordinates in the viewer and break the main loop, allowing the popup to close when the user clicks outside it. --- ...2026-02-08-close-popup-on-outside-click.md | 69 +++++++++++++++++++ scripts/viewer.sh | 15 ++-- 2 files changed, 80 insertions(+), 4 deletions(-) create mode 100644 docs/plans/done/2026-02-08-close-popup-on-outside-click.md diff --git a/docs/plans/done/2026-02-08-close-popup-on-outside-click.md b/docs/plans/done/2026-02-08-close-popup-on-outside-click.md new file mode 100644 index 0000000..7e1c692 --- /dev/null +++ b/docs/plans/done/2026-02-08-close-popup-on-outside-click.md @@ -0,0 +1,69 @@ +# Close Popup by Clicking Outside + +## Context + +The tmux-binding-help popup currently only closes via keyboard (`q` or `Escape`). Users expect to be able to click outside the popup to dismiss it, as is standard for overlay/popup UIs. In tmux 3.4+, the popup may already close on outside click at the tmux level, but for tmux 3.2-3.3 the click may be forwarded to the viewer's PTY with out-of-bounds coordinates. Adding viewer-level detection of these out-of-bounds coordinates provides coverage across tmux versions. + +## Changes + +All changes are in **`scripts/viewer.sh`** (3 edits, ~10 lines changed). + +### 1. Include column in the MOUSE_LEFT token (line 523) + +```bash +# Before: +0) printf 'MOUSE_LEFT:%d' "$mouse_row" ;; + +# After: +0) printf 'MOUSE_LEFT:%d:%d' "$mouse_row" "$mouse_col" ;; +``` + +The column is already parsed and validated at line 510-518 but currently discarded. This makes it available to the main loop. + +### 2. Extract both row and column in the main loop (lines 589-594) + +```bash +# Before: + # Extract mouse row from MOUSE_LEFT:ROW token + local mouse_row=0 + if [[ "$key" == MOUSE_LEFT:* ]]; then + mouse_row="${key#MOUSE_LEFT:}" + key="MOUSE_LEFT" + fi + +# After: + # Extract mouse row and column from MOUSE_LEFT:ROW:COL token + local mouse_row=0 mouse_col=0 + if [[ "$key" == MOUSE_LEFT:* ]]; then + local mouse_coords="${key#MOUSE_LEFT:}" + mouse_row="${mouse_coords%%:*}" + mouse_col="${mouse_coords#*:}" + key="MOUSE_LEFT" + fi +``` + +### 3. Add boundary check before mode dispatch (insert after line 594, before line 596) + +```bash + # Close popup on click outside content area + if [[ "$key" == "MOUSE_LEFT" ]] && ((mouse_row > TERM_ROWS || mouse_col > TERM_COLS)); then + break + fi +``` + +Placed before the search-mode/normal-mode `if`, so it applies regardless of current mode. The `break` exits the main loop, triggering `cleanup()` via the EXIT trap, and the popup closes via `-E`. + +Coordinates < 1 are already rejected as `MOUSE_OTHER` in `read_key()` (line 516), so only the upper-bound check is needed here. + +## What does not change + +- `click_select()`, scroll events, `popup.sh`, `cleanup()`, release/other mouse events +- No new dependencies or version requirements + +## Verification + +1. Open the popup, click outside it -- popup should close +2. Click inside on bindings, group headers, search bar, footer, empty area -- all should work as before +3. Enter search mode, click outside -- popup should close +4. Scroll inside popup -- should work as before +5. `make test` -- existing parser tests should pass diff --git a/scripts/viewer.sh b/scripts/viewer.sh index 397c7b0..ae2b0c5 100755 --- a/scripts/viewer.sh +++ b/scripts/viewer.sh @@ -520,7 +520,7 @@ read_key() { # Strip modifier bits (shift=4, meta=8, ctrl=16) mouse_button=$((mouse_button & ~(4 | 8 | 16))) case "$mouse_button" in - 0) printf 'MOUSE_LEFT:%d' "$mouse_row" ;; + 0) printf 'MOUSE_LEFT:%d:%d' "$mouse_row" "$mouse_col" ;; 64) printf 'MOUSE_SCROLL_UP' ;; 65) printf 'MOUSE_SCROLL_DOWN' ;; *) printf 'MOUSE_OTHER' ;; @@ -586,13 +586,20 @@ main() { key="$(read_key)" || continue - # Extract mouse row from MOUSE_LEFT:ROW token - local mouse_row=0 + # Extract mouse row and column from MOUSE_LEFT:ROW:COL token + local mouse_row=0 mouse_col=0 if [[ "$key" == MOUSE_LEFT:* ]]; then - mouse_row="${key#MOUSE_LEFT:}" + local mouse_coords="${key#MOUSE_LEFT:}" + mouse_row="${mouse_coords%%:*}" + mouse_col="${mouse_coords#*:}" key="MOUSE_LEFT" fi + # Close popup on click outside content area + if [[ "$key" == "MOUSE_LEFT" ]] && ((mouse_row > TERM_ROWS || mouse_col > TERM_COLS)); then + break + fi + if ((SEARCH_MODE)); then case "$key" in ENTER) From be2bad09261cefc024fbca41ff75c4b0baaaed26 Mon Sep 17 00:00:00 2001 From: Christopher Boone Date: Sun, 8 Feb 2026 21:52:28 -0500 Subject: [PATCH 3/3] docs: add Copilot instruction for POPUP_WIDTH - 2 column calculation --- .github/shell.instructions.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/shell.instructions.md b/.github/shell.instructions.md index b2ee38e..6e7c03b 100644 --- a/.github/shell.instructions.md +++ b/.github/shell.instructions.md @@ -6,3 +6,4 @@ applyTo: "**/*.sh" - **Single-quoted path interpolation in tmux commands**: Embedding `$CURRENT_DIR` inside single-quoted segments of tmux `display-popup` command strings is intentional. The variable is expanded by the outer shell before tmux receives the command. This is the conventional tmux plugin approach. - **Repeated simple function calls over loop abstractions**: When a function like `move_up` or `move_down` is called a small fixed number of times (e.g., 3), repeating the call is preferred over a loop or helper. This is more readable in Bash and avoids unnecessary abstraction for trivial repetition. Do not suggest extracting loops or helpers for small fixed-count call sequences. - **Parsed-but-not-persisted variables are intentional**: When a parsed value (e.g., a mouse column coordinate) is used only for validation and not stored in a global, this is intentional. Do not flag it as unused. The variable serves its purpose during parsing/validation and does not need to be persisted if no downstream code requires it. +- **POPUP_WIDTH - 2 for TERM_COLS is intentional**: In `viewer.sh`, `TERM_COLS` is preferentially set to `POPUP_WIDTH - 2` (outer popup width minus border) rather than `stty size` columns. Both values represent the same inner content width inside a tmux popup. Using the passed-in `POPUP_WIDTH` is preferred because it is a known value from `popup.sh`; `stty size` is the fallback when `POPUP_WIDTH` is not available. Do not suggest replacing `POPUP_WIDTH - 2` with `stty size` columns.