1package systemviews
2
3import (
4 "fmt"
5 "reflect"
6 "sort"
7 "strings"
8
9 "github.com/bnema/dumber/internal/application/dto"
10 "github.com/bnema/dumber/internal/application/port"
11)
12
13type configRenderData struct {
14 Config dto.SystemviewConfigPayload
15 Keybindings port.KeybindingsConfig
16 Notice string
17 Error string
18}
19
20// maxFlattenDepth caps recursive config flattening to avoid runaway nesting.
21const maxFlattenDepth = 16
22
23func configHTML(data configRenderData) string {
24 return mustRenderComponent(ConfigView(data))
25}
26
27func configKeybindingSummary(keybindings port.KeybindingsConfig) string {
28 if keybindings.Groups == nil {
29 return "Keybindings unavailable"
30 }
31 groups, bindings := summarizeKeybindings(keybindings)
32 return fmt.Sprintf(
33 "Keybindings loaded (%d %s, %d %s)",
34 groups,
35 plural(groups, "group"),
36 bindings,
37 plural(bindings, "binding"),
38 )
39}
40
41func configKeybindingGroups(keybindings port.KeybindingsConfig) []renderedKeybindingGroup {
42 if keybindings.Groups == nil {
43 return nil
44 }
45 return convertPortKeybindingGroups(keybindings.Groups)
46}
47
48func keybindingGroupTitle(group renderedKeybindingGroup) string {
49 title := strings.TrimSpace(group.DisplayName)
50 if title == "" {
51 title = strings.TrimSpace(group.Mode)
52 }
53 if title == "" {
54 return "Keybinding group"
55 }
56 return title
57}
58
59func keybindingGroupMeta(group renderedKeybindingGroup) string {
60 meta := make([]string, 0, 2)
61 if strings.TrimSpace(group.Mode) != "" {
62 meta = append(meta, "mode: "+group.Mode)
63 }
64 if strings.TrimSpace(group.Activation) != "" {
65 meta = append(meta, "activation: "+group.Activation)
66 }
67 return strings.Join(meta, " • ")
68}
69
70func keybindingDescription(binding renderedKeybinding) string {
71 description := strings.TrimSpace(binding.Description)
72 if description != "" {
73 return description
74 }
75 if strings.TrimSpace(binding.Action) != "" {
76 return strings.TrimSpace(binding.Action)
77 }
78 return "Unnamed binding"
79}
80
81func keybindingAction(binding renderedKeybinding) string {
82 action := strings.TrimSpace(binding.Action)
83 if action == "" {
84 return "binding"
85 }
86 return action
87}
88
89func keybindingIsCustom(binding renderedKeybinding) bool {
90 return binding.IsCustom || !sameStringSlice(binding.Keys, binding.DefaultKeys)
91}
92
93func keybindingStatus(binding renderedKeybinding) string {
94 if keybindingIsCustom(binding) {
95 return "custom"
96 }
97 return "default"
98}
99
100func keybindingKeysValue(keys []string) string {
101 return strings.Join(keys, ", ")
102}
103
104type searchShortcutRow struct {
105 Key string
106 URL string
107 Description string
108}
109
110func searchShortcutRows(shortcuts map[string]dto.SearchShortcut) []searchShortcutRow {
111 if len(shortcuts) == 0 {
112 return nil
113 }
114 keys := make([]string, 0, len(shortcuts))
115 for key := range shortcuts {
116 keys = append(keys, key)
117 }
118 sort.Strings(keys)
119 rows := make([]searchShortcutRow, 0, len(keys))
120 for _, key := range keys {
121 shortcut := shortcuts[key]
122 rows = append(rows, searchShortcutRow{
123 Key: key,
124 URL: shortcut.URL,
125 Description: shortcut.Description,
126 })
127 }
128 return rows
129}
130
131func formatConfigInt(value int) string {
132 return fmt.Sprintf("%d", value)
133}
134
135func formatConfigFloat(value float64) string {
136 return fmt.Sprintf("%g", value)
137}
138
139func colorSchemeSelected(current, option string) bool {
140 return strings.EqualFold(strings.TrimSpace(current), option)
141}
142
143func performanceProfileSelected(current, option string) bool {
144 return strings.EqualFold(strings.TrimSpace(current), option)
145}
146
147func engineIsWebKit(engine string) bool {
148 return strings.EqualFold(strings.TrimSpace(engine), "webkit")
149}
150
151func flattenValues(value any) []kvPair {
152 rows := make([]kvPair, 0)
153 flattenValue(&rows, "", reflect.ValueOf(value), 0)
154 return rows
155}
156
157func flattenValue(rows *[]kvPair, path string, value reflect.Value, depth int) {
158 if !value.IsValid() {
159 appendFlatValue(rows, path, "null")
160 return
161 }
162 if depth >= maxFlattenDepth {
163 appendFlatValue(rows, path, "...")
164 return
165 }
166
167 for value.Kind() == reflect.Interface {
168 if value.IsNil() {
169 appendFlatValue(rows, path, "null")
170 return
171 }
172 value = value.Elem()
173 }
174
175 for value.Kind() == reflect.Pointer {
176 if value.IsNil() {
177 appendFlatValue(rows, path, "null")
178 return
179 }
180 value = value.Elem()
181 }
182
183 switch value.Kind() {
184 case reflect.Struct:
185 flattenStructValue(rows, path, value, depth)
186 case reflect.Map:
187 flattenMapValue(rows, path, value, depth)
188 case reflect.Slice, reflect.Array:
189 flattenListValue(rows, path, value, depth)
190 default:
191 appendFlatValue(rows, path, formatFlatValue(value))
192 }
193}
194
195func flattenStructValue(rows *[]kvPair, path string, value reflect.Value, depth int) {
196 typeOfValue := value.Type()
197 for i := 0; i < value.NumField(); i++ {
198 field := typeOfValue.Field(i)
199 if !field.IsExported() {
200 continue
201 }
202 name := jsonFieldName(field)
203 if name == "-" {
204 continue
205 }
206 flattenValue(rows, childFlatPath(path, name), value.Field(i), depth+1)
207 }
208}
209
210func flattenMapValue(rows *[]kvPair, path string, value reflect.Value, depth int) {
211 if value.Len() == 0 {
212 appendFlatValue(rows, path, "{}")
213 return
214 }
215
216 keys := value.MapKeys()
217 sort.Slice(keys, func(i, j int) bool {
218 return mapKeyString(keys[i]) < mapKeyString(keys[j])
219 })
220
221 for _, key := range keys {
222 name := mapKeyString(key)
223 flattenValue(rows, childFlatPath(path, name), value.MapIndex(key), depth+1)
224 }
225}
226
227func flattenListValue(rows *[]kvPair, path string, value reflect.Value, depth int) {
228 if value.Len() == 0 {
229 appendFlatValue(rows, path, "[]")
230 return
231 }
232
233 for i := 0; i < value.Len(); i++ {
234 nextPath := fmt.Sprintf("%s[%d]", path, i)
235 if path == "" {
236 nextPath = fmt.Sprintf("[%d]", i)
237 }
238 flattenValue(rows, nextPath, value.Index(i), depth+1)
239 }
240}
241
242func childFlatPath(path, name string) string {
243 if path == "" {
244 return name
245 }
246 return path + "." + name
247}
248
249func appendFlatValue(rows *[]kvPair, path, value string) {
250 if strings.TrimSpace(path) == "" {
251 path = "value"
252 }
253 *rows = append(*rows, kvPair{Label: path, Value: value})
254}
255
256func formatFlatValue(value reflect.Value) string {
257 if !value.IsValid() {
258 return "null"
259 }
260
261 switch value.Kind() {
262 case reflect.String:
263 return value.String()
264 case reflect.Bool:
265 return fmt.Sprint(value.Bool())
266 case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
267 return fmt.Sprint(value.Int())
268 case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
269 return fmt.Sprint(value.Uint())
270 case reflect.Float32, reflect.Float64:
271 return fmt.Sprint(value.Float())
272 case reflect.Complex64, reflect.Complex128:
273 return fmt.Sprint(value.Complex())
274 default:
275 if !value.CanInterface() {
276 return "<unexported>"
277 }
278 return fmt.Sprint(value.Interface())
279 }
280}
281
282func jsonFieldName(field reflect.StructField) string {
283 name := field.Tag.Get("json")
284 if name == "" {
285 return field.Name
286 }
287 if idx := strings.Index(name, ","); idx >= 0 {
288 name = name[:idx]
289 }
290 if name == "" {
291 return field.Name
292 }
293 return name
294}
295
296func mapKeyString(value reflect.Value) string {
297 if value.Kind() == reflect.String {
298 return value.String()
299 }
300 if value.CanInterface() {
301 return fmt.Sprint(value.Interface())
302 }
303 return formatFlatValue(value)
304}
305
306func summarizeKeybindings(keybindings port.KeybindingsConfig) (groups, bindings int) {
307 for _, group := range keybindings.Groups {
308 groups++
309 bindings += len(group.Bindings)
310 }
311 return groups, bindings
312}
313
314func convertPortKeybindingGroups(groups []port.KeybindingGroup) []renderedKeybindingGroup {
315 rendered := make([]renderedKeybindingGroup, 0, len(groups))
316 for _, group := range groups {
317 rendered = append(rendered, renderedKeybindingGroup{
318 DisplayName: strings.TrimSpace(group.DisplayName),
319 Mode: strings.TrimSpace(group.Mode),
320 Activation: strings.TrimSpace(group.Activation),
321 Bindings: convertPortKeybindingEntries(group.Bindings),
322 })
323 }
324 return rendered
325}
326
327func convertPortKeybindingEntries(bindings []port.KeybindingEntry) []renderedKeybinding {
328 rendered := make([]renderedKeybinding, 0, len(bindings))
329 for _, binding := range bindings {
330 rendered = append(rendered, renderedKeybinding{
331 Description: strings.TrimSpace(binding.Description),
332 Action: strings.TrimSpace(binding.Action),
333 Keys: append([]string(nil), binding.Keys...),
334 DefaultKeys: append([]string(nil), binding.DefaultKeys...),
335 IsCustom: binding.IsCustom,
336 })
337 }
338 return rendered
339}
340
341type renderedKeybindingGroup struct {
342 DisplayName string
343 Mode string
344 Activation string
345 Bindings []renderedKeybinding
346}
347
348type renderedKeybinding struct {
349 Description string
350 Action string
351 Keys []string
352 DefaultKeys []string
353 IsCustom bool
354}
355
356func sameStringSlice(a, b []string) bool {
357 if len(a) != len(b) {
358 return false
359 }
360 for i := range a {
361 if a[i] != b[i] {
362 return false
363 }
364 }
365 return true
366}
367
368func plural(count int, singular string) string {
369 if count == 1 {
370 return singular
371 }
372 return singular + "s"
373}