detached 804337e

feat(cef): install accessibility handler at wrap time

commit
804337e6ba6d1183ec755cccaddc16e7de4e8e28
author
brice · · 2026-07-29 08:07 UTC
committer
bnema · · 2026-08-14 02:42 UTC
parent
25599f9
signature
unsigned
4 files · +236 -2
A internal/infrastructure/cef/accessibility_handler.go +68 -0
@@ -0,0 +1,68 @@
1+package cef
2+
3+import (
4+ "time"
5+
6+ purecef "github.com/bnema/purego-cef/cef"
7+)
8+
9+// accessibilityPayload is the UI-thread serialization result handed to a
10+// non-blocking sink. Capture, stats, and I/O happen off the CEF UI thread.
11+type accessibilityPayload struct {
12+ Kind string
13+ JSON string
14+ Bytes int
15+ SerializeNanos int64
16+}
17+
18+// accessibilityPayloadSink is the optional capture worker seam. Production
19+// leaves WebView.a11yWorker nil unless DUMBER_A11Y_CAPTURE=1.
20+type accessibilityPayloadSink interface {
21+ Submit(accessibilityPayload) bool
22+}
23+
24+type accessibilityHandler struct {
25+ sink func(accessibilityPayload) bool
26+ writeJSON func(purecef.Value, purecef.JsonWriterOptions) string
27+}
28+
29+var _ purecef.AccessibilityHandler = (*accessibilityHandler)(nil)
30+
31+func newAccessibilityHandler(sink func(accessibilityPayload) bool) purecef.AccessibilityHandler {
32+ return &accessibilityHandler{
33+ sink: sink,
34+ writeJSON: purecef.WriteJson,
35+ }
36+}
37+
38+func (h *accessibilityHandler) OnAccessibilityTreeChange(value purecef.Value) {
39+ h.emit("tree", value)
40+}
41+
42+func (h *accessibilityHandler) OnAccessibilityLocationChange(value purecef.Value) {
43+ h.emit("location", value)
44+}
45+
46+func (h *accessibilityHandler) emit(kind string, value purecef.Value) {
47+ if h == nil || h.sink == nil {
48+ return
49+ }
50+ if value == nil {
51+ return
52+ }
53+ started := time.Now()
54+ jsonText := h.writeJSON(value, purecef.JsonWriterOptionsJsonWriterDefault)
55+ h.sink(accessibilityPayload{
56+ Kind: kind,
57+ JSON: jsonText,
58+ Bytes: len(jsonText),
59+ SerializeNanos: time.Since(started).Nanoseconds(),
60+ })
61+}
62+
63+func (wv *WebView) enqueueAccessibilityPayload(p accessibilityPayload) bool {
64+ if wv == nil || wv.a11yWorker == nil {
65+ return false
66+ }
67+ return wv.a11yWorker.Submit(p)
68+}
@@ -0,0 +1,68 @@
1+package cef
2+
3+import (
4+ "time"
5+
6+ purecef "github.com/bnema/purego-cef/cef"
7+)
8+
9+// accessibilityPayload is the UI-thread serialization result handed to a
10+// non-blocking sink. Capture, stats, and I/O happen off the CEF UI thread.
11+type accessibilityPayload struct {
12+ Kind string
13+ JSON string
14+ Bytes int
15+ SerializeNanos int64
16+}
17+
18+// accessibilityPayloadSink is the optional capture worker seam. Production
19+// leaves WebView.a11yWorker nil unless DUMBER_A11Y_CAPTURE=1.
20+type accessibilityPayloadSink interface {
21+ Submit(accessibilityPayload) bool
22+}
23+
24+type accessibilityHandler struct {
25+ sink func(accessibilityPayload) bool
26+ writeJSON func(purecef.Value, purecef.JsonWriterOptions) string
27+}
28+
29+var _ purecef.AccessibilityHandler = (*accessibilityHandler)(nil)
30+
31+func newAccessibilityHandler(sink func(accessibilityPayload) bool) purecef.AccessibilityHandler {
32+ return &accessibilityHandler{
33+ sink: sink,
34+ writeJSON: purecef.WriteJson,
35+ }
36+}
37+
38+func (h *accessibilityHandler) OnAccessibilityTreeChange(value purecef.Value) {
39+ h.emit("tree", value)
40+}
41+
42+func (h *accessibilityHandler) OnAccessibilityLocationChange(value purecef.Value) {
43+ h.emit("location", value)
44+}
45+
46+func (h *accessibilityHandler) emit(kind string, value purecef.Value) {
47+ if h == nil || h.sink == nil {
48+ return
49+ }
50+ if value == nil {
51+ return
52+ }
53+ started := time.Now()
54+ jsonText := h.writeJSON(value, purecef.JsonWriterOptionsJsonWriterDefault)
55+ h.sink(accessibilityPayload{
56+ Kind: kind,
57+ JSON: jsonText,
58+ Bytes: len(jsonText),
59+ SerializeNanos: time.Since(started).Nanoseconds(),
60+ })
61+}
62+
63+func (wv *WebView) enqueueAccessibilityPayload(p accessibilityPayload) bool {
64+ if wv == nil || wv.a11yWorker == nil {
65+ return false
66+ }
67+ return wv.a11yWorker.Submit(p)
68+}
A internal/infrastructure/cef/accessibility_handler_test.go +153 -0
@@ -0,0 +1,153 @@
1+package cef
2+
3+import (
4+ "testing"
5+
6+ purecef "github.com/bnema/purego-cef/cef"
7+ cefmocks "github.com/bnema/purego-cef/cef/mocks"
8+ "github.com/stretchr/testify/assert"
9+ "github.com/stretchr/testify/require"
10+)
11+
12+type stubCEFValue struct{}
13+
14+func (stubCEFValue) IsValid() bool { return true }
15+func (stubCEFValue) IsOwned() bool { return false }
16+func (stubCEFValue) IsReadOnly() bool { return true }
17+func (stubCEFValue) IsSame(purecef.Value) bool { return false }
18+func (stubCEFValue) IsEqual(purecef.Value) bool { return false }
19+func (stubCEFValue) Copy() purecef.Value { return stubCEFValue{} }
20+func (stubCEFValue) GetType() purecef.ValueType { return 0 }
21+func (stubCEFValue) GetBool() int32 { return 0 }
22+func (stubCEFValue) GetInt() int32 { return 0 }
23+func (stubCEFValue) GetDouble() float64 { return 0 }
24+func (stubCEFValue) GetString() string { return "" }
25+func (stubCEFValue) GetBinary() purecef.BinaryValue { return nil }
26+func (stubCEFValue) GetDictionary() purecef.DictionaryValue { return nil }
27+func (stubCEFValue) GetList() purecef.ListValue { return nil }
28+func (stubCEFValue) SetNull() int32 { return 0 }
29+func (stubCEFValue) SetBool(int32) int32 { return 0 }
30+func (stubCEFValue) SetInt(int32) int32 { return 0 }
31+func (stubCEFValue) SetDouble(float64) int32 { return 0 }
32+func (stubCEFValue) SetString(string) int32 { return 0 }
33+func (stubCEFValue) SetBinary(purecef.BinaryValue) int32 { return 0 }
34+func (stubCEFValue) SetDictionary(purecef.DictionaryValue) int32 { return 0 }
35+func (stubCEFValue) SetList(purecef.ListValue) int32 { return 0 }
36+
37+func TestRenderHandlerExposesAccessibilityAtWrapTime(t *testing.T) {
38+ h := &dumberRenderHandler{a11y: newAccessibilityHandler(nil)}
39+ if h.GetAccessibilityHandler() == nil {
40+ t.Fatal("CEF would omit the accessibility override at wrap time")
41+ }
42+}
43+
44+func TestRenderHandlerExposesAccessibilityWhenMainExists(t *testing.T) {
45+ // Must fail open (nil) when a11y was not installed, even if main exists —
46+ // never fall back to h.main.GetAccessibilityHandler().
47+ mainHandler := cefmocks.NewMockRenderHandler(t)
48+ h := &dumberRenderHandler{main: mainHandler, a11y: nil}
49+ assert.Nil(t, h.GetAccessibilityHandler())
50+ mainHandler.AssertNotCalled(t, "GetAccessibilityHandler")
51+
52+ h.a11y = newAccessibilityHandler(nil)
53+ require.NotNil(t, h.GetAccessibilityHandler())
54+ mainHandler.AssertNotCalled(t, "GetAccessibilityHandler")
55+}
56+
57+func TestNewDumberRenderHandlerInstallsNonNilAccessibilityHandler(t *testing.T) {
58+ require.Nil(t, newDumberRenderHandler(nil))
59+
60+ wv := &WebView{viewBridge: &Cef2gtkAdapter{}}
61+ rh := newDumberRenderHandler(wv)
62+ require.NotNil(t, rh)
63+ require.NotNil(t, rh.GetAccessibilityHandler())
64+
65+ handler, ok := rh.GetAccessibilityHandler().(*accessibilityHandler)
66+ require.True(t, ok)
67+ assert.Nil(t, handler.sink)
68+}
69+
70+func TestAccessibilityHandler_NilSinkSkipsWriteJSON(t *testing.T) {
71+ calls := 0
72+ handler, ok := newAccessibilityHandler(nil).(*accessibilityHandler)
73+ require.True(t, ok)
74+ handler.writeJSON = func(purecef.Value, purecef.JsonWriterOptions) string {
75+ calls++
76+ return `{"unexpected":true}`
77+ }
78+
79+ handler.OnAccessibilityTreeChange(stubCEFValue{})
80+ handler.OnAccessibilityLocationChange(stubCEFValue{})
81+ assert.Equal(t, 0, calls)
82+}
83+
84+func TestAccessibilityHandler_NilValueIgnored(t *testing.T) {
85+ calls := 0
86+ var got []accessibilityPayload
87+ handler, ok := newAccessibilityHandler(func(p accessibilityPayload) bool {
88+ got = append(got, p)
89+ return true
90+ }).(*accessibilityHandler)
91+ require.True(t, ok)
92+ handler.writeJSON = func(purecef.Value, purecef.JsonWriterOptions) string {
93+ calls++
94+ return `{}`
95+ }
96+
97+ handler.OnAccessibilityTreeChange(nil)
98+ handler.OnAccessibilityLocationChange(nil)
99+ assert.Equal(t, 0, calls)
100+ assert.Empty(t, got)
101+}
102+
103+func TestAccessibilityHandler_TreeAndLocationWriteJSONOnce(t *testing.T) {
104+ var writeCalls []string
105+ var got []accessibilityPayload
106+ handler, ok := newAccessibilityHandler(func(p accessibilityPayload) bool {
107+ got = append(got, p)
108+ return true
109+ }).(*accessibilityHandler)
110+ require.True(t, ok)
111+ handler.writeJSON = func(value purecef.Value, options purecef.JsonWriterOptions) string {
112+ require.Equal(t, purecef.JsonWriterOptionsJsonWriterDefault, options)
113+ require.NotNil(t, value)
114+ writeCalls = append(writeCalls, "write")
115+ return `{"ok":true}`
116+ }
117+
118+ handler.OnAccessibilityTreeChange(stubCEFValue{})
119+ handler.OnAccessibilityLocationChange(stubCEFValue{})
120+
121+ require.Len(t, writeCalls, 2)
122+ require.Len(t, got, 2)
123+ assert.Equal(t, "tree", got[0].Kind)
124+ assert.Equal(t, `{"ok":true}`, got[0].JSON)
125+ assert.Equal(t, len(`{"ok":true}`), got[0].Bytes)
126+ assert.GreaterOrEqual(t, got[0].SerializeNanos, int64(0))
127+ assert.Equal(t, "location", got[1].Kind)
128+ assert.Equal(t, `{"ok":true}`, got[1].JSON)
129+ assert.Equal(t, len(`{"ok":true}`), got[1].Bytes)
130+ assert.GreaterOrEqual(t, got[1].SerializeNanos, int64(0))
131+}
132+
133+func TestAccessibilityHandler_ProductionNoCaptureZeroWriteJSON(t *testing.T) {
134+ calls := 0
135+
136+ // Factory/production path without DUMBER_A11Y_CAPTURE=1: worker stays nil,
137+ // sink is truly nil, WriteJson must not run.
138+ wv := &WebView{viewBridge: &Cef2gtkAdapter{}}
139+ rh := newDumberRenderHandler(wv)
140+ require.NotNil(t, rh)
141+
142+ handler, ok := rh.GetAccessibilityHandler().(*accessibilityHandler)
143+ require.True(t, ok)
144+ require.Nil(t, handler.sink)
145+ handler.writeJSON = func(purecef.Value, purecef.JsonWriterOptions) string {
146+ calls++
147+ return `{}`
148+ }
149+
150+ handler.OnAccessibilityTreeChange(stubCEFValue{})
151+ handler.OnAccessibilityLocationChange(stubCEFValue{})
152+ assert.Equal(t, 0, calls)
153+}
@@ -0,0 +1,153 @@
1+package cef
2+
3+import (
4+ "testing"
5+
6+ purecef "github.com/bnema/purego-cef/cef"
7+ cefmocks "github.com/bnema/purego-cef/cef/mocks"
8+ "github.com/stretchr/testify/assert"
9+ "github.com/stretchr/testify/require"
10+)
11+
12+type stubCEFValue struct{}
13+
14+func (stubCEFValue) IsValid() bool { return true }
15+func (stubCEFValue) IsOwned() bool { return false }
16+func (stubCEFValue) IsReadOnly() bool { return true }
17+func (stubCEFValue) IsSame(purecef.Value) bool { return false }
18+func (stubCEFValue) IsEqual(purecef.Value) bool { return false }
19+func (stubCEFValue) Copy() purecef.Value { return stubCEFValue{} }
20+func (stubCEFValue) GetType() purecef.ValueType { return 0 }
21+func (stubCEFValue) GetBool() int32 { return 0 }
22+func (stubCEFValue) GetInt() int32 { return 0 }
23+func (stubCEFValue) GetDouble() float64 { return 0 }
24+func (stubCEFValue) GetString() string { return "" }
25+func (stubCEFValue) GetBinary() purecef.BinaryValue { return nil }
26+func (stubCEFValue) GetDictionary() purecef.DictionaryValue { return nil }
27+func (stubCEFValue) GetList() purecef.ListValue { return nil }
28+func (stubCEFValue) SetNull() int32 { return 0 }
29+func (stubCEFValue) SetBool(int32) int32 { return 0 }
30+func (stubCEFValue) SetInt(int32) int32 { return 0 }
31+func (stubCEFValue) SetDouble(float64) int32 { return 0 }
32+func (stubCEFValue) SetString(string) int32 { return 0 }
33+func (stubCEFValue) SetBinary(purecef.BinaryValue) int32 { return 0 }
34+func (stubCEFValue) SetDictionary(purecef.DictionaryValue) int32 { return 0 }
35+func (stubCEFValue) SetList(purecef.ListValue) int32 { return 0 }
36+
37+func TestRenderHandlerExposesAccessibilityAtWrapTime(t *testing.T) {
38+ h := &dumberRenderHandler{a11y: newAccessibilityHandler(nil)}
39+ if h.GetAccessibilityHandler() == nil {
40+ t.Fatal("CEF would omit the accessibility override at wrap time")
41+ }
42+}
43+
44+func TestRenderHandlerExposesAccessibilityWhenMainExists(t *testing.T) {
45+ // Must fail open (nil) when a11y was not installed, even if main exists —
46+ // never fall back to h.main.GetAccessibilityHandler().
47+ mainHandler := cefmocks.NewMockRenderHandler(t)
48+ h := &dumberRenderHandler{main: mainHandler, a11y: nil}
49+ assert.Nil(t, h.GetAccessibilityHandler())
50+ mainHandler.AssertNotCalled(t, "GetAccessibilityHandler")
51+
52+ h.a11y = newAccessibilityHandler(nil)
53+ require.NotNil(t, h.GetAccessibilityHandler())
54+ mainHandler.AssertNotCalled(t, "GetAccessibilityHandler")
55+}
56+
57+func TestNewDumberRenderHandlerInstallsNonNilAccessibilityHandler(t *testing.T) {
58+ require.Nil(t, newDumberRenderHandler(nil))
59+
60+ wv := &WebView{viewBridge: &Cef2gtkAdapter{}}
61+ rh := newDumberRenderHandler(wv)
62+ require.NotNil(t, rh)
63+ require.NotNil(t, rh.GetAccessibilityHandler())
64+
65+ handler, ok := rh.GetAccessibilityHandler().(*accessibilityHandler)
66+ require.True(t, ok)
67+ assert.Nil(t, handler.sink)
68+}
69+
70+func TestAccessibilityHandler_NilSinkSkipsWriteJSON(t *testing.T) {
71+ calls := 0
72+ handler, ok := newAccessibilityHandler(nil).(*accessibilityHandler)
73+ require.True(t, ok)
74+ handler.writeJSON = func(purecef.Value, purecef.JsonWriterOptions) string {
75+ calls++
76+ return `{"unexpected":true}`
77+ }
78+
79+ handler.OnAccessibilityTreeChange(stubCEFValue{})
80+ handler.OnAccessibilityLocationChange(stubCEFValue{})
81+ assert.Equal(t, 0, calls)
82+}
83+
84+func TestAccessibilityHandler_NilValueIgnored(t *testing.T) {
85+ calls := 0
86+ var got []accessibilityPayload
87+ handler, ok := newAccessibilityHandler(func(p accessibilityPayload) bool {
88+ got = append(got, p)
89+ return true
90+ }).(*accessibilityHandler)
91+ require.True(t, ok)
92+ handler.writeJSON = func(purecef.Value, purecef.JsonWriterOptions) string {
93+ calls++
94+ return `{}`
95+ }
96+
97+ handler.OnAccessibilityTreeChange(nil)
98+ handler.OnAccessibilityLocationChange(nil)
99+ assert.Equal(t, 0, calls)
100+ assert.Empty(t, got)
101+}
102+
103+func TestAccessibilityHandler_TreeAndLocationWriteJSONOnce(t *testing.T) {
104+ var writeCalls []string
105+ var got []accessibilityPayload
106+ handler, ok := newAccessibilityHandler(func(p accessibilityPayload) bool {
107+ got = append(got, p)
108+ return true
109+ }).(*accessibilityHandler)
110+ require.True(t, ok)
111+ handler.writeJSON = func(value purecef.Value, options purecef.JsonWriterOptions) string {
112+ require.Equal(t, purecef.JsonWriterOptionsJsonWriterDefault, options)
113+ require.NotNil(t, value)
114+ writeCalls = append(writeCalls, "write")
115+ return `{"ok":true}`
116+ }
117+
118+ handler.OnAccessibilityTreeChange(stubCEFValue{})
119+ handler.OnAccessibilityLocationChange(stubCEFValue{})
120+
121+ require.Len(t, writeCalls, 2)
122+ require.Len(t, got, 2)
123+ assert.Equal(t, "tree", got[0].Kind)
124+ assert.Equal(t, `{"ok":true}`, got[0].JSON)
125+ assert.Equal(t, len(`{"ok":true}`), got[0].Bytes)
126+ assert.GreaterOrEqual(t, got[0].SerializeNanos, int64(0))
127+ assert.Equal(t, "location", got[1].Kind)
128+ assert.Equal(t, `{"ok":true}`, got[1].JSON)
129+ assert.Equal(t, len(`{"ok":true}`), got[1].Bytes)
130+ assert.GreaterOrEqual(t, got[1].SerializeNanos, int64(0))
131+}
132+
133+func TestAccessibilityHandler_ProductionNoCaptureZeroWriteJSON(t *testing.T) {
134+ calls := 0
135+
136+ // Factory/production path without DUMBER_A11Y_CAPTURE=1: worker stays nil,
137+ // sink is truly nil, WriteJson must not run.
138+ wv := &WebView{viewBridge: &Cef2gtkAdapter{}}
139+ rh := newDumberRenderHandler(wv)
140+ require.NotNil(t, rh)
141+
142+ handler, ok := rh.GetAccessibilityHandler().(*accessibilityHandler)
143+ require.True(t, ok)
144+ require.Nil(t, handler.sink)
145+ handler.writeJSON = func(purecef.Value, purecef.JsonWriterOptions) string {
146+ calls++
147+ return `{}`
148+ }
149+
150+ handler.OnAccessibilityTreeChange(stubCEFValue{})
151+ handler.OnAccessibilityLocationChange(stubCEFValue{})
152+ assert.Equal(t, 0, calls)
153+}
M internal/infrastructure/cef/render_handler_adapter.go +11 -2
@@ -19,6 +19,7 @@ type dumberRenderHandler struct {
1919 main purecef.RenderHandler
2020 popup purecef.RenderHandler
2121 popupSurface popupSurface
22+ a11y purecef.AccessibilityHandler
2223 }
2324
2425 var _ purecef.RenderHandler = (*dumberRenderHandler)(nil)
@@ -48,6 +49,14 @@ func newDumberRenderHandler(wv *WebView) purecef.RenderHandler {
4849 return nil
4950 }
5051 h := &dumberRenderHandler{wv: wv}
52+ // Install a non-nil AccessibilityHandler before CEF wraps the render
53+ // handler. Outside capture the sink is truly nil so callbacks return before
54+ // WriteJson; capture workers (Task 2) wire enqueueAccessibilityPayload.
55+ var sink func(accessibilityPayload) bool
56+ if wv.a11yWorker != nil {
57+ sink = wv.enqueueAccessibilityPayload
58+ }
59+ h.a11y = newAccessibilityHandler(sink)
5160
5261 var unsupportedPaintOnce sync.Once
5362 hooks := startupPresentationHooks(activeStartupTrace())
@@ -110,10 +119,10 @@ func (h *dumberRenderHandler) renderTargetForPaint(elementType purecef.PaintElem
110119 }
111120
112121 func (h *dumberRenderHandler) GetAccessibilityHandler() purecef.AccessibilityHandler {
113- if h == nil || h.main == nil {
122+ if h == nil {
114123 return nil
115124 }
116- return h.main.GetAccessibilityHandler()
125+ return h.a11y
117126 }
118127
119128 func (h *dumberRenderHandler) GetRootScreenRect(browser purecef.Browser, rect *purecef.Rect) int32 {
@@ -19,6 +19,7 @@ type dumberRenderHandler struct {
19 main purecef.RenderHandler19 main purecef.RenderHandler
20 popup purecef.RenderHandler20 popup purecef.RenderHandler
21 popupSurface popupSurface21 popupSurface popupSurface
22+ a11y purecef.AccessibilityHandler
22 }23 }
23 24
24 var _ purecef.RenderHandler = (*dumberRenderHandler)(nil)25 var _ purecef.RenderHandler = (*dumberRenderHandler)(nil)
@@ -48,6 +49,14 @@ func newDumberRenderHandler(wv *WebView) purecef.RenderHandler {
48 return nil49 return nil
49 }50 }
50 h := &dumberRenderHandler{wv: wv}51 h := &dumberRenderHandler{wv: wv}
52+ // Install a non-nil AccessibilityHandler before CEF wraps the render
53+ // handler. Outside capture the sink is truly nil so callbacks return before
54+ // WriteJson; capture workers (Task 2) wire enqueueAccessibilityPayload.
55+ var sink func(accessibilityPayload) bool
56+ if wv.a11yWorker != nil {
57+ sink = wv.enqueueAccessibilityPayload
58+ }
59+ h.a11y = newAccessibilityHandler(sink)
51 60
52 var unsupportedPaintOnce sync.Once61 var unsupportedPaintOnce sync.Once
53 hooks := startupPresentationHooks(activeStartupTrace())62 hooks := startupPresentationHooks(activeStartupTrace())
@@ -110,10 +119,10 @@ func (h *dumberRenderHandler) renderTargetForPaint(elementType purecef.PaintElem
110 }119 }
111 120
112 func (h *dumberRenderHandler) GetAccessibilityHandler() purecef.AccessibilityHandler {121 func (h *dumberRenderHandler) GetAccessibilityHandler() purecef.AccessibilityHandler {
113- if h == nil || h.main == nil {122+ if h == nil {
114 return nil123 return nil
115 }124 }
116- return h.main.GetAccessibilityHandler()125+ return h.a11y
117 }126 }
118 127
119 func (h *dumberRenderHandler) GetRootScreenRect(browser purecef.Browser, rect *purecef.Rect) int32 {128 func (h *dumberRenderHandler) GetRootScreenRect(browser purecef.Browser, rect *purecef.Rect) int32 {
M internal/infrastructure/cef/webview.go +4 -0
@@ -152,6 +152,10 @@ type WebView struct {
152152
153153 pageScrollQueue pageScrollQueue
154154
155+ // a11yWorker receives serialized accessibility payloads off the CEF UI
156+ // thread. Production leaves it nil unless DUMBER_A11Y_CAPTURE=1.
157+ a11yWorker accessibilityPayloadSink
158+
155159 // beginFrameTick drives CEF external BeginFrame requests while the GTK
156160 // widget is visible. Access is guarded by mu.
157161 beginFrameTick *gtk.TickCallback
@@ -152,6 +152,10 @@ type WebView struct {
152 152
153 pageScrollQueue pageScrollQueue153 pageScrollQueue pageScrollQueue
154 154
155+ // a11yWorker receives serialized accessibility payloads off the CEF UI
156+ // thread. Production leaves it nil unless DUMBER_A11Y_CAPTURE=1.
157+ a11yWorker accessibilityPayloadSink
158+
155 // beginFrameTick drives CEF external BeginFrame requests while the GTK159 // beginFrameTick drives CEF external BeginFrame requests while the GTK
156 // widget is visible. Access is guarded by mu.160 // widget is visible. Access is guarded by mu.
157 beginFrameTick *gtk.TickCallback161 beginFrameTick *gtk.TickCallback

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