v0.9.0 636c44c

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	return nil
137}
138
139//export goOnKeyPress
140func goOnKeyPress(id C.ulong, keyval C.guint, state C.GdkModifierType) C.gboolean {
141	// Normalize and dispatch
142	uid := uintptr(id)
143	kv := uint(keyval)
144	st := uint(state)
145	if dispatchAccelerator(uid, kv, st) {
146		return C.gboolean(1)
147	}
148	return C.gboolean(0)
149}
150
151func dispatchAccelerator(uid uintptr, keyval uint, state uint) bool {
152	ctrl := (state & uint(C.GDK_CONTROL_MASK)) != 0
153	alt := (state & uint(C.GDK_ALT_MASK)) != 0
154	shift := (state & uint(C.GDK_SHIFT_MASK)) != 0
155	// Map keyval to string names
156	var keyName string
157	logMinusEvent := false
158	switch keyval {
159	case uint(C.GDK_KEY_minus), uint(C.GDK_KEY_KP_Subtract), uint(C.GDK_KEY_underscore):
160		// Normalize minus; log raw detection for diagnostics
161		logMinusEvent = true
162		keyName = "-"
163	case uint(C.GDK_KEY_equal), uint(C.GDK_KEY_KP_Add):
164		keyName = "="
165	case uint(C.GDK_KEY_0), uint(C.GDK_KEY_KP_0):
166		keyName = "0"
167	case uint(C.GDK_KEY_Left):
168		keyName = "ArrowLeft"
169	case uint(C.GDK_KEY_Right):
170		keyName = "ArrowRight"
171	case uint(C.GDK_KEY_Up):
172		keyName = "ArrowUp"
173	case uint(C.GDK_KEY_Down):
174		keyName = "ArrowDown"
175	case uint(C.GDK_KEY_KP_Left):
176		keyName = "ArrowLeft"
177	case uint(C.GDK_KEY_KP_Right):
178		keyName = "ArrowRight"
179	case uint(C.GDK_KEY_KP_Up):
180		keyName = "ArrowUp"
181	case uint(C.GDK_KEY_KP_Down):
182		keyName = "ArrowDown"
183	case uint(C.GDK_KEY_F12):
184		keyName = "F12"
185	case uint(C.GDK_KEY_F5):
186		keyName = "F5"
187	case uint(C.GDK_KEY_c), uint(C.GDK_KEY_C):
188		keyName = "c"
189	case uint(C.GDK_KEY_r), uint(C.GDK_KEY_R):
190		keyName = "r"
191	case uint(C.GDK_KEY_l), uint(C.GDK_KEY_L):
192		keyName = "l"
193	case uint(C.GDK_KEY_f), uint(C.GDK_KEY_F):
194		keyName = "f"
195	case uint(C.GDK_KEY_p), uint(C.GDK_KEY_P):
196		keyName = "p"
197	default:
198		// If Control held, log unknown keyval for diagnostics
199		if ctrl {
200			log.Printf("[accelerator-miss] ctrl keyval=0x%x", keyval)
201		}
202		return false
203	}
204
205	now := time.Now()
206	rawKey := fmt.Sprintf("raw:%v:%v:%v:%s", ctrl, alt, shift, keyName)
207	if shouldDebounceShortcut(uid, rawKey, now) {
208		return false
209	}
210
211	if logMinusEvent {
212		log.Printf("[accelerator-raw] minus key detected ctrl=%v alt=%v keyval=0x%x", ctrl, alt, keyval)
213	}
214
215	// Candidate accelerator strings in order
216	handled := false
217	candidates := []string{keyName}
218	if ctrl && shift {
219		// Ctrl+Shift combinations
220		candidates = append([]string{"cmdorctrl+shift+" + keyName, "ctrl+shift+" + keyName}, candidates...)
221	} else if ctrl {
222		// Common style: modifier + '+' + keyName (e.g., cmdorctrl+=)
223		candidates = append([]string{"cmdorctrl+" + keyName, "ctrl+" + keyName}, candidates...)
224		// Also accept style without the extra '+' for punctuation keys (e.g., cmdorctrl-)
225		candidates = append([]string{"cmdorctrl" + keyName, "ctrl" + keyName}, candidates...)
226	}
227	if alt {
228		candidates = append([]string{"alt+" + keyName}, candidates...)
229	}
230
231	// Compute a single normalized shortcut string to forward to GUI (single source of truth)
232	normalized := ""
233	if ctrl && shift {
234		normalized = "cmdorctrl+shift+" + keyName
235	} else if ctrl {
236		normalized = "cmdorctrl+" + keyName
237	} else if alt {
238		normalized = "alt+" + keyName
239	} else if keyName != "" {
240		// For future modes like Vim, we may want to forward non-modified keys
241		// For now, only forward if a modifier is present to avoid noise
242		normalized = ""
243	}
244
245	// Forward normalized shortcut to GUI KeyboardService via DOM event if present
246	if normalized != "" && !shouldDebounceShortcut(uid, "dom:"+normalized, now) {
247		regMu.RLock()
248		vw := viewByID[uid]
249		regMu.RUnlock()
250		if vw != nil {
251			_ = vw.InjectScript(fmt.Sprintf("document.dispatchEvent(new CustomEvent('dumber:key',{detail:{shortcut:'%s'}}));", normalized))
252		}
253	}
254
255	regMu.RLock()
256	reg := viewShortcuts[uid]
257	regMu.RUnlock()
258	if reg == nil {
259		return handled
260	}
261	for _, name := range candidates {
262		if cb, ok := reg[name]; ok {
263			if shouldDebounceShortcut(uid, "shortcut:"+name, now) {
264				return handled
265			}
266			log.Printf("[accelerator] %s", name)
267			cb()
268			handled = true
269			break
270		}
271	}
272
273	return handled
274}
275
276func nextViewID() uintptr {
277	return uintptr(atomic.AddUint64(&viewIDCounter, 1))
278}
279
280func registerView(id uintptr, w *WebView) {
281	regMu.Lock()
282	viewByID[id] = w
283	regMu.Unlock()
284}
285
286func unregisterView(id uintptr) {
287	regMu.Lock()
288	delete(viewByID, id)
289	delete(viewShortcuts, id)
290	regMu.Unlock()
291}
292
293// AttachKeyboardControllers attaches GTK4 key and mouse controllers to the view widget.
294func AttachKeyboardControllers(w *WebView) {
295	if w == nil || w.native == nil || w.native.view == nil {
296		return
297	}
298	C.attach_key_controller(w.native.view, C.ulong(w.id))
299	if w.native.win != nil {
300		C.attach_key_controller(w.native.win, C.ulong(w.id))
301	}
302	C.attach_mouse_gesture(w.native.view, C.ulong(w.id))
303	// Legacy controller captures raw button presses (8/9) for back/forward reliably
304	C.attach_mouse_legacy(w.native.view, C.ulong(w.id))
305	log.Printf("[input] GTK4 key/mouse controllers attached")
306}
307
308//export goOnScroll
309func goOnScroll(id C.ulong, dx C.double, dy C.double, state C.GdkModifierType) {
310	uid := uintptr(id)
311	regMu.RLock()
312	vw := viewByID[uid]
313	regMu.RUnlock()
314	if vw == nil {
315		return
316	}
317	// Ctrl+scroll zooms
318	if (uint(state) & uint(C.GDK_CONTROL_MASK)) != 0 {
319		if float64(dy) < 0 {
320			// scroll up -> zoom in
321			nz := vw.zoom
322			if nz <= 0 {
323				nz = 1.0
324			}
325			nz *= 1.1
326			if nz < 0.25 {
327				nz = 0.25
328			}
329			if nz > 5.0 {
330				nz = 5.0
331			}
332			_ = vw.SetZoom(nz)
333		} else if float64(dy) > 0 {
334			// scroll down -> zoom out
335			nz := vw.zoom
336			if nz <= 0 {
337				nz = 1.0
338			}
339			nz /= 1.1
340			if nz < 0.25 {
341				nz = 0.25
342			}
343			if nz > 5.0 {
344				nz = 5.0
345			}
346			_ = vw.SetZoom(nz)
347		}
348	}
349}
350
351//export goOnThemeChanged
352func goOnThemeChanged(id C.ulong, preferDark C.int) {
353	uid := uintptr(id)
354	regMu.RLock()
355	vw := viewByID[uid]
356	regMu.RUnlock()
357	if vw == nil {
358		return
359	}
360	// Inject runtime theme update into the page
361	dval := "false"
362	if int(preferDark) != 0 {
363		dval = "true"
364	}
365	js := fmt.Sprintf(`(() => { try { const d=%s; const cs=d?'dark':'light'; console.log('[dumber] theme change:'+cs);
366let m=document.querySelector('meta[name="color-scheme"]');
367if(!m){ m=document.createElement('meta'); m.name='color-scheme'; document.head.appendChild(m); }
368m.setAttribute('content', d ? 'dark light' : 'light dark');
369let s=document.getElementById('__dumber_theme_style');
370if(!s){ s=document.createElement('style'); s.id='__dumber_theme_style'; document.documentElement.appendChild(s); }
371s.textContent=':root{color-scheme:' + (d?'dark':'light') + ';}';
372// Call the unified theme setter for Tailwind dark mode
373if (window.__dumber_setTheme) {
374  window.__dumber_setTheme(cs);
375} else {
376  // Retry after a short delay in case bridge hasn't loaded yet
377  setTimeout(() => {
378    if (window.__dumber_setTheme) {
379      window.__dumber_setTheme(cs);
380    } else {
381      // Fallback: directly apply dark class
382      console.warn('[dumber] Theme setter not available, using fallback');
383      if (d) {
384        document.documentElement.classList.add('dark');
385      } else {
386        document.documentElement.classList.remove('dark');
387      }
388    }
389  }, 100);
390}
391} catch(e) { console.warn('[dumber] theme runtime update failed', e); } })();`, dval)
392	_ = vw.InjectScript(js)
393}
394
395//export goOnUcmMessage
396func goOnUcmMessage(id C.ulong, json *C.char) {
397	uid := uintptr(id)
398	regMu.RLock()
399	vw := viewByID[uid]
400	regMu.RUnlock()
401	if vw == nil || json == nil {
402		return
403	}
404	goPayload := C.GoString(json)
405
406	// Handle rendering backend detection messages specially
407	if handleRenderingBackendMessage(goPayload) {
408		return
409	}
410
411	vw.dispatchScriptMessage(goPayload)
412}
413
414//export goOnTitleChanged
415func goOnTitleChanged(id C.ulong, ctitle *C.char) {
416	uid := uintptr(id)
417	regMu.RLock()
418	vw := viewByID[uid]
419	regMu.RUnlock()
420	if vw == nil || ctitle == nil {
421		return
422	}
423	title := C.GoString(ctitle)
424	vw.dispatchTitleChanged(title)
425}
426
427//export goOnURIChanged
428func goOnURIChanged(id C.ulong, curi *C.char) {
429	uid := uintptr(id)
430	regMu.RLock()
431	vw := viewByID[uid]
432	regMu.RUnlock()
433	if vw == nil || curi == nil {
434		return
435	}
436	uri := C.GoString(curi)
437	vw.dispatchURIChanged(uri)
438}
439
440//export goHandleNewWindowPolicy
441func goHandleNewWindowPolicy(id C.ulong, curi *C.char, navType C.int, cframeName *C.char, isUserGesture C.gboolean, modifiers C.guint, mouseButton C.guint) C.gboolean {
442	uid := uintptr(id)
443	regMu.RLock()
444	vw := viewByID[uid]
445	regMu.RUnlock()
446
447	if vw == nil {
448		log.Printf("[webkit] Policy decision: no WebView found for ID %d", uid)
449		return 0 // FALSE
450	}
451
452	uri := ""
453	if curi != nil {
454		uri = C.GoString(curi)
455	}
456
457	frameName := ""
458	if cframeName != nil {
459		frameName = C.GoString(cframeName)
460	}
461
462	// Navigation type constants from WebKit headers
463	navTypeNames := map[int]string{
464		0: "LINK_CLICKED",
465		1: "FORM_SUBMITTED",
466		2: "BACK_FORWARD",
467		3: "RELOAD",
468		4: "FORM_RESUBMITTED",
469		5: "OTHER",
470	}
471
472	navTypeName := navTypeNames[int(navType)]
473	if navTypeName == "" {
474		navTypeName = fmt.Sprintf("UNKNOWN(%d)", int(navType))
475	}
476
477	// Convert parameters for analysis
478	userGesture := isUserGesture != 0
479	mods := uint(modifiers)
480	button := uint(mouseButton)
481
482	// Check for modifier keys that indicate new tab intent
483	const (
484		GDK_CONTROL_MASK = 0x04
485		GDK_META_MASK    = 0x10000000 // Command key on macOS
486	)
487	hasCtrlOrCmd := (mods&GDK_CONTROL_MASK != 0) || (mods&GDK_META_MASK != 0)
488
489	log.Printf("[webkit] Policy decision for new window: URI=%s, NavType=%s, FrameName=%s, UserGesture=%v, Modifiers=0x%x, Button=%d, CtrlOrCmd=%v",
490		uri, navTypeName, frameName, userGesture, mods, button, hasCtrlOrCmd)
491
492	// Enhanced detection logic for _blank vs popup using reliable heuristics
493	isRegularPane := false
494
495	// Criteria for treating as regular pane (new panel):
496	// 1. Explicit _blank frame name (target="_blank" links)
497	// 2. User gesture with Ctrl/Cmd+click (open in new panel intent)
498	// 3. User-initiated OTHER navigation with no frame name (window.open("url", "_blank"))
499	if frameName == "_blank" {
500		isRegularPane = true
501		log.Printf("[webkit] _blank frame detected - will handle as new panel")
502	} else if userGesture && hasCtrlOrCmd && (navType == 0 || navType == 5) {
503		// WEBKIT_NAVIGATION_TYPE_LINK_CLICKED = 0, WEBKIT_NAVIGATION_TYPE_OTHER = 5
504		isRegularPane = true
505		log.Printf("[webkit] Ctrl/Cmd+click detected - will handle as new panel")
506	} else if userGesture && navType == 5 && frameName == "" {
507		// window.open("url", "_blank") typically: user gesture + OTHER + no frame name
508		isRegularPane = true
509		log.Printf("[webkit] User-initiated window.open() detected - will handle as new panel")
510	}
511
512	// Handle _blank targets by creating new panels directly (prevent WindowFeatures crash)
513	if isRegularPane {
514		log.Printf("[webkit] Creating new panel for _blank target: %s", uri)
515
516		// Create new panel through workspace manager using existing popup handler
517		if vw.popupHandler != nil {
518			// Call popup handler to create a new panel
519			// The workspace manager will handle this as a regular panel based on URI markers
520			vw.popupHandler(uri + "#__dumber_frame_blank")
521		}
522
523		return 1 // TRUE - we handled it by creating a new panel
524	}
525
526	// For actual popups, let the create signal handle it
527	if vw.popupHandler != nil {
528		log.Printf("[webkit] Letting popup go through create signal for WindowFeatures handling: %s", uri)
529		return 0 // FALSE - let WebKit call create signal for popup handling
530	} else {
531		log.Printf("[webkit] No popup handler registered - allowing native popup for: %s", uri)
532		return 0 // FALSE - no handler, allow default popup
533	}
534}
535
536//export goHandleLoadChanged
537func goHandleLoadChanged(id C.ulong, curi *C.char, loadEvent C.int) {
538	uid := uintptr(id)
539	regMu.RLock()
540	vw := viewByID[uid]
541	regMu.RUnlock()
542
543	if vw == nil {
544		return
545	}
546
547	// OAuth auto-close is handled via WebKit's "close" signal when providers call window.close()
548	// No need to detect OAuth callback URLs - let the providers handle it
549	_ = curi // Suppress unused parameter warning
550}
551
552//export goHandleWebViewClose
553func goHandleWebViewClose(id C.ulong) {
554	uid := uintptr(id)
555	regMu.RLock()
556	vw := viewByID[uid]
557	regMu.RUnlock()
558
559	if vw == nil {
560		log.Printf("[webkit] WebView close signal: no WebView found for ID %d", uid)
561		return
562	}
563
564	log.Printf("[webkit] WebView close signal received for view_id: %d", uid)
565
566	if vw.closeHandler != nil {
567		log.Printf("[webkit] Calling close handler for view_id: %d", uid)
568		vw.closeHandler()
569	} else {
570		log.Printf("[webkit] No close handler registered for view_id: %d", uid)
571	}
572}
573
574//export goCreateNewPanel
575func goCreateNewPanel(id C.ulong, curi *C.char) {
576	uid := uintptr(id)
577	regMu.RLock()
578	vw := viewByID[uid]
579	regMu.RUnlock()
580
581	if vw == nil {
582		log.Printf("[webkit] Create panel: no WebView found for ID %d", uid)
583		return
584	}
585
586	uri := ""
587	if curi != nil {
588		uri = C.GoString(curi)
589	}
590
591	log.Printf("[webkit] Creating new panel for _blank target: %s", uri)
592
593	// Create new panel through popup handler if available
594	if vw.popupHandler != nil {
595		vw.popupHandler(uri)
596		log.Printf("[webkit] New panel creation handled by popup handler")
597	} else {
598		log.Printf("[webkit] No popup handler available for panel creation")
599	}
600}
601
602//export goHandlePopupGeometry
603func goHandlePopupGeometry(id C.ulong, x, y, width, height C.int) {
604	// Store geometry for workspace manager to use
605	uid := uintptr(id)
606	regMu.Lock()
607	if vw := viewByID[uid]; vw != nil {
608		// For now, we'll store this in a simple way
609		// The workspace manager will handle the geometry
610		log.Printf("[webkit] Popup geometry: %dx%d at (%d,%d)",
611			int(width), int(height), int(x), int(y))
612	}
613	regMu.Unlock()
614}
615
616//export goOnButtonPress
617func goOnButtonPress(id C.ulong, button C.guint, state C.GdkModifierType) {
618	uid := uintptr(id)
619	btn := uint(button)
620	log.Printf("[mouse] button=%d state=0x%x", btn, uint(state))
621	regMu.RLock()
622	vw := viewByID[uid]
623	regMu.RUnlock()
624	if vw == nil {
625		return
626	}
627	switch btn {
628	case 8: // Back button on many mice
629		_ = vw.GoBack()
630	case 9: // Forward button on many mice
631		_ = vw.GoForward()
632	}
633}
634
635//export goQuitMainLoop
636func goQuitMainLoop() {
637	log.Printf("[webkit] Window close requested - quitting main loop")
638	QuitMainLoop()
639}
640
641// handleRenderingBackendMessage processes rendering backend detection messages
642func handleRenderingBackendMessage(payload string) bool {
643	// Parse JSON to check if this is a rendering backend detection message
644	if !strings.Contains(payload, "rendering_backend_detection") {
645		return false
646	}
647
648	// Simple JSON parsing for the specific structure we expect
649	// {"type":"rendering_backend_detection","data":{...}}
650	var msg struct {
651		Type string `json:"type"`
652		Data struct {
653			WebGLAvailable         bool   `json:"webgl_available"`
654			Renderer               string `json:"renderer"`
655			Vendor                 string `json:"vendor"`
656			Version                string `json:"version"`
657			ShadingLanguageVersion string `json:"shading_language_version"`
658			MaxTextureSize         int    `json:"max_texture_size"`
659			HardwareAccelerated    bool   `json:"hardware_accelerated"`
660		} `json:"data"`
661	}
662
663	if err := json.Unmarshal([]byte(payload), &msg); err != nil {
664		log.Printf("[webkit] Failed to parse rendering backend message: %v", err)
665		return false
666	}
667
668	if msg.Type != "rendering_backend_detection" {
669		return false
670	}
671
672	// Log the detected rendering backend information
673	backendType := "SOFTWARE"
674	if msg.Data.HardwareAccelerated {
675		backendType = "HARDWARE"
676	}
677
678	log.Printf("[webkit] Detected rendering backend: %s", backendType)
679	log.Printf("[webkit] WebGL available: %v", msg.Data.WebGLAvailable)
680	log.Printf("[webkit] Renderer: %s", msg.Data.Renderer)
681	log.Printf("[webkit] Vendor: %s", msg.Data.Vendor)
682	log.Printf("[webkit] Version: %s", msg.Data.Version)
683	log.Printf("[webkit] Max texture size: %d", msg.Data.MaxTextureSize)
684
685	return true
686}

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