1package main
2
3import (
4 "context"
5 "embed"
6 "encoding/json"
7 "fmt"
8 "log"
9 "mime"
10 neturl "net/url"
11 "os"
12 "os/exec"
13 "os/signal"
14 "path/filepath"
15 "runtime"
16 "strconv"
17 "strings"
18 "syscall"
19
20 "github.com/bnema/dumber/internal/cli"
21 "github.com/bnema/dumber/internal/config"
22 "github.com/bnema/dumber/internal/db"
23 "github.com/bnema/dumber/pkg/clipboard"
24 "github.com/bnema/dumber/pkg/webkit"
25 "github.com/bnema/dumber/services"
26)
27
28// Build information set via ldflags
29var (
30 version = "dev"
31 commit = "none"
32 buildDate = "unknown"
33)
34
35func main() {
36 // Check if we should run the CLI mode
37 if shouldRunCLI() {
38 runCLI()
39 return
40 }
41
42 // Otherwise run the GUI browser
43 runBrowser()
44}
45
46// shouldRunCLI determines if we should run in CLI mode based on arguments
47func shouldRunCLI() bool {
48 // No args = GUI landing page mode
49 if len(os.Args) <= 1 {
50 return false
51 }
52
53 // Check for GUI-specific flags
54 for _, arg := range os.Args[1:] {
55 if arg == "--gui" || arg == "-g" {
56 return false // Explicit GUI mode
57 }
58 }
59
60 // Check for browse command - this should open GUI in direct navigation mode
61 if len(os.Args) >= 2 && os.Args[1] == "browse" {
62 return false // Browse command uses GUI mode but navigates directly
63 }
64
65 // Any other arguments mean CLI mode
66 return true
67}
68
69// runCLI executes the CLI functionality using the existing CLI package
70func runCLI() {
71 // Initialize configuration system
72 if err := config.Init(); err != nil {
73 fmt.Fprintf(os.Stderr, "Error initializing configuration: %v\n", err)
74 os.Exit(1)
75 }
76
77 // Start configuration watching for live reload
78 if err := config.Watch(); err != nil {
79 // Don't exit on watch error, just warn
80 fmt.Fprintf(os.Stderr, "Warning: failed to start config watching: %v\n", err)
81 }
82
83 rootCmd := cli.NewRootCmd(version, commit, buildDate)
84
85 // Handle dmenu flag at the root level for direct integration
86 if len(os.Args) > 1 && os.Args[1] == "--dmenu" {
87 // Create a temporary CLI instance to handle dmenu mode
88 cliInstance, err := cli.NewCLI()
89 if err != nil {
90 fmt.Fprintf(os.Stderr, "Error initializing CLI: %v\n", err)
91 os.Exit(1)
92 }
93 defer func() {
94 if closeErr := cliInstance.Close(); closeErr != nil {
95 fmt.Fprintf(os.Stderr, "Warning: failed to close database: %v\n", closeErr)
96 }
97 }()
98
99 // Generate dmenu options
100 dmenuCmd := cli.NewDmenuCmd()
101 if err := dmenuCmd.RunE(dmenuCmd, []string{}); err != nil {
102 fmt.Fprintf(os.Stderr, "Error in dmenu mode: %v\n", err)
103 os.Exit(1)
104 }
105 return
106 }
107
108 // Execute the root command
109 if err := rootCmd.Execute(); err != nil {
110 fmt.Fprintf(os.Stderr, "Error: %v\n", err)
111 os.Exit(1)
112 }
113}
114
115//go:embed frontend/dist
116var assets embed.FS
117
118// runBrowser launches a minimal native WebKit window and integrates services.
119func runBrowser() {
120 log.Printf("Starting GUI mode (webkit_cgo=%v)", webkit.IsNativeAvailable())
121 // GTK requires all UI calls to run on the main OS thread
122 if webkit.IsNativeAvailable() {
123 runtime.LockOSThread()
124 defer runtime.UnlockOSThread()
125 }
126
127 // Proactively harden media pipeline to avoid WebProcess crashes from buggy VAAPI/plugins.
128 // Can be disabled with DUMBER_MEDIA_SAFE=0
129 if os.Getenv("DUMBER_MEDIA_SAFE") != "0" {
130 // Demote VAAPI-related elements so they won't be auto-picked
131 if os.Getenv("GST_PLUGIN_FEATURE_RANK") == "" {
132 os.Setenv("GST_PLUGIN_FEATURE_RANK", "vaapisink:0,vaapidecodebin:0,vaapih264dec:0,vaapivideoconvert:0")
133 }
134 // Provide a safe audio sink to avoid autoaudiosink failures
135 if os.Getenv("GST_AUDIO_SINK") == "" {
136 os.Setenv("GST_AUDIO_SINK", "fakesink")
137 }
138 // Optionally disable media stream features that can engage complex pipelines
139 if os.Getenv("WEBKIT_DISABLE_MEDIA_STREAM") == "" {
140 os.Setenv("WEBKIT_DISABLE_MEDIA_STREAM", "1")
141 }
142 log.Printf("[media] Safe mode enabled (override with DUMBER_MEDIA_SAFE=0)")
143 }
144
145 if err := config.Init(); err != nil {
146 log.Fatalf("Failed to initialize config: %v", err)
147 }
148 cfg := config.Get()
149 log.Printf("Config initialized")
150
151 // Detect keyboard layout/locale and hint to webkit layer
152 detectAndSetKeyboardLocale()
153
154 // Initialize database
155 database, err := db.InitDB(cfg.Database.Path)
156 if err != nil {
157 log.Fatalf("Failed to initialize database: %v", err)
158 }
159 defer database.Close()
160 queries := db.New(database)
161 log.Printf("Database opened at %s", cfg.Database.Path)
162
163 // Initialize services
164 parserService := services.NewParserService(cfg, queries)
165 browserService := services.NewBrowserService(cfg, queries)
166 // JS controls injection is disabled; using native shortcuts via GTK.
167
168 // Track current URL for zoom persistence
169 // Initialize to the initial homepage so shortcuts work immediately
170 var currentURL string = "dumb://homepage"
171
172 // Register custom scheme resolver for dumb://
173 webkit.SetURISchemeResolver(func(uri string) (string, []byte, bool) {
174 log.Printf("[scheme] request: %s", uri)
175 // Known forms:
176 // - dumb://homepage or dumb:homepage → index.html
177 // - dumb://app/index.html, dumb://app/<path> → serve from frontend/dist/<path>
178 // - dumb://<anything> without path → index.html
179 u, err := neturl.Parse(uri)
180 if err != nil || u.Scheme != "dumb" {
181 return "", nil, false
182 }
183 // API routes
184 if u.Host == "api" || strings.HasPrefix(u.Opaque, "api") || strings.HasPrefix(u.Path, "/api") {
185 // Normalize path for opaque or hierarchical forms
186 path := u.Path
187 if path == "" && u.Opaque != "" {
188 // e.g., dumb:api/config or dumb:api/history/recent
189 parts := strings.SplitN(u.Opaque, ":", 2)
190 if len(parts) == 2 {
191 path = "/" + parts[1]
192 }
193 }
194 if strings.HasPrefix(path, "/api/") {
195 path = strings.TrimPrefix(path, "/api")
196 }
197 if path == "/api" {
198 path = "/"
199 }
200 switch {
201 case strings.HasPrefix(path, "/rendering/status"):
202 log.Printf("[api] GET /rendering/status")
203 // Report configured rendering mode; runtime GPU state depends on WebKit internals
204 resp := struct {
205 Mode string `json:"mode"`
206 }{Mode: string(cfg.RenderingMode)}
207 b, _ := json.Marshal(resp)
208 return "application/json; charset=utf-8", b, true
209 case strings.HasPrefix(path, "/config"):
210 log.Printf("[api] GET /config")
211 // Build config info
212 cfgPath, _ := config.GetConfigFile()
213 info := struct {
214 ConfigPath string `json:"config_path"`
215 DatabasePath string `json:"database_path"`
216 SearchShortcuts map[string]config.SearchShortcut `json:"search_shortcuts"`
217 Appearance config.AppearanceConfig `json:"appearance"`
218 }{
219 ConfigPath: cfgPath,
220 DatabasePath: cfg.Database.Path,
221 SearchShortcuts: cfg.SearchShortcuts,
222 Appearance: cfg.Appearance,
223 }
224 b, _ := json.Marshal(info)
225 return "application/json; charset=utf-8", b, true
226 case strings.HasPrefix(path, "/history/recent"):
227 log.Printf("[api] GET /history/recent%s", u.RawQuery)
228 // Parse limit
229 q := u.Query()
230 limit := 50
231 if l := q.Get("limit"); l != "" {
232 if n, err := strconv.Atoi(l); err == nil && n > 0 {
233 limit = n
234 }
235 }
236 ctx := context.Background()
237 entries, err := browserService.GetRecentHistory(ctx, limit)
238 if err != nil {
239 return "application/json; charset=utf-8", []byte("[]"), true
240 }
241 b, _ := json.Marshal(entries)
242 return "application/json; charset=utf-8", b, true
243 case strings.HasPrefix(path, "/history/search"):
244 log.Printf("[api] GET /history/search%s", u.RawQuery)
245 q := u.Query()
246 query := q.Get("q")
247 limit := 50
248 if l := q.Get("limit"); l != "" {
249 if n, err := strconv.Atoi(l); err == nil && n > 0 {
250 limit = n
251 }
252 }
253 ctx := context.Background()
254 entries, err := browserService.SearchHistory(ctx, query, limit)
255 if err != nil {
256 return "application/json; charset=utf-8", []byte("[]"), true
257 }
258 b, _ := json.Marshal(entries)
259 return "application/json; charset=utf-8", b, true
260 case strings.HasPrefix(path, "/history/stats"):
261 log.Printf("[api] GET /history/stats")
262 ctx := context.Background()
263 stats, err := browserService.GetHistoryStats(ctx)
264 if err != nil {
265 return "application/json; charset=utf-8", []byte("{}"), true
266 }
267 b, _ := json.Marshal(stats)
268 return "application/json; charset=utf-8", b, true
269 default:
270 return "application/json; charset=utf-8", []byte("{}"), true
271 }
272 }
273 // Resolve target path inside embed FS
274 var rel string
275 if u.Opaque == "homepage" || (u.Host == "homepage" && (u.Path == "" || u.Path == "/")) || (u.Host == "" && (u.Path == "" || u.Path == "/")) {
276 rel = "index.html"
277 } else {
278 host := u.Host
279 p := strings.TrimPrefix(u.Path, "/")
280 if host == "app" && p == "" {
281 rel = "index.html"
282 } else if host == "app" {
283 rel = p
284 } else if host == "homepage" && p != "" {
285 // dumb://homepage/<asset>
286 rel = p
287 } else if p != "" {
288 rel = p
289 } else {
290 rel = "index.html"
291 }
292 }
293 // Special-case homepage favicon: map .ico request to embedded SVG file
294 if (u.Host == "homepage" || u.Opaque == "homepage") && strings.EqualFold(rel, "favicon.ico") {
295 log.Printf("[scheme] asset: rel=%s (host=%s path=%s) → mapping to favicon.svg", rel, u.Host, u.Path)
296 data, rerr := assets.ReadFile(filepath.ToSlash(filepath.Join("frontend", "dist", "favicon.svg")))
297 if rerr == nil {
298 return "image/svg+xml", data, true
299 }
300 }
301
302 log.Printf("[scheme] asset: rel=%s (host=%s path=%s)", rel, u.Host, u.Path)
303 data, rerr := assets.ReadFile(filepath.ToSlash(filepath.Join("frontend", "dist", rel)))
304 if rerr != nil {
305 log.Printf("[scheme] not found: %s", rel)
306 return "", nil, false
307 }
308 // Determine mime type
309 mt := mime.TypeByExtension(strings.ToLower(filepath.Ext(rel)))
310 if mt == "" {
311 // Fallbacks
312 switch strings.ToLower(filepath.Ext(rel)) {
313 case ".js":
314 mt = "application/javascript"
315 case ".css":
316 mt = "text/css"
317 case ".svg":
318 mt = "image/svg+xml"
319 case ".ico":
320 mt = "image/x-icon"
321 default:
322 mt = "text/html; charset=utf-8"
323 }
324 }
325 return mt, data, true
326 })
327
328 // Create WebKit view
329 log.Printf("Creating WebView (native backend expected: %v)", webkit.IsNativeAvailable())
330 dataDir, _ := config.GetDataDir()
331 stateDir, _ := config.GetStateDir()
332 webkitData := filepath.Join(dataDir, "webkit")
333 webkitCache := filepath.Join(stateDir, "webkit-cache")
334 _ = os.MkdirAll(webkitData, 0o755)
335 _ = os.MkdirAll(webkitCache, 0o755)
336 view, err := webkit.NewWebView(&webkit.Config{
337 InitialURL: "dumb://homepage",
338 ZoomDefault: 1.0,
339 EnableDeveloperExtras: true,
340 DataDir: webkitData,
341 CacheDir: webkitCache,
342 DefaultSansFont: cfg.Appearance.SansFont,
343 DefaultSerifFont: cfg.Appearance.SerifFont,
344 DefaultMonospaceFont: cfg.Appearance.MonospaceFont,
345 DefaultFontSize: cfg.Appearance.DefaultFontSize,
346 Rendering: webkit.RenderingConfig{Mode: string(cfg.RenderingMode)},
347 Memory: readMemoryConfigFromEnv(),
348 })
349 if err != nil {
350 log.Printf("Warning: failed to create WebView: %v", err)
351 } else {
352 browserService.AttachWebView(view)
353 // Use native window as title updater
354 if win := view.Window(); win != nil {
355 browserService.SetWindowTitleUpdater(win)
356 }
357 // Persist page titles to DB when they change
358 view.RegisterTitleChangedHandler(func(title string) {
359 ctx := context.Background()
360 url := view.GetCurrentURL()
361 if url != "" && title != "" {
362 if err := browserService.UpdatePageTitle(ctx, url, title); err != nil {
363 log.Printf("Warning: failed to update page title: %v", err)
364 }
365 }
366 })
367 // Track current URL changes and apply saved zoom
368 view.RegisterURIChangedHandler(func(u string) {
369 currentURL = u
370 if u == "" {
371 return
372 }
373 ctx := context.Background()
374 if z, err := browserService.GetZoomLevel(ctx, u); err == nil {
375 _ = view.SetZoom(z)
376 key := services.ZoomKeyForLog(u)
377 log.Printf("[zoom] loaded %.2f for %s", z, key)
378 }
379 })
380 // Persist zoom changes per-domain
381 view.RegisterZoomChangedHandler(func(level float64) {
382 url := view.GetCurrentURL()
383 if url == "" {
384 return
385 }
386 ctx := context.Background()
387 if err := browserService.SetZoomLevel(ctx, url, level); err != nil {
388 log.Printf("[zoom] failed to save level %.2f for %s: %v", level, url, err)
389 return
390 }
391 key := services.ZoomKeyForLog(url)
392 log.Printf("[zoom] saved %.2f for %s", level, key)
393 })
394 // Bridge UCM messages: navigate + history queries
395 view.RegisterScriptMessageHandler(func(payload string) {
396 type msg struct {
397 Type string `json:"type"`
398 URL string `json:"url"`
399 Q string `json:"q"`
400 Limit int `json:"limit"`
401 Value string `json:"value"`
402 // Wails fetch bridge
403 ID string `json:"id"`
404 Payload json.RawMessage `json:"payload"`
405 }
406 var m msg
407 if err := json.Unmarshal([]byte(payload), &m); err != nil {
408 return
409 }
410 switch m.Type {
411 case "navigate":
412 ctx := context.Background()
413 res, err := parserService.ParseInput(ctx, m.URL)
414 if err == nil {
415 currentURL = res.URL
416 browserService.Navigate(ctx, currentURL)
417 _ = view.LoadURL(currentURL)
418 if z, zerr := browserService.GetZoomLevel(ctx, currentURL); zerr == nil {
419 _ = view.SetZoom(z)
420 }
421 }
422 case "query":
423 ctx := context.Background()
424 lim := m.Limit
425 if lim <= 0 || lim > 25 {
426 lim = 10
427 }
428 entries, err := browserService.SearchHistory(ctx, m.Q, lim)
429 if err != nil {
430 return
431 }
432 // Map to lightweight items
433 type item struct {
434 URL string `json:"url"`
435 Favicon string `json:"favicon"`
436 }
437 buildFavicon := func(raw string) string {
438 u, err := parserService.ParseInput(ctx, raw)
439 if err != nil || u.URL == "" {
440 return ""
441 }
442 parsed, perr := neturl.Parse(u.URL)
443 if perr != nil || parsed.Host == "" {
444 return ""
445 }
446 scheme := parsed.Scheme
447 if scheme == "" {
448 scheme = "https"
449 }
450 return scheme + "://" + parsed.Host + "/favicon.ico"
451 }
452 items := make([]item, 0, len(entries))
453 for _, e := range entries {
454 items = append(items, item{URL: e.URL, Favicon: buildFavicon(e.URL)})
455 }
456 b, _ := json.Marshal(items)
457 // Update suggestions in page
458 _ = view.InjectScript("window.__dumber_setSuggestions && window.__dumber_setSuggestions(" + string(b) + ")")
459 case "wails":
460 // Handle Wails runtime fetch bridge calls for homepage
461 // Payload contains { methodID, methodName?, args }
462 var p struct {
463 MethodID uint32 `json:"methodID"`
464 MethodName string `json:"methodName"`
465 Args json.RawMessage `json:"args"`
466 }
467 if err := json.Unmarshal(m.Payload, &p); err != nil {
468 return
469 }
470 // Only implement the IDs we need
471 switch p.MethodID {
472 case 3708519028: // BrowserService.GetRecentHistory(limit)
473 var args []interface{}
474 _ = json.Unmarshal(p.Args, &args)
475 limit := 50
476 if len(args) > 0 {
477 if f, ok := args[0].(float64); ok {
478 limit = int(f)
479 }
480 }
481 ctx := context.Background()
482 entries, err := browserService.GetRecentHistory(ctx, limit)
483 if err != nil {
484 return
485 }
486 resp, _ := json.Marshal(entries)
487 _ = view.InjectScript("window.__dumber_wails_resolve('" + m.ID + "', " + string(resp) + ")")
488 case 4078533762: // BrowserService.GetSearchShortcuts()
489 ctx := context.Background()
490 shortcuts, err := browserService.GetSearchShortcuts(ctx)
491 if err != nil {
492 return
493 }
494 resp, _ := json.Marshal(shortcuts)
495 _ = view.InjectScript("window.__dumber_wails_resolve('" + m.ID + "', " + string(resp) + ")")
496 default:
497 // Return empty JSON to avoid breaking UI
498 _ = view.InjectScript("window.__dumber_wails_resolve('" + m.ID + "', '{}')")
499 }
500 case "theme":
501 if m.Value != "" {
502 log.Printf("[theme] page reported color-scheme: %s", m.Value)
503 }
504 }
505 })
506 // Apply initial zoom setting and log it before showing the window
507 {
508 ctx := context.Background()
509 u := view.GetCurrentURL()
510 if u == "" {
511 u = "dumb://homepage"
512 }
513 if z, zerr := browserService.GetZoomLevel(ctx, u); zerr == nil {
514 if err := view.SetZoom(z); err != nil {
515 log.Printf("Warning: failed to set initial zoom: %v", err)
516 } else {
517 key := services.ZoomKeyForLog(u)
518 log.Printf("[zoom] loaded %.2f for %s", z, key)
519 }
520 }
521 }
522
523 log.Printf("Showing WebView window…")
524 if err := view.Show(); err != nil {
525 log.Printf("Warning: failed to show WebView: %v", err)
526 } else {
527 if !webkit.IsNativeAvailable() {
528 log.Printf("Notice: running without webkit_cgo tag — no native window will be displayed.")
529 }
530 }
531 }
532
533 // Parse browse argument if present
534 if len(os.Args) >= 3 && os.Args[1] == "browse" {
535 log.Printf("Browse command detected: %s", os.Args[2])
536 ctx := context.Background()
537 result, err := parserService.ParseInput(ctx, os.Args[2])
538 if err == nil {
539 currentURL = result.URL
540 log.Printf("Parsed input → URL: %s", currentURL)
541 browserService.Navigate(ctx, currentURL)
542 if view != nil {
543 log.Printf("Loading URL in WebView: %s", currentURL)
544 if err := view.LoadURL(currentURL); err != nil {
545 log.Printf("Warning: failed to load URL: %v", err)
546 }
547 if z, zerr := browserService.GetZoomLevel(ctx, currentURL); zerr == nil {
548 if err := view.SetZoom(z); err != nil {
549 log.Printf("Warning: failed to set zoom: %v", err)
550 } else {
551 key := services.ZoomKeyForLog(currentURL)
552 log.Printf("[zoom] loaded %.2f for %s", z, key)
553 }
554 }
555 // No JS injection; native accelerators handle zoom/devtools.
556 }
557 }
558 }
559
560 // Register basic keyboard shortcuts on the native view to preserve behavior
561 if view != nil {
562 // DevTools
563 _ = view.RegisterKeyboardShortcut("F12", func() { log.Printf("Shortcut: F12 (devtools)"); _ = view.ShowDevTools() })
564 // Omnibox (Ctrl+L): rely on injected script listener
565 _ = view.RegisterKeyboardShortcut("cmdorctrl+l", func() {
566 log.Printf("Shortcut: Omnibox toggle")
567 _ = view.InjectScript("window.__dumber_toggle && window.__dumber_toggle()")
568 })
569 // Copy URL (Ctrl+Shift+C)
570 _ = view.RegisterKeyboardShortcut("cmdorctrl+shift+c", func() {
571 log.Printf("Shortcut: Copy URL")
572 currentURL := view.GetCurrentURL()
573 if currentURL == "" {
574 log.Printf("No URL to copy")
575 _ = view.InjectScript(`window.__dumber_showToast && window.__dumber_showToast("No URL to copy", 2000)`)
576 return
577 }
578
579 if err := clipboard.CopyToClipboard(currentURL); err != nil {
580 log.Printf("Failed to copy URL to clipboard: %v", err)
581 _ = view.InjectScript(`window.__dumber_showToast && window.__dumber_showToast("Failed to copy URL", 2000)`)
582 } else {
583 log.Printf("URL copied to clipboard: %s", currentURL)
584 _ = view.InjectScript(`window.__dumber_showToast && window.__dumber_showToast("URL copied to clipboard", 2000)`)
585 }
586 })
587 // Zoom handled natively in webkit package (built-in shortcuts)
588 }
589
590 // Set up signal handling for graceful shutdown
591 sigChan := make(chan os.Signal, 1)
592 signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
593
594 // Enter GTK main loop only when native backend is available.
595 if webkit.IsNativeAvailable() {
596 // Handle signals in a goroutine to quit the main loop
597 go func() {
598 sig := <-sigChan
599 log.Printf("Received signal %v - shutting down gracefully", sig)
600 webkit.QuitMainLoop()
601 }()
602
603 log.Printf("Entering GTK main loop…")
604 webkit.RunMainLoop()
605 log.Printf("GTK main loop exited")
606 } else {
607 log.Printf("Not entering GUI loop (non-CGO build)")
608 // In non-CGO mode, just wait for signals
609 sig := <-sigChan
610 log.Printf("Received signal %v - exiting", sig)
611 }
612}
613
614// readMemoryConfigFromEnv reads optional memory optimization knobs from environment variables.
615// Defaults favor performance; only applied when env vars are set.
616func readMemoryConfigFromEnv() webkit.MemoryConfig {
617 var mc webkit.MemoryConfig
618
619 // Cache model (document_viewer | web_browser | primary_web_browser)
620 if v := os.Getenv("DUMBER_CACHE_MODEL"); v != "" {
621 switch strings.ToLower(v) {
622 case "document_viewer", "documentviewer", "doc":
623 mc.CacheModel = webkit.CacheModelDocumentViewer
624 case "primary_web_browser", "primary", "primarywebbrowser":
625 mc.CacheModel = webkit.CacheModelPrimaryWebBrowser
626 default:
627 mc.CacheModel = webkit.CacheModelWebBrowser
628 }
629 }
630
631 // Page cache toggle (default true, only override if provided)
632 if v := os.Getenv("DUMBER_ENABLE_PAGE_CACHE"); v != "" {
633 mc.EnablePageCache = v == "1" || strings.EqualFold(v, "true") || strings.EqualFold(v, "yes")
634 } else {
635 mc.EnablePageCache = true
636 }
637
638 // Memory limit (MB)
639 if v := os.Getenv("DUMBER_MEMORY_LIMIT_MB"); v != "" {
640 if n, err := strconv.Atoi(v); err == nil && n >= 0 {
641 mc.MemoryLimitMB = n
642 }
643 }
644
645 // Thresholds
646 if v := os.Getenv("DUMBER_MEMORY_CONSERVATIVE"); v != "" {
647 if f, err := strconv.ParseFloat(v, 64); err == nil {
648 mc.ConservativeThreshold = f
649 }
650 }
651 if v := os.Getenv("DUMBER_MEMORY_STRICT"); v != "" {
652 if f, err := strconv.ParseFloat(v, 64); err == nil {
653 mc.StrictThreshold = f
654 }
655 }
656 if v := os.Getenv("DUMBER_MEMORY_KILL"); v != "" {
657 if f, err := strconv.ParseFloat(v, 64); err == nil {
658 mc.KillThreshold = f
659 }
660 }
661 if v := os.Getenv("DUMBER_MEMORY_POLL_INTERVAL"); v != "" {
662 if f, err := strconv.ParseFloat(v, 64); err == nil {
663 mc.PollIntervalSeconds = f
664 }
665 }
666
667 // Periodic GC (seconds)
668 if v := os.Getenv("DUMBER_GC_INTERVAL"); v != "" {
669 if n, err := strconv.Atoi(v); err == nil && n >= 0 {
670 mc.EnableGCInterval = n
671 }
672 }
673
674 // Recycle after N page loads
675 if v := os.Getenv("DUMBER_RECYCLE_THRESHOLD"); v != "" {
676 if n, err := strconv.Atoi(v); err == nil && n >= 0 {
677 mc.ProcessRecycleThreshold = n
678 }
679 }
680
681 // Enable memory monitoring logs
682 if v := os.Getenv("DUMBER_ENABLE_MEMORY_MONITORING"); v != "" {
683 mc.EnableMemoryMonitoring = v == "1" || strings.EqualFold(v, "true") || strings.EqualFold(v, "yes")
684 }
685
686 return mc
687}
688
689// detectAndSetKeyboardLocale attempts to determine the current keyboard layout
690// and hints it to the webkit input layer for accelerator compatibility.
691func detectAndSetKeyboardLocale() {
692 // 0) Explicit override
693 locale := os.Getenv("DUMBER_KEYBOARD_LOCALE")
694 if locale == "" {
695 locale = os.Getenv("DUMB_BROWSER_KEYBOARD_LOCALE") // legacy prefix
696 }
697 // 1) Environment
698 if locale == "" {
699 locale = os.Getenv("LC_ALL")
700 }
701 if locale == "" {
702 locale = os.Getenv("LANG")
703 }
704 if locale == "" {
705 locale = os.Getenv("LC_CTYPE")
706 }
707 // Trim variants
708 if i := strings.IndexByte(locale, '.'); i > 0 {
709 locale = locale[:i]
710 }
711 if i := strings.IndexByte(locale, '@'); i > 0 {
712 locale = locale[:i]
713 }
714 if i := strings.IndexByte(locale, '_'); i > 0 {
715 locale = locale[:i]
716 }
717
718 // 2) Try XKB env override
719 if locale == "" {
720 locale = os.Getenv("XKB_DEFAULT_LAYOUT")
721 }
722
723 // 3) Best-effort probe of setxkbmap/localectl (non-fatal)
724 if locale == "" {
725 if out, err := exec.Command("localectl", "status").Output(); err == nil {
726 s := string(out)
727 for _, line := range strings.Split(s, "\n") {
728 line = strings.TrimSpace(line)
729 if strings.HasPrefix(strings.ToLower(line), "x11 layout:") || strings.HasPrefix(strings.ToLower(line), "keyboard layout:") {
730 parts := strings.SplitN(line, ":", 2)
731 if len(parts) == 2 {
732 cand := strings.TrimSpace(parts[1])
733 if cand != "" {
734 locale = cand
735 }
736 }
737 break
738 }
739 }
740 }
741 }
742 if locale == "" {
743 if out, err := exec.Command("setxkbmap", "-query").Output(); err == nil {
744 s := string(out)
745 for _, line := range strings.Split(s, "\n") {
746 line = strings.TrimSpace(line)
747 if strings.HasPrefix(strings.ToLower(line), "layout:") {
748 parts := strings.SplitN(line, ":", 2)
749 if len(parts) == 2 {
750 cand := strings.TrimSpace(parts[1])
751 if cand != "" {
752 locale = cand
753 }
754 }
755 break
756 }
757 }
758 }
759 }
760
761 if locale == "" {
762 locale = "en"
763 }
764 // No layout-specific remaps; log for diagnostics only
765 log.Printf("[locale] keyboard locale detected: %s", locale)
766}
767
768// getHomepageFaviconSVG returns the SVG used as a favicon for the homepage
769// when WebKit requests dumb://homepage/favicon.ico.
770// getHomepageFaviconSVG was inlined previously; now favicon.svg is emitted at build time.