detached 7b780df

fix(palette): complete navigation and modal geometry

commit
7b780dfb67fa7f0023b0f1365675ca8d97ea2dd6
author
brice · · 2026-07-03 05:52 UTC
committer
brice · · 2026-07-03 07:36 UTC
parent
84a7898
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +44 -1
M README.md +2 -0
@@ -47,6 +47,8 @@ All bindings use the Alt modifier directly, no prefix key.
4747 | Alt+Space | open the command palette |
4848 | Alt+1 .. Alt+9 | switch to tab by number |
4949
50+Some terminals encode Alt+Space as a non-breaking space instead of `ESC Space`; in those terminals, the palette binding depends on whether the terminal can send `ESC Space` for Alt+Space.
51+
5052 ## Command palette
5153
5254 Open the palette with Alt+Space, then type a command code or fuzzy-match its label. Use Up/Down or Ctrl-N/Ctrl-P to move through matches, Enter to run the selected command, and Esc to close the palette. Some terminals emit NBSP for Alt+Space; in those terminals the binding depends on terminal configuration.
@@ -47,6 +47,8 @@ All bindings use the Alt modifier directly, no prefix key.
47 | Alt+Space | open the command palette |47 | Alt+Space | open the command palette |
48 | Alt+1 .. Alt+9 | switch to tab by number |48 | Alt+1 .. Alt+9 | switch to tab by number |
49 49
50+Some terminals encode Alt+Space as a non-breaking space instead of `ESC Space`; in those terminals, the palette binding depends on whether the terminal can send `ESC Space` for Alt+Space.
51+
50 ## Command palette52 ## Command palette
51 53
52 Open the palette with Alt+Space, then type a command code or fuzzy-match its label. Use Up/Down or Ctrl-N/Ctrl-P to move through matches, Enter to run the selected command, and Esc to close the palette. Some terminals emit NBSP for Alt+Space; in those terminals the binding depends on terminal configuration.54 Open the palette with Alt+Space, then type a command code or fuzzy-match its label. Use Up/Down or Ctrl-N/Ctrl-P to move through matches, Enter to run the selected command, and Esc to close the palette. Some terminals emit NBSP for Alt+Space; in those terminals the binding depends on terminal configuration.
M internal/usecase/daemon/palette.go +9 -1
@@ -11,7 +11,7 @@ import (
1111 "github.com/bnema/vev/pkg/renderer"
1212 )
1313
14-var paletteModal = ui.Modal{WidthPct: 80, HeightPct: 40, MinWidth: 32, MinHeight: 6, Title: " Commands ", Anchor: ui.AnchorBottom, BottomMargin: 1}
14+var paletteModal = ui.Modal{WidthPct: 100, MinWidth: 32, FixedHeight: 11, Title: " Commands ", Anchor: ui.AnchorBottom, BottomMargin: 1}
1515
1616 func (d *Daemon) enterPalette(sess *session, ac *attachedClient) {
1717 d.closePalette(ac)
@@ -74,6 +74,14 @@ func (d *Daemon) handlePaletteInput(ac *attachedClient, data []byte) {
7474 case 0x03:
7575 exit = true
7676 i++
77+ case 0x0e:
78+ ac.palette.Down()
79+ changed = true
80+ i++
81+ case 0x10:
82+ ac.palette.Up()
83+ changed = true
84+ i++
7785 case '\r', '\n':
7886 cmd, ok = ac.palette.Selected()
7987 if ok {
@@ -11,7 +11,7 @@ import (
11 "github.com/bnema/vev/pkg/renderer"11 "github.com/bnema/vev/pkg/renderer"
12 )12 )
13 13
14-var paletteModal = ui.Modal{WidthPct: 80, HeightPct: 40, MinWidth: 32, MinHeight: 6, Title: " Commands ", Anchor: ui.AnchorBottom, BottomMargin: 1}14+var paletteModal = ui.Modal{WidthPct: 100, MinWidth: 32, FixedHeight: 11, Title: " Commands ", Anchor: ui.AnchorBottom, BottomMargin: 1}
15 15
16 func (d *Daemon) enterPalette(sess *session, ac *attachedClient) {16 func (d *Daemon) enterPalette(sess *session, ac *attachedClient) {
17 d.closePalette(ac)17 d.closePalette(ac)
@@ -74,6 +74,14 @@ func (d *Daemon) handlePaletteInput(ac *attachedClient, data []byte) {
74 case 0x03:74 case 0x03:
75 exit = true75 exit = true
76 i++76 i++
77+ case 0x0e:
78+ ac.palette.Down()
79+ changed = true
80+ i++
81+ case 0x10:
82+ ac.palette.Up()
83+ changed = true
84+ i++
77 case '\r', '\n':85 case '\r', '\n':
78 cmd, ok = ac.palette.Selected()86 cmd, ok = ac.palette.Selected()
79 if ok {87 if ok {
M internal/usecase/daemon/palette_test.go +33 -0
@@ -5,7 +5,9 @@ import (
55
66 "github.com/stretchr/testify/require"
77
8+ "github.com/bnema/vev/internal/domain"
89 "github.com/bnema/vev/internal/ports"
10+ "github.com/bnema/vev/internal/usecase/ui"
911 )
1012
1113 func TestPaletteOpenTypeEnterRunAndEscClose(t *testing.T) {
@@ -53,6 +55,37 @@ func TestPaletteEnterNoMatchKeepsOpenAndEscapeSplit(t *testing.T) {
5355 awaitFrame(t, sends, ports.MsgOutput)
5456 }
5557
58+func TestPaletteCtrlNAndCtrlPNavigate(t *testing.T) {
59+ d, sess, ac, sends, releases := newManualTabSession(t, 1)
60+ defer releases[0]()
61+
62+ d.handleInput(sess, ac, []byte("\x1b "))
63+ awaitFrame(t, sends, ports.MsgOutput)
64+
65+ d.handlePaletteInput(ac, []byte{0x0e})
66+ awaitFrame(t, sends, ports.MsgOutput)
67+ cmd, ok := ac.palette.Selected()
68+ require.True(t, ok)
69+ require.Equal(t, "CLT", cmd.Code)
70+
71+ d.handlePaletteInput(ac, []byte{0x10})
72+ awaitFrame(t, sends, ports.MsgOutput)
73+ cmd, ok = ac.palette.Selected()
74+ require.True(t, ok)
75+ require.Equal(t, "CNT", cmd.Code)
76+}
77+
78+func TestPaletteModalGeometry(t *testing.T) {
79+ require.Equal(t, 100, paletteModal.WidthPct)
80+ require.Equal(t, 11, paletteModal.FixedHeight)
81+ require.Equal(t, ui.AnchorBottom, paletteModal.Anchor)
82+ require.Equal(t, 1, paletteModal.BottomMargin)
83+ require.Equal(t, 32, paletteModal.MinWidth)
84+
85+ bounds := paletteModal.Bounds(domain.Size{Cols: 120, Rows: 40})
86+ require.Equal(t, domain.Rect{X: 0, Y: 28, Width: 120, Height: 11}, bounds)
87+}
88+
5689 func TestPaletteUTF8PendingCompletesFilter(t *testing.T) {
5790 d, sess, ac, sends, releases := newManualTabSession(t, 1)
5891 defer releases[0]()
@@ -5,7 +5,9 @@ import (
5 5
6 "github.com/stretchr/testify/require"6 "github.com/stretchr/testify/require"
7 7
8+ "github.com/bnema/vev/internal/domain"
8 "github.com/bnema/vev/internal/ports"9 "github.com/bnema/vev/internal/ports"
10+ "github.com/bnema/vev/internal/usecase/ui"
9 )11 )
10 12
11 func TestPaletteOpenTypeEnterRunAndEscClose(t *testing.T) {13 func TestPaletteOpenTypeEnterRunAndEscClose(t *testing.T) {
@@ -53,6 +55,37 @@ func TestPaletteEnterNoMatchKeepsOpenAndEscapeSplit(t *testing.T) {
53 awaitFrame(t, sends, ports.MsgOutput)55 awaitFrame(t, sends, ports.MsgOutput)
54 }56 }
55 57
58+func TestPaletteCtrlNAndCtrlPNavigate(t *testing.T) {
59+ d, sess, ac, sends, releases := newManualTabSession(t, 1)
60+ defer releases[0]()
61+
62+ d.handleInput(sess, ac, []byte("\x1b "))
63+ awaitFrame(t, sends, ports.MsgOutput)
64+
65+ d.handlePaletteInput(ac, []byte{0x0e})
66+ awaitFrame(t, sends, ports.MsgOutput)
67+ cmd, ok := ac.palette.Selected()
68+ require.True(t, ok)
69+ require.Equal(t, "CLT", cmd.Code)
70+
71+ d.handlePaletteInput(ac, []byte{0x10})
72+ awaitFrame(t, sends, ports.MsgOutput)
73+ cmd, ok = ac.palette.Selected()
74+ require.True(t, ok)
75+ require.Equal(t, "CNT", cmd.Code)
76+}
77+
78+func TestPaletteModalGeometry(t *testing.T) {
79+ require.Equal(t, 100, paletteModal.WidthPct)
80+ require.Equal(t, 11, paletteModal.FixedHeight)
81+ require.Equal(t, ui.AnchorBottom, paletteModal.Anchor)
82+ require.Equal(t, 1, paletteModal.BottomMargin)
83+ require.Equal(t, 32, paletteModal.MinWidth)
84+
85+ bounds := paletteModal.Bounds(domain.Size{Cols: 120, Rows: 40})
86+ require.Equal(t, domain.Rect{X: 0, Y: 28, Width: 120, Height: 11}, bounds)
87+}
88+
56 func TestPaletteUTF8PendingCompletesFilter(t *testing.T) {89 func TestPaletteUTF8PendingCompletesFilter(t *testing.T) {
57 d, sess, ac, sends, releases := newManualTabSession(t, 1)90 d, sess, ac, sends, releases := newManualTabSession(t, 1)
58 defer releases[0]()91 defer releases[0]()

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 13

↑↓ moveenter openesc close