Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand Down
1 change: 1 addition & 0 deletions .github/shell.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
69 changes: 69 additions & 0 deletions docs/plans/done/2026-02-08-close-popup-on-outside-click.md
Original file line number Diff line number Diff line change
@@ -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).
Comment thread
cboone marked this conversation as resolved.

### 1. Include column in the MOUSE_LEFT token (line 523)

Comment thread
cboone marked this conversation as resolved.
```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)

Comment thread
cboone marked this conversation as resolved.
```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
15 changes: 11 additions & 4 deletions scripts/viewer.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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' ;;
Expand Down Expand Up @@ -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
Comment thread
cboone marked this conversation as resolved.

if ((SEARCH_MODE)); then
case "$key" in
ENTER | DOWN)
Expand Down