1package styles
2
3import (
4 "fmt"
5 "path/filepath"
6 "strings"
7
8 "github.com/charmbracelet/lipgloss"
9
10 "github.com/bnema/dumber/internal/application/port"
11)
12
13// ConfigRenderer renders config status messages with styled output.
14type ConfigRenderer struct {
15 theme *Theme
16}
17
18// NewConfigRenderer creates a new config renderer with the given theme.
19func NewConfigRenderer(theme *Theme) *ConfigRenderer {
20 return &ConfigRenderer{theme: theme}
21}
22
23// RenderConfigInfo renders the config file info with status.
24func (r *ConfigRenderer) RenderConfigInfo(path string, changeCount int) string {
25 iconStyle := lipgloss.NewStyle().Foreground(r.theme.Accent)
26 pathStyle := r.theme.Subtle
27
28 var status string
29 if changeCount > 0 {
30 countStyle := lipgloss.NewStyle().Foreground(r.theme.Warning)
31 status = fmt.Sprintf("\n %s %s changes detected",
32 iconStyle.Render(IconInfo),
33 countStyle.Render(fmt.Sprintf("%d", changeCount)),
34 )
35 }
36
37 return fmt.Sprintf(
38 "\n %s Config %s%s\n",
39 iconStyle.Render(IconConfig),
40 pathStyle.Render(path),
41 status,
42 )
43}
44
45// RenderMissingKeys renders the list of missing keys with their types and default values.
46func (r *ConfigRenderer) RenderMissingKeys(keys []port.KeyInfo) string {
47 if len(keys) == 0 {
48 return ""
49 }
50
51 iconStyle := lipgloss.NewStyle().Foreground(r.theme.Accent)
52 keyStyle := r.theme.Highlight
53 typeStyle := r.theme.Subtle
54 valueStyle := lipgloss.NewStyle().Foreground(r.theme.Text)
55
56 var sb strings.Builder
57 fmt.Fprintf(&sb, "\n Missing settings (%d):\n", len(keys))
58
59 for _, key := range keys {
60 fmt.Fprintf(&sb,
61 " %s %s\n Type: %s | Default: %s\n",
62 iconStyle.Render(IconCursor),
63 keyStyle.Render(key.Key),
64 typeStyle.Render(key.Type),
65 valueStyle.Render(key.DefaultValue),
66 )
67 }
68
69 return sb.String()
70}
71
72// RenderChanges renders the list of detected changes (added, renamed, removed).
73func (r *ConfigRenderer) RenderChanges(changes []port.KeyChange) string {
74 if len(changes) == 0 {
75 return ""
76 }
77
78 addStyle := lipgloss.NewStyle().Foreground(r.theme.Success)
79 renameStyle := lipgloss.NewStyle().Foreground(r.theme.Warning)
80 removeStyle := lipgloss.NewStyle().Foreground(r.theme.Error)
81 keyStyle := r.theme.Highlight
82 valueStyle := r.theme.Subtle
83
84 // Group changes by type
85 var added, renamed, removed []port.KeyChange
86 for _, c := range changes {
87 switch c.Type {
88 case port.KeyChangeAdded:
89 added = append(added, c)
90 case port.KeyChangeRenamed:
91 renamed = append(renamed, c)
92 case port.KeyChangeRemoved:
93 removed = append(removed, c)
94 }
95 }
96
97 var sb strings.Builder
98
99 // Show renamed keys first (most important for user to understand)
100 if len(renamed) > 0 {
101 fmt.Fprintf(&sb, "\n Renamed settings (%d):\n", len(renamed))
102 for _, c := range renamed {
103 fmt.Fprintf(&sb,
104 " %s %s %s %s\n Value: %s\n",
105 renameStyle.Render("~"),
106 valueStyle.Render(c.OldKey),
107 renameStyle.Render("→"),
108 keyStyle.Render(c.NewKey),
109 valueStyle.Render(c.OldValue),
110 )
111 }
112 }
113
114 // Show new settings
115 if len(added) > 0 {
116 fmt.Fprintf(&sb, "\n New settings (%d):\n", len(added))
117 for _, c := range added {
118 fmt.Fprintf(&sb,
119 " %s %s\n Default: %s\n",
120 addStyle.Render("+"),
121 keyStyle.Render(c.NewKey),
122 valueStyle.Render(c.NewValue),
123 )
124 }
125 }
126
127 // Show deprecated settings (will be removed)
128 if len(removed) > 0 {
129 fmt.Fprintf(&sb, "\n Deprecated settings (%d):\n", len(removed))
130 for _, c := range removed {
131 fmt.Fprintf(&sb,
132 " %s %s %s\n",
133 removeStyle.Render("-"),
134 valueStyle.Render(c.OldKey),
135 removeStyle.Render("(will be removed)"),
136 )
137 }
138 }
139
140 return sb.String()
141}
142
143// RenderChangesSummary renders a summary of changes for the config info header.
144func (r *ConfigRenderer) RenderChangesSummary(changes []port.KeyChange) string {
145 var added, renamed, removed int
146 for _, c := range changes {
147 switch c.Type {
148 case port.KeyChangeAdded:
149 added++
150 case port.KeyChangeRenamed:
151 renamed++
152 case port.KeyChangeRemoved:
153 removed++
154 }
155 }
156
157 iconStyle := lipgloss.NewStyle().Foreground(r.theme.Accent)
158 addStyle := lipgloss.NewStyle().Foreground(r.theme.Success)
159 renameStyle := lipgloss.NewStyle().Foreground(r.theme.Warning)
160 removeStyle := lipgloss.NewStyle().Foreground(r.theme.Error)
161
162 var parts []string
163 if renamed > 0 {
164 parts = append(parts, renameStyle.Render(fmt.Sprintf("%d renamed", renamed)))
165 }
166 if added > 0 {
167 parts = append(parts, addStyle.Render(fmt.Sprintf("%d new", added)))
168 }
169 if removed > 0 {
170 parts = append(parts, removeStyle.Render(fmt.Sprintf("%d deprecated", removed)))
171 }
172
173 if len(parts) == 0 {
174 return ""
175 }
176
177 return fmt.Sprintf("\n %s %s", iconStyle.Render(IconInfo), strings.Join(parts, ", "))
178}
179
180// RenderMigrationSuccess renders the success message after migration.
181func (r *ConfigRenderer) RenderMigrationSuccess(count int, path string) string {
182 iconStyle := lipgloss.NewStyle().Foreground(r.theme.Success)
183 countStyle := r.theme.Highlight
184 pathStyle := r.theme.Subtle
185
186 // Get just the filename from the path
187 filename := filepath.Base(path)
188
189 return fmt.Sprintf(
190 "\n %s Applied %s changes to %s\n",
191 iconStyle.Render(IconCheck),
192 countStyle.Render(fmt.Sprintf("%d", count)),
193 pathStyle.Render(filename),
194 )
195}
196
197// RenderUpToDate renders the "config is up to date" message.
198func (r *ConfigRenderer) RenderUpToDate(path string) string {
199 iconStyle := lipgloss.NewStyle().Foreground(r.theme.Success)
200 pathStyle := r.theme.Subtle
201
202 return fmt.Sprintf(
203 "\n %s Config %s\n %s Config is up to date\n",
204 iconStyle.Render(IconConfig),
205 pathStyle.Render(path),
206 iconStyle.Render(IconCheck),
207 )
208}
209
210// RenderError renders an error message.
211func (r *ConfigRenderer) RenderError(err error) string {
212 iconStyle := lipgloss.NewStyle().Foreground(r.theme.Error)
213
214 return fmt.Sprintf(
215 "\n %s Config error: %v\n",
216 iconStyle.Render(IconX),
217 err,
218 )
219}
220
221// RenderChecking renders the "checking config..." message with spinner.
222func (*ConfigRenderer) RenderChecking(spinner string) string {
223 return fmt.Sprintf("\n %s Checking config...\n", spinner)
224}
225
226// RenderMigrateHint renders a hint to run the migrate command.
227func (r *ConfigRenderer) RenderMigrateHint() string {
228 hintStyle := r.theme.Subtle
229
230 return fmt.Sprintf(
231 "\n %s\n",
232 hintStyle.Render("Run 'dumber config migrate' to add missing defaults."),
233 )
234}
235
236// RenderNoConfigFile renders message when config file doesn't exist yet.
237func (r *ConfigRenderer) RenderNoConfigFile(path string) string {
238 iconStyle := lipgloss.NewStyle().Foreground(r.theme.Accent)
239 pathStyle := r.theme.Subtle
240 hintStyle := r.theme.Subtle
241
242 return fmt.Sprintf(
243 "\n %s Config %s\n %s\n",
244 iconStyle.Render(IconConfig),
245 pathStyle.Render(path),
246 hintStyle.Render("Config file will be created on first run with all defaults."),
247 )
248}
249
250// RenderEngineMigrationSuccess renders the success message after migrating legacy
251// sections to [engine]/[engine.webkit].
252func (r *ConfigRenderer) RenderEngineMigrationSuccess() string {
253 iconStyle := lipgloss.NewStyle().Foreground(r.theme.Success)
254
255 return fmt.Sprintf(
256 " %s Migrated legacy sections to [engine]/[engine.webkit]\n",
257 iconStyle.Render(IconCheck),
258 )
259}
260
261// RenderOpening renders a brief "opening config" message for `dumber config open`.
262func (r *ConfigRenderer) RenderOpening(path, editor string) string {
263 iconStyle := r.theme.Subtle
264 pathStyle := r.theme.Highlight
265 editorStyle := r.theme.Subtle
266
267 return fmt.Sprintf(
268 "%s Opening %s with %s",
269 iconStyle.Render(IconCursor),
270 pathStyle.Render(path),
271 editorStyle.Render(editor),
272 )
273}