detached 2322edd

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 (
1313 "path/filepath"
1414 "strings"
1515 "strconv"
16+ "os/exec"
1617
1718
1819 "github.com/bnema/dumber/internal/cli"
@@ -145,6 +146,9 @@ func runBrowser() {
145146 cfg := config.Get()
146147 log.Printf("Config initialized")
147148
149+ // Detect keyboard layout/locale and hint to webkit layer
150+ detectAndSetKeyboardLocale()
151+
148152 // Initialize database
149153 database, err := db.InitDB(cfg.Database.Path)
150154 if err != nil {
@@ -344,8 +348,22 @@ func runBrowser() {
344348 ctx := context.Background()
345349 if z, err := browserService.GetZoomLevel(ctx, u); err == nil {
346350 _ = view.SetZoom(z)
351+ key := services.ZoomKeyForLog(u)
352+ log.Printf("[zoom] loaded %.2f for %s", z, key)
347353 }
348354 })
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+ })
349367 // Bridge UCM messages: navigate + history queries
350368 view.RegisterScriptMessageHandler(func(payload string) {
351369 type msg struct {
@@ -429,6 +447,21 @@ func runBrowser() {
429447 if m.Value != "" { log.Printf("[theme] page reported color-scheme: %s", m.Value) }
430448 }
431449 })
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+
432465 log.Printf("Showing WebView window…")
433466 if err := view.Show(); err != nil {
434467 log.Printf("Warning: failed to show WebView: %v", err)
@@ -454,9 +487,11 @@ func runBrowser() {
454487 log.Printf("Warning: failed to load URL: %v", err)
455488 }
456489 if z, zerr := browserService.GetZoomLevel(ctx, currentURL); zerr == nil {
457- log.Printf("Applying stored zoom: %.2f", z)
458490 if err := view.SetZoom(z); err != nil {
459491 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)
460495 }
461496 }
462497 // No JS injection; native accelerators handle zoom/devtools.
@@ -485,3 +520,64 @@ func runBrowser() {
485520 log.Printf("Not entering GUI loop (non-CGO build)")
486521 }
487522 }
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 database152 // 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 queries367 // 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 {
1919 level = zoomMax
2020 }
2121 w.zoom = level
22+ w.dispatchZoomChanged(w.zoom)
2223 return nil
2324 }
2425
@@ -19,6 +19,7 @@ func (w *WebView) SetZoom(level float64) error {
19 level = zoomMax19 level = zoomMax
20 }20 }
21 w.zoom = level21 w.zoom = level
22+ w.dispatchZoomChanged(w.zoom)
22 return nil23 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 {
2020 }
2121 w.zoom = level
2222 C.webkit_web_view_set_zoom_level(w.native.wv, C.gdouble(level))
23+ w.dispatchZoomChanged(w.zoom)
2324 return nil
2425 }
2526
@@ -20,6 +20,7 @@ func (w *WebView) SetZoom(level float64) error {
20 }20 }
21 w.zoom = level21 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 nil24 return nil
24 }25 }
25 26
M services/browser_service.go +3 -0
@@ -483,3 +483,6 @@ func zoomKeyFromURL(raw string) string {
483483 // extractDomain extracts the domain from a URL for zoom level storage
484484 // Domain-based zoom allows settings to persist across different pages of the same site
485485 // 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 storage483 // 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 site484 // 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) }

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