v0.11.0 dc50343

keyboard_cgo.go RawHistoryFolder

1//go:build webkit_cgo
2
3package webkit
4
5/*
6#cgo pkg-config: gtk4 webkitgtk-6.0
7#include <gtk/gtk.h>
8#include <gdk/gdk.h>
9#include <webkit/webkit.h>
10
11// Declarations for Go callbacks
12extern gboolean goOnKeyPress(unsigned long id, unsigned int keyval, GdkModifierType state);
13extern void goOnButtonPress(unsigned long id, unsigned int button, GdkModifierType state);
14extern void goOnScroll(unsigned long id, double dx, double dy, GdkModifierType state);
15
16// Key controller callback -> forward to Go
17static gboolean on_key_pressed(GtkEventControllerKey* controller, guint keyval, guint keycode, GdkModifierType state, gpointer user_data) {
18    (void)controller; (void)keycode;
19    return goOnKeyPress((unsigned long)user_data, keyval, state);
20}
21
22// Mouse click gesture -> forward side buttons to Go
23static void on_click_pressed(GtkGestureClick* gesture, gint n_press, gdouble x, gdouble y, gpointer user_data) {
24    (void)n_press; (void)x; (void)y;
25    guint button = gtk_gesture_single_get_current_button(GTK_GESTURE_SINGLE(gesture));
26    // Pass 0 for modifier state (not provided via gesture API)
27    goOnButtonPress((unsigned long)user_data, button, 0);
28}
29
30// Attach controllers to a widget (GTK4)
31static void attach_key_controller(GtkWidget* widget, unsigned long id) {
32    if (!widget) return;
33    GtkEventController* keyc = gtk_event_controller_key_new();
34    // Capture phase so we see keys even if child consumes them (e.g., WebKit)
35    gtk_event_controller_set_propagation_phase(keyc, GTK_PHASE_CAPTURE);
36    g_signal_connect_data(keyc, "key-pressed", G_CALLBACK(on_key_pressed), (gpointer)id, NULL, 0);
37    gtk_widget_add_controller(widget, keyc);
38}
39
40static void attach_mouse_gesture(GtkWidget* widget, unsigned long id) {
41    if (!widget) return;
42    GtkGesture* click = gtk_gesture_click_new();
43    g_signal_connect_data(click, "pressed", G_CALLBACK(on_click_pressed), (gpointer)id, NULL, 0);
44    gtk_widget_add_controller(widget, GTK_EVENT_CONTROLLER(click));
45}
46
47// Legacy controller to capture side buttons (8/9) reliably in GTK4
48static gboolean on_legacy_event(GtkEventController* controller, GdkEvent* event, gpointer user_data) {
49    (void)controller;
50    if (!event) return FALSE;
51    GdkEventType type = gdk_event_get_event_type(event);
52    if (type == GDK_BUTTON_PRESS) {
53        guint button = gdk_button_event_get_button(event);
54        GdkModifierType state = gdk_event_get_modifier_state(event);
55        if (button == 8 || button == 9) {
56            goOnButtonPress((unsigned long)user_data, button, state);
57            return TRUE; // consume side buttons
58        }
59        return FALSE; // let WebKit handle normal clicks
60    } else if (type == GDK_SCROLL) {
61        double dx = 0.0, dy = 0.0;
62        gdk_scroll_event_get_deltas(event, &dx, &dy);
63        GdkModifierType state = gdk_event_get_modifier_state(event);
64        goOnScroll((unsigned long)user_data, dx, dy, state);
65        return FALSE; // don't steal regular scrolling
66    }
67    return FALSE;
68}
69
70static void attach_mouse_legacy(GtkWidget* widget, unsigned long id) {
71    if (!widget) return;
72    GtkEventController* legacy = gtk_event_controller_legacy_new();
73    gtk_event_controller_set_propagation_phase(legacy, GTK_PHASE_CAPTURE);
74    g_signal_connect_data(legacy, "event", G_CALLBACK(on_legacy_event), (gpointer)id, NULL, 0);
75    gtk_widget_add_controller(widget, legacy);
76}
77*/
78import "C"
79
80import (
81	"encoding/json"
82	"fmt"
83	"log"
84	"strings"
85	"sync"
86	"sync/atomic"
87	"time"
88)
89
90// Registry of accelerators per view id.
91type shortcutRegistry map[string]func()
92
93var (
94	regMu         sync.RWMutex
95	viewShortcuts = make(map[uintptr]shortcutRegistry)
96	viewIDCounter uint64
97	viewByID      = make(map[uintptr]*WebView)
98	lastKeyTime   = make(map[uintptr]map[string]time.Time)
99)
100
101const shortcutDebounceWindow = 120 * time.Millisecond
102
103// shouldDebounceShortcut returns true when the given key should be skipped
104// because it was delivered within the debounce window for the same view.
105func shouldDebounceShortcut(uid uintptr, key string, now time.Time) bool {
106	regMu.Lock()
107	defer regMu.Unlock()
108
109	if _, ok := lastKeyTime[uid]; !ok {
110		lastKeyTime[uid] = make(map[string]time.Time)
111	}
112	last := lastKeyTime[uid][key]
113	if now.Sub(last) < shortcutDebounceWindow {
114		return true
115	}
116	lastKeyTime[uid][key] = now
117	return false
118}
119
120// Locale hint function removed; no layout-specific remaps.
121
122// RegisterKeyboardShortcut registers a callback under an accelerator string.
123func (w *WebView) RegisterKeyboardShortcut(accel string, callback func()) error {
124	if w == nil || w.destroyed {
125		return ErrNotImplemented
126	}
127	if accel == "" || callback == nil {
128		return fmt.Errorf("invalid shortcut registration")
129	}
130	regMu.Lock()
131	if _, ok := viewShortcuts[w.id]; !ok {
132		viewShortcuts[w.id] = make(shortcutRegistry)
133	}
134	viewShortcuts[w.id][accel] = callback
135	regMu.Unlock()
136
137	// Register in global shortcuts registry for automatic webpage blocking
138	RegisterGlobalShortcut(accel)
139
140	return nil
141}
142
143//export goOnKeyPress
144func goOnKeyPress(id C.ulong, keyval C.guint, state C.GdkModifierType) C.gboolean {
145	// Normalize and dispatch
146	uid := uintptr(id)
147	kv := uint(keyval)
148	st := uint(state)
149
150	if dispatchAccelerator(uid, kv, st) {
151		return C.gboolean(1)
152	}
153	return C.gboolean(0)
154}
155
156// getCLevelBridgeShortcut returns the shortcut string for shortcuts that need C-level bridge handling.
157// These shortcuts are blocked at the webpage level and have their callbacks triggered directly from C code.
158// Returns empty string if the shortcut doesn't need C-level handling.
159func getCLevelBridgeShortcut(ctrl, alt, shift bool, keyName string) string {
160	// Alt+Arrow shortcuts - for workspace pane navigation
161	if alt && !ctrl && !shift {
162		switch keyName {
163		case "ArrowLeft":
164			return "alt+arrowleft"
165		case "ArrowRight":
166			return "alt+arrowright"
167		case "ArrowUp":
168			return "alt+arrowup"
169		case "ArrowDown":
170			return "alt+arrowdown"
171		}
172	}
173
174	// Ctrl+W shortcut - for closing current pane via workspace manager
175	if ctrl && !alt && !shift && keyName == "w" {
176		return "ctrl+w"
177	}
178
179	// Add other C-level bridge shortcuts here as needed
180	// Note: Most shortcuts (Ctrl+L, Ctrl+F, zoom) should NOT be here
181	// They work fine through normal GTK4 handling without C-level intervention
182
183	return ""
184}
185
186func dispatchAccelerator(uid uintptr, keyval uint, state uint) bool {
187	ctrl := (state & uint(C.GDK_CONTROL_MASK)) != 0
188	alt := (state & uint(C.GDK_ALT_MASK)) != 0
189	shift := (state & uint(C.GDK_SHIFT_MASK)) != 0
190	// Map keyval to string names
191	var keyName string
192	logMinusEvent := false
193	switch keyval {
194	case uint(C.GDK_KEY_minus), uint(C.GDK_KEY_KP_Subtract), uint(C.GDK_KEY_underscore):
195		// Normalize minus; log raw detection for diagnostics
196		logMinusEvent = true
197		keyName = "-"
198	case uint(C.GDK_KEY_equal), uint(C.GDK_KEY_KP_Add):
199		keyName = "="
200	case uint(C.GDK_KEY_0), uint(C.GDK_KEY_KP_0):
201		keyName = "0"
202	case uint(C.GDK_KEY_Left):
203		keyName = "ArrowLeft"
204	case uint(C.GDK_KEY_Right):
205		keyName = "ArrowRight"
206	case uint(C.GDK_KEY_Up):
207		keyName = "ArrowUp"
208	case uint(C.GDK_KEY_Down):
209		keyName = "ArrowDown"
210	case uint(C.GDK_KEY_KP_Left):
211		keyName = "ArrowLeft"
212	case uint(C.GDK_KEY_KP_Right):
213		keyName = "ArrowRight"
214	case uint(C.GDK_KEY_KP_Up):
215		keyName = "ArrowUp"
216	case uint(C.GDK_KEY_KP_Down):
217		keyName = "ArrowDown"
218	case uint(C.GDK_KEY_F12):
219		keyName = "F12"
220	case uint(C.GDK_KEY_F5):
221		keyName = "F5"
222	case uint(C.GDK_KEY_c), uint(C.GDK_KEY_C):
223		keyName = "c"
224	case uint(C.GDK_KEY_r), uint(C.GDK_KEY_R):
225		keyName = "r"
226	case uint(C.GDK_KEY_l), uint(C.GDK_KEY_L):
227		keyName = "l"
228	case uint(C.GDK_KEY_f), uint(C.GDK_KEY_F):
229		keyName = "f"
230	case uint(C.GDK_KEY_p), uint(C.GDK_KEY_P):
231		keyName = "p"
232	case uint(C.GDK_KEY_w), uint(C.GDK_KEY_W):
233		keyName = "w"
234	default:
235		// Filter out modifier keys themselves (like Ctrl, Alt, Shift)
236		// These keyvals shouldn't be logged as misses
237		switch keyval {
238		case 0xffe1, 0xffe2: // GDK_KEY_Shift_L, GDK_KEY_Shift_R
239			return false
240		case 0xffe3, 0xffe4: // GDK_KEY_Control_L, GDK_KEY_Control_R
241			return false
242		case 0xffe9, 0xffea: // GDK_KEY_Alt_L, GDK_KEY_Alt_R
243			return false
244		case 0xffeb, 0xffec: // GDK_KEY_Super_L, GDK_KEY_Super_R
245			return false
246		}
247
248		// If Control held, log unknown keyval for diagnostics (but not modifier keys)
249		if ctrl {
250			log.Printf("[accelerator-miss] ctrl keyval=0x%x", keyval)
251		}
252		return false
253	}
254
255	// Check for shortcuts that need C-level handling and trigger if found
256	shortcut := getCLevelBridgeShortcut(ctrl, alt, shift, keyName)
257	if shortcut != "" {
258		// For all C-level bridge shortcuts (Alt+Arrow and Ctrl+W), use global window shortcut handler
259		// The window shortcut handler now properly handles both popup and regular panes
260		shortcutsRegistryMu.RLock()
261		handle, exists := globalShortcutHandles[shortcut]
262		shortcutsRegistryMu.RUnlock()
263
264		if exists && handle != 0 {
265			invokeWindowShortcutCallback(handle)
266			return true
267		}
268	}
269
270	now := time.Now()
271	rawKey := fmt.Sprintf("raw:%v:%v:%v:%s", ctrl, alt, shift, keyName)
272	if shouldDebounceShortcut(uid, rawKey, now) {
273		return false
274	}
275
276	if logMinusEvent {
277		log.Printf("[accelerator-raw] minus key detected ctrl=%v alt=%v keyval=0x%x", ctrl, alt, keyval)
278	}
279
280	// Candidate accelerator strings in order
281	handled := false
282	candidates := []string{keyName}
283	if ctrl && shift {
284		// Ctrl+Shift combinations
285		candidates = append([]string{"cmdorctrl+shift+" + keyName, "ctrl+shift+" + keyName}, candidates...)
286	} else if ctrl {
287		// Common style: modifier + '+' + keyName (e.g., cmdorctrl+=)
288		candidates = append([]string{"cmdorctrl+" + keyName, "ctrl+" + keyName}, candidates...)
289		// Also accept style without the extra '+' for punctuation keys (e.g., cmdorctrl-)
290		candidates = append([]string{"cmdorctrl" + keyName, "ctrl" + keyName}, candidates...)
291	}
292	if alt {
293		candidates = append([]string{"alt+" + keyName}, candidates...)
294	}
295
296	// Compute a single normalized shortcut string to forward to GUI (single source of truth)
297	normalized := ""
298	if ctrl && shift {
299		normalized = "cmdorctrl+shift+" + keyName
300	} else if ctrl {
301		normalized = "cmdorctrl+" + keyName
302	} else if alt {
303		normalized = "alt+" + keyName
304	} else if keyName != "" {
305		// For future modes like Vim, we may want to forward non-modified keys
306		// For now, only forward if a modifier is present to avoid noise
307		normalized = ""
308	}
309
310	// Forward normalized shortcut to GUI KeyboardService via DOM event if present
311	if normalized != "" && !shouldDebounceShortcut(uid, "dom:"+normalized, now) {
312		regMu.RLock()
313		vw := viewByID[uid]
314		regMu.RUnlock()
315		if vw != nil {
316			_ = vw.InjectScript(fmt.Sprintf("document.dispatchEvent(new CustomEvent('dumber:key',{detail:{shortcut:'%s'}}));", normalized))
317		}
318	}
319
320	regMu.RLock()
321	reg := viewShortcuts[uid]
322	regMu.RUnlock()
323	if reg == nil {
324		return handled
325	}
326
327	for _, name := range candidates {
328		if cb, ok := reg[name]; ok {
329			if shouldDebounceShortcut(uid, "shortcut:"+name, now) {
330				return handled
331			}
332			log.Printf("[accelerator] %s", name)
333			cb()
334			handled = true
335			break
336		}
337	}
338
339	return handled
340}
341
342func nextViewID() uintptr {
343	return uintptr(atomic.AddUint64(&viewIDCounter, 1))
344}
345
346func registerView(id uintptr, w *WebView) {
347	regMu.Lock()
348	viewByID[id] = w
349	regMu.Unlock()
350}
351
352func unregisterView(id uintptr) {
353	regMu.Lock()
354	delete(viewByID, id)
355	delete(viewShortcuts, id)
356	regMu.Unlock()
357}
358
359// AttachKeyboardControllers attaches GTK4 key and mouse controllers to the view widget.
360func AttachKeyboardControllers(w *WebView) {
361	if w == nil || w.native == nil || w.native.view == nil {
362		return
363	}
364	C.attach_key_controller(w.native.view, C.ulong(w.id))
365	if w.native.win != nil {
366		C.attach_key_controller(w.native.win, C.ulong(w.id))
367	}
368	C.attach_mouse_gesture(w.native.view, C.ulong(w.id))
369	// Legacy controller captures raw button presses (8/9) for back/forward reliably
370	C.attach_mouse_legacy(w.native.view, C.ulong(w.id))
371	log.Printf("[input] GTK4 key/mouse controllers attached")
372}
373
374//export goOnScroll
375func goOnScroll(id C.ulong, dx C.double, dy C.double, state C.GdkModifierType) {
376	uid := uintptr(id)
377	regMu.RLock()
378	vw := viewByID[uid]
379	regMu.RUnlock()
380	if vw == nil {
381		return
382	}
383	// Ctrl+scroll zooms
384	if (uint(state) & uint(C.GDK_CONTROL_MASK)) != 0 {
385		if float64(dy) < 0 {
386			// scroll up -> zoom in
387			nz := vw.zoom
388			if nz <= 0 {
389				nz = 1.0
390			}
391			nz *= 1.1
392			if nz < 0.25 {
393				nz = 0.25
394			}
395			if nz > 5.0 {
396				nz = 5.0
397			}
398			_ = vw.SetZoom(nz)
399		} else if float64(dy) > 0 {
400			// scroll down -> zoom out
401			nz := vw.zoom
402			if nz <= 0 {
403				nz = 1.0
404			}
405			nz /= 1.1
406			if nz < 0.25 {
407				nz = 0.25
408			}
409			if nz > 5.0 {
410				nz = 5.0
411			}
412			_ = vw.SetZoom(nz)
413		}
414	}
415}
416
417//export goOnThemeChanged
418func goOnThemeChanged(id C.ulong, preferDark C.int) {
419	uid := uintptr(id)
420	regMu.RLock()
421	vw := viewByID[uid]
422	regMu.RUnlock()
423	if vw == nil {
424		return
425	}
426	// Inject runtime theme update into the page
427	dval := "false"
428	if int(preferDark) != 0 {
429		dval = "true"
430	}
431	js := fmt.Sprintf(`(() => { try { const d=%s; const cs=d?'dark':'light'; console.log('[dumber] theme change:'+cs);
432let m=document.querySelector('meta[name="color-scheme"]');
433if(!m){ m=document.createElement('meta'); m.name='color-scheme'; document.head.appendChild(m); }
434m.setAttribute('content', d ? 'dark light' : 'light dark');
435let s=document.getElementById('__dumber_theme_style');
436if(!s){ s=document.createElement('style'); s.id='__dumber_theme_style'; document.documentElement.appendChild(s); }
437s.textContent=':root{color-scheme:' + (d?'dark':'light') + ';}';
438// Call the unified theme setter for Tailwind dark mode
439if (window.__dumber_setTheme) {
440  window.__dumber_setTheme(cs);
441} else {
442  // Retry after a short delay in case bridge hasn't loaded yet
443  setTimeout(() => {
444    if (window.__dumber_setTheme) {
445      window.__dumber_setTheme(cs);
446    } else {
447      // Fallback: directly apply dark class
448      console.warn('[dumber] Theme setter not available, using fallback');
449      if (d) {
450        document.documentElement.classList.add('dark');
451      } else {
452        document.documentElement.classList.remove('dark');
453      }
454    }
455  }, 100);
456}
457} catch(e) { console.warn('[dumber] theme runtime update failed', e); } })();`, dval)
458	_ = vw.InjectScript(js)
459}
460
461//export goOnUcmMessage
462func goOnUcmMessage(id C.ulong, json *C.char) {
463	uid := uintptr(id)
464	regMu.RLock()
465	vw := viewByID[uid]
466	regMu.RUnlock()
467	if vw == nil || json == nil {
468		return
469	}
470	goPayload := C.GoString(json)
471
472	// Handle rendering backend detection messages specially
473	if handleRenderingBackendMessage(goPayload) {
474		return
475	}
476
477	// Handle keyboard blocking messages
478	if handleKeyboardBlockingMessage(vw, goPayload) {
479		return
480	}
481
482	vw.dispatchScriptMessage(goPayload)
483}
484
485//export goOnTitleChanged
486func goOnTitleChanged(id C.ulong, ctitle *C.char) {
487	uid := uintptr(id)
488	regMu.RLock()
489	vw := viewByID[uid]
490	regMu.RUnlock()
491	if vw == nil || ctitle == nil {
492		return
493	}
494	title := C.GoString(ctitle)
495	vw.dispatchTitleChanged(title)
496}
497
498//export goOnURIChanged
499func goOnURIChanged(id C.ulong, curi *C.char) {
500	uid := uintptr(id)
501	regMu.RLock()
502	vw := viewByID[uid]
503	regMu.RUnlock()
504	if vw == nil || curi == nil {
505		return
506	}
507	uri := C.GoString(curi)
508	vw.dispatchURIChanged(uri)
509}
510
511//export goOnFaviconURIChanged
512func goOnFaviconURIChanged(id C.ulong, pageURI *C.char, faviconURI *C.char) {
513	uid := uintptr(id)
514	pageURLStr := C.GoString(pageURI)
515	faviconURLStr := C.GoString(faviconURI)
516
517	log.Printf("[favicon] WebKit favicon URI changed for WebView ID %d - favicon: %s", uid, faviconURLStr)
518
519	regMu.RLock()
520	vw := viewByID[uid]
521	regMu.RUnlock()
522	if vw == nil {
523		log.Printf("[favicon] WebView not found for ID %d", uid)
524		return
525	}
526
527	vw.dispatchFaviconURIChanged(pageURLStr, faviconURLStr)
528}
529
530//export goHandleNewWindowPolicy
531func goHandleNewWindowPolicy(id C.ulong, curi *C.char, navType C.int, cframeName *C.char, isUserGesture C.gboolean, modifiers C.guint, mouseButton C.guint) C.gboolean {
532	uid := uintptr(id)
533	regMu.RLock()
534	vw := viewByID[uid]
535	regMu.RUnlock()
536
537	if vw == nil {
538		log.Printf("[webkit] Policy decision: no WebView found for ID %d", uid)
539		return 0 // FALSE
540	}
541
542	uri := ""
543	if curi != nil {
544		uri = C.GoString(curi)
545	}
546
547	frameName := ""
548	if cframeName != nil {
549		frameName = C.GoString(cframeName)
550	}
551
552	// Navigation type constants from WebKit headers
553	navTypeNames := map[int]string{
554		0: "LINK_CLICKED",
555		1: "FORM_SUBMITTED",
556		2: "BACK_FORWARD",
557		3: "RELOAD",
558		4: "FORM_RESUBMITTED",
559		5: "OTHER",
560	}
561
562	navTypeName := navTypeNames[int(navType)]
563	if navTypeName == "" {
564		navTypeName = fmt.Sprintf("UNKNOWN(%d)", int(navType))
565	}
566
567	// Convert parameters for analysis
568	userGesture := isUserGesture != 0
569	mods := uint(modifiers)
570	button := uint(mouseButton)
571
572	// Check for modifier keys that indicate new tab intent
573	const (
574		GDK_CONTROL_MASK = 0x04
575		GDK_META_MASK    = 0x10000000 // Command key on macOS
576	)
577	hasCtrlOrCmd := (mods&GDK_CONTROL_MASK != 0) || (mods&GDK_META_MASK != 0)
578
579	log.Printf("[webkit] Policy decision for new window: URI=%s, NavType=%s, FrameName=%s, UserGesture=%v, Modifiers=0x%x, Button=%d, CtrlOrCmd=%v",
580		uri, navTypeName, frameName, userGesture, mods, button, hasCtrlOrCmd)
581
582	// Enhanced detection logic for _blank vs popup using reliable heuristics
583	isRegularPane := false
584
585	// Criteria for treating as regular pane (new panel):
586	// 1. Explicit _blank frame name (target="_blank" links)
587	// 2. User gesture with Ctrl/Cmd+click (open in new panel intent)
588	// 3. User-initiated OTHER navigation with no frame name (window.open("url", "_blank"))
589	if frameName == "_blank" {
590		isRegularPane = true
591		log.Printf("[webkit] _blank frame detected - will handle as new panel")
592	} else if userGesture && hasCtrlOrCmd && (navType == 0 || navType == 5) {
593		// WEBKIT_NAVIGATION_TYPE_LINK_CLICKED = 0, WEBKIT_NAVIGATION_TYPE_OTHER = 5
594		isRegularPane = true
595		log.Printf("[webkit] Ctrl/Cmd+click detected - will handle as new panel")
596	} else if userGesture && navType == 5 && frameName == "" {
597		// window.open("url", "_blank") typically: user gesture + OTHER + no frame name
598		isRegularPane = true
599		log.Printf("[webkit] User-initiated window.open() detected - will handle as new panel")
600	}
601
602	// Handle _blank targets by creating new panels directly (prevent WindowFeatures crash)
603	if isRegularPane {
604		log.Printf("[webkit] Creating new panel for _blank target: %s", uri)
605
606		// Create new panel through workspace manager using existing popup handler
607		if vw.popupHandler != nil {
608			// Call popup handler to create a new panel
609			// The workspace manager will handle this as a regular panel based on URI markers
610			vw.popupHandler(uri + "#__dumber_frame_blank")
611		}
612
613		return 1 // TRUE - we handled it by creating a new panel
614	}
615
616	// For actual popups, let the create signal handle it
617	if vw.popupHandler != nil {
618		log.Printf("[webkit] Letting popup go through create signal for WindowFeatures handling: %s", uri)
619		return 0 // FALSE - let WebKit call create signal for popup handling
620	} else {
621		log.Printf("[webkit] No popup handler registered - allowing native popup for: %s", uri)
622		return 0 // FALSE - no handler, allow default popup
623	}
624}
625
626//export goHandleLoadChanged
627func goHandleLoadChanged(id C.ulong, curi *C.char, loadEvent C.int) {
628	uid := uintptr(id)
629	regMu.RLock()
630	vw := viewByID[uid]
631	regMu.RUnlock()
632
633	if vw == nil {
634		return
635	}
636
637	// OAuth auto-close is handled via WebKit's "close" signal when providers call window.close()
638	// No need to detect OAuth callback URLs - let the providers handle it
639	_ = curi // Suppress unused parameter warning
640}
641
642//export goHandleWebViewClose
643func goHandleWebViewClose(id C.ulong) {
644	uid := uintptr(id)
645	regMu.RLock()
646	vw := viewByID[uid]
647	regMu.RUnlock()
648
649	if vw == nil {
650		log.Printf("[webkit] WebView close signal: no WebView found for ID %d", uid)
651		return
652	}
653
654	log.Printf("[webkit] WebView close signal received for view_id: %d", uid)
655
656	if vw.closeHandler != nil {
657		log.Printf("[webkit] Calling close handler for view_id: %d", uid)
658		vw.closeHandler()
659	} else {
660		log.Printf("[webkit] No close handler registered for view_id: %d", uid)
661	}
662}
663
664//export goCreateNewPanel
665func goCreateNewPanel(id C.ulong, curi *C.char) {
666	uid := uintptr(id)
667	regMu.RLock()
668	vw := viewByID[uid]
669	regMu.RUnlock()
670
671	if vw == nil {
672		log.Printf("[webkit] Create panel: no WebView found for ID %d", uid)
673		return
674	}
675
676	uri := ""
677	if curi != nil {
678		uri = C.GoString(curi)
679	}
680
681	log.Printf("[webkit] Creating new panel for _blank target: %s", uri)
682
683	// Create new panel through popup handler if available
684	if vw.popupHandler != nil {
685		vw.popupHandler(uri)
686		log.Printf("[webkit] New panel creation handled by popup handler")
687	} else {
688		log.Printf("[webkit] No popup handler available for panel creation")
689	}
690}
691
692//export goHandlePopupGeometry
693func goHandlePopupGeometry(id C.ulong, x, y, width, height C.int) {
694	// Store geometry for workspace manager to use
695	uid := uintptr(id)
696	regMu.Lock()
697	if vw := viewByID[uid]; vw != nil {
698		// For now, we'll store this in a simple way
699		// The workspace manager will handle the geometry
700		log.Printf("[webkit] Popup geometry: %dx%d at (%d,%d)",
701			int(width), int(height), int(x), int(y))
702	}
703	regMu.Unlock()
704}
705
706//export goOnButtonPress
707func goOnButtonPress(id C.ulong, button C.guint, state C.GdkModifierType) {
708	uid := uintptr(id)
709	btn := uint(button)
710	log.Printf("[mouse] button=%d state=0x%x", btn, uint(state))
711	regMu.RLock()
712	vw := viewByID[uid]
713	regMu.RUnlock()
714	if vw == nil {
715		return
716	}
717	switch btn {
718	case 8: // Back button on many mice
719		_ = vw.GoBack()
720	case 9: // Forward button on many mice
721		_ = vw.GoForward()
722	}
723}
724
725//export goQuitMainLoop
726func goQuitMainLoop() {
727	log.Printf("[webkit] Window close requested - quitting main loop")
728	QuitMainLoop()
729}
730
731// handleRenderingBackendMessage processes rendering backend detection messages
732func handleRenderingBackendMessage(payload string) bool {
733	// Parse JSON to check if this is a rendering backend detection message
734	if !strings.Contains(payload, "rendering_backend_detection") {
735		return false
736	}
737
738	// Simple JSON parsing for the specific structure we expect
739	// {"type":"rendering_backend_detection","data":{...}}
740	var msg struct {
741		Type string `json:"type"`
742		Data struct {
743			WebGLAvailable         bool   `json:"webgl_available"`
744			Renderer               string `json:"renderer"`
745			Vendor                 string `json:"vendor"`
746			Version                string `json:"version"`
747			ShadingLanguageVersion string `json:"shading_language_version"`
748			MaxTextureSize         int    `json:"max_texture_size"`
749			HardwareAccelerated    bool   `json:"hardware_accelerated"`
750		} `json:"data"`
751	}
752
753	if err := json.Unmarshal([]byte(payload), &msg); err != nil {
754		log.Printf("[webkit] Failed to parse rendering backend message: %v", err)
755		return false
756	}
757
758	if msg.Type != "rendering_backend_detection" {
759		return false
760	}
761
762	// Log the detected rendering backend information
763	backendType := "SOFTWARE"
764	if msg.Data.HardwareAccelerated {
765		backendType = "HARDWARE"
766	}
767
768	log.Printf("[webkit] Detected rendering backend: %s", backendType)
769	log.Printf("[webkit] WebGL available: %v", msg.Data.WebGLAvailable)
770	log.Printf("[webkit] Renderer: %s", msg.Data.Renderer)
771	log.Printf("[webkit] Vendor: %s", msg.Data.Vendor)
772	log.Printf("[webkit] Version: %s", msg.Data.Version)
773	log.Printf("[webkit] Max texture size: %d", msg.Data.MaxTextureSize)
774
775	return true
776}
777
778// handleKeyboardBlockingMessage handles keyboard blocking control messages from omnibox
779func handleKeyboardBlockingMessage(vw *WebView, payload string) bool {
780	if vw == nil || vw.IsDestroyed() {
781		return false
782	}
783
784	var msg struct {
785		Type   string `json:"type"`
786		Action string `json:"action"`
787	}
788
789	if err := json.Unmarshal([]byte(payload), &msg); err != nil {
790		return false
791	}
792
793	if msg.Type != "keyboard_blocking" {
794		return false
795	}
796
797	switch msg.Action {
798	case "enable":
799		log.Printf("[webkit] Enabling page keyboard blocking for omnibox")
800		if err := vw.EnablePageKeyboardBlocking(); err != nil {
801			log.Printf("[webkit] Failed to enable keyboard blocking: %v", err)
802		}
803	case "disable":
804		log.Printf("[webkit] Disabling page keyboard blocking")
805		if err := vw.DisablePageKeyboardBlocking(); err != nil {
806			log.Printf("[webkit] Failed to disable keyboard blocking: %v", err)
807		}
808	default:
809		log.Printf("[webkit] Unknown keyboard blocking action: %s", msg.Action)
810	}
811
812	return true
813}

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