feat(favorites): add native bookmark shortcuts
- commit
- 4991fbe6b883a99bb2d5f769397769d76881764d
- author
- brice · · 2026-06-28 16:12 UTC
- committer
- brice · · 2026-06-28 16:17 UTC
- parent
- 3857325
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
13 files · +522 -59
M internal/infrastructure/config/defaults.go +12 -11
| @@ -322,17 +322,18 @@ func DefaultConfig() *Config { | |||
| 322 | 322 | }, | |
| 323 | 323 | Shortcuts: GlobalShortcutsConfig{ | |
| 324 | 324 | Actions: map[string]ActionBinding{ | |
| 325 | - | "toggle-floating-pane": {Keys: []string{"alt+f"}, Desc: "Toggle floating pane"}, | |
| 326 | - | "toggle-history-systemview": {Keys: []string{"ctrl+h"}, Desc: "Toggle History in right split"}, | |
| 327 | - | "toggle-favorites-systemview": {Keys: []string{}, Desc: "Toggle Favorites in right split"}, | |
| 328 | - | "toggle-config-systemview": {Keys: []string{}, Desc: "Toggle Config in right split"}, | |
| 329 | - | "close-pane": {Keys: []string{"ctrl+w"}, Desc: "Close active pane"}, | |
| 330 | - | "next-tab": {Keys: []string{"ctrl+tab"}, Desc: "Switch to next tab"}, | |
| 331 | - | "previous-tab": {Keys: []string{"ctrl+shift+tab"}, Desc: "Switch to previous tab"}, | |
| 332 | - | "consume-or-expel-left": {Keys: []string{"alt+["}, Desc: "Consume/expel pane left"}, | |
| 333 | - | "consume-or-expel-right": {Keys: []string{"alt+]"}, Desc: "Consume/expel pane right"}, | |
| 334 | - | "consume-or-expel-up": {Keys: []string{"alt+{"}, Desc: "Consume/expel pane up"}, | |
| 335 | - | "consume-or-expel-down": {Keys: []string{"alt+}"}, Desc: "Consume/expel pane down"}, | |
| 325 | + | "toggle-floating-pane": {Keys: []string{"alt+f"}, Desc: "Toggle floating pane"}, | |
| 326 | + | "toggle-history-systemview": {Keys: []string{"ctrl+h"}, Desc: "Toggle History in right split"}, | |
| 327 | + | "toggle-favorites-systemview": {Keys: []string{"ctrl+b"}, Desc: "Toggle Favorites sidebar"}, | |
| 328 | + | "toggle-current-page-favorite": {Keys: []string{"ctrl+d"}, Desc: "Add/remove current page favorite"}, | |
| 329 | + | "toggle-config-systemview": {Keys: []string{}, Desc: "Toggle Config in right split"}, | |
| 330 | + | "close-pane": {Keys: []string{"ctrl+w"}, Desc: "Close active pane"}, | |
| 331 | + | "next-tab": {Keys: []string{"ctrl+tab"}, Desc: "Switch to next tab"}, | |
| 332 | + | "previous-tab": {Keys: []string{"ctrl+shift+tab"}, Desc: "Switch to previous tab"}, | |
| 333 | + | "consume-or-expel-left": {Keys: []string{"alt+["}, Desc: "Consume/expel pane left"}, | |
| 334 | + | "consume-or-expel-right": {Keys: []string{"alt+]"}, Desc: "Consume/expel pane right"}, | |
| 335 | + | "consume-or-expel-up": {Keys: []string{"alt+{"}, Desc: "Consume/expel pane up"}, | |
| 336 | + | "consume-or-expel-down": {Keys: []string{"alt+}"}, Desc: "Consume/expel pane down"}, | |
| 336 | 337 | }, | |
| 337 | 338 | }, | |
| 338 | 339 | FloatingPane: FloatingPaneConfig{ | |
| @@ -322,17 +322,18 @@ func DefaultConfig() *Config { | |||||
| 322 | }, | 322 | }, | ||
| 323 | Shortcuts: GlobalShortcutsConfig{ | 323 | Shortcuts: GlobalShortcutsConfig{ | ||
| 324 | Actions: map[string]ActionBinding{ | 324 | Actions: map[string]ActionBinding{ | ||
| 325 | - | "toggle-floating-pane": {Keys: []string{"alt+f"}, Desc: "Toggle floating pane"}, | 325 | + | "toggle-floating-pane": {Keys: []string{"alt+f"}, Desc: "Toggle floating pane"}, |
| 326 | - | "toggle-history-systemview": {Keys: []string{"ctrl+h"}, Desc: "Toggle History in right split"}, | 326 | + | "toggle-history-systemview": {Keys: []string{"ctrl+h"}, Desc: "Toggle History in right split"}, |
| 327 | - | "toggle-favorites-systemview": {Keys: []string{}, Desc: "Toggle Favorites in right split"}, | 327 | + | "toggle-favorites-systemview": {Keys: []string{"ctrl+b"}, Desc: "Toggle Favorites sidebar"}, |
| 328 | - | "toggle-config-systemview": {Keys: []string{}, Desc: "Toggle Config in right split"}, | 328 | + | "toggle-current-page-favorite": {Keys: []string{"ctrl+d"}, Desc: "Add/remove current page favorite"}, |
| 329 | - | "close-pane": {Keys: []string{"ctrl+w"}, Desc: "Close active pane"}, | 329 | + | "toggle-config-systemview": {Keys: []string{}, Desc: "Toggle Config in right split"}, |
| 330 | - | "next-tab": {Keys: []string{"ctrl+tab"}, Desc: "Switch to next tab"}, | 330 | + | "close-pane": {Keys: []string{"ctrl+w"}, Desc: "Close active pane"}, |
| 331 | - | "previous-tab": {Keys: []string{"ctrl+shift+tab"}, Desc: "Switch to previous tab"}, | 331 | + | "next-tab": {Keys: []string{"ctrl+tab"}, Desc: "Switch to next tab"}, |
| 332 | - | "consume-or-expel-left": {Keys: []string{"alt+["}, Desc: "Consume/expel pane left"}, | 332 | + | "previous-tab": {Keys: []string{"ctrl+shift+tab"}, Desc: "Switch to previous tab"}, |
| 333 | - | "consume-or-expel-right": {Keys: []string{"alt+]"}, Desc: "Consume/expel pane right"}, | 333 | + | "consume-or-expel-left": {Keys: []string{"alt+["}, Desc: "Consume/expel pane left"}, |
| 334 | - | "consume-or-expel-up": {Keys: []string{"alt+{"}, Desc: "Consume/expel pane up"}, | 334 | + | "consume-or-expel-right": {Keys: []string{"alt+]"}, Desc: "Consume/expel pane right"}, |
| 335 | - | "consume-or-expel-down": {Keys: []string{"alt+}"}, Desc: "Consume/expel pane down"}, | 335 | + | "consume-or-expel-up": {Keys: []string{"alt+{"}, Desc: "Consume/expel pane up"}, |
| 336 | + | "consume-or-expel-down": {Keys: []string{"alt+}"}, Desc: "Consume/expel pane down"}, | |||
| 336 | }, | 337 | }, | ||
| 337 | }, | 338 | }, | ||
| 338 | FloatingPane: FloatingPaneConfig{ | 339 | FloatingPane: FloatingPaneConfig{ | ||
M internal/infrastructure/config/defaults_test.go +2 -1
| @@ -41,7 +41,8 @@ func TestDefaultConfig_CoreDefaults(t *testing.T) { | |||
| 41 | 41 | ||
| 42 | 42 | // System view shortcuts are first-class global actions. | |
| 43 | 43 | requireActionBinding(t, cfg.Workspace.Shortcuts.Actions, "toggle-history-systemview", []string{"ctrl+h"}) | |
| 44 | - | requireActionBinding(t, cfg.Workspace.Shortcuts.Actions, "toggle-favorites-systemview", []string{}) | |
| 44 | + | requireActionBinding(t, cfg.Workspace.Shortcuts.Actions, "toggle-favorites-systemview", []string{"ctrl+b"}) | |
| 45 | + | requireActionBinding(t, cfg.Workspace.Shortcuts.Actions, "toggle-current-page-favorite", []string{"ctrl+d"}) | |
| 45 | 46 | requireActionBinding(t, cfg.Workspace.Shortcuts.Actions, "toggle-config-systemview", []string{}) | |
| 46 | 47 | ||
| 47 | 48 | // Old sections (Rendering, Privacy, Performance, Runtime) have been removed from Config. | |
| @@ -41,7 +41,8 @@ func TestDefaultConfig_CoreDefaults(t *testing.T) { | |||||
| 41 | 41 | ||||
| 42 | // System view shortcuts are first-class global actions. | 42 | // System view shortcuts are first-class global actions. | ||
| 43 | requireActionBinding(t, cfg.Workspace.Shortcuts.Actions, "toggle-history-systemview", []string{"ctrl+h"}) | 43 | requireActionBinding(t, cfg.Workspace.Shortcuts.Actions, "toggle-history-systemview", []string{"ctrl+h"}) | ||
| 44 | - | requireActionBinding(t, cfg.Workspace.Shortcuts.Actions, "toggle-favorites-systemview", []string{}) | 44 | + | requireActionBinding(t, cfg.Workspace.Shortcuts.Actions, "toggle-favorites-systemview", []string{"ctrl+b"}) |
| 45 | + | requireActionBinding(t, cfg.Workspace.Shortcuts.Actions, "toggle-current-page-favorite", []string{"ctrl+d"}) | |||
| 45 | requireActionBinding(t, cfg.Workspace.Shortcuts.Actions, "toggle-config-systemview", []string{}) | 46 | requireActionBinding(t, cfg.Workspace.Shortcuts.Actions, "toggle-config-systemview", []string{}) | ||
| 46 | 47 | ||||
| 47 | // Old sections (Rendering, Privacy, Performance, Runtime) have been removed from Config. | 48 | // Old sections (Rendering, Privacy, Performance, Runtime) have been removed from Config. | ||
M internal/ui/app.go +8 -1
| @@ -2040,7 +2040,12 @@ func (a *App) hideAndRestoreFocusForBrowserWindow(bw *browserWindow) { | |||
| 2040 | 2040 | if bw == nil { | |
| 2041 | 2041 | return | |
| 2042 | 2042 | } | |
| 2043 | - | bw.hideHistorySidebar() | |
| 2043 | + | switch bw.activeSidebarKind { | |
| 2044 | + | case nativeSidebarFavorites: | |
| 2045 | + | bw.hideFavoritesSidebar() | |
| 2046 | + | default: | |
| 2047 | + | bw.hideHistorySidebar() | |
| 2048 | + | } | |
| 2044 | 2049 | if wsView := a.activeWorkspaceViewForBrowserWindow(bw); wsView != nil { | |
| 2045 | 2050 | if ws := a.activeWorkspaceForBrowserWindow(bw); ws != nil { | |
| 2046 | 2051 | wsView.FocusPane(ws.ActivePaneID) | |
| @@ -3357,6 +3362,8 @@ func (a *App) wireKeyboardActions() { | |||
| 3357 | 3362 | return a.EjectActivePaneToWindow(ctx, paneID) | |
| 3358 | 3363 | }) | |
| 3359 | 3364 | a.kbDispatcher.SetOnToggleHistorySidebar(a.toggleHistorySidebarAction) | |
| 3365 | + | a.kbDispatcher.SetOnToggleFavoritesSidebar(a.toggleFavoritesSidebarAction) | |
| 3366 | + | a.kbDispatcher.SetOnToggleCurrentPageFavorite(a.toggleCurrentPageFavoriteAction) | |
| 3360 | 3367 | a.kbDispatcher.SetOnToggleFloatingPane(func(ctx context.Context) error { | |
| 3361 | 3368 | return a.ToggleFloatingPane(ctx) | |
| 3362 | 3369 | }) | |
| @@ -2040,7 +2040,12 @@ func (a *App) hideAndRestoreFocusForBrowserWindow(bw *browserWindow) { | |||||
| 2040 | if bw == nil { | 2040 | if bw == nil { | ||
| 2041 | return | 2041 | return | ||
| 2042 | } | 2042 | } | ||
| 2043 | - | bw.hideHistorySidebar() | 2043 | + | switch bw.activeSidebarKind { |
| 2044 | + | case nativeSidebarFavorites: | |||
| 2045 | + | bw.hideFavoritesSidebar() | |||
| 2046 | + | default: | |||
| 2047 | + | bw.hideHistorySidebar() | |||
| 2048 | + | } | |||
| 2044 | if wsView := a.activeWorkspaceViewForBrowserWindow(bw); wsView != nil { | 2049 | if wsView := a.activeWorkspaceViewForBrowserWindow(bw); wsView != nil { | ||
| 2045 | if ws := a.activeWorkspaceForBrowserWindow(bw); ws != nil { | 2050 | if ws := a.activeWorkspaceForBrowserWindow(bw); ws != nil { | ||
| 2046 | wsView.FocusPane(ws.ActivePaneID) | 2051 | wsView.FocusPane(ws.ActivePaneID) | ||
| @@ -3357,6 +3362,8 @@ func (a *App) wireKeyboardActions() { | |||||
| 3357 | return a.EjectActivePaneToWindow(ctx, paneID) | 3362 | return a.EjectActivePaneToWindow(ctx, paneID) | ||
| 3358 | }) | 3363 | }) | ||
| 3359 | a.kbDispatcher.SetOnToggleHistorySidebar(a.toggleHistorySidebarAction) | 3364 | a.kbDispatcher.SetOnToggleHistorySidebar(a.toggleHistorySidebarAction) | ||
| 3365 | + | a.kbDispatcher.SetOnToggleFavoritesSidebar(a.toggleFavoritesSidebarAction) | |||
| 3366 | + | a.kbDispatcher.SetOnToggleCurrentPageFavorite(a.toggleCurrentPageFavoriteAction) | |||
| 3360 | a.kbDispatcher.SetOnToggleFloatingPane(func(ctx context.Context) error { | 3367 | a.kbDispatcher.SetOnToggleFloatingPane(func(ctx context.Context) error { | ||
| 3361 | return a.ToggleFloatingPane(ctx) | 3368 | return a.ToggleFloatingPane(ctx) | ||
| 3362 | }) | 3369 | }) | ||
M internal/ui/browser_window.go +16 -0
| @@ -19,6 +19,14 @@ import ( | |||
| 19 | 19 | "github.com/bnema/puregotk/v4/glib" | |
| 20 | 20 | ) | |
| 21 | 21 | ||
| 22 | + | type nativeSidebarKind string | |
| 23 | + | ||
| 24 | + | const ( | |
| 25 | + | nativeSidebarNone nativeSidebarKind = "" | |
| 26 | + | nativeSidebarHistory nativeSidebarKind = "history" | |
| 27 | + | nativeSidebarFavorites nativeSidebarKind = "favorites" | |
| 28 | + | ) | |
| 29 | + | ||
| 22 | 30 | type browserWindow struct { | |
| 23 | 31 | id string | |
| 24 | 32 | initialURL string | |
| @@ -39,8 +47,10 @@ type browserWindow struct { | |||
| 39 | 47 | permissionDialog port.PermissionDialogPresenter | |
| 40 | 48 | webrtcIndicator *component.WebRTCPermissionIndicator | |
| 41 | 49 | historySidebar *component.HistorySidebar | |
| 50 | + | favoritesSidebar *component.FavoritesSidebar | |
| 42 | 51 | historySidebarReloader historySidebarReloader | |
| 43 | 52 | sidebarVisible bool | |
| 53 | + | activeSidebarKind nativeSidebarKind | |
| 44 | 54 | } | |
| 45 | 55 | ||
| 46 | 56 | func (bw *browserWindow) detachInputForDestroy() { | |
| @@ -73,6 +83,9 @@ func (bw *browserWindow) clearShellState() { | |||
| 73 | 83 | if bw.historySidebar != nil { | |
| 74 | 84 | bw.historySidebar.Destroy() | |
| 75 | 85 | } | |
| 86 | + | if bw.favoritesSidebar != nil { | |
| 87 | + | bw.favoritesSidebar.Destroy() | |
| 88 | + | } | |
| 76 | 89 | if bw.touchpadNavIndicator != nil { | |
| 77 | 90 | bw.touchpadNavIndicator.Destroy() | |
| 78 | 91 | } | |
| @@ -91,7 +104,9 @@ func (bw *browserWindow) clearShellState() { | |||
| 91 | 104 | bw.permissionDialog = nil | |
| 92 | 105 | bw.webrtcIndicator = nil | |
| 93 | 106 | bw.historySidebar = nil | |
| 107 | + | bw.favoritesSidebar = nil | |
| 94 | 108 | bw.historySidebarReloader = nil | |
| 109 | + | bw.activeSidebarKind = nativeSidebarNone | |
| 95 | 110 | } | |
| 96 | 111 | ||
| 97 | 112 | func (bw *browserWindow) initChrome(ctx context.Context, a *App) { | |
| @@ -106,6 +121,7 @@ func (bw *browserWindow) initChrome(ctx context.Context, a *App) { | |||
| 106 | 121 | bw.initSessionManager(ctx, a) | |
| 107 | 122 | bw.initTabPicker(ctx, a) | |
| 108 | 123 | bw.initHistorySidebar(ctx, a) | |
| 124 | + | bw.initFavoritesSidebar(ctx, a) | |
| 109 | 125 | } | |
| 110 | 126 | ||
| 111 | 127 | func (bw *browserWindow) initToasterOverlay(a *App) { | |
| @@ -19,6 +19,14 @@ import ( | |||||
| 19 | "github.com/bnema/puregotk/v4/glib" | 19 | "github.com/bnema/puregotk/v4/glib" | ||
| 20 | ) | 20 | ) | ||
| 21 | 21 | ||||
| 22 | + | type nativeSidebarKind string | |||
| 23 | + | ||||
| 24 | + | const ( | |||
| 25 | + | nativeSidebarNone nativeSidebarKind = "" | |||
| 26 | + | nativeSidebarHistory nativeSidebarKind = "history" | |||
| 27 | + | nativeSidebarFavorites nativeSidebarKind = "favorites" | |||
| 28 | + | ) | |||
| 29 | + | ||||
| 22 | type browserWindow struct { | 30 | type browserWindow struct { | ||
| 23 | id string | 31 | id string | ||
| 24 | initialURL string | 32 | initialURL string | ||
| @@ -39,8 +47,10 @@ type browserWindow struct { | |||||
| 39 | permissionDialog port.PermissionDialogPresenter | 47 | permissionDialog port.PermissionDialogPresenter | ||
| 40 | webrtcIndicator *component.WebRTCPermissionIndicator | 48 | webrtcIndicator *component.WebRTCPermissionIndicator | ||
| 41 | historySidebar *component.HistorySidebar | 49 | historySidebar *component.HistorySidebar | ||
| 50 | + | favoritesSidebar *component.FavoritesSidebar | |||
| 42 | historySidebarReloader historySidebarReloader | 51 | historySidebarReloader historySidebarReloader | ||
| 43 | sidebarVisible bool | 52 | sidebarVisible bool | ||
| 53 | + | activeSidebarKind nativeSidebarKind | |||
| 44 | } | 54 | } | ||
| 45 | 55 | ||||
| 46 | func (bw *browserWindow) detachInputForDestroy() { | 56 | func (bw *browserWindow) detachInputForDestroy() { | ||
| @@ -73,6 +83,9 @@ func (bw *browserWindow) clearShellState() { | |||||
| 73 | if bw.historySidebar != nil { | 83 | if bw.historySidebar != nil { | ||
| 74 | bw.historySidebar.Destroy() | 84 | bw.historySidebar.Destroy() | ||
| 75 | } | 85 | } | ||
| 86 | + | if bw.favoritesSidebar != nil { | |||
| 87 | + | bw.favoritesSidebar.Destroy() | |||
| 88 | + | } | |||
| 76 | if bw.touchpadNavIndicator != nil { | 89 | if bw.touchpadNavIndicator != nil { | ||
| 77 | bw.touchpadNavIndicator.Destroy() | 90 | bw.touchpadNavIndicator.Destroy() | ||
| 78 | } | 91 | } | ||
| @@ -91,7 +104,9 @@ func (bw *browserWindow) clearShellState() { | |||||
| 91 | bw.permissionDialog = nil | 104 | bw.permissionDialog = nil | ||
| 92 | bw.webrtcIndicator = nil | 105 | bw.webrtcIndicator = nil | ||
| 93 | bw.historySidebar = nil | 106 | bw.historySidebar = nil | ||
| 107 | + | bw.favoritesSidebar = nil | |||
| 94 | bw.historySidebarReloader = nil | 108 | bw.historySidebarReloader = nil | ||
| 109 | + | bw.activeSidebarKind = nativeSidebarNone | |||
| 95 | } | 110 | } | ||
| 96 | 111 | ||||
| 97 | func (bw *browserWindow) initChrome(ctx context.Context, a *App) { | 112 | func (bw *browserWindow) initChrome(ctx context.Context, a *App) { | ||
| @@ -106,6 +121,7 @@ func (bw *browserWindow) initChrome(ctx context.Context, a *App) { | |||||
| 106 | bw.initSessionManager(ctx, a) | 121 | bw.initSessionManager(ctx, a) | ||
| 107 | bw.initTabPicker(ctx, a) | 122 | bw.initTabPicker(ctx, a) | ||
| 108 | bw.initHistorySidebar(ctx, a) | 123 | bw.initHistorySidebar(ctx, a) | ||
| 124 | + | bw.initFavoritesSidebar(ctx, a) | |||
| 109 | } | 125 | } | ||
| 110 | 126 | ||||
| 111 | func (bw *browserWindow) initToasterOverlay(a *App) { | 127 | func (bw *browserWindow) initToasterOverlay(a *App) { | ||
A internal/ui/browser_window_favorites_sidebar.go +134 -0
| @@ -0,0 +1,134 @@ | |||
| 1 | + | package ui | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "fmt" | |
| 6 | + | "strings" | |
| 7 | + | ||
| 8 | + | "github.com/bnema/dumber/internal/application/port" | |
| 9 | + | "github.com/bnema/dumber/internal/application/usecase" | |
| 10 | + | "github.com/bnema/dumber/internal/logging" | |
| 11 | + | "github.com/bnema/dumber/internal/ui/component" | |
| 12 | + | ) | |
| 13 | + | ||
| 14 | + | // initFavoritesSidebar creates the native favorites sidebar when favorites are configured. | |
| 15 | + | func (bw *browserWindow) initFavoritesSidebar(ctx context.Context, a *App) { | |
| 16 | + | if bw == nil || a == nil || bw.mainWindow == nil || a.deps == nil || a.deps.FavoritesUC == nil { | |
| 17 | + | return | |
| 18 | + | } | |
| 19 | + | log := logging.FromContext(ctx) | |
| 20 | + | ||
| 21 | + | cfg := a.buildFavoritesSidebarConfig(bw) | |
| 22 | + | sidebar := component.NewFavoritesSidebar(ctx, cfg) | |
| 23 | + | if sidebar == nil { | |
| 24 | + | log.Warn().Msg("failed to create favorites sidebar") | |
| 25 | + | return | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | bw.favoritesSidebar = sidebar | |
| 29 | + | bw.favoritesSidebar.Hide() | |
| 30 | + | bw.applySidebarWidthConfig(a) | |
| 31 | + | log.Debug().Msg("favorites sidebar initialized") | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | func (a *App) buildFavoritesSidebarConfig(bw *browserWindow) component.FavoritesSidebarConfig { | |
| 35 | + | var favoritesUC port.FavoritesSidebarFavorites | |
| 36 | + | if a.deps != nil { | |
| 37 | + | favoritesUC = a.deps.FavoritesUC | |
| 38 | + | } | |
| 39 | + | return component.FavoritesSidebarConfig{ | |
| 40 | + | FavoritesUC: favoritesUC, | |
| 41 | + | OnNavigate: func(navCtx context.Context, url string) error { | |
| 42 | + | return a.navigateFavoritesSidebarSelection(navCtx, bw, url) | |
| 43 | + | }, | |
| 44 | + | OnNavigateKeepOpen: func(navCtx context.Context, url string) error { | |
| 45 | + | return a.navigateFavoritesSidebarSelection(navCtx, bw, url) | |
| 46 | + | }, | |
| 47 | + | OnOpenInNewPane: func(splitCtx context.Context, url string) error { | |
| 48 | + | if a.wsCoord == nil || !a.hasBrowserWindow(bw) { | |
| 49 | + | return nil | |
| 50 | + | } | |
| 51 | + | a.activateBrowserWindow(bw) | |
| 52 | + | return a.wsCoord.SplitWithURL(splitCtx, usecase.SplitRight, url) | |
| 53 | + | }, | |
| 54 | + | OnClose: func() { | |
| 55 | + | a.hideAndRestoreFocusForBrowserWindow(bw) | |
| 56 | + | }, | |
| 57 | + | } | |
| 58 | + | } | |
| 59 | + | ||
| 60 | + | func (a *App) navigateFavoritesSidebarSelection(ctx context.Context, bw *browserWindow, url string) error { | |
| 61 | + | if a == nil || bw == nil || !a.hasBrowserWindow(bw) { | |
| 62 | + | return nil | |
| 63 | + | } | |
| 64 | + | return a.navigateFromBrowserWindow(ctx, bw, url) | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | func (bw *browserWindow) toggleFavoritesSidebar() { | |
| 68 | + | if bw == nil || bw.favoritesSidebar == nil { | |
| 69 | + | return | |
| 70 | + | } | |
| 71 | + | if bw.sidebarVisible && bw.activeSidebarKind == nativeSidebarFavorites { | |
| 72 | + | bw.hideFavoritesSidebar() | |
| 73 | + | return | |
| 74 | + | } | |
| 75 | + | bw.showFavoritesSidebar() | |
| 76 | + | } | |
| 77 | + | ||
| 78 | + | func (bw *browserWindow) showFavoritesSidebar() { | |
| 79 | + | if bw == nil || bw.favoritesSidebar == nil || bw.mainWindow == nil { | |
| 80 | + | return | |
| 81 | + | } | |
| 82 | + | bw.mountNativeSidebar(nativeSidebarFavorites) | |
| 83 | + | bw.favoritesSidebar.Show() | |
| 84 | + | bw.mainWindow.SetSidebarVisible(true) | |
| 85 | + | bw.sidebarVisible = true | |
| 86 | + | bw.activeSidebarKind = nativeSidebarFavorites | |
| 87 | + | } | |
| 88 | + | ||
| 89 | + | func (bw *browserWindow) hideFavoritesSidebar() { | |
| 90 | + | if bw == nil || bw.favoritesSidebar == nil || bw.mainWindow == nil { | |
| 91 | + | return | |
| 92 | + | } | |
| 93 | + | bw.favoritesSidebar.Hide() | |
| 94 | + | bw.mainWindow.SetSidebarVisible(false) | |
| 95 | + | bw.sidebarVisible = false | |
| 96 | + | if bw.activeSidebarKind == nativeSidebarFavorites { | |
| 97 | + | bw.activeSidebarKind = nativeSidebarNone | |
| 98 | + | } | |
| 99 | + | } | |
| 100 | + | ||
| 101 | + | func (a *App) toggleFavoritesSidebarAction(ctx context.Context) error { | |
| 102 | + | bw := a.lastFocusedBrowserWindow() | |
| 103 | + | if bw == nil { | |
| 104 | + | return fmt.Errorf("favorites sidebar unavailable: no focused browser window") | |
| 105 | + | } | |
| 106 | + | if bw.favoritesSidebar == nil { | |
| 107 | + | return fmt.Errorf("favorites sidebar unavailable: native sidebar not initialized") | |
| 108 | + | } | |
| 109 | + | if bw.sidebarVisible && bw.activeSidebarKind == nativeSidebarFavorites { | |
| 110 | + | a.hideAndRestoreFocusForBrowserWindow(bw) | |
| 111 | + | return nil | |
| 112 | + | } | |
| 113 | + | bw.toggleFavoritesSidebar() | |
| 114 | + | return nil | |
| 115 | + | } | |
| 116 | + | ||
| 117 | + | func (a *App) toggleCurrentPageFavoriteAction(ctx context.Context) error { | |
| 118 | + | if a == nil || a.deps == nil || a.deps.FavoritesUC == nil { | |
| 119 | + | return fmt.Errorf("favorites unavailable: usecase not configured") | |
| 120 | + | } | |
| 121 | + | _, wv := a.activeWebViewForBrowserWindow(a.lastFocusedBrowserWindow()) | |
| 122 | + | if wv == nil { | |
| 123 | + | return fmt.Errorf("favorites unavailable: no active webview") | |
| 124 | + | } | |
| 125 | + | uri := strings.TrimSpace(wv.URI()) | |
| 126 | + | if uri == "" { | |
| 127 | + | return fmt.Errorf("favorites unavailable: active page has no URI") | |
| 128 | + | } | |
| 129 | + | _, err := a.deps.FavoritesUC.Toggle(ctx, uri, wv.Title()) | |
| 130 | + | if err != nil { | |
| 131 | + | return err | |
| 132 | + | } | |
| 133 | + | return nil | |
| 134 | + | } | |
| @@ -0,0 +1,134 @@ | |||||
| 1 | + | package ui | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "fmt" | |||
| 6 | + | "strings" | |||
| 7 | + | ||||
| 8 | + | "github.com/bnema/dumber/internal/application/port" | |||
| 9 | + | "github.com/bnema/dumber/internal/application/usecase" | |||
| 10 | + | "github.com/bnema/dumber/internal/logging" | |||
| 11 | + | "github.com/bnema/dumber/internal/ui/component" | |||
| 12 | + | ) | |||
| 13 | + | ||||
| 14 | + | // initFavoritesSidebar creates the native favorites sidebar when favorites are configured. | |||
| 15 | + | func (bw *browserWindow) initFavoritesSidebar(ctx context.Context, a *App) { | |||
| 16 | + | if bw == nil || a == nil || bw.mainWindow == nil || a.deps == nil || a.deps.FavoritesUC == nil { | |||
| 17 | + | return | |||
| 18 | + | } | |||
| 19 | + | log := logging.FromContext(ctx) | |||
| 20 | + | ||||
| 21 | + | cfg := a.buildFavoritesSidebarConfig(bw) | |||
| 22 | + | sidebar := component.NewFavoritesSidebar(ctx, cfg) | |||
| 23 | + | if sidebar == nil { | |||
| 24 | + | log.Warn().Msg("failed to create favorites sidebar") | |||
| 25 | + | return | |||
| 26 | + | } | |||
| 27 | + | ||||
| 28 | + | bw.favoritesSidebar = sidebar | |||
| 29 | + | bw.favoritesSidebar.Hide() | |||
| 30 | + | bw.applySidebarWidthConfig(a) | |||
| 31 | + | log.Debug().Msg("favorites sidebar initialized") | |||
| 32 | + | } | |||
| 33 | + | ||||
| 34 | + | func (a *App) buildFavoritesSidebarConfig(bw *browserWindow) component.FavoritesSidebarConfig { | |||
| 35 | + | var favoritesUC port.FavoritesSidebarFavorites | |||
| 36 | + | if a.deps != nil { | |||
| 37 | + | favoritesUC = a.deps.FavoritesUC | |||
| 38 | + | } | |||
| 39 | + | return component.FavoritesSidebarConfig{ | |||
| 40 | + | FavoritesUC: favoritesUC, | |||
| 41 | + | OnNavigate: func(navCtx context.Context, url string) error { | |||
| 42 | + | return a.navigateFavoritesSidebarSelection(navCtx, bw, url) | |||
| 43 | + | }, | |||
| 44 | + | OnNavigateKeepOpen: func(navCtx context.Context, url string) error { | |||
| 45 | + | return a.navigateFavoritesSidebarSelection(navCtx, bw, url) | |||
| 46 | + | }, | |||
| 47 | + | OnOpenInNewPane: func(splitCtx context.Context, url string) error { | |||
| 48 | + | if a.wsCoord == nil || !a.hasBrowserWindow(bw) { | |||
| 49 | + | return nil | |||
| 50 | + | } | |||
| 51 | + | a.activateBrowserWindow(bw) | |||
| 52 | + | return a.wsCoord.SplitWithURL(splitCtx, usecase.SplitRight, url) | |||
| 53 | + | }, | |||
| 54 | + | OnClose: func() { | |||
| 55 | + | a.hideAndRestoreFocusForBrowserWindow(bw) | |||
| 56 | + | }, | |||
| 57 | + | } | |||
| 58 | + | } | |||
| 59 | + | ||||
| 60 | + | func (a *App) navigateFavoritesSidebarSelection(ctx context.Context, bw *browserWindow, url string) error { | |||
| 61 | + | if a == nil || bw == nil || !a.hasBrowserWindow(bw) { | |||
| 62 | + | return nil | |||
| 63 | + | } | |||
| 64 | + | return a.navigateFromBrowserWindow(ctx, bw, url) | |||
| 65 | + | } | |||
| 66 | + | ||||
| 67 | + | func (bw *browserWindow) toggleFavoritesSidebar() { | |||
| 68 | + | if bw == nil || bw.favoritesSidebar == nil { | |||
| 69 | + | return | |||
| 70 | + | } | |||
| 71 | + | if bw.sidebarVisible && bw.activeSidebarKind == nativeSidebarFavorites { | |||
| 72 | + | bw.hideFavoritesSidebar() | |||
| 73 | + | return | |||
| 74 | + | } | |||
| 75 | + | bw.showFavoritesSidebar() | |||
| 76 | + | } | |||
| 77 | + | ||||
| 78 | + | func (bw *browserWindow) showFavoritesSidebar() { | |||
| 79 | + | if bw == nil || bw.favoritesSidebar == nil || bw.mainWindow == nil { | |||
| 80 | + | return | |||
| 81 | + | } | |||
| 82 | + | bw.mountNativeSidebar(nativeSidebarFavorites) | |||
| 83 | + | bw.favoritesSidebar.Show() | |||
| 84 | + | bw.mainWindow.SetSidebarVisible(true) | |||
| 85 | + | bw.sidebarVisible = true | |||
| 86 | + | bw.activeSidebarKind = nativeSidebarFavorites | |||
| 87 | + | } | |||
| 88 | + | ||||
| 89 | + | func (bw *browserWindow) hideFavoritesSidebar() { | |||
| 90 | + | if bw == nil || bw.favoritesSidebar == nil || bw.mainWindow == nil { | |||
| 91 | + | return | |||
| 92 | + | } | |||
| 93 | + | bw.favoritesSidebar.Hide() | |||
| 94 | + | bw.mainWindow.SetSidebarVisible(false) | |||
| 95 | + | bw.sidebarVisible = false | |||
| 96 | + | if bw.activeSidebarKind == nativeSidebarFavorites { | |||
| 97 | + | bw.activeSidebarKind = nativeSidebarNone | |||
| 98 | + | } | |||
| 99 | + | } | |||
| 100 | + | ||||
| 101 | + | func (a *App) toggleFavoritesSidebarAction(ctx context.Context) error { | |||
| 102 | + | bw := a.lastFocusedBrowserWindow() | |||
| 103 | + | if bw == nil { | |||
| 104 | + | return fmt.Errorf("favorites sidebar unavailable: no focused browser window") | |||
| 105 | + | } | |||
| 106 | + | if bw.favoritesSidebar == nil { | |||
| 107 | + | return fmt.Errorf("favorites sidebar unavailable: native sidebar not initialized") | |||
| 108 | + | } | |||
| 109 | + | if bw.sidebarVisible && bw.activeSidebarKind == nativeSidebarFavorites { | |||
| 110 | + | a.hideAndRestoreFocusForBrowserWindow(bw) | |||
| 111 | + | return nil | |||
| 112 | + | } | |||
| 113 | + | bw.toggleFavoritesSidebar() | |||
| 114 | + | return nil | |||
| 115 | + | } | |||
| 116 | + | ||||
| 117 | + | func (a *App) toggleCurrentPageFavoriteAction(ctx context.Context) error { | |||
| 118 | + | if a == nil || a.deps == nil || a.deps.FavoritesUC == nil { | |||
| 119 | + | return fmt.Errorf("favorites unavailable: usecase not configured") | |||
| 120 | + | } | |||
| 121 | + | _, wv := a.activeWebViewForBrowserWindow(a.lastFocusedBrowserWindow()) | |||
| 122 | + | if wv == nil { | |||
| 123 | + | return fmt.Errorf("favorites unavailable: no active webview") | |||
| 124 | + | } | |||
| 125 | + | uri := strings.TrimSpace(wv.URI()) | |||
| 126 | + | if uri == "" { | |||
| 127 | + | return fmt.Errorf("favorites unavailable: active page has no URI") | |||
| 128 | + | } | |||
| 129 | + | _, err := a.deps.FavoritesUC.Toggle(ctx, uri, wv.Title()) | |||
| 130 | + | if err != nil { | |||
| 131 | + | return err | |||
| 132 | + | } | |||
| 133 | + | return nil | |||
| 134 | + | } | |||
A internal/ui/browser_window_favorites_sidebar_test.go +208 -0
| @@ -0,0 +1,208 @@ | |||
| 1 | + | package ui | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "errors" | |
| 6 | + | "testing" | |
| 7 | + | ||
| 8 | + | "github.com/bnema/dumber/internal/application/usecase" | |
| 9 | + | "github.com/bnema/dumber/internal/domain/entity" | |
| 10 | + | "github.com/bnema/dumber/internal/ui/component" | |
| 11 | + | contentcoord "github.com/bnema/dumber/internal/ui/coordinator/content" | |
| 12 | + | "github.com/bnema/dumber/internal/ui/window" | |
| 13 | + | "github.com/stretchr/testify/assert" | |
| 14 | + | "github.com/stretchr/testify/require" | |
| 15 | + | ) | |
| 16 | + | ||
| 17 | + | func TestBrowserWindow_FavoritesSidebarToggle_CoexistsWithHistorySidebar(t *testing.T) { | |
| 18 | + | bw := &browserWindow{ | |
| 19 | + | id: "test-window", | |
| 20 | + | mainWindow: &window.MainWindow{}, | |
| 21 | + | historySidebar: &component.HistorySidebar{}, | |
| 22 | + | favoritesSidebar: &component.FavoritesSidebar{}, | |
| 23 | + | } | |
| 24 | + | ||
| 25 | + | bw.showHistorySidebar() | |
| 26 | + | assert.True(t, bw.sidebarVisible) | |
| 27 | + | assert.Equal(t, nativeSidebarHistory, bw.activeSidebarKind) | |
| 28 | + | ||
| 29 | + | bw.toggleFavoritesSidebar() | |
| 30 | + | assert.True(t, bw.sidebarVisible) | |
| 31 | + | assert.Equal(t, nativeSidebarFavorites, bw.activeSidebarKind) | |
| 32 | + | ||
| 33 | + | bw.toggleHistorySidebar() | |
| 34 | + | assert.True(t, bw.sidebarVisible) | |
| 35 | + | assert.Equal(t, nativeSidebarHistory, bw.activeSidebarKind) | |
| 36 | + | ||
| 37 | + | bw.toggleHistorySidebar() | |
| 38 | + | assert.False(t, bw.sidebarVisible) | |
| 39 | + | assert.Equal(t, nativeSidebarNone, bw.activeSidebarKind) | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | func TestApp_ToggleFavoritesSidebarActionHidesActiveFavoritesSidebar(t *testing.T) { | |
| 43 | + | bw := &browserWindow{ | |
| 44 | + | id: "w", | |
| 45 | + | mainWindow: &window.MainWindow{}, | |
| 46 | + | favoritesSidebar: &component.FavoritesSidebar{}, | |
| 47 | + | sidebarVisible: true, | |
| 48 | + | activeSidebarKind: nativeSidebarFavorites, | |
| 49 | + | } | |
| 50 | + | app := &App{browserWindows: map[string]*browserWindow{bw.id: bw}, lastFocusedWindowID: bw.id} | |
| 51 | + | ||
| 52 | + | require.NoError(t, app.toggleFavoritesSidebarAction(context.Background())) | |
| 53 | + | ||
| 54 | + | assert.False(t, bw.sidebarVisible) | |
| 55 | + | assert.Equal(t, nativeSidebarNone, bw.activeSidebarKind) | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | func TestFavoritesSidebarConfigOnCloseHidesFavoritesSidebar(t *testing.T) { | |
| 59 | + | bw := &browserWindow{ | |
| 60 | + | id: "w", | |
| 61 | + | mainWindow: &window.MainWindow{}, | |
| 62 | + | favoritesSidebar: &component.FavoritesSidebar{}, | |
| 63 | + | sidebarVisible: true, | |
| 64 | + | activeSidebarKind: nativeSidebarFavorites, | |
| 65 | + | } | |
| 66 | + | app := &App{browserWindows: map[string]*browserWindow{bw.id: bw}, lastFocusedWindowID: bw.id} | |
| 67 | + | ||
| 68 | + | cfg := app.buildFavoritesSidebarConfig(bw) | |
| 69 | + | require.NotNil(t, cfg.OnClose) | |
| 70 | + | cfg.OnClose() | |
| 71 | + | ||
| 72 | + | assert.False(t, bw.sidebarVisible) | |
| 73 | + | assert.Equal(t, nativeSidebarNone, bw.activeSidebarKind) | |
| 74 | + | } | |
| 75 | + | ||
| 76 | + | func TestApp_ToggleFavoritesSidebarActionErrorsWhenUnavailable(t *testing.T) { | |
| 77 | + | app := &App{browserWindows: map[string]*browserWindow{"w": {id: "w"}}, lastFocusedWindowID: "w"} | |
| 78 | + | err := app.toggleFavoritesSidebarAction(context.Background()) | |
| 79 | + | require.Error(t, err) | |
| 80 | + | assert.ErrorContains(t, err, "favorites sidebar unavailable") | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | func TestApp_ToggleCurrentPageFavoriteTogglesActiveWebViewURI(t *testing.T) { | |
| 84 | + | ctx := context.Background() | |
| 85 | + | tab := entity.NewTab(entity.TabID("tab-1"), entity.WorkspaceID("ws-1"), entity.NewPane(entity.PaneID("pane-1"))) | |
| 86 | + | tabs := entity.NewTabList() | |
| 87 | + | tabs.Add(tab) | |
| 88 | + | tabs.SetActive(tab.ID) | |
| 89 | + | bw := &browserWindow{id: "w", tabs: tabs} | |
| 90 | + | ||
| 91 | + | contentCoord := contentcoord.NewCoordinator(ctx, nil, nil, nil, nil, nil, nil, nil) | |
| 92 | + | wv := &recordingWebView{id: 1, loadURILastURI: "https://example.com/page"} | |
| 93 | + | contentCoord.RegisterPopupWebView(entity.PaneID("pane-1"), wv) | |
| 94 | + | ||
| 95 | + | favRepo := newMemoryFavoriteRepo() | |
| 96 | + | app := &App{ | |
| 97 | + | deps: &Dependencies{FavoritesUC: usecase.NewManageFavoritesUseCase(favRepo, &memoryTagRepo{})}, | |
| 98 | + | browserWindows: map[string]*browserWindow{bw.id: bw}, | |
| 99 | + | lastFocusedWindowID: bw.id, | |
| 100 | + | contentCoord: contentCoord, | |
| 101 | + | workspaceViews: map[entity.TabID]*component.WorkspaceView{tab.ID: {}}, | |
| 102 | + | } | |
| 103 | + | ||
| 104 | + | require.NoError(t, app.toggleCurrentPageFavoriteAction(ctx)) | |
| 105 | + | assert.Len(t, favRepo.byURL, 1) | |
| 106 | + | _, ok := favRepo.byURL["https://example.com/page"] | |
| 107 | + | assert.True(t, ok) | |
| 108 | + | ||
| 109 | + | require.NoError(t, app.toggleCurrentPageFavoriteAction(ctx)) | |
| 110 | + | assert.Empty(t, favRepo.byURL) | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | func TestApp_ToggleCurrentPageFavoriteErrorCases(t *testing.T) { | |
| 114 | + | ctx := context.Background() | |
| 115 | + | assert.ErrorContains(t, (&App{}).toggleCurrentPageFavoriteAction(ctx), "usecase not configured") | |
| 116 | + | ||
| 117 | + | app := &App{deps: &Dependencies{FavoritesUC: usecase.NewManageFavoritesUseCase(newMemoryFavoriteRepo(), &memoryTagRepo{})}} | |
| 118 | + | assert.ErrorContains(t, app.toggleCurrentPageFavoriteAction(ctx), "no active webview") | |
| 119 | + | ||
| 120 | + | tab := entity.NewTab(entity.TabID("tab-1"), entity.WorkspaceID("ws-1"), entity.NewPane(entity.PaneID("pane-1"))) | |
| 121 | + | tabs := entity.NewTabList() | |
| 122 | + | tabs.Add(tab) | |
| 123 | + | tabs.SetActive(tab.ID) | |
| 124 | + | bw := &browserWindow{id: "w", tabs: tabs} | |
| 125 | + | contentCoord := contentcoord.NewCoordinator(ctx, nil, nil, nil, nil, nil, nil, nil) | |
| 126 | + | contentCoord.RegisterPopupWebView(entity.PaneID("pane-1"), &recordingWebView{id: 2}) | |
| 127 | + | app = &App{ | |
| 128 | + | deps: &Dependencies{FavoritesUC: usecase.NewManageFavoritesUseCase(newMemoryFavoriteRepo(), &memoryTagRepo{})}, | |
| 129 | + | browserWindows: map[string]*browserWindow{bw.id: bw}, | |
| 130 | + | lastFocusedWindowID: bw.id, | |
| 131 | + | contentCoord: contentCoord, | |
| 132 | + | workspaceViews: map[entity.TabID]*component.WorkspaceView{tab.ID: {}}, | |
| 133 | + | } | |
| 134 | + | assert.ErrorContains(t, app.toggleCurrentPageFavoriteAction(ctx), "active page has no URI") | |
| 135 | + | } | |
| 136 | + | ||
| 137 | + | type memoryFavoriteRepo struct { | |
| 138 | + | nextID entity.FavoriteID | |
| 139 | + | byID map[entity.FavoriteID]*entity.Favorite | |
| 140 | + | byURL map[string]*entity.Favorite | |
| 141 | + | err error | |
| 142 | + | } | |
| 143 | + | ||
| 144 | + | func newMemoryFavoriteRepo() *memoryFavoriteRepo { | |
| 145 | + | return &memoryFavoriteRepo{nextID: 1, byID: map[entity.FavoriteID]*entity.Favorite{}, byURL: map[string]*entity.Favorite{}} | |
| 146 | + | } | |
| 147 | + | ||
| 148 | + | func (r *memoryFavoriteRepo) Save(_ context.Context, fav *entity.Favorite) error { | |
| 149 | + | if r.err != nil { | |
| 150 | + | return r.err | |
| 151 | + | } | |
| 152 | + | if fav.ID == 0 { | |
| 153 | + | fav.ID = r.nextID | |
| 154 | + | r.nextID++ | |
| 155 | + | } | |
| 156 | + | cp := *fav | |
| 157 | + | r.byID[fav.ID] = &cp | |
| 158 | + | r.byURL[fav.URL] = &cp | |
| 159 | + | return nil | |
| 160 | + | } | |
| 161 | + | func (r *memoryFavoriteRepo) FindByID(context.Context, entity.FavoriteID) (*entity.Favorite, error) { | |
| 162 | + | return nil, r.err | |
| 163 | + | } | |
| 164 | + | func (r *memoryFavoriteRepo) FindByURL(_ context.Context, url string) (*entity.Favorite, error) { | |
| 165 | + | return r.byURL[url], r.err | |
| 166 | + | } | |
| 167 | + | func (r *memoryFavoriteRepo) GetAll(context.Context) ([]*entity.Favorite, error) { return nil, r.err } | |
| 168 | + | func (r *memoryFavoriteRepo) GetByTag(context.Context, entity.TagID) ([]*entity.Favorite, error) { | |
| 169 | + | return nil, r.err | |
| 170 | + | } | |
| 171 | + | func (r *memoryFavoriteRepo) GetByShortcut(context.Context, int) (*entity.Favorite, error) { | |
| 172 | + | return nil, r.err | |
| 173 | + | } | |
| 174 | + | func (r *memoryFavoriteRepo) UpdatePosition(context.Context, entity.FavoriteID, int) error { | |
| 175 | + | return r.err | |
| 176 | + | } | |
| 177 | + | func (r *memoryFavoriteRepo) SetShortcut(context.Context, entity.FavoriteID, *int) error { | |
| 178 | + | return r.err | |
| 179 | + | } | |
| 180 | + | func (r *memoryFavoriteRepo) Delete(_ context.Context, id entity.FavoriteID) error { | |
| 181 | + | if r.err != nil { | |
| 182 | + | return r.err | |
| 183 | + | } | |
| 184 | + | fav := r.byID[id] | |
| 185 | + | if fav == nil { | |
| 186 | + | return errors.New("favorite not found") | |
| 187 | + | } | |
| 188 | + | delete(r.byID, id) | |
| 189 | + | delete(r.byURL, fav.URL) | |
| 190 | + | return nil | |
| 191 | + | } | |
| 192 | + | ||
| 193 | + | type memoryTagRepo struct{} | |
| 194 | + | ||
| 195 | + | func (*memoryTagRepo) Save(context.Context, *entity.Tag) error { return nil } | |
| 196 | + | func (*memoryTagRepo) FindByID(context.Context, entity.TagID) (*entity.Tag, error) { return nil, nil } | |
| 197 | + | func (*memoryTagRepo) FindByName(context.Context, string) (*entity.Tag, error) { return nil, nil } | |
| 198 | + | func (*memoryTagRepo) GetAll(context.Context) ([]*entity.Tag, error) { return nil, nil } | |
| 199 | + | func (*memoryTagRepo) AssignToFavorite(context.Context, entity.TagID, entity.FavoriteID) error { | |
| 200 | + | return nil | |
| 201 | + | } | |
| 202 | + | func (*memoryTagRepo) RemoveFromFavorite(context.Context, entity.TagID, entity.FavoriteID) error { | |
| 203 | + | return nil | |
| 204 | + | } | |
| 205 | + | func (*memoryTagRepo) GetForFavorite(context.Context, entity.FavoriteID) ([]*entity.Tag, error) { | |
| 206 | + | return nil, nil | |
| 207 | + | } | |
| 208 | + | func (*memoryTagRepo) Delete(context.Context, entity.TagID) error { return nil } | |
| @@ -0,0 +1,208 @@ | |||||
| 1 | + | package ui | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "errors" | |||
| 6 | + | "testing" | |||
| 7 | + | ||||
| 8 | + | "github.com/bnema/dumber/internal/application/usecase" | |||
| 9 | + | "github.com/bnema/dumber/internal/domain/entity" | |||
| 10 | + | "github.com/bnema/dumber/internal/ui/component" | |||
| 11 | + | contentcoord "github.com/bnema/dumber/internal/ui/coordinator/content" | |||
| 12 | + | "github.com/bnema/dumber/internal/ui/window" | |||
| 13 | + | "github.com/stretchr/testify/assert" | |||
| 14 | + | "github.com/stretchr/testify/require" | |||
| 15 | + | ) | |||
| 16 | + | ||||
| 17 | + | func TestBrowserWindow_FavoritesSidebarToggle_CoexistsWithHistorySidebar(t *testing.T) { | |||
| 18 | + | bw := &browserWindow{ | |||
| 19 | + | id: "test-window", | |||
| 20 | + | mainWindow: &window.MainWindow{}, | |||
| 21 | + | historySidebar: &component.HistorySidebar{}, | |||
| 22 | + | favoritesSidebar: &component.FavoritesSidebar{}, | |||
| 23 | + | } | |||
| 24 | + | ||||
| 25 | + | bw.showHistorySidebar() | |||
| 26 | + | assert.True(t, bw.sidebarVisible) | |||
| 27 | + | assert.Equal(t, nativeSidebarHistory, bw.activeSidebarKind) | |||
| 28 | + | ||||
| 29 | + | bw.toggleFavoritesSidebar() | |||
| 30 | + | assert.True(t, bw.sidebarVisible) | |||
| 31 | + | assert.Equal(t, nativeSidebarFavorites, bw.activeSidebarKind) | |||
| 32 | + | ||||
| 33 | + | bw.toggleHistorySidebar() | |||
| 34 | + | assert.True(t, bw.sidebarVisible) | |||
| 35 | + | assert.Equal(t, nativeSidebarHistory, bw.activeSidebarKind) | |||
| 36 | + | ||||
| 37 | + | bw.toggleHistorySidebar() | |||
| 38 | + | assert.False(t, bw.sidebarVisible) | |||
| 39 | + | assert.Equal(t, nativeSidebarNone, bw.activeSidebarKind) | |||
| 40 | + | } | |||
| 41 | + | ||||
| 42 | + | func TestApp_ToggleFavoritesSidebarActionHidesActiveFavoritesSidebar(t *testing.T) { | |||
| 43 | + | bw := &browserWindow{ | |||
| 44 | + | id: "w", | |||
| 45 | + | mainWindow: &window.MainWindow{}, | |||
| 46 | + | favoritesSidebar: &component.FavoritesSidebar{}, | |||
| 47 | + | sidebarVisible: true, | |||
| 48 | + | activeSidebarKind: nativeSidebarFavorites, | |||
| 49 | + | } | |||
| 50 | + | app := &App{browserWindows: map[string]*browserWindow{bw.id: bw}, lastFocusedWindowID: bw.id} | |||
| 51 | + | ||||
| 52 | + | require.NoError(t, app.toggleFavoritesSidebarAction(context.Background())) | |||
| 53 | + | ||||
| 54 | + | assert.False(t, bw.sidebarVisible) | |||
| 55 | + | assert.Equal(t, nativeSidebarNone, bw.activeSidebarKind) | |||
| 56 | + | } | |||
| 57 | + | ||||
| 58 | + | func TestFavoritesSidebarConfigOnCloseHidesFavoritesSidebar(t *testing.T) { | |||
| 59 | + | bw := &browserWindow{ | |||
| 60 | + | id: "w", | |||
| 61 | + | mainWindow: &window.MainWindow{}, | |||
| 62 | + | favoritesSidebar: &component.FavoritesSidebar{}, | |||
| 63 | + | sidebarVisible: true, | |||
| 64 | + | activeSidebarKind: nativeSidebarFavorites, | |||
| 65 | + | } | |||
| 66 | + | app := &App{browserWindows: map[string]*browserWindow{bw.id: bw}, lastFocusedWindowID: bw.id} | |||
| 67 | + | ||||
| 68 | + | cfg := app.buildFavoritesSidebarConfig(bw) | |||
| 69 | + | require.NotNil(t, cfg.OnClose) | |||
| 70 | + | cfg.OnClose() | |||
| 71 | + | ||||
| 72 | + | assert.False(t, bw.sidebarVisible) | |||
| 73 | + | assert.Equal(t, nativeSidebarNone, bw.activeSidebarKind) | |||
| 74 | + | } | |||
| 75 | + | ||||
| 76 | + | func TestApp_ToggleFavoritesSidebarActionErrorsWhenUnavailable(t *testing.T) { | |||
| 77 | + | app := &App{browserWindows: map[string]*browserWindow{"w": {id: "w"}}, lastFocusedWindowID: "w"} | |||
| 78 | + | err := app.toggleFavoritesSidebarAction(context.Background()) | |||
| 79 | + | require.Error(t, err) | |||
| 80 | + | assert.ErrorContains(t, err, "favorites sidebar unavailable") | |||
| 81 | + | } | |||
| 82 | + | ||||
| 83 | + | func TestApp_ToggleCurrentPageFavoriteTogglesActiveWebViewURI(t *testing.T) { | |||
| 84 | + | ctx := context.Background() | |||
| 85 | + | tab := entity.NewTab(entity.TabID("tab-1"), entity.WorkspaceID("ws-1"), entity.NewPane(entity.PaneID("pane-1"))) | |||
| 86 | + | tabs := entity.NewTabList() | |||
| 87 | + | tabs.Add(tab) | |||
| 88 | + | tabs.SetActive(tab.ID) | |||
| 89 | + | bw := &browserWindow{id: "w", tabs: tabs} | |||
| 90 | + | ||||
| 91 | + | contentCoord := contentcoord.NewCoordinator(ctx, nil, nil, nil, nil, nil, nil, nil) | |||
| 92 | + | wv := &recordingWebView{id: 1, loadURILastURI: "https://example.com/page"} | |||
| 93 | + | contentCoord.RegisterPopupWebView(entity.PaneID("pane-1"), wv) | |||
| 94 | + | ||||
| 95 | + | favRepo := newMemoryFavoriteRepo() | |||
| 96 | + | app := &App{ | |||
| 97 | + | deps: &Dependencies{FavoritesUC: usecase.NewManageFavoritesUseCase(favRepo, &memoryTagRepo{})}, | |||
| 98 | + | browserWindows: map[string]*browserWindow{bw.id: bw}, | |||
| 99 | + | lastFocusedWindowID: bw.id, | |||
| 100 | + | contentCoord: contentCoord, | |||
| 101 | + | workspaceViews: map[entity.TabID]*component.WorkspaceView{tab.ID: {}}, | |||
| 102 | + | } | |||
| 103 | + | ||||
| 104 | + | require.NoError(t, app.toggleCurrentPageFavoriteAction(ctx)) | |||
| 105 | + | assert.Len(t, favRepo.byURL, 1) | |||
| 106 | + | _, ok := favRepo.byURL["https://example.com/page"] | |||
| 107 | + | assert.True(t, ok) | |||
| 108 | + | ||||
| 109 | + | require.NoError(t, app.toggleCurrentPageFavoriteAction(ctx)) | |||
| 110 | + | assert.Empty(t, favRepo.byURL) | |||
| 111 | + | } | |||
| 112 | + | ||||
| 113 | + | func TestApp_ToggleCurrentPageFavoriteErrorCases(t *testing.T) { | |||
| 114 | + | ctx := context.Background() | |||
| 115 | + | assert.ErrorContains(t, (&App{}).toggleCurrentPageFavoriteAction(ctx), "usecase not configured") | |||
| 116 | + | ||||
| 117 | + | app := &App{deps: &Dependencies{FavoritesUC: usecase.NewManageFavoritesUseCase(newMemoryFavoriteRepo(), &memoryTagRepo{})}} | |||
| 118 | + | assert.ErrorContains(t, app.toggleCurrentPageFavoriteAction(ctx), "no active webview") | |||
| 119 | + | ||||
| 120 | + | tab := entity.NewTab(entity.TabID("tab-1"), entity.WorkspaceID("ws-1"), entity.NewPane(entity.PaneID("pane-1"))) | |||
| 121 | + | tabs := entity.NewTabList() | |||
| 122 | + | tabs.Add(tab) | |||
| 123 | + | tabs.SetActive(tab.ID) | |||
| 124 | + | bw := &browserWindow{id: "w", tabs: tabs} | |||
| 125 | + | contentCoord := contentcoord.NewCoordinator(ctx, nil, nil, nil, nil, nil, nil, nil) | |||
| 126 | + | contentCoord.RegisterPopupWebView(entity.PaneID("pane-1"), &recordingWebView{id: 2}) | |||
| 127 | + | app = &App{ | |||
| 128 | + | deps: &Dependencies{FavoritesUC: usecase.NewManageFavoritesUseCase(newMemoryFavoriteRepo(), &memoryTagRepo{})}, | |||
| 129 | + | browserWindows: map[string]*browserWindow{bw.id: bw}, | |||
| 130 | + | lastFocusedWindowID: bw.id, | |||
| 131 | + | contentCoord: contentCoord, | |||
| 132 | + | workspaceViews: map[entity.TabID]*component.WorkspaceView{tab.ID: {}}, | |||
| 133 | + | } | |||
| 134 | + | assert.ErrorContains(t, app.toggleCurrentPageFavoriteAction(ctx), "active page has no URI") | |||
| 135 | + | } | |||
| 136 | + | ||||
| 137 | + | type memoryFavoriteRepo struct { | |||
| 138 | + | nextID entity.FavoriteID | |||
| 139 | + | byID map[entity.FavoriteID]*entity.Favorite | |||
| 140 | + | byURL map[string]*entity.Favorite | |||
| 141 | + | err error | |||
| 142 | + | } | |||
| 143 | + | ||||
| 144 | + | func newMemoryFavoriteRepo() *memoryFavoriteRepo { | |||
| 145 | + | return &memoryFavoriteRepo{nextID: 1, byID: map[entity.FavoriteID]*entity.Favorite{}, byURL: map[string]*entity.Favorite{}} | |||
| 146 | + | } | |||
| 147 | + | ||||
| 148 | + | func (r *memoryFavoriteRepo) Save(_ context.Context, fav *entity.Favorite) error { | |||
| 149 | + | if r.err != nil { | |||
| 150 | + | return r.err | |||
| 151 | + | } | |||
| 152 | + | if fav.ID == 0 { | |||
| 153 | + | fav.ID = r.nextID | |||
| 154 | + | r.nextID++ | |||
| 155 | + | } | |||
| 156 | + | cp := *fav | |||
| 157 | + | r.byID[fav.ID] = &cp | |||
| 158 | + | r.byURL[fav.URL] = &cp | |||
| 159 | + | return nil | |||
| 160 | + | } | |||
| 161 | + | func (r *memoryFavoriteRepo) FindByID(context.Context, entity.FavoriteID) (*entity.Favorite, error) { | |||
| 162 | + | return nil, r.err | |||
| 163 | + | } | |||
| 164 | + | func (r *memoryFavoriteRepo) FindByURL(_ context.Context, url string) (*entity.Favorite, error) { | |||
| 165 | + | return r.byURL[url], r.err | |||
| 166 | + | } | |||
| 167 | + | func (r *memoryFavoriteRepo) GetAll(context.Context) ([]*entity.Favorite, error) { return nil, r.err } | |||
| 168 | + | func (r *memoryFavoriteRepo) GetByTag(context.Context, entity.TagID) ([]*entity.Favorite, error) { | |||
| 169 | + | return nil, r.err | |||
| 170 | + | } | |||
| 171 | + | func (r *memoryFavoriteRepo) GetByShortcut(context.Context, int) (*entity.Favorite, error) { | |||
| 172 | + | return nil, r.err | |||
| 173 | + | } | |||
| 174 | + | func (r *memoryFavoriteRepo) UpdatePosition(context.Context, entity.FavoriteID, int) error { | |||
| 175 | + | return r.err | |||
| 176 | + | } | |||
| 177 | + | func (r *memoryFavoriteRepo) SetShortcut(context.Context, entity.FavoriteID, *int) error { | |||
| 178 | + | return r.err | |||
| 179 | + | } | |||
| 180 | + | func (r *memoryFavoriteRepo) Delete(_ context.Context, id entity.FavoriteID) error { | |||
| 181 | + | if r.err != nil { | |||
| 182 | + | return r.err | |||
| 183 | + | } | |||
| 184 | + | fav := r.byID[id] | |||
| 185 | + | if fav == nil { | |||
| 186 | + | return errors.New("favorite not found") | |||
| 187 | + | } | |||
| 188 | + | delete(r.byID, id) | |||
| 189 | + | delete(r.byURL, fav.URL) | |||
| 190 | + | return nil | |||
| 191 | + | } | |||
| 192 | + | ||||
| 193 | + | type memoryTagRepo struct{} | |||
| 194 | + | ||||
| 195 | + | func (*memoryTagRepo) Save(context.Context, *entity.Tag) error { return nil } | |||
| 196 | + | func (*memoryTagRepo) FindByID(context.Context, entity.TagID) (*entity.Tag, error) { return nil, nil } | |||
| 197 | + | func (*memoryTagRepo) FindByName(context.Context, string) (*entity.Tag, error) { return nil, nil } | |||
| 198 | + | func (*memoryTagRepo) GetAll(context.Context) ([]*entity.Tag, error) { return nil, nil } | |||
| 199 | + | func (*memoryTagRepo) AssignToFavorite(context.Context, entity.TagID, entity.FavoriteID) error { | |||
| 200 | + | return nil | |||
| 201 | + | } | |||
| 202 | + | func (*memoryTagRepo) RemoveFromFavorite(context.Context, entity.TagID, entity.FavoriteID) error { | |||
| 203 | + | return nil | |||
| 204 | + | } | |||
| 205 | + | func (*memoryTagRepo) GetForFavorite(context.Context, entity.FavoriteID) ([]*entity.Tag, error) { | |||
| 206 | + | return nil, nil | |||
| 207 | + | } | |||
| 208 | + | func (*memoryTagRepo) Delete(context.Context, entity.TagID) error { return nil } | |||
M internal/ui/browser_window_history_sidebar.go +30 -2
| @@ -34,6 +34,7 @@ func (bw *browserWindow) initHistorySidebar(ctx context.Context, a *App) { | |||
| 34 | 34 | bw.historySidebar.Hide() | |
| 35 | 35 | bw.mainWindow.SetSidebarVisible(false) | |
| 36 | 36 | bw.sidebarVisible = false | |
| 37 | + | bw.activeSidebarKind = nativeSidebarNone | |
| 37 | 38 | ||
| 38 | 39 | // Apply sidebar width from config, falling back to the default 320px. | |
| 39 | 40 | // The width is clamped to [280, 380] by SetSidebarWidth internally. | |
| @@ -84,7 +85,7 @@ func (bw *browserWindow) toggleHistorySidebar() { | |||
| 84 | 85 | return | |
| 85 | 86 | } | |
| 86 | 87 | ||
| 87 | - | if bw.sidebarVisible { | |
| 88 | + | if bw.sidebarVisible && bw.activeSidebarKind == nativeSidebarHistory { | |
| 88 | 89 | bw.hideHistorySidebar() | |
| 89 | 90 | } else { | |
| 90 | 91 | bw.showHistorySidebar() | |
| @@ -97,9 +98,11 @@ func (bw *browserWindow) showHistorySidebar() { | |||
| 97 | 98 | if bw == nil || bw.historySidebar == nil || bw.mainWindow == nil { | |
| 98 | 99 | return | |
| 99 | 100 | } | |
| 101 | + | bw.mountNativeSidebar(nativeSidebarHistory) | |
| 100 | 102 | bw.historySidebar.Show() | |
| 101 | 103 | bw.mainWindow.SetSidebarVisible(true) | |
| 102 | 104 | bw.sidebarVisible = true | |
| 105 | + | bw.activeSidebarKind = nativeSidebarHistory | |
| 103 | 106 | } | |
| 104 | 107 | ||
| 105 | 108 | // hideHistorySidebar hides the sidebar. Callers should also restore focus | |
| @@ -111,6 +114,31 @@ func (bw *browserWindow) hideHistorySidebar() { | |||
| 111 | 114 | bw.historySidebar.Hide() | |
| 112 | 115 | bw.mainWindow.SetSidebarVisible(false) | |
| 113 | 116 | bw.sidebarVisible = false | |
| 117 | + | if bw.activeSidebarKind == nativeSidebarHistory { | |
| 118 | + | bw.activeSidebarKind = nativeSidebarNone | |
| 119 | + | } | |
| 120 | + | } | |
| 121 | + | ||
| 122 | + | func (bw *browserWindow) mountNativeSidebar(kind nativeSidebarKind) { | |
| 123 | + | if bw == nil || bw.mainWindow == nil || bw.activeSidebarKind == kind { | |
| 124 | + | return | |
| 125 | + | } | |
| 126 | + | if bw.historySidebar != nil { | |
| 127 | + | bw.historySidebar.Hide() | |
| 128 | + | } | |
| 129 | + | if bw.favoritesSidebar != nil { | |
| 130 | + | bw.favoritesSidebar.Hide() | |
| 131 | + | } | |
| 132 | + | switch kind { | |
| 133 | + | case nativeSidebarHistory: | |
| 134 | + | if bw.historySidebar != nil { | |
| 135 | + | bw.mainWindow.SetSidebarWidget(bw.historySidebar.Widget()) | |
| 136 | + | } | |
| 137 | + | case nativeSidebarFavorites: | |
| 138 | + | if bw.favoritesSidebar != nil { | |
| 139 | + | bw.mainWindow.SetSidebarWidget(bw.favoritesSidebar.Widget()) | |
| 140 | + | } | |
| 141 | + | } | |
| 114 | 142 | } | |
| 115 | 143 | ||
| 116 | 144 | // historySidebarWidthConfig extracts the config-backed sidebar width and | |
| @@ -143,7 +171,7 @@ func (a *App) toggleHistorySidebarAction(ctx context.Context) error { | |||
| 143 | 171 | if bw.historySidebar == nil { | |
| 144 | 172 | return fmt.Errorf("history sidebar unavailable: native sidebar not initialized") | |
| 145 | 173 | } | |
| 146 | - | if bw.sidebarVisible { | |
| 174 | + | if bw.sidebarVisible && bw.activeSidebarKind == nativeSidebarHistory { | |
| 147 | 175 | a.hideAndRestoreFocusForBrowserWindow(bw) | |
| 148 | 176 | return nil | |
| 149 | 177 | } | |
| @@ -34,6 +34,7 @@ func (bw *browserWindow) initHistorySidebar(ctx context.Context, a *App) { | |||||
| 34 | bw.historySidebar.Hide() | 34 | bw.historySidebar.Hide() | ||
| 35 | bw.mainWindow.SetSidebarVisible(false) | 35 | bw.mainWindow.SetSidebarVisible(false) | ||
| 36 | bw.sidebarVisible = false | 36 | bw.sidebarVisible = false | ||
| 37 | + | bw.activeSidebarKind = nativeSidebarNone | |||
| 37 | 38 | ||||
| 38 | // Apply sidebar width from config, falling back to the default 320px. | 39 | // Apply sidebar width from config, falling back to the default 320px. | ||
| 39 | // The width is clamped to [280, 380] by SetSidebarWidth internally. | 40 | // The width is clamped to [280, 380] by SetSidebarWidth internally. | ||
| @@ -84,7 +85,7 @@ func (bw *browserWindow) toggleHistorySidebar() { | |||||
| 84 | return | 85 | return | ||
| 85 | } | 86 | } | ||
| 86 | 87 | ||||
| 87 | - | if bw.sidebarVisible { | 88 | + | if bw.sidebarVisible && bw.activeSidebarKind == nativeSidebarHistory { |
| 88 | bw.hideHistorySidebar() | 89 | bw.hideHistorySidebar() | ||
| 89 | } else { | 90 | } else { | ||
| 90 | bw.showHistorySidebar() | 91 | bw.showHistorySidebar() | ||
| @@ -97,9 +98,11 @@ func (bw *browserWindow) showHistorySidebar() { | |||||
| 97 | if bw == nil || bw.historySidebar == nil || bw.mainWindow == nil { | 98 | if bw == nil || bw.historySidebar == nil || bw.mainWindow == nil { | ||
| 98 | return | 99 | return | ||
| 99 | } | 100 | } | ||
| 101 | + | bw.mountNativeSidebar(nativeSidebarHistory) | |||
| 100 | bw.historySidebar.Show() | 102 | bw.historySidebar.Show() | ||
| 101 | bw.mainWindow.SetSidebarVisible(true) | 103 | bw.mainWindow.SetSidebarVisible(true) | ||
| 102 | bw.sidebarVisible = true | 104 | bw.sidebarVisible = true | ||
| 105 | + | bw.activeSidebarKind = nativeSidebarHistory | |||
| 103 | } | 106 | } | ||
| 104 | 107 | ||||
| 105 | // hideHistorySidebar hides the sidebar. Callers should also restore focus | 108 | // hideHistorySidebar hides the sidebar. Callers should also restore focus | ||
| @@ -111,6 +114,31 @@ func (bw *browserWindow) hideHistorySidebar() { | |||||
| 111 | bw.historySidebar.Hide() | 114 | bw.historySidebar.Hide() | ||
| 112 | bw.mainWindow.SetSidebarVisible(false) | 115 | bw.mainWindow.SetSidebarVisible(false) | ||
| 113 | bw.sidebarVisible = false | 116 | bw.sidebarVisible = false | ||
| 117 | + | if bw.activeSidebarKind == nativeSidebarHistory { | |||
| 118 | + | bw.activeSidebarKind = nativeSidebarNone | |||
| 119 | + | } | |||
| 120 | + | } | |||
| 121 | + | ||||
| 122 | + | func (bw *browserWindow) mountNativeSidebar(kind nativeSidebarKind) { | |||
| 123 | + | if bw == nil || bw.mainWindow == nil || bw.activeSidebarKind == kind { | |||
| 124 | + | return | |||
| 125 | + | } | |||
| 126 | + | if bw.historySidebar != nil { | |||
| 127 | + | bw.historySidebar.Hide() | |||
| 128 | + | } | |||
| 129 | + | if bw.favoritesSidebar != nil { | |||
| 130 | + | bw.favoritesSidebar.Hide() | |||
| 131 | + | } | |||
| 132 | + | switch kind { | |||
| 133 | + | case nativeSidebarHistory: | |||
| 134 | + | if bw.historySidebar != nil { | |||
| 135 | + | bw.mainWindow.SetSidebarWidget(bw.historySidebar.Widget()) | |||
| 136 | + | } | |||
| 137 | + | case nativeSidebarFavorites: | |||
| 138 | + | if bw.favoritesSidebar != nil { | |||
| 139 | + | bw.mainWindow.SetSidebarWidget(bw.favoritesSidebar.Widget()) | |||
| 140 | + | } | |||
| 141 | + | } | |||
| 114 | } | 142 | } | ||
| 115 | 143 | ||||
| 116 | // historySidebarWidthConfig extracts the config-backed sidebar width and | 144 | // historySidebarWidthConfig extracts the config-backed sidebar width and | ||
| @@ -143,7 +171,7 @@ func (a *App) toggleHistorySidebarAction(ctx context.Context) error { | |||||
| 143 | if bw.historySidebar == nil { | 171 | if bw.historySidebar == nil { | ||
| 144 | return fmt.Errorf("history sidebar unavailable: native sidebar not initialized") | 172 | return fmt.Errorf("history sidebar unavailable: native sidebar not initialized") | ||
| 145 | } | 173 | } | ||
| 146 | - | if bw.sidebarVisible { | 174 | + | if bw.sidebarVisible && bw.activeSidebarKind == nativeSidebarHistory { |
| 147 | a.hideAndRestoreFocusForBrowserWindow(bw) | 175 | a.hideAndRestoreFocusForBrowserWindow(bw) | ||
| 148 | return nil | 176 | return nil | ||
| 149 | } | 177 | } | ||
M internal/ui/dispatcher/keyboard.go +39 -20
| @@ -36,25 +36,27 @@ type KeyboardActions struct { | |||
| 36 | 36 | ||
| 37 | 37 | // KeyboardDispatcher routes keyboard actions to appropriate coordinators. | |
| 38 | 38 | type KeyboardDispatcher struct { | |
| 39 | - | actions KeyboardActions | |
| 40 | - | wsCoord *coordinator.WorkspaceCoordinator | |
| 41 | - | navCoord *coordinator.NavigationCoordinator | |
| 42 | - | zoomUC *usecase.ManageZoomUseCase | |
| 43 | - | copyURLUC *usecase.CopyURLUseCase | |
| 44 | - | actionHandlers map[input.Action]func(ctx context.Context) error | |
| 45 | - | onQuit func() | |
| 46 | - | onFindOpen func(ctx context.Context) error | |
| 47 | - | onFindNext func(ctx context.Context) error | |
| 48 | - | onFindPrev func(ctx context.Context) error | |
| 49 | - | onFindClose func(ctx context.Context) error | |
| 50 | - | activePaneID func(ctx context.Context) entity.PaneID | |
| 51 | - | onSessionOpen func(ctx context.Context, paneID entity.PaneID) error | |
| 52 | - | onMovePaneToTab func(ctx context.Context, paneID entity.PaneID) error | |
| 53 | - | onMovePaneToNext func(ctx context.Context, paneID entity.PaneID) error | |
| 54 | - | onEjectPaneToWindow func(ctx context.Context, paneID entity.PaneID) error | |
| 55 | - | onToggleHistorySidebar func(ctx context.Context) error | |
| 56 | - | onToggleFloating func(ctx context.Context) error | |
| 57 | - | onOpenFloating func(ctx context.Context, target input.FloatingProfileTarget) error | |
| 39 | + | actions KeyboardActions | |
| 40 | + | wsCoord *coordinator.WorkspaceCoordinator | |
| 41 | + | navCoord *coordinator.NavigationCoordinator | |
| 42 | + | zoomUC *usecase.ManageZoomUseCase | |
| 43 | + | copyURLUC *usecase.CopyURLUseCase | |
| 44 | + | actionHandlers map[input.Action]func(ctx context.Context) error | |
| 45 | + | onQuit func() | |
| 46 | + | onFindOpen func(ctx context.Context) error | |
| 47 | + | onFindNext func(ctx context.Context) error | |
| 48 | + | onFindPrev func(ctx context.Context) error | |
| 49 | + | onFindClose func(ctx context.Context) error | |
| 50 | + | activePaneID func(ctx context.Context) entity.PaneID | |
| 51 | + | onSessionOpen func(ctx context.Context, paneID entity.PaneID) error | |
| 52 | + | onMovePaneToTab func(ctx context.Context, paneID entity.PaneID) error | |
| 53 | + | onMovePaneToNext func(ctx context.Context, paneID entity.PaneID) error | |
| 54 | + | onEjectPaneToWindow func(ctx context.Context, paneID entity.PaneID) error | |
| 55 | + | onToggleHistorySidebar func(ctx context.Context) error | |
| 56 | + | onToggleFavoritesSidebar func(ctx context.Context) error | |
| 57 | + | onToggleCurrentFavorite func(ctx context.Context) error | |
| 58 | + | onToggleFloating func(ctx context.Context) error | |
| 59 | + | onOpenFloating func(ctx context.Context, target input.FloatingProfileTarget) error | |
| 58 | 60 | } | |
| 59 | 61 | ||
| 60 | 62 | // NewKeyboardDispatcher creates a new KeyboardDispatcher. | |
| @@ -128,6 +130,14 @@ func (d *KeyboardDispatcher) SetOnToggleHistorySidebar(fn func(ctx context.Conte | |||
| 128 | 130 | d.onToggleHistorySidebar = fn | |
| 129 | 131 | } | |
| 130 | 132 | ||
| 133 | + | func (d *KeyboardDispatcher) SetOnToggleFavoritesSidebar(fn func(ctx context.Context) error) { | |
| 134 | + | d.onToggleFavoritesSidebar = fn | |
| 135 | + | } | |
| 136 | + | ||
| 137 | + | func (d *KeyboardDispatcher) SetOnToggleCurrentPageFavorite(fn func(ctx context.Context) error) { | |
| 138 | + | d.onToggleCurrentFavorite = fn | |
| 139 | + | } | |
| 140 | + | ||
| 131 | 141 | func (d *KeyboardDispatcher) SetOnToggleFloatingPane(fn func(ctx context.Context) error) { | |
| 132 | 142 | d.onToggleFloating = fn | |
| 133 | 143 | } | |
| @@ -262,7 +272,16 @@ func (d *KeyboardDispatcher) initActionHandlers() { | |||
| 262 | 272 | return d.onToggleHistorySidebar(ctx) | |
| 263 | 273 | }, | |
| 264 | 274 | input.ActionToggleFavoritesSystemView: func(ctx context.Context) error { | |
| 265 | - | return d.wsCoord.ToggleSystemViewRight(ctx, favoritesSystemViewURL) | |
| 275 | + | if d.onToggleFavoritesSidebar == nil { | |
| 276 | + | return fmt.Errorf("favorites sidebar unavailable: toggle handler not wired") | |
| 277 | + | } | |
| 278 | + | return d.onToggleFavoritesSidebar(ctx) | |
| 279 | + | }, | |
| 280 | + | input.ActionToggleCurrentPageFavorite: func(ctx context.Context) error { | |
| 281 | + | if d.onToggleCurrentFavorite == nil { | |
| 282 | + | return fmt.Errorf("favorites unavailable: toggle current page handler not wired") | |
| 283 | + | } | |
| 284 | + | return d.onToggleCurrentFavorite(ctx) | |
| 266 | 285 | }, | |
| 267 | 286 | input.ActionToggleConfigSystemView: func(ctx context.Context) error { | |
| 268 | 287 | return d.wsCoord.ToggleSystemViewRight(ctx, configSystemViewURL) | |
| @@ -36,25 +36,27 @@ type KeyboardActions struct { | |||||
| 36 | 36 | ||||
| 37 | // KeyboardDispatcher routes keyboard actions to appropriate coordinators. | 37 | // KeyboardDispatcher routes keyboard actions to appropriate coordinators. | ||
| 38 | type KeyboardDispatcher struct { | 38 | type KeyboardDispatcher struct { | ||
| 39 | - | actions KeyboardActions | 39 | + | actions KeyboardActions |
| 40 | - | wsCoord *coordinator.WorkspaceCoordinator | 40 | + | wsCoord *coordinator.WorkspaceCoordinator |
| 41 | - | navCoord *coordinator.NavigationCoordinator | 41 | + | navCoord *coordinator.NavigationCoordinator |
| 42 | - | zoomUC *usecase.ManageZoomUseCase | 42 | + | zoomUC *usecase.ManageZoomUseCase |
| 43 | - | copyURLUC *usecase.CopyURLUseCase | 43 | + | copyURLUC *usecase.CopyURLUseCase |
| 44 | - | actionHandlers map[input.Action]func(ctx context.Context) error | 44 | + | actionHandlers map[input.Action]func(ctx context.Context) error |
| 45 | - | onQuit func() | 45 | + | onQuit func() |
| 46 | - | onFindOpen func(ctx context.Context) error | 46 | + | onFindOpen func(ctx context.Context) error |
| 47 | - | onFindNext func(ctx context.Context) error | 47 | + | onFindNext func(ctx context.Context) error |
| 48 | - | onFindPrev func(ctx context.Context) error | 48 | + | onFindPrev func(ctx context.Context) error |
| 49 | - | onFindClose func(ctx context.Context) error | 49 | + | onFindClose func(ctx context.Context) error |
| 50 | - | activePaneID func(ctx context.Context) entity.PaneID | 50 | + | activePaneID func(ctx context.Context) entity.PaneID |
| 51 | - | onSessionOpen func(ctx context.Context, paneID entity.PaneID) error | 51 | + | onSessionOpen func(ctx context.Context, paneID entity.PaneID) error |
| 52 | - | onMovePaneToTab func(ctx context.Context, paneID entity.PaneID) error | 52 | + | onMovePaneToTab func(ctx context.Context, paneID entity.PaneID) error |
| 53 | - | onMovePaneToNext func(ctx context.Context, paneID entity.PaneID) error | 53 | + | onMovePaneToNext func(ctx context.Context, paneID entity.PaneID) error |
| 54 | - | onEjectPaneToWindow func(ctx context.Context, paneID entity.PaneID) error | 54 | + | onEjectPaneToWindow func(ctx context.Context, paneID entity.PaneID) error |
| 55 | - | onToggleHistorySidebar func(ctx context.Context) error | 55 | + | onToggleHistorySidebar func(ctx context.Context) error |
| 56 | - | onToggleFloating func(ctx context.Context) error | 56 | + | onToggleFavoritesSidebar func(ctx context.Context) error |
| 57 | - | onOpenFloating func(ctx context.Context, target input.FloatingProfileTarget) error | 57 | + | onToggleCurrentFavorite func(ctx context.Context) error |
| 58 | + | onToggleFloating func(ctx context.Context) error | |||
| 59 | + | onOpenFloating func(ctx context.Context, target input.FloatingProfileTarget) error | |||
| 58 | } | 60 | } | ||
| 59 | 61 | ||||
| 60 | // NewKeyboardDispatcher creates a new KeyboardDispatcher. | 62 | // NewKeyboardDispatcher creates a new KeyboardDispatcher. | ||
| @@ -128,6 +130,14 @@ func (d *KeyboardDispatcher) SetOnToggleHistorySidebar(fn func(ctx context.Conte | |||||
| 128 | d.onToggleHistorySidebar = fn | 130 | d.onToggleHistorySidebar = fn | ||
| 129 | } | 131 | } | ||
| 130 | 132 | ||||
| 133 | + | func (d *KeyboardDispatcher) SetOnToggleFavoritesSidebar(fn func(ctx context.Context) error) { | |||
| 134 | + | d.onToggleFavoritesSidebar = fn | |||
| 135 | + | } | |||
| 136 | + | ||||
| 137 | + | func (d *KeyboardDispatcher) SetOnToggleCurrentPageFavorite(fn func(ctx context.Context) error) { | |||
| 138 | + | d.onToggleCurrentFavorite = fn | |||
| 139 | + | } | |||
| 140 | + | ||||
| 131 | func (d *KeyboardDispatcher) SetOnToggleFloatingPane(fn func(ctx context.Context) error) { | 141 | func (d *KeyboardDispatcher) SetOnToggleFloatingPane(fn func(ctx context.Context) error) { | ||
| 132 | d.onToggleFloating = fn | 142 | d.onToggleFloating = fn | ||
| 133 | } | 143 | } | ||
| @@ -262,7 +272,16 @@ func (d *KeyboardDispatcher) initActionHandlers() { | |||||
| 262 | return d.onToggleHistorySidebar(ctx) | 272 | return d.onToggleHistorySidebar(ctx) | ||
| 263 | }, | 273 | }, | ||
| 264 | input.ActionToggleFavoritesSystemView: func(ctx context.Context) error { | 274 | input.ActionToggleFavoritesSystemView: func(ctx context.Context) error { | ||
| 265 | - | return d.wsCoord.ToggleSystemViewRight(ctx, favoritesSystemViewURL) | 275 | + | if d.onToggleFavoritesSidebar == nil { |
| 276 | + | return fmt.Errorf("favorites sidebar unavailable: toggle handler not wired") | |||
| 277 | + | } | |||
| 278 | + | return d.onToggleFavoritesSidebar(ctx) | |||
| 279 | + | }, | |||
| 280 | + | input.ActionToggleCurrentPageFavorite: func(ctx context.Context) error { | |||
| 281 | + | if d.onToggleCurrentFavorite == nil { | |||
| 282 | + | return fmt.Errorf("favorites unavailable: toggle current page handler not wired") | |||
| 283 | + | } | |||
| 284 | + | return d.onToggleCurrentFavorite(ctx) | |||
| 266 | }, | 285 | }, | ||
| 267 | input.ActionToggleConfigSystemView: func(ctx context.Context) error { | 286 | input.ActionToggleConfigSystemView: func(ctx context.Context) error { | ||
| 268 | return d.wsCoord.ToggleSystemViewRight(ctx, configSystemViewURL) | 287 | return d.wsCoord.ToggleSystemViewRight(ctx, configSystemViewURL) | ||
M internal/ui/dispatcher/keyboard_test.go +40 -0
| @@ -115,6 +115,46 @@ func TestKeyboardDispatcher_ToggleHistorySidebarSetThenUnsetReturnsError(t *test | |||
| 115 | 115 | assert.ErrorContains(t, err, "history sidebar unavailable") | |
| 116 | 116 | } | |
| 117 | 117 | ||
| 118 | + | func TestKeyboardDispatcher_ToggleFavoritesSidebarCallsCallbackAndPropagatesErrors(t *testing.T) { | |
| 119 | + | ctx := context.Background() | |
| 120 | + | d := NewKeyboardDispatcher(ctx, &coordinator.WorkspaceCoordinator{}, &coordinator.NavigationCoordinator{}, nil, nil, KeyboardActions{}, func(context.Context) entity.PaneID { return "" }) | |
| 121 | + | ||
| 122 | + | missingErr := d.Dispatch(ctx, input.ActionToggleFavoritesSystemView) | |
| 123 | + | require.Error(t, missingErr) | |
| 124 | + | assert.ErrorContains(t, missingErr, "favorites sidebar unavailable") | |
| 125 | + | ||
| 126 | + | wantErr := fmt.Errorf("favorites failed") | |
| 127 | + | d.SetOnToggleFavoritesSidebar(func(context.Context) error { return wantErr }) | |
| 128 | + | err := d.Dispatch(ctx, input.ActionToggleFavoritesSystemView) | |
| 129 | + | require.Error(t, err) | |
| 130 | + | assert.ErrorIs(t, err, wantErr) | |
| 131 | + | ||
| 132 | + | var called bool | |
| 133 | + | d.SetOnToggleFavoritesSidebar(func(context.Context) error { called = true; return nil }) | |
| 134 | + | require.NoError(t, d.Dispatch(ctx, input.ActionToggleFavoritesSystemView)) | |
| 135 | + | assert.True(t, called) | |
| 136 | + | } | |
| 137 | + | ||
| 138 | + | func TestKeyboardDispatcher_ToggleCurrentPageFavoriteCallsCallbackAndPropagatesErrors(t *testing.T) { | |
| 139 | + | ctx := context.Background() | |
| 140 | + | d := NewKeyboardDispatcher(ctx, &coordinator.WorkspaceCoordinator{}, &coordinator.NavigationCoordinator{}, nil, nil, KeyboardActions{}, func(context.Context) entity.PaneID { return "" }) | |
| 141 | + | ||
| 142 | + | missingErr := d.Dispatch(ctx, input.ActionToggleCurrentPageFavorite) | |
| 143 | + | require.Error(t, missingErr) | |
| 144 | + | assert.ErrorContains(t, missingErr, "toggle current page handler not wired") | |
| 145 | + | ||
| 146 | + | wantErr := fmt.Errorf("toggle failed") | |
| 147 | + | d.SetOnToggleCurrentPageFavorite(func(context.Context) error { return wantErr }) | |
| 148 | + | err := d.Dispatch(ctx, input.ActionToggleCurrentPageFavorite) | |
| 149 | + | require.Error(t, err) | |
| 150 | + | assert.ErrorIs(t, err, wantErr) | |
| 151 | + | ||
| 152 | + | var called bool | |
| 153 | + | d.SetOnToggleCurrentPageFavorite(func(context.Context) error { called = true; return nil }) | |
| 154 | + | require.NoError(t, d.Dispatch(ctx, input.ActionToggleCurrentPageFavorite)) | |
| 155 | + | assert.True(t, called) | |
| 156 | + | } | |
| 157 | + | ||
| 118 | 158 | func TestKeyboardDispatcher_PassesActivePaneIDToShellCallbacks(t *testing.T) { | |
| 119 | 159 | ctx := context.Background() | |
| 120 | 160 | activePaneID := entity.PaneID("pane-1") | |
| @@ -115,6 +115,46 @@ func TestKeyboardDispatcher_ToggleHistorySidebarSetThenUnsetReturnsError(t *test | |||||
| 115 | assert.ErrorContains(t, err, "history sidebar unavailable") | 115 | assert.ErrorContains(t, err, "history sidebar unavailable") | ||
| 116 | } | 116 | } | ||
| 117 | 117 | ||||
| 118 | + | func TestKeyboardDispatcher_ToggleFavoritesSidebarCallsCallbackAndPropagatesErrors(t *testing.T) { | |||
| 119 | + | ctx := context.Background() | |||
| 120 | + | d := NewKeyboardDispatcher(ctx, &coordinator.WorkspaceCoordinator{}, &coordinator.NavigationCoordinator{}, nil, nil, KeyboardActions{}, func(context.Context) entity.PaneID { return "" }) | |||
| 121 | + | ||||
| 122 | + | missingErr := d.Dispatch(ctx, input.ActionToggleFavoritesSystemView) | |||
| 123 | + | require.Error(t, missingErr) | |||
| 124 | + | assert.ErrorContains(t, missingErr, "favorites sidebar unavailable") | |||
| 125 | + | ||||
| 126 | + | wantErr := fmt.Errorf("favorites failed") | |||
| 127 | + | d.SetOnToggleFavoritesSidebar(func(context.Context) error { return wantErr }) | |||
| 128 | + | err := d.Dispatch(ctx, input.ActionToggleFavoritesSystemView) | |||
| 129 | + | require.Error(t, err) | |||
| 130 | + | assert.ErrorIs(t, err, wantErr) | |||
| 131 | + | ||||
| 132 | + | var called bool | |||
| 133 | + | d.SetOnToggleFavoritesSidebar(func(context.Context) error { called = true; return nil }) | |||
| 134 | + | require.NoError(t, d.Dispatch(ctx, input.ActionToggleFavoritesSystemView)) | |||
| 135 | + | assert.True(t, called) | |||
| 136 | + | } | |||
| 137 | + | ||||
| 138 | + | func TestKeyboardDispatcher_ToggleCurrentPageFavoriteCallsCallbackAndPropagatesErrors(t *testing.T) { | |||
| 139 | + | ctx := context.Background() | |||
| 140 | + | d := NewKeyboardDispatcher(ctx, &coordinator.WorkspaceCoordinator{}, &coordinator.NavigationCoordinator{}, nil, nil, KeyboardActions{}, func(context.Context) entity.PaneID { return "" }) | |||
| 141 | + | ||||
| 142 | + | missingErr := d.Dispatch(ctx, input.ActionToggleCurrentPageFavorite) | |||
| 143 | + | require.Error(t, missingErr) | |||
| 144 | + | assert.ErrorContains(t, missingErr, "toggle current page handler not wired") | |||
| 145 | + | ||||
| 146 | + | wantErr := fmt.Errorf("toggle failed") | |||
| 147 | + | d.SetOnToggleCurrentPageFavorite(func(context.Context) error { return wantErr }) | |||
| 148 | + | err := d.Dispatch(ctx, input.ActionToggleCurrentPageFavorite) | |||
| 149 | + | require.Error(t, err) | |||
| 150 | + | assert.ErrorIs(t, err, wantErr) | |||
| 151 | + | ||||
| 152 | + | var called bool | |||
| 153 | + | d.SetOnToggleCurrentPageFavorite(func(context.Context) error { called = true; return nil }) | |||
| 154 | + | require.NoError(t, d.Dispatch(ctx, input.ActionToggleCurrentPageFavorite)) | |||
| 155 | + | assert.True(t, called) | |||
| 156 | + | } | |||
| 157 | + | ||||
| 118 | func TestKeyboardDispatcher_PassesActivePaneIDToShellCallbacks(t *testing.T) { | 158 | func TestKeyboardDispatcher_PassesActivePaneIDToShellCallbacks(t *testing.T) { | ||
| 119 | ctx := context.Background() | 159 | ctx := context.Background() | ||
| 120 | activePaneID := entity.PaneID("pane-1") | 160 | activePaneID := entity.PaneID("pane-1") | ||
M internal/ui/input/global_shortcuts.go +19 -16
| @@ -484,22 +484,24 @@ func appendGlobalShortcutEventFields(evt *zerolog.Event, binding KeyBinding, act | |||
| 484 | 484 | ||
| 485 | 485 | func globalShortcutActionMap() map[string]Action { | |
| 486 | 486 | return map[string]Action{ | |
| 487 | - | "toggle_floating_pane": ActionToggleFloatingPane, | |
| 488 | - | "toggle-floating-pane": ActionToggleFloatingPane, | |
| 489 | - | "consume_or_expel_left": ActionConsumeOrExpelLeft, | |
| 490 | - | "consume-or-expel-left": ActionConsumeOrExpelLeft, | |
| 491 | - | "consume_or_expel_right": ActionConsumeOrExpelRight, | |
| 492 | - | "consume-or-expel-right": ActionConsumeOrExpelRight, | |
| 493 | - | "consume_or_expel_up": ActionConsumeOrExpelUp, | |
| 494 | - | "consume-or-expel-up": ActionConsumeOrExpelUp, | |
| 495 | - | "consume_or_expel_down": ActionConsumeOrExpelDown, | |
| 496 | - | "consume-or-expel-down": ActionConsumeOrExpelDown, | |
| 497 | - | "toggle_history_systemview": ActionToggleHistorySystemView, | |
| 498 | - | "toggle-history-systemview": ActionToggleHistorySystemView, | |
| 499 | - | "toggle_favorites_systemview": ActionToggleFavoritesSystemView, | |
| 500 | - | "toggle-favorites-systemview": ActionToggleFavoritesSystemView, | |
| 501 | - | "toggle_config_systemview": ActionToggleConfigSystemView, | |
| 502 | - | "toggle-config-systemview": ActionToggleConfigSystemView, | |
| 487 | + | "toggle_floating_pane": ActionToggleFloatingPane, | |
| 488 | + | "toggle-floating-pane": ActionToggleFloatingPane, | |
| 489 | + | "consume_or_expel_left": ActionConsumeOrExpelLeft, | |
| 490 | + | "consume-or-expel-left": ActionConsumeOrExpelLeft, | |
| 491 | + | "consume_or_expel_right": ActionConsumeOrExpelRight, | |
| 492 | + | "consume-or-expel-right": ActionConsumeOrExpelRight, | |
| 493 | + | "consume_or_expel_up": ActionConsumeOrExpelUp, | |
| 494 | + | "consume-or-expel-up": ActionConsumeOrExpelUp, | |
| 495 | + | "consume_or_expel_down": ActionConsumeOrExpelDown, | |
| 496 | + | "consume-or-expel-down": ActionConsumeOrExpelDown, | |
| 497 | + | "toggle_history_systemview": ActionToggleHistorySystemView, | |
| 498 | + | "toggle-history-systemview": ActionToggleHistorySystemView, | |
| 499 | + | "toggle_favorites_systemview": ActionToggleFavoritesSystemView, | |
| 500 | + | "toggle-favorites-systemview": ActionToggleFavoritesSystemView, | |
| 501 | + | "toggle_current_page_favorite": ActionToggleCurrentPageFavorite, | |
| 502 | + | "toggle-current-page-favorite": ActionToggleCurrentPageFavorite, | |
| 503 | + | "toggle_config_systemview": ActionToggleConfigSystemView, | |
| 504 | + | "toggle-config-systemview": ActionToggleConfigSystemView, | |
| 503 | 505 | } | |
| 504 | 506 | } | |
| 505 | 507 | ||
| @@ -711,6 +713,7 @@ func isRepeatedGlobalShortcutSuppressed(action Action) bool { | |||
| 711 | 713 | ActionToggleFloatingPane, | |
| 712 | 714 | ActionToggleHistorySystemView, | |
| 713 | 715 | ActionToggleFavoritesSystemView, | |
| 716 | + | ActionToggleCurrentPageFavorite, | |
| 714 | 717 | ActionToggleConfigSystemView, | |
| 715 | 718 | ActionCopyURL, | |
| 716 | 719 | ActionConsumeOrExpelLeft, | |
| @@ -484,22 +484,24 @@ func appendGlobalShortcutEventFields(evt *zerolog.Event, binding KeyBinding, act | |||||
| 484 | 484 | ||||
| 485 | func globalShortcutActionMap() map[string]Action { | 485 | func globalShortcutActionMap() map[string]Action { | ||
| 486 | return map[string]Action{ | 486 | return map[string]Action{ | ||
| 487 | - | "toggle_floating_pane": ActionToggleFloatingPane, | 487 | + | "toggle_floating_pane": ActionToggleFloatingPane, |
| 488 | - | "toggle-floating-pane": ActionToggleFloatingPane, | 488 | + | "toggle-floating-pane": ActionToggleFloatingPane, |
| 489 | - | "consume_or_expel_left": ActionConsumeOrExpelLeft, | 489 | + | "consume_or_expel_left": ActionConsumeOrExpelLeft, |
| 490 | - | "consume-or-expel-left": ActionConsumeOrExpelLeft, | 490 | + | "consume-or-expel-left": ActionConsumeOrExpelLeft, |
| 491 | - | "consume_or_expel_right": ActionConsumeOrExpelRight, | 491 | + | "consume_or_expel_right": ActionConsumeOrExpelRight, |
| 492 | - | "consume-or-expel-right": ActionConsumeOrExpelRight, | 492 | + | "consume-or-expel-right": ActionConsumeOrExpelRight, |
| 493 | - | "consume_or_expel_up": ActionConsumeOrExpelUp, | 493 | + | "consume_or_expel_up": ActionConsumeOrExpelUp, |
| 494 | - | "consume-or-expel-up": ActionConsumeOrExpelUp, | 494 | + | "consume-or-expel-up": ActionConsumeOrExpelUp, |
| 495 | - | "consume_or_expel_down": ActionConsumeOrExpelDown, | 495 | + | "consume_or_expel_down": ActionConsumeOrExpelDown, |
| 496 | - | "consume-or-expel-down": ActionConsumeOrExpelDown, | 496 | + | "consume-or-expel-down": ActionConsumeOrExpelDown, |
| 497 | - | "toggle_history_systemview": ActionToggleHistorySystemView, | 497 | + | "toggle_history_systemview": ActionToggleHistorySystemView, |
| 498 | - | "toggle-history-systemview": ActionToggleHistorySystemView, | 498 | + | "toggle-history-systemview": ActionToggleHistorySystemView, |
| 499 | - | "toggle_favorites_systemview": ActionToggleFavoritesSystemView, | 499 | + | "toggle_favorites_systemview": ActionToggleFavoritesSystemView, |
| 500 | - | "toggle-favorites-systemview": ActionToggleFavoritesSystemView, | 500 | + | "toggle-favorites-systemview": ActionToggleFavoritesSystemView, |
| 501 | - | "toggle_config_systemview": ActionToggleConfigSystemView, | 501 | + | "toggle_current_page_favorite": ActionToggleCurrentPageFavorite, |
| 502 | - | "toggle-config-systemview": ActionToggleConfigSystemView, | 502 | + | "toggle-current-page-favorite": ActionToggleCurrentPageFavorite, |
| 503 | + | "toggle_config_systemview": ActionToggleConfigSystemView, | |||
| 504 | + | "toggle-config-systemview": ActionToggleConfigSystemView, | |||
| 503 | } | 505 | } | ||
| 504 | } | 506 | } | ||
| 505 | 507 | ||||
| @@ -711,6 +713,7 @@ func isRepeatedGlobalShortcutSuppressed(action Action) bool { | |||||
| 711 | ActionToggleFloatingPane, | 713 | ActionToggleFloatingPane, | ||
| 712 | ActionToggleHistorySystemView, | 714 | ActionToggleHistorySystemView, | ||
| 713 | ActionToggleFavoritesSystemView, | 715 | ActionToggleFavoritesSystemView, | ||
| 716 | + | ActionToggleCurrentPageFavorite, | |||
| 714 | ActionToggleConfigSystemView, | 717 | ActionToggleConfigSystemView, | ||
| 715 | ActionCopyURL, | 718 | ActionCopyURL, | ||
| 716 | ActionConsumeOrExpelLeft, | 719 | ActionConsumeOrExpelLeft, | ||
M internal/ui/input/global_shortcuts_test.go +1 -0
| @@ -231,6 +231,7 @@ func TestGlobalShortcutHandlerSuppressesRepeatedAdditionalOneShotUIActions(t *te | |||
| 231 | 231 | ActionOpenDevTools, | |
| 232 | 232 | ActionToggleFloatingPane, | |
| 233 | 233 | ActionToggleFavoritesSystemView, | |
| 234 | + | ActionToggleCurrentPageFavorite, | |
| 234 | 235 | ActionToggleConfigSystemView, | |
| 235 | 236 | ActionCopyURL, | |
| 236 | 237 | ActionConsumeOrExpelLeft, | |
| @@ -231,6 +231,7 @@ func TestGlobalShortcutHandlerSuppressesRepeatedAdditionalOneShotUIActions(t *te | |||||
| 231 | ActionOpenDevTools, | 231 | ActionOpenDevTools, | ||
| 232 | ActionToggleFloatingPane, | 232 | ActionToggleFloatingPane, | ||
| 233 | ActionToggleFavoritesSystemView, | 233 | ActionToggleFavoritesSystemView, | ||
| 234 | + | ActionToggleCurrentPageFavorite, | |||
| 234 | ActionToggleConfigSystemView, | 235 | ActionToggleConfigSystemView, | ||
| 235 | ActionCopyURL, | 236 | ActionCopyURL, | ||
| 236 | ActionConsumeOrExpelLeft, | 237 | ActionConsumeOrExpelLeft, | ||
M internal/ui/input/shortcuts.go +11 -8
| @@ -190,6 +190,7 @@ const ( | |||
| 190 | 190 | ActionToggleFloatingPane Action = "toggle_floating_pane" | |
| 191 | 191 | ActionToggleHistorySystemView Action = "toggle_history_systemview" | |
| 192 | 192 | ActionToggleFavoritesSystemView Action = "toggle_favorites_systemview" | |
| 193 | + | ActionToggleCurrentPageFavorite Action = "toggle_current_page_favorite" | |
| 193 | 194 | ActionToggleConfigSystemView Action = "toggle_config_systemview" | |
| 194 | 195 | ||
| 195 | 196 | // Clipboard | |
| @@ -564,14 +565,16 @@ func (s *ShortcutSet) buildModeShortcuts(ctx context.Context, bindings map[strin | |||
| 564 | 565 | } | |
| 565 | 566 | ||
| 566 | 567 | var configActionToAction = map[string]Action{ | |
| 567 | - | "toggle_floating_pane": ActionToggleFloatingPane, | |
| 568 | - | "toggle-floating-pane": ActionToggleFloatingPane, | |
| 569 | - | "toggle_history_systemview": ActionToggleHistorySystemView, | |
| 570 | - | "toggle-history-systemview": ActionToggleHistorySystemView, | |
| 571 | - | "toggle_favorites_systemview": ActionToggleFavoritesSystemView, | |
| 572 | - | "toggle-favorites-systemview": ActionToggleFavoritesSystemView, | |
| 573 | - | "toggle_config_systemview": ActionToggleConfigSystemView, | |
| 574 | - | "toggle-config-systemview": ActionToggleConfigSystemView, | |
| 568 | + | "toggle_floating_pane": ActionToggleFloatingPane, | |
| 569 | + | "toggle-floating-pane": ActionToggleFloatingPane, | |
| 570 | + | "toggle_history_systemview": ActionToggleHistorySystemView, | |
| 571 | + | "toggle-history-systemview": ActionToggleHistorySystemView, | |
| 572 | + | "toggle_favorites_systemview": ActionToggleFavoritesSystemView, | |
| 573 | + | "toggle-favorites-systemview": ActionToggleFavoritesSystemView, | |
| 574 | + | "toggle_current_page_favorite": ActionToggleCurrentPageFavorite, | |
| 575 | + | "toggle-current-page-favorite": ActionToggleCurrentPageFavorite, | |
| 576 | + | "toggle_config_systemview": ActionToggleConfigSystemView, | |
| 577 | + | "toggle-config-systemview": ActionToggleConfigSystemView, | |
| 575 | 578 | ||
| 576 | 579 | // Tab actions | |
| 577 | 580 | "new_tab": ActionNewTab, | |
| @@ -190,6 +190,7 @@ const ( | |||||
| 190 | ActionToggleFloatingPane Action = "toggle_floating_pane" | 190 | ActionToggleFloatingPane Action = "toggle_floating_pane" | ||
| 191 | ActionToggleHistorySystemView Action = "toggle_history_systemview" | 191 | ActionToggleHistorySystemView Action = "toggle_history_systemview" | ||
| 192 | ActionToggleFavoritesSystemView Action = "toggle_favorites_systemview" | 192 | ActionToggleFavoritesSystemView Action = "toggle_favorites_systemview" | ||
| 193 | + | ActionToggleCurrentPageFavorite Action = "toggle_current_page_favorite" | |||
| 193 | ActionToggleConfigSystemView Action = "toggle_config_systemview" | 194 | ActionToggleConfigSystemView Action = "toggle_config_systemview" | ||
| 194 | 195 | ||||
| 195 | // Clipboard | 196 | // Clipboard | ||
| @@ -564,14 +565,16 @@ func (s *ShortcutSet) buildModeShortcuts(ctx context.Context, bindings map[strin | |||||
| 564 | } | 565 | } | ||
| 565 | 566 | ||||
| 566 | var configActionToAction = map[string]Action{ | 567 | var configActionToAction = map[string]Action{ | ||
| 567 | - | "toggle_floating_pane": ActionToggleFloatingPane, | 568 | + | "toggle_floating_pane": ActionToggleFloatingPane, |
| 568 | - | "toggle-floating-pane": ActionToggleFloatingPane, | 569 | + | "toggle-floating-pane": ActionToggleFloatingPane, |
| 569 | - | "toggle_history_systemview": ActionToggleHistorySystemView, | 570 | + | "toggle_history_systemview": ActionToggleHistorySystemView, |
| 570 | - | "toggle-history-systemview": ActionToggleHistorySystemView, | 571 | + | "toggle-history-systemview": ActionToggleHistorySystemView, |
| 571 | - | "toggle_favorites_systemview": ActionToggleFavoritesSystemView, | 572 | + | "toggle_favorites_systemview": ActionToggleFavoritesSystemView, |
| 572 | - | "toggle-favorites-systemview": ActionToggleFavoritesSystemView, | 573 | + | "toggle-favorites-systemview": ActionToggleFavoritesSystemView, |
| 573 | - | "toggle_config_systemview": ActionToggleConfigSystemView, | 574 | + | "toggle_current_page_favorite": ActionToggleCurrentPageFavorite, |
| 574 | - | "toggle-config-systemview": ActionToggleConfigSystemView, | 575 | + | "toggle-current-page-favorite": ActionToggleCurrentPageFavorite, |
| 576 | + | "toggle_config_systemview": ActionToggleConfigSystemView, | |||
| 577 | + | "toggle-config-systemview": ActionToggleConfigSystemView, | |||
| 575 | 578 | ||||
| 576 | // Tab actions | 579 | // Tab actions | ||
| 577 | "new_tab": ActionNewTab, | 580 | "new_tab": ActionNewTab, | ||
M internal/ui/input/shortcuts_test.go +2 -0
| @@ -402,6 +402,7 @@ func TestMapConfigAction_ToggleSystemViews(t *testing.T) { | |||
| 402 | 402 | }{ | |
| 403 | 403 | {name: "toggle_history_systemview", want: ActionToggleHistorySystemView}, | |
| 404 | 404 | {name: "toggle_favorites_systemview", want: ActionToggleFavoritesSystemView}, | |
| 405 | + | {name: "toggle_current_page_favorite", want: ActionToggleCurrentPageFavorite}, | |
| 405 | 406 | {name: "toggle_config_systemview", want: ActionToggleConfigSystemView}, | |
| 406 | 407 | } | |
| 407 | 408 | ||
| @@ -421,6 +422,7 @@ func TestMapConfigAction_ToggleSystemViewsHyphenAlias(t *testing.T) { | |||
| 421 | 422 | }{ | |
| 422 | 423 | {name: "toggle-history-systemview", want: ActionToggleHistorySystemView}, | |
| 423 | 424 | {name: "toggle-favorites-systemview", want: ActionToggleFavoritesSystemView}, | |
| 425 | + | {name: "toggle-current-page-favorite", want: ActionToggleCurrentPageFavorite}, | |
| 424 | 426 | {name: "toggle-config-systemview", want: ActionToggleConfigSystemView}, | |
| 425 | 427 | } | |
| 426 | 428 | ||
| @@ -402,6 +402,7 @@ func TestMapConfigAction_ToggleSystemViews(t *testing.T) { | |||||
| 402 | }{ | 402 | }{ | ||
| 403 | {name: "toggle_history_systemview", want: ActionToggleHistorySystemView}, | 403 | {name: "toggle_history_systemview", want: ActionToggleHistorySystemView}, | ||
| 404 | {name: "toggle_favorites_systemview", want: ActionToggleFavoritesSystemView}, | 404 | {name: "toggle_favorites_systemview", want: ActionToggleFavoritesSystemView}, | ||
| 405 | + | {name: "toggle_current_page_favorite", want: ActionToggleCurrentPageFavorite}, | |||
| 405 | {name: "toggle_config_systemview", want: ActionToggleConfigSystemView}, | 406 | {name: "toggle_config_systemview", want: ActionToggleConfigSystemView}, | ||
| 406 | } | 407 | } | ||
| 407 | 408 | ||||
| @@ -421,6 +422,7 @@ func TestMapConfigAction_ToggleSystemViewsHyphenAlias(t *testing.T) { | |||||
| 421 | }{ | 422 | }{ | ||
| 422 | {name: "toggle-history-systemview", want: ActionToggleHistorySystemView}, | 423 | {name: "toggle-history-systemview", want: ActionToggleHistorySystemView}, | ||
| 423 | {name: "toggle-favorites-systemview", want: ActionToggleFavoritesSystemView}, | 424 | {name: "toggle-favorites-systemview", want: ActionToggleFavoritesSystemView}, | ||
| 425 | + | {name: "toggle-current-page-favorite", want: ActionToggleCurrentPageFavorite}, | |||
| 424 | {name: "toggle-config-systemview", want: ActionToggleConfigSystemView}, | 426 | {name: "toggle-config-systemview", want: ActionToggleConfigSystemView}, | ||
| 425 | } | 427 | } | ||
| 426 | 428 | ||||