1package modes
2
3import (
4 "strings"
5
6 "github.com/mattn/go-runewidth"
7
8 "github.com/patriceckhart/zot/packages/tui"
9)
10
11type settingsDialog struct {
12 active bool
13 title string
14 items []settingsItem
15 cursor int
16 selecting bool
17 optionCursor int
18 parentItems []settingsItem
19 parentCursor int
20}
21
22type settingsItem struct {
23 key string
24 label string
25 desc string
26 value bool
27 options []settingsOption
28 children []settingsItem
29 picker bool
30 choice int
31 disabled bool
32 hint string
33}
34
35type settingsOption struct {
36 value string
37 label string
38 desc string
39}
40
41type settingsAction struct {
42 Toggle bool
43 Key string
44 Value bool
45 StringValue string
46 ModelShortcutSlot int
47 Close bool
48}
49
50func newSettingsDialog() *settingsDialog { return &settingsDialog{} }
51
52func (d *settingsDialog) Open(items []settingsItem) bool {
53 if len(items) == 0 {
54 return false
55 }
56 d.title = "settings"
57 d.items = items
58 d.cursor = 0
59 d.selecting = false
60 d.optionCursor = 0
61 d.parentItems = nil
62 d.parentCursor = 0
63 d.active = true
64 return true
65}
66
67func (d *settingsDialog) Close() {
68 d.active = false
69 d.selecting = false
70 d.parentItems = nil
71}
72func (d *settingsDialog) Active() bool { return d != nil && d.active }
73
74func (d *settingsDialog) HandleKey(k tui.Key) settingsAction {
75 if d.selecting {
76 return d.handleOptionKey(k)
77 }
78 switch k.Kind {
79 case tui.KeyUp:
80 if d.cursor > 0 {
81 d.cursor--
82 }
83 case tui.KeyDown:
84 if d.cursor < len(d.items)-1 {
85 d.cursor++
86 }
87 case tui.KeyBackspace:
88 if len(d.items) > 0 {
89 it := d.items[d.cursor]
90 if strings.HasPrefix(it.key, "quick_model_") {
91 return settingsAction{Toggle: true, Key: it.key, StringValue: ""}
92 }
93 }
94 case tui.KeyEsc:
95 if len(d.parentItems) > 0 {
96 d.items = d.parentItems
97 d.cursor = d.parentCursor
98 d.parentItems = nil
99 d.parentCursor = 0
100 d.title = "settings"
101 return settingsAction{}
102 }
103 d.Close()
104 return settingsAction{Close: true}
105 case tui.KeyEnter:
106 return d.toggleCurrent()
107 case tui.KeyRune:
108 if k.Rune == ' ' {
109 return d.toggleCurrent()
110 }
111 }
112 return settingsAction{}
113}
114
115func (d *settingsDialog) handleOptionKey(k tui.Key) settingsAction {
116 it := d.items[d.cursor]
117 switch k.Kind {
118 case tui.KeyUp:
119 if d.optionCursor > 0 {
120 d.optionCursor--
121 }
122 case tui.KeyDown:
123 if d.optionCursor < len(it.options)-1 {
124 d.optionCursor++
125 }
126 case tui.KeyEsc:
127 d.selecting = false
128 case tui.KeyEnter:
129 return d.selectCurrentOption()
130 case tui.KeyRune:
131 if k.Rune == ' ' {
132 return d.selectCurrentOption()
133 }
134 }
135 return settingsAction{}
136}
137
138func (d *settingsDialog) toggleCurrent() settingsAction {
139 if len(d.items) == 0 {
140 d.Close()
141 return settingsAction{Close: true}
142 }
143 it := d.items[d.cursor]
144 if it.disabled {
145 return settingsAction{}
146 }
147 if it.picker {
148 slotText := strings.TrimPrefix(it.key, "quick_model_")
149 slot := 0
150 for _, r := range slotText {
151 if r < '0' || r > '9' {
152 slot = 0
153 break
154 }
155 slot = slot*10 + int(r-'0')
156 }
157 return settingsAction{ModelShortcutSlot: slot}
158 }
159 if len(it.children) > 0 {
160 d.parentItems = d.items
161 d.parentCursor = d.cursor
162 d.items = it.children
163 d.cursor = 0
164 d.optionCursor = 0
165 d.title = "settings: " + it.label
166 return settingsAction{}
167 }
168 if len(it.options) > 0 {
169 d.optionCursor = it.choice
170 if d.optionCursor < 0 || d.optionCursor >= len(it.options) {
171 d.optionCursor = 0
172 }
173 d.selecting = true
174 return settingsAction{}
175 }
176 it.value = !it.value
177 d.items[d.cursor] = it
178 return settingsAction{Toggle: true, Key: it.key, Value: it.value}
179}
180
181func (d *settingsDialog) selectCurrentOption() settingsAction {
182 if len(d.items) == 0 {
183 d.Close()
184 return settingsAction{Close: true}
185 }
186 it := d.items[d.cursor]
187 if len(it.options) == 0 {
188 d.selecting = false
189 return settingsAction{}
190 }
191 if d.optionCursor < 0 || d.optionCursor >= len(it.options) {
192 d.optionCursor = 0
193 }
194 it.choice = d.optionCursor
195 d.items[d.cursor] = it
196 d.selecting = false
197 return settingsAction{Toggle: true, Key: it.key, StringValue: it.options[it.choice].value}
198}
199
200func (d *settingsDialog) Render(th tui.Theme, width int) []string {
201 if !d.Active() {
202 return nil
203 }
204 if d.selecting {
205 return d.renderOptions(th, width)
206 }
207 var lines []string
208 lines = append(lines, frameHeader(th, d.title, width))
209 if len(d.parentItems) > 0 {
210 lines = append(lines, th.FG256(th.Muted, "change with enter/space, esc to go back:"))
211 } else {
212 lines = append(lines, th.FG256(th.Muted, "change with enter/space, esc to close:"))
213 }
214 for i, it := range d.items {
215 box := "[ ]"
216 if it.value {
217 box = "[✓]"
218 }
219 plain := " " + box + " " + it.label
220 if it.picker || len(it.children) > 0 {
221 box = "[→]"
222 plain = " " + box + " " + it.label
223 } else if len(it.options) > 0 {
224 box = "[→]"
225 if it.choice < 0 || it.choice >= len(it.options) {
226 it.choice = 0
227 }
228 plain = " " + box + " " + it.label + ": " + it.options[it.choice].label
229 }
230 if it.hint != "" {
231 plain += " " + th.FG256(th.Muted, "("+it.hint+")")
232 }
233 if it.disabled {
234 lines = append(lines, th.FG256(th.Muted, plain))
235 } else if i == d.cursor {
236 lines = append(lines, th.PadHighlight(plain, width))
237 } else {
238 lines = append(lines, plain)
239 }
240 if it.desc != "" {
241 for _, desc := range wrapSettingDescription(it.desc, width, 6) {
242 lines = append(lines, th.FG256(th.Muted, desc))
243 }
244 }
245 }
246 lines = append(lines, frameRule(th, width))
247 return lines
248}
249
250func (d *settingsDialog) renderOptions(th tui.Theme, width int) []string {
251 if len(d.items) == 0 || d.cursor < 0 || d.cursor >= len(d.items) {
252 d.selecting = false
253 return d.Render(th, width)
254 }
255 it := d.items[d.cursor]
256 lines := []string{frameHeader(th, "settings: "+it.label, width)}
257 if it.desc != "" {
258 lines = append(lines, th.FG256(th.Muted, it.desc))
259 }
260 lines = append(lines, th.FG256(th.Muted, "select with enter/space, esc to go back:"))
261 for idx, opt := range it.options {
262 marker := " "
263 if idx == it.choice {
264 marker = "✓ "
265 }
266 plain := " " + marker + opt.label
267 if idx == d.optionCursor {
268 lines = append(lines, th.PadHighlight(plain, width))
269 } else {
270 lines = append(lines, plain)
271 }
272 if opt.desc != "" {
273 for _, desc := range wrapSettingDescription(opt.desc, width, 6) {
274 lines = append(lines, th.FG256(th.Muted, desc))
275 }
276 }
277 }
278 lines = append(lines, frameRule(th, width))
279 return lines
280}
281
282func wrapSettingDescription(desc string, width, indent int) []string {
283 prefix := strings.Repeat(" ", indent)
284 limit := width - indent
285 if limit < 20 {
286 limit = 20
287 }
288 words := strings.Fields(desc)
289 if len(words) == 0 {
290 return nil
291 }
292 var lines []string
293 line := words[0]
294 for _, word := range words[1:] {
295 candidate := line + " " + word
296 if runewidth.StringWidth(candidate) <= limit {
297 line = candidate
298 continue
299 }
300 lines = append(lines, prefix+line)
301 line = word
302 }
303 lines = append(lines, prefix+line)
304 return lines
305}