1package config
2
3import "math"
4
5const (
6 sectionWorkspace = "workspace"
7 sectionSession = "session"
8)
9
10// LegacyConfigTransformer implements port.ConfigTransformer.
11// It transforms old-format action bindings (slices) to new ActionBinding format.
12type LegacyConfigTransformer struct {
13 defaults *Config
14}
15
16// NewLegacyConfigTransformer creates a new transformer with defaults for description lookup.
17func NewLegacyConfigTransformer() *LegacyConfigTransformer {
18 return &LegacyConfigTransformer{defaults: DefaultConfig()}
19}
20
21// TransformLegacyActions converts old-format action bindings to new format.
22// This handles migration from:
23//
24// [workspace.pane_mode.actions]
25// "focus-left" = ["h", "arrowleft"]
26//
27// To:
28//
29// [workspace.pane_mode.actions.focus-left]
30// keys = ["h", "arrowleft"]
31// desc = "Focus pane to the left"
32func (t *LegacyConfigTransformer) TransformLegacyActions(rawConfig map[string]any) {
33 actionPaths := [][]string{
34 {"workspace", "pane_mode", "actions"},
35 {"workspace", "tab_mode", "actions"},
36 {"workspace", "resize_mode", "actions"},
37 {"workspace", "shortcuts", "actions"},
38 {"session", "session_mode", "actions"},
39 }
40
41 for _, path := range actionPaths {
42 t.transformActionSection(rawConfig, path)
43 }
44}
45
46// TransformLegacyEngineConfig removes deprecated engine config keys.
47func (t *LegacyConfigTransformer) TransformLegacyEngineConfig(rawConfig map[string]any) {
48 adaptiveEnabled, hasAdaptive := nestedConfigBoolValue(rawConfig, "engine", "cef", "adaptive_windowless_frame_rate")
49 t.transformLegacyEngineConfig(rawConfig, hasAdaptive, adaptiveEnabled)
50}
51
52func (t *LegacyConfigTransformer) TransformLegacyEngineConfigWithExplicitAdaptive(
53 rawConfig map[string]any,
54 hasAdaptive, adaptiveEnabled bool,
55) {
56 t.transformLegacyEngineConfig(rawConfig, hasAdaptive, adaptiveEnabled)
57}
58
59func (*LegacyConfigTransformer) transformLegacyEngineConfig(rawConfig map[string]any, hasAdaptive, adaptiveEnabled bool) {
60 engine, ok := rawConfig["engine"].(map[string]any)
61 if !ok {
62 return
63 }
64
65 cef, ok := engine["cef"].(map[string]any)
66 if !ok {
67 return
68 }
69
70 delete(cef, "enable_context_menu_handler")
71 migrateLegacyCEFWindowlessFrameRateDefault(cef, hasAdaptive, adaptiveEnabled)
72}
73
74func migrateLegacyCEFWindowlessFrameRateDefault(cef map[string]any, hasAdaptive, adaptiveEnabled bool) {
75 if hasAdaptive && !adaptiveEnabled {
76 return
77 }
78 frameRate, ok := int64ConfigValue(cef["windowless_frame_rate"])
79 if !ok || frameRate != defaultCEFWindowlessFrameRate {
80 return
81 }
82 cef["adaptive_windowless_frame_rate"] = true
83 cef["windowless_frame_rate"] = 0
84 if _, hasMax := cef["windowless_frame_rate_max"]; !hasMax {
85 cef["windowless_frame_rate_max"] = defaultCEFWindowlessFrameRateMax
86 }
87}
88
89func nestedConfigBoolValue(rawConfig map[string]any, path ...string) (bool, bool) {
90 current := rawConfig
91 for i, key := range path {
92 value, ok := current[key]
93 if !ok {
94 return false, false
95 }
96 if i == len(path)-1 {
97 parsed, parsedOK := value.(bool)
98 return parsed, parsedOK
99 }
100 next, ok := value.(map[string]any)
101 if !ok {
102 return false, false
103 }
104 current = next
105 }
106 return false, false
107}
108
109func int64ConfigValue(value any) (int64, bool) {
110 switch v := value.(type) {
111 case int:
112 return int64(v), true
113 case int8:
114 return int64(v), true
115 case int16:
116 return int64(v), true
117 case int32:
118 return int64(v), true
119 case int64:
120 return v, true
121 case uint:
122 return int64(v), true
123 case uint8:
124 return int64(v), true
125 case uint16:
126 return int64(v), true
127 case uint32:
128 return int64(v), true
129 case uint64:
130 if v > math.MaxInt64 {
131 return 0, false
132 }
133 return int64(v), true
134 default:
135 return 0, false
136 }
137}
138
139func (t *LegacyConfigTransformer) transformActionSection(rawConfig map[string]any, path []string) {
140 // Navigate to the parent of the actions section
141 current := rawConfig
142 for _, key := range path[:len(path)-1] {
143 next, ok := current[key].(map[string]any)
144 if !ok {
145 return // Path doesn't exist
146 }
147 current = next
148 }
149
150 actionsKey := path[len(path)-1]
151 actionsRaw, ok := current[actionsKey]
152 if !ok {
153 return
154 }
155
156 actions, ok := actionsRaw.(map[string]any)
157 if !ok {
158 return
159 }
160
161 // Check each action and transform if needed
162 for actionName, actionValue := range actions {
163 // If value is a slice (old format), convert to ActionBinding format
164 if slice, ok := actionValue.([]any); ok {
165 keys := make([]string, 0, len(slice))
166 for _, v := range slice {
167 if s, ok := v.(string); ok {
168 keys = append(keys, s)
169 }
170 }
171
172 actions[actionName] = map[string]any{
173 "keys": keys,
174 "desc": t.getDefaultDesc(path, actionName),
175 }
176 }
177 }
178}
179
180// TransformLegacyPopupsToBrowsingContexts maps old workspace.popups.* keys to
181// workspace.browsing_contexts.* when the new section is absent. This allows
182// existing user configs with [workspace.popups] to be silently migrated.
183func (t *LegacyConfigTransformer) TransformLegacyPopupsToBrowsingContexts(rawConfig map[string]any) {
184 workspace, ok := rawConfig["workspace"].(map[string]any)
185 if !ok {
186 return
187 }
188
189 // Only transform if the new key does not already exist.
190 if _, hasNew := workspace["browsing_contexts"]; hasNew {
191 return
192 }
193
194 t.ForceLegacyPopupsToBrowsingContexts(rawConfig)
195}
196
197// ForceLegacyPopupsToBrowsingContexts maps old workspace.popups.* keys to
198// workspace.browsing_contexts.* even when the new section already exists in the
199// raw map. This is used by the loader because Viper's AllSettings includes
200// defaults for the new section that do not indicate user intent.
201func (*LegacyConfigTransformer) ForceLegacyPopupsToBrowsingContexts(rawConfig map[string]any) {
202 workspace, ok := rawConfig["workspace"].(map[string]any)
203 if !ok {
204 return
205 }
206
207 oldPopups, ok := workspace["popups"]
208 if !ok {
209 return
210 }
211
212 // Move the popups map to browsing_contexts and delete the old key.
213 workspace["browsing_contexts"] = oldPopups
214 delete(workspace, "popups")
215}
216
217func (t *LegacyConfigTransformer) getDefaultDesc(path []string, actionName string) string {
218 switch {
219 case path[0] == sectionWorkspace && path[1] == "pane_mode":
220 if b, ok := t.defaults.Workspace.PaneMode.Actions[actionName]; ok {
221 return b.Desc
222 }
223 case path[0] == sectionWorkspace && path[1] == "tab_mode":
224 if b, ok := t.defaults.Workspace.TabMode.Actions[actionName]; ok {
225 return b.Desc
226 }
227 case path[0] == sectionWorkspace && path[1] == "resize_mode":
228 if b, ok := t.defaults.Workspace.ResizeMode.Actions[actionName]; ok {
229 return b.Desc
230 }
231 case path[0] == sectionWorkspace && path[1] == "shortcuts":
232 if b, ok := t.defaults.Workspace.Shortcuts.Actions[actionName]; ok {
233 return b.Desc
234 }
235 case path[0] == sectionSession && path[1] == "session_mode":
236 if b, ok := t.defaults.Session.SessionMode.Actions[actionName]; ok {
237 return b.Desc
238 }
239 }
240 return ""
241}