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