Skip to content

Commit b09dc5d

Browse files
committed
feat: update documentation for status line and help overlay
- add ? help keybinding to README and plugin usage reference - add status line and help overlay to features list - move plan to completed
1 parent e043f7d commit b09dc5d

6 files changed

Lines changed: 228 additions & 43 deletions

File tree

.claude-plugin/skills/revdiff/references/usage.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ revdiff HEAD~1 # review last commit
4646
| `v` | Toggle collapsed diff mode (shows final text with change markers) |
4747
| `.` | Expand/collapse individual hunk under cursor (collapsed mode only) |
4848
| `f` | Toggle filter: all files / annotated only |
49+
| `?` | Toggle help overlay showing all keybindings |
4950
| `q` | Quit, output annotations to stdout |
5051
| `Q` | Discard all annotations and quit (confirms if annotations exist) |
5152

.claude-plugin/skills/revdiff/scripts/launch-revdiff.sh

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,17 @@ trap 'rm -f "$OUTPUT_FILE"' EXIT
2020
REVDIFF_CMD="$REVDIFF_BIN --output=$OUTPUT_FILE $*"
2121
CWD="$(pwd)"
2222

23+
# build descriptive title: "revdiff: dirname [ref]"
24+
DIR_NAME=$(basename "$CWD")
25+
TITLE_REF=""
26+
for arg in "$@"; do
27+
case "$arg" in --*) ;; *) TITLE_REF="$arg"; break ;; esac
28+
done
29+
OVERLAY_TITLE="rd: ${DIR_NAME}${TITLE_REF:+ [$TITLE_REF]}"
30+
2331
# tmux: display-popup -E blocks until command exits
2432
if [ -n "${TMUX:-}" ] && command -v tmux >/dev/null 2>&1; then
25-
tmux display-popup -E -w 90% -h 90% -T " revdiff " -d "$CWD" -- sh -c "$REVDIFF_CMD"
33+
tmux display-popup -E -w 90% -h 90% -T " $OVERLAY_TITLE " -d "$CWD" -- sh -c "$REVDIFF_CMD"
2634
cat "$OUTPUT_FILE"
2735
exit 0
2836
fi
@@ -33,7 +41,7 @@ if [ -n "$KITTY_SOCK" ] && command -v kitty >/dev/null 2>&1; then
3341
SENTINEL=$(mktemp /tmp/revdiff-done-XXXXXX)
3442
rm -f "$SENTINEL"
3543

36-
KITTY_ARGS=(kitty @ --to "$KITTY_SOCK" launch --type=overlay --title="revdiff" --cwd="$CWD")
44+
KITTY_ARGS=(kitty @ --to "$KITTY_SOCK" launch --type=overlay --title="$OVERLAY_TITLE" --cwd="$CWD")
3745
if [ -n "${KITTY_WINDOW_ID:-}" ]; then
3846
KITTY_ARGS+=(--match "id:${KITTY_WINDOW_ID}")
3947
fi

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ Built for a specific use case: reviewing code changes without leaving a terminal
1313
- Two-pane TUI: file tree (left) + colorized diff viewport (right)
1414
- Hunk navigation to jump between change groups
1515
- Filter file tree to show only annotated files
16+
- Status line with filename, diff stats, hunk position, and mode indicators
17+
- Help overlay (`?`) showing all keybindings organized by section
1618
- Fully customizable colors via environment variables, CLI flags, or config file
1719

1820
![revdiff screenshot](screenshot.png)
@@ -233,6 +235,7 @@ revdiff HEAD~1
233235
| `v` | Toggle collapsed diff mode (shows final text with change markers) |
234236
| `.` | Expand/collapse individual hunk under cursor (collapsed mode only) |
235237
| `f` | Toggle filter: all files / annotated only (shown when annotations exist) |
238+
| `?` | Toggle help overlay showing all keybindings |
236239
| `q` | Quit, output annotations to stdout |
237240
| `Q` | Discard all annotations and quit (confirms if annotations exist) |
238241

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# Split Status Bar into Status Line + Help Overlay
2+
3+
## Overview
4+
Replace the current single status bar (which mixes status info and shortcut hints) with two separate concerns:
5+
1. **Status line** — shows file info, diff stats, hunk position, mode indicators, annotation count
6+
2. **Help overlay** — a modal popup triggered by `?` showing all keybindings organized by section
7+
8+
The current `statusBarText()` in `ui/model.go:488-551` is crowded and hard to scan. Splitting status from help makes both more useful: the status line becomes a clean info strip, and the help overlay provides comprehensive reference without cluttering the screen.
9+
10+
## Context
11+
- **Primary file:** `ui/model.go``statusBarText()` (lines 488-551), `View()` (lines 442-486), `handleKey()` (lines 188-259)
12+
- **Styles:** `ui/styles.go``StatusBar` style, `Colors` struct
13+
- **Diff navigation:** `ui/diffview.go``currentHunk()`, `findHunks()`
14+
- **Collapsed mode:** `ui/collapsed.go``collapsedState`
15+
- **Model fields:** `ui/model.go``currFile`, `diffLines`, `store`, `collapsed`, etc.
16+
17+
## Solution Overview
18+
19+
### Status line layout (left to right)
20+
```
21+
filename +N/-N hunk X/Y ▼ ◉ 3 annotations ? help
22+
```
23+
- **filename** — current file path (truncated from left with `` if too long)
24+
- **+N/-N** — additions/deletions count for the current file
25+
- **hunk X/Y** — current hunk position (only when cursor is on a changed line)
26+
- **** — collapsed mode indicator (only when active)
27+
- **** — filter active indicator (only when active)
28+
- **right-aligned:** annotation count + `? help` hint
29+
30+
### Help overlay
31+
- Triggered by `?` key, dismissed by `?` or `esc`
32+
- Centered bordered box rendered on top of the main view
33+
- Sections: Navigation, Annotations, View, Quit
34+
- Uses lipgloss border styling consistent with existing pane borders
35+
36+
## Technical Details
37+
38+
### New fields in Model
39+
- `showHelp bool` — true when help overlay is visible
40+
- No new files needed — help rendering goes in a new `helpOverlay()` method in `model.go`
41+
42+
### File stats computation
43+
- Count adds/removes from `m.diffLines` on file load (in `handleFileLoaded`)
44+
- Cache as `fileAdds int`, `fileRemoves int` fields on Model
45+
- Reset on file change
46+
47+
### Status line segments
48+
Each segment is a small string. Segments are joined with double-space separators. Right-aligned section uses padding like current implementation.
49+
50+
### Help overlay rendering
51+
- Build help text as a lipgloss-bordered box
52+
- When `m.showHelp` is true, `View()` replaces the main content area with the centered help popup (standard bubbletea modal pattern — no true compositing, the help box replaces tree+diff content)
53+
- Use `lipgloss.Place(m.width, paneHeight, lipgloss.Center, lipgloss.Center, helpBox)` + status bar below
54+
- Note: bubbletea reports `?` key correctly via `msg.String()` (shifted `/` key)
55+
56+
### Narrow terminal handling
57+
- Truncate filename from left with `` prefix when space is tight
58+
- Drop lower-priority segments (hunk, mode icons) if width is insufficient
59+
60+
## Development Approach
61+
- **Testing approach:** regular (code first, then tests)
62+
- Complete each task fully before moving to the next
63+
- Run tests after each change
64+
- Maintain backward compatibility (existing CLI flags, config, styles all still work)
65+
66+
## Implementation Steps
67+
68+
### Task 1: Compute and cache file diff stats
69+
70+
**Files:**
71+
- Modify: `ui/model.go`
72+
- Modify: `ui/model_test.go`
73+
74+
- [x] add `fileAdds` and `fileRemoves` int fields to `Model` struct
75+
- [x] add `computeFileStats()` method that counts add/remove lines from `m.diffLines`
76+
- [x] call `computeFileStats()` in `handleFileLoaded` after setting `m.diffLines`
77+
- [x] write tests for `computeFileStats()` with various diff line combinations
78+
- [x] run `make test` — must pass before task 2
79+
80+
### Task 2: Rewrite status line and update tests
81+
82+
**Files:**
83+
- Modify: `ui/model.go`
84+
- Modify: `ui/model_test.go`
85+
86+
- [x] rewrite `statusBarText()` to show: filename, +N/-N stats, hunk X/Y, mode icons (▼ ◉), right-aligned annotation count + `? help`
87+
- [x] keep special cases for `inConfirmDiscard` and `annotating` modes unchanged
88+
- [x] implement filename truncation with `` prefix for narrow terminals
89+
- [x] drop hunk and mode icons gracefully when terminal is too narrow
90+
- [x] update existing `statusBarText` tests to match new format (no shortcut hints, has filename/stats)
91+
- [x] add test cases for: filename truncation, mode indicators present/absent, stats display
92+
- [x] add test cases for narrow terminal width graceful degradation
93+
- [x] run `make test` — must pass before task 3
94+
95+
### Task 3: Add help overlay rendering
96+
97+
**Files:**
98+
- Modify: `ui/model.go`
99+
100+
- [x] add `showHelp bool` field to Model
101+
- [x] add `helpOverlay()` method returning the bordered help text with sections (Navigation, Annotations, View, Quit)
102+
- [x] modify `View()` to overlay help popup using `lipgloss.Place()` when `m.showHelp` is true
103+
- [x] write tests for `helpOverlay()` verifying section headers and key listings are present
104+
- [x] run `make test` — must pass before task 4
105+
106+
### Task 4: Wire up `?` key handling
107+
108+
**Files:**
109+
- Modify: `ui/model.go`
110+
- Modify: `ui/model_test.go`
111+
112+
- [x] handle `?` key in `handleKey()` to toggle `m.showHelp`
113+
- [x] handle `esc` key to close help when `m.showHelp` is true
114+
- [x] block all other key handling when help overlay is showing (except `?` and `esc`)
115+
- [x] write tests for help toggle behavior (open, close with ?, close with esc)
116+
- [x] write test that other keys are blocked when help is showing
117+
- [x] run `make test` — must pass before task 5
118+
119+
### Task 5: Verify acceptance criteria
120+
- [x] verify status line shows filename, stats, hunk, mode icons, annotations, help hint
121+
- [x] verify help overlay opens with `?` and closes with `?` or `esc`
122+
- [x] verify no shortcut hints in status bar anymore (all moved to help overlay)
123+
- [x] verify special modes (annotation input, discard confirm) still work in status bar
124+
- [x] run full test suite: `make test`
125+
- [x] run linter: `make lint`
126+
127+
### Task 6: [Final] Update documentation
128+
- [x] update README.md with new `?` help shortcut and status line description
129+
- [x] update `.claude-plugin/skills/revdiff/references/usage.md` with `?` help keybinding
130+
- [x] update CLAUDE.md if any new patterns discovered
131+
- [x] move this plan to `docs/plans/completed/`
132+
133+
## Post-Completion
134+
135+
**Manual verification:**
136+
- test with narrow terminal widths (< 80 cols) to verify truncation
137+
- test with large diffs (many hunks) to verify hunk counter
138+
- test collapsed mode + filter active to verify both icons show
139+
- verify help overlay looks correct with different color themes

ui/model.go

Lines changed: 48 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/charmbracelet/bubbles/viewport"
1212
tea "github.com/charmbracelet/bubbletea"
1313
"github.com/charmbracelet/lipgloss"
14+
"github.com/mattn/go-runewidth"
1415

1516
"github.com/umputun/revdiff/annotation"
1617
"github.com/umputun/revdiff/diff"
@@ -520,14 +521,9 @@ func (m Model) statusBarText() string {
520521
// build left-side segments
521522
var segments []string
522523

523-
// filename segment
524+
// filename and diff stats segments
524525
if m.currFile != "" {
525-
segments = append(segments, m.currFile)
526-
}
527-
528-
// diff stats segment
529-
if m.currFile != "" {
530-
segments = append(segments, fmt.Sprintf("+%d/-%d", m.fileAdds, m.fileRemoves))
526+
segments = append(segments, m.currFile, fmt.Sprintf("+%d/-%d", m.fileAdds, m.fileRemoves))
531527
}
532528

533529
// hunk position (only when cursor is on a changed line in diff pane)
@@ -548,41 +544,57 @@ func (m Model) statusBarText() string {
548544
// build right-side segments
549545
var rightParts []string
550546
if cnt := m.store.Count(); cnt > 0 {
551-
rightParts = append(rightParts, fmt.Sprintf("%d annotations", cnt))
547+
suffix := "annotations"
548+
if cnt == 1 {
549+
suffix = "annotation"
550+
}
551+
rightParts = append(rightParts, fmt.Sprintf("%d %s", cnt, suffix))
552552
}
553553
rightParts = append(rightParts, "? help")
554554

555555
left := strings.Join(segments, " ")
556556
right := strings.Join(rightParts, " ")
557557

558558
// truncate filename from left with … if status line is too wide
559-
minRight := len(right) + 4 // 2 for status bar padding + 2 for separator
559+
minRight := lipgloss.Width(right) + 4 // 2 for status bar padding + 2 for separator
560560
available := max(m.width-minRight, 0)
561561

562562
// graceful degradation: drop segments from right to left when too narrow
563-
if len(left) > available {
563+
if lipgloss.Width(left) > available {
564564
// rebuild without mode icons first
565565
segments = m.statusSegmentsNoIcons()
566566
left = strings.Join(segments, " ")
567567
}
568-
if len(left) > available {
568+
if lipgloss.Width(left) > available {
569569
// rebuild without hunk info
570570
segments = m.statusSegmentsMinimal()
571571
left = strings.Join(segments, " ")
572572
}
573-
if len(left) > available && m.currFile != "" {
574-
// truncate filename
573+
if lipgloss.Width(left) > available && m.currFile != "" {
574+
// truncate filename from left, keeping end of path.
575+
// uses display-width measurement to handle wide characters (CJK, emoji)
575576
statsStr := fmt.Sprintf("+%d/-%d", m.fileAdds, m.fileRemoves)
576-
nameMax := max(available-len(statsStr)-2, 4) // 2 for separator between name and stats
577+
nameMax := max(available-lipgloss.Width(statsStr)-2, 4) // 2 for separator between name and stats
577578
name := m.currFile
578-
if len(name) > nameMax {
579-
name = "…" + name[len(name)-nameMax+1:]
579+
if lipgloss.Width(name) > nameMax {
580+
budget := nameMax - 1 // reserve 1 cell for "…"
581+
runes := []rune(name)
582+
w, cutIdx := 0, len(runes)
583+
for i := len(runes) - 1; i >= 0; i-- {
584+
rw := runewidth.RuneWidth(runes[i])
585+
if w+rw > budget {
586+
break
587+
}
588+
w += rw
589+
cutIdx = i
590+
}
591+
name = "…" + string(runes[cutIdx:])
580592
}
581593
left = name + " " + statsStr
582594
}
583595

584596
// pad left to push right section to the end
585-
padding := m.width - len(left) - len(right) - 2 // 2 for status bar padding
597+
padding := m.width - lipgloss.Width(left) - lipgloss.Width(right) - 2 // 2 for status bar padding
586598
if padding > 0 {
587599
return left + strings.Repeat(" ", padding) + right
588600
}
@@ -619,28 +631,31 @@ func (m Model) statusSegmentsMinimal() []string {
619631
func (m Model) helpOverlay() string {
620632
help := "" +
621633
"Navigation\n" +
622-
" tab switch pane\n" +
623-
" n / p next / prev file\n" +
624-
" j / k scroll down / up\n" +
625-
" g / G top / bottom\n" +
626-
" h / l scroll left / right\n" +
627-
" { / } prev / next hunk\n" +
628-
" enter focus diff pane\n" +
634+
" tab switch pane\n" +
635+
" n / p next / prev file\n" +
636+
" j / k scroll down / up\n" +
637+
" PgDn/PgUp page down / up\n" +
638+
" Ctrl+d/u half-page down / up\n" +
639+
" Home/End top / bottom\n" +
640+
" h / l focus tree / diff pane\n" +
641+
" \u2190 / \u2192 scroll left / right (diff)\n" +
642+
" [ / ] prev / next hunk\n" +
643+
" enter focus diff pane\n" +
629644
"\n" +
630645
"Annotations\n" +
631-
" enter annotate line (diff pane)\n" +
632-
" A annotate file\n" +
633-
" f filter annotated files\n" +
646+
" a / enter annotate line (diff pane)\n" +
647+
" A annotate file\n" +
648+
" d delete annotation\n" +
634649
"\n" +
635650
"View\n" +
636-
" v toggle collapsed mode\n" +
637-
" . expand/collapse hunk\n" +
638-
" [ / ] narrow / widen tree\n" +
651+
" v toggle collapsed mode\n" +
652+
" . expand/collapse hunk\n" +
653+
" f filter annotated files\n" +
639654
"\n" +
640655
"Quit\n" +
641-
" q quit\n" +
642-
" Q discard annotations & quit\n" +
643-
" ? / esc close help"
656+
" q quit\n" +
657+
" Q discard annotations & quit\n" +
658+
" ? / esc close help"
644659

645660
border := lipgloss.NormalBorder()
646661
boxStyle := lipgloss.NewStyle().

0 commit comments

Comments
 (0)