detached 142d625

fix(favorites): migrate sidebar shortcut

commit
142d6251bd2256f2e541203f997f1081b88aa0cc
author
brice · · 2026-08-12 04:13 UTC
parent
3721706
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
14 files · +163 -76
M docs/reference/keybindings.md +1 -1
@@ -127,7 +127,7 @@ keys = ["alt+f"]
127127 [workspace.shortcuts.actions.toggle-history-systemview]
128128 keys = ["ctrl+h"]
129129
130-[workspace.shortcuts.actions.toggle-favorites-systemview]
130+[workspace.shortcuts.actions.toggle-favorites-sidebar]
131131 keys = ["ctrl+b"]
132132
133133 [workspace.shortcuts.actions.toggle-current-page-favorite]
@@ -127,7 +127,7 @@ keys = ["alt+f"]
127 [workspace.shortcuts.actions.toggle-history-systemview]127 [workspace.shortcuts.actions.toggle-history-systemview]
128 keys = ["ctrl+h"]128 keys = ["ctrl+h"]
129 129
130-[workspace.shortcuts.actions.toggle-favorites-systemview]130+[workspace.shortcuts.actions.toggle-favorites-sidebar]
131 keys = ["ctrl+b"]131 keys = ["ctrl+b"]
132 132
133 [workspace.shortcuts.actions.toggle-current-page-favorite]133 [workspace.shortcuts.actions.toggle-current-page-favorite]
M internal/infrastructure/config/defaults.go +1 -1
@@ -324,7 +324,7 @@ func DefaultConfig() *Config {
324324 Actions: map[string]ActionBinding{
325325 "toggle-floating-pane": {Keys: []string{"alt+f"}, Desc: "Toggle floating pane"},
326326 "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"},
327+ "toggle-favorites-sidebar": {Keys: []string{"ctrl+b"}, Desc: "Toggle Favorites sidebar"},
328328 "toggle-current-page-favorite": {Keys: []string{"ctrl+d"}, Desc: "Add/remove current page favorite"},
329329 "toggle-config-systemview": {Keys: []string{}, Desc: "Toggle Config in right split"},
330330 "close-pane": {Keys: []string{"ctrl+w"}, Desc: "Close active pane"},
@@ -324,7 +324,7 @@ func DefaultConfig() *Config {
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{"ctrl+b"}, Desc: "Toggle Favorites sidebar"},327+ "toggle-favorites-sidebar": {Keys: []string{"ctrl+b"}, Desc: "Toggle Favorites sidebar"},
328 "toggle-current-page-favorite": {Keys: []string{"ctrl+d"}, Desc: "Add/remove current page favorite"},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"},329 "toggle-config-systemview": {Keys: []string{}, Desc: "Toggle Config in right split"},
330 "close-pane": {Keys: []string{"ctrl+w"}, Desc: "Close active pane"},330 "close-pane": {Keys: []string{"ctrl+w"}, Desc: "Close active pane"},
M internal/infrastructure/config/defaults_test.go +1 -1
@@ -41,7 +41,7 @@ func TestDefaultConfig_CoreDefaults(t *testing.T) {
4141
4242 // System view shortcuts are first-class global actions.
4343 requireActionBinding(t, cfg.Workspace.Shortcuts.Actions, "toggle-history-systemview", []string{"ctrl+h"})
44- requireActionBinding(t, cfg.Workspace.Shortcuts.Actions, "toggle-favorites-systemview", []string{"ctrl+b"})
44+ requireActionBinding(t, cfg.Workspace.Shortcuts.Actions, "toggle-favorites-sidebar", []string{"ctrl+b"})
4545 requireActionBinding(t, cfg.Workspace.Shortcuts.Actions, "toggle-current-page-favorite", []string{"ctrl+d"})
4646 requireActionBinding(t, cfg.Workspace.Shortcuts.Actions, "toggle-config-systemview", []string{})
4747
@@ -41,7 +41,7 @@ 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{"ctrl+b"})44+ requireActionBinding(t, cfg.Workspace.Shortcuts.Actions, "toggle-favorites-sidebar", []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-current-page-favorite", []string{"ctrl+d"})
46 requireActionBinding(t, cfg.Workspace.Shortcuts.Actions, "toggle-config-systemview", []string{})46 requireActionBinding(t, cfg.Workspace.Shortcuts.Actions, "toggle-config-systemview", []string{})
47 47
M internal/infrastructure/config/loader.go +0 -4
@@ -175,10 +175,6 @@ func (m *Manager) transformLegacyConfig() {
175175 transformer.TransformLegacyPopupsToBrowsingContexts(rawConfig)
176176 }
177177
178- // Merge newly added default actions into in-memory config so existing persisted
179- // action maps keep working before the user writes a migrated config to disk.
180- configMigrator.mergeMissingDefaultActions(rawConfig)
181-
182178 // Apply transformed config back to viper
183179 for key, value := range rawConfig {
184180 m.viper.Set(key, value)
@@ -175,10 +175,6 @@ func (m *Manager) transformLegacyConfig() {
175 transformer.TransformLegacyPopupsToBrowsingContexts(rawConfig)175 transformer.TransformLegacyPopupsToBrowsingContexts(rawConfig)
176 }176 }
177 177
178- // Merge newly added default actions into in-memory config so existing persisted
179- // action maps keep working before the user writes a migrated config to disk.
180- configMigrator.mergeMissingDefaultActions(rawConfig)
181-
182 // Apply transformed config back to viper178 // Apply transformed config back to viper
183 for key, value := range rawConfig {179 for key, value := range rawConfig {
184 m.viper.Set(key, value)180 m.viper.Set(key, value)
M internal/infrastructure/config/loader_actions_test.go +34 -12
@@ -36,7 +36,7 @@ touchpad_navigation_max_vertical_ratio = 2.0
3636 assert.InDelta(t, 2.0, cfg.Engine.CEF.Input.TouchpadNavigationMaxVerticalRatio, 0.001)
3737 }
3838
39-func TestManagerLoad_MergesMissingPaneModeActionsInMemory(t *testing.T) {
39+func TestManagerLoad_DoesNotMergeMissingPaneModeActionsInMemory(t *testing.T) {
4040 home := t.TempDir()
4141 t.Setenv("HOME", home)
4242 t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
@@ -64,15 +64,40 @@ desc = "Custom stack pane"
6464 cfg := mgr.Get()
6565 require.NotNil(t, cfg)
6666
67- eject, ok := cfg.Workspace.PaneMode.Actions["eject-pane-to-window"]
68- require.True(t, ok)
69- assert.Equal(t, []string{"w"}, eject.Keys)
67+ _, ok := cfg.Workspace.PaneMode.Actions["eject-pane-to-window"]
68+ assert.False(t, ok)
7069
7170 stack := cfg.Workspace.PaneMode.Actions["stack-pane"]
7271 assert.Equal(t, "Custom stack pane", stack.Desc)
7372 }
7473
75-func TestManagerLoad_UpgradesObsoleteFavoritesShortcutDefaultInMemory(t *testing.T) {
74+func TestManagerLoad_DoesNotMigrateLegacyFavoritesShortcut(t *testing.T) {
75+ home := t.TempDir()
76+ t.Setenv("HOME", home)
77+ t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
78+ t.Setenv("XDG_DATA_HOME", filepath.Join(home, ".local", "share"))
79+ t.Setenv("XDG_STATE_HOME", filepath.Join(home, ".local", "state"))
80+ t.Setenv("XDG_CACHE_HOME", filepath.Join(home, ".cache"))
81+
82+ configFile, err := GetConfigFile()
83+ require.NoError(t, err)
84+ require.NoError(t, os.MkdirAll(filepath.Dir(configFile), 0o755))
85+ require.NoError(t, os.WriteFile(configFile, []byte(`
86+[workspace.shortcuts.actions.toggle-favorites-systemview]
87+keys = []
88+desc = "Custom favorites shortcut"
89+`), 0o644))
90+
91+ mgr, err := NewManager()
92+ require.NoError(t, err)
93+ require.NoError(t, mgr.Load())
94+
95+ favorites := mgr.Get().Workspace.Shortcuts.Actions["toggle-favorites-systemview"]
96+ assert.Empty(t, favorites.Keys)
97+ assert.Equal(t, "Custom favorites shortcut", favorites.Desc)
98+}
99+
100+func TestManagerLoad_DoesNotMigrateLegacyFavoritesShortcutDefault(t *testing.T) {
76101 home := t.TempDir()
77102 t.Setenv("HOME", home)
78103 t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
@@ -96,11 +121,8 @@ desc = "Toggle Favorites in right split"
96121 cfg := mgr.Get()
97122 require.NotNil(t, cfg)
98123 favorites := cfg.Workspace.Shortcuts.Actions["toggle-favorites-systemview"]
99- assert.Equal(t, []string{"ctrl+b"}, favorites.Keys)
100- assert.Equal(t, "Toggle Favorites sidebar", favorites.Desc)
101- currentPage, ok := cfg.Workspace.Shortcuts.Actions["toggle-current-page-favorite"]
102- require.True(t, ok)
103- expectedCurrentPage := DefaultConfig().Workspace.Shortcuts.Actions["toggle-current-page-favorite"]
104- assert.Equal(t, expectedCurrentPage.Keys, currentPage.Keys)
105- assert.Equal(t, expectedCurrentPage.Desc, currentPage.Desc)
124+ assert.Empty(t, favorites.Keys)
125+ assert.Equal(t, "Toggle Favorites in right split", favorites.Desc)
126+ _, ok := cfg.Workspace.Shortcuts.Actions["toggle-favorites-sidebar"]
127+ assert.False(t, ok)
106128 }
@@ -36,7 +36,7 @@ touchpad_navigation_max_vertical_ratio = 2.0
36 assert.InDelta(t, 2.0, cfg.Engine.CEF.Input.TouchpadNavigationMaxVerticalRatio, 0.001)36 assert.InDelta(t, 2.0, cfg.Engine.CEF.Input.TouchpadNavigationMaxVerticalRatio, 0.001)
37 }37 }
38 38
39-func TestManagerLoad_MergesMissingPaneModeActionsInMemory(t *testing.T) {39+func TestManagerLoad_DoesNotMergeMissingPaneModeActionsInMemory(t *testing.T) {
40 home := t.TempDir()40 home := t.TempDir()
41 t.Setenv("HOME", home)41 t.Setenv("HOME", home)
42 t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))42 t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
@@ -64,15 +64,40 @@ desc = "Custom stack pane"
64 cfg := mgr.Get()64 cfg := mgr.Get()
65 require.NotNil(t, cfg)65 require.NotNil(t, cfg)
66 66
67- eject, ok := cfg.Workspace.PaneMode.Actions["eject-pane-to-window"]67+ _, ok := cfg.Workspace.PaneMode.Actions["eject-pane-to-window"]
68- require.True(t, ok)68+ assert.False(t, ok)
69- assert.Equal(t, []string{"w"}, eject.Keys)
70 69
71 stack := cfg.Workspace.PaneMode.Actions["stack-pane"]70 stack := cfg.Workspace.PaneMode.Actions["stack-pane"]
72 assert.Equal(t, "Custom stack pane", stack.Desc)71 assert.Equal(t, "Custom stack pane", stack.Desc)
73 }72 }
74 73
75-func TestManagerLoad_UpgradesObsoleteFavoritesShortcutDefaultInMemory(t *testing.T) {74+func TestManagerLoad_DoesNotMigrateLegacyFavoritesShortcut(t *testing.T) {
75+ home := t.TempDir()
76+ t.Setenv("HOME", home)
77+ t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
78+ t.Setenv("XDG_DATA_HOME", filepath.Join(home, ".local", "share"))
79+ t.Setenv("XDG_STATE_HOME", filepath.Join(home, ".local", "state"))
80+ t.Setenv("XDG_CACHE_HOME", filepath.Join(home, ".cache"))
81+
82+ configFile, err := GetConfigFile()
83+ require.NoError(t, err)
84+ require.NoError(t, os.MkdirAll(filepath.Dir(configFile), 0o755))
85+ require.NoError(t, os.WriteFile(configFile, []byte(`
86+[workspace.shortcuts.actions.toggle-favorites-systemview]
87+keys = []
88+desc = "Custom favorites shortcut"
89+`), 0o644))
90+
91+ mgr, err := NewManager()
92+ require.NoError(t, err)
93+ require.NoError(t, mgr.Load())
94+
95+ favorites := mgr.Get().Workspace.Shortcuts.Actions["toggle-favorites-systemview"]
96+ assert.Empty(t, favorites.Keys)
97+ assert.Equal(t, "Custom favorites shortcut", favorites.Desc)
98+}
99+
100+func TestManagerLoad_DoesNotMigrateLegacyFavoritesShortcutDefault(t *testing.T) {
76 home := t.TempDir()101 home := t.TempDir()
77 t.Setenv("HOME", home)102 t.Setenv("HOME", home)
78 t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))103 t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
@@ -96,11 +121,8 @@ desc = "Toggle Favorites in right split"
96 cfg := mgr.Get()121 cfg := mgr.Get()
97 require.NotNil(t, cfg)122 require.NotNil(t, cfg)
98 favorites := cfg.Workspace.Shortcuts.Actions["toggle-favorites-systemview"]123 favorites := cfg.Workspace.Shortcuts.Actions["toggle-favorites-systemview"]
99- assert.Equal(t, []string{"ctrl+b"}, favorites.Keys)124+ assert.Empty(t, favorites.Keys)
100- assert.Equal(t, "Toggle Favorites sidebar", favorites.Desc)125+ assert.Equal(t, "Toggle Favorites in right split", favorites.Desc)
101- currentPage, ok := cfg.Workspace.Shortcuts.Actions["toggle-current-page-favorite"]126+ _, ok := cfg.Workspace.Shortcuts.Actions["toggle-favorites-sidebar"]
102- require.True(t, ok)127+ assert.False(t, ok)
103- expectedCurrentPage := DefaultConfig().Workspace.Shortcuts.Actions["toggle-current-page-favorite"]
104- assert.Equal(t, expectedCurrentPage.Keys, currentPage.Keys)
105- assert.Equal(t, expectedCurrentPage.Desc, currentPage.Desc)
106 }128 }
M internal/infrastructure/config/migrate.go +54 -35
@@ -25,11 +25,6 @@ type Migrator struct {
2525 defaultConfig *Config
2626 }
2727
28-// configMigrator reuses the default-action metadata needed during config load.
29-// It is immutable after initialization; per-command migration still constructs
30-// explicit Migrator instances when callers need independent state.
31-var configMigrator = NewMigrator()
32-
3328 // NewMigrator creates a new Migrator instance.
3429 func NewMigrator() *Migrator {
3530 v := viper.New()
@@ -124,13 +119,16 @@ func (m *Migrator) DetectChanges() ([]port.KeyChange, error) {
124119 userKeySet[k] = true
125120 }
126121
127- legacyBrowsingContextRenames := m.detectLegacyBrowsingContextRenames(rawUserKeysWithValues)
122+ legacyRenames := append(
123+ m.detectLegacyBrowsingContextRenames(rawUserKeysWithValues),
124+ m.detectLegacyFavoritesSidebarRename(userKeysWithValues)...,
125+ )
128126 var changes []port.KeyChange
129- changes = append(changes, legacyBrowsingContextRenames...)
127+ changes = append(changes, legacyRenames...)
130128
131- handledLegacyDeprecatedKeys := make(map[string]bool, len(legacyBrowsingContextRenames))
132- handledLegacyMissingKeys := make(map[string]bool, len(legacyBrowsingContextRenames))
133- for _, change := range legacyBrowsingContextRenames {
129+ handledLegacyDeprecatedKeys := make(map[string]bool, len(legacyRenames))
130+ handledLegacyMissingKeys := make(map[string]bool, len(legacyRenames))
131+ for _, change := range legacyRenames {
134132 handledLegacyDeprecatedKeys[change.OldKey] = true
135133 handledLegacyMissingKeys[change.NewKey] = true
136134 }
@@ -451,10 +449,11 @@ func (m *Migrator) Migrate() ([]string, error) {
451449 }
452450 }
453451
454- // Remove deprecated and renamed keys from the raw config
452+ // Remove deprecated and renamed keys from the raw config.
455453 for key := range keysToRemove {
456454 m.deleteNestedKey(rawConfig, key)
457455 }
456+ m.upgradeLegacyFavoritesSidebarDefault(rawConfig)
458457 m.mergeMissingDefaultActions(rawConfig)
459458
460459 // Create a new Viper instance with defaults for added keys
@@ -730,6 +729,31 @@ func (m *Migrator) getRawUserConfigKeysWithValues(configFile string) (map[string
730729 return result, nil
731730 }
732731
732+func (m *Migrator) detectLegacyFavoritesSidebarRename(userKeys map[string]any) []port.KeyChange {
733+ const (
734+ actionsKey = "workspace.shortcuts.actions"
735+ legacyName = "toggle-favorites-systemview"
736+ newName = "toggle-favorites-sidebar"
737+ )
738+
739+ actionsValue, ok := userKeys[actionsKey]
740+ if !ok {
741+ return nil
742+ }
743+ actions, ok := m.toStringAnyMap(actionsValue)
744+ if !ok || actions[legacyName] == nil || actions[newName] != nil {
745+ return nil
746+ }
747+
748+ return []port.KeyChange{{
749+ Type: port.KeyChangeRenamed,
750+ OldKey: actionsKey + "." + legacyName,
751+ NewKey: actionsKey + "." + newName,
752+ OldValue: m.formatValue(actions[legacyName]),
753+ NewValue: m.formatValue(m.defaultConfig.Workspace.Shortcuts.Actions[newName]),
754+ }}
755+}
756+
733757 func (m *Migrator) detectLegacyBrowsingContextRenames(rawUserKeys map[string]any) []port.KeyChange {
734758 type renamePair struct {
735759 oldKey string
@@ -859,17 +883,11 @@ func (m *Migrator) detectMissingDefaultActions(userKeysWithValues map[string]any
859883 }
860884
861885 for actionName := range actionMap.actions {
862- if actionValue, exists := userActions[actionName]; exists {
863- if m.isObsoleteDefaultAction(actionMap.key, actionName, actionValue) {
864- missing = append(missing, actionMap.key+"."+actionName)
865- }
886+ if _, exists := userActions[actionName]; exists {
866887 continue
867888 }
868889 legacyActionName := strings.ReplaceAll(actionName, "-", "_")
869- if actionValue, exists := userActions[legacyActionName]; exists {
870- if m.isObsoleteDefaultAction(actionMap.key, actionName, actionValue) {
871- missing = append(missing, actionMap.key+"."+actionName)
872- }
890+ if _, exists := userActions[legacyActionName]; exists {
873891 continue
874892 }
875893 missing = append(missing, actionMap.key+"."+actionName)
@@ -894,18 +912,11 @@ func (m *Migrator) mergeMissingDefaultActions(rawConfig map[string]any) {
894912 }
895913
896914 for actionName, defaultAction := range actionMap.actions {
897- if actionValue, exists := userActions[actionName]; exists {
898- if m.isObsoleteDefaultAction(actionMap.key, actionName, actionValue) {
899- userActions[actionName] = defaultAction
900- }
915+ if _, exists := userActions[actionName]; exists {
901916 continue
902917 }
903918 legacyActionName := strings.ReplaceAll(actionName, "-", "_")
904- if actionValue, exists := userActions[legacyActionName]; exists {
905- if m.isObsoleteDefaultAction(actionMap.key, actionName, actionValue) {
906- delete(userActions, legacyActionName)
907- userActions[actionName] = defaultAction
908- }
919+ if _, exists := userActions[legacyActionName]; exists {
909920 continue
910921 }
911922 userActions[actionName] = defaultAction
@@ -915,15 +926,23 @@ func (m *Migrator) mergeMissingDefaultActions(rawConfig map[string]any) {
915926 }
916927 }
917928
918-func (m *Migrator) isObsoleteDefaultAction(actionMapKey, actionName string, actionValue any) bool {
919- if actionMapKey != "workspace.shortcuts.actions" || actionName != "toggle-favorites-systemview" {
920- return false
921- }
922- binding, ok := m.actionBindingFromAny(actionValue)
929+func (m *Migrator) upgradeLegacyFavoritesSidebarDefault(rawConfig map[string]any) {
930+ const (
931+ actionsKey = "workspace.shortcuts.actions"
932+ actionName = "toggle-favorites-sidebar"
933+ )
934+
935+ actionsValue := m.getNestedValue(rawConfig, actionsKey)
936+ actions, ok := m.toStringAnyMap(actionsValue)
923937 if !ok {
924- return false
938+ return
939+ }
940+ binding, ok := m.actionBindingFromAny(actions[actionName])
941+ if !ok || !reflect.DeepEqual(binding, entity.ActionBinding{Keys: []string{}, Desc: "Toggle Favorites in right split"}) {
942+ return
925943 }
926- return len(binding.Keys) == 0 && strings.TrimSpace(binding.Desc) == "Toggle Favorites in right split"
944+ actions[actionName] = m.defaultConfig.Workspace.Shortcuts.Actions[actionName]
945+ m.setNestedValue(rawConfig, actionsKey, actions)
927946 }
928947
929948 func (m *Migrator) actionBindingFromAny(value any) (entity.ActionBinding, bool) {
@@ -25,11 +25,6 @@ type Migrator struct {
25 defaultConfig *Config25 defaultConfig *Config
26 }26 }
27 27
28-// configMigrator reuses the default-action metadata needed during config load.
29-// It is immutable after initialization; per-command migration still constructs
30-// explicit Migrator instances when callers need independent state.
31-var configMigrator = NewMigrator()
32-
33 // NewMigrator creates a new Migrator instance.28 // NewMigrator creates a new Migrator instance.
34 func NewMigrator() *Migrator {29 func NewMigrator() *Migrator {
35 v := viper.New()30 v := viper.New()
@@ -124,13 +119,16 @@ func (m *Migrator) DetectChanges() ([]port.KeyChange, error) {
124 userKeySet[k] = true119 userKeySet[k] = true
125 }120 }
126 121
127- legacyBrowsingContextRenames := m.detectLegacyBrowsingContextRenames(rawUserKeysWithValues)122+ legacyRenames := append(
123+ m.detectLegacyBrowsingContextRenames(rawUserKeysWithValues),
124+ m.detectLegacyFavoritesSidebarRename(userKeysWithValues)...,
125+ )
128 var changes []port.KeyChange126 var changes []port.KeyChange
129- changes = append(changes, legacyBrowsingContextRenames...)127+ changes = append(changes, legacyRenames...)
130 128
131- handledLegacyDeprecatedKeys := make(map[string]bool, len(legacyBrowsingContextRenames))129+ handledLegacyDeprecatedKeys := make(map[string]bool, len(legacyRenames))
132- handledLegacyMissingKeys := make(map[string]bool, len(legacyBrowsingContextRenames))130+ handledLegacyMissingKeys := make(map[string]bool, len(legacyRenames))
133- for _, change := range legacyBrowsingContextRenames {131+ for _, change := range legacyRenames {
134 handledLegacyDeprecatedKeys[change.OldKey] = true132 handledLegacyDeprecatedKeys[change.OldKey] = true
135 handledLegacyMissingKeys[change.NewKey] = true133 handledLegacyMissingKeys[change.NewKey] = true
136 }134 }
@@ -451,10 +449,11 @@ func (m *Migrator) Migrate() ([]string, error) {
451 }449 }
452 }450 }
453 451
454- // Remove deprecated and renamed keys from the raw config452+ // Remove deprecated and renamed keys from the raw config.
455 for key := range keysToRemove {453 for key := range keysToRemove {
456 m.deleteNestedKey(rawConfig, key)454 m.deleteNestedKey(rawConfig, key)
457 }455 }
456+ m.upgradeLegacyFavoritesSidebarDefault(rawConfig)
458 m.mergeMissingDefaultActions(rawConfig)457 m.mergeMissingDefaultActions(rawConfig)
459 458
460 // Create a new Viper instance with defaults for added keys459 // Create a new Viper instance with defaults for added keys
@@ -730,6 +729,31 @@ func (m *Migrator) getRawUserConfigKeysWithValues(configFile string) (map[string
730 return result, nil729 return result, nil
731 }730 }
732 731
732+func (m *Migrator) detectLegacyFavoritesSidebarRename(userKeys map[string]any) []port.KeyChange {
733+ const (
734+ actionsKey = "workspace.shortcuts.actions"
735+ legacyName = "toggle-favorites-systemview"
736+ newName = "toggle-favorites-sidebar"
737+ )
738+
739+ actionsValue, ok := userKeys[actionsKey]
740+ if !ok {
741+ return nil
742+ }
743+ actions, ok := m.toStringAnyMap(actionsValue)
744+ if !ok || actions[legacyName] == nil || actions[newName] != nil {
745+ return nil
746+ }
747+
748+ return []port.KeyChange{{
749+ Type: port.KeyChangeRenamed,
750+ OldKey: actionsKey + "." + legacyName,
751+ NewKey: actionsKey + "." + newName,
752+ OldValue: m.formatValue(actions[legacyName]),
753+ NewValue: m.formatValue(m.defaultConfig.Workspace.Shortcuts.Actions[newName]),
754+ }}
755+}
756+
733 func (m *Migrator) detectLegacyBrowsingContextRenames(rawUserKeys map[string]any) []port.KeyChange {757 func (m *Migrator) detectLegacyBrowsingContextRenames(rawUserKeys map[string]any) []port.KeyChange {
734 type renamePair struct {758 type renamePair struct {
735 oldKey string759 oldKey string
@@ -859,17 +883,11 @@ func (m *Migrator) detectMissingDefaultActions(userKeysWithValues map[string]any
859 }883 }
860 884
861 for actionName := range actionMap.actions {885 for actionName := range actionMap.actions {
862- if actionValue, exists := userActions[actionName]; exists {886+ if _, exists := userActions[actionName]; exists {
863- if m.isObsoleteDefaultAction(actionMap.key, actionName, actionValue) {
864- missing = append(missing, actionMap.key+"."+actionName)
865- }
866 continue887 continue
867 }888 }
868 legacyActionName := strings.ReplaceAll(actionName, "-", "_")889 legacyActionName := strings.ReplaceAll(actionName, "-", "_")
869- if actionValue, exists := userActions[legacyActionName]; exists {890+ if _, exists := userActions[legacyActionName]; exists {
870- if m.isObsoleteDefaultAction(actionMap.key, actionName, actionValue) {
871- missing = append(missing, actionMap.key+"."+actionName)
872- }
873 continue891 continue
874 }892 }
875 missing = append(missing, actionMap.key+"."+actionName)893 missing = append(missing, actionMap.key+"."+actionName)
@@ -894,18 +912,11 @@ func (m *Migrator) mergeMissingDefaultActions(rawConfig map[string]any) {
894 }912 }
895 913
896 for actionName, defaultAction := range actionMap.actions {914 for actionName, defaultAction := range actionMap.actions {
897- if actionValue, exists := userActions[actionName]; exists {915+ if _, exists := userActions[actionName]; exists {
898- if m.isObsoleteDefaultAction(actionMap.key, actionName, actionValue) {
899- userActions[actionName] = defaultAction
900- }
901 continue916 continue
902 }917 }
903 legacyActionName := strings.ReplaceAll(actionName, "-", "_")918 legacyActionName := strings.ReplaceAll(actionName, "-", "_")
904- if actionValue, exists := userActions[legacyActionName]; exists {919+ if _, exists := userActions[legacyActionName]; exists {
905- if m.isObsoleteDefaultAction(actionMap.key, actionName, actionValue) {
906- delete(userActions, legacyActionName)
907- userActions[actionName] = defaultAction
908- }
909 continue920 continue
910 }921 }
911 userActions[actionName] = defaultAction922 userActions[actionName] = defaultAction
@@ -915,15 +926,23 @@ func (m *Migrator) mergeMissingDefaultActions(rawConfig map[string]any) {
915 }926 }
916 }927 }
917 928
918-func (m *Migrator) isObsoleteDefaultAction(actionMapKey, actionName string, actionValue any) bool {929+func (m *Migrator) upgradeLegacyFavoritesSidebarDefault(rawConfig map[string]any) {
919- if actionMapKey != "workspace.shortcuts.actions" || actionName != "toggle-favorites-systemview" {930+ const (
920- return false931+ actionsKey = "workspace.shortcuts.actions"
921- }932+ actionName = "toggle-favorites-sidebar"
922- binding, ok := m.actionBindingFromAny(actionValue)933+ )
934+
935+ actionsValue := m.getNestedValue(rawConfig, actionsKey)
936+ actions, ok := m.toStringAnyMap(actionsValue)
923 if !ok {937 if !ok {
924- return false938+ return
939+ }
940+ binding, ok := m.actionBindingFromAny(actions[actionName])
941+ if !ok || !reflect.DeepEqual(binding, entity.ActionBinding{Keys: []string{}, Desc: "Toggle Favorites in right split"}) {
942+ return
925 }943 }
926- return len(binding.Keys) == 0 && strings.TrimSpace(binding.Desc) == "Toggle Favorites in right split"944+ actions[actionName] = m.defaultConfig.Workspace.Shortcuts.Actions[actionName]
945+ m.setNestedValue(rawConfig, actionsKey, actions)
927 }946 }
928 947
929 func (m *Migrator) actionBindingFromAny(value any) (entity.ActionBinding, bool) {948 func (m *Migrator) actionBindingFromAny(value any) (entity.ActionBinding, bool) {
M internal/infrastructure/config/migrate_test.go +56 -7
@@ -1283,7 +1283,50 @@ section:
12831283 })
12841284 }
12851285
1286-func TestMigrator_ReportsAndMigratesObsoleteFavoritesShortcutDefault(t *testing.T) {
1286+func TestMigrator_Migrate_RenamesCustomFavoritesSidebarShortcut(t *testing.T) {
1287+ home := t.TempDir()
1288+ t.Setenv("HOME", home)
1289+ t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
1290+ t.Setenv("XDG_DATA_HOME", filepath.Join(home, ".local", "share"))
1291+ t.Setenv("XDG_STATE_HOME", filepath.Join(home, ".local", "state"))
1292+ t.Setenv("XDG_CACHE_HOME", filepath.Join(home, ".cache"))
1293+
1294+ configFile, err := GetConfigFile()
1295+ require.NoError(t, err)
1296+ require.NoError(t, os.MkdirAll(filepath.Dir(configFile), 0o755))
1297+ require.NoError(t, os.WriteFile(configFile, []byte(`
1298+[workspace.shortcuts.actions.toggle-favorites-systemview]
1299+keys = []
1300+desc = "Custom favorites shortcut"
1301+`), 0o644))
1302+
1303+ migrator := NewMigrator()
1304+ result, err := migrator.CheckMigration()
1305+ require.NoError(t, err)
1306+ require.NotNil(t, result)
1307+ assert.Contains(t, result.MissingKeys, "workspace.shortcuts.actions.toggle-favorites-sidebar")
1308+
1309+ changes, err := migrator.DetectChanges()
1310+ require.NoError(t, err)
1311+ assertContainsChange(t, changes, port.KeyChangeRenamed,
1312+ "workspace.shortcuts.actions.toggle-favorites-systemview",
1313+ "workspace.shortcuts.actions.toggle-favorites-sidebar")
1314+
1315+ applied, err := migrator.Migrate()
1316+ require.NoError(t, err)
1317+ assert.Contains(t, applied, "workspace.shortcuts.actions.toggle-favorites-systemview -> workspace.shortcuts.actions.toggle-favorites-sidebar")
1318+
1319+ mgr, err := NewManager()
1320+ require.NoError(t, err)
1321+ require.NoError(t, mgr.Load())
1322+ _, ok := mgr.Get().Workspace.Shortcuts.Actions["toggle-favorites-systemview"]
1323+ assert.False(t, ok)
1324+ favorites := mgr.Get().Workspace.Shortcuts.Actions["toggle-favorites-sidebar"]
1325+ assert.Empty(t, favorites.Keys)
1326+ assert.Equal(t, "Custom favorites shortcut", favorites.Desc)
1327+}
1328+
1329+func TestMigrator_Migrate_UpgradesLegacyFavoritesSidebarDefault(t *testing.T) {
12871330 home := t.TempDir()
12881331 t.Setenv("HOME", home)
12891332 t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
@@ -1304,17 +1347,19 @@ desc = "Toggle Favorites in right split"
13041347 result, err := migrator.CheckMigration()
13051348 require.NoError(t, err)
13061349 require.NotNil(t, result)
1307- assert.Contains(t, result.MissingKeys, "workspace.shortcuts.actions.toggle-favorites-systemview")
1350+ assert.Contains(t, result.MissingKeys, "workspace.shortcuts.actions.toggle-favorites-sidebar")
13081351 assert.Contains(t, result.MissingKeys, "workspace.shortcuts.actions.toggle-current-page-favorite")
13091352
13101353 changes, err := migrator.DetectChanges()
13111354 require.NoError(t, err)
1312- assertContainsChange(t, changes, port.KeyChangeAdded, "", "workspace.shortcuts.actions.toggle-favorites-systemview")
1355+ assertContainsChange(t, changes, port.KeyChangeRenamed,
1356+ "workspace.shortcuts.actions.toggle-favorites-systemview",
1357+ "workspace.shortcuts.actions.toggle-favorites-sidebar")
13131358 assertContainsChange(t, changes, port.KeyChangeAdded, "", "workspace.shortcuts.actions.toggle-current-page-favorite")
13141359
13151360 applied, err := migrator.Migrate()
13161361 require.NoError(t, err)
1317- assert.Contains(t, applied, "workspace.shortcuts.actions.toggle-favorites-systemview")
1362+ assert.Contains(t, applied, "workspace.shortcuts.actions.toggle-favorites-systemview -> workspace.shortcuts.actions.toggle-favorites-sidebar")
13181363 assert.Contains(t, applied, "workspace.shortcuts.actions.toggle-current-page-favorite")
13191364
13201365 raw, err := migrator.readRawConfig(configFile)
@@ -1327,10 +1372,12 @@ desc = "Toggle Favorites in right split"
13271372 require.True(t, ok)
13281373
13291374 expectedActions := DefaultConfig().Workspace.Shortcuts.Actions
1330- favorites, ok := actions["toggle-favorites-systemview"].(map[string]any)
1375+ _, ok = actions["toggle-favorites-systemview"]
1376+ assert.False(t, ok)
1377+ favorites, ok := actions["toggle-favorites-sidebar"].(map[string]any)
13311378 require.True(t, ok)
13321379 assert.Equal(t, []any{"ctrl+b"}, favorites["keys"])
1333- assert.Equal(t, expectedActions["toggle-favorites-systemview"].Desc, favorites["desc"])
1380+ assert.Equal(t, expectedActions["toggle-favorites-sidebar"].Desc, favorites["desc"])
13341381 currentPage, ok := actions["toggle-current-page-favorite"].(map[string]any)
13351382 require.True(t, ok)
13361383 assert.Equal(t, []any{"ctrl+d"}, currentPage["keys"])
@@ -1341,6 +1388,8 @@ desc = "Toggle Favorites in right split"
13411388 require.NoError(t, mgr.Load())
13421389 cfg := mgr.Get()
13431390 require.NotNil(t, cfg)
1344- assert.Equal(t, []string{"ctrl+b"}, cfg.Workspace.Shortcuts.Actions["toggle-favorites-systemview"].Keys)
1391+ _, ok = cfg.Workspace.Shortcuts.Actions["toggle-favorites-systemview"]
1392+ assert.False(t, ok)
1393+ assert.Equal(t, []string{"ctrl+b"}, cfg.Workspace.Shortcuts.Actions["toggle-favorites-sidebar"].Keys)
13451394 assert.Equal(t, []string{"ctrl+d"}, cfg.Workspace.Shortcuts.Actions["toggle-current-page-favorite"].Keys)
13461395 }
@@ -1283,7 +1283,50 @@ section:
1283 })1283 })
1284 }1284 }
1285 1285
1286-func TestMigrator_ReportsAndMigratesObsoleteFavoritesShortcutDefault(t *testing.T) {1286+func TestMigrator_Migrate_RenamesCustomFavoritesSidebarShortcut(t *testing.T) {
1287+ home := t.TempDir()
1288+ t.Setenv("HOME", home)
1289+ t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
1290+ t.Setenv("XDG_DATA_HOME", filepath.Join(home, ".local", "share"))
1291+ t.Setenv("XDG_STATE_HOME", filepath.Join(home, ".local", "state"))
1292+ t.Setenv("XDG_CACHE_HOME", filepath.Join(home, ".cache"))
1293+
1294+ configFile, err := GetConfigFile()
1295+ require.NoError(t, err)
1296+ require.NoError(t, os.MkdirAll(filepath.Dir(configFile), 0o755))
1297+ require.NoError(t, os.WriteFile(configFile, []byte(`
1298+[workspace.shortcuts.actions.toggle-favorites-systemview]
1299+keys = []
1300+desc = "Custom favorites shortcut"
1301+`), 0o644))
1302+
1303+ migrator := NewMigrator()
1304+ result, err := migrator.CheckMigration()
1305+ require.NoError(t, err)
1306+ require.NotNil(t, result)
1307+ assert.Contains(t, result.MissingKeys, "workspace.shortcuts.actions.toggle-favorites-sidebar")
1308+
1309+ changes, err := migrator.DetectChanges()
1310+ require.NoError(t, err)
1311+ assertContainsChange(t, changes, port.KeyChangeRenamed,
1312+ "workspace.shortcuts.actions.toggle-favorites-systemview",
1313+ "workspace.shortcuts.actions.toggle-favorites-sidebar")
1314+
1315+ applied, err := migrator.Migrate()
1316+ require.NoError(t, err)
1317+ assert.Contains(t, applied, "workspace.shortcuts.actions.toggle-favorites-systemview -> workspace.shortcuts.actions.toggle-favorites-sidebar")
1318+
1319+ mgr, err := NewManager()
1320+ require.NoError(t, err)
1321+ require.NoError(t, mgr.Load())
1322+ _, ok := mgr.Get().Workspace.Shortcuts.Actions["toggle-favorites-systemview"]
1323+ assert.False(t, ok)
1324+ favorites := mgr.Get().Workspace.Shortcuts.Actions["toggle-favorites-sidebar"]
1325+ assert.Empty(t, favorites.Keys)
1326+ assert.Equal(t, "Custom favorites shortcut", favorites.Desc)
1327+}
1328+
1329+func TestMigrator_Migrate_UpgradesLegacyFavoritesSidebarDefault(t *testing.T) {
1287 home := t.TempDir()1330 home := t.TempDir()
1288 t.Setenv("HOME", home)1331 t.Setenv("HOME", home)
1289 t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))1332 t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
@@ -1304,17 +1347,19 @@ desc = "Toggle Favorites in right split"
1304 result, err := migrator.CheckMigration()1347 result, err := migrator.CheckMigration()
1305 require.NoError(t, err)1348 require.NoError(t, err)
1306 require.NotNil(t, result)1349 require.NotNil(t, result)
1307- assert.Contains(t, result.MissingKeys, "workspace.shortcuts.actions.toggle-favorites-systemview")1350+ assert.Contains(t, result.MissingKeys, "workspace.shortcuts.actions.toggle-favorites-sidebar")
1308 assert.Contains(t, result.MissingKeys, "workspace.shortcuts.actions.toggle-current-page-favorite")1351 assert.Contains(t, result.MissingKeys, "workspace.shortcuts.actions.toggle-current-page-favorite")
1309 1352
1310 changes, err := migrator.DetectChanges()1353 changes, err := migrator.DetectChanges()
1311 require.NoError(t, err)1354 require.NoError(t, err)
1312- assertContainsChange(t, changes, port.KeyChangeAdded, "", "workspace.shortcuts.actions.toggle-favorites-systemview")1355+ assertContainsChange(t, changes, port.KeyChangeRenamed,
1356+ "workspace.shortcuts.actions.toggle-favorites-systemview",
1357+ "workspace.shortcuts.actions.toggle-favorites-sidebar")
1313 assertContainsChange(t, changes, port.KeyChangeAdded, "", "workspace.shortcuts.actions.toggle-current-page-favorite")1358 assertContainsChange(t, changes, port.KeyChangeAdded, "", "workspace.shortcuts.actions.toggle-current-page-favorite")
1314 1359
1315 applied, err := migrator.Migrate()1360 applied, err := migrator.Migrate()
1316 require.NoError(t, err)1361 require.NoError(t, err)
1317- assert.Contains(t, applied, "workspace.shortcuts.actions.toggle-favorites-systemview")1362+ assert.Contains(t, applied, "workspace.shortcuts.actions.toggle-favorites-systemview -> workspace.shortcuts.actions.toggle-favorites-sidebar")
1318 assert.Contains(t, applied, "workspace.shortcuts.actions.toggle-current-page-favorite")1363 assert.Contains(t, applied, "workspace.shortcuts.actions.toggle-current-page-favorite")
1319 1364
1320 raw, err := migrator.readRawConfig(configFile)1365 raw, err := migrator.readRawConfig(configFile)
@@ -1327,10 +1372,12 @@ desc = "Toggle Favorites in right split"
1327 require.True(t, ok)1372 require.True(t, ok)
1328 1373
1329 expectedActions := DefaultConfig().Workspace.Shortcuts.Actions1374 expectedActions := DefaultConfig().Workspace.Shortcuts.Actions
1330- favorites, ok := actions["toggle-favorites-systemview"].(map[string]any)1375+ _, ok = actions["toggle-favorites-systemview"]
1376+ assert.False(t, ok)
1377+ favorites, ok := actions["toggle-favorites-sidebar"].(map[string]any)
1331 require.True(t, ok)1378 require.True(t, ok)
1332 assert.Equal(t, []any{"ctrl+b"}, favorites["keys"])1379 assert.Equal(t, []any{"ctrl+b"}, favorites["keys"])
1333- assert.Equal(t, expectedActions["toggle-favorites-systemview"].Desc, favorites["desc"])1380+ assert.Equal(t, expectedActions["toggle-favorites-sidebar"].Desc, favorites["desc"])
1334 currentPage, ok := actions["toggle-current-page-favorite"].(map[string]any)1381 currentPage, ok := actions["toggle-current-page-favorite"].(map[string]any)
1335 require.True(t, ok)1382 require.True(t, ok)
1336 assert.Equal(t, []any{"ctrl+d"}, currentPage["keys"])1383 assert.Equal(t, []any{"ctrl+d"}, currentPage["keys"])
@@ -1341,6 +1388,8 @@ desc = "Toggle Favorites in right split"
1341 require.NoError(t, mgr.Load())1388 require.NoError(t, mgr.Load())
1342 cfg := mgr.Get()1389 cfg := mgr.Get()
1343 require.NotNil(t, cfg)1390 require.NotNil(t, cfg)
1344- assert.Equal(t, []string{"ctrl+b"}, cfg.Workspace.Shortcuts.Actions["toggle-favorites-systemview"].Keys)1391+ _, ok = cfg.Workspace.Shortcuts.Actions["toggle-favorites-systemview"]
1392+ assert.False(t, ok)
1393+ assert.Equal(t, []string{"ctrl+b"}, cfg.Workspace.Shortcuts.Actions["toggle-favorites-sidebar"].Keys)
1345 assert.Equal(t, []string{"ctrl+d"}, cfg.Workspace.Shortcuts.Actions["toggle-current-page-favorite"].Keys)1394 assert.Equal(t, []string{"ctrl+d"}, cfg.Workspace.Shortcuts.Actions["toggle-current-page-favorite"].Keys)
1346 }1395 }
M internal/ui/browser_window_favorites_sidebar.go +3 -2
@@ -119,7 +119,8 @@ func (a *App) toggleCurrentPageFavoriteAction(ctx context.Context) error {
119119 if a == nil || a.deps == nil || a.deps.FavoritesUC == nil {
120120 return fmt.Errorf("favorites unavailable: usecase not configured")
121121 }
122- _, wv := a.activeWebViewForBrowserWindow(a.lastFocusedBrowserWindow())
122+ bw := a.lastFocusedBrowserWindow()
123+ _, wv := a.activeWebViewForBrowserWindow(bw)
123124 if wv == nil {
124125 return fmt.Errorf("favorites unavailable: no active webview")
125126 }
@@ -132,7 +133,7 @@ func (a *App) toggleCurrentPageFavoriteAction(ctx context.Context) error {
132133 return err
133134 }
134135 if result != nil && strings.TrimSpace(result.Message) != "" {
135- a.showToastOnLastFocusedBrowserWindow(ctx, result.Message, component.ToastSuccess,
136+ a.showToastOnBrowserWindow(ctx, bw, result.Message, component.ToastSuccess,
136137 component.WithDuration(component.ToastBriefDurationMs),
137138 component.WithPosition(component.ToastPositionBottomRight),
138139 )
@@ -119,7 +119,8 @@ func (a *App) toggleCurrentPageFavoriteAction(ctx context.Context) error {
119 if a == nil || a.deps == nil || a.deps.FavoritesUC == nil {119 if a == nil || a.deps == nil || a.deps.FavoritesUC == nil {
120 return fmt.Errorf("favorites unavailable: usecase not configured")120 return fmt.Errorf("favorites unavailable: usecase not configured")
121 }121 }
122- _, wv := a.activeWebViewForBrowserWindow(a.lastFocusedBrowserWindow())122+ bw := a.lastFocusedBrowserWindow()
123+ _, wv := a.activeWebViewForBrowserWindow(bw)
123 if wv == nil {124 if wv == nil {
124 return fmt.Errorf("favorites unavailable: no active webview")125 return fmt.Errorf("favorites unavailable: no active webview")
125 }126 }
@@ -132,7 +133,7 @@ func (a *App) toggleCurrentPageFavoriteAction(ctx context.Context) error {
132 return err133 return err
133 }134 }
134 if result != nil && strings.TrimSpace(result.Message) != "" {135 if result != nil && strings.TrimSpace(result.Message) != "" {
135- a.showToastOnLastFocusedBrowserWindow(ctx, result.Message, component.ToastSuccess,136+ a.showToastOnBrowserWindow(ctx, bw, result.Message, component.ToastSuccess,
136 component.WithDuration(component.ToastBriefDurationMs),137 component.WithDuration(component.ToastBriefDurationMs),
137 component.WithPosition(component.ToastPositionBottomRight),138 component.WithPosition(component.ToastPositionBottomRight),
138 )139 )
M internal/ui/dispatcher/keyboard.go +1 -1
@@ -271,7 +271,7 @@ func (d *KeyboardDispatcher) initActionHandlers() {
271271 }
272272 return d.onToggleHistorySidebar(ctx)
273273 },
274- input.ActionToggleFavoritesSystemView: func(ctx context.Context) error {
274+ input.ActionToggleFavoritesSidebar: func(ctx context.Context) error {
275275 if d.onToggleFavoritesSidebar == nil {
276276 return fmt.Errorf("favorites sidebar unavailable: toggle handler not wired")
277277 }
@@ -271,7 +271,7 @@ func (d *KeyboardDispatcher) initActionHandlers() {
271 }271 }
272 return d.onToggleHistorySidebar(ctx)272 return d.onToggleHistorySidebar(ctx)
273 },273 },
274- input.ActionToggleFavoritesSystemView: func(ctx context.Context) error {274+ input.ActionToggleFavoritesSidebar: func(ctx context.Context) error {
275 if d.onToggleFavoritesSidebar == nil {275 if d.onToggleFavoritesSidebar == nil {
276 return fmt.Errorf("favorites sidebar unavailable: toggle handler not wired")276 return fmt.Errorf("favorites sidebar unavailable: toggle handler not wired")
277 }277 }
M internal/ui/dispatcher/keyboard_test.go +3 -3
@@ -119,19 +119,19 @@ func TestKeyboardDispatcher_ToggleFavoritesSidebarCallsCallbackAndPropagatesErro
119119 ctx := context.Background()
120120 d := NewKeyboardDispatcher(ctx, &coordinator.WorkspaceCoordinator{}, &coordinator.NavigationCoordinator{}, nil, nil, KeyboardActions{}, func(context.Context) entity.PaneID { return "" })
121121
122- missingErr := d.Dispatch(ctx, input.ActionToggleFavoritesSystemView)
122+ missingErr := d.Dispatch(ctx, input.ActionToggleFavoritesSidebar)
123123 require.Error(t, missingErr)
124124 require.ErrorContains(t, missingErr, "favorites sidebar unavailable")
125125
126126 wantErr := fmt.Errorf("favorites failed")
127127 d.SetOnToggleFavoritesSidebar(func(context.Context) error { return wantErr })
128- err := d.Dispatch(ctx, input.ActionToggleFavoritesSystemView)
128+ err := d.Dispatch(ctx, input.ActionToggleFavoritesSidebar)
129129 require.Error(t, err)
130130 require.ErrorIs(t, err, wantErr)
131131
132132 var called bool
133133 d.SetOnToggleFavoritesSidebar(func(context.Context) error { called = true; return nil })
134- require.NoError(t, d.Dispatch(ctx, input.ActionToggleFavoritesSystemView))
134+ require.NoError(t, d.Dispatch(ctx, input.ActionToggleFavoritesSidebar))
135135 assert.True(t, called)
136136 }
137137
@@ -119,19 +119,19 @@ func TestKeyboardDispatcher_ToggleFavoritesSidebarCallsCallbackAndPropagatesErro
119 ctx := context.Background()119 ctx := context.Background()
120 d := NewKeyboardDispatcher(ctx, &coordinator.WorkspaceCoordinator{}, &coordinator.NavigationCoordinator{}, nil, nil, KeyboardActions{}, func(context.Context) entity.PaneID { return "" })120 d := NewKeyboardDispatcher(ctx, &coordinator.WorkspaceCoordinator{}, &coordinator.NavigationCoordinator{}, nil, nil, KeyboardActions{}, func(context.Context) entity.PaneID { return "" })
121 121
122- missingErr := d.Dispatch(ctx, input.ActionToggleFavoritesSystemView)122+ missingErr := d.Dispatch(ctx, input.ActionToggleFavoritesSidebar)
123 require.Error(t, missingErr)123 require.Error(t, missingErr)
124 require.ErrorContains(t, missingErr, "favorites sidebar unavailable")124 require.ErrorContains(t, missingErr, "favorites sidebar unavailable")
125 125
126 wantErr := fmt.Errorf("favorites failed")126 wantErr := fmt.Errorf("favorites failed")
127 d.SetOnToggleFavoritesSidebar(func(context.Context) error { return wantErr })127 d.SetOnToggleFavoritesSidebar(func(context.Context) error { return wantErr })
128- err := d.Dispatch(ctx, input.ActionToggleFavoritesSystemView)128+ err := d.Dispatch(ctx, input.ActionToggleFavoritesSidebar)
129 require.Error(t, err)129 require.Error(t, err)
130 require.ErrorIs(t, err, wantErr)130 require.ErrorIs(t, err, wantErr)
131 131
132 var called bool132 var called bool
133 d.SetOnToggleFavoritesSidebar(func(context.Context) error { called = true; return nil })133 d.SetOnToggleFavoritesSidebar(func(context.Context) error { called = true; return nil })
134- require.NoError(t, d.Dispatch(ctx, input.ActionToggleFavoritesSystemView))134+ require.NoError(t, d.Dispatch(ctx, input.ActionToggleFavoritesSidebar))
135 assert.True(t, called)135 assert.True(t, called)
136 }136 }
137 137
M internal/ui/input/global_shortcuts.go +3 -3
@@ -496,8 +496,8 @@ func globalShortcutActionMap() map[string]Action {
496496 "consume-or-expel-down": ActionConsumeOrExpelDown,
497497 "toggle_history_systemview": ActionToggleHistorySystemView,
498498 "toggle-history-systemview": ActionToggleHistorySystemView,
499- "toggle_favorites_systemview": ActionToggleFavoritesSystemView,
500- "toggle-favorites-systemview": ActionToggleFavoritesSystemView,
499+ "toggle_favorites_sidebar": ActionToggleFavoritesSidebar,
500+ "toggle-favorites-sidebar": ActionToggleFavoritesSidebar,
501501 "toggle_current_page_favorite": ActionToggleCurrentPageFavorite,
502502 "toggle-current-page-favorite": ActionToggleCurrentPageFavorite,
503503 "toggle_config_systemview": ActionToggleConfigSystemView,
@@ -712,7 +712,7 @@ func isRepeatedGlobalShortcutSuppressed(action Action) bool {
712712 ActionToggleFullscreen,
713713 ActionToggleFloatingPane,
714714 ActionToggleHistorySystemView,
715- ActionToggleFavoritesSystemView,
715+ ActionToggleFavoritesSidebar,
716716 ActionToggleCurrentPageFavorite,
717717 ActionToggleConfigSystemView,
718718 ActionCopyURL,
@@ -496,8 +496,8 @@ func globalShortcutActionMap() map[string]Action {
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_sidebar": ActionToggleFavoritesSidebar,
500- "toggle-favorites-systemview": ActionToggleFavoritesSystemView,500+ "toggle-favorites-sidebar": ActionToggleFavoritesSidebar,
501 "toggle_current_page_favorite": ActionToggleCurrentPageFavorite,501 "toggle_current_page_favorite": ActionToggleCurrentPageFavorite,
502 "toggle-current-page-favorite": ActionToggleCurrentPageFavorite,502 "toggle-current-page-favorite": ActionToggleCurrentPageFavorite,
503 "toggle_config_systemview": ActionToggleConfigSystemView,503 "toggle_config_systemview": ActionToggleConfigSystemView,
@@ -712,7 +712,7 @@ func isRepeatedGlobalShortcutSuppressed(action Action) bool {
712 ActionToggleFullscreen,712 ActionToggleFullscreen,
713 ActionToggleFloatingPane,713 ActionToggleFloatingPane,
714 ActionToggleHistorySystemView,714 ActionToggleHistorySystemView,
715- ActionToggleFavoritesSystemView,715+ ActionToggleFavoritesSidebar,
716 ActionToggleCurrentPageFavorite,716 ActionToggleCurrentPageFavorite,
717 ActionToggleConfigSystemView,717 ActionToggleConfigSystemView,
718 ActionCopyURL,718 ActionCopyURL,
M internal/ui/input/global_shortcuts_test.go +1 -1
@@ -230,7 +230,7 @@ func TestGlobalShortcutHandlerSuppressesRepeatedAdditionalOneShotUIActions(t *te
230230 ActionFindPrev,
231231 ActionOpenDevTools,
232232 ActionToggleFloatingPane,
233- ActionToggleFavoritesSystemView,
233+ ActionToggleFavoritesSidebar,
234234 ActionToggleCurrentPageFavorite,
235235 ActionToggleConfigSystemView,
236236 ActionCopyURL,
@@ -230,7 +230,7 @@ func TestGlobalShortcutHandlerSuppressesRepeatedAdditionalOneShotUIActions(t *te
230 ActionFindPrev,230 ActionFindPrev,
231 ActionOpenDevTools,231 ActionOpenDevTools,
232 ActionToggleFloatingPane,232 ActionToggleFloatingPane,
233- ActionToggleFavoritesSystemView,233+ ActionToggleFavoritesSidebar,
234 ActionToggleCurrentPageFavorite,234 ActionToggleCurrentPageFavorite,
235 ActionToggleConfigSystemView,235 ActionToggleConfigSystemView,
236 ActionCopyURL,236 ActionCopyURL,
M internal/ui/input/shortcuts.go +3 -3
@@ -189,7 +189,7 @@ const (
189189 ActionToggleFullscreen Action = "toggle_fullscreen"
190190 ActionToggleFloatingPane Action = "toggle_floating_pane"
191191 ActionToggleHistorySystemView Action = "toggle_history_systemview"
192- ActionToggleFavoritesSystemView Action = "toggle_favorites_systemview"
192+ ActionToggleFavoritesSidebar Action = "toggle_favorites_sidebar"
193193 ActionToggleCurrentPageFavorite Action = "toggle_current_page_favorite"
194194 ActionToggleConfigSystemView Action = "toggle_config_systemview"
195195
@@ -569,8 +569,8 @@ var configActionToAction = map[string]Action{
569569 "toggle-floating-pane": ActionToggleFloatingPane,
570570 "toggle_history_systemview": ActionToggleHistorySystemView,
571571 "toggle-history-systemview": ActionToggleHistorySystemView,
572- "toggle_favorites_systemview": ActionToggleFavoritesSystemView,
573- "toggle-favorites-systemview": ActionToggleFavoritesSystemView,
572+ "toggle_favorites_sidebar": ActionToggleFavoritesSidebar,
573+ "toggle-favorites-sidebar": ActionToggleFavoritesSidebar,
574574 "toggle_current_page_favorite": ActionToggleCurrentPageFavorite,
575575 "toggle-current-page-favorite": ActionToggleCurrentPageFavorite,
576576 "toggle_config_systemview": ActionToggleConfigSystemView,
@@ -189,7 +189,7 @@ const (
189 ActionToggleFullscreen Action = "toggle_fullscreen"189 ActionToggleFullscreen Action = "toggle_fullscreen"
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+ ActionToggleFavoritesSidebar Action = "toggle_favorites_sidebar"
193 ActionToggleCurrentPageFavorite Action = "toggle_current_page_favorite"193 ActionToggleCurrentPageFavorite Action = "toggle_current_page_favorite"
194 ActionToggleConfigSystemView Action = "toggle_config_systemview"194 ActionToggleConfigSystemView Action = "toggle_config_systemview"
195 195
@@ -569,8 +569,8 @@ var configActionToAction = map[string]Action{
569 "toggle-floating-pane": ActionToggleFloatingPane,569 "toggle-floating-pane": ActionToggleFloatingPane,
570 "toggle_history_systemview": ActionToggleHistorySystemView,570 "toggle_history_systemview": ActionToggleHistorySystemView,
571 "toggle-history-systemview": ActionToggleHistorySystemView,571 "toggle-history-systemview": ActionToggleHistorySystemView,
572- "toggle_favorites_systemview": ActionToggleFavoritesSystemView,572+ "toggle_favorites_sidebar": ActionToggleFavoritesSidebar,
573- "toggle-favorites-systemview": ActionToggleFavoritesSystemView,573+ "toggle-favorites-sidebar": ActionToggleFavoritesSidebar,
574 "toggle_current_page_favorite": ActionToggleCurrentPageFavorite,574 "toggle_current_page_favorite": ActionToggleCurrentPageFavorite,
575 "toggle-current-page-favorite": ActionToggleCurrentPageFavorite,575 "toggle-current-page-favorite": ActionToggleCurrentPageFavorite,
576 "toggle_config_systemview": ActionToggleConfigSystemView,576 "toggle_config_systemview": ActionToggleConfigSystemView,
M internal/ui/input/shortcuts_test.go +2 -2
@@ -401,7 +401,7 @@ func TestMapConfigAction_ToggleSystemViews(t *testing.T) {
401401 want Action
402402 }{
403403 {name: "toggle_history_systemview", want: ActionToggleHistorySystemView},
404- {name: "toggle_favorites_systemview", want: ActionToggleFavoritesSystemView},
404+ {name: "toggle_favorites_sidebar", want: ActionToggleFavoritesSidebar},
405405 {name: "toggle_current_page_favorite", want: ActionToggleCurrentPageFavorite},
406406 {name: "toggle_config_systemview", want: ActionToggleConfigSystemView},
407407 }
@@ -421,7 +421,7 @@ func TestMapConfigAction_ToggleSystemViewsHyphenAlias(t *testing.T) {
421421 want Action
422422 }{
423423 {name: "toggle-history-systemview", want: ActionToggleHistorySystemView},
424- {name: "toggle-favorites-systemview", want: ActionToggleFavoritesSystemView},
424+ {name: "toggle-favorites-sidebar", want: ActionToggleFavoritesSidebar},
425425 {name: "toggle-current-page-favorite", want: ActionToggleCurrentPageFavorite},
426426 {name: "toggle-config-systemview", want: ActionToggleConfigSystemView},
427427 }
@@ -401,7 +401,7 @@ func TestMapConfigAction_ToggleSystemViews(t *testing.T) {
401 want Action401 want Action
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_sidebar", want: ActionToggleFavoritesSidebar},
405 {name: "toggle_current_page_favorite", want: ActionToggleCurrentPageFavorite},405 {name: "toggle_current_page_favorite", want: ActionToggleCurrentPageFavorite},
406 {name: "toggle_config_systemview", want: ActionToggleConfigSystemView},406 {name: "toggle_config_systemview", want: ActionToggleConfigSystemView},
407 }407 }
@@ -421,7 +421,7 @@ func TestMapConfigAction_ToggleSystemViewsHyphenAlias(t *testing.T) {
421 want Action421 want Action
422 }{422 }{
423 {name: "toggle-history-systemview", want: ActionToggleHistorySystemView},423 {name: "toggle-history-systemview", want: ActionToggleHistorySystemView},
424- {name: "toggle-favorites-systemview", want: ActionToggleFavoritesSystemView},424+ {name: "toggle-favorites-sidebar", want: ActionToggleFavoritesSidebar},
425 {name: "toggle-current-page-favorite", want: ActionToggleCurrentPageFavorite},425 {name: "toggle-current-page-favorite", want: ActionToggleCurrentPageFavorite},
426 {name: "toggle-config-systemview", want: ActionToggleConfigSystemView},426 {name: "toggle-config-systemview", want: ActionToggleConfigSystemView},
427 }427 }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close