feat(systemviews): add toggle split shortcuts
- commit
- 3183f095a2af6eb80b761622aaf0dc2931c8d06d
- author
- brice · · 2026-04-24 02:11 UTC
- parent
- 4f3ae02
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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: | |||
| 80 | 80 | | Action | Keys | | |
| 81 | 81 | |--------|------| | |
| 82 | 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 | 86 | | Close pane (or release floating pane) | `Ctrl+W` | | |
| 84 | 87 | | Next tab | `Ctrl+Tab` | | |
| 85 | 88 | | Previous tab | `Ctrl+Shift+Tab` | | |
| @@ -90,6 +93,7 @@ These work outside modal modes: | |||
| 90 | 93 | ||
| 91 | 94 | - `Alt+F` is the only floating-pane shortcut enabled by default. | |
| 92 | 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 | 97 | - `Ctrl+W` closes the active pane; when the floating pane is active, it fully releases that floating session. | |
| 94 | 98 | - Any URL shortcut (for example `Alt+G`) must be defined explicitly in `workspace.floating_pane.profiles`. | |
| 95 | 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 | 118 | [workspace.shortcuts.actions.toggle_floating_pane] | |
| 115 | 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 | 130 | [workspace.floating_pane] | |
| 118 | 131 | width_pct = 0.82 | |
| 119 | 132 | 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.82 | 131 | width_pct = 0.82 | ||
| 119 | height_pct = 0.72 | 132 | height_pct = 0.72 | ||
M docs/systemviews-wasm-crud-plan.md +3 -3
| @@ -84,9 +84,9 @@ Add first-class global actions: | |||
| 84 | 84 | ||
| 85 | 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. | | |
| 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`. | | |
| 90 | 90 | ||
| 91 | 91 | Implementation path: | |
| 92 | 92 | ||
| @@ -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 { | |||
| 238 | 238 | }, | |
| 239 | 239 | Shortcuts: GlobalShortcutsConfig{ | |
| 240 | 240 | 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"}, | |
| 249 | 252 | }, | |
| 250 | 253 | }, | |
| 251 | 254 | 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) { | |||
| 26 | 26 | assert.False(t, cfg.Engine.CEF.TraceHandlers) | |
| 27 | 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 | 34 | // Old sections (Rendering, Privacy, Performance, Runtime) have been removed from Config. | |
| 30 | 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 | + | } | |
| @@ -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 | |||
| 171 | 171 | ||
| 172 | 172 | // Split splits the active pane in the given direction. | |
| 173 | 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 | 183 | log := logging.FromContext(ctx) | |
| 175 | 184 | ||
| 176 | 185 | splitCtx, ok := c.prepareSplit(ctx, direction) | |
| @@ -182,7 +191,7 @@ func (c *WorkspaceCoordinator) Split(ctx context.Context, direction usecase.Spli | |||
| 182 | 191 | Workspace: splitCtx.ws, | |
| 183 | 192 | TargetPane: splitCtx.activePane, | |
| 184 | 193 | Direction: direction, | |
| 185 | - | InitialURL: domainurl.Normalize(c.newPaneURL), | |
| 194 | + | InitialURL: initialURL, | |
| 186 | 195 | }) | |
| 187 | 196 | if err != nil { | |
| 188 | 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 | 221 | 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 | 307 | func (c *WorkspaceCoordinator) prepareSplit( | |
| 216 | 308 | ctx context.Context, | |
| 217 | 309 | 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 nil | 221 | 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 ( | |||
| 14 | 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 | 23 | // KeyboardDispatcher routes keyboard actions to appropriate coordinators. | |
| 18 | 24 | type KeyboardDispatcher struct { | |
| 19 | 25 | tabCoord *coordinator.TabCoordinator | |
| @@ -225,6 +231,15 @@ func (d *KeyboardDispatcher) initActionHandlers() { | |||
| 225 | 231 | } | |
| 226 | 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 | 243 | input.ActionToggleFullscreen: func(ctx context.Context) error { | |
| 229 | 244 | return d.logNoop(ctx, "toggle fullscreen action (not yet implemented)") | |
| 230 | 245 | }, | |
| @@ -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.TabCoordinator | 25 | 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 ( | |||
| 7 | 7 | "github.com/bnema/dumber/internal/application/usecase" | |
| 8 | 8 | "github.com/bnema/dumber/internal/domain/entity" | |
| 9 | 9 | domainurl "github.com/bnema/dumber/internal/domain/url" | |
| 10 | + | "github.com/bnema/dumber/internal/ui/component" | |
| 10 | 11 | "github.com/bnema/dumber/internal/ui/coordinator" | |
| 11 | 12 | "github.com/bnema/dumber/internal/ui/input" | |
| 12 | 13 | "github.com/stretchr/testify/assert" | |
| @@ -114,6 +115,39 @@ func TestKeyboardDispatcher_AltNumberErrorsWhenMissingTabRequiresCreationWithout | |||
| 114 | 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 | 151 | func TestKeyboardDispatcher_PassesActivePaneIDToShellCallbacks(t *testing.T) { | |
| 118 | 152 | ctx := context.Background() | |
| 119 | 153 | 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( | |||
| 106 | 106 | if workspace != nil { | |
| 107 | 107 | // Map action names to action constants for global shortcuts | |
| 108 | 108 | 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, | |
| 115 | 121 | } | |
| 116 | 122 | ||
| 117 | 123 | for actionName, actionBinding := range workspace.Shortcuts.Actions { | |
| @@ -286,12 +292,18 @@ func (h *GlobalShortcutHandler) ReloadShortcuts(ctx context.Context, workspace * | |||
| 286 | 292 | // Re-register config-driven shortcuts | |
| 287 | 293 | if workspace != nil { | |
| 288 | 294 | 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, | |
| 295 | 307 | } | |
| 296 | 308 | ||
| 297 | 309 | 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 shortcuts | 107 | // 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 shortcuts | 292 | // 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 ( | |||
| 179 | 179 | ActionZoomReset Action = "zoom_reset" | |
| 180 | 180 | ||
| 181 | 181 | // 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" | |
| 190 | 193 | ||
| 191 | 194 | // Clipboard | |
| 192 | 195 | ActionCopyURL Action = "copy_url" | |
| @@ -362,22 +365,28 @@ func (s *ShortcutSet) registerConfiguredShortcuts(cfg *entity.WorkspaceConfig) { | |||
| 362 | 365 | // However, these standard browser shortcuts ARE global: | |
| 363 | 366 | // Map action names to action constants | |
| 364 | 367 | 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, | |
| 381 | 390 | } | |
| 382 | 391 | ||
| 383 | 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 | 591 | 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, | |
| 585 | 600 | ||
| 586 | 601 | // Tab actions | |
| 587 | 602 | "new-tab": ActionNewTab, | |
| @@ -179,14 +179,17 @@ const ( | |||||
| 179 | ActionZoomReset Action = "zoom_reset" | 179 | ActionZoomReset Action = "zoom_reset" | ||
| 180 | 180 | ||||
| 181 | // UI | 181 | // 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 | // Clipboard | 194 | // 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 constants | 366 | // 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 actions | 601 | // 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) { | |||
| 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 | 405 | func TestMapConfigAction_ToggleFloatingPaneHyphenAlias(t *testing.T) { | |
| 387 | 406 | action := mapConfigAction("toggle-floating-pane") | |
| 388 | 407 | 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 { | ||