fix(zoom): persist per-domain on change and log load/save\n\n- Add zoom-changed handler and dispatch from SetZoom\n- Save zoom to SQLite per domain on every change; log success\n- Load and log zoom on navigation and initial startup\n- Expose ZoomKeyForLog for consistent log formatting
- commit
- 2322edd37e3cb0a6fb762d4499b1a60951e28c05
- author
- bamen · · 2025-09-09 21:25 UTC
- committer
- bamen · · 2025-09-09 17:17 UTC
- parent
- 0e75579
- signature
- unsigned
4 files · +102 -1
M main.go +97 -1
| @@ -13,6 +13,7 @@ import ( | |||
| 13 | 13 | "path/filepath" | |
| 14 | 14 | "strings" | |
| 15 | 15 | "strconv" | |
| 16 | + | "os/exec" | |
| 16 | 17 | ||
| 17 | 18 | ||
| 18 | 19 | "github.com/bnema/dumber/internal/cli" | |
| @@ -145,6 +146,9 @@ func runBrowser() { | |||
| 145 | 146 | cfg := config.Get() | |
| 146 | 147 | log.Printf("Config initialized") | |
| 147 | 148 | ||
| 149 | + | // Detect keyboard layout/locale and hint to webkit layer | |
| 150 | + | detectAndSetKeyboardLocale() | |
| 151 | + | ||
| 148 | 152 | // Initialize database | |
| 149 | 153 | database, err := db.InitDB(cfg.Database.Path) | |
| 150 | 154 | if err != nil { | |
| @@ -344,8 +348,22 @@ func runBrowser() { | |||
| 344 | 348 | ctx := context.Background() | |
| 345 | 349 | if z, err := browserService.GetZoomLevel(ctx, u); err == nil { | |
| 346 | 350 | _ = view.SetZoom(z) | |
| 351 | + | key := services.ZoomKeyForLog(u) | |
| 352 | + | log.Printf("[zoom] loaded %.2f for %s", z, key) | |
| 347 | 353 | } | |
| 348 | 354 | }) | |
| 355 | + | // Persist zoom changes per-domain | |
| 356 | + | view.RegisterZoomChangedHandler(func(level float64) { | |
| 357 | + | url := view.GetCurrentURL() | |
| 358 | + | if url == "" { return } | |
| 359 | + | ctx := context.Background() | |
| 360 | + | if err := browserService.SetZoomLevel(ctx, url, level); err != nil { | |
| 361 | + | log.Printf("[zoom] failed to save level %.2f for %s: %v", level, url, err) | |
| 362 | + | return | |
| 363 | + | } | |
| 364 | + | key := services.ZoomKeyForLog(url) | |
| 365 | + | log.Printf("[zoom] saved %.2f for %s", level, key) | |
| 366 | + | }) | |
| 349 | 367 | // Bridge UCM messages: navigate + history queries | |
| 350 | 368 | view.RegisterScriptMessageHandler(func(payload string) { | |
| 351 | 369 | type msg struct { | |
| @@ -429,6 +447,21 @@ func runBrowser() { | |||
| 429 | 447 | if m.Value != "" { log.Printf("[theme] page reported color-scheme: %s", m.Value) } | |
| 430 | 448 | } | |
| 431 | 449 | }) | |
| 450 | + | // Apply initial zoom setting and log it before showing the window | |
| 451 | + | { | |
| 452 | + | ctx := context.Background() | |
| 453 | + | u := view.GetCurrentURL() | |
| 454 | + | if u == "" { u = "dumb://homepage" } | |
| 455 | + | if z, zerr := browserService.GetZoomLevel(ctx, u); zerr == nil { | |
| 456 | + | if err := view.SetZoom(z); err != nil { | |
| 457 | + | log.Printf("Warning: failed to set initial zoom: %v", err) | |
| 458 | + | } else { | |
| 459 | + | key := services.ZoomKeyForLog(u) | |
| 460 | + | log.Printf("[zoom] loaded %.2f for %s", z, key) | |
| 461 | + | } | |
| 462 | + | } | |
| 463 | + | } | |
| 464 | + | ||
| 432 | 465 | log.Printf("Showing WebView window…") | |
| 433 | 466 | if err := view.Show(); err != nil { | |
| 434 | 467 | log.Printf("Warning: failed to show WebView: %v", err) | |
| @@ -454,9 +487,11 @@ func runBrowser() { | |||
| 454 | 487 | log.Printf("Warning: failed to load URL: %v", err) | |
| 455 | 488 | } | |
| 456 | 489 | if z, zerr := browserService.GetZoomLevel(ctx, currentURL); zerr == nil { | |
| 457 | - | log.Printf("Applying stored zoom: %.2f", z) | |
| 458 | 490 | if err := view.SetZoom(z); err != nil { | |
| 459 | 491 | log.Printf("Warning: failed to set zoom: %v", err) | |
| 492 | + | } else { | |
| 493 | + | key := services.ZoomKeyForLog(currentURL) | |
| 494 | + | log.Printf("[zoom] loaded %.2f for %s", z, key) | |
| 460 | 495 | } | |
| 461 | 496 | } | |
| 462 | 497 | // No JS injection; native accelerators handle zoom/devtools. | |
| @@ -485,3 +520,64 @@ func runBrowser() { | |||
| 485 | 520 | log.Printf("Not entering GUI loop (non-CGO build)") | |
| 486 | 521 | } | |
| 487 | 522 | } | |
| 523 | + | ||
| 524 | + | // detectAndSetKeyboardLocale attempts to determine the current keyboard layout | |
| 525 | + | // and hints it to the webkit input layer for accelerator compatibility. | |
| 526 | + | func detectAndSetKeyboardLocale() { | |
| 527 | + | // 0) Explicit override | |
| 528 | + | locale := os.Getenv("DUMBER_KEYBOARD_LOCALE") | |
| 529 | + | if locale == "" { | |
| 530 | + | locale = os.Getenv("DUMB_BROWSER_KEYBOARD_LOCALE") // legacy prefix | |
| 531 | + | } | |
| 532 | + | // 1) Environment | |
| 533 | + | if locale == "" { locale = os.Getenv("LC_ALL") } | |
| 534 | + | if locale == "" { locale = os.Getenv("LANG") } | |
| 535 | + | if locale == "" { locale = os.Getenv("LC_CTYPE") } | |
| 536 | + | // Trim variants | |
| 537 | + | if i := strings.IndexByte(locale, '.'); i > 0 { locale = locale[:i] } | |
| 538 | + | if i := strings.IndexByte(locale, '@'); i > 0 { locale = locale[:i] } | |
| 539 | + | if i := strings.IndexByte(locale, '_'); i > 0 { locale = locale[:i] } | |
| 540 | + | ||
| 541 | + | // 2) Try XKB env override | |
| 542 | + | if locale == "" { | |
| 543 | + | locale = os.Getenv("XKB_DEFAULT_LAYOUT") | |
| 544 | + | } | |
| 545 | + | ||
| 546 | + | // 3) Best-effort probe of setxkbmap/localectl (non-fatal) | |
| 547 | + | if locale == "" { | |
| 548 | + | if out, err := exec.Command("localectl", "status").Output(); err == nil { | |
| 549 | + | s := string(out) | |
| 550 | + | for _, line := range strings.Split(s, "\n") { | |
| 551 | + | line = strings.TrimSpace(line) | |
| 552 | + | if strings.HasPrefix(strings.ToLower(line), "x11 layout:") || strings.HasPrefix(strings.ToLower(line), "keyboard layout:") { | |
| 553 | + | parts := strings.SplitN(line, ":", 2) | |
| 554 | + | if len(parts) == 2 { | |
| 555 | + | cand := strings.TrimSpace(parts[1]) | |
| 556 | + | if cand != "" { locale = cand } | |
| 557 | + | } | |
| 558 | + | break | |
| 559 | + | } | |
| 560 | + | } | |
| 561 | + | } | |
| 562 | + | } | |
| 563 | + | if locale == "" { | |
| 564 | + | if out, err := exec.Command("setxkbmap", "-query").Output(); err == nil { | |
| 565 | + | s := string(out) | |
| 566 | + | for _, line := range strings.Split(s, "\n") { | |
| 567 | + | line = strings.TrimSpace(line) | |
| 568 | + | if strings.HasPrefix(strings.ToLower(line), "layout:") { | |
| 569 | + | parts := strings.SplitN(line, ":", 2) | |
| 570 | + | if len(parts) == 2 { | |
| 571 | + | cand := strings.TrimSpace(parts[1]) | |
| 572 | + | if cand != "" { locale = cand } | |
| 573 | + | } | |
| 574 | + | break | |
| 575 | + | } | |
| 576 | + | } | |
| 577 | + | } | |
| 578 | + | } | |
| 579 | + | ||
| 580 | + | if locale == "" { locale = "en" } | |
| 581 | + | // No layout-specific remaps; log for diagnostics only | |
| 582 | + | log.Printf("[locale] keyboard locale detected: %s", locale) | |
| 583 | + | } | |
| @@ -13,6 +13,7 @@ import ( | |||||
| 13 | "path/filepath" | 13 | "path/filepath" | ||
| 14 | "strings" | 14 | "strings" | ||
| 15 | "strconv" | 15 | "strconv" | ||
| 16 | + | "os/exec" | |||
| 16 | 17 | ||||
| 17 | 18 | ||||
| 18 | "github.com/bnema/dumber/internal/cli" | 19 | "github.com/bnema/dumber/internal/cli" | ||
| @@ -145,6 +146,9 @@ func runBrowser() { | |||||
| 145 | cfg := config.Get() | 146 | cfg := config.Get() | ||
| 146 | log.Printf("Config initialized") | 147 | log.Printf("Config initialized") | ||
| 147 | 148 | ||||
| 149 | + | // Detect keyboard layout/locale and hint to webkit layer | |||
| 150 | + | detectAndSetKeyboardLocale() | |||
| 151 | + | ||||
| 148 | // Initialize database | 152 | // Initialize database | ||
| 149 | database, err := db.InitDB(cfg.Database.Path) | 153 | database, err := db.InitDB(cfg.Database.Path) | ||
| 150 | if err != nil { | 154 | if err != nil { | ||
| @@ -344,8 +348,22 @@ func runBrowser() { | |||||
| 344 | ctx := context.Background() | 348 | ctx := context.Background() | ||
| 345 | if z, err := browserService.GetZoomLevel(ctx, u); err == nil { | 349 | if z, err := browserService.GetZoomLevel(ctx, u); err == nil { | ||
| 346 | _ = view.SetZoom(z) | 350 | _ = view.SetZoom(z) | ||
| 351 | + | key := services.ZoomKeyForLog(u) | |||
| 352 | + | log.Printf("[zoom] loaded %.2f for %s", z, key) | |||
| 347 | } | 353 | } | ||
| 348 | }) | 354 | }) | ||
| 355 | + | // Persist zoom changes per-domain | |||
| 356 | + | view.RegisterZoomChangedHandler(func(level float64) { | |||
| 357 | + | url := view.GetCurrentURL() | |||
| 358 | + | if url == "" { return } | |||
| 359 | + | ctx := context.Background() | |||
| 360 | + | if err := browserService.SetZoomLevel(ctx, url, level); err != nil { | |||
| 361 | + | log.Printf("[zoom] failed to save level %.2f for %s: %v", level, url, err) | |||
| 362 | + | return | |||
| 363 | + | } | |||
| 364 | + | key := services.ZoomKeyForLog(url) | |||
| 365 | + | log.Printf("[zoom] saved %.2f for %s", level, key) | |||
| 366 | + | }) | |||
| 349 | // Bridge UCM messages: navigate + history queries | 367 | // Bridge UCM messages: navigate + history queries | ||
| 350 | view.RegisterScriptMessageHandler(func(payload string) { | 368 | view.RegisterScriptMessageHandler(func(payload string) { | ||
| 351 | type msg struct { | 369 | type msg struct { | ||
| @@ -429,6 +447,21 @@ func runBrowser() { | |||||
| 429 | if m.Value != "" { log.Printf("[theme] page reported color-scheme: %s", m.Value) } | 447 | if m.Value != "" { log.Printf("[theme] page reported color-scheme: %s", m.Value) } | ||
| 430 | } | 448 | } | ||
| 431 | }) | 449 | }) | ||
| 450 | + | // Apply initial zoom setting and log it before showing the window | |||
| 451 | + | { | |||
| 452 | + | ctx := context.Background() | |||
| 453 | + | u := view.GetCurrentURL() | |||
| 454 | + | if u == "" { u = "dumb://homepage" } | |||
| 455 | + | if z, zerr := browserService.GetZoomLevel(ctx, u); zerr == nil { | |||
| 456 | + | if err := view.SetZoom(z); err != nil { | |||
| 457 | + | log.Printf("Warning: failed to set initial zoom: %v", err) | |||
| 458 | + | } else { | |||
| 459 | + | key := services.ZoomKeyForLog(u) | |||
| 460 | + | log.Printf("[zoom] loaded %.2f for %s", z, key) | |||
| 461 | + | } | |||
| 462 | + | } | |||
| 463 | + | } | |||
| 464 | + | ||||
| 432 | log.Printf("Showing WebView window…") | 465 | log.Printf("Showing WebView window…") | ||
| 433 | if err := view.Show(); err != nil { | 466 | if err := view.Show(); err != nil { | ||
| 434 | log.Printf("Warning: failed to show WebView: %v", err) | 467 | log.Printf("Warning: failed to show WebView: %v", err) | ||
| @@ -454,9 +487,11 @@ func runBrowser() { | |||||
| 454 | log.Printf("Warning: failed to load URL: %v", err) | 487 | log.Printf("Warning: failed to load URL: %v", err) | ||
| 455 | } | 488 | } | ||
| 456 | if z, zerr := browserService.GetZoomLevel(ctx, currentURL); zerr == nil { | 489 | if z, zerr := browserService.GetZoomLevel(ctx, currentURL); zerr == nil { | ||
| 457 | - | log.Printf("Applying stored zoom: %.2f", z) | |||
| 458 | if err := view.SetZoom(z); err != nil { | 490 | if err := view.SetZoom(z); err != nil { | ||
| 459 | log.Printf("Warning: failed to set zoom: %v", err) | 491 | log.Printf("Warning: failed to set zoom: %v", err) | ||
| 492 | + | } else { | |||
| 493 | + | key := services.ZoomKeyForLog(currentURL) | |||
| 494 | + | log.Printf("[zoom] loaded %.2f for %s", z, key) | |||
| 460 | } | 495 | } | ||
| 461 | } | 496 | } | ||
| 462 | // No JS injection; native accelerators handle zoom/devtools. | 497 | // No JS injection; native accelerators handle zoom/devtools. | ||
| @@ -485,3 +520,64 @@ func runBrowser() { | |||||
| 485 | log.Printf("Not entering GUI loop (non-CGO build)") | 520 | log.Printf("Not entering GUI loop (non-CGO build)") | ||
| 486 | } | 521 | } | ||
| 487 | } | 522 | } | ||
| 523 | + | ||||
| 524 | + | // detectAndSetKeyboardLocale attempts to determine the current keyboard layout | |||
| 525 | + | // and hints it to the webkit input layer for accelerator compatibility. | |||
| 526 | + | func detectAndSetKeyboardLocale() { | |||
| 527 | + | // 0) Explicit override | |||
| 528 | + | locale := os.Getenv("DUMBER_KEYBOARD_LOCALE") | |||
| 529 | + | if locale == "" { | |||
| 530 | + | locale = os.Getenv("DUMB_BROWSER_KEYBOARD_LOCALE") // legacy prefix | |||
| 531 | + | } | |||
| 532 | + | // 1) Environment | |||
| 533 | + | if locale == "" { locale = os.Getenv("LC_ALL") } | |||
| 534 | + | if locale == "" { locale = os.Getenv("LANG") } | |||
| 535 | + | if locale == "" { locale = os.Getenv("LC_CTYPE") } | |||
| 536 | + | // Trim variants | |||
| 537 | + | if i := strings.IndexByte(locale, '.'); i > 0 { locale = locale[:i] } | |||
| 538 | + | if i := strings.IndexByte(locale, '@'); i > 0 { locale = locale[:i] } | |||
| 539 | + | if i := strings.IndexByte(locale, '_'); i > 0 { locale = locale[:i] } | |||
| 540 | + | ||||
| 541 | + | // 2) Try XKB env override | |||
| 542 | + | if locale == "" { | |||
| 543 | + | locale = os.Getenv("XKB_DEFAULT_LAYOUT") | |||
| 544 | + | } | |||
| 545 | + | ||||
| 546 | + | // 3) Best-effort probe of setxkbmap/localectl (non-fatal) | |||
| 547 | + | if locale == "" { | |||
| 548 | + | if out, err := exec.Command("localectl", "status").Output(); err == nil { | |||
| 549 | + | s := string(out) | |||
| 550 | + | for _, line := range strings.Split(s, "\n") { | |||
| 551 | + | line = strings.TrimSpace(line) | |||
| 552 | + | if strings.HasPrefix(strings.ToLower(line), "x11 layout:") || strings.HasPrefix(strings.ToLower(line), "keyboard layout:") { | |||
| 553 | + | parts := strings.SplitN(line, ":", 2) | |||
| 554 | + | if len(parts) == 2 { | |||
| 555 | + | cand := strings.TrimSpace(parts[1]) | |||
| 556 | + | if cand != "" { locale = cand } | |||
| 557 | + | } | |||
| 558 | + | break | |||
| 559 | + | } | |||
| 560 | + | } | |||
| 561 | + | } | |||
| 562 | + | } | |||
| 563 | + | if locale == "" { | |||
| 564 | + | if out, err := exec.Command("setxkbmap", "-query").Output(); err == nil { | |||
| 565 | + | s := string(out) | |||
| 566 | + | for _, line := range strings.Split(s, "\n") { | |||
| 567 | + | line = strings.TrimSpace(line) | |||
| 568 | + | if strings.HasPrefix(strings.ToLower(line), "layout:") { | |||
| 569 | + | parts := strings.SplitN(line, ":", 2) | |||
| 570 | + | if len(parts) == 2 { | |||
| 571 | + | cand := strings.TrimSpace(parts[1]) | |||
| 572 | + | if cand != "" { locale = cand } | |||
| 573 | + | } | |||
| 574 | + | break | |||
| 575 | + | } | |||
| 576 | + | } | |||
| 577 | + | } | |||
| 578 | + | } | |||
| 579 | + | ||||
| 580 | + | if locale == "" { locale = "en" } | |||
| 581 | + | // No layout-specific remaps; log for diagnostics only | |||
| 582 | + | log.Printf("[locale] keyboard locale detected: %s", locale) | |||
| 583 | + | } | |||
M pkg/webkit/zoom.go +1 -0
| @@ -19,6 +19,7 @@ func (w *WebView) SetZoom(level float64) error { | |||
| 19 | 19 | level = zoomMax | |
| 20 | 20 | } | |
| 21 | 21 | w.zoom = level | |
| 22 | + | w.dispatchZoomChanged(w.zoom) | |
| 22 | 23 | return nil | |
| 23 | 24 | } | |
| 24 | 25 | ||
| @@ -19,6 +19,7 @@ func (w *WebView) SetZoom(level float64) error { | |||||
| 19 | level = zoomMax | 19 | level = zoomMax | ||
| 20 | } | 20 | } | ||
| 21 | w.zoom = level | 21 | w.zoom = level | ||
| 22 | + | w.dispatchZoomChanged(w.zoom) | |||
| 22 | return nil | 23 | return nil | ||
| 23 | } | 24 | } | ||
| 24 | 25 | ||||
M pkg/webkit/zoom_cgo.go +1 -0
| @@ -20,6 +20,7 @@ func (w *WebView) SetZoom(level float64) error { | |||
| 20 | 20 | } | |
| 21 | 21 | w.zoom = level | |
| 22 | 22 | C.webkit_web_view_set_zoom_level(w.native.wv, C.gdouble(level)) | |
| 23 | + | w.dispatchZoomChanged(w.zoom) | |
| 23 | 24 | return nil | |
| 24 | 25 | } | |
| 25 | 26 | ||
| @@ -20,6 +20,7 @@ func (w *WebView) SetZoom(level float64) error { | |||||
| 20 | } | 20 | } | ||
| 21 | w.zoom = level | 21 | w.zoom = level | ||
| 22 | C.webkit_web_view_set_zoom_level(w.native.wv, C.gdouble(level)) | 22 | C.webkit_web_view_set_zoom_level(w.native.wv, C.gdouble(level)) | ||
| 23 | + | w.dispatchZoomChanged(w.zoom) | |||
| 23 | return nil | 24 | return nil | ||
| 24 | } | 25 | } | ||
| 25 | 26 | ||||
M services/browser_service.go +3 -0
| @@ -483,3 +483,6 @@ func zoomKeyFromURL(raw string) string { | |||
| 483 | 483 | // extractDomain extracts the domain from a URL for zoom level storage | |
| 484 | 484 | // Domain-based zoom allows settings to persist across different pages of the same site | |
| 485 | 485 | // extractDomain is no longer used; zoom is stored per URL for contract parity. | |
| 486 | + | ||
| 487 | + | // ZoomKeyForLog exposes the derived zoom key (host or raw URL) for logging from other packages. | |
| 488 | + | func ZoomKeyForLog(raw string) string { return zoomKeyFromURL(raw) } | |
| @@ -483,3 +483,6 @@ func zoomKeyFromURL(raw string) string { | |||||
| 483 | // extractDomain extracts the domain from a URL for zoom level storage | 483 | // extractDomain extracts the domain from a URL for zoom level storage | ||
| 484 | // Domain-based zoom allows settings to persist across different pages of the same site | 484 | // Domain-based zoom allows settings to persist across different pages of the same site | ||
| 485 | // extractDomain is no longer used; zoom is stored per URL for contract parity. | 485 | // extractDomain is no longer used; zoom is stored per URL for contract parity. | ||
| 486 | + | ||||
| 487 | + | // ZoomKeyForLog exposes the derived zoom key (host or raw URL) for logging from other packages. | |||
| 488 | + | func ZoomKeyForLog(raw string) string { return zoomKeyFromURL(raw) } | |||