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