detached 9d0a15c

feat(config): add navigation overflow settings

commit
9d0a15cfa0771b268ca663289cdcae1e8d4930cd
author
brice · · 2026-07-25 05:04 UTC
parent
20a6275
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
7 files · +139 -0
M docs/configuration.md +10 -0
@@ -19,6 +19,10 @@ floating.command =
1919 floating.width = 80%
2020 floating.height = 80%
2121
22+# Let directional keyboard focus continue past a pane edge. Both default off.
23+nav.overflow-tabs = off
24+nav.overflow-sessions = off
25+
2226 # Show the focused pane's terminal title in tab labels; off keeps the process name only.
2327 tabs.terminal-title = on
2428
@@ -84,6 +88,12 @@ Slots `0`, `7`, `8`, and `15` are valid explicit selections, but log a warning b
8488
8589 Key specs: `alt+<char>`, `alt+space`, `alt+left/right/up/down`, `alt+1` through `alt+9`. Configuring an action replaces all of its built-in aliases (set `focus-pane-left` and the Alt+Arrow alias is gone). Tab switching also accepts the top-row symbols of non-QWERTY layouts, so AZERTY works without extra config.
8690
91+## Navigation overflow
92+
93+Both settings are independent and default to `off`. With `nav.overflow-tabs = on`, left/right keyboard focus (`Alt+H`/`Alt+L` by default) continues from a pane edge to the adjacent tab. With `nav.overflow-sessions = on`, up/down keyboard focus (`Alt+K`/`Alt+J` by default) continues to the adjacent live session in alphabetical order. Neither setting wraps at the first or last destination.
94+
95+Overflow applies only to keyboard focus actions; mouse navigation does not overflow, and floating panes never overflow. Hot reload applies either setting to subsequent navigation without restarting the daemon.
96+
8797 ## Copy mode
8898
8999 Scroll up with the mouse to enter copy mode. Use `h`, `j`, `k`, and `l` to move, `w`, `b`, and `e` for word motions, `v` or Space to start line selection, and `y` or Enter to copy.
@@ -19,6 +19,10 @@ floating.command =
19 floating.width = 80%19 floating.width = 80%
20 floating.height = 80%20 floating.height = 80%
21 21
22+# Let directional keyboard focus continue past a pane edge. Both default off.
23+nav.overflow-tabs = off
24+nav.overflow-sessions = off
25+
22 # Show the focused pane's terminal title in tab labels; off keeps the process name only.26 # Show the focused pane's terminal title in tab labels; off keeps the process name only.
23 tabs.terminal-title = on27 tabs.terminal-title = on
24 28
@@ -84,6 +88,12 @@ Slots `0`, `7`, `8`, and `15` are valid explicit selections, but log a warning b
84 88
85 Key specs: `alt+<char>`, `alt+space`, `alt+left/right/up/down`, `alt+1` through `alt+9`. Configuring an action replaces all of its built-in aliases (set `focus-pane-left` and the Alt+Arrow alias is gone). Tab switching also accepts the top-row symbols of non-QWERTY layouts, so AZERTY works without extra config.89 Key specs: `alt+<char>`, `alt+space`, `alt+left/right/up/down`, `alt+1` through `alt+9`. Configuring an action replaces all of its built-in aliases (set `focus-pane-left` and the Alt+Arrow alias is gone). Tab switching also accepts the top-row symbols of non-QWERTY layouts, so AZERTY works without extra config.
86 90
91+## Navigation overflow
92+
93+Both settings are independent and default to `off`. With `nav.overflow-tabs = on`, left/right keyboard focus (`Alt+H`/`Alt+L` by default) continues from a pane edge to the adjacent tab. With `nav.overflow-sessions = on`, up/down keyboard focus (`Alt+K`/`Alt+J` by default) continues to the adjacent live session in alphabetical order. Neither setting wraps at the first or last destination.
94+
95+Overflow applies only to keyboard focus actions; mouse navigation does not overflow, and floating panes never overflow. Hot reload applies either setting to subsequent navigation without restarting the daemon.
96+
87 ## Copy mode97 ## Copy mode
88 98
89 Scroll up with the mouse to enter copy mode. Use `h`, `j`, `k`, and `l` to move, `w`, `b`, and `e` for word motions, `v` or Space to start line selection, and `y` or Enter to copy.99 Scroll up with the mouse to enter copy mode. Use `h`, `j`, `k`, and `l` to move, `w`, `b`, and `e` for word motions, `v` or Space to start line selection, and `y` or Enter to copy.
M internal/adapters/config/config.go +17 -0
@@ -33,6 +33,7 @@ func Parse(r io.Reader) (domain.Config, []domain.Warning, error) {
3333 seenFloatingKeys := make(map[string]bool)
3434 seenCopyKeys := make(map[string]bool)
3535 seenPaletteKeys := make(map[string]bool)
36+ seenNavKeys := make(map[string]bool)
3637 seenTabsKeys := make(map[string]bool)
3738
3839 scanner := bufio.NewScanner(r)
@@ -118,6 +119,22 @@ func Parse(r io.Reader) (domain.Config, []domain.Warning, error) {
118119 continue
119120 }
120121 cfg.Palette = domain.PaletteConfig{Anchor: anchor, AnchorSet: true}
122+ case key == "nav.overflow-tabs":
123+ warnings = warnDuplicateKey(warnings, seenNavKeys, key, lineNo)
124+ on, ok := parseOnOff(value)
125+ if !ok {
126+ warnings = append(warnings, domain.Warning{Line: lineNo, Msg: fmt.Sprintf("invalid nav.overflow-tabs %q", value)})
127+ continue
128+ }
129+ cfg.Nav.OverflowTabs = on
130+ case key == "nav.overflow-sessions":
131+ warnings = warnDuplicateKey(warnings, seenNavKeys, key, lineNo)
132+ on, ok := parseOnOff(value)
133+ if !ok {
134+ warnings = append(warnings, domain.Warning{Line: lineNo, Msg: fmt.Sprintf("invalid nav.overflow-sessions %q", value)})
135+ continue
136+ }
137+ cfg.Nav.OverflowSessions = on
121138 case key == "tabs.terminal-title":
122139 warnings = warnDuplicateKey(warnings, seenTabsKeys, key, lineNo)
123140 on, ok := parseOnOff(value)
@@ -33,6 +33,7 @@ func Parse(r io.Reader) (domain.Config, []domain.Warning, error) {
33 seenFloatingKeys := make(map[string]bool)33 seenFloatingKeys := make(map[string]bool)
34 seenCopyKeys := make(map[string]bool)34 seenCopyKeys := make(map[string]bool)
35 seenPaletteKeys := make(map[string]bool)35 seenPaletteKeys := make(map[string]bool)
36+ seenNavKeys := make(map[string]bool)
36 seenTabsKeys := make(map[string]bool)37 seenTabsKeys := make(map[string]bool)
37 38
38 scanner := bufio.NewScanner(r)39 scanner := bufio.NewScanner(r)
@@ -118,6 +119,22 @@ func Parse(r io.Reader) (domain.Config, []domain.Warning, error) {
118 continue119 continue
119 }120 }
120 cfg.Palette = domain.PaletteConfig{Anchor: anchor, AnchorSet: true}121 cfg.Palette = domain.PaletteConfig{Anchor: anchor, AnchorSet: true}
122+ case key == "nav.overflow-tabs":
123+ warnings = warnDuplicateKey(warnings, seenNavKeys, key, lineNo)
124+ on, ok := parseOnOff(value)
125+ if !ok {
126+ warnings = append(warnings, domain.Warning{Line: lineNo, Msg: fmt.Sprintf("invalid nav.overflow-tabs %q", value)})
127+ continue
128+ }
129+ cfg.Nav.OverflowTabs = on
130+ case key == "nav.overflow-sessions":
131+ warnings = warnDuplicateKey(warnings, seenNavKeys, key, lineNo)
132+ on, ok := parseOnOff(value)
133+ if !ok {
134+ warnings = append(warnings, domain.Warning{Line: lineNo, Msg: fmt.Sprintf("invalid nav.overflow-sessions %q", value)})
135+ continue
136+ }
137+ cfg.Nav.OverflowSessions = on
121 case key == "tabs.terminal-title":138 case key == "tabs.terminal-title":
122 warnings = warnDuplicateKey(warnings, seenTabsKeys, key, lineNo)139 warnings = warnDuplicateKey(warnings, seenTabsKeys, key, lineNo)
123 on, ok := parseOnOff(value)140 on, ok := parseOnOff(value)
M internal/adapters/config/config_test.go +77 -0
@@ -476,6 +476,83 @@ func TestParseTabsTerminalTitle(t *testing.T) {
476476 }
477477 }
478478
479+func TestParseNavigationOverflow(t *testing.T) {
480+ t.Parallel()
481+
482+ tests := []struct {
483+ name string
484+ input string
485+ want domain.NavConfig
486+ wantWarnings []domain.Warning
487+ }{
488+ {
489+ name: "absent defaults both settings off",
490+ },
491+ {
492+ name: "tabs on enables only tab overflow",
493+ input: "nav.overflow-tabs = on\n",
494+ want: domain.NavConfig{OverflowTabs: true},
495+ },
496+ {
497+ name: "sessions on enables only session overflow",
498+ input: "nav.overflow-sessions = on\n",
499+ want: domain.NavConfig{OverflowSessions: true},
500+ },
501+ {
502+ name: "off disables both settings",
503+ input: "nav.overflow-tabs = off\nnav.overflow-sessions = off\n",
504+ },
505+ {
506+ name: "settings are independent",
507+ input: "nav.overflow-tabs = on\nnav.overflow-sessions = off\n",
508+ want: domain.NavConfig{OverflowTabs: true},
509+ },
510+ {
511+ name: "invalid values warn and preserve defaults",
512+ input: "nav.overflow-tabs = maybe\nnav.overflow-sessions = always\n",
513+ wantWarnings: []domain.Warning{
514+ {Line: 1, Msg: `invalid nav.overflow-tabs "maybe"`},
515+ {Line: 2, Msg: `invalid nav.overflow-sessions "always"`},
516+ },
517+ },
518+ {
519+ name: "duplicate keys warn and last valid value wins",
520+ input: "nav.overflow-tabs = off\nnav.overflow-tabs = on\nnav.overflow-sessions = on\nnav.overflow-sessions = off\n",
521+ want: domain.NavConfig{OverflowTabs: true},
522+ wantWarnings: []domain.Warning{
523+ {Line: 2, Msg: `duplicate key "nav.overflow-tabs"`},
524+ {Line: 4, Msg: `duplicate key "nav.overflow-sessions"`},
525+ },
526+ },
527+ {
528+ name: "invalid duplicate still warns and keeps previous value",
529+ input: "nav.overflow-tabs = on\nnav.overflow-tabs = maybe\n",
530+ want: domain.NavConfig{OverflowTabs: true},
531+ wantWarnings: []domain.Warning{
532+ {Line: 2, Msg: `duplicate key "nav.overflow-tabs"`},
533+ {Line: 2, Msg: `invalid nav.overflow-tabs "maybe"`},
534+ },
535+ },
536+ }
537+
538+ for _, tt := range tests {
539+ t.Run(tt.name, func(t *testing.T) {
540+ t.Parallel()
541+
542+ cfg, warnings, err := Parse(strings.NewReader(tt.input))
543+ require.NoError(t, err)
544+ require.Equal(t, tt.want, cfg.Nav)
545+ require.Equal(t, tt.wantWarnings, warnings)
546+ })
547+ }
548+}
549+
550+func TestDefaultsDisableNavigationOverflow(t *testing.T) {
551+ t.Parallel()
552+
553+ require.Equal(t, domain.NavConfig{}, domain.Defaults().Nav)
554+}
555+
479556 func TestDefaultsCopiesSnapshotRestoreProcesses(t *testing.T) {
480557 t.Parallel()
481558
@@ -476,6 +476,83 @@ func TestParseTabsTerminalTitle(t *testing.T) {
476 }476 }
477 }477 }
478 478
479+func TestParseNavigationOverflow(t *testing.T) {
480+ t.Parallel()
481+
482+ tests := []struct {
483+ name string
484+ input string
485+ want domain.NavConfig
486+ wantWarnings []domain.Warning
487+ }{
488+ {
489+ name: "absent defaults both settings off",
490+ },
491+ {
492+ name: "tabs on enables only tab overflow",
493+ input: "nav.overflow-tabs = on\n",
494+ want: domain.NavConfig{OverflowTabs: true},
495+ },
496+ {
497+ name: "sessions on enables only session overflow",
498+ input: "nav.overflow-sessions = on\n",
499+ want: domain.NavConfig{OverflowSessions: true},
500+ },
501+ {
502+ name: "off disables both settings",
503+ input: "nav.overflow-tabs = off\nnav.overflow-sessions = off\n",
504+ },
505+ {
506+ name: "settings are independent",
507+ input: "nav.overflow-tabs = on\nnav.overflow-sessions = off\n",
508+ want: domain.NavConfig{OverflowTabs: true},
509+ },
510+ {
511+ name: "invalid values warn and preserve defaults",
512+ input: "nav.overflow-tabs = maybe\nnav.overflow-sessions = always\n",
513+ wantWarnings: []domain.Warning{
514+ {Line: 1, Msg: `invalid nav.overflow-tabs "maybe"`},
515+ {Line: 2, Msg: `invalid nav.overflow-sessions "always"`},
516+ },
517+ },
518+ {
519+ name: "duplicate keys warn and last valid value wins",
520+ input: "nav.overflow-tabs = off\nnav.overflow-tabs = on\nnav.overflow-sessions = on\nnav.overflow-sessions = off\n",
521+ want: domain.NavConfig{OverflowTabs: true},
522+ wantWarnings: []domain.Warning{
523+ {Line: 2, Msg: `duplicate key "nav.overflow-tabs"`},
524+ {Line: 4, Msg: `duplicate key "nav.overflow-sessions"`},
525+ },
526+ },
527+ {
528+ name: "invalid duplicate still warns and keeps previous value",
529+ input: "nav.overflow-tabs = on\nnav.overflow-tabs = maybe\n",
530+ want: domain.NavConfig{OverflowTabs: true},
531+ wantWarnings: []domain.Warning{
532+ {Line: 2, Msg: `duplicate key "nav.overflow-tabs"`},
533+ {Line: 2, Msg: `invalid nav.overflow-tabs "maybe"`},
534+ },
535+ },
536+ }
537+
538+ for _, tt := range tests {
539+ t.Run(tt.name, func(t *testing.T) {
540+ t.Parallel()
541+
542+ cfg, warnings, err := Parse(strings.NewReader(tt.input))
543+ require.NoError(t, err)
544+ require.Equal(t, tt.want, cfg.Nav)
545+ require.Equal(t, tt.wantWarnings, warnings)
546+ })
547+ }
548+}
549+
550+func TestDefaultsDisableNavigationOverflow(t *testing.T) {
551+ t.Parallel()
552+
553+ require.Equal(t, domain.NavConfig{}, domain.Defaults().Nav)
554+}
555+
479 func TestDefaultsCopiesSnapshotRestoreProcesses(t *testing.T) {556 func TestDefaultsCopiesSnapshotRestoreProcesses(t *testing.T) {
480 t.Parallel()557 t.Parallel()
481 558
M internal/domain/config.go +7 -0
@@ -69,6 +69,12 @@ type FloatingConfig struct {
6969 Height int
7070 }
7171
72+// NavConfig contains directional navigation overflow settings.
73+type NavConfig struct {
74+ OverflowTabs bool
75+ OverflowSessions bool
76+}
77+
7278 // TabsConfig contains tab label display settings.
7379 type TabsConfig struct {
7480 // TerminalTitle includes the focused pane's OSC terminal title in tab
@@ -90,6 +96,7 @@ type Config struct {
9096 Copy CopyConfig
9197 Palette PaletteConfig
9298 Floating FloatingConfig
99+ Nav NavConfig
93100 Tabs TabsConfig
94101 }
95102
@@ -69,6 +69,12 @@ type FloatingConfig struct {
69 Height int69 Height int
70 }70 }
71 71
72+// NavConfig contains directional navigation overflow settings.
73+type NavConfig struct {
74+ OverflowTabs bool
75+ OverflowSessions bool
76+}
77+
72 // TabsConfig contains tab label display settings.78 // TabsConfig contains tab label display settings.
73 type TabsConfig struct {79 type TabsConfig struct {
74 // TerminalTitle includes the focused pane's OSC terminal title in tab80 // TerminalTitle includes the focused pane's OSC terminal title in tab
@@ -90,6 +96,7 @@ type Config struct {
90 Copy CopyConfig96 Copy CopyConfig
91 Palette PaletteConfig97 Palette PaletteConfig
92 Floating FloatingConfig98 Floating FloatingConfig
99+ Nav NavConfig
93 Tabs TabsConfig100 Tabs TabsConfig
94 }101 }
95 102
M internal/usecase/daemon/config.go +9 -0
@@ -56,6 +56,8 @@ func (d *Daemon) ApplyConfig(cfg domain.Config) {
5656 d.copyConfig.Store(&copyConfig)
5757 palette := cfg.Palette
5858 d.paletteConfig.Store(&palette)
59+ nav := cfg.Nav
60+ d.navConfig.Store(&nav)
5961 tabs := cfg.Tabs
6062 d.tabsConfig.Store(&tabs)
6163 d.storeThemeConfig(cfg)
@@ -262,6 +264,13 @@ func (d *Daemon) currentPaletteConfig() domain.PaletteConfig {
262264 return domain.Defaults().Palette
263265 }
264266
267+func (d *Daemon) currentNavConfig() domain.NavConfig {
268+ if cfg := d.navConfig.Load(); cfg != nil {
269+ return *cfg
270+ }
271+ return domain.Defaults().Nav
272+}
273+
265274 func (d *Daemon) currentTabsConfig() domain.TabsConfig {
266275 if cfg := d.tabsConfig.Load(); cfg != nil {
267276 return *cfg
@@ -56,6 +56,8 @@ func (d *Daemon) ApplyConfig(cfg domain.Config) {
56 d.copyConfig.Store(&copyConfig)56 d.copyConfig.Store(&copyConfig)
57 palette := cfg.Palette57 palette := cfg.Palette
58 d.paletteConfig.Store(&palette)58 d.paletteConfig.Store(&palette)
59+ nav := cfg.Nav
60+ d.navConfig.Store(&nav)
59 tabs := cfg.Tabs61 tabs := cfg.Tabs
60 d.tabsConfig.Store(&tabs)62 d.tabsConfig.Store(&tabs)
61 d.storeThemeConfig(cfg)63 d.storeThemeConfig(cfg)
@@ -262,6 +264,13 @@ func (d *Daemon) currentPaletteConfig() domain.PaletteConfig {
262 return domain.Defaults().Palette264 return domain.Defaults().Palette
263 }265 }
264 266
267+func (d *Daemon) currentNavConfig() domain.NavConfig {
268+ if cfg := d.navConfig.Load(); cfg != nil {
269+ return *cfg
270+ }
271+ return domain.Defaults().Nav
272+}
273+
265 func (d *Daemon) currentTabsConfig() domain.TabsConfig {274 func (d *Daemon) currentTabsConfig() domain.TabsConfig {
266 if cfg := d.tabsConfig.Load(); cfg != nil {275 if cfg := d.tabsConfig.Load(); cfg != nil {
267 return *cfg276 return *cfg
M internal/usecase/daemon/config_test.go +18 -0
@@ -365,6 +365,24 @@ func TestApplyConfigPublishesImmutableFloatingSnapshot(t *testing.T) {
365365 require.Equal(t, domain.FloatingConfig{Command: "lazygit", Width: 90, Height: 85}, second)
366366 }
367367
368+func TestApplyConfigPublishesImmutableNavSnapshot(t *testing.T) {
369+ d := newTestDaemon(t, nil, stubClock{})
370+ require.Equal(t, domain.NavConfig{}, d.currentNavConfig())
371+
372+ firstConfig := domain.Defaults()
373+ firstConfig.Nav = domain.NavConfig{OverflowTabs: true}
374+ d.ApplyConfig(firstConfig)
375+ first := d.currentNavConfig()
376+
377+ secondConfig := domain.Defaults()
378+ secondConfig.Nav = domain.NavConfig{OverflowSessions: true}
379+ d.ApplyConfig(secondConfig)
380+ second := d.currentNavConfig()
381+
382+ require.Equal(t, domain.NavConfig{OverflowTabs: true}, first)
383+ require.Equal(t, domain.NavConfig{OverflowSessions: true}, second)
384+}
385+
368386 func TestApplyConfigPublishesImmutableTabsSnapshot(t *testing.T) {
369387 d := newTestDaemon(t, nil, stubClock{})
370388 require.Equal(t, domain.Defaults().Tabs, d.currentTabsConfig())
@@ -365,6 +365,24 @@ func TestApplyConfigPublishesImmutableFloatingSnapshot(t *testing.T) {
365 require.Equal(t, domain.FloatingConfig{Command: "lazygit", Width: 90, Height: 85}, second)365 require.Equal(t, domain.FloatingConfig{Command: "lazygit", Width: 90, Height: 85}, second)
366 }366 }
367 367
368+func TestApplyConfigPublishesImmutableNavSnapshot(t *testing.T) {
369+ d := newTestDaemon(t, nil, stubClock{})
370+ require.Equal(t, domain.NavConfig{}, d.currentNavConfig())
371+
372+ firstConfig := domain.Defaults()
373+ firstConfig.Nav = domain.NavConfig{OverflowTabs: true}
374+ d.ApplyConfig(firstConfig)
375+ first := d.currentNavConfig()
376+
377+ secondConfig := domain.Defaults()
378+ secondConfig.Nav = domain.NavConfig{OverflowSessions: true}
379+ d.ApplyConfig(secondConfig)
380+ second := d.currentNavConfig()
381+
382+ require.Equal(t, domain.NavConfig{OverflowTabs: true}, first)
383+ require.Equal(t, domain.NavConfig{OverflowSessions: true}, second)
384+}
385+
368 func TestApplyConfigPublishesImmutableTabsSnapshot(t *testing.T) {386 func TestApplyConfigPublishesImmutableTabsSnapshot(t *testing.T) {
369 d := newTestDaemon(t, nil, stubClock{})387 d := newTestDaemon(t, nil, stubClock{})
370 require.Equal(t, domain.Defaults().Tabs, d.currentTabsConfig())388 require.Equal(t, domain.Defaults().Tabs, d.currentTabsConfig())
M internal/usecase/daemon/daemon.go +1 -0
@@ -162,6 +162,7 @@ type Daemon struct {
162162 floatingConfig atomic.Pointer[domain.FloatingConfig]
163163 copyConfig atomic.Pointer[domain.CopyConfig]
164164 paletteConfig atomic.Pointer[domain.PaletteConfig]
165+ navConfig atomic.Pointer[domain.NavConfig]
165166 tabsConfig atomic.Pointer[domain.TabsConfig]
166167 themeConfig atomic.Pointer[themeConfigSnapshot]
167168 barScripts *barScriptState
@@ -162,6 +162,7 @@ type Daemon struct {
162 floatingConfig atomic.Pointer[domain.FloatingConfig]162 floatingConfig atomic.Pointer[domain.FloatingConfig]
163 copyConfig atomic.Pointer[domain.CopyConfig]163 copyConfig atomic.Pointer[domain.CopyConfig]
164 paletteConfig atomic.Pointer[domain.PaletteConfig]164 paletteConfig atomic.Pointer[domain.PaletteConfig]
165+ navConfig atomic.Pointer[domain.NavConfig]
165 tabsConfig atomic.Pointer[domain.TabsConfig]166 tabsConfig atomic.Pointer[domain.TabsConfig]
166 themeConfig atomic.Pointer[themeConfigSnapshot]167 themeConfig atomic.Pointer[themeConfigSnapshot]
167 barScripts *barScriptState168 barScripts *barScriptState

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