detached 2b27f37

feat(ui): add page refresh shortcuts and fix zoom toaster behavior

- Add Ctrl+R, Ctrl+Shift+R, and F5 keyboard shortcuts for page refresh
- Implement Reload() and ReloadBypassCache() WebKit methods
- Fix zoom toaster showing on every same-domain navigation
- Add _blank link handler to load popups in current window
- Update README with new keyboard shortcuts
commit
2b27f3776ce548529e1835d53ab4f1c590d87a70
author
bamen · · 2025-09-12 07:09 UTC
parent
db5f019
signature
unsigned
6 files · +131 -13
M CHANGELOG.md +17 -0
@@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file.
44
55 ## [Unreleased]
66
7+### Added
8+- Page refresh functionality with standard keyboard shortcuts:
9+ - **Ctrl+R** / **Cmd+R**: Reload current page
10+ - **Ctrl+Shift+R** / **Cmd+Shift+R**: Hard reload (bypass cache)
11+ - **F5**: Alternative reload key
12+ - WebKit integration with proper `webkit_web_view_reload()` and `webkit_web_view_reload_bypass_cache()` calls
13+
14+### Fixed
15+- Zoom level toast no longer shows on every page navigation within the same domain:
16+ - Added smart detection to differentiate user-initiated vs programmatic zoom changes
17+ - Toast now only appears when manually adjusting zoom or entering a new domain
18+ - Prevents annoying notifications during regular browsing within the same site
19+- Links with `target="_blank"` now load in current window instead of being ignored:
20+ - Added WebKit `create` signal handler to intercept new window requests
21+ - Redirects popup/new tab links to current window for seamless single-window browsing
22+ - Maintains user experience consistency in tab-less browser architecture
23+
724 ## [0.5.0] - 2025-09-12
825
926 ### Added
@@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file.
4 4
5 ## [Unreleased]5 ## [Unreleased]
6 6
7+### Added
8+- Page refresh functionality with standard keyboard shortcuts:
9+ - **Ctrl+R** / **Cmd+R**: Reload current page
10+ - **Ctrl+Shift+R** / **Cmd+Shift+R**: Hard reload (bypass cache)
11+ - **F5**: Alternative reload key
12+ - WebKit integration with proper `webkit_web_view_reload()` and `webkit_web_view_reload_bypass_cache()` calls
13+
14+### Fixed
15+- Zoom level toast no longer shows on every page navigation within the same domain:
16+ - Added smart detection to differentiate user-initiated vs programmatic zoom changes
17+ - Toast now only appears when manually adjusting zoom or entering a new domain
18+ - Prevents annoying notifications during regular browsing within the same site
19+- Links with `target="_blank"` now load in current window instead of being ignored:
20+ - Added WebKit `create` signal handler to intercept new window requests
21+ - Redirects popup/new tab links to current window for seamless single-window browsing
22+ - Maintains user experience consistency in tab-less browser architecture
23+
7 ## [0.5.0] - 2025-09-1224 ## [0.5.0] - 2025-09-12
8 25
9 ### Added26 ### Added
M README.md +3 -0
@@ -41,6 +41,9 @@ Dumber is a minimalist browser and launcher companion focused on speed and simpl
4141 | **Ctrl/Cmd+-** | Zoom Out | Works across keyboard layouts |
4242 | **Ctrl/Cmd+0** | Reset Zoom | Return to 100% zoom |
4343 | **Ctrl/Cmd+Shift+C** | Copy URL | Copy current URL to clipboard with toast |
44+| **Ctrl/Cmd+R** | Reload Page | Refresh current page |
45+| **Ctrl/Cmd+Shift+R** | Hard Reload | Refresh ignoring cache |
46+| **F5** | Reload Page | Alternative reload key |
4447 | **Alt+←** | Navigate Back | Go to previous page |
4548 | **Alt+→** | Navigate Forward | Go to next page |
4649
@@ -41,6 +41,9 @@ Dumber is a minimalist browser and launcher companion focused on speed and simpl
41 | **Ctrl/Cmd+-** | Zoom Out | Works across keyboard layouts |41 | **Ctrl/Cmd+-** | Zoom Out | Works across keyboard layouts |
42 | **Ctrl/Cmd+0** | Reset Zoom | Return to 100% zoom |42 | **Ctrl/Cmd+0** | Reset Zoom | Return to 100% zoom |
43 | **Ctrl/Cmd+Shift+C** | Copy URL | Copy current URL to clipboard with toast |43 | **Ctrl/Cmd+Shift+C** | Copy URL | Copy current URL to clipboard with toast |
44+| **Ctrl/Cmd+R** | Reload Page | Refresh current page |
45+| **Ctrl/Cmd+Shift+R** | Hard Reload | Refresh ignoring cache |
46+| **F5** | Reload Page | Alternative reload key |
44 | **Alt+←** | Navigate Back | Go to previous page |47 | **Alt+←** | Navigate Back | Go to previous page |
45 | **Alt+→** | Navigate Forward | Go to next page |48 | **Alt+→** | Navigate Forward | Go to next page |
46 49
M main.go +58 -13
@@ -260,6 +260,12 @@ func runBrowser() {
260260 // Track current URL for zoom persistence
261261 // Initialize to the initial homepage so shortcuts work immediately
262262 var currentURL string = "dumb://homepage"
263+
264+ // Track last zoom domain to avoid showing toaster on every navigation within same domain
265+ var lastZoomDomain string
266+
267+ // Flag to prevent showing toast when programmatically setting zoom (not user-initiated)
268+ var programmaticZoomChange bool
263269
264270 // Register custom scheme resolver for dumb://
265271 webkit.SetURISchemeResolver(func(uri string) (string, []byte, bool) {
@@ -471,10 +477,28 @@ func runBrowser() {
471477 return
472478 }
473479 ctx := context.Background()
480+ currentDomain := services.ZoomKeyForLog(u)
481+
474482 if z, err := browserService.GetZoomLevel(ctx, u); err == nil {
483+ programmaticZoomChange = true
475484 _ = view.SetZoom(z)
476- key := services.ZoomKeyForLog(u)
477- log.Printf("[zoom] loaded %.2f for %s", z, key)
485+ programmaticZoomChange = false
486+ log.Printf("[zoom] loaded %.2f for %s", z, currentDomain)
487+
488+ // Only show toast when entering a new domain (not on first load)
489+ if currentDomain != lastZoomDomain && lastZoomDomain != "" {
490+ js := fmt.Sprintf(`try {
491+ if (typeof window.__dumber_showZoomToast === 'function') {
492+ window.__dumber_showZoomToast(%f);
493+ }
494+ } catch(e) {
495+ console.error('[dumber] Zoom toast error:', e);
496+ }`, z)
497+ if err := view.InjectScript(js); err != nil {
498+ log.Printf("[zoom] failed to show domain change toast: %v", err)
499+ }
500+ }
501+ lastZoomDomain = currentDomain
478502 }
479503 })
480504 // Persist zoom changes per-domain
@@ -491,18 +515,20 @@ func runBrowser() {
491515 key := services.ZoomKeyForLog(url)
492516 log.Printf("[zoom] saved %.2f for %s", level, key)
493517
494- // Show zoom toast notification
495- js := fmt.Sprintf(`try {
496- if (typeof window.__dumber_showZoomToast === 'function') {
497- window.__dumber_showZoomToast(%f);
498- } else {
499- console.warn('[dumber] Zoom toast function not available yet');
518+ // Only show toast for user-initiated zoom changes, not programmatic ones
519+ if !programmaticZoomChange {
520+ js := fmt.Sprintf(`try {
521+ if (typeof window.__dumber_showZoomToast === 'function') {
522+ window.__dumber_showZoomToast(%f);
523+ } else {
524+ console.warn('[dumber] Zoom toast function not available yet');
525+ }
526+ } catch(e) {
527+ console.error('[dumber] Zoom toast error:', e);
528+ }`, level)
529+ if err := view.InjectScript(js); err != nil {
530+ log.Printf("[zoom] failed to show toast: %v", err)
500531 }
501- } catch(e) {
502- console.error('[dumber] Zoom toast error:', e);
503- }`, level)
504- if err := view.InjectScript(js); err != nil {
505- log.Printf("[zoom] failed to show toast: %v", err)
506532 }
507533 })
508534 // Bridge UCM messages: navigate + history queries
@@ -532,7 +558,9 @@ func runBrowser() {
532558 }
533559 _ = view.LoadURL(currentURL)
534560 if z, zerr := browserService.GetZoomLevel(ctx, currentURL); zerr == nil {
561+ programmaticZoomChange = true
535562 _ = view.SetZoom(z)
563+ programmaticZoomChange = false
536564 }
537565 }
538566 case "query":
@@ -627,12 +655,14 @@ func runBrowser() {
627655 u = "dumb://homepage"
628656 }
629657 if z, zerr := browserService.GetZoomLevel(ctx, u); zerr == nil {
658+ programmaticZoomChange = true
630659 if err := view.SetZoom(z); err != nil {
631660 log.Printf("Warning: failed to set initial zoom: %v", err)
632661 } else {
633662 key := services.ZoomKeyForLog(u)
634663 log.Printf("[zoom] loaded %.2f for %s", z, key)
635664 }
665+ programmaticZoomChange = false
636666 }
637667 }
638668
@@ -661,12 +691,14 @@ func runBrowser() {
661691 log.Printf("Warning: failed to load URL: %v", err)
662692 }
663693 if z, zerr := browserService.GetZoomLevel(ctx, currentURL); zerr == nil {
694+ programmaticZoomChange = true
664695 if err := view.SetZoom(z); err != nil {
665696 log.Printf("Warning: failed to set zoom: %v", err)
666697 } else {
667698 key := services.ZoomKeyForLog(currentURL)
668699 log.Printf("[zoom] loaded %.2f for %s", z, key)
669700 }
701+ programmaticZoomChange = false
670702 }
671703 // No JS injection; native accelerators handle zoom/devtools.
672704 }
@@ -700,6 +732,19 @@ func runBrowser() {
700732 _ = view.InjectScript(`window.__dumber_showToast && window.__dumber_showToast("URL copied to clipboard", 2000)`)
701733 }
702734 })
735+ // Page refresh shortcuts
736+ _ = view.RegisterKeyboardShortcut("cmdorctrl+r", func() {
737+ log.Printf("Shortcut: Reload page")
738+ _ = view.Reload()
739+ })
740+ _ = view.RegisterKeyboardShortcut("cmdorctrl+shift+r", func() {
741+ log.Printf("Shortcut: Hard reload page")
742+ _ = view.ReloadBypassCache()
743+ })
744+ _ = view.RegisterKeyboardShortcut("F5", func() {
745+ log.Printf("Shortcut: F5 reload")
746+ _ = view.Reload()
747+ })
703748 // Zoom handled natively in webkit package (built-in shortcuts)
704749 }
705750
@@ -260,6 +260,12 @@ func runBrowser() {
260 // Track current URL for zoom persistence260 // Track current URL for zoom persistence
261 // Initialize to the initial homepage so shortcuts work immediately261 // Initialize to the initial homepage so shortcuts work immediately
262 var currentURL string = "dumb://homepage"262 var currentURL string = "dumb://homepage"
263+
264+ // Track last zoom domain to avoid showing toaster on every navigation within same domain
265+ var lastZoomDomain string
266+
267+ // Flag to prevent showing toast when programmatically setting zoom (not user-initiated)
268+ var programmaticZoomChange bool
263 269
264 // Register custom scheme resolver for dumb://270 // Register custom scheme resolver for dumb://
265 webkit.SetURISchemeResolver(func(uri string) (string, []byte, bool) {271 webkit.SetURISchemeResolver(func(uri string) (string, []byte, bool) {
@@ -471,10 +477,28 @@ func runBrowser() {
471 return477 return
472 }478 }
473 ctx := context.Background()479 ctx := context.Background()
480+ currentDomain := services.ZoomKeyForLog(u)
481+
474 if z, err := browserService.GetZoomLevel(ctx, u); err == nil {482 if z, err := browserService.GetZoomLevel(ctx, u); err == nil {
483+ programmaticZoomChange = true
475 _ = view.SetZoom(z)484 _ = view.SetZoom(z)
476- key := services.ZoomKeyForLog(u)485+ programmaticZoomChange = false
477- log.Printf("[zoom] loaded %.2f for %s", z, key)486+ log.Printf("[zoom] loaded %.2f for %s", z, currentDomain)
487+
488+ // Only show toast when entering a new domain (not on first load)
489+ if currentDomain != lastZoomDomain && lastZoomDomain != "" {
490+ js := fmt.Sprintf(`try {
491+ if (typeof window.__dumber_showZoomToast === 'function') {
492+ window.__dumber_showZoomToast(%f);
493+ }
494+ } catch(e) {
495+ console.error('[dumber] Zoom toast error:', e);
496+ }`, z)
497+ if err := view.InjectScript(js); err != nil {
498+ log.Printf("[zoom] failed to show domain change toast: %v", err)
499+ }
500+ }
501+ lastZoomDomain = currentDomain
478 }502 }
479 })503 })
480 // Persist zoom changes per-domain504 // Persist zoom changes per-domain
@@ -491,18 +515,20 @@ func runBrowser() {
491 key := services.ZoomKeyForLog(url)515 key := services.ZoomKeyForLog(url)
492 log.Printf("[zoom] saved %.2f for %s", level, key)516 log.Printf("[zoom] saved %.2f for %s", level, key)
493 517
494- // Show zoom toast notification518+ // Only show toast for user-initiated zoom changes, not programmatic ones
495- js := fmt.Sprintf(`try { 519+ if !programmaticZoomChange {
496- if (typeof window.__dumber_showZoomToast === 'function') {520+ js := fmt.Sprintf(`try {
497- window.__dumber_showZoomToast(%f);521+ if (typeof window.__dumber_showZoomToast === 'function') {
498- } else {522+ window.__dumber_showZoomToast(%f);
499- console.warn('[dumber] Zoom toast function not available yet');523+ } else {
524+ console.warn('[dumber] Zoom toast function not available yet');
525+ }
526+ } catch(e) {
527+ console.error('[dumber] Zoom toast error:', e);
528+ }`, level)
529+ if err := view.InjectScript(js); err != nil {
530+ log.Printf("[zoom] failed to show toast: %v", err)
500 }531 }
501- } catch(e) {
502- console.error('[dumber] Zoom toast error:', e);
503- }`, level)
504- if err := view.InjectScript(js); err != nil {
505- log.Printf("[zoom] failed to show toast: %v", err)
506 }532 }
507 })533 })
508 // Bridge UCM messages: navigate + history queries534 // Bridge UCM messages: navigate + history queries
@@ -532,7 +558,9 @@ func runBrowser() {
532 }558 }
533 _ = view.LoadURL(currentURL)559 _ = view.LoadURL(currentURL)
534 if z, zerr := browserService.GetZoomLevel(ctx, currentURL); zerr == nil {560 if z, zerr := browserService.GetZoomLevel(ctx, currentURL); zerr == nil {
561+ programmaticZoomChange = true
535 _ = view.SetZoom(z)562 _ = view.SetZoom(z)
563+ programmaticZoomChange = false
536 }564 }
537 }565 }
538 case "query":566 case "query":
@@ -627,12 +655,14 @@ func runBrowser() {
627 u = "dumb://homepage"655 u = "dumb://homepage"
628 }656 }
629 if z, zerr := browserService.GetZoomLevel(ctx, u); zerr == nil {657 if z, zerr := browserService.GetZoomLevel(ctx, u); zerr == nil {
658+ programmaticZoomChange = true
630 if err := view.SetZoom(z); err != nil {659 if err := view.SetZoom(z); err != nil {
631 log.Printf("Warning: failed to set initial zoom: %v", err)660 log.Printf("Warning: failed to set initial zoom: %v", err)
632 } else {661 } else {
633 key := services.ZoomKeyForLog(u)662 key := services.ZoomKeyForLog(u)
634 log.Printf("[zoom] loaded %.2f for %s", z, key)663 log.Printf("[zoom] loaded %.2f for %s", z, key)
635 }664 }
665+ programmaticZoomChange = false
636 }666 }
637 }667 }
638 668
@@ -661,12 +691,14 @@ func runBrowser() {
661 log.Printf("Warning: failed to load URL: %v", err)691 log.Printf("Warning: failed to load URL: %v", err)
662 }692 }
663 if z, zerr := browserService.GetZoomLevel(ctx, currentURL); zerr == nil {693 if z, zerr := browserService.GetZoomLevel(ctx, currentURL); zerr == nil {
694+ programmaticZoomChange = true
664 if err := view.SetZoom(z); err != nil {695 if err := view.SetZoom(z); err != nil {
665 log.Printf("Warning: failed to set zoom: %v", err)696 log.Printf("Warning: failed to set zoom: %v", err)
666 } else {697 } else {
667 key := services.ZoomKeyForLog(currentURL)698 key := services.ZoomKeyForLog(currentURL)
668 log.Printf("[zoom] loaded %.2f for %s", z, key)699 log.Printf("[zoom] loaded %.2f for %s", z, key)
669 }700 }
701+ programmaticZoomChange = false
670 }702 }
671 // No JS injection; native accelerators handle zoom/devtools.703 // No JS injection; native accelerators handle zoom/devtools.
672 }704 }
@@ -700,6 +732,19 @@ func runBrowser() {
700 _ = view.InjectScript(`window.__dumber_showToast && window.__dumber_showToast("URL copied to clipboard", 2000)`)732 _ = view.InjectScript(`window.__dumber_showToast && window.__dumber_showToast("URL copied to clipboard", 2000)`)
701 }733 }
702 })734 })
735+ // Page refresh shortcuts
736+ _ = view.RegisterKeyboardShortcut("cmdorctrl+r", func() {
737+ log.Printf("Shortcut: Reload page")
738+ _ = view.Reload()
739+ })
740+ _ = view.RegisterKeyboardShortcut("cmdorctrl+shift+r", func() {
741+ log.Printf("Shortcut: Hard reload page")
742+ _ = view.ReloadBypassCache()
743+ })
744+ _ = view.RegisterKeyboardShortcut("F5", func() {
745+ log.Printf("Shortcut: F5 reload")
746+ _ = view.Reload()
747+ })
703 // Zoom handled natively in webkit package (built-in shortcuts)748 // Zoom handled natively in webkit package (built-in shortcuts)
704 }749 }
705 750
M pkg/webkit/keyboard_cgo.go +6 -0
@@ -145,8 +145,14 @@ func dispatchAccelerator(uid uintptr, keyval uint, state uint) {
145145 keyName = "ArrowRight"
146146 case uint(C.GDK_KEY_F12):
147147 keyName = "F12"
148+ case uint(C.GDK_KEY_F5):
149+ keyName = "F5"
148150 case uint(C.GDK_KEY_c), uint(C.GDK_KEY_C):
149151 keyName = "c"
152+ case uint(C.GDK_KEY_r), uint(C.GDK_KEY_R):
153+ keyName = "r"
154+ case uint(C.GDK_KEY_l), uint(C.GDK_KEY_L):
155+ keyName = "l"
150156 default:
151157 // If Control held, log unknown keyval for diagnostics
152158 if ctrl {
@@ -145,8 +145,14 @@ func dispatchAccelerator(uid uintptr, keyval uint, state uint) {
145 keyName = "ArrowRight"145 keyName = "ArrowRight"
146 case uint(C.GDK_KEY_F12):146 case uint(C.GDK_KEY_F12):
147 keyName = "F12"147 keyName = "F12"
148+ case uint(C.GDK_KEY_F5):
149+ keyName = "F5"
148 case uint(C.GDK_KEY_c), uint(C.GDK_KEY_C):150 case uint(C.GDK_KEY_c), uint(C.GDK_KEY_C):
149 keyName = "c"151 keyName = "c"
152+ case uint(C.GDK_KEY_r), uint(C.GDK_KEY_R):
153+ keyName = "r"
154+ case uint(C.GDK_KEY_l), uint(C.GDK_KEY_L):
155+ keyName = "l"
150 default:156 default:
151 // If Control held, log unknown keyval for diagnostics157 // If Control held, log unknown keyval for diagnostics
152 if ctrl {158 if ctrl {
M pkg/webkit/webview.go +6 -0
@@ -93,6 +93,12 @@ func (w *WebView) GoBack() error { return ErrNotImplemented }
9393 // GoForward is not supported in the non-CGO stub.
9494 func (w *WebView) GoForward() error { return ErrNotImplemented }
9595
96+// Reload is not supported in the non-CGO stub.
97+func (w *WebView) Reload() error { return ErrNotImplemented }
98+
99+// ReloadBypassCache is not supported in the non-CGO stub.
100+func (w *WebView) ReloadBypassCache() error { return ErrNotImplemented }
101+
96102 // ShowDevTools is a no-op in the non-CGO build.
97103 func (w *WebView) ShowDevTools() error { return nil }
98104
@@ -93,6 +93,12 @@ func (w *WebView) GoBack() error { return ErrNotImplemented }
93 // GoForward is not supported in the non-CGO stub.93 // GoForward is not supported in the non-CGO stub.
94 func (w *WebView) GoForward() error { return ErrNotImplemented }94 func (w *WebView) GoForward() error { return ErrNotImplemented }
95 95
96+// Reload is not supported in the non-CGO stub.
97+func (w *WebView) Reload() error { return ErrNotImplemented }
98+
99+// ReloadBypassCache is not supported in the non-CGO stub.
100+func (w *WebView) ReloadBypassCache() error { return ErrNotImplemented }
101+
96 // ShowDevTools is a no-op in the non-CGO build.102 // ShowDevTools is a no-op in the non-CGO build.
97 func (w *WebView) ShowDevTools() error { return nil }103 func (w *WebView) ShowDevTools() error { return nil }
98 104
M pkg/webkit/webview_cgo.go +41 -0
@@ -38,6 +38,26 @@ static gboolean on_load_failed_with_tls_errors(WebKitWebView *web_view,
3838 GTlsCertificate *certificate,
3939 GTlsCertificateFlags errors,
4040 gpointer user_data);
41+
42+// Handle create signal for _blank links - redirect to current window
43+static WebKitWebView* on_create_new_window(WebKitWebView* web_view,
44+ WebKitNavigationAction* navigation_action,
45+ gpointer user_data) {
46+ (void)user_data;
47+
48+ // Get the request from navigation action
49+ WebKitURIRequest* request = webkit_navigation_action_get_request(navigation_action);
50+ if (request) {
51+ const gchar* uri = webkit_uri_request_get_uri(request);
52+ if (uri) {
53+ // Load in current window instead of creating new one
54+ webkit_web_view_load_uri(web_view, uri);
55+ }
56+ }
57+
58+ // Return NULL to prevent new window creation
59+ return NULL;
60+}
4161 static void connect_tls_error_handler(WebKitWebView* wv);
4262
4363 // Dialog callback structure for TLS warnings
@@ -116,6 +136,9 @@ static GtkWidget* new_webview_with_ucm_and_session(const char* data_dir, const c
116136 GdkRGBA black_color = { 0.0, 0.0, 0.0, 1.0 };
117137 webkit_web_view_set_background_color(WEBKIT_WEB_VIEW(w), &black_color);
118138
139+ // Handle create signal to redirect _blank links to current window
140+ g_signal_connect(G_OBJECT(w), "create", G_CALLBACK(on_create_new_window), NULL);
141+
119142 // Connect TLS error handler
120143 connect_tls_error_handler(WEBKIT_WEB_VIEW(w));
121144 if (out_ucm) { *out_ucm = u; }
@@ -1034,6 +1057,24 @@ func (w *WebView) GoForward() error {
10341057 return nil
10351058 }
10361059
1060+func (w *WebView) Reload() error {
1061+ if w == nil || w.destroyed || w.native == nil || w.native.wv == nil {
1062+ return ErrNotImplemented
1063+ }
1064+ C.webkit_web_view_reload(w.native.wv)
1065+ log.Printf("[webkit] Reload page")
1066+ return nil
1067+}
1068+
1069+func (w *WebView) ReloadBypassCache() error {
1070+ if w == nil || w.destroyed || w.native == nil || w.native.wv == nil {
1071+ return ErrNotImplemented
1072+ }
1073+ C.webkit_web_view_reload_bypass_cache(w.native.wv)
1074+ log.Printf("[webkit] Hard reload page (bypass cache)")
1075+ return nil
1076+}
1077+
10371078 // RegisterScriptMessageHandler registers a callback invoked when the content script posts a message.
10381079 func (w *WebView) RegisterScriptMessageHandler(cb func(payload string)) { w.msgHandler = cb }
10391080
@@ -38,6 +38,26 @@ static gboolean on_load_failed_with_tls_errors(WebKitWebView *web_view,
38 GTlsCertificate *certificate,38 GTlsCertificate *certificate,
39 GTlsCertificateFlags errors,39 GTlsCertificateFlags errors,
40 gpointer user_data);40 gpointer user_data);
41+
42+// Handle create signal for _blank links - redirect to current window
43+static WebKitWebView* on_create_new_window(WebKitWebView* web_view,
44+ WebKitNavigationAction* navigation_action,
45+ gpointer user_data) {
46+ (void)user_data;
47+
48+ // Get the request from navigation action
49+ WebKitURIRequest* request = webkit_navigation_action_get_request(navigation_action);
50+ if (request) {
51+ const gchar* uri = webkit_uri_request_get_uri(request);
52+ if (uri) {
53+ // Load in current window instead of creating new one
54+ webkit_web_view_load_uri(web_view, uri);
55+ }
56+ }
57+
58+ // Return NULL to prevent new window creation
59+ return NULL;
60+}
41 static void connect_tls_error_handler(WebKitWebView* wv);61 static void connect_tls_error_handler(WebKitWebView* wv);
42 62
43 // Dialog callback structure for TLS warnings63 // Dialog callback structure for TLS warnings
@@ -116,6 +136,9 @@ static GtkWidget* new_webview_with_ucm_and_session(const char* data_dir, const c
116 GdkRGBA black_color = { 0.0, 0.0, 0.0, 1.0 };136 GdkRGBA black_color = { 0.0, 0.0, 0.0, 1.0 };
117 webkit_web_view_set_background_color(WEBKIT_WEB_VIEW(w), &black_color);137 webkit_web_view_set_background_color(WEBKIT_WEB_VIEW(w), &black_color);
118 138
139+ // Handle create signal to redirect _blank links to current window
140+ g_signal_connect(G_OBJECT(w), "create", G_CALLBACK(on_create_new_window), NULL);
141+
119 // Connect TLS error handler142 // Connect TLS error handler
120 connect_tls_error_handler(WEBKIT_WEB_VIEW(w));143 connect_tls_error_handler(WEBKIT_WEB_VIEW(w));
121 if (out_ucm) { *out_ucm = u; }144 if (out_ucm) { *out_ucm = u; }
@@ -1034,6 +1057,24 @@ func (w *WebView) GoForward() error {
1034 return nil1057 return nil
1035 }1058 }
1036 1059
1060+func (w *WebView) Reload() error {
1061+ if w == nil || w.destroyed || w.native == nil || w.native.wv == nil {
1062+ return ErrNotImplemented
1063+ }
1064+ C.webkit_web_view_reload(w.native.wv)
1065+ log.Printf("[webkit] Reload page")
1066+ return nil
1067+}
1068+
1069+func (w *WebView) ReloadBypassCache() error {
1070+ if w == nil || w.destroyed || w.native == nil || w.native.wv == nil {
1071+ return ErrNotImplemented
1072+ }
1073+ C.webkit_web_view_reload_bypass_cache(w.native.wv)
1074+ log.Printf("[webkit] Hard reload page (bypass cache)")
1075+ return nil
1076+}
1077+
1037 // RegisterScriptMessageHandler registers a callback invoked when the content script posts a message.1078 // RegisterScriptMessageHandler registers a callback invoked when the content script posts a message.
1038 func (w *WebView) RegisterScriptMessageHandler(cb func(payload string)) { w.msgHandler = cb }1079 func (w *WebView) RegisterScriptMessageHandler(cb func(payload string)) { w.msgHandler = cb }
1039 1080

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