v0.1 bf7c821

keyboard_cgo.go RawHistoryFolder

1//go:build webkit_cgo
2
3package webkit
4
5/*
6#cgo pkg-config: gtk+-3.0
7#include <gtk/gtk.h>
8#include <gdk/gdk.h>
9*/
10import "C"
11
12import (
13    "fmt"
14    "log"
15    "sync/atomic"
16    "sync"
17    "time"
18)
19
20// Registry of accelerators per view id.
21type shortcutRegistry map[string]func()
22
23var (
24    regMu         sync.RWMutex
25    viewShortcuts = make(map[uintptr]shortcutRegistry)
26    viewIDCounter  uint64
27    viewByID      = make(map[uintptr]*WebView)
28    lastKeyTime   = make(map[uintptr]map[string]time.Time)
29)
30
31// RegisterKeyboardShortcut registers a callback under an accelerator string.
32func (w *WebView) RegisterKeyboardShortcut(accel string, callback func()) error {
33    if w == nil || w.destroyed {
34        return ErrNotImplemented
35    }
36    if accel == "" || callback == nil {
37        return fmt.Errorf("invalid shortcut registration")
38    }
39    regMu.Lock()
40    if _, ok := viewShortcuts[w.id]; !ok {
41        viewShortcuts[w.id] = make(shortcutRegistry)
42    }
43    viewShortcuts[w.id][accel] = callback
44    regMu.Unlock()
45    return nil
46}
47
48//export goOnKeyPress
49func goOnKeyPress(id C.ulong, keyval C.guint, state C.GdkModifierType) {
50    // Normalize and dispatch
51    uid := uintptr(id)
52    kv := uint(keyval)
53    st := uint(state)
54    dispatchAccelerator(uid, kv, st)
55}
56
57func dispatchAccelerator(uid uintptr, keyval uint, state uint) {
58    ctrl := (state & uint(C.GDK_CONTROL_MASK)) != 0
59    // Map keyval to string names
60    var keyName string
61    switch keyval {
62    case uint(C.GDK_KEY_plus), uint(C.GDK_KEY_KP_Add):
63        keyName = "plus"
64    case uint(C.GDK_KEY_equal):
65        keyName = "="
66    case uint(C.GDK_KEY_minus), uint(C.GDK_KEY_KP_Subtract):
67        keyName = "-"
68    case uint(C.GDK_KEY_0), uint(C.GDK_KEY_KP_0):
69        keyName = "0"
70    case uint(C.GDK_KEY_F12):
71        keyName = "F12"
72    default:
73        return
74    }
75
76    // Candidate accelerator strings in order
77    candidates := []string{keyName}
78    if ctrl {
79        candidates = append([]string{"cmdorctrl+" + keyName, "ctrl+" + keyName}, candidates...)
80    }
81
82    regMu.RLock()
83    reg := viewShortcuts[uid]
84    regMu.RUnlock()
85    if reg == nil {
86        return
87    }
88    for _, name := range candidates {
89        if cb, ok := reg[name]; ok {
90            // Debounce duplicates within 120ms for the same view+key
91            regMu.Lock()
92            if _, ok := lastKeyTime[uid]; !ok { lastKeyTime[uid] = make(map[string]time.Time) }
93            last := lastKeyTime[uid][name]
94            now := time.Now()
95            if now.Sub(last) < 120*time.Millisecond {
96                // Skip duplicate
97                regMu.Unlock()
98                return
99            }
100            lastKeyTime[uid][name] = now
101            regMu.Unlock()
102            log.Printf("[accelerator] %s", name)
103            cb()
104            break
105        }
106    }
107}
108
109func nextViewID() uintptr {
110    return uintptr(atomic.AddUint64(&viewIDCounter, 1))
111}
112
113func registerView(id uintptr, w *WebView) {
114    regMu.Lock()
115    viewByID[id] = w
116    regMu.Unlock()
117}
118
119func unregisterView(id uintptr) {
120    regMu.Lock()
121    delete(viewByID, id)
122    delete(viewShortcuts, id)
123    regMu.Unlock()
124}
125
126//export goOnUcmMessage
127func goOnUcmMessage(id C.ulong, json *C.char) {
128    uid := uintptr(id)
129    regMu.RLock()
130    vw := viewByID[uid]
131    regMu.RUnlock()
132    if vw == nil || json == nil { return }
133    goPayload := C.GoString(json)
134    vw.dispatchScriptMessage(goPayload)
135}
136
137//export goOnTitleChanged
138func goOnTitleChanged(id C.ulong, ctitle *C.char) {
139    uid := uintptr(id)
140    regMu.RLock()
141    vw := viewByID[uid]
142    regMu.RUnlock()
143    if vw == nil || ctitle == nil { return }
144    title := C.GoString(ctitle)
145    vw.dispatchTitleChanged(title)
146}
147
148//export goOnURIChanged
149func goOnURIChanged(id C.ulong, curi *C.char) {
150    uid := uintptr(id)
151    regMu.RLock()
152    vw := viewByID[uid]
153    regMu.RUnlock()
154    if vw == nil || curi == nil { return }
155    uri := C.GoString(curi)
156    vw.dispatchURIChanged(uri)
157}
158
159//export goOnButtonPress
160func goOnButtonPress(id C.ulong, button C.guint, state C.GdkModifierType) {
161    uid := uintptr(id)
162    btn := uint(button)
163    log.Printf("[mouse] button=%d state=0x%x", btn, uint(state))
164    regMu.RLock()
165    vw := viewByID[uid]
166    regMu.RUnlock()
167    if vw == nil {
168        return
169    }
170    switch btn {
171    case 8: // Back button on many mice
172        _ = vw.GoBack()
173    case 9: // Forward button on many mice
174        _ = vw.GoForward()
175    }
176}

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