v0.29.0 21b3150

transform_legacy.go RawHistoryFolder

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}

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