v0.33.2 57f21d6

config.go RawHistoryFolder

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}

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