detached 5214440

fix(cef): debounce selection updates and drop old bridge code

commit
521444082436df743bc7ca30d0392726fa19a533
author
brice · · 2026-04-17 15:24 UTC
parent
2a3f82d
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +1 -130
M internal/application/usecase/clipboard_text_orchestrator.go +0 -25
@@ -9,7 +9,6 @@ import (
99 "unicode/utf8"
1010
1111 "github.com/bnema/dumber/internal/application/port"
12- "github.com/bnema/dumber/internal/logging"
1312 )
1413
1514 const explicitClipboardDedupWindow = 250 * time.Millisecond
@@ -109,11 +108,6 @@ func (uc *ClipboardTextOrchestratorUseCase) HandleSelectionUpdate(
109108 }
110109 if err := uc.clipboard.WriteText(ctx, input.Text); err != nil {
111110 uc.mu.Unlock()
112- logging.FromContext(ctx).Debug().
113- Err(err).
114- Int("text_len", textLen).
115- Str("source_engine", string(input.SourceEngine)).
116- Msg("clipboard selection write failed")
117111 return fmt.Errorf("clipboard write failed: %w", err)
118112 }
119113
@@ -131,10 +125,6 @@ func (uc *ClipboardTextOrchestratorUseCase) HandleSelectionUpdate(
131125 if uc.toast != nil {
132126 uc.toast(textLen)
133127 }
134- logging.FromContext(ctx).Debug().
135- Int("text_len", textLen).
136- Str("source_engine", string(input.SourceEngine)).
137- Msg("clipboard selection copied")
138128 return nil
139129 }
140130
@@ -172,12 +162,6 @@ func (uc *ClipboardTextOrchestratorUseCase) HandleExplicitCopy(
172162 if !input.NativeHandled {
173163 if err := uc.clipboard.WriteText(ctx, input.Text); err != nil {
174164 uc.mu.Unlock()
175- logging.FromContext(ctx).Debug().
176- Err(err).
177- Int("text_len", textLen).
178- Str("source_engine", string(input.SourceEngine)).
179- Str("action", input.Action).
180- Msg("clipboard explicit write failed")
181165 return fmt.Errorf("clipboard write failed: %w", err)
182166 }
183167 }
@@ -188,15 +172,6 @@ func (uc *ClipboardTextOrchestratorUseCase) HandleExplicitCopy(
188172 if uc.toast != nil {
189173 uc.toast(textLen)
190174 }
191- logEntry := logging.FromContext(ctx).Debug().
192- Int("text_len", textLen).
193- Str("source_engine", string(input.SourceEngine)).
194- Str("action", input.Action)
195- if input.NativeHandled {
196- logEntry.Bool("native_handled", true).Msg("clipboard explicit copied (native handled)")
197- return nil
198- }
199- logEntry.Msg("clipboard explicit copied")
200175 return nil
201176 }
202177
@@ -9,7 +9,6 @@ import (
9 "unicode/utf8"9 "unicode/utf8"
10 10
11 "github.com/bnema/dumber/internal/application/port"11 "github.com/bnema/dumber/internal/application/port"
12- "github.com/bnema/dumber/internal/logging"
13 )12 )
14 13
15 const explicitClipboardDedupWindow = 250 * time.Millisecond14 const explicitClipboardDedupWindow = 250 * time.Millisecond
@@ -109,11 +108,6 @@ func (uc *ClipboardTextOrchestratorUseCase) HandleSelectionUpdate(
109 }108 }
110 if err := uc.clipboard.WriteText(ctx, input.Text); err != nil {109 if err := uc.clipboard.WriteText(ctx, input.Text); err != nil {
111 uc.mu.Unlock()110 uc.mu.Unlock()
112- logging.FromContext(ctx).Debug().
113- Err(err).
114- Int("text_len", textLen).
115- Str("source_engine", string(input.SourceEngine)).
116- Msg("clipboard selection write failed")
117 return fmt.Errorf("clipboard write failed: %w", err)111 return fmt.Errorf("clipboard write failed: %w", err)
118 }112 }
119 113
@@ -131,10 +125,6 @@ func (uc *ClipboardTextOrchestratorUseCase) HandleSelectionUpdate(
131 if uc.toast != nil {125 if uc.toast != nil {
132 uc.toast(textLen)126 uc.toast(textLen)
133 }127 }
134- logging.FromContext(ctx).Debug().
135- Int("text_len", textLen).
136- Str("source_engine", string(input.SourceEngine)).
137- Msg("clipboard selection copied")
138 return nil128 return nil
139 }129 }
140 130
@@ -172,12 +162,6 @@ func (uc *ClipboardTextOrchestratorUseCase) HandleExplicitCopy(
172 if !input.NativeHandled {162 if !input.NativeHandled {
173 if err := uc.clipboard.WriteText(ctx, input.Text); err != nil {163 if err := uc.clipboard.WriteText(ctx, input.Text); err != nil {
174 uc.mu.Unlock()164 uc.mu.Unlock()
175- logging.FromContext(ctx).Debug().
176- Err(err).
177- Int("text_len", textLen).
178- Str("source_engine", string(input.SourceEngine)).
179- Str("action", input.Action).
180- Msg("clipboard explicit write failed")
181 return fmt.Errorf("clipboard write failed: %w", err)165 return fmt.Errorf("clipboard write failed: %w", err)
182 }166 }
183 }167 }
@@ -188,15 +172,6 @@ func (uc *ClipboardTextOrchestratorUseCase) HandleExplicitCopy(
188 if uc.toast != nil {172 if uc.toast != nil {
189 uc.toast(textLen)173 uc.toast(textLen)
190 }174 }
191- logEntry := logging.FromContext(ctx).Debug().
192- Int("text_len", textLen).
193- Str("source_engine", string(input.SourceEngine)).
194- Str("action", input.Action)
195- if input.NativeHandled {
196- logEntry.Bool("native_handled", true).Msg("clipboard explicit copied (native handled)")
197- return nil
198- }
199- logEntry.Msg("clipboard explicit copied")
200 return nil175 return nil
201 }176 }
202 177
M internal/infrastructure/cef/factory.go +0 -6
@@ -103,12 +103,6 @@ func (f *WebViewFactory) Create(ctx context.Context) (port.WebView, error) {
103103 wv.findCtrl = newFindController()
104104
105105 input := newInputBridge(ctx, f.scale)
106- input.selectionText = wv.selectedTextSnapshot
107- input.explicitCopyText = func(action, text string) {
108- if f.engine != nil {
109- f.engine.handleExplicitClipboardBridgeText(wv.id, action, text)
110- }
111- }
112106 input.attachTo(pipeline.glArea)
113107 wv.input = input
114108
@@ -103,12 +103,6 @@ func (f *WebViewFactory) Create(ctx context.Context) (port.WebView, error) {
103 wv.findCtrl = newFindController()103 wv.findCtrl = newFindController()
104 104
105 input := newInputBridge(ctx, f.scale)105 input := newInputBridge(ctx, f.scale)
106- input.selectionText = wv.selectedTextSnapshot
107- input.explicitCopyText = func(action, text string) {
108- if f.engine != nil {
109- f.engine.handleExplicitClipboardBridgeText(wv.id, action, text)
110- }
111- }
112 input.attachTo(pipeline.glArea)106 input.attachTo(pipeline.glArea)
113 wv.input = input107 wv.input = input
114 108
M internal/infrastructure/cef/input_bridge.go +1 -48
@@ -57,14 +57,6 @@ type inputBridge struct {
5757 // ImeCommitText. Set once during attachTo, read-only afterwards.
5858 clipboard *gdk.Clipboard
5959
60- // selectionText returns the browser's latest known text selection so Ctrl+C
61- // / Ctrl+X can route the selection through the shared explicit-copy path.
62- selectionText func() string
63-
64- // explicitCopyText routes explicit copy/cut text through the shared
65- // clipboard orchestration path.
66- explicitCopyText func(action, text string)
67-
6860 // IMContext for dead key / compose sequence support. Held as a field
6961 // to prevent garbage collection while the controller is alive.
7062 imContext *gtk.IMContextSimple
@@ -231,8 +223,6 @@ func (ib *inputBridge) attachFocusAndKeyboard(glArea *gtk.GLArea) {
231223 keyPressCb := func(_ gtk.EventControllerKey, keyval, keycode uint, state gdk.ModifierType) bool {
232224 mods := uint(state)
233225
234- ib.maybeMirrorClipboardShortcut(keyval, mods)
235-
236226 // Intercept Ctrl+V / Ctrl+Shift+V: CEF OSR cannot access the
237227 // Wayland/X11 clipboard, so we read from GDK and inject via
238228 // ImeCommitText instead of letting CEF handle the paste.
@@ -381,39 +371,6 @@ func (ib *inputBridge) hasGTKFocus() bool {
381371 return ib.gtkFocused.Load()
382372 }
383373
384-func (ib *inputBridge) maybeMirrorClipboardShortcut(keyval, mods uint) {
385- action, ok := clipboardShortcutAction(keyval, mods)
386- if !ok || ib == nil || ib.selectionText == nil || ib.explicitCopyText == nil {
387- return
388- }
389- text := ib.selectionText()
390- if text == "" {
391- return
392- }
393- logging.FromContext(ib.ctx).Debug().
394- Str("action", action).
395- Int("text_len", len(text)).
396- Msg("cef: clipboard shortcut routed through explicit copy handler")
397- ib.explicitCopyText(action, text)
398-}
399-
400-func clipboardShortcutAction(keyval, mods uint) (string, bool) {
401- if mods&uint(gdk.ControlMaskValue) == 0 {
402- return "", false
403- }
404- if mods&uint(gdk.ShiftMaskValue) != 0 || mods&uint(gdk.AltMaskValue) != 0 {
405- return "", false
406- }
407- switch keyval {
408- case gdkKeyLowercaseC, gdkKeyUppercaseC:
409- return "copy", true
410- case gdkKeyLowercaseX, gdkKeyUppercaseX:
411- return "cut", true
412- default:
413- return "", false
414- }
415-}
416-
417374 func (ib *inputBridge) onKeyPress(keyval, keycode, mods uint) {
418375 ib.mu.Lock()
419376 host := ib.host
@@ -634,13 +591,9 @@ const (
634591 gdkKeyPageUp = 0xff55
635592 gdkKeyPageDown = 0xff56
636593
637- // Clipboard shortcut keyvals.
638- gdkKeyLowercaseC = 0x063
639- gdkKeyUppercaseC = 0x043
594+ // Paste shortcut keyvals.
640595 gdkKeyLowercaseV = 0x076
641596 gdkKeyUppercaseV = 0x056
642- gdkKeyLowercaseX = 0x078
643- gdkKeyUppercaseX = 0x058
644597 )
645598
646599 // keyvalToChar converts a GDK keyval to a UTF-16 character for CEF CHAR events.
@@ -57,14 +57,6 @@ type inputBridge struct {
57 // ImeCommitText. Set once during attachTo, read-only afterwards.57 // ImeCommitText. Set once during attachTo, read-only afterwards.
58 clipboard *gdk.Clipboard58 clipboard *gdk.Clipboard
59 59
60- // selectionText returns the browser's latest known text selection so Ctrl+C
61- // / Ctrl+X can route the selection through the shared explicit-copy path.
62- selectionText func() string
63-
64- // explicitCopyText routes explicit copy/cut text through the shared
65- // clipboard orchestration path.
66- explicitCopyText func(action, text string)
67-
68 // IMContext for dead key / compose sequence support. Held as a field60 // IMContext for dead key / compose sequence support. Held as a field
69 // to prevent garbage collection while the controller is alive.61 // to prevent garbage collection while the controller is alive.
70 imContext *gtk.IMContextSimple62 imContext *gtk.IMContextSimple
@@ -231,8 +223,6 @@ func (ib *inputBridge) attachFocusAndKeyboard(glArea *gtk.GLArea) {
231 keyPressCb := func(_ gtk.EventControllerKey, keyval, keycode uint, state gdk.ModifierType) bool {223 keyPressCb := func(_ gtk.EventControllerKey, keyval, keycode uint, state gdk.ModifierType) bool {
232 mods := uint(state)224 mods := uint(state)
233 225
234- ib.maybeMirrorClipboardShortcut(keyval, mods)
235-
236 // Intercept Ctrl+V / Ctrl+Shift+V: CEF OSR cannot access the226 // Intercept Ctrl+V / Ctrl+Shift+V: CEF OSR cannot access the
237 // Wayland/X11 clipboard, so we read from GDK and inject via227 // Wayland/X11 clipboard, so we read from GDK and inject via
238 // ImeCommitText instead of letting CEF handle the paste.228 // ImeCommitText instead of letting CEF handle the paste.
@@ -381,39 +371,6 @@ func (ib *inputBridge) hasGTKFocus() bool {
381 return ib.gtkFocused.Load()371 return ib.gtkFocused.Load()
382 }372 }
383 373
384-func (ib *inputBridge) maybeMirrorClipboardShortcut(keyval, mods uint) {
385- action, ok := clipboardShortcutAction(keyval, mods)
386- if !ok || ib == nil || ib.selectionText == nil || ib.explicitCopyText == nil {
387- return
388- }
389- text := ib.selectionText()
390- if text == "" {
391- return
392- }
393- logging.FromContext(ib.ctx).Debug().
394- Str("action", action).
395- Int("text_len", len(text)).
396- Msg("cef: clipboard shortcut routed through explicit copy handler")
397- ib.explicitCopyText(action, text)
398-}
399-
400-func clipboardShortcutAction(keyval, mods uint) (string, bool) {
401- if mods&uint(gdk.ControlMaskValue) == 0 {
402- return "", false
403- }
404- if mods&uint(gdk.ShiftMaskValue) != 0 || mods&uint(gdk.AltMaskValue) != 0 {
405- return "", false
406- }
407- switch keyval {
408- case gdkKeyLowercaseC, gdkKeyUppercaseC:
409- return "copy", true
410- case gdkKeyLowercaseX, gdkKeyUppercaseX:
411- return "cut", true
412- default:
413- return "", false
414- }
415-}
416-
417 func (ib *inputBridge) onKeyPress(keyval, keycode, mods uint) {374 func (ib *inputBridge) onKeyPress(keyval, keycode, mods uint) {
418 ib.mu.Lock()375 ib.mu.Lock()
419 host := ib.host376 host := ib.host
@@ -634,13 +591,9 @@ const (
634 gdkKeyPageUp = 0xff55591 gdkKeyPageUp = 0xff55
635 gdkKeyPageDown = 0xff56592 gdkKeyPageDown = 0xff56
636 593
637- // Clipboard shortcut keyvals.594+ // Paste shortcut keyvals.
638- gdkKeyLowercaseC = 0x063
639- gdkKeyUppercaseC = 0x043
640 gdkKeyLowercaseV = 0x076595 gdkKeyLowercaseV = 0x076
641 gdkKeyUppercaseV = 0x056596 gdkKeyUppercaseV = 0x056
642- gdkKeyLowercaseX = 0x078
643- gdkKeyUppercaseX = 0x058
644 )597 )
645 598
646 // keyvalToChar converts a GDK keyval to a UTF-16 character for CEF CHAR events.599 // keyvalToChar converts a GDK keyval to a UTF-16 character for CEF CHAR events.
M internal/infrastructure/cef/input_bridge_test.go +0 -51
@@ -1,11 +1,9 @@
11 package cef
22
33 import (
4- "context"
54 "testing"
65
76 purecef "github.com/bnema/purego-cef/cef"
8- "github.com/bnema/puregotk/v4/gdk"
97 "github.com/stretchr/testify/require"
108 )
119
@@ -41,52 +39,3 @@ func TestSyncWindowlessBrowserFocus_ReassertsVisibleFocusedAndInvalidates(t *tes
4139 require.Equal(t, []int32{1}, host.focusValues)
4240 require.Equal(t, []purecef.PaintElementType{purecef.PaintElementTypePetView}, host.invalidations)
4341 }
44-
45-func TestClipboardShortcutAction_RecognizesCopyAndCut(t *testing.T) {
46- action, ok := clipboardShortcutAction(gdkKeyLowercaseC, uint(gdk.ControlMaskValue))
47- require.True(t, ok)
48- require.Equal(t, "copy", action)
49-
50- action, ok = clipboardShortcutAction(gdkKeyUppercaseX, uint(gdk.ControlMaskValue))
51- require.True(t, ok)
52- require.Equal(t, "cut", action)
53-}
54-
55-func TestClipboardShortcutAction_IgnoresCtrlShiftC(t *testing.T) {
56- action, ok := clipboardShortcutAction(gdkKeyUppercaseC, uint(gdk.ControlMaskValue|gdk.ShiftMaskValue))
57- require.False(t, ok)
58- require.Empty(t, action)
59-}
60-
61-func TestMaybeMirrorClipboardShortcut_DelegatesExplicitCopyAction(t *testing.T) {
62- var gotAction string
63- var gotText string
64- ib := &inputBridge{
65- ctx: context.Background(),
66- selectionText: func() string { return "selected text" },
67- explicitCopyText: func(action, text string) {
68- gotAction = action
69- gotText = text
70- },
71- }
72-
73- ib.maybeMirrorClipboardShortcut(gdkKeyLowercaseC, uint(gdk.ControlMaskValue))
74-
75- require.Equal(t, "copy", gotAction)
76- require.Equal(t, "selected text", gotText)
77-}
78-
79-func TestMaybeMirrorClipboardShortcut_SkipsWhenSelectionEmpty(t *testing.T) {
80- called := false
81- ib := &inputBridge{
82- ctx: context.Background(),
83- selectionText: func() string { return "" },
84- explicitCopyText: func(string, string) {
85- called = true
86- },
87- }
88-
89- ib.maybeMirrorClipboardShortcut(gdkKeyLowercaseC, uint(gdk.ControlMaskValue))
90-
91- require.False(t, called)
92-}
@@ -1,11 +1,9 @@
1 package cef1 package cef
2 2
3 import (3 import (
4- "context"
5 "testing"4 "testing"
6 5
7 purecef "github.com/bnema/purego-cef/cef"6 purecef "github.com/bnema/purego-cef/cef"
8- "github.com/bnema/puregotk/v4/gdk"
9 "github.com/stretchr/testify/require"7 "github.com/stretchr/testify/require"
10 )8 )
11 9
@@ -41,52 +39,3 @@ func TestSyncWindowlessBrowserFocus_ReassertsVisibleFocusedAndInvalidates(t *tes
41 require.Equal(t, []int32{1}, host.focusValues)39 require.Equal(t, []int32{1}, host.focusValues)
42 require.Equal(t, []purecef.PaintElementType{purecef.PaintElementTypePetView}, host.invalidations)40 require.Equal(t, []purecef.PaintElementType{purecef.PaintElementTypePetView}, host.invalidations)
43 }41 }
44-
45-func TestClipboardShortcutAction_RecognizesCopyAndCut(t *testing.T) {
46- action, ok := clipboardShortcutAction(gdkKeyLowercaseC, uint(gdk.ControlMaskValue))
47- require.True(t, ok)
48- require.Equal(t, "copy", action)
49-
50- action, ok = clipboardShortcutAction(gdkKeyUppercaseX, uint(gdk.ControlMaskValue))
51- require.True(t, ok)
52- require.Equal(t, "cut", action)
53-}
54-
55-func TestClipboardShortcutAction_IgnoresCtrlShiftC(t *testing.T) {
56- action, ok := clipboardShortcutAction(gdkKeyUppercaseC, uint(gdk.ControlMaskValue|gdk.ShiftMaskValue))
57- require.False(t, ok)
58- require.Empty(t, action)
59-}
60-
61-func TestMaybeMirrorClipboardShortcut_DelegatesExplicitCopyAction(t *testing.T) {
62- var gotAction string
63- var gotText string
64- ib := &inputBridge{
65- ctx: context.Background(),
66- selectionText: func() string { return "selected text" },
67- explicitCopyText: func(action, text string) {
68- gotAction = action
69- gotText = text
70- },
71- }
72-
73- ib.maybeMirrorClipboardShortcut(gdkKeyLowercaseC, uint(gdk.ControlMaskValue))
74-
75- require.Equal(t, "copy", gotAction)
76- require.Equal(t, "selected text", gotText)
77-}
78-
79-func TestMaybeMirrorClipboardShortcut_SkipsWhenSelectionEmpty(t *testing.T) {
80- called := false
81- ib := &inputBridge{
82- ctx: context.Background(),
83- selectionText: func() string { return "" },
84- explicitCopyText: func(string, string) {
85- called = true
86- },
87- }
88-
89- ib.maybeMirrorClipboardShortcut(gdkKeyLowercaseC, uint(gdk.ControlMaskValue))
90-
91- require.False(t, called)
92-}

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