detached 3721706

fix(config): migrate favorites sidebar shortcut

commit
37217069c98349aba20ab90e194e9c17642e1201
author
brice · · 2026-06-28 18:32 UTC
committer
brice · · 2026-06-28 18:37 UTC
parent
abd344b
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +203 -7
M internal/infrastructure/config/loader_actions_test.go +33 -0
@@ -71,3 +71,36 @@ desc = "Custom stack pane"
7171 stack := cfg.Workspace.PaneMode.Actions["stack-pane"]
7272 assert.Equal(t, "Custom stack pane", stack.Desc)
7373 }
74+
75+func TestManagerLoad_UpgradesObsoleteFavoritesShortcutDefaultInMemory(t *testing.T) {
76+ home := t.TempDir()
77+ t.Setenv("HOME", home)
78+ t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
79+ t.Setenv("XDG_DATA_HOME", filepath.Join(home, ".local", "share"))
80+ t.Setenv("XDG_STATE_HOME", filepath.Join(home, ".local", "state"))
81+ t.Setenv("XDG_CACHE_HOME", filepath.Join(home, ".cache"))
82+
83+ configFile, err := GetConfigFile()
84+ require.NoError(t, err)
85+ require.NoError(t, os.MkdirAll(filepath.Dir(configFile), 0o755))
86+ require.NoError(t, os.WriteFile(configFile, []byte(`
87+[workspace.shortcuts.actions.toggle-favorites-systemview]
88+keys = []
89+desc = "Toggle Favorites in right split"
90+`), 0o644))
91+
92+ mgr, err := NewManager()
93+ require.NoError(t, err)
94+ require.NoError(t, mgr.Load())
95+
96+ cfg := mgr.Get()
97+ require.NotNil(t, cfg)
98+ 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)
106+}
@@ -71,3 +71,36 @@ desc = "Custom stack pane"
71 stack := cfg.Workspace.PaneMode.Actions["stack-pane"]71 stack := cfg.Workspace.PaneMode.Actions["stack-pane"]
72 assert.Equal(t, "Custom stack pane", stack.Desc)72 assert.Equal(t, "Custom stack pane", stack.Desc)
73 }73 }
74+
75+func TestManagerLoad_UpgradesObsoleteFavoritesShortcutDefaultInMemory(t *testing.T) {
76+ home := t.TempDir()
77+ t.Setenv("HOME", home)
78+ t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
79+ t.Setenv("XDG_DATA_HOME", filepath.Join(home, ".local", "share"))
80+ t.Setenv("XDG_STATE_HOME", filepath.Join(home, ".local", "state"))
81+ t.Setenv("XDG_CACHE_HOME", filepath.Join(home, ".cache"))
82+
83+ configFile, err := GetConfigFile()
84+ require.NoError(t, err)
85+ require.NoError(t, os.MkdirAll(filepath.Dir(configFile), 0o755))
86+ require.NoError(t, os.WriteFile(configFile, []byte(`
87+[workspace.shortcuts.actions.toggle-favorites-systemview]
88+keys = []
89+desc = "Toggle Favorites in right split"
90+`), 0o644))
91+
92+ mgr, err := NewManager()
93+ require.NoError(t, err)
94+ require.NoError(t, mgr.Load())
95+
96+ cfg := mgr.Get()
97+ require.NotNil(t, cfg)
98+ 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)
106+}
M internal/infrastructure/config/migrate.go +101 -6
@@ -58,17 +58,22 @@ func (m *Migrator) CheckMigration() (*port.MigrationResult, error) {
5858 return nil, nil
5959 }
6060
61- // Get user-defined keys from the TOML file
62- userKeys, err := m.getUserConfigKeys(configFile)
61+ userKeysWithValues, err := m.getUserConfigKeysWithValues(configFile)
6362 if err != nil {
6463 return nil, fmt.Errorf("failed to parse user config: %w", err)
6564 }
6665
66+ userKeys := make(map[string]bool, len(userKeysWithValues))
67+ for key := range userKeysWithValues {
68+ userKeys[key] = true
69+ }
70+
6771 // Get all default keys
6872 defaultKeys := m.getAllDefaultKeys()
6973
7074 // Find missing keys (in defaults but not in user config)
7175 missingKeys := m.findMissingKeys(defaultKeys, userKeys)
76+ missingKeys = appendUniqueKeys(missingKeys, m.detectMissingDefaultActions(userKeysWithValues))
7277
7378 if len(missingKeys) == 0 {
7479 return nil, nil
@@ -854,11 +859,17 @@ func (m *Migrator) detectMissingDefaultActions(userKeysWithValues map[string]any
854859 }
855860
856861 for actionName := range actionMap.actions {
857- if _, exists := userActions[actionName]; exists {
862+ if actionValue, exists := userActions[actionName]; exists {
863+ if m.isObsoleteDefaultAction(actionMap.key, actionName, actionValue) {
864+ missing = append(missing, actionMap.key+"."+actionName)
865+ }
858866 continue
859867 }
860868 legacyActionName := strings.ReplaceAll(actionName, "-", "_")
861- if _, exists := userActions[legacyActionName]; exists {
869+ if actionValue, exists := userActions[legacyActionName]; exists {
870+ if m.isObsoleteDefaultAction(actionMap.key, actionName, actionValue) {
871+ missing = append(missing, actionMap.key+"."+actionName)
872+ }
862873 continue
863874 }
864875 missing = append(missing, actionMap.key+"."+actionName)
@@ -883,11 +894,18 @@ func (m *Migrator) mergeMissingDefaultActions(rawConfig map[string]any) {
883894 }
884895
885896 for actionName, defaultAction := range actionMap.actions {
886- if _, exists := userActions[actionName]; exists {
897+ if actionValue, exists := userActions[actionName]; exists {
898+ if m.isObsoleteDefaultAction(actionMap.key, actionName, actionValue) {
899+ userActions[actionName] = defaultAction
900+ }
887901 continue
888902 }
889903 legacyActionName := strings.ReplaceAll(actionName, "-", "_")
890- if _, exists := userActions[legacyActionName]; exists {
904+ if actionValue, exists := userActions[legacyActionName]; exists {
905+ if m.isObsoleteDefaultAction(actionMap.key, actionName, actionValue) {
906+ delete(userActions, legacyActionName)
907+ userActions[actionName] = defaultAction
908+ }
891909 continue
892910 }
893911 userActions[actionName] = defaultAction
@@ -897,6 +915,83 @@ func (m *Migrator) mergeMissingDefaultActions(rawConfig map[string]any) {
897915 }
898916 }
899917
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)
923+ if !ok {
924+ return false
925+ }
926+ return len(binding.Keys) == 0 && strings.TrimSpace(binding.Desc) == "Toggle Favorites in right split"
927+}
928+
929+func (m *Migrator) actionBindingFromAny(value any) (entity.ActionBinding, bool) {
930+ switch v := value.(type) {
931+ case entity.ActionBinding:
932+ return v, true
933+ case map[string]any:
934+ return actionBindingFromMap(v)
935+ default:
936+ mapping, ok := m.toStringAnyMap(value)
937+ if !ok {
938+ return entity.ActionBinding{}, false
939+ }
940+ return actionBindingFromMap(mapping)
941+ }
942+}
943+
944+func actionBindingFromMap(m map[string]any) (entity.ActionBinding, bool) {
945+ var binding entity.ActionBinding
946+ if keysValue, exists := m["keys"]; exists {
947+ keys, ok := stringSliceFromAny(keysValue)
948+ if !ok {
949+ return entity.ActionBinding{}, false
950+ }
951+ binding.Keys = keys
952+ }
953+ if descValue, exists := m["desc"]; exists {
954+ desc, ok := descValue.(string)
955+ if !ok {
956+ return entity.ActionBinding{}, false
957+ }
958+ binding.Desc = desc
959+ }
960+ return binding, true
961+}
962+
963+func stringSliceFromAny(value any) ([]string, bool) {
964+ switch v := value.(type) {
965+ case []string:
966+ return append([]string(nil), v...), true
967+ case []any:
968+ out := make([]string, 0, len(v))
969+ for _, item := range v {
970+ s, ok := item.(string)
971+ if !ok {
972+ return nil, false
973+ }
974+ out = append(out, s)
975+ }
976+ return out, true
977+ default:
978+ rv := reflect.ValueOf(value)
979+ if rv.Kind() != reflect.Slice {
980+ return nil, false
981+ }
982+ out := make([]string, 0, rv.Len())
983+ for i := 0; i < rv.Len(); i++ {
984+ item := rv.Index(i).Interface()
985+ s, ok := item.(string)
986+ if !ok {
987+ return nil, false
988+ }
989+ out = append(out, s)
990+ }
991+ return out, true
992+ }
993+}
994+
900995 func appendUniqueKeys(base, extra []string) []string {
901996 if len(extra) == 0 {
902997 return base
@@ -58,17 +58,22 @@ func (m *Migrator) CheckMigration() (*port.MigrationResult, error) {
58 return nil, nil58 return nil, nil
59 }59 }
60 60
61- // Get user-defined keys from the TOML file61+ userKeysWithValues, err := m.getUserConfigKeysWithValues(configFile)
62- userKeys, err := m.getUserConfigKeys(configFile)
63 if err != nil {62 if err != nil {
64 return nil, fmt.Errorf("failed to parse user config: %w", err)63 return nil, fmt.Errorf("failed to parse user config: %w", err)
65 }64 }
66 65
66+ userKeys := make(map[string]bool, len(userKeysWithValues))
67+ for key := range userKeysWithValues {
68+ userKeys[key] = true
69+ }
70+
67 // Get all default keys71 // Get all default keys
68 defaultKeys := m.getAllDefaultKeys()72 defaultKeys := m.getAllDefaultKeys()
69 73
70 // Find missing keys (in defaults but not in user config)74 // Find missing keys (in defaults but not in user config)
71 missingKeys := m.findMissingKeys(defaultKeys, userKeys)75 missingKeys := m.findMissingKeys(defaultKeys, userKeys)
76+ missingKeys = appendUniqueKeys(missingKeys, m.detectMissingDefaultActions(userKeysWithValues))
72 77
73 if len(missingKeys) == 0 {78 if len(missingKeys) == 0 {
74 return nil, nil79 return nil, nil
@@ -854,11 +859,17 @@ func (m *Migrator) detectMissingDefaultActions(userKeysWithValues map[string]any
854 }859 }
855 860
856 for actionName := range actionMap.actions {861 for actionName := range actionMap.actions {
857- if _, exists := userActions[actionName]; exists {862+ if actionValue, exists := userActions[actionName]; exists {
863+ if m.isObsoleteDefaultAction(actionMap.key, actionName, actionValue) {
864+ missing = append(missing, actionMap.key+"."+actionName)
865+ }
858 continue866 continue
859 }867 }
860 legacyActionName := strings.ReplaceAll(actionName, "-", "_")868 legacyActionName := strings.ReplaceAll(actionName, "-", "_")
861- if _, exists := userActions[legacyActionName]; exists {869+ if actionValue, exists := userActions[legacyActionName]; exists {
870+ if m.isObsoleteDefaultAction(actionMap.key, actionName, actionValue) {
871+ missing = append(missing, actionMap.key+"."+actionName)
872+ }
862 continue873 continue
863 }874 }
864 missing = append(missing, actionMap.key+"."+actionName)875 missing = append(missing, actionMap.key+"."+actionName)
@@ -883,11 +894,18 @@ func (m *Migrator) mergeMissingDefaultActions(rawConfig map[string]any) {
883 }894 }
884 895
885 for actionName, defaultAction := range actionMap.actions {896 for actionName, defaultAction := range actionMap.actions {
886- if _, exists := userActions[actionName]; exists {897+ if actionValue, exists := userActions[actionName]; exists {
898+ if m.isObsoleteDefaultAction(actionMap.key, actionName, actionValue) {
899+ userActions[actionName] = defaultAction
900+ }
887 continue901 continue
888 }902 }
889 legacyActionName := strings.ReplaceAll(actionName, "-", "_")903 legacyActionName := strings.ReplaceAll(actionName, "-", "_")
890- if _, exists := userActions[legacyActionName]; exists {904+ if actionValue, exists := userActions[legacyActionName]; exists {
905+ if m.isObsoleteDefaultAction(actionMap.key, actionName, actionValue) {
906+ delete(userActions, legacyActionName)
907+ userActions[actionName] = defaultAction
908+ }
891 continue909 continue
892 }910 }
893 userActions[actionName] = defaultAction911 userActions[actionName] = defaultAction
@@ -897,6 +915,83 @@ func (m *Migrator) mergeMissingDefaultActions(rawConfig map[string]any) {
897 }915 }
898 }916 }
899 917
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)
923+ if !ok {
924+ return false
925+ }
926+ return len(binding.Keys) == 0 && strings.TrimSpace(binding.Desc) == "Toggle Favorites in right split"
927+}
928+
929+func (m *Migrator) actionBindingFromAny(value any) (entity.ActionBinding, bool) {
930+ switch v := value.(type) {
931+ case entity.ActionBinding:
932+ return v, true
933+ case map[string]any:
934+ return actionBindingFromMap(v)
935+ default:
936+ mapping, ok := m.toStringAnyMap(value)
937+ if !ok {
938+ return entity.ActionBinding{}, false
939+ }
940+ return actionBindingFromMap(mapping)
941+ }
942+}
943+
944+func actionBindingFromMap(m map[string]any) (entity.ActionBinding, bool) {
945+ var binding entity.ActionBinding
946+ if keysValue, exists := m["keys"]; exists {
947+ keys, ok := stringSliceFromAny(keysValue)
948+ if !ok {
949+ return entity.ActionBinding{}, false
950+ }
951+ binding.Keys = keys
952+ }
953+ if descValue, exists := m["desc"]; exists {
954+ desc, ok := descValue.(string)
955+ if !ok {
956+ return entity.ActionBinding{}, false
957+ }
958+ binding.Desc = desc
959+ }
960+ return binding, true
961+}
962+
963+func stringSliceFromAny(value any) ([]string, bool) {
964+ switch v := value.(type) {
965+ case []string:
966+ return append([]string(nil), v...), true
967+ case []any:
968+ out := make([]string, 0, len(v))
969+ for _, item := range v {
970+ s, ok := item.(string)
971+ if !ok {
972+ return nil, false
973+ }
974+ out = append(out, s)
975+ }
976+ return out, true
977+ default:
978+ rv := reflect.ValueOf(value)
979+ if rv.Kind() != reflect.Slice {
980+ return nil, false
981+ }
982+ out := make([]string, 0, rv.Len())
983+ for i := 0; i < rv.Len(); i++ {
984+ item := rv.Index(i).Interface()
985+ s, ok := item.(string)
986+ if !ok {
987+ return nil, false
988+ }
989+ out = append(out, s)
990+ }
991+ return out, true
992+ }
993+}
994+
900 func appendUniqueKeys(base, extra []string) []string {995 func appendUniqueKeys(base, extra []string) []string {
901 if len(extra) == 0 {996 if len(extra) == 0 {
902 return base997 return base
M internal/infrastructure/config/migrate_test.go +62 -0
@@ -1282,3 +1282,65 @@ section:
12821282 assert.InDelta(t, 42.0, section["number"].(float64), 0.001)
12831283 })
12841284 }
1285+
1286+func TestMigrator_ReportsAndMigratesObsoleteFavoritesShortcutDefault(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 = "Toggle Favorites in right split"
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-systemview")
1308+ assert.Contains(t, result.MissingKeys, "workspace.shortcuts.actions.toggle-current-page-favorite")
1309+
1310+ changes, err := migrator.DetectChanges()
1311+ require.NoError(t, err)
1312+ assertContainsChange(t, changes, port.KeyChangeAdded, "", "workspace.shortcuts.actions.toggle-favorites-systemview")
1313+ assertContainsChange(t, changes, port.KeyChangeAdded, "", "workspace.shortcuts.actions.toggle-current-page-favorite")
1314+
1315+ applied, err := migrator.Migrate()
1316+ require.NoError(t, err)
1317+ assert.Contains(t, applied, "workspace.shortcuts.actions.toggle-favorites-systemview")
1318+ assert.Contains(t, applied, "workspace.shortcuts.actions.toggle-current-page-favorite")
1319+
1320+ raw, err := migrator.readRawConfig(configFile)
1321+ require.NoError(t, err)
1322+ workspace, ok := raw["workspace"].(map[string]any)
1323+ require.True(t, ok)
1324+ shortcuts, ok := workspace["shortcuts"].(map[string]any)
1325+ require.True(t, ok)
1326+ actions, ok := shortcuts["actions"].(map[string]any)
1327+ require.True(t, ok)
1328+
1329+ expectedActions := DefaultConfig().Workspace.Shortcuts.Actions
1330+ favorites, ok := actions["toggle-favorites-systemview"].(map[string]any)
1331+ require.True(t, ok)
1332+ assert.Equal(t, []any{"ctrl+b"}, favorites["keys"])
1333+ assert.Equal(t, expectedActions["toggle-favorites-systemview"].Desc, favorites["desc"])
1334+ currentPage, ok := actions["toggle-current-page-favorite"].(map[string]any)
1335+ require.True(t, ok)
1336+ assert.Equal(t, []any{"ctrl+d"}, currentPage["keys"])
1337+ assert.Equal(t, expectedActions["toggle-current-page-favorite"].Desc, currentPage["desc"])
1338+
1339+ mgr, err := NewManager()
1340+ require.NoError(t, err)
1341+ require.NoError(t, mgr.Load())
1342+ cfg := mgr.Get()
1343+ require.NotNil(t, cfg)
1344+ assert.Equal(t, []string{"ctrl+b"}, cfg.Workspace.Shortcuts.Actions["toggle-favorites-systemview"].Keys)
1345+ assert.Equal(t, []string{"ctrl+d"}, cfg.Workspace.Shortcuts.Actions["toggle-current-page-favorite"].Keys)
1346+}
@@ -1282,3 +1282,65 @@ section:
1282 assert.InDelta(t, 42.0, section["number"].(float64), 0.001)1282 assert.InDelta(t, 42.0, section["number"].(float64), 0.001)
1283 })1283 })
1284 }1284 }
1285+
1286+func TestMigrator_ReportsAndMigratesObsoleteFavoritesShortcutDefault(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 = "Toggle Favorites in right split"
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-systemview")
1308+ assert.Contains(t, result.MissingKeys, "workspace.shortcuts.actions.toggle-current-page-favorite")
1309+
1310+ changes, err := migrator.DetectChanges()
1311+ require.NoError(t, err)
1312+ assertContainsChange(t, changes, port.KeyChangeAdded, "", "workspace.shortcuts.actions.toggle-favorites-systemview")
1313+ assertContainsChange(t, changes, port.KeyChangeAdded, "", "workspace.shortcuts.actions.toggle-current-page-favorite")
1314+
1315+ applied, err := migrator.Migrate()
1316+ require.NoError(t, err)
1317+ assert.Contains(t, applied, "workspace.shortcuts.actions.toggle-favorites-systemview")
1318+ assert.Contains(t, applied, "workspace.shortcuts.actions.toggle-current-page-favorite")
1319+
1320+ raw, err := migrator.readRawConfig(configFile)
1321+ require.NoError(t, err)
1322+ workspace, ok := raw["workspace"].(map[string]any)
1323+ require.True(t, ok)
1324+ shortcuts, ok := workspace["shortcuts"].(map[string]any)
1325+ require.True(t, ok)
1326+ actions, ok := shortcuts["actions"].(map[string]any)
1327+ require.True(t, ok)
1328+
1329+ expectedActions := DefaultConfig().Workspace.Shortcuts.Actions
1330+ favorites, ok := actions["toggle-favorites-systemview"].(map[string]any)
1331+ require.True(t, ok)
1332+ assert.Equal(t, []any{"ctrl+b"}, favorites["keys"])
1333+ assert.Equal(t, expectedActions["toggle-favorites-systemview"].Desc, favorites["desc"])
1334+ currentPage, ok := actions["toggle-current-page-favorite"].(map[string]any)
1335+ require.True(t, ok)
1336+ assert.Equal(t, []any{"ctrl+d"}, currentPage["keys"])
1337+ assert.Equal(t, expectedActions["toggle-current-page-favorite"].Desc, currentPage["desc"])
1338+
1339+ mgr, err := NewManager()
1340+ require.NoError(t, err)
1341+ require.NoError(t, mgr.Load())
1342+ cfg := mgr.Get()
1343+ require.NotNil(t, cfg)
1344+ assert.Equal(t, []string{"ctrl+b"}, cfg.Workspace.Shortcuts.Actions["toggle-favorites-systemview"].Keys)
1345+ assert.Equal(t, []string{"ctrl+d"}, cfg.Workspace.Shortcuts.Actions["toggle-current-page-favorite"].Keys)
1346+}
M internal/ui/browser_window_favorites_sidebar.go +7 -1
@@ -127,10 +127,16 @@ func (a *App) toggleCurrentPageFavoriteAction(ctx context.Context) error {
127127 if uri == "" {
128128 return fmt.Errorf("favorites unavailable: active page has no URI")
129129 }
130- _, err := a.deps.FavoritesUC.Toggle(ctx, uri, wv.Title())
130+ result, err := a.deps.FavoritesUC.Toggle(ctx, uri, wv.Title())
131131 if err != nil {
132132 return err
133133 }
134+ if result != nil && strings.TrimSpace(result.Message) != "" {
135+ a.showToastOnLastFocusedBrowserWindow(ctx, result.Message, component.ToastSuccess,
136+ component.WithDuration(component.ToastBriefDurationMs),
137+ component.WithPosition(component.ToastPositionBottomRight),
138+ )
139+ }
134140 a.reloadVisibleFavoritesSidebars("current-page-favorite-toggle")
135141 return nil
136142 }
@@ -127,10 +127,16 @@ func (a *App) toggleCurrentPageFavoriteAction(ctx context.Context) error {
127 if uri == "" {127 if uri == "" {
128 return fmt.Errorf("favorites unavailable: active page has no URI")128 return fmt.Errorf("favorites unavailable: active page has no URI")
129 }129 }
130- _, err := a.deps.FavoritesUC.Toggle(ctx, uri, wv.Title())130+ result, err := a.deps.FavoritesUC.Toggle(ctx, uri, wv.Title())
131 if err != nil {131 if err != nil {
132 return err132 return err
133 }133 }
134+ if result != nil && strings.TrimSpace(result.Message) != "" {
135+ a.showToastOnLastFocusedBrowserWindow(ctx, result.Message, component.ToastSuccess,
136+ component.WithDuration(component.ToastBriefDurationMs),
137+ component.WithPosition(component.ToastPositionBottomRight),
138+ )
139+ }
134 a.reloadVisibleFavoritesSidebars("current-page-favorite-toggle")140 a.reloadVisibleFavoritesSidebars("current-page-favorite-toggle")
135 return nil141 return nil
136 }142 }

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