detached 239bcd7

Merge pull request #100 from bnema/fix/command-picker-centered-default

fix(palette): center command picker by default
commit
239bcd776d05ce75e0b0eac42d7f4267c1765929
author
bnema · · 2026-07-16 12:58 UTC
committer
GitHub · · 2026-07-16 12:58 UTC
merge of
1beec74 + 88ed2de · diff against the first parent
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
4 files · +15 -8
M docs/configuration.md +2 -2
@@ -8,7 +8,7 @@ theme = auto
88
99 # Palette placement: auto, center, top-left, top, top-right, left, right,
1010 # bottom-left, bottom, or bottom-right.
11-palette.anchor = auto
11+palette.anchor = center
1212
1313 # One prewarmed floating terminal per tab. An empty command uses the normal shell.
1414 floating.command =
@@ -86,7 +86,7 @@ Set `copy.word-separators = ""` to use only Unicode whitespace as a separator.
8686
8787 ## Palette anchor
8888
89-`auto` uses a bottom shelf on narrow terminals and a bottom-right rail from 96 columns up. Reload moves an open palette without losing your query.
89+The command palette is centered by default. Set `palette.anchor` to another anchor to reposition it, or use `auto` for a bottom shelf on narrow terminals and a bottom-right rail from 96 columns up. Reload moves an open palette without losing your query.
9090
9191 ## Floating terminal
9292
@@ -8,7 +8,7 @@ theme = auto
8 8
9 # Palette placement: auto, center, top-left, top, top-right, left, right,9 # Palette placement: auto, center, top-left, top, top-right, left, right,
10 # bottom-left, bottom, or bottom-right.10 # bottom-left, bottom, or bottom-right.
11-palette.anchor = auto11+palette.anchor = center
12 12
13 # One prewarmed floating terminal per tab. An empty command uses the normal shell.13 # One prewarmed floating terminal per tab. An empty command uses the normal shell.
14 floating.command =14 floating.command =
@@ -86,7 +86,7 @@ Set `copy.word-separators = ""` to use only Unicode whitespace as a separator.
86 86
87 ## Palette anchor87 ## Palette anchor
88 88
89-`auto` uses a bottom shelf on narrow terminals and a bottom-right rail from 96 columns up. Reload moves an open palette without losing your query.89+The command palette is centered by default. Set `palette.anchor` to another anchor to reposition it, or use `auto` for a bottom shelf on narrow terminals and a bottom-right rail from 96 columns up. Reload moves an open palette without losing your query.
90 90
91 ## Floating terminal91 ## Floating terminal
92 92
M internal/adapters/config/config_test.go +3 -0
@@ -347,6 +347,9 @@ func TestParse(t *testing.T) {
347347 if tt.want.Copy.WordSeparators == "" {
348348 tt.want.Copy = domain.Defaults().Copy
349349 }
350+ if !tt.want.Palette.AnchorSet {
351+ tt.want.Palette = domain.Defaults().Palette
352+ }
350353 got, warnings, err := Parse(strings.NewReader(tt.input))
351354 if err != nil {
352355 t.Fatalf("Parse() error = %v", err)
@@ -347,6 +347,9 @@ func TestParse(t *testing.T) {
347 if tt.want.Copy.WordSeparators == "" {347 if tt.want.Copy.WordSeparators == "" {
348 tt.want.Copy = domain.Defaults().Copy348 tt.want.Copy = domain.Defaults().Copy
349 }349 }
350+ if !tt.want.Palette.AnchorSet {
351+ tt.want.Palette = domain.Defaults().Palette
352+ }
350 got, warnings, err := Parse(strings.NewReader(tt.input))353 got, warnings, err := Parse(strings.NewReader(tt.input))
351 if err != nil {354 if err != nil {
352 t.Fatalf("Parse() error = %v", err)355 t.Fatalf("Parse() error = %v", err)
M internal/adapters/config/palette_anchor_test.go +6 -6
@@ -21,10 +21,10 @@ func TestParsePaletteAnchor(t *testing.T) {
2121 want domain.PaletteConfig
2222 warnings []domain.Warning
2323 }{
24- {name: "default is auto", want: domain.PaletteConfig{}},
24+ {name: "default is centered", want: domain.PaletteConfig{Anchor: domain.AnchorCenter, AnchorSet: true}},
2525 {name: "explicit auto is case insensitive", input: "palette.anchor = AuTo\n", want: domain.PaletteConfig{}},
2626 {name: "concrete anchors are trimmed and case insensitive", input: "palette.anchor = CENTER\npalette.anchor = top-left\npalette.anchor = Top\npalette.anchor = TOP-RIGHT\npalette.anchor = left\npalette.anchor = RIGHT\npalette.anchor = bottom-left\npalette.anchor = Bottom\npalette.anchor = BOTTOM-RIGHT \n", want: domain.PaletteConfig{Anchor: domain.AnchorBottomRight, AnchorSet: true}, warnings: []domain.Warning{{Line: 2, Msg: "duplicate key \"palette.anchor\""}, {Line: 3, Msg: "duplicate key \"palette.anchor\""}, {Line: 4, Msg: "duplicate key \"palette.anchor\""}, {Line: 5, Msg: "duplicate key \"palette.anchor\""}, {Line: 6, Msg: "duplicate key \"palette.anchor\""}, {Line: 7, Msg: "duplicate key \"palette.anchor\""}, {Line: 8, Msg: "duplicate key \"palette.anchor\""}, {Line: 9, Msg: "duplicate key \"palette.anchor\""}}},
27- {name: "invalid first preserves auto", input: "palette.anchor = diagonal\n", want: domain.PaletteConfig{}, warnings: []domain.Warning{{Line: 1, Msg: "invalid palette.anchor \"diagonal\""}}},
27+ {name: "invalid first preserves centered default", input: "palette.anchor = diagonal\n", want: domain.PaletteConfig{Anchor: domain.AnchorCenter, AnchorSet: true}, warnings: []domain.Warning{{Line: 1, Msg: "invalid palette.anchor \"diagonal\""}}},
2828 {name: "valid then invalid preserves valid", input: "palette.anchor = top\npalette.anchor = diagonal\n", want: domain.PaletteConfig{Anchor: domain.AnchorTop, AnchorSet: true}, warnings: []domain.Warning{{Line: 2, Msg: "duplicate key \"palette.anchor\""}, {Line: 2, Msg: "invalid palette.anchor \"diagonal\""}}},
2929 {name: "explicit then auto clears to auto", input: "palette.anchor = left\npalette.anchor = auto\n", want: domain.PaletteConfig{}, warnings: []domain.Warning{{Line: 2, Msg: "duplicate key \"palette.anchor\""}}},
3030 }
@@ -39,15 +39,15 @@ func TestParsePaletteAnchor(t *testing.T) {
3939 }
4040 }
4141
42-func TestLoadMissingPaletteAnchorDefaultsToAuto(t *testing.T) {
42+func TestLoadMissingPaletteAnchorDefaultsToCenter(t *testing.T) {
4343 t.Parallel()
4444 cfg, warnings, err := Load(filepath.Join(t.TempDir(), "missing"))
4545 require.NoError(t, err)
4646 require.Empty(t, warnings)
47- require.Equal(t, domain.PaletteConfig{}, cfg.Palette)
47+ require.Equal(t, domain.PaletteConfig{Anchor: domain.AnchorCenter, AnchorSet: true}, cfg.Palette)
4848 }
4949
50-func TestWatchPaletteAnchorRestoresAutoAfterFileRemoval(t *testing.T) {
50+func TestWatchPaletteAnchorRestoresCenteredDefaultAfterFileRemoval(t *testing.T) {
5151 path := filepath.Join(t.TempDir(), "config")
5252 require.NoError(t, os.WriteFile(path, []byte("palette.anchor = top-right\n"), 0o600))
5353 clk := newFakeClock(time.Unix(0, 0))
@@ -66,7 +66,7 @@ func TestWatchPaletteAnchorRestoresAutoAfterFileRemoval(t *testing.T) {
6666 clk.waitForTimers(t, 1)
6767 clk.advance(2 * time.Second)
6868 got := <-changes
69- require.Equal(t, domain.PaletteConfig{}, got.Palette)
69+ require.Equal(t, domain.PaletteConfig{Anchor: domain.AnchorCenter, AnchorSet: true}, got.Palette)
7070 cancel()
7171 require.ErrorIs(t, <-done, context.Canceled)
7272 }
@@ -21,10 +21,10 @@ func TestParsePaletteAnchor(t *testing.T) {
21 want domain.PaletteConfig21 want domain.PaletteConfig
22 warnings []domain.Warning22 warnings []domain.Warning
23 }{23 }{
24- {name: "default is auto", want: domain.PaletteConfig{}},24+ {name: "default is centered", want: domain.PaletteConfig{Anchor: domain.AnchorCenter, AnchorSet: true}},
25 {name: "explicit auto is case insensitive", input: "palette.anchor = AuTo\n", want: domain.PaletteConfig{}},25 {name: "explicit auto is case insensitive", input: "palette.anchor = AuTo\n", want: domain.PaletteConfig{}},
26 {name: "concrete anchors are trimmed and case insensitive", input: "palette.anchor = CENTER\npalette.anchor = top-left\npalette.anchor = Top\npalette.anchor = TOP-RIGHT\npalette.anchor = left\npalette.anchor = RIGHT\npalette.anchor = bottom-left\npalette.anchor = Bottom\npalette.anchor = BOTTOM-RIGHT \n", want: domain.PaletteConfig{Anchor: domain.AnchorBottomRight, AnchorSet: true}, warnings: []domain.Warning{{Line: 2, Msg: "duplicate key \"palette.anchor\""}, {Line: 3, Msg: "duplicate key \"palette.anchor\""}, {Line: 4, Msg: "duplicate key \"palette.anchor\""}, {Line: 5, Msg: "duplicate key \"palette.anchor\""}, {Line: 6, Msg: "duplicate key \"palette.anchor\""}, {Line: 7, Msg: "duplicate key \"palette.anchor\""}, {Line: 8, Msg: "duplicate key \"palette.anchor\""}, {Line: 9, Msg: "duplicate key \"palette.anchor\""}}},26 {name: "concrete anchors are trimmed and case insensitive", input: "palette.anchor = CENTER\npalette.anchor = top-left\npalette.anchor = Top\npalette.anchor = TOP-RIGHT\npalette.anchor = left\npalette.anchor = RIGHT\npalette.anchor = bottom-left\npalette.anchor = Bottom\npalette.anchor = BOTTOM-RIGHT \n", want: domain.PaletteConfig{Anchor: domain.AnchorBottomRight, AnchorSet: true}, warnings: []domain.Warning{{Line: 2, Msg: "duplicate key \"palette.anchor\""}, {Line: 3, Msg: "duplicate key \"palette.anchor\""}, {Line: 4, Msg: "duplicate key \"palette.anchor\""}, {Line: 5, Msg: "duplicate key \"palette.anchor\""}, {Line: 6, Msg: "duplicate key \"palette.anchor\""}, {Line: 7, Msg: "duplicate key \"palette.anchor\""}, {Line: 8, Msg: "duplicate key \"palette.anchor\""}, {Line: 9, Msg: "duplicate key \"palette.anchor\""}}},
27- {name: "invalid first preserves auto", input: "palette.anchor = diagonal\n", want: domain.PaletteConfig{}, warnings: []domain.Warning{{Line: 1, Msg: "invalid palette.anchor \"diagonal\""}}},27+ {name: "invalid first preserves centered default", input: "palette.anchor = diagonal\n", want: domain.PaletteConfig{Anchor: domain.AnchorCenter, AnchorSet: true}, warnings: []domain.Warning{{Line: 1, Msg: "invalid palette.anchor \"diagonal\""}}},
28 {name: "valid then invalid preserves valid", input: "palette.anchor = top\npalette.anchor = diagonal\n", want: domain.PaletteConfig{Anchor: domain.AnchorTop, AnchorSet: true}, warnings: []domain.Warning{{Line: 2, Msg: "duplicate key \"palette.anchor\""}, {Line: 2, Msg: "invalid palette.anchor \"diagonal\""}}},28 {name: "valid then invalid preserves valid", input: "palette.anchor = top\npalette.anchor = diagonal\n", want: domain.PaletteConfig{Anchor: domain.AnchorTop, AnchorSet: true}, warnings: []domain.Warning{{Line: 2, Msg: "duplicate key \"palette.anchor\""}, {Line: 2, Msg: "invalid palette.anchor \"diagonal\""}}},
29 {name: "explicit then auto clears to auto", input: "palette.anchor = left\npalette.anchor = auto\n", want: domain.PaletteConfig{}, warnings: []domain.Warning{{Line: 2, Msg: "duplicate key \"palette.anchor\""}}},29 {name: "explicit then auto clears to auto", input: "palette.anchor = left\npalette.anchor = auto\n", want: domain.PaletteConfig{}, warnings: []domain.Warning{{Line: 2, Msg: "duplicate key \"palette.anchor\""}}},
30 }30 }
@@ -39,15 +39,15 @@ func TestParsePaletteAnchor(t *testing.T) {
39 }39 }
40 }40 }
41 41
42-func TestLoadMissingPaletteAnchorDefaultsToAuto(t *testing.T) {42+func TestLoadMissingPaletteAnchorDefaultsToCenter(t *testing.T) {
43 t.Parallel()43 t.Parallel()
44 cfg, warnings, err := Load(filepath.Join(t.TempDir(), "missing"))44 cfg, warnings, err := Load(filepath.Join(t.TempDir(), "missing"))
45 require.NoError(t, err)45 require.NoError(t, err)
46 require.Empty(t, warnings)46 require.Empty(t, warnings)
47- require.Equal(t, domain.PaletteConfig{}, cfg.Palette)47+ require.Equal(t, domain.PaletteConfig{Anchor: domain.AnchorCenter, AnchorSet: true}, cfg.Palette)
48 }48 }
49 49
50-func TestWatchPaletteAnchorRestoresAutoAfterFileRemoval(t *testing.T) {50+func TestWatchPaletteAnchorRestoresCenteredDefaultAfterFileRemoval(t *testing.T) {
51 path := filepath.Join(t.TempDir(), "config")51 path := filepath.Join(t.TempDir(), "config")
52 require.NoError(t, os.WriteFile(path, []byte("palette.anchor = top-right\n"), 0o600))52 require.NoError(t, os.WriteFile(path, []byte("palette.anchor = top-right\n"), 0o600))
53 clk := newFakeClock(time.Unix(0, 0))53 clk := newFakeClock(time.Unix(0, 0))
@@ -66,7 +66,7 @@ func TestWatchPaletteAnchorRestoresAutoAfterFileRemoval(t *testing.T) {
66 clk.waitForTimers(t, 1)66 clk.waitForTimers(t, 1)
67 clk.advance(2 * time.Second)67 clk.advance(2 * time.Second)
68 got := <-changes68 got := <-changes
69- require.Equal(t, domain.PaletteConfig{}, got.Palette)69+ require.Equal(t, domain.PaletteConfig{Anchor: domain.AnchorCenter, AnchorSet: true}, got.Palette)
70 cancel()70 cancel()
71 require.ErrorIs(t, <-done, context.Canceled)71 require.ErrorIs(t, <-done, context.Canceled)
72 }72 }
M internal/domain/config.go +4 -0
@@ -115,6 +115,10 @@ func Defaults() Config {
115115 Copy: CopyConfig{
116116 WordSeparators: DefaultWordSeparators,
117117 },
118+ Palette: PaletteConfig{
119+ Anchor: AnchorCenter,
120+ AnchorSet: true,
121+ },
118122 Floating: FloatingConfig{
119123 Width: 80,
120124 Height: 80,
@@ -115,6 +115,10 @@ func Defaults() Config {
115 Copy: CopyConfig{115 Copy: CopyConfig{
116 WordSeparators: DefaultWordSeparators,116 WordSeparators: DefaultWordSeparators,
117 },117 },
118+ Palette: PaletteConfig{
119+ Anchor: AnchorCenter,
120+ AnchorSet: true,
121+ },
118 Floating: FloatingConfig{122 Floating: FloatingConfig{
119 Width: 80,123 Width: 80,
120 Height: 80,124 Height: 80,

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 12

Tags 11

↑↓ moveenter openesc close