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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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" | |||
| 71 | 71 | stack := cfg.Workspace.PaneMode.Actions["stack-pane"] | |
| 72 | 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 | + | } | |
| @@ -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) { | |||
| 58 | 58 | return nil, nil | |
| 59 | 59 | } | |
| 60 | 60 | ||
| 61 | - | // Get user-defined keys from the TOML file | |
| 62 | - | userKeys, err := m.getUserConfigKeys(configFile) | |
| 61 | + | userKeysWithValues, err := m.getUserConfigKeysWithValues(configFile) | |
| 63 | 62 | if err != nil { | |
| 64 | 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 | 71 | // Get all default keys | |
| 68 | 72 | defaultKeys := m.getAllDefaultKeys() | |
| 69 | 73 | ||
| 70 | 74 | // Find missing keys (in defaults but not in user config) | |
| 71 | 75 | missingKeys := m.findMissingKeys(defaultKeys, userKeys) | |
| 76 | + | missingKeys = appendUniqueKeys(missingKeys, m.detectMissingDefaultActions(userKeysWithValues)) | |
| 72 | 77 | ||
| 73 | 78 | if len(missingKeys) == 0 { | |
| 74 | 79 | return nil, nil | |
| @@ -854,11 +859,17 @@ func (m *Migrator) detectMissingDefaultActions(userKeysWithValues map[string]any | |||
| 854 | 859 | } | |
| 855 | 860 | ||
| 856 | 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 | 866 | continue | |
| 859 | 867 | } | |
| 860 | 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 | 873 | continue | |
| 863 | 874 | } | |
| 864 | 875 | missing = append(missing, actionMap.key+"."+actionName) | |
| @@ -883,11 +894,18 @@ func (m *Migrator) mergeMissingDefaultActions(rawConfig map[string]any) { | |||
| 883 | 894 | } | |
| 884 | 895 | ||
| 885 | 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 | 901 | continue | |
| 888 | 902 | } | |
| 889 | 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 | 909 | continue | |
| 892 | 910 | } | |
| 893 | 911 | 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 | 995 | func appendUniqueKeys(base, extra []string) []string { | |
| 901 | 996 | if len(extra) == 0 { | |
| 902 | 997 | return base | |
| @@ -58,17 +58,22 @@ func (m *Migrator) CheckMigration() (*port.MigrationResult, error) { | |||||
| 58 | return nil, nil | 58 | return nil, nil | ||
| 59 | } | 59 | } | ||
| 60 | 60 | ||||
| 61 | - | // Get user-defined keys from the TOML file | 61 | + | 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 keys | 71 | // 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, nil | 79 | 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 | continue | 866 | 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 | continue | 873 | 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 | continue | 901 | 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 | continue | 909 | continue | ||
| 892 | } | 910 | } | ||
| 893 | userActions[actionName] = defaultAction | 911 | 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 base | 997 | return base | ||
M internal/infrastructure/config/migrate_test.go +62 -0
| @@ -1282,3 +1282,65 @@ section: | |||
| 1282 | 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 | + | } | |
| @@ -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 { | |||
| 127 | 127 | if uri == "" { | |
| 128 | 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 | 131 | if err != nil { | |
| 132 | 132 | 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 | 140 | a.reloadVisibleFavoritesSidebars("current-page-favorite-toggle") | |
| 135 | 141 | return nil | |
| 136 | 142 | } | |
| @@ -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 err | 132 | 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 nil | 141 | return nil | ||
| 136 | } | 142 | } | ||