detached 3183f09

feat(systemviews): add toggle split shortcuts

commit
3183f095a2af6eb80b761622aaf0dc2931c8d06d
author
brice · · 2026-04-24 02:11 UTC
parent
4f3ae02
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
11 files · +358 -50
M docs/reference/keybindings.md +13 -0
@@ -80,6 +80,9 @@ These work outside modal modes:
8080 | Action | Keys |
8181 |--------|------|
8282 | Toggle floating pane | `Alt+F` |
83+| Toggle History system view in right split | `Ctrl+H` |
84+| Toggle Favorites system view in right split | unbound by default |
85+| Toggle Config system view in right split | unbound by default |
8386 | Close pane (or release floating pane) | `Ctrl+W` |
8487 | Next tab | `Ctrl+Tab` |
8588 | Previous tab | `Ctrl+Shift+Tab` |
@@ -90,6 +93,7 @@ These work outside modal modes:
9093
9194 - `Alt+F` is the only floating-pane shortcut enabled by default.
9295 - `Alt+F` toggles floating visibility and keeps floating pane state intact.
96+- `Ctrl+H` toggles `dumb://history`: it focuses an existing History pane, opens it in a right split if missing, or closes it when already active.
9397 - `Ctrl+W` closes the active pane; when the floating pane is active, it fully releases that floating session.
9498 - Any URL shortcut (for example `Alt+G`) must be defined explicitly in `workspace.floating_pane.profiles`.
9599 - Floating profile shortcuts support modifier combos with `ctrl`, `shift`, and `alt` (for example `ctrl+shift+y` or `ctrl+alt+m`).
@@ -114,6 +118,15 @@ keys = ["ctrl+w"]
114118 [workspace.shortcuts.actions.toggle_floating_pane]
115119 keys = ["alt+f"]
116120
121+[workspace.shortcuts.actions.toggle_history_systemview]
122+keys = ["ctrl+h"]
123+
124+[workspace.shortcuts.actions.toggle_favorites_systemview]
125+keys = []
126+
127+[workspace.shortcuts.actions.toggle_config_systemview]
128+keys = []
129+
117130 [workspace.floating_pane]
118131 width_pct = 0.82
119132 height_pct = 0.72
@@ -80,6 +80,9 @@ These work outside modal modes:
80 | Action | Keys |80 | Action | Keys |
81 |--------|------|81 |--------|------|
82 | Toggle floating pane | `Alt+F` |82 | Toggle floating pane | `Alt+F` |
83+| Toggle History system view in right split | `Ctrl+H` |
84+| Toggle Favorites system view in right split | unbound by default |
85+| Toggle Config system view in right split | unbound by default |
83 | Close pane (or release floating pane) | `Ctrl+W` |86 | Close pane (or release floating pane) | `Ctrl+W` |
84 | Next tab | `Ctrl+Tab` |87 | Next tab | `Ctrl+Tab` |
85 | Previous tab | `Ctrl+Shift+Tab` |88 | Previous tab | `Ctrl+Shift+Tab` |
@@ -90,6 +93,7 @@ These work outside modal modes:
90 93
91 - `Alt+F` is the only floating-pane shortcut enabled by default.94 - `Alt+F` is the only floating-pane shortcut enabled by default.
92 - `Alt+F` toggles floating visibility and keeps floating pane state intact.95 - `Alt+F` toggles floating visibility and keeps floating pane state intact.
96+- `Ctrl+H` toggles `dumb://history`: it focuses an existing History pane, opens it in a right split if missing, or closes it when already active.
93 - `Ctrl+W` closes the active pane; when the floating pane is active, it fully releases that floating session.97 - `Ctrl+W` closes the active pane; when the floating pane is active, it fully releases that floating session.
94 - Any URL shortcut (for example `Alt+G`) must be defined explicitly in `workspace.floating_pane.profiles`.98 - Any URL shortcut (for example `Alt+G`) must be defined explicitly in `workspace.floating_pane.profiles`.
95 - Floating profile shortcuts support modifier combos with `ctrl`, `shift`, and `alt` (for example `ctrl+shift+y` or `ctrl+alt+m`).99 - Floating profile shortcuts support modifier combos with `ctrl`, `shift`, and `alt` (for example `ctrl+shift+y` or `ctrl+alt+m`).
@@ -114,6 +118,15 @@ keys = ["ctrl+w"]
114 [workspace.shortcuts.actions.toggle_floating_pane]118 [workspace.shortcuts.actions.toggle_floating_pane]
115 keys = ["alt+f"]119 keys = ["alt+f"]
116 120
121+[workspace.shortcuts.actions.toggle_history_systemview]
122+keys = ["ctrl+h"]
123+
124+[workspace.shortcuts.actions.toggle_favorites_systemview]
125+keys = []
126+
127+[workspace.shortcuts.actions.toggle_config_systemview]
128+keys = []
129+
117 [workspace.floating_pane]130 [workspace.floating_pane]
118 width_pct = 0.82131 width_pct = 0.82
119 height_pct = 0.72132 height_pct = 0.72
M docs/systemviews-wasm-crud-plan.md +3 -3
@@ -84,9 +84,9 @@ Add first-class global actions:
8484
8585 | Action | Default key | Target behavior |
8686 | --- | --- | --- |
87-| `open_history_systemview` | `ctrl+h` | Split active pane to the right with `dumb://history`, or focus an existing history systemview pane if the chosen behavior is focus-first. |
88-| `open_favorites_systemview` | Proposed: unset by default or `ctrl+shift+h` only if accepted. | Split/focus `dumb://favorites`. |
89-| `open_config_systemview` | Proposed: unset by default to avoid conflicts; user can bind in config. | Split/focus `dumb://config`. |
87+| `toggle_history_systemview` | `ctrl+h` | Split active pane to the right with `dumb://history`, focus an existing history systemview pane, or close it if already active. |
88+| `toggle_favorites_systemview` | Unset by default. | Split/focus/toggle `dumb://favorites`. |
89+| `toggle_config_systemview` | Unset by default; user can bind in config. | Split/focus/toggle `dumb://config`. |
9090
9191 Implementation path:
9292
@@ -84,9 +84,9 @@ Add first-class global actions:
84 84
85 | Action | Default key | Target behavior |85 | Action | Default key | Target behavior |
86 | --- | --- | --- |86 | --- | --- | --- |
87-| `open_history_systemview` | `ctrl+h` | Split active pane to the right with `dumb://history`, or focus an existing history systemview pane if the chosen behavior is focus-first. |87+| `toggle_history_systemview` | `ctrl+h` | Split active pane to the right with `dumb://history`, focus an existing history systemview pane, or close it if already active. |
88-| `open_favorites_systemview` | Proposed: unset by default or `ctrl+shift+h` only if accepted. | Split/focus `dumb://favorites`. |88+| `toggle_favorites_systemview` | Unset by default. | Split/focus/toggle `dumb://favorites`. |
89-| `open_config_systemview` | Proposed: unset by default to avoid conflicts; user can bind in config. | Split/focus `dumb://config`. |89+| `toggle_config_systemview` | Unset by default; user can bind in config. | Split/focus/toggle `dumb://config`. |
90 90
91 Implementation path:91 Implementation path:
92 92
M internal/infrastructure/config/defaults.go +11 -8
@@ -238,14 +238,17 @@ func DefaultConfig() *Config {
238238 },
239239 Shortcuts: GlobalShortcutsConfig{
240240 Actions: map[string]ActionBinding{
241- "toggle_floating_pane": {Keys: []string{"alt+f"}, Desc: "Toggle floating pane"},
242- "close_pane": {Keys: []string{"ctrl+w"}, Desc: "Close active pane"},
243- "next_tab": {Keys: []string{"ctrl+tab"}, Desc: "Switch to next tab"},
244- "previous_tab": {Keys: []string{"ctrl+shift+tab"}, Desc: "Switch to previous tab"},
245- "consume_or_expel_left": {Keys: []string{"alt+["}, Desc: "Consume/expel pane left"},
246- "consume_or_expel_right": {Keys: []string{"alt+]"}, Desc: "Consume/expel pane right"},
247- "consume_or_expel_up": {Keys: []string{"alt+{"}, Desc: "Consume/expel pane up"},
248- "consume_or_expel_down": {Keys: []string{"alt+}"}, Desc: "Consume/expel pane down"},
241+ "toggle_floating_pane": {Keys: []string{"alt+f"}, Desc: "Toggle floating pane"},
242+ "toggle_history_systemview": {Keys: []string{"ctrl+h"}, Desc: "Toggle History in right split"},
243+ "toggle_favorites_systemview": {Keys: []string{}, Desc: "Toggle Favorites in right split"},
244+ "toggle_config_systemview": {Keys: []string{}, Desc: "Toggle Config in right split"},
245+ "close_pane": {Keys: []string{"ctrl+w"}, Desc: "Close active pane"},
246+ "next_tab": {Keys: []string{"ctrl+tab"}, Desc: "Switch to next tab"},
247+ "previous_tab": {Keys: []string{"ctrl+shift+tab"}, Desc: "Switch to previous tab"},
248+ "consume_or_expel_left": {Keys: []string{"alt+["}, Desc: "Consume/expel pane left"},
249+ "consume_or_expel_right": {Keys: []string{"alt+]"}, Desc: "Consume/expel pane right"},
250+ "consume_or_expel_up": {Keys: []string{"alt+{"}, Desc: "Consume/expel pane up"},
251+ "consume_or_expel_down": {Keys: []string{"alt+}"}, Desc: "Consume/expel pane down"},
249252 },
250253 },
251254 FloatingPane: FloatingPaneConfig{
@@ -238,14 +238,17 @@ func DefaultConfig() *Config {
238 },238 },
239 Shortcuts: GlobalShortcutsConfig{239 Shortcuts: GlobalShortcutsConfig{
240 Actions: map[string]ActionBinding{240 Actions: map[string]ActionBinding{
241- "toggle_floating_pane": {Keys: []string{"alt+f"}, Desc: "Toggle floating pane"},241+ "toggle_floating_pane": {Keys: []string{"alt+f"}, Desc: "Toggle floating pane"},
242- "close_pane": {Keys: []string{"ctrl+w"}, Desc: "Close active pane"},242+ "toggle_history_systemview": {Keys: []string{"ctrl+h"}, Desc: "Toggle History in right split"},
243- "next_tab": {Keys: []string{"ctrl+tab"}, Desc: "Switch to next tab"},243+ "toggle_favorites_systemview": {Keys: []string{}, Desc: "Toggle Favorites in right split"},
244- "previous_tab": {Keys: []string{"ctrl+shift+tab"}, Desc: "Switch to previous tab"},244+ "toggle_config_systemview": {Keys: []string{}, Desc: "Toggle Config in right split"},
245- "consume_or_expel_left": {Keys: []string{"alt+["}, Desc: "Consume/expel pane left"},245+ "close_pane": {Keys: []string{"ctrl+w"}, Desc: "Close active pane"},
246- "consume_or_expel_right": {Keys: []string{"alt+]"}, Desc: "Consume/expel pane right"},246+ "next_tab": {Keys: []string{"ctrl+tab"}, Desc: "Switch to next tab"},
247- "consume_or_expel_up": {Keys: []string{"alt+{"}, Desc: "Consume/expel pane up"},247+ "previous_tab": {Keys: []string{"ctrl+shift+tab"}, Desc: "Switch to previous tab"},
248- "consume_or_expel_down": {Keys: []string{"alt+}"}, Desc: "Consume/expel pane down"},248+ "consume_or_expel_left": {Keys: []string{"alt+["}, Desc: "Consume/expel pane left"},
249+ "consume_or_expel_right": {Keys: []string{"alt+]"}, Desc: "Consume/expel pane right"},
250+ "consume_or_expel_up": {Keys: []string{"alt+{"}, Desc: "Consume/expel pane up"},
251+ "consume_or_expel_down": {Keys: []string{"alt+}"}, Desc: "Consume/expel pane down"},
249 },252 },
250 },253 },
251 FloatingPane: FloatingPaneConfig{254 FloatingPane: FloatingPaneConfig{
M internal/infrastructure/config/defaults_test.go +12 -0
@@ -26,6 +26,18 @@ func TestDefaultConfig_CoreDefaults(t *testing.T) {
2626 assert.False(t, cfg.Engine.CEF.TraceHandlers)
2727 assert.True(t, cfg.Engine.WebKit.ITPEnabled)
2828
29+ // System view shortcuts are first-class global actions.
30+ requireActionBinding(t, cfg.Workspace.Shortcuts.Actions, "toggle_history_systemview", []string{"ctrl+h"})
31+ requireActionBinding(t, cfg.Workspace.Shortcuts.Actions, "toggle_favorites_systemview", []string{})
32+ requireActionBinding(t, cfg.Workspace.Shortcuts.Actions, "toggle_config_systemview", []string{})
33+
2934 // Old sections (Rendering, Privacy, Performance, Runtime) have been removed from Config.
3035 // Their values now live under cfg.Engine / cfg.Engine.WebKit (validated above).
3136 }
37+
38+func requireActionBinding(t *testing.T, actions map[string]ActionBinding, action string, keys []string) {
39+ t.Helper()
40+ binding, ok := actions[action]
41+ assert.True(t, ok, "missing action binding %q", action)
42+ assert.Equal(t, keys, binding.Keys)
43+}
@@ -26,6 +26,18 @@ func TestDefaultConfig_CoreDefaults(t *testing.T) {
26 assert.False(t, cfg.Engine.CEF.TraceHandlers)26 assert.False(t, cfg.Engine.CEF.TraceHandlers)
27 assert.True(t, cfg.Engine.WebKit.ITPEnabled)27 assert.True(t, cfg.Engine.WebKit.ITPEnabled)
28 28
29+ // System view shortcuts are first-class global actions.
30+ requireActionBinding(t, cfg.Workspace.Shortcuts.Actions, "toggle_history_systemview", []string{"ctrl+h"})
31+ requireActionBinding(t, cfg.Workspace.Shortcuts.Actions, "toggle_favorites_systemview", []string{})
32+ requireActionBinding(t, cfg.Workspace.Shortcuts.Actions, "toggle_config_systemview", []string{})
33+
29 // Old sections (Rendering, Privacy, Performance, Runtime) have been removed from Config.34 // Old sections (Rendering, Privacy, Performance, Runtime) have been removed from Config.
30 // Their values now live under cfg.Engine / cfg.Engine.WebKit (validated above).35 // Their values now live under cfg.Engine / cfg.Engine.WebKit (validated above).
31 }36 }
37+
38+func requireActionBinding(t *testing.T, actions map[string]ActionBinding, action string, keys []string) {
39+ t.Helper()
40+ binding, ok := actions[action]
41+ assert.True(t, ok, "missing action binding %q", action)
42+ assert.Equal(t, keys, binding.Keys)
43+}
M internal/ui/coordinator/workspace.go +93 -1
@@ -171,6 +171,15 @@ func setupPaneViewHover(ctx context.Context, pv *component.PaneView, wsView *com
171171
172172 // Split splits the active pane in the given direction.
173173 func (c *WorkspaceCoordinator) Split(ctx context.Context, direction usecase.SplitDirection) error {
174+ return c.splitWithInitialURL(ctx, direction, domainurl.Normalize(c.newPaneURL))
175+}
176+
177+// SplitWithURL splits the active pane in the given direction and loads initialURL.
178+func (c *WorkspaceCoordinator) SplitWithURL(ctx context.Context, direction usecase.SplitDirection, initialURL string) error {
179+ return c.splitWithInitialURL(ctx, direction, domainurl.Normalize(initialURL))
180+}
181+
182+func (c *WorkspaceCoordinator) splitWithInitialURL(ctx context.Context, direction usecase.SplitDirection, initialURL string) error {
174183 log := logging.FromContext(ctx)
175184
176185 splitCtx, ok := c.prepareSplit(ctx, direction)
@@ -182,7 +191,7 @@ func (c *WorkspaceCoordinator) Split(ctx context.Context, direction usecase.Spli
182191 Workspace: splitCtx.ws,
183192 TargetPane: splitCtx.activePane,
184193 Direction: direction,
185- InitialURL: domainurl.Normalize(c.newPaneURL),
194+ InitialURL: initialURL,
186195 })
187196 if err != nil {
188197 log.Error().Err(err).Str("direction", string(direction)).Msg("failed to split pane")
@@ -212,6 +221,89 @@ func (c *WorkspaceCoordinator) Split(ctx context.Context, direction usecase.Spli
212221 return nil
213222 }
214223
224+// ToggleSystemViewRight focuses an existing system view pane, closes it if it is
225+// already active, or opens it in a right split when no matching pane exists.
226+func (c *WorkspaceCoordinator) ToggleSystemViewRight(ctx context.Context, targetURL string) error {
227+ log := logging.FromContext(ctx)
228+ targetURL = domainurl.Normalize(targetURL)
229+ if targetURL == "" {
230+ return fmt.Errorf("system view URL is required")
231+ }
232+ if c.getActiveWS == nil {
233+ log.Warn().Str("url", targetURL).Msg("active workspace provider not configured")
234+ return nil
235+ }
236+
237+ ws, wsView := c.getActiveWS()
238+ if ws == nil {
239+ log.Warn().Str("url", targetURL).Msg("no active workspace for system view toggle")
240+ return nil
241+ }
242+
243+ activePane := ws.ActivePane()
244+ if paneMatchesURL(activePane, targetURL) {
245+ if ws.PaneCount() <= 1 {
246+ log.Debug().Str("url", targetURL).Msg("system view is active in the last pane; leaving it open")
247+ return nil
248+ }
249+ log.Debug().Str("url", targetURL).Msg("system view already active; closing pane")
250+ return c.ClosePane(ctx)
251+ }
252+
253+ if existing := findPaneByURL(ws, targetURL); existing != nil {
254+ log.Debug().Str("url", targetURL).Str("pane_id", string(existing.ID)).Msg("focusing existing system view pane")
255+ c.focusExistingPane(ctx, ws, wsView, existing.ID)
256+ return nil
257+ }
258+
259+ log.Debug().Str("url", targetURL).Msg("opening system view in right split")
260+ return c.SplitWithURL(ctx, usecase.SplitRight, targetURL)
261+}
262+
263+func paneMatchesURL(node *entity.PaneNode, targetURL string) bool {
264+ return node != nil && node.Pane != nil && domainurl.Normalize(node.Pane.URI) == targetURL
265+}
266+
267+func findPaneByURL(ws *entity.Workspace, targetURL string) *entity.Pane {
268+ if ws == nil {
269+ return nil
270+ }
271+ for _, pane := range ws.AllPanes() {
272+ if pane != nil && domainurl.Normalize(pane.URI) == targetURL {
273+ return pane
274+ }
275+ }
276+ return nil
277+}
278+
279+func (c *WorkspaceCoordinator) focusExistingPane(
280+ ctx context.Context,
281+ ws *entity.Workspace,
282+ wsView *component.WorkspaceView,
283+ paneID entity.PaneID,
284+) {
285+ oldActivePaneID := ws.ActivePaneID
286+ ws.ActivePaneID = paneID
287+
288+ if wsView != nil {
289+ wsView.CancelAllPendingHovers()
290+ wsView.SuppressHover(component.KeyboardFocusSuppressDuration)
291+ if oldActivePaneID != "" && oldActivePaneID != paneID {
292+ wsView.DeactivatePane(oldActivePaneID)
293+ }
294+ if err := wsView.SetActivePaneID(paneID); err != nil {
295+ logging.FromContext(ctx).Warn().Err(err).Str("pane_id", string(paneID)).Msg("failed to focus existing pane in view")
296+ } else {
297+ wsView.FocusPane(paneID)
298+ }
299+ if paneNode := ws.FindPane(paneID); paneNode != nil && paneNode.Parent != nil && paneNode.Parent.IsStacked {
300+ c.syncStackedViewActive(ctx, wsView, paneNode)
301+ }
302+ }
303+
304+ c.notifyStateChanged()
305+}
306+
215307 func (c *WorkspaceCoordinator) prepareSplit(
216308 ctx context.Context,
217309 direction usecase.SplitDirection,
@@ -171,6 +171,15 @@ func setupPaneViewHover(ctx context.Context, pv *component.PaneView, wsView *com
171 171
172 // Split splits the active pane in the given direction.172 // Split splits the active pane in the given direction.
173 func (c *WorkspaceCoordinator) Split(ctx context.Context, direction usecase.SplitDirection) error {173 func (c *WorkspaceCoordinator) Split(ctx context.Context, direction usecase.SplitDirection) error {
174+ return c.splitWithInitialURL(ctx, direction, domainurl.Normalize(c.newPaneURL))
175+}
176+
177+// SplitWithURL splits the active pane in the given direction and loads initialURL.
178+func (c *WorkspaceCoordinator) SplitWithURL(ctx context.Context, direction usecase.SplitDirection, initialURL string) error {
179+ return c.splitWithInitialURL(ctx, direction, domainurl.Normalize(initialURL))
180+}
181+
182+func (c *WorkspaceCoordinator) splitWithInitialURL(ctx context.Context, direction usecase.SplitDirection, initialURL string) error {
174 log := logging.FromContext(ctx)183 log := logging.FromContext(ctx)
175 184
176 splitCtx, ok := c.prepareSplit(ctx, direction)185 splitCtx, ok := c.prepareSplit(ctx, direction)
@@ -182,7 +191,7 @@ func (c *WorkspaceCoordinator) Split(ctx context.Context, direction usecase.Spli
182 Workspace: splitCtx.ws,191 Workspace: splitCtx.ws,
183 TargetPane: splitCtx.activePane,192 TargetPane: splitCtx.activePane,
184 Direction: direction,193 Direction: direction,
185- InitialURL: domainurl.Normalize(c.newPaneURL),194+ InitialURL: initialURL,
186 })195 })
187 if err != nil {196 if err != nil {
188 log.Error().Err(err).Str("direction", string(direction)).Msg("failed to split pane")197 log.Error().Err(err).Str("direction", string(direction)).Msg("failed to split pane")
@@ -212,6 +221,89 @@ func (c *WorkspaceCoordinator) Split(ctx context.Context, direction usecase.Spli
212 return nil221 return nil
213 }222 }
214 223
224+// ToggleSystemViewRight focuses an existing system view pane, closes it if it is
225+// already active, or opens it in a right split when no matching pane exists.
226+func (c *WorkspaceCoordinator) ToggleSystemViewRight(ctx context.Context, targetURL string) error {
227+ log := logging.FromContext(ctx)
228+ targetURL = domainurl.Normalize(targetURL)
229+ if targetURL == "" {
230+ return fmt.Errorf("system view URL is required")
231+ }
232+ if c.getActiveWS == nil {
233+ log.Warn().Str("url", targetURL).Msg("active workspace provider not configured")
234+ return nil
235+ }
236+
237+ ws, wsView := c.getActiveWS()
238+ if ws == nil {
239+ log.Warn().Str("url", targetURL).Msg("no active workspace for system view toggle")
240+ return nil
241+ }
242+
243+ activePane := ws.ActivePane()
244+ if paneMatchesURL(activePane, targetURL) {
245+ if ws.PaneCount() <= 1 {
246+ log.Debug().Str("url", targetURL).Msg("system view is active in the last pane; leaving it open")
247+ return nil
248+ }
249+ log.Debug().Str("url", targetURL).Msg("system view already active; closing pane")
250+ return c.ClosePane(ctx)
251+ }
252+
253+ if existing := findPaneByURL(ws, targetURL); existing != nil {
254+ log.Debug().Str("url", targetURL).Str("pane_id", string(existing.ID)).Msg("focusing existing system view pane")
255+ c.focusExistingPane(ctx, ws, wsView, existing.ID)
256+ return nil
257+ }
258+
259+ log.Debug().Str("url", targetURL).Msg("opening system view in right split")
260+ return c.SplitWithURL(ctx, usecase.SplitRight, targetURL)
261+}
262+
263+func paneMatchesURL(node *entity.PaneNode, targetURL string) bool {
264+ return node != nil && node.Pane != nil && domainurl.Normalize(node.Pane.URI) == targetURL
265+}
266+
267+func findPaneByURL(ws *entity.Workspace, targetURL string) *entity.Pane {
268+ if ws == nil {
269+ return nil
270+ }
271+ for _, pane := range ws.AllPanes() {
272+ if pane != nil && domainurl.Normalize(pane.URI) == targetURL {
273+ return pane
274+ }
275+ }
276+ return nil
277+}
278+
279+func (c *WorkspaceCoordinator) focusExistingPane(
280+ ctx context.Context,
281+ ws *entity.Workspace,
282+ wsView *component.WorkspaceView,
283+ paneID entity.PaneID,
284+) {
285+ oldActivePaneID := ws.ActivePaneID
286+ ws.ActivePaneID = paneID
287+
288+ if wsView != nil {
289+ wsView.CancelAllPendingHovers()
290+ wsView.SuppressHover(component.KeyboardFocusSuppressDuration)
291+ if oldActivePaneID != "" && oldActivePaneID != paneID {
292+ wsView.DeactivatePane(oldActivePaneID)
293+ }
294+ if err := wsView.SetActivePaneID(paneID); err != nil {
295+ logging.FromContext(ctx).Warn().Err(err).Str("pane_id", string(paneID)).Msg("failed to focus existing pane in view")
296+ } else {
297+ wsView.FocusPane(paneID)
298+ }
299+ if paneNode := ws.FindPane(paneID); paneNode != nil && paneNode.Parent != nil && paneNode.Parent.IsStacked {
300+ c.syncStackedViewActive(ctx, wsView, paneNode)
301+ }
302+ }
303+
304+ c.notifyStateChanged()
305+}
306+
215 func (c *WorkspaceCoordinator) prepareSplit(307 func (c *WorkspaceCoordinator) prepareSplit(
216 ctx context.Context,308 ctx context.Context,
217 direction usecase.SplitDirection,309 direction usecase.SplitDirection,
A internal/ui/coordinator/workspace_systemview_test.go +93 -0
@@ -0,0 +1,93 @@
1+package coordinator
2+
3+import (
4+ "context"
5+ "testing"
6+
7+ "github.com/bnema/dumber/internal/application/usecase"
8+ "github.com/bnema/dumber/internal/domain/entity"
9+ "github.com/bnema/dumber/internal/ui/component"
10+ "github.com/stretchr/testify/assert"
11+ "github.com/stretchr/testify/require"
12+)
13+
14+func TestWorkspaceCoordinator_ToggleSystemViewRightSplitsWithTargetURL(t *testing.T) {
15+ ctx := context.Background()
16+ ids := []string{"pane-2", "split-1"}
17+ idx := 0
18+ uc := usecase.NewManagePanesUseCase(func() string {
19+ id := ids[idx]
20+ idx++
21+ return id
22+ })
23+
24+ initialPane := entity.NewPane("pane-1")
25+ initialPane.URI = "https://example.com"
26+ ws := entity.NewWorkspace("ws-1", initialPane)
27+
28+ coord := NewWorkspaceCoordinator(ctx, WorkspaceCoordinatorConfig{
29+ PanesUC: uc,
30+ GetActiveWS: func() (*entity.Workspace, *component.WorkspaceView) {
31+ return ws, nil
32+ },
33+ })
34+
35+ err := coord.ToggleSystemViewRight(ctx, "dumb://history")
36+ require.NoError(t, err)
37+
38+ require.Equal(t, 2, ws.PaneCount())
39+ active := ws.ActivePane()
40+ require.NotNil(t, active)
41+ require.NotNil(t, active.Pane)
42+ assert.Equal(t, entity.PaneID("pane-2"), active.Pane.ID)
43+ assert.Equal(t, "dumb://history", active.Pane.URI)
44+}
45+
46+func TestWorkspaceCoordinator_ToggleSystemViewRightFocusesExistingPane(t *testing.T) {
47+ ctx := context.Background()
48+ left := testLeafNode("pane-1")
49+ left.Pane.URI = "https://example.com"
50+ right := testLeafNode("pane-2")
51+ right.Pane.URI = "dumb://history"
52+ root := testSplitNode("split-1", left, right)
53+ ws := &entity.Workspace{ID: "ws-1", Root: root, ActivePaneID: left.Pane.ID}
54+
55+ coord := NewWorkspaceCoordinator(ctx, WorkspaceCoordinatorConfig{
56+ GetActiveWS: func() (*entity.Workspace, *component.WorkspaceView) {
57+ return ws, nil
58+ },
59+ })
60+
61+ err := coord.ToggleSystemViewRight(ctx, "dumb://history")
62+ require.NoError(t, err)
63+
64+ assert.Equal(t, entity.PaneID("pane-2"), ws.ActivePaneID)
65+ assert.Equal(t, 2, ws.PaneCount())
66+}
67+
68+func TestWorkspaceCoordinator_ToggleSystemViewRightClosesActivePane(t *testing.T) {
69+ ctx := context.Background()
70+ uc := usecase.NewManagePanesUseCase(func() string { return "unused" })
71+ left := testLeafNode("pane-1")
72+ left.Pane.URI = "https://example.com"
73+ right := testLeafNode("pane-2")
74+ right.Pane.URI = "dumb://history"
75+ root := testSplitNode("split-1", left, right)
76+ ws := &entity.Workspace{ID: "ws-1", Root: root, ActivePaneID: right.Pane.ID}
77+
78+ coord := NewWorkspaceCoordinator(ctx, WorkspaceCoordinatorConfig{
79+ PanesUC: uc,
80+ GetActiveWS: func() (*entity.Workspace, *component.WorkspaceView) {
81+ return ws, nil
82+ },
83+ })
84+
85+ err := coord.ToggleSystemViewRight(ctx, "dumb://history")
86+ require.NoError(t, err)
87+
88+ assert.Equal(t, 1, ws.PaneCount())
89+ assert.Equal(t, entity.PaneID("pane-1"), ws.ActivePaneID)
90+ active := ws.ActivePane()
91+ require.NotNil(t, active)
92+ assert.Equal(t, "https://example.com", active.Pane.URI)
93+}
@@ -0,0 +1,93 @@
1+package coordinator
2+
3+import (
4+ "context"
5+ "testing"
6+
7+ "github.com/bnema/dumber/internal/application/usecase"
8+ "github.com/bnema/dumber/internal/domain/entity"
9+ "github.com/bnema/dumber/internal/ui/component"
10+ "github.com/stretchr/testify/assert"
11+ "github.com/stretchr/testify/require"
12+)
13+
14+func TestWorkspaceCoordinator_ToggleSystemViewRightSplitsWithTargetURL(t *testing.T) {
15+ ctx := context.Background()
16+ ids := []string{"pane-2", "split-1"}
17+ idx := 0
18+ uc := usecase.NewManagePanesUseCase(func() string {
19+ id := ids[idx]
20+ idx++
21+ return id
22+ })
23+
24+ initialPane := entity.NewPane("pane-1")
25+ initialPane.URI = "https://example.com"
26+ ws := entity.NewWorkspace("ws-1", initialPane)
27+
28+ coord := NewWorkspaceCoordinator(ctx, WorkspaceCoordinatorConfig{
29+ PanesUC: uc,
30+ GetActiveWS: func() (*entity.Workspace, *component.WorkspaceView) {
31+ return ws, nil
32+ },
33+ })
34+
35+ err := coord.ToggleSystemViewRight(ctx, "dumb://history")
36+ require.NoError(t, err)
37+
38+ require.Equal(t, 2, ws.PaneCount())
39+ active := ws.ActivePane()
40+ require.NotNil(t, active)
41+ require.NotNil(t, active.Pane)
42+ assert.Equal(t, entity.PaneID("pane-2"), active.Pane.ID)
43+ assert.Equal(t, "dumb://history", active.Pane.URI)
44+}
45+
46+func TestWorkspaceCoordinator_ToggleSystemViewRightFocusesExistingPane(t *testing.T) {
47+ ctx := context.Background()
48+ left := testLeafNode("pane-1")
49+ left.Pane.URI = "https://example.com"
50+ right := testLeafNode("pane-2")
51+ right.Pane.URI = "dumb://history"
52+ root := testSplitNode("split-1", left, right)
53+ ws := &entity.Workspace{ID: "ws-1", Root: root, ActivePaneID: left.Pane.ID}
54+
55+ coord := NewWorkspaceCoordinator(ctx, WorkspaceCoordinatorConfig{
56+ GetActiveWS: func() (*entity.Workspace, *component.WorkspaceView) {
57+ return ws, nil
58+ },
59+ })
60+
61+ err := coord.ToggleSystemViewRight(ctx, "dumb://history")
62+ require.NoError(t, err)
63+
64+ assert.Equal(t, entity.PaneID("pane-2"), ws.ActivePaneID)
65+ assert.Equal(t, 2, ws.PaneCount())
66+}
67+
68+func TestWorkspaceCoordinator_ToggleSystemViewRightClosesActivePane(t *testing.T) {
69+ ctx := context.Background()
70+ uc := usecase.NewManagePanesUseCase(func() string { return "unused" })
71+ left := testLeafNode("pane-1")
72+ left.Pane.URI = "https://example.com"
73+ right := testLeafNode("pane-2")
74+ right.Pane.URI = "dumb://history"
75+ root := testSplitNode("split-1", left, right)
76+ ws := &entity.Workspace{ID: "ws-1", Root: root, ActivePaneID: right.Pane.ID}
77+
78+ coord := NewWorkspaceCoordinator(ctx, WorkspaceCoordinatorConfig{
79+ PanesUC: uc,
80+ GetActiveWS: func() (*entity.Workspace, *component.WorkspaceView) {
81+ return ws, nil
82+ },
83+ })
84+
85+ err := coord.ToggleSystemViewRight(ctx, "dumb://history")
86+ require.NoError(t, err)
87+
88+ assert.Equal(t, 1, ws.PaneCount())
89+ assert.Equal(t, entity.PaneID("pane-1"), ws.ActivePaneID)
90+ active := ws.ActivePane()
91+ require.NotNil(t, active)
92+ assert.Equal(t, "https://example.com", active.Pane.URI)
93+}
M internal/ui/dispatcher/keyboard.go +15 -0
@@ -14,6 +14,12 @@ import (
1414 "github.com/bnema/puregotk/v4/glib"
1515 )
1616
17+const (
18+ historySystemViewURL = "dumb://history"
19+ favoritesSystemViewURL = "dumb://favorites"
20+ configSystemViewURL = "dumb://config"
21+)
22+
1723 // KeyboardDispatcher routes keyboard actions to appropriate coordinators.
1824 type KeyboardDispatcher struct {
1925 tabCoord *coordinator.TabCoordinator
@@ -225,6 +231,15 @@ func (d *KeyboardDispatcher) initActionHandlers() {
225231 }
226232 return d.logNoop(ctx, "toggle floating pane action (no handler)")
227233 },
234+ input.ActionToggleHistorySystemView: func(ctx context.Context) error {
235+ return d.wsCoord.ToggleSystemViewRight(ctx, historySystemViewURL)
236+ },
237+ input.ActionToggleFavoritesSystemView: func(ctx context.Context) error {
238+ return d.wsCoord.ToggleSystemViewRight(ctx, favoritesSystemViewURL)
239+ },
240+ input.ActionToggleConfigSystemView: func(ctx context.Context) error {
241+ return d.wsCoord.ToggleSystemViewRight(ctx, configSystemViewURL)
242+ },
228243 input.ActionToggleFullscreen: func(ctx context.Context) error {
229244 return d.logNoop(ctx, "toggle fullscreen action (not yet implemented)")
230245 },
@@ -14,6 +14,12 @@ import (
14 "github.com/bnema/puregotk/v4/glib"14 "github.com/bnema/puregotk/v4/glib"
15 )15 )
16 16
17+const (
18+ historySystemViewURL = "dumb://history"
19+ favoritesSystemViewURL = "dumb://favorites"
20+ configSystemViewURL = "dumb://config"
21+)
22+
17 // KeyboardDispatcher routes keyboard actions to appropriate coordinators.23 // KeyboardDispatcher routes keyboard actions to appropriate coordinators.
18 type KeyboardDispatcher struct {24 type KeyboardDispatcher struct {
19 tabCoord *coordinator.TabCoordinator25 tabCoord *coordinator.TabCoordinator
@@ -225,6 +231,15 @@ func (d *KeyboardDispatcher) initActionHandlers() {
225 }231 }
226 return d.logNoop(ctx, "toggle floating pane action (no handler)")232 return d.logNoop(ctx, "toggle floating pane action (no handler)")
227 },233 },
234+ input.ActionToggleHistorySystemView: func(ctx context.Context) error {
235+ return d.wsCoord.ToggleSystemViewRight(ctx, historySystemViewURL)
236+ },
237+ input.ActionToggleFavoritesSystemView: func(ctx context.Context) error {
238+ return d.wsCoord.ToggleSystemViewRight(ctx, favoritesSystemViewURL)
239+ },
240+ input.ActionToggleConfigSystemView: func(ctx context.Context) error {
241+ return d.wsCoord.ToggleSystemViewRight(ctx, configSystemViewURL)
242+ },
228 input.ActionToggleFullscreen: func(ctx context.Context) error {243 input.ActionToggleFullscreen: func(ctx context.Context) error {
229 return d.logNoop(ctx, "toggle fullscreen action (not yet implemented)")244 return d.logNoop(ctx, "toggle fullscreen action (not yet implemented)")
230 },245 },
M internal/ui/dispatcher/keyboard_test.go +34 -0
@@ -7,6 +7,7 @@ import (
77 "github.com/bnema/dumber/internal/application/usecase"
88 "github.com/bnema/dumber/internal/domain/entity"
99 domainurl "github.com/bnema/dumber/internal/domain/url"
10+ "github.com/bnema/dumber/internal/ui/component"
1011 "github.com/bnema/dumber/internal/ui/coordinator"
1112 "github.com/bnema/dumber/internal/ui/input"
1213 "github.com/stretchr/testify/assert"
@@ -114,6 +115,39 @@ func TestKeyboardDispatcher_AltNumberErrorsWhenMissingTabRequiresCreationWithout
114115 assert.Equal(t, tabs.TabAt(0).ID, tabs.ActiveTabID)
115116 }
116117
118+func TestKeyboardDispatcher_ToggleHistorySystemViewOpensRightSplit(t *testing.T) {
119+ ctx := context.Background()
120+ ids := []string{"pane-2", "split-1"}
121+ idx := 0
122+ panesUC := usecase.NewManagePanesUseCase(func() string {
123+ id := ids[idx]
124+ idx++
125+ return id
126+ })
127+
128+ initialPane := entity.NewPane("pane-1")
129+ initialPane.URI = "https://example.com"
130+ ws := entity.NewWorkspace("ws-1", initialPane)
131+ wsCoord := coordinator.NewWorkspaceCoordinator(ctx, coordinator.WorkspaceCoordinatorConfig{
132+ PanesUC: panesUC,
133+ GetActiveWS: func() (*entity.Workspace, *component.WorkspaceView) {
134+ return ws, nil
135+ },
136+ })
137+
138+ d := NewKeyboardDispatcher(ctx, &coordinator.TabCoordinator{}, wsCoord, &coordinator.NavigationCoordinator{}, nil, nil, "", func(context.Context) entity.PaneID { return "" })
139+
140+ err := d.Dispatch(ctx, input.ActionToggleHistorySystemView)
141+ require.NoError(t, err)
142+
143+ require.Equal(t, 2, ws.PaneCount())
144+ active := ws.ActivePane()
145+ require.NotNil(t, active)
146+ require.NotNil(t, active.Pane)
147+ assert.Equal(t, entity.PaneID("pane-2"), active.Pane.ID)
148+ assert.Equal(t, "dumb://history", active.Pane.URI)
149+}
150+
117151 func TestKeyboardDispatcher_PassesActivePaneIDToShellCallbacks(t *testing.T) {
118152 ctx := context.Background()
119153 activePaneID := entity.PaneID("pane-1")
@@ -7,6 +7,7 @@ import (
7 "github.com/bnema/dumber/internal/application/usecase"7 "github.com/bnema/dumber/internal/application/usecase"
8 "github.com/bnema/dumber/internal/domain/entity"8 "github.com/bnema/dumber/internal/domain/entity"
9 domainurl "github.com/bnema/dumber/internal/domain/url"9 domainurl "github.com/bnema/dumber/internal/domain/url"
10+ "github.com/bnema/dumber/internal/ui/component"
10 "github.com/bnema/dumber/internal/ui/coordinator"11 "github.com/bnema/dumber/internal/ui/coordinator"
11 "github.com/bnema/dumber/internal/ui/input"12 "github.com/bnema/dumber/internal/ui/input"
12 "github.com/stretchr/testify/assert"13 "github.com/stretchr/testify/assert"
@@ -114,6 +115,39 @@ func TestKeyboardDispatcher_AltNumberErrorsWhenMissingTabRequiresCreationWithout
114 assert.Equal(t, tabs.TabAt(0).ID, tabs.ActiveTabID)115 assert.Equal(t, tabs.TabAt(0).ID, tabs.ActiveTabID)
115 }116 }
116 117
118+func TestKeyboardDispatcher_ToggleHistorySystemViewOpensRightSplit(t *testing.T) {
119+ ctx := context.Background()
120+ ids := []string{"pane-2", "split-1"}
121+ idx := 0
122+ panesUC := usecase.NewManagePanesUseCase(func() string {
123+ id := ids[idx]
124+ idx++
125+ return id
126+ })
127+
128+ initialPane := entity.NewPane("pane-1")
129+ initialPane.URI = "https://example.com"
130+ ws := entity.NewWorkspace("ws-1", initialPane)
131+ wsCoord := coordinator.NewWorkspaceCoordinator(ctx, coordinator.WorkspaceCoordinatorConfig{
132+ PanesUC: panesUC,
133+ GetActiveWS: func() (*entity.Workspace, *component.WorkspaceView) {
134+ return ws, nil
135+ },
136+ })
137+
138+ d := NewKeyboardDispatcher(ctx, &coordinator.TabCoordinator{}, wsCoord, &coordinator.NavigationCoordinator{}, nil, nil, "", func(context.Context) entity.PaneID { return "" })
139+
140+ err := d.Dispatch(ctx, input.ActionToggleHistorySystemView)
141+ require.NoError(t, err)
142+
143+ require.Equal(t, 2, ws.PaneCount())
144+ active := ws.ActivePane()
145+ require.NotNil(t, active)
146+ require.NotNil(t, active.Pane)
147+ assert.Equal(t, entity.PaneID("pane-2"), active.Pane.ID)
148+ assert.Equal(t, "dumb://history", active.Pane.URI)
149+}
150+
117 func TestKeyboardDispatcher_PassesActivePaneIDToShellCallbacks(t *testing.T) {151 func TestKeyboardDispatcher_PassesActivePaneIDToShellCallbacks(t *testing.T) {
118 ctx := context.Background()152 ctx := context.Background()
119 activePaneID := entity.PaneID("pane-1")153 activePaneID := entity.PaneID("pane-1")
M internal/ui/input/global_shortcuts.go +24 -12
@@ -106,12 +106,18 @@ func NewGlobalShortcutHandler(
106106 if workspace != nil {
107107 // Map action names to action constants for global shortcuts
108108 actionMap := map[string]Action{
109- "toggle_floating_pane": ActionToggleFloatingPane,
110- "toggle-floating-pane": ActionToggleFloatingPane,
111- "consume_or_expel_left": ActionConsumeOrExpelLeft,
112- "consume_or_expel_right": ActionConsumeOrExpelRight,
113- "consume_or_expel_up": ActionConsumeOrExpelUp,
114- "consume_or_expel_down": ActionConsumeOrExpelDown,
109+ "toggle_floating_pane": ActionToggleFloatingPane,
110+ "toggle-floating-pane": ActionToggleFloatingPane,
111+ "consume_or_expel_left": ActionConsumeOrExpelLeft,
112+ "consume_or_expel_right": ActionConsumeOrExpelRight,
113+ "consume_or_expel_up": ActionConsumeOrExpelUp,
114+ "consume_or_expel_down": ActionConsumeOrExpelDown,
115+ "toggle_history_systemview": ActionToggleHistorySystemView,
116+ "toggle-history-systemview": ActionToggleHistorySystemView,
117+ "toggle_favorites_systemview": ActionToggleFavoritesSystemView,
118+ "toggle-favorites-systemview": ActionToggleFavoritesSystemView,
119+ "toggle_config_systemview": ActionToggleConfigSystemView,
120+ "toggle-config-systemview": ActionToggleConfigSystemView,
115121 }
116122
117123 for actionName, actionBinding := range workspace.Shortcuts.Actions {
@@ -286,12 +292,18 @@ func (h *GlobalShortcutHandler) ReloadShortcuts(ctx context.Context, workspace *
286292 // Re-register config-driven shortcuts
287293 if workspace != nil {
288294 actionMap := map[string]Action{
289- "toggle_floating_pane": ActionToggleFloatingPane,
290- "toggle-floating-pane": ActionToggleFloatingPane,
291- "consume_or_expel_left": ActionConsumeOrExpelLeft,
292- "consume_or_expel_right": ActionConsumeOrExpelRight,
293- "consume_or_expel_up": ActionConsumeOrExpelUp,
294- "consume_or_expel_down": ActionConsumeOrExpelDown,
295+ "toggle_floating_pane": ActionToggleFloatingPane,
296+ "toggle-floating-pane": ActionToggleFloatingPane,
297+ "consume_or_expel_left": ActionConsumeOrExpelLeft,
298+ "consume_or_expel_right": ActionConsumeOrExpelRight,
299+ "consume_or_expel_up": ActionConsumeOrExpelUp,
300+ "consume_or_expel_down": ActionConsumeOrExpelDown,
301+ "toggle_history_systemview": ActionToggleHistorySystemView,
302+ "toggle-history-systemview": ActionToggleHistorySystemView,
303+ "toggle_favorites_systemview": ActionToggleFavoritesSystemView,
304+ "toggle-favorites-systemview": ActionToggleFavoritesSystemView,
305+ "toggle_config_systemview": ActionToggleConfigSystemView,
306+ "toggle-config-systemview": ActionToggleConfigSystemView,
295307 }
296308
297309 for actionName, actionBinding := range workspace.Shortcuts.Actions {
@@ -106,12 +106,18 @@ func NewGlobalShortcutHandler(
106 if workspace != nil {106 if workspace != nil {
107 // Map action names to action constants for global shortcuts107 // Map action names to action constants for global shortcuts
108 actionMap := map[string]Action{108 actionMap := map[string]Action{
109- "toggle_floating_pane": ActionToggleFloatingPane,109+ "toggle_floating_pane": ActionToggleFloatingPane,
110- "toggle-floating-pane": ActionToggleFloatingPane,110+ "toggle-floating-pane": ActionToggleFloatingPane,
111- "consume_or_expel_left": ActionConsumeOrExpelLeft,111+ "consume_or_expel_left": ActionConsumeOrExpelLeft,
112- "consume_or_expel_right": ActionConsumeOrExpelRight,112+ "consume_or_expel_right": ActionConsumeOrExpelRight,
113- "consume_or_expel_up": ActionConsumeOrExpelUp,113+ "consume_or_expel_up": ActionConsumeOrExpelUp,
114- "consume_or_expel_down": ActionConsumeOrExpelDown,114+ "consume_or_expel_down": ActionConsumeOrExpelDown,
115+ "toggle_history_systemview": ActionToggleHistorySystemView,
116+ "toggle-history-systemview": ActionToggleHistorySystemView,
117+ "toggle_favorites_systemview": ActionToggleFavoritesSystemView,
118+ "toggle-favorites-systemview": ActionToggleFavoritesSystemView,
119+ "toggle_config_systemview": ActionToggleConfigSystemView,
120+ "toggle-config-systemview": ActionToggleConfigSystemView,
115 }121 }
116 122
117 for actionName, actionBinding := range workspace.Shortcuts.Actions {123 for actionName, actionBinding := range workspace.Shortcuts.Actions {
@@ -286,12 +292,18 @@ func (h *GlobalShortcutHandler) ReloadShortcuts(ctx context.Context, workspace *
286 // Re-register config-driven shortcuts292 // Re-register config-driven shortcuts
287 if workspace != nil {293 if workspace != nil {
288 actionMap := map[string]Action{294 actionMap := map[string]Action{
289- "toggle_floating_pane": ActionToggleFloatingPane,295+ "toggle_floating_pane": ActionToggleFloatingPane,
290- "toggle-floating-pane": ActionToggleFloatingPane,296+ "toggle-floating-pane": ActionToggleFloatingPane,
291- "consume_or_expel_left": ActionConsumeOrExpelLeft,297+ "consume_or_expel_left": ActionConsumeOrExpelLeft,
292- "consume_or_expel_right": ActionConsumeOrExpelRight,298+ "consume_or_expel_right": ActionConsumeOrExpelRight,
293- "consume_or_expel_up": ActionConsumeOrExpelUp,299+ "consume_or_expel_up": ActionConsumeOrExpelUp,
294- "consume_or_expel_down": ActionConsumeOrExpelDown,300+ "consume_or_expel_down": ActionConsumeOrExpelDown,
301+ "toggle_history_systemview": ActionToggleHistorySystemView,
302+ "toggle-history-systemview": ActionToggleHistorySystemView,
303+ "toggle_favorites_systemview": ActionToggleFavoritesSystemView,
304+ "toggle-favorites-systemview": ActionToggleFavoritesSystemView,
305+ "toggle_config_systemview": ActionToggleConfigSystemView,
306+ "toggle-config-systemview": ActionToggleConfigSystemView,
295 }307 }
296 308
297 for actionName, actionBinding := range workspace.Shortcuts.Actions {309 for actionName, actionBinding := range workspace.Shortcuts.Actions {
M internal/ui/input/shortcuts.go +41 -26
@@ -179,14 +179,17 @@ const (
179179 ActionZoomReset Action = "zoom_reset"
180180
181181 // UI
182- ActionOpenOmnibox Action = "open_omnibox"
183- ActionOpenFind Action = "open_find"
184- ActionFindNext Action = "find_next"
185- ActionFindPrev Action = "find_prev"
186- ActionCloseFind Action = "close_find"
187- ActionOpenDevTools Action = "open_devtools"
188- ActionToggleFullscreen Action = "toggle_fullscreen"
189- ActionToggleFloatingPane Action = "toggle_floating_pane"
182+ ActionOpenOmnibox Action = "open_omnibox"
183+ ActionOpenFind Action = "open_find"
184+ ActionFindNext Action = "find_next"
185+ ActionFindPrev Action = "find_prev"
186+ ActionCloseFind Action = "close_find"
187+ ActionOpenDevTools Action = "open_devtools"
188+ ActionToggleFullscreen Action = "toggle_fullscreen"
189+ ActionToggleFloatingPane Action = "toggle_floating_pane"
190+ ActionToggleHistorySystemView Action = "toggle_history_systemview"
191+ ActionToggleFavoritesSystemView Action = "toggle_favorites_systemview"
192+ ActionToggleConfigSystemView Action = "toggle_config_systemview"
190193
191194 // Clipboard
192195 ActionCopyURL Action = "copy_url"
@@ -362,22 +365,28 @@ func (s *ShortcutSet) registerConfiguredShortcuts(cfg *entity.WorkspaceConfig) {
362365 // However, these standard browser shortcuts ARE global:
363366 // Map action names to action constants
364367 actionMap := map[string]Action{
365- "toggle_floating_pane": ActionToggleFloatingPane,
366- "toggle-floating-pane": ActionToggleFloatingPane,
367- "close_pane": ActionClosePane,
368- "close-pane": ActionClosePane,
369- "next_tab": ActionNextTab,
370- "next-tab": ActionNextTab,
371- "previous_tab": ActionPreviousTab,
372- "previous-tab": ActionPreviousTab,
373- "consume_or_expel_left": ActionConsumeOrExpelLeft,
374- "consume-or-expel-left": ActionConsumeOrExpelLeft,
375- "consume_or_expel_right": ActionConsumeOrExpelRight,
376- "consume-or-expel-right": ActionConsumeOrExpelRight,
377- "consume_or_expel_up": ActionConsumeOrExpelUp,
378- "consume-or-expel-up": ActionConsumeOrExpelUp,
379- "consume_or_expel_down": ActionConsumeOrExpelDown,
380- "consume-or-expel-down": ActionConsumeOrExpelDown,
368+ "toggle_floating_pane": ActionToggleFloatingPane,
369+ "toggle-floating-pane": ActionToggleFloatingPane,
370+ "close_pane": ActionClosePane,
371+ "close-pane": ActionClosePane,
372+ "next_tab": ActionNextTab,
373+ "next-tab": ActionNextTab,
374+ "previous_tab": ActionPreviousTab,
375+ "previous-tab": ActionPreviousTab,
376+ "consume_or_expel_left": ActionConsumeOrExpelLeft,
377+ "consume-or-expel-left": ActionConsumeOrExpelLeft,
378+ "consume_or_expel_right": ActionConsumeOrExpelRight,
379+ "consume-or-expel-right": ActionConsumeOrExpelRight,
380+ "consume_or_expel_up": ActionConsumeOrExpelUp,
381+ "consume-or-expel-up": ActionConsumeOrExpelUp,
382+ "consume_or_expel_down": ActionConsumeOrExpelDown,
383+ "consume-or-expel-down": ActionConsumeOrExpelDown,
384+ "toggle_history_systemview": ActionToggleHistorySystemView,
385+ "toggle-history-systemview": ActionToggleHistorySystemView,
386+ "toggle_favorites_systemview": ActionToggleFavoritesSystemView,
387+ "toggle-favorites-systemview": ActionToggleFavoritesSystemView,
388+ "toggle_config_systemview": ActionToggleConfigSystemView,
389+ "toggle-config-systemview": ActionToggleConfigSystemView,
381390 }
382391
383392 for actionName, actionBinding := range cfg.Shortcuts.Actions {
@@ -580,8 +589,14 @@ func (s *ShortcutSet) buildModeShortcuts(ctx context.Context, bindings map[strin
580589 }
581590
582591 var configActionToAction = map[string]Action{
583- "toggle_floating_pane": ActionToggleFloatingPane,
584- "toggle-floating-pane": ActionToggleFloatingPane,
592+ "toggle_floating_pane": ActionToggleFloatingPane,
593+ "toggle-floating-pane": ActionToggleFloatingPane,
594+ "toggle_history_systemview": ActionToggleHistorySystemView,
595+ "toggle-history-systemview": ActionToggleHistorySystemView,
596+ "toggle_favorites_systemview": ActionToggleFavoritesSystemView,
597+ "toggle-favorites-systemview": ActionToggleFavoritesSystemView,
598+ "toggle_config_systemview": ActionToggleConfigSystemView,
599+ "toggle-config-systemview": ActionToggleConfigSystemView,
585600
586601 // Tab actions
587602 "new-tab": ActionNewTab,
@@ -179,14 +179,17 @@ const (
179 ActionZoomReset Action = "zoom_reset"179 ActionZoomReset Action = "zoom_reset"
180 180
181 // UI181 // UI
182- ActionOpenOmnibox Action = "open_omnibox"182+ ActionOpenOmnibox Action = "open_omnibox"
183- ActionOpenFind Action = "open_find"183+ ActionOpenFind Action = "open_find"
184- ActionFindNext Action = "find_next"184+ ActionFindNext Action = "find_next"
185- ActionFindPrev Action = "find_prev"185+ ActionFindPrev Action = "find_prev"
186- ActionCloseFind Action = "close_find"186+ ActionCloseFind Action = "close_find"
187- ActionOpenDevTools Action = "open_devtools"187+ ActionOpenDevTools Action = "open_devtools"
188- ActionToggleFullscreen Action = "toggle_fullscreen"188+ ActionToggleFullscreen Action = "toggle_fullscreen"
189- ActionToggleFloatingPane Action = "toggle_floating_pane"189+ ActionToggleFloatingPane Action = "toggle_floating_pane"
190+ ActionToggleHistorySystemView Action = "toggle_history_systemview"
191+ ActionToggleFavoritesSystemView Action = "toggle_favorites_systemview"
192+ ActionToggleConfigSystemView Action = "toggle_config_systemview"
190 193
191 // Clipboard194 // Clipboard
192 ActionCopyURL Action = "copy_url"195 ActionCopyURL Action = "copy_url"
@@ -362,22 +365,28 @@ func (s *ShortcutSet) registerConfiguredShortcuts(cfg *entity.WorkspaceConfig) {
362 // However, these standard browser shortcuts ARE global:365 // However, these standard browser shortcuts ARE global:
363 // Map action names to action constants366 // Map action names to action constants
364 actionMap := map[string]Action{367 actionMap := map[string]Action{
365- "toggle_floating_pane": ActionToggleFloatingPane,368+ "toggle_floating_pane": ActionToggleFloatingPane,
366- "toggle-floating-pane": ActionToggleFloatingPane,369+ "toggle-floating-pane": ActionToggleFloatingPane,
367- "close_pane": ActionClosePane,370+ "close_pane": ActionClosePane,
368- "close-pane": ActionClosePane,371+ "close-pane": ActionClosePane,
369- "next_tab": ActionNextTab,372+ "next_tab": ActionNextTab,
370- "next-tab": ActionNextTab,373+ "next-tab": ActionNextTab,
371- "previous_tab": ActionPreviousTab,374+ "previous_tab": ActionPreviousTab,
372- "previous-tab": ActionPreviousTab,375+ "previous-tab": ActionPreviousTab,
373- "consume_or_expel_left": ActionConsumeOrExpelLeft,376+ "consume_or_expel_left": ActionConsumeOrExpelLeft,
374- "consume-or-expel-left": ActionConsumeOrExpelLeft,377+ "consume-or-expel-left": ActionConsumeOrExpelLeft,
375- "consume_or_expel_right": ActionConsumeOrExpelRight,378+ "consume_or_expel_right": ActionConsumeOrExpelRight,
376- "consume-or-expel-right": ActionConsumeOrExpelRight,379+ "consume-or-expel-right": ActionConsumeOrExpelRight,
377- "consume_or_expel_up": ActionConsumeOrExpelUp,380+ "consume_or_expel_up": ActionConsumeOrExpelUp,
378- "consume-or-expel-up": ActionConsumeOrExpelUp,381+ "consume-or-expel-up": ActionConsumeOrExpelUp,
379- "consume_or_expel_down": ActionConsumeOrExpelDown,382+ "consume_or_expel_down": ActionConsumeOrExpelDown,
380- "consume-or-expel-down": ActionConsumeOrExpelDown,383+ "consume-or-expel-down": ActionConsumeOrExpelDown,
384+ "toggle_history_systemview": ActionToggleHistorySystemView,
385+ "toggle-history-systemview": ActionToggleHistorySystemView,
386+ "toggle_favorites_systemview": ActionToggleFavoritesSystemView,
387+ "toggle-favorites-systemview": ActionToggleFavoritesSystemView,
388+ "toggle_config_systemview": ActionToggleConfigSystemView,
389+ "toggle-config-systemview": ActionToggleConfigSystemView,
381 }390 }
382 391
383 for actionName, actionBinding := range cfg.Shortcuts.Actions {392 for actionName, actionBinding := range cfg.Shortcuts.Actions {
@@ -580,8 +589,14 @@ func (s *ShortcutSet) buildModeShortcuts(ctx context.Context, bindings map[strin
580 }589 }
581 590
582 var configActionToAction = map[string]Action{591 var configActionToAction = map[string]Action{
583- "toggle_floating_pane": ActionToggleFloatingPane,592+ "toggle_floating_pane": ActionToggleFloatingPane,
584- "toggle-floating-pane": ActionToggleFloatingPane,593+ "toggle-floating-pane": ActionToggleFloatingPane,
594+ "toggle_history_systemview": ActionToggleHistorySystemView,
595+ "toggle-history-systemview": ActionToggleHistorySystemView,
596+ "toggle_favorites_systemview": ActionToggleFavoritesSystemView,
597+ "toggle-favorites-systemview": ActionToggleFavoritesSystemView,
598+ "toggle_config_systemview": ActionToggleConfigSystemView,
599+ "toggle-config-systemview": ActionToggleConfigSystemView,
585 600
586 // Tab actions601 // Tab actions
587 "new-tab": ActionNewTab,602 "new-tab": ActionNewTab,
M internal/ui/input/shortcuts_test.go +19 -0
@@ -383,6 +383,25 @@ func TestMapConfigAction_ToggleFloatingPane(t *testing.T) {
383383 }
384384 }
385385
386+func TestMapConfigAction_ToggleSystemViews(t *testing.T) {
387+ tests := []struct {
388+ name string
389+ want Action
390+ }{
391+ {name: "toggle_history_systemview", want: ActionToggleHistorySystemView},
392+ {name: "toggle_favorites_systemview", want: ActionToggleFavoritesSystemView},
393+ {name: "toggle_config_systemview", want: ActionToggleConfigSystemView},
394+ }
395+
396+ for _, tt := range tests {
397+ t.Run(tt.name, func(t *testing.T) {
398+ if got := mapConfigAction(tt.name); got != tt.want {
399+ t.Fatalf("mapConfigAction(%s) = %s, want %s", tt.name, got, tt.want)
400+ }
401+ })
402+ }
403+}
404+
386405 func TestMapConfigAction_ToggleFloatingPaneHyphenAlias(t *testing.T) {
387406 action := mapConfigAction("toggle-floating-pane")
388407 if action != ActionToggleFloatingPane {
@@ -383,6 +383,25 @@ func TestMapConfigAction_ToggleFloatingPane(t *testing.T) {
383 }383 }
384 }384 }
385 385
386+func TestMapConfigAction_ToggleSystemViews(t *testing.T) {
387+ tests := []struct {
388+ name string
389+ want Action
390+ }{
391+ {name: "toggle_history_systemview", want: ActionToggleHistorySystemView},
392+ {name: "toggle_favorites_systemview", want: ActionToggleFavoritesSystemView},
393+ {name: "toggle_config_systemview", want: ActionToggleConfigSystemView},
394+ }
395+
396+ for _, tt := range tests {
397+ t.Run(tt.name, func(t *testing.T) {
398+ if got := mapConfigAction(tt.name); got != tt.want {
399+ t.Fatalf("mapConfigAction(%s) = %s, want %s", tt.name, got, tt.want)
400+ }
401+ })
402+ }
403+}
404+
386 func TestMapConfigAction_ToggleFloatingPaneHyphenAlias(t *testing.T) {405 func TestMapConfigAction_ToggleFloatingPaneHyphenAlias(t *testing.T) {
387 action := mapConfigAction("toggle-floating-pane")406 action := mapConfigAction("toggle-floating-pane")
388 if action != ActionToggleFloatingPane {407 if action != ActionToggleFloatingPane {

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 8

Tags 42

↑↓ moveenter openesc close