1package config
2
3import (
4 "os"
5 "path/filepath"
6
7 "github.com/bnema/dumber/internal/domain/entity"
8)
9
10// Default configuration constants
11const (
12 // History defaults
13 defaultMaxHistoryEntries = 10000 // entries
14 defaultRetentionDays = 365 // 1 year
15
16 // Dmenu defaults
17 defaultMaxHistoryDays = 30 // days
18
19 // Logging defaults
20 defaultMaxLogAgeDays = 7 // days
21 defaultMaxLogFiles = 100 // session log files
22
23 // Appearance defaults
24 defaultSidebarWidth = 320 // px, clamped to [280, 380]
25 defaultFontSize = 16 // points
26 defaultExternalThemeProvider = "noctalia"
27 defaultExternalThemeFormat = "colors-json"
28 defaultExternalThemeColorsFilename = "colors.json"
29 defaultExternalThemeTemplateFormat = "dumber-json"
30 defaultExternalThemeTemplateFilename = "noctalia-theme.json"
31
32 // Workspace defaults
33 defaultNewPaneURL = "about:blank"
34
35 // Omnibox defaults
36 defaultOmniboxInitialBehavior = OmniboxInitialBehaviorRecent
37 defaultOmniboxMostVisitedDays = 30
38 defaultOmniboxAutoOpenOnNewPane = false
39
40 // Workspace defaults
41 defaultPaneActivationShortcut = "ctrl+p"
42 defaultPaneTimeoutMilliseconds = 3000
43 defaultTabActivationShortcut = "ctrl+t"
44 defaultTabTimeoutMilliseconds = 3000
45 defaultResizeActivationShortcut = "ctrl+n"
46 defaultResizeTimeoutMilliseconds = 3000
47 defaultResizeStepPercent = 5.0
48 defaultResizeMinPanePercent = 10.0
49 defaultTabBarPosition = "bottom"
50 defaultPopupPlacement = "right"
51 defaultFloatingPaneWidthPct = 0.82
52 defaultFloatingPaneHeightPct = 0.72
53
54 // Session defaults
55 defaultSessionActivationShortcut = "ctrl+o"
56 defaultSessionTimeoutMilliseconds = 3000
57 defaultSnapshotIntervalMs = 5000
58 defaultMaxExitedSessions = 50
59 defaultMaxExitedSessionAgeDays = 7
60
61 // Workspace styling defaults
62 // Active pane border (overlay)
63 defaultBorderWidth = 1
64 defaultBorderColor = "@theme_selected_bg_color"
65
66 // Mode border width (consolidated for all modes)
67 defaultModeBorderWidth = 4
68
69 // Mode colors (used for both borders and toaster)
70 defaultPaneModeColor = "#4A90E2" // Blue for pane mode
71 defaultTabModeColor = "#FFA500" // Orange for tab mode
72 defaultSessionModeColor = "#9B59B6" // Purple for session mode
73 defaultResizeModeColor = "#00D4AA" // Cyan/teal for resize mode
74
75 // Mode indicator toaster
76 defaultModeIndicatorToasterEnabled = true
77
78 // Other styling
79 defaultTransitionDuration = 120
80 defaultUIScale = 1.0 // UI scale multiplier (1.0 = 100%, 1.2 = 120%)
81
82 // Performance defaults
83 defaultZoomCacheSize = 256 // domains to cache (~20KB memory)
84 defaultWebViewPoolPrewarmCount = 4 // WebViews to pre-create at startup
85 defaultCEFWindowlessFrameRate = 60 // static OSR frame rate when adaptive CEF pacing is disabled
86 defaultCEFWindowlessFrameRateMax = 240 // adaptive OSR frame-rate hard cap
87 defaultCEFScrollMultiplier = 1.0
88 defaultCEFScrollPreciseMultiplier = 2.5
89 defaultCEFScrollMaxDelta = 0
90 defaultCEFTouchpadNavigation = true
91 // Require a deliberate swipe before history navigation so ordinary
92 // horizontal scrolling is unlikely to cross the commit threshold.
93 defaultCEFTouchpadNavigationDelta = 320.0
94 defaultCEFTouchpadNavigationRatio = 0.5
95
96 // Skia threading defaults (0 = unset, -1 = unset for GPU threads)
97 defaultSkiaCPUPaintingThreads = 0
98 defaultSkiaGPUPaintingThreads = -1 // -1 means unset; 0 would disable GPU tile painting
99)
100
101func defaultBrowsingContextConfig() BrowsingContextConfig {
102 return BrowsingContextConfig{
103 Behavior: PopupBehaviorSplit,
104 Placement: defaultPopupPlacement,
105 OpenInNewPane: true,
106 FollowPaneContext: true,
107 BlankTargetBehavior: "stacked",
108 EnableSmartDetection: true,
109 OAuthAutoClose: true,
110 }
111}
112
113// getDefaultLogDir returns the default log directory, falls back to empty string on error
114func getDefaultLogDir() string {
115 logDir, err := GetLogDir()
116 if err != nil {
117 return ""
118 }
119 return logDir
120}
121
122// getDefaultExternalThemePath returns Noctalia's native colors.json path.
123func getDefaultExternalThemePath() string {
124 return getDefaultExternalThemePathForFormat(defaultExternalThemeFormat)
125}
126
127func getDefaultExternalThemePathForFormat(format string) string {
128 switch format {
129 case defaultExternalThemeTemplateFormat:
130 return getDefaultExternalThemeTemplatePath()
131 default:
132 return getDefaultNoctaliaColorsPath()
133 }
134}
135
136func getDefaultNoctaliaColorsPath() string {
137 configHome, err := os.UserConfigDir()
138 if err != nil {
139 return ""
140 }
141 return filepath.Join(configHome, defaultExternalThemeProvider, defaultExternalThemeColorsFilename)
142}
143
144func getDefaultExternalThemeTemplatePath() string {
145 configDir, err := GetConfigDir()
146 if err != nil {
147 return ""
148 }
149 return filepath.Join(configDir, defaultExternalThemeTemplateFilename)
150}
151
152// DefaultConfig returns the default configuration values for dumber.
153func DefaultConfig() *Config {
154 browsingContextDefaults := defaultBrowsingContextConfig()
155
156 return &Config{
157 Database: DatabaseConfig{
158 // Path is set dynamically in config.Load()
159 },
160 History: HistoryConfig{
161 MaxEntries: defaultMaxHistoryEntries,
162 RetentionPeriodDays: defaultRetentionDays, // 1 year
163 CleanupIntervalDays: 1, // daily cleanup
164 },
165 SearchShortcuts: GetDefaultSearchShortcuts(),
166 DefaultSearchEngine: "https://duckduckgo.com/?q=%s",
167 Dmenu: DmenuConfig{
168 MaxHistoryDays: defaultMaxHistoryDays,
169 ShowVisitCount: true,
170 ShowLastVisited: true,
171 HistoryPrefix: "🕒",
172 ShortcutPrefix: "🔍",
173 URLPrefix: "🌐",
174 DateFormat: "2006-01-02 15:04",
175 SortByVisitCount: true,
176 },
177 Logging: LoggingConfig{
178 Level: "info",
179 Format: "text", // text or json
180 MaxAge: defaultMaxLogAgeDays,
181 MaxFiles: defaultMaxLogFiles,
182 LogDir: getDefaultLogDir(),
183 EnableFileLog: true,
184 CaptureConsole: false, // Disabled by default
185 CaptureGTKLogs: false, // Disabled by default
186 },
187 Appearance: AppearanceConfig{
188 SansFont: "Fira Sans",
189 SerifFont: "Fira Sans",
190 MonospaceFont: "Fira Code",
191 GtkFont: "Adwaita Sans",
192 DefaultFontSize: defaultFontSize,
193 LightPalette: ColorPalette{
194 Background: "#fafafa",
195 Surface: "#f4f4f5",
196 SurfaceVariant: "#e4e4e7",
197 Text: "#18181b",
198 Muted: "#71717a",
199 Accent: "#22c55e", // Green-500 - vibrant primary
200 Border: "#d4d4d8",
201 },
202 DarkPalette: ColorPalette{
203 Background: "#0a0a0b",
204 Surface: "#18181b",
205 SurfaceVariant: "#27272a",
206 Text: "#fafafa",
207 Muted: "#a1a1aa",
208 Accent: "#4ade80", // Green-400 - vibrant primary
209 Border: "#3f3f46",
210 },
211 ColorScheme: "default", // default follows system theme
212 ExternalTheme: entity.ExternalThemeConfig{
213 Enabled: false,
214 Provider: defaultExternalThemeProvider,
215 Format: defaultExternalThemeFormat,
216 Path: getDefaultExternalThemePath(),
217 },
218 },
219 Debug: DebugConfig{
220 EnableDevTools: true,
221 },
222 Engine: EngineConfig{
223 Type: EngineTypeCEF,
224 Profile: ProfileDefault,
225 PoolPrewarmCount: defaultWebViewPoolPrewarmCount,
226 ZoomCacheSize: defaultZoomCacheSize,
227 // With ITP enabled, WebKit ignores ACCEPT_NO_THIRD_PARTY — ITP handles
228 // third-party cookie isolation more intelligently. Using Always + ITP
229 // matches Epiphany's model and avoids a misleading setting.
230 CookiePolicy: CookiePolicyAlways,
231 CEF: CEFEngineConfig{
232 RenderStack: CEFRenderStackVulkan,
233 AdaptiveWindowlessFrameRate: true,
234 WindowlessFrameRate: 0,
235 WindowlessFrameRateMax: defaultCEFWindowlessFrameRateMax,
236 LogFile: "",
237 EnableAudioHandler: true,
238 Input: CEFInputConfig{
239 ScrollWheelMultiplier: defaultCEFScrollMultiplier,
240 ScrollPreciseMultiplier: defaultCEFScrollPreciseMultiplier,
241 ScrollHorizontalMultiplier: defaultCEFScrollMultiplier,
242 ScrollVerticalMultiplier: defaultCEFScrollMultiplier,
243 ScrollMaxDelta: defaultCEFScrollMaxDelta,
244 TouchpadNavigationEnabled: defaultCEFTouchpadNavigation,
245 TouchpadNavigationMinDelta: defaultCEFTouchpadNavigationDelta,
246 TouchpadNavigationMaxVerticalRatio: defaultCEFTouchpadNavigationRatio,
247 },
248 },
249 WebKit: WebKitEngineConfig{
250 ITPEnabled: true,
251 SkiaCPUPaintingThreads: defaultSkiaCPUPaintingThreads,
252 SkiaGPUPaintingThreads: defaultSkiaGPUPaintingThreads,
253 GSKRenderer: GSKRendererAuto,
254 GLRenderingMode: GLRenderingModeAuto,
255 },
256 },
257 DefaultWebpageZoom: 1.2, // 120% default zoom for better readability
258 DefaultUIScale: defaultUIScale, // 1.0 = 100%, 2.0 = 200%
259 SidebarWidth: defaultSidebarWidth, // 320px, clamped to [280, 380]
260 Workspace: WorkspaceConfig{
261 NewPaneURL: defaultNewPaneURL,
262 SwitchToTabOnMove: true,
263 PaneMode: PaneModeConfig{
264 ActivationShortcut: defaultPaneActivationShortcut,
265 TimeoutMilliseconds: defaultPaneTimeoutMilliseconds,
266 Actions: map[string]ActionBinding{
267 "split-right": {Keys: []string{"arrowright", "r"}, Desc: "Split pane to the right"},
268 "split-left": {Keys: []string{"arrowleft", "l"}, Desc: "Split pane to the left"},
269 "split-up": {Keys: []string{"arrowup", "u"}, Desc: "Split pane upward"},
270 "split-down": {Keys: []string{"arrowdown", "d"}, Desc: "Split pane downward"},
271 "stack-pane": {Keys: []string{"s"}, Desc: "Stack pane with sibling"},
272 "close-pane": {Keys: []string{"x"}, Desc: "Close current pane"},
273 "move-pane-to-tab": {Keys: []string{"m"}, Desc: "Move pane to different tab"},
274 "move-pane-to-next-tab": {Keys: []string{"M", "shift+m"}, Desc: "Move pane to next tab"},
275 "eject-pane-to-window": {Keys: []string{"w"}, Desc: "Eject active pane to a new window"},
276
277 "consume-or-expel-left": {Keys: []string{"["}, Desc: "Consume/expel pane left"},
278 "consume-or-expel-right": {Keys: []string{"]"}, Desc: "Consume/expel pane right"},
279 "consume-or-expel-up": {Keys: []string{"{"}, Desc: "Consume/expel pane up"},
280 "consume-or-expel-down": {Keys: []string{"}"}, Desc: "Consume/expel pane down"},
281
282 "focus-right": {Keys: []string{"shift+arrowright", "shift+l"}, Desc: "Focus pane to the right"},
283 "focus-left": {Keys: []string{"shift+arrowleft", "shift+h"}, Desc: "Focus pane to the left"},
284 "focus-up": {Keys: []string{"shift+arrowup", "shift+k"}, Desc: "Focus pane above"},
285 "focus-down": {Keys: []string{"shift+arrowdown", "shift+j"}, Desc: "Focus pane below"},
286 "confirm": {Keys: []string{"enter"}, Desc: "Confirm action"},
287 "cancel": {Keys: []string{"escape"}, Desc: "Cancel/exit mode"},
288 },
289 },
290 TabMode: TabModeConfig{
291 ActivationShortcut: defaultTabActivationShortcut,
292 TimeoutMilliseconds: defaultTabTimeoutMilliseconds,
293 Actions: map[string]ActionBinding{
294 "new-tab": {Keys: []string{"n", "c"}, Desc: "Create new tab"},
295 "close-tab": {Keys: []string{"x"}, Desc: "Close current tab"},
296 "next-tab": {Keys: []string{"l", "tab"}, Desc: "Switch to next tab"},
297 "previous-tab": {Keys: []string{"h", "shift+tab"}, Desc: "Switch to previous tab"},
298 "rename-tab": {Keys: []string{"r"}, Desc: "Rename current tab"},
299 "confirm": {Keys: []string{"enter"}, Desc: "Confirm action"},
300 "cancel": {Keys: []string{"escape"}, Desc: "Cancel/exit mode"},
301 },
302 },
303 ResizeMode: ResizeModeConfig{
304 ActivationShortcut: defaultResizeActivationShortcut,
305 TimeoutMilliseconds: defaultResizeTimeoutMilliseconds,
306 StepPercent: defaultResizeStepPercent,
307 MinPanePercent: defaultResizeMinPanePercent,
308 Actions: map[string]ActionBinding{
309 "resize-increase-left": {Keys: []string{"h", "arrowleft"}, Desc: "Increase size leftward"},
310 "resize-increase-down": {Keys: []string{"j", "arrowdown"}, Desc: "Increase size downward"},
311 "resize-increase-up": {Keys: []string{"k", "arrowup"}, Desc: "Increase size upward"},
312 "resize-increase-right": {Keys: []string{"l", "arrowright"}, Desc: "Increase size rightward"},
313 "resize-decrease-left": {Keys: []string{"H"}, Desc: "Decrease size leftward"},
314 "resize-decrease-down": {Keys: []string{"J"}, Desc: "Decrease size downward"},
315 "resize-decrease-up": {Keys: []string{"K"}, Desc: "Decrease size upward"},
316 "resize-decrease-right": {Keys: []string{"L"}, Desc: "Decrease size rightward"},
317 "resize-increase": {Keys: []string{"+", "="}, Desc: "Increase pane size"},
318 "resize-decrease": {Keys: []string{"-"}, Desc: "Decrease pane size"},
319 "confirm": {Keys: []string{"enter"}, Desc: "Confirm action"},
320 "cancel": {Keys: []string{"escape"}, Desc: "Cancel/exit mode"},
321 },
322 },
323 Shortcuts: GlobalShortcutsConfig{
324 Actions: map[string]ActionBinding{
325 "toggle-floating-pane": {Keys: []string{"alt+f"}, Desc: "Toggle floating pane"},
326 "toggle-history-systemview": {Keys: []string{"ctrl+h"}, Desc: "Toggle History in right split"},
327 "toggle-favorites-systemview": {Keys: []string{}, Desc: "Toggle Favorites in right split"},
328 "toggle-config-systemview": {Keys: []string{}, Desc: "Toggle Config in right split"},
329 "close-pane": {Keys: []string{"ctrl+w"}, Desc: "Close active pane"},
330 "next-tab": {Keys: []string{"ctrl+tab"}, Desc: "Switch to next tab"},
331 "previous-tab": {Keys: []string{"ctrl+shift+tab"}, Desc: "Switch to previous tab"},
332 "consume-or-expel-left": {Keys: []string{"alt+["}, Desc: "Consume/expel pane left"},
333 "consume-or-expel-right": {Keys: []string{"alt+]"}, Desc: "Consume/expel pane right"},
334 "consume-or-expel-up": {Keys: []string{"alt+{"}, Desc: "Consume/expel pane up"},
335 "consume-or-expel-down": {Keys: []string{"alt+}"}, Desc: "Consume/expel pane down"},
336 },
337 },
338 FloatingPane: FloatingPaneConfig{
339 WidthPct: defaultFloatingPaneWidthPct,
340 HeightPct: defaultFloatingPaneHeightPct,
341 Profiles: map[string]FloatingPaneProfile{},
342 },
343 TabBarPosition: defaultTabBarPosition,
344 HideTabBarWhenSingleTab: true,
345 BrowsingContexts: browsingContextDefaults,
346 Popups: browsingContextDefaults,
347 Styling: WorkspaceStylingConfig{
348 BorderWidth: defaultBorderWidth,
349 BorderColor: defaultBorderColor,
350 ModeBorderWidth: defaultModeBorderWidth,
351 PaneModeColor: defaultPaneModeColor,
352 TabModeColor: defaultTabModeColor,
353 SessionModeColor: defaultSessionModeColor,
354 ResizeModeColor: defaultResizeModeColor,
355 ModeIndicatorToasterEnabled: defaultModeIndicatorToasterEnabled,
356 TransitionDuration: defaultTransitionDuration,
357 },
358 },
359 ContentFiltering: ContentFilteringConfig{
360 Enabled: true, // Ad blocking enabled by default
361 AutoUpdate: true, // Auto-update filters from GitHub releases
362 },
363 Clipboard: ClipboardConfig{
364 AutoCopyOnSelection: true, // Enabled by default (zellij-style)
365 },
366 Omnibox: OmniboxConfig{
367 InitialBehavior: defaultOmniboxInitialBehavior,
368 MostVisitedDays: defaultOmniboxMostVisitedDays,
369 AutoOpenOnNewPane: defaultOmniboxAutoOpenOnNewPane,
370 },
371 Session: SessionConfig{
372 AutoRestore: false,
373 SnapshotIntervalMs: defaultSnapshotIntervalMs,
374 MaxExitedSessions: defaultMaxExitedSessions,
375 MaxExitedSessionAgeDays: defaultMaxExitedSessionAgeDays,
376 SessionMode: SessionModeConfig{
377 ActivationShortcut: defaultSessionActivationShortcut,
378 TimeoutMilliseconds: defaultSessionTimeoutMilliseconds,
379 Actions: map[string]ActionBinding{
380 "session-manager": {Keys: []string{"s", "w"}, Desc: "Open session manager"},
381 "confirm": {Keys: []string{"enter"}, Desc: "Confirm action"},
382 "cancel": {Keys: []string{"escape"}, Desc: "Cancel/exit mode"},
383 },
384 },
385 },
386 Media: MediaConfig{
387 HardwareDecodingMode: HardwareDecodingAuto, // auto allows sw fallback
388 PreferAV1: false, // Don't force codec preference, let site choose
389 ShowDiagnosticsOnStartup: false, // Disabled - diagnostics can be noisy
390 // GStreamer fields (ForceVSync, GLRenderingMode, GStreamerDebugLevel)
391 // moved to [engine.webkit] — zero values here prevent them from being
392 // written back when marshaling the Config struct.
393 },
394 Update: UpdateConfig{
395 EnableOnStartup: true, // Check for updates on startup by default
396 AutoDownload: false, // Conservative: don't auto-download by default
397 NotifyOnNewSettings: true, // Show toast when new config settings available
398 },
399 Downloads: DownloadsConfig{
400 Path: "", // Empty = use XDG_DOWNLOAD_DIR or ~/Downloads
401 },
402 }
403}
404
405// GetDefaultSearchShortcuts returns the default search shortcuts.
406func GetDefaultSearchShortcuts() map[string]SearchShortcut {
407 return map[string]SearchShortcut{
408 "ddg": {
409 URL: "https://duckduckgo.com/?q=%s",
410 Description: "DuckDuckGo search",
411 },
412 "g": {
413 URL: "https://www.google.com/search?q=%s",
414 Description: "Google search",
415 },
416 "gi": {
417 URL: "https://www.google.com/search?tbm=isch&q=%s",
418 Description: "Google Images search",
419 },
420 "gh": {
421 URL: "https://github.com/search?q=%s",
422 Description: "GitHub search",
423 },
424 "go": {
425 URL: "https://pkg.go.dev/search?q=%s",
426 Description: "Go package search",
427 },
428 "mdn": {
429 URL: "https://developer.mozilla.org/en-US/search?q=%s",
430 Description: "MDN Web Docs search",
431 },
432 "npm": {
433 URL: "https://www.npmjs.com/search?q=%s",
434 Description: "npm package search",
435 },
436 "r": {
437 URL: "https://www.reddit.com/search?q=%s",
438 Description: "Reddit search",
439 },
440 "so": {
441 URL: "https://stackoverflow.com/search?q=%s",
442 Description: "Stack Overflow search",
443 },
444 "w": {
445 URL: "https://en.wikipedia.org/wiki/%s",
446 Description: "Wikipedia search",
447 },
448 "yt": {
449 URL: "https://www.youtube.com/results?search_query=%s",
450 Description: "YouTube search",
451 },
452 }
453}