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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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. | |||
| 47 | 47 | | Alt+Space | open the command palette | | |
| 48 | 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 | 52 | ## Command palette | |
| 51 | 53 | ||
| 52 | 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. | |
| @@ -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 palette | 52 | ## 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 ( | |||
| 11 | 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 | 16 | func (d *Daemon) enterPalette(sess *session, ac *attachedClient) { | |
| 17 | 17 | d.closePalette(ac) | |
| @@ -74,6 +74,14 @@ func (d *Daemon) handlePaletteInput(ac *attachedClient, data []byte) { | |||
| 74 | 74 | case 0x03: | |
| 75 | 75 | exit = true | |
| 76 | 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 | 85 | case '\r', '\n': | |
| 78 | 86 | cmd, ok = ac.palette.Selected() | |
| 79 | 87 | 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 = true | 75 | 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 ( | |||
| 5 | 5 | ||
| 6 | 6 | "github.com/stretchr/testify/require" | |
| 7 | 7 | ||
| 8 | + | "github.com/bnema/vev/internal/domain" | |
| 8 | 9 | "github.com/bnema/vev/internal/ports" | |
| 10 | + | "github.com/bnema/vev/internal/usecase/ui" | |
| 9 | 11 | ) | |
| 10 | 12 | ||
| 11 | 13 | func TestPaletteOpenTypeEnterRunAndEscClose(t *testing.T) { | |
| @@ -53,6 +55,37 @@ func TestPaletteEnterNoMatchKeepsOpenAndEscapeSplit(t *testing.T) { | |||
| 53 | 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 | 89 | func TestPaletteUTF8PendingCompletesFilter(t *testing.T) { | |
| 57 | 90 | d, sess, ac, sends, releases := newManualTabSession(t, 1) | |
| 58 | 91 | 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]() | ||