diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 127f7b0..32a7764 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -8,6 +8,7 @@ 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: +``` make test ``` 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. 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 2809741..cce53aa 100755 --- a/scripts/viewer.sh +++ b/scripts/viewer.sh @@ -504,7 +504,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' ;; @@ -570,13 +570,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 | DOWN)