detached e1b799b

refactor(context-menu): scope adapter helpers per engine

Keep GTK menu rendering and resolved-image save helpers inside the WebKit and CEF adapters so the final wiring matches the engine-specific runtime boundaries.
commit
e1b799b3b229427be6d3812d4e17e6f30eb43f90
author
brice · · 2026-04-13 17:38 UTC
committer
brice · · 2026-04-14 12:06 UTC
parent
8bed89d
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
8 files · +141 -12
M internal/infrastructure/cef/context_menu.go +3 -3
@@ -2,11 +2,11 @@ package cef
22
33 import (
44 "context"
5- purecef "github.com/bnema/purego-cef/cef"
65 "strings"
76
7+ purecef "github.com/bnema/purego-cef/cef"
8+
89 "github.com/bnema/dumber/internal/application/port"
9- "github.com/bnema/dumber/internal/infrastructure/contextmenu"
1010 )
1111
1212 // ===========================================================================
@@ -71,7 +71,7 @@ func (h *handlerSet) RunContextMenu(
7171 y := params.GetYcoord()
7272 glArea := h.wv.pipeline.glArea
7373
74- contextmenu.NewRenderer(h.wv.runOnGTK).Show(
74+ NewRenderer(h.wv.runOnGTK).Show(
7575 items,
7676 &glArea.Widget,
7777 x,
@@ -2,11 +2,11 @@ package cef
2 2
3 import (3 import (
4 "context"4 "context"
5- purecef "github.com/bnema/purego-cef/cef"
6 "strings"5 "strings"
7 6
7+ purecef "github.com/bnema/purego-cef/cef"
8+
8 "github.com/bnema/dumber/internal/application/port"9 "github.com/bnema/dumber/internal/application/port"
9- "github.com/bnema/dumber/internal/infrastructure/contextmenu"
10 )10 )
11 11
12 // ===========================================================================12 // ===========================================================================
@@ -71,7 +71,7 @@ func (h *handlerSet) RunContextMenu(
71 y := params.GetYcoord()71 y := params.GetYcoord()
72 glArea := h.wv.pipeline.glArea72 glArea := h.wv.pipeline.glArea
73 73
74- contextmenu.NewRenderer(h.wv.runOnGTK).Show(74+ NewRenderer(h.wv.runOnGTK).Show(
75 items,75 items,
76 &glArea.Widget,76 &glArea.Widget,
77 x,77 x,
R internal/infrastructure/contextmenu/renderer.go → internal/infrastructure/cef/renderer.go +1 -1 renamed, 99% similar
@@ -1,4 +1,4 @@
1-package contextmenu
1+package cef
22
33 import (
44 "github.com/bnema/dumber/internal/application/port"
@@ -1,4 +1,4 @@
1-package contextmenu1+package cef
2 2
3 import (3 import (
4 "github.com/bnema/dumber/internal/application/port"4 "github.com/bnema/dumber/internal/application/port"
M internal/infrastructure/webkit/context_menu.go +1 -2
@@ -12,7 +12,6 @@ import (
1212 "time"
1313
1414 "github.com/bnema/dumber/internal/application/port"
15- "github.com/bnema/dumber/internal/infrastructure/contextmenu"
1615 "github.com/bnema/dumber/internal/logging"
1716 "github.com/bnema/puregotk-webkit/webkit"
1817 )
@@ -299,7 +298,7 @@ type contextMenuPipeline struct {
299298 clipboard port.Clipboard
300299 resolver port.ImageDataResolver
301300 saver port.ResolvedImageSaver
302- renderer *contextmenu.Renderer
301+ renderer *Renderer
303302 }
304303
305304 func (p *contextMenuPipeline) newExecutor(wv *WebView) port.ContextMenuActionExecutor {
@@ -12,7 +12,6 @@ import (
12 "time"12 "time"
13 13
14 "github.com/bnema/dumber/internal/application/port"14 "github.com/bnema/dumber/internal/application/port"
15- "github.com/bnema/dumber/internal/infrastructure/contextmenu"
16 "github.com/bnema/dumber/internal/logging"15 "github.com/bnema/dumber/internal/logging"
17 "github.com/bnema/puregotk-webkit/webkit"16 "github.com/bnema/puregotk-webkit/webkit"
18 )17 )
@@ -299,7 +298,7 @@ type contextMenuPipeline struct {
299 clipboard port.Clipboard298 clipboard port.Clipboard
300 resolver port.ImageDataResolver299 resolver port.ImageDataResolver
301 saver port.ResolvedImageSaver300 saver port.ResolvedImageSaver
302- renderer *contextmenu.Renderer301+ renderer *Renderer
303 }302 }
304 303
305 func (p *contextMenuPipeline) newExecutor(wv *WebView) port.ContextMenuActionExecutor {304 func (p *contextMenuPipeline) newExecutor(wv *WebView) port.ContextMenuActionExecutor {
M internal/infrastructure/webkit/engine.go +2 -3
@@ -6,7 +6,6 @@ import (
66
77 "github.com/bnema/dumber/internal/application/port"
88 "github.com/bnema/dumber/internal/infrastructure/config"
9- "github.com/bnema/dumber/internal/infrastructure/contextmenu"
109 "github.com/bnema/dumber/internal/infrastructure/filtering"
1110 "github.com/bnema/dumber/internal/infrastructure/handlers"
1211 "github.com/rs/zerolog"
@@ -170,11 +169,11 @@ func (e *Engine) UpdateSettings(ctx context.Context, update port.EngineSettingsU
170169 // WebKit-specific concern.
171170 func (e *Engine) installContextMenuPipeline() {
172171 resolver := NewContextMenuResolver()
173- saver := contextmenu.NewResolvedImageSaver(e.downloadPreparer, e.downloadPath)
172+ saver := NewResolvedImageSaver(e.downloadPreparer, e.downloadPath)
174173
175174 // WebKit's context-menu signal fires on the GTK main thread, so no
176175 // dispatch wrapper is needed.
177- renderer := contextmenu.NewRenderer(nil)
176+ renderer := NewRenderer(nil)
178177
179178 pipeline := &contextMenuPipeline{
180179 builder: e.ctxMenuBuilder,
@@ -6,7 +6,6 @@ import (
6 6
7 "github.com/bnema/dumber/internal/application/port"7 "github.com/bnema/dumber/internal/application/port"
8 "github.com/bnema/dumber/internal/infrastructure/config"8 "github.com/bnema/dumber/internal/infrastructure/config"
9- "github.com/bnema/dumber/internal/infrastructure/contextmenu"
10 "github.com/bnema/dumber/internal/infrastructure/filtering"9 "github.com/bnema/dumber/internal/infrastructure/filtering"
11 "github.com/bnema/dumber/internal/infrastructure/handlers"10 "github.com/bnema/dumber/internal/infrastructure/handlers"
12 "github.com/rs/zerolog"11 "github.com/rs/zerolog"
@@ -170,11 +169,11 @@ func (e *Engine) UpdateSettings(ctx context.Context, update port.EngineSettingsU
170 // WebKit-specific concern.169 // WebKit-specific concern.
171 func (e *Engine) installContextMenuPipeline() {170 func (e *Engine) installContextMenuPipeline() {
172 resolver := NewContextMenuResolver()171 resolver := NewContextMenuResolver()
173- saver := contextmenu.NewResolvedImageSaver(e.downloadPreparer, e.downloadPath)172+ saver := NewResolvedImageSaver(e.downloadPreparer, e.downloadPath)
174 173
175 // WebKit's context-menu signal fires on the GTK main thread, so no174 // WebKit's context-menu signal fires on the GTK main thread, so no
176 // dispatch wrapper is needed.175 // dispatch wrapper is needed.
177- renderer := contextmenu.NewRenderer(nil)176+ renderer := NewRenderer(nil)
178 177
179 pipeline := &contextMenuPipeline{178 pipeline := &contextMenuPipeline{
180 builder: e.ctxMenuBuilder,179 builder: e.ctxMenuBuilder,
A internal/infrastructure/webkit/renderer.go +131 -0
@@ -0,0 +1,131 @@
1+package webkit
2+
3+import (
4+ "github.com/bnema/dumber/internal/application/port"
5+ "github.com/bnema/puregotk/v4/gdk"
6+ "github.com/bnema/puregotk/v4/gtk"
7+)
8+
9+type renderItem struct {
10+ item port.MenuItem
11+ separator bool
12+}
13+
14+// Renderer shows GTK context menus on the provided anchor widget.
15+type Renderer struct {
16+ dispatch func(func())
17+}
18+
19+// NewRenderer creates a new Renderer.
20+func NewRenderer(dispatch func(func())) *Renderer {
21+ return &Renderer{dispatch: dispatch}
22+}
23+
24+func buildButtons(items []port.MenuItem) []renderItem {
25+ renderItems := make([]renderItem, 0, len(items))
26+ for _, item := range items {
27+ if isSeparator(item) {
28+ renderItems = append(renderItems, renderItem{separator: true})
29+ continue
30+ }
31+ renderItems = append(renderItems, renderItem{item: item})
32+ }
33+ return renderItems
34+}
35+
36+func isSeparator(item port.MenuItem) bool {
37+ return item.Action == "" && item.Label == ""
38+}
39+
40+// Show renders a popover menu on the GTK thread.
41+func (r *Renderer) Show(
42+ items []port.MenuItem,
43+ anchor *gtk.Widget,
44+ x, y int32,
45+ onSelect func(port.MenuItem),
46+ onClose func(),
47+) {
48+ if r == nil {
49+ return
50+ }
51+ run := func() {
52+ showMenu(anchor, buildButtons(items), x, y, onSelect, onClose)
53+ }
54+ if r.dispatch == nil {
55+ run()
56+ return
57+ }
58+ r.dispatch(run)
59+}
60+
61+func showMenu(
62+ anchor *gtk.Widget,
63+ items []renderItem,
64+ x, y int32,
65+ onSelect func(port.MenuItem),
66+ onClose func(),
67+) {
68+ if anchor == nil {
69+ if onClose != nil {
70+ onClose()
71+ }
72+ return
73+ }
74+
75+ box := gtk.NewBox(gtk.OrientationVerticalValue, 0)
76+ box.AddCssClass("context-menu")
77+
78+ selected := false
79+ var popover *gtk.Popover
80+
81+ for _, item := range items {
82+ if item.separator {
83+ sep := gtk.NewSeparator(gtk.OrientationHorizontalValue)
84+ box.Append(&sep.Widget)
85+ continue
86+ }
87+
88+ btn := gtk.NewButton()
89+ btn.SetLabel(item.item.Label)
90+ btn.AddCssClass("flat")
91+
92+ menuItem := item.item
93+ clickCb := func(_ gtk.Button) {
94+ selected = true
95+ if onSelect != nil {
96+ onSelect(menuItem)
97+ }
98+ if popover != nil {
99+ popover.Popdown()
100+ }
101+ }
102+ btn.ConnectClicked(&clickCb)
103+ box.Append(&btn.Widget)
104+ }
105+
106+ if len(items) == 0 {
107+ if onClose != nil {
108+ onClose()
109+ }
110+ return
111+ }
112+
113+ popover = gtk.NewPopover()
114+ popover.SetChild(&box.Widget)
115+ popover.SetParent(anchor)
116+ popover.SetHasArrow(false)
117+ popover.SetAutohide(true)
118+
119+ rect := &gdk.Rectangle{X: int(x), Y: int(y), Width: 1, Height: 1}
120+ popover.SetPointingTo(rect)
121+
122+ closedCb := func(_ gtk.Popover) {
123+ if !selected && onClose != nil {
124+ onClose()
125+ }
126+ popover.Unparent()
127+ }
128+ popover.ConnectClosed(&closedCb)
129+
130+ popover.Popup()
131+}
@@ -0,0 +1,131 @@
1+package webkit
2+
3+import (
4+ "github.com/bnema/dumber/internal/application/port"
5+ "github.com/bnema/puregotk/v4/gdk"
6+ "github.com/bnema/puregotk/v4/gtk"
7+)
8+
9+type renderItem struct {
10+ item port.MenuItem
11+ separator bool
12+}
13+
14+// Renderer shows GTK context menus on the provided anchor widget.
15+type Renderer struct {
16+ dispatch func(func())
17+}
18+
19+// NewRenderer creates a new Renderer.
20+func NewRenderer(dispatch func(func())) *Renderer {
21+ return &Renderer{dispatch: dispatch}
22+}
23+
24+func buildButtons(items []port.MenuItem) []renderItem {
25+ renderItems := make([]renderItem, 0, len(items))
26+ for _, item := range items {
27+ if isSeparator(item) {
28+ renderItems = append(renderItems, renderItem{separator: true})
29+ continue
30+ }
31+ renderItems = append(renderItems, renderItem{item: item})
32+ }
33+ return renderItems
34+}
35+
36+func isSeparator(item port.MenuItem) bool {
37+ return item.Action == "" && item.Label == ""
38+}
39+
40+// Show renders a popover menu on the GTK thread.
41+func (r *Renderer) Show(
42+ items []port.MenuItem,
43+ anchor *gtk.Widget,
44+ x, y int32,
45+ onSelect func(port.MenuItem),
46+ onClose func(),
47+) {
48+ if r == nil {
49+ return
50+ }
51+ run := func() {
52+ showMenu(anchor, buildButtons(items), x, y, onSelect, onClose)
53+ }
54+ if r.dispatch == nil {
55+ run()
56+ return
57+ }
58+ r.dispatch(run)
59+}
60+
61+func showMenu(
62+ anchor *gtk.Widget,
63+ items []renderItem,
64+ x, y int32,
65+ onSelect func(port.MenuItem),
66+ onClose func(),
67+) {
68+ if anchor == nil {
69+ if onClose != nil {
70+ onClose()
71+ }
72+ return
73+ }
74+
75+ box := gtk.NewBox(gtk.OrientationVerticalValue, 0)
76+ box.AddCssClass("context-menu")
77+
78+ selected := false
79+ var popover *gtk.Popover
80+
81+ for _, item := range items {
82+ if item.separator {
83+ sep := gtk.NewSeparator(gtk.OrientationHorizontalValue)
84+ box.Append(&sep.Widget)
85+ continue
86+ }
87+
88+ btn := gtk.NewButton()
89+ btn.SetLabel(item.item.Label)
90+ btn.AddCssClass("flat")
91+
92+ menuItem := item.item
93+ clickCb := func(_ gtk.Button) {
94+ selected = true
95+ if onSelect != nil {
96+ onSelect(menuItem)
97+ }
98+ if popover != nil {
99+ popover.Popdown()
100+ }
101+ }
102+ btn.ConnectClicked(&clickCb)
103+ box.Append(&btn.Widget)
104+ }
105+
106+ if len(items) == 0 {
107+ if onClose != nil {
108+ onClose()
109+ }
110+ return
111+ }
112+
113+ popover = gtk.NewPopover()
114+ popover.SetChild(&box.Widget)
115+ popover.SetParent(anchor)
116+ popover.SetHasArrow(false)
117+ popover.SetAutohide(true)
118+
119+ rect := &gdk.Rectangle{X: int(x), Y: int(y), Width: 1, Height: 1}
120+ popover.SetPointingTo(rect)
121+
122+ closedCb := func(_ gtk.Popover) {
123+ if !selected && onClose != nil {
124+ onClose()
125+ }
126+ popover.Unparent()
127+ }
128+ popover.ConnectClosed(&closedCb)
129+
130+ popover.Popup()
131+}
R internal/infrastructure/contextmenu/renderer_test.go → internal/infrastructure/webkit/renderer_test.go +1 -1 renamed, 97% similar
@@ -1,4 +1,4 @@
1-package contextmenu
1+package webkit
22
33 import (
44 "testing"
@@ -1,4 +1,4 @@
1-package contextmenu1+package webkit
2 2
3 import (3 import (
4 "testing"4 "testing"
R internal/infrastructure/contextmenu/resolved_image_saver.go → internal/infrastructure/webkit/resolved_image_saver.go +1 -1 renamed, 99% similar
@@ -1,4 +1,4 @@
1-package contextmenu
1+package webkit
22
33 import (
44 "context"
@@ -1,4 +1,4 @@
1-package contextmenu1+package webkit
2 2
3 import (3 import (
4 "context"4 "context"
R internal/infrastructure/contextmenu/resolved_image_saver_test.go → internal/infrastructure/webkit/resolved_image_saver_test.go +1 -1 renamed, 99% similar
@@ -1,4 +1,4 @@
1-package contextmenu
1+package webkit
22
33 import (
44 "context"
@@ -1,4 +1,4 @@
1-package contextmenu1+package webkit
2 2
3 import (3 import (
4 "context"4 "context"

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