v0.2.0 759cc92

keyboard_cgo.go RawHistoryFolder

1//go:build webkit_cgo
2
3package webkit
4
5/*
6#cgo pkg-config: gtk4
7#include <gtk/gtk.h>
8#include <gdk/gdk.h>
9
10// Declarations for Go callbacks
11extern void goOnKeyPress(unsigned long id, unsigned int keyval, GdkModifierType state);
12extern void goOnButtonPress(unsigned long id, unsigned int button, GdkModifierType state);
13
14// Key controller callback -> forward to Go
15static gboolean on_key_pressed(GtkEventControllerKey* controller, guint keyval, guint keycode, GdkModifierType state, gpointer user_data) {
16    (void)controller; (void)keycode;
17    goOnKeyPress((unsigned long)user_data, keyval, state);
18    return FALSE; // do not stop other handlers
19}
20
21// Mouse click gesture -> forward side buttons to Go
22static void on_click_pressed(GtkGestureClick* gesture, gint n_press, gdouble x, gdouble y, gpointer user_data) {
23    (void)n_press; (void)x; (void)y;
24    guint button = gtk_gesture_single_get_current_button(GTK_GESTURE_SINGLE(gesture));
25    // Pass 0 for modifier state (not provided via gesture API)
26    goOnButtonPress((unsigned long)user_data, button, 0);
27}
28
29// Attach controllers to a widget (GTK4)
30static void attach_key_controller(GtkWidget* widget, unsigned long id) {
31    if (!widget) return;
32    GtkEventController* keyc = gtk_event_controller_key_new();
33    g_signal_connect_data(keyc, "key-pressed", G_CALLBACK(on_key_pressed), (gpointer)id, NULL, 0);
34    gtk_widget_add_controller(widget, keyc);
35}
36
37static void attach_mouse_gesture(GtkWidget* widget, unsigned long id) {
38    if (!widget) return;
39    GtkGesture* click = gtk_gesture_click_new();
40    g_signal_connect_data(click, "pressed", G_CALLBACK(on_click_pressed), (gpointer)id, NULL, 0);
41    gtk_widget_add_controller(widget, GTK_EVENT_CONTROLLER(click));
42}
43*/
44import "C"
45
46import (
47    "fmt"
48    "log"
49    "sync/atomic"
50    "sync"
51    "time"
52)
53
54// Registry of accelerators per view id.
55type shortcutRegistry map[string]func()
56
57var (
58    regMu         sync.RWMutex
59    viewShortcuts = make(map[uintptr]shortcutRegistry)
60    viewIDCounter  uint64
61    viewByID      = make(map[uintptr]*WebView)
62    lastKeyTime   = make(map[uintptr]map[string]time.Time)
63)
64
65// RegisterKeyboardShortcut registers a callback under an accelerator string.
66func (w *WebView) RegisterKeyboardShortcut(accel string, callback func()) error {
67    if w == nil || w.destroyed {
68        return ErrNotImplemented
69    }
70    if accel == "" || callback == nil {
71        return fmt.Errorf("invalid shortcut registration")
72    }
73    regMu.Lock()
74    if _, ok := viewShortcuts[w.id]; !ok {
75        viewShortcuts[w.id] = make(shortcutRegistry)
76    }
77    viewShortcuts[w.id][accel] = callback
78    regMu.Unlock()
79    return nil
80}
81
82//export goOnKeyPress
83func goOnKeyPress(id C.ulong, keyval C.guint, state C.GdkModifierType) {
84    // Normalize and dispatch
85    uid := uintptr(id)
86    kv := uint(keyval)
87    st := uint(state)
88    dispatchAccelerator(uid, kv, st)
89}
90
91func dispatchAccelerator(uid uintptr, keyval uint, state uint) {
92    ctrl := (state & uint(C.GDK_CONTROL_MASK)) != 0
93    // Map keyval to string names
94    var keyName string
95    switch keyval {
96    case uint(C.GDK_KEY_plus), uint(C.GDK_KEY_KP_Add):
97        keyName = "plus"
98    case uint(C.GDK_KEY_equal):
99        keyName = "="
100    case uint(C.GDK_KEY_minus), uint(C.GDK_KEY_KP_Subtract), uint(C.GDK_KEY_underscore):
101        // Treat underscore as minus for layouts where '-' requires Shift (e.g., AZERTY)
102        keyName = "-"
103    case uint(C.GDK_KEY_0), uint(C.GDK_KEY_KP_0):
104        keyName = "0"
105    case uint(C.GDK_KEY_F12):
106        keyName = "F12"
107    default:
108        return
109    }
110
111    // Candidate accelerator strings in order
112    candidates := []string{keyName}
113    if ctrl {
114        candidates = append([]string{"cmdorctrl+" + keyName, "ctrl+" + keyName}, candidates...)
115    }
116
117    regMu.RLock()
118    reg := viewShortcuts[uid]
119    regMu.RUnlock()
120    if reg == nil {
121        return
122    }
123    for _, name := range candidates {
124        if cb, ok := reg[name]; ok {
125            // Debounce duplicates within 120ms for the same view+key
126            regMu.Lock()
127            if _, ok := lastKeyTime[uid]; !ok { lastKeyTime[uid] = make(map[string]time.Time) }
128            last := lastKeyTime[uid][name]
129            now := time.Now()
130            if now.Sub(last) < 120*time.Millisecond {
131                // Skip duplicate
132                regMu.Unlock()
133                return
134            }
135            lastKeyTime[uid][name] = now
136            regMu.Unlock()
137            log.Printf("[accelerator] %s", name)
138            cb()
139            break
140        }
141    }
142}
143
144func nextViewID() uintptr {
145    return uintptr(atomic.AddUint64(&viewIDCounter, 1))
146}
147
148func registerView(id uintptr, w *WebView) {
149    regMu.Lock()
150    viewByID[id] = w
151    regMu.Unlock()
152}
153
154func unregisterView(id uintptr) {
155    regMu.Lock()
156    delete(viewByID, id)
157    delete(viewShortcuts, id)
158    regMu.Unlock()
159}
160
161// AttachKeyboardControllers attaches GTK4 key and mouse controllers to the view widget.
162func AttachKeyboardControllers(w *WebView) {
163    if w == nil || w.native == nil || w.native.view == nil {
164        return
165    }
166    C.attach_key_controller(w.native.view, C.ulong(w.id))
167    if w.native.win != nil {
168        C.attach_key_controller(w.native.win, C.ulong(w.id))
169    }
170    C.attach_mouse_gesture(w.native.view, C.ulong(w.id))
171    log.Printf("[input] GTK4 key/mouse controllers attached")
172}
173
174//export goOnThemeChanged
175func goOnThemeChanged(id C.ulong, preferDark C.int) {
176    uid := uintptr(id)
177    regMu.RLock()
178    vw := viewByID[uid]
179    regMu.RUnlock()
180    if vw == nil {
181        return
182    }
183    // Inject runtime theme update into the page
184    dval := "false"
185    if int(preferDark) != 0 { dval = "true" }
186    js := fmt.Sprintf(`(() => { try { const d=%s; const cs=d?'dark':'light'; console.log('[dumber] theme change:'+cs);
187let m=document.querySelector('meta[name="color-scheme"]');
188if(!m){ m=document.createElement('meta'); m.name='color-scheme'; document.head.appendChild(m); }
189m.setAttribute('content', d ? 'dark light' : 'light dark');
190let s=document.getElementById('__dumber_theme_style');
191if(!s){ s=document.createElement('style'); s.id='__dumber_theme_style'; document.documentElement.appendChild(s); }
192s.textContent=':root{color-scheme:' + (d?'dark':'light') + ';}';
193} catch(e) { console.warn('[dumber] theme runtime update failed', e); } })();`, dval)
194    _ = vw.InjectScript(js)
195}
196
197//export goOnUcmMessage
198func goOnUcmMessage(id C.ulong, json *C.char) {
199    uid := uintptr(id)
200    regMu.RLock()
201    vw := viewByID[uid]
202    regMu.RUnlock()
203    if vw == nil || json == nil { return }
204    goPayload := C.GoString(json)
205    vw.dispatchScriptMessage(goPayload)
206}
207
208//export goOnTitleChanged
209func goOnTitleChanged(id C.ulong, ctitle *C.char) {
210    uid := uintptr(id)
211    regMu.RLock()
212    vw := viewByID[uid]
213    regMu.RUnlock()
214    if vw == nil || ctitle == nil { return }
215    title := C.GoString(ctitle)
216    vw.dispatchTitleChanged(title)
217}
218
219//export goOnURIChanged
220func goOnURIChanged(id C.ulong, curi *C.char) {
221    uid := uintptr(id)
222    regMu.RLock()
223    vw := viewByID[uid]
224    regMu.RUnlock()
225    if vw == nil || curi == nil { return }
226    uri := C.GoString(curi)
227    vw.dispatchURIChanged(uri)
228}
229
230//export goOnButtonPress
231func goOnButtonPress(id C.ulong, button C.guint, state C.GdkModifierType) {
232    uid := uintptr(id)
233    btn := uint(button)
234    log.Printf("[mouse] button=%d state=0x%x", btn, uint(state))
235    regMu.RLock()
236    vw := viewByID[uid]
237    regMu.RUnlock()
238    if vw == nil {
239        return
240    }
241    switch btn {
242    case 8: // Back button on many mice
243        _ = vw.GoBack()
244    case 9: // Forward button on many mice
245        _ = vw.GoForward()
246    }
247}

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