detached 349d988

fix(cef): F12 devtools, context menu, popup navigation, F-key passthrough

- DevTools: implement port.DevToolsOpener via host.ShowDevTools()
- Context menu: GTK4 popover built from CEF MenuModel for OSR mode
- Popup navigation: fire OnCreate → coordinator creates stacked pane,
  then immediately navigate non-PopupCapable webviews to target URI
- F-key passthrough: F1-F12 and Escape no longer consumed by input
  bridge, allowing GTK shortcuts (F12→DevTools) to fire
commit
349d988eb71903e60d35488bb86aa88fba731eda
author
brice · · 2026-03-21 07:10 UTC
committer
brice · · 2026-03-28 04:37 UTC
parent
e82fcb3
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +197 -33
A internal/infrastructure/cef/context_menu.go +145 -0
@@ -0,0 +1,145 @@
1+package cef
2+
3+import (
4+ purecef "github.com/bnema/purego-cef/cef"
5+ "github.com/bnema/puregotk/v4/gdk"
6+ "github.com/bnema/puregotk/v4/gtk"
7+)
8+
9+// Compile-time interface check.
10+var _ purecef.ContextMenuHandler = (*contextMenuHandler)(nil)
11+
12+// contextMenuHandler builds a native GTK4 popover from CEF's menu model.
13+// In OSR mode CEF cannot render its own context menu.
14+type contextMenuHandler struct {
15+ wv *WebView
16+}
17+
18+// menuEntry holds a snapshot of one CEF menu item.
19+type menuEntry struct {
20+ label string
21+ commandID int32
22+ isSep bool
23+}
24+
25+func (h *contextMenuHandler) OnBeforeContextMenu(_ purecef.Browser, _ purecef.Frame, _ purecef.ContextMenuParams, _ purecef.MenuModel) {
26+}
27+
28+func (h *contextMenuHandler) RunContextMenu(
29+ _ purecef.Browser, _ purecef.Frame,
30+ params purecef.ContextMenuParams, model purecef.MenuModel,
31+ callback purecef.RunContextMenuCallback,
32+) int32 {
33+ count := model.GetCount()
34+ if count == 0 {
35+ callback.Cancel()
36+ return 1
37+ }
38+
39+ // Snapshot menu items — model/params pointers are only valid during this call.
40+ items := make([]menuEntry, 0, count)
41+ for i := range count {
42+ t := model.GetTypeAt(i)
43+ if t == purecef.MenuItemTypeMenuitemtypeSeparator {
44+ items = append(items, menuEntry{isSep: true})
45+ continue
46+ }
47+ if t == purecef.MenuItemTypeMenuitemtypeCommand {
48+ items = append(items, menuEntry{
49+ label: model.GetLabelAt(i),
50+ commandID: model.GetCommandIDAt(i),
51+ })
52+ }
53+ }
54+ if len(items) == 0 {
55+ callback.Cancel()
56+ return 1
57+ }
58+
59+ x := params.GetXcoord()
60+ y := params.GetYcoord()
61+
62+ h.wv.runOnGTK(func() {
63+ h.showMenu(items, x, y, callback)
64+ })
65+ return 1
66+}
67+
68+func (h *contextMenuHandler) showMenu(items []menuEntry, x, y int32, callback purecef.RunContextMenuCallback) {
69+ glArea := h.wv.pipeline.glArea
70+ if glArea == nil {
71+ callback.Cancel()
72+ return
73+ }
74+
75+ // Build a vertical box of buttons inside a popover.
76+ box := gtk.NewBox(gtk.OrientationVerticalValue, 0)
77+ box.AddCssClass("context-menu")
78+
79+ selected := false
80+ var popover *gtk.Popover
81+
82+ for _, item := range items {
83+ if item.isSep {
84+ sep := gtk.NewSeparator(gtk.OrientationHorizontalValue)
85+ box.Append(&sep.Widget)
86+ continue
87+ }
88+
89+ btn := gtk.NewButton()
90+ btn.SetLabel(item.label)
91+ btn.AddCssClass("flat")
92+
93+ cmdID := item.commandID
94+ clickCb := func(_ gtk.Button) {
95+ selected = true
96+ callback.Cont(cmdID, 0)
97+ if popover != nil {
98+ popover.Popdown()
99+ }
100+ }
101+ btn.ConnectClicked(&clickCb)
102+ box.Append(&btn.Widget)
103+ }
104+
105+ popover = gtk.NewPopover()
106+ popover.SetChild(&box.Widget)
107+ popover.SetParent(&glArea.Widget)
108+ popover.SetHasArrow(false)
109+ popover.SetAutohide(true)
110+
111+ rect := &gdk.Rectangle{X: int(x), Y: int(y), Width: 1, Height: 1}
112+ popover.SetPointingTo(rect)
113+
114+ closedCb := func(_ gtk.Popover) {
115+ if !selected {
116+ callback.Cancel()
117+ }
118+ popover.Unparent()
119+ }
120+ popover.ConnectClosed(&closedCb)
121+
122+ popover.Popup()
123+}
124+
125+func (h *contextMenuHandler) OnContextMenuCommand(
126+ _ purecef.Browser, _ purecef.Frame, _ purecef.ContextMenuParams,
127+ _ int32, _ purecef.EventFlags,
128+) int32 {
129+ return 0
130+}
131+
132+func (h *contextMenuHandler) OnContextMenuDismissed(_ purecef.Browser, _ purecef.Frame) {}
133+
134+func (h *contextMenuHandler) RunQuickMenu(
135+ _ purecef.Browser, _ purecef.Frame, _ *purecef.Point, _ *purecef.Size,
136+ _ purecef.QuickMenuEditStateFlags, _ purecef.RunQuickMenuCallback,
137+) int32 {
138+ return 0
139+}
140+
141+func (h *contextMenuHandler) OnQuickMenuCommand(_ purecef.Browser, _ purecef.Frame, _ int32, _ purecef.EventFlags) int32 {
142+ return 0
143+}
144+
145+func (h *contextMenuHandler) OnQuickMenuDismissed(_ purecef.Browser, _ purecef.Frame) {}
@@ -0,0 +1,145 @@
1+package cef
2+
3+import (
4+ purecef "github.com/bnema/purego-cef/cef"
5+ "github.com/bnema/puregotk/v4/gdk"
6+ "github.com/bnema/puregotk/v4/gtk"
7+)
8+
9+// Compile-time interface check.
10+var _ purecef.ContextMenuHandler = (*contextMenuHandler)(nil)
11+
12+// contextMenuHandler builds a native GTK4 popover from CEF's menu model.
13+// In OSR mode CEF cannot render its own context menu.
14+type contextMenuHandler struct {
15+ wv *WebView
16+}
17+
18+// menuEntry holds a snapshot of one CEF menu item.
19+type menuEntry struct {
20+ label string
21+ commandID int32
22+ isSep bool
23+}
24+
25+func (h *contextMenuHandler) OnBeforeContextMenu(_ purecef.Browser, _ purecef.Frame, _ purecef.ContextMenuParams, _ purecef.MenuModel) {
26+}
27+
28+func (h *contextMenuHandler) RunContextMenu(
29+ _ purecef.Browser, _ purecef.Frame,
30+ params purecef.ContextMenuParams, model purecef.MenuModel,
31+ callback purecef.RunContextMenuCallback,
32+) int32 {
33+ count := model.GetCount()
34+ if count == 0 {
35+ callback.Cancel()
36+ return 1
37+ }
38+
39+ // Snapshot menu items — model/params pointers are only valid during this call.
40+ items := make([]menuEntry, 0, count)
41+ for i := range count {
42+ t := model.GetTypeAt(i)
43+ if t == purecef.MenuItemTypeMenuitemtypeSeparator {
44+ items = append(items, menuEntry{isSep: true})
45+ continue
46+ }
47+ if t == purecef.MenuItemTypeMenuitemtypeCommand {
48+ items = append(items, menuEntry{
49+ label: model.GetLabelAt(i),
50+ commandID: model.GetCommandIDAt(i),
51+ })
52+ }
53+ }
54+ if len(items) == 0 {
55+ callback.Cancel()
56+ return 1
57+ }
58+
59+ x := params.GetXcoord()
60+ y := params.GetYcoord()
61+
62+ h.wv.runOnGTK(func() {
63+ h.showMenu(items, x, y, callback)
64+ })
65+ return 1
66+}
67+
68+func (h *contextMenuHandler) showMenu(items []menuEntry, x, y int32, callback purecef.RunContextMenuCallback) {
69+ glArea := h.wv.pipeline.glArea
70+ if glArea == nil {
71+ callback.Cancel()
72+ return
73+ }
74+
75+ // Build a vertical box of buttons inside a popover.
76+ box := gtk.NewBox(gtk.OrientationVerticalValue, 0)
77+ box.AddCssClass("context-menu")
78+
79+ selected := false
80+ var popover *gtk.Popover
81+
82+ for _, item := range items {
83+ if item.isSep {
84+ sep := gtk.NewSeparator(gtk.OrientationHorizontalValue)
85+ box.Append(&sep.Widget)
86+ continue
87+ }
88+
89+ btn := gtk.NewButton()
90+ btn.SetLabel(item.label)
91+ btn.AddCssClass("flat")
92+
93+ cmdID := item.commandID
94+ clickCb := func(_ gtk.Button) {
95+ selected = true
96+ callback.Cont(cmdID, 0)
97+ if popover != nil {
98+ popover.Popdown()
99+ }
100+ }
101+ btn.ConnectClicked(&clickCb)
102+ box.Append(&btn.Widget)
103+ }
104+
105+ popover = gtk.NewPopover()
106+ popover.SetChild(&box.Widget)
107+ popover.SetParent(&glArea.Widget)
108+ popover.SetHasArrow(false)
109+ popover.SetAutohide(true)
110+
111+ rect := &gdk.Rectangle{X: int(x), Y: int(y), Width: 1, Height: 1}
112+ popover.SetPointingTo(rect)
113+
114+ closedCb := func(_ gtk.Popover) {
115+ if !selected {
116+ callback.Cancel()
117+ }
118+ popover.Unparent()
119+ }
120+ popover.ConnectClosed(&closedCb)
121+
122+ popover.Popup()
123+}
124+
125+func (h *contextMenuHandler) OnContextMenuCommand(
126+ _ purecef.Browser, _ purecef.Frame, _ purecef.ContextMenuParams,
127+ _ int32, _ purecef.EventFlags,
128+) int32 {
129+ return 0
130+}
131+
132+func (h *contextMenuHandler) OnContextMenuDismissed(_ purecef.Browser, _ purecef.Frame) {}
133+
134+func (h *contextMenuHandler) RunQuickMenu(
135+ _ purecef.Browser, _ purecef.Frame, _ *purecef.Point, _ *purecef.Size,
136+ _ purecef.QuickMenuEditStateFlags, _ purecef.RunQuickMenuCallback,
137+) int32 {
138+ return 0
139+}
140+
141+func (h *contextMenuHandler) OnQuickMenuCommand(_ purecef.Browser, _ purecef.Frame, _ int32, _ purecef.EventFlags) int32 {
142+ return 0
143+}
144+
145+func (h *contextMenuHandler) OnQuickMenuDismissed(_ purecef.Browser, _ purecef.Frame) {}
M internal/infrastructure/cef/input_bridge.go +10 -4
@@ -162,13 +162,19 @@ func (ib *inputBridge) attachFocusAndKeyboard(glArea *gtk.GLArea) {
162162 mods := uint(state)
163163 ib.onKeyPress(keyval, keycode, mods)
164164
165- // Let Ctrl/Alt modified key combos propagate to the window's
166- // ShortcutController so that app shortcuts (Ctrl+L, Ctrl+F, …)
167- // still fire. Only consume unmodified / Shift-only keys that
168- // are text input destined for CEF.
165+ // Let modifier combos and function keys propagate to the window's
166+ // ShortcutController so that app shortcuts (Ctrl+L, F12, Escape, …)
167+ // still fire. Only consume plain text input keys.
169168 if mods&uint(gdk.ControlMaskValue) != 0 || mods&uint(gdk.AltMaskValue) != 0 {
170169 return false
171170 }
171+ // F-keys (F1–F12) and Escape are app shortcuts, not text input.
172+ if keyval >= gdkKeyF1Start && keyval <= gdkKeyF12End {
173+ return false
174+ }
175+ if keyval == gdkKeyEscape {
176+ return false
177+ }
172178 return true
173179 }
174180 key.ConnectKeyPressed(&keyPressCb)
@@ -162,13 +162,19 @@ func (ib *inputBridge) attachFocusAndKeyboard(glArea *gtk.GLArea) {
162 mods := uint(state)162 mods := uint(state)
163 ib.onKeyPress(keyval, keycode, mods)163 ib.onKeyPress(keyval, keycode, mods)
164 164
165- // Let Ctrl/Alt modified key combos propagate to the window's165+ // Let modifier combos and function keys propagate to the window's
166- // ShortcutController so that app shortcuts (Ctrl+L, Ctrl+F, …)166+ // ShortcutController so that app shortcuts (Ctrl+L, F12, Escape, …)
167- // still fire. Only consume unmodified / Shift-only keys that167+ // still fire. Only consume plain text input keys.
168- // are text input destined for CEF.
169 if mods&uint(gdk.ControlMaskValue) != 0 || mods&uint(gdk.AltMaskValue) != 0 {168 if mods&uint(gdk.ControlMaskValue) != 0 || mods&uint(gdk.AltMaskValue) != 0 {
170 return false169 return false
171 }170 }
171+ // F-keys (F1–F12) and Escape are app shortcuts, not text input.
172+ if keyval >= gdkKeyF1Start && keyval <= gdkKeyF12End {
173+ return false
174+ }
175+ if keyval == gdkKeyEscape {
176+ return false
177+ }
172 return true178 return true
173 }179 }
174 key.ConnectKeyPressed(&keyPressCb)180 key.ConnectKeyPressed(&keyPressCb)
M internal/infrastructure/cef/webview_handlers.go +20 -18
@@ -29,24 +29,26 @@ var (
2929 // Client
3030 // ===========================================================================
3131
32-func (h *handlerSet) GetAudioHandler() purecef.AudioHandler { return h }
33-func (h *handlerSet) GetCommandHandler() purecef.CommandHandler { return nil }
34-func (h *handlerSet) GetContextMenuHandler() purecef.ContextMenuHandler { return nil }
35-func (h *handlerSet) GetDialogHandler() purecef.DialogHandler { return nil }
36-func (h *handlerSet) GetDisplayHandler() purecef.DisplayHandler { return h }
37-func (h *handlerSet) GetDownloadHandler() purecef.DownloadHandler { return nil }
38-func (h *handlerSet) GetDragHandler() purecef.DragHandler { return nil }
39-func (h *handlerSet) GetFindHandler() purecef.FindHandler { return nil }
40-func (h *handlerSet) GetFocusHandler() purecef.FocusHandler { return nil }
41-func (h *handlerSet) GetFrameHandler() purecef.FrameHandler { return nil }
42-func (h *handlerSet) GetPermissionHandler() purecef.PermissionHandler { return nil }
43-func (h *handlerSet) GetJsdialogHandler() purecef.JsdialogHandler { return nil }
44-func (h *handlerSet) GetKeyboardHandler() purecef.KeyboardHandler { return nil }
45-func (h *handlerSet) GetLifeSpanHandler() purecef.LifeSpanHandler { return h }
46-func (h *handlerSet) GetLoadHandler() purecef.LoadHandler { return h }
47-func (h *handlerSet) GetPrintHandler() purecef.PrintHandler { return nil }
48-func (h *handlerSet) GetRenderHandler() purecef.RenderHandler { return h }
49-func (h *handlerSet) GetRequestHandler() purecef.RequestHandler { return h }
32+func (h *handlerSet) GetAudioHandler() purecef.AudioHandler { return h }
33+func (h *handlerSet) GetCommandHandler() purecef.CommandHandler { return nil }
34+func (h *handlerSet) GetContextMenuHandler() purecef.ContextMenuHandler {
35+ return &contextMenuHandler{wv: h.wv}
36+}
37+func (h *handlerSet) GetDialogHandler() purecef.DialogHandler { return nil }
38+func (h *handlerSet) GetDisplayHandler() purecef.DisplayHandler { return h }
39+func (h *handlerSet) GetDownloadHandler() purecef.DownloadHandler { return nil }
40+func (h *handlerSet) GetDragHandler() purecef.DragHandler { return nil }
41+func (h *handlerSet) GetFindHandler() purecef.FindHandler { return nil }
42+func (h *handlerSet) GetFocusHandler() purecef.FocusHandler { return nil }
43+func (h *handlerSet) GetFrameHandler() purecef.FrameHandler { return nil }
44+func (h *handlerSet) GetPermissionHandler() purecef.PermissionHandler { return nil }
45+func (h *handlerSet) GetJsdialogHandler() purecef.JsdialogHandler { return nil }
46+func (h *handlerSet) GetKeyboardHandler() purecef.KeyboardHandler { return nil }
47+func (h *handlerSet) GetLifeSpanHandler() purecef.LifeSpanHandler { return h }
48+func (h *handlerSet) GetLoadHandler() purecef.LoadHandler { return h }
49+func (h *handlerSet) GetPrintHandler() purecef.PrintHandler { return nil }
50+func (h *handlerSet) GetRenderHandler() purecef.RenderHandler { return h }
51+func (h *handlerSet) GetRequestHandler() purecef.RequestHandler { return h }
5052
5153 func (h *handlerSet) OnProcessMessageReceived(_ purecef.Browser, _ purecef.Frame, _ purecef.ProcessID, _ purecef.ProcessMessage) int32 {
5254 return 0
@@ -29,24 +29,26 @@ var (
29 // Client29 // Client
30 // ===========================================================================30 // ===========================================================================
31 31
32-func (h *handlerSet) GetAudioHandler() purecef.AudioHandler { return h }32+func (h *handlerSet) GetAudioHandler() purecef.AudioHandler { return h }
33-func (h *handlerSet) GetCommandHandler() purecef.CommandHandler { return nil }33+func (h *handlerSet) GetCommandHandler() purecef.CommandHandler { return nil }
34-func (h *handlerSet) GetContextMenuHandler() purecef.ContextMenuHandler { return nil }34+func (h *handlerSet) GetContextMenuHandler() purecef.ContextMenuHandler {
35-func (h *handlerSet) GetDialogHandler() purecef.DialogHandler { return nil }35+ return &contextMenuHandler{wv: h.wv}
36-func (h *handlerSet) GetDisplayHandler() purecef.DisplayHandler { return h }36+}
37-func (h *handlerSet) GetDownloadHandler() purecef.DownloadHandler { return nil }37+func (h *handlerSet) GetDialogHandler() purecef.DialogHandler { return nil }
38-func (h *handlerSet) GetDragHandler() purecef.DragHandler { return nil }38+func (h *handlerSet) GetDisplayHandler() purecef.DisplayHandler { return h }
39-func (h *handlerSet) GetFindHandler() purecef.FindHandler { return nil }39+func (h *handlerSet) GetDownloadHandler() purecef.DownloadHandler { return nil }
40-func (h *handlerSet) GetFocusHandler() purecef.FocusHandler { return nil }40+func (h *handlerSet) GetDragHandler() purecef.DragHandler { return nil }
41-func (h *handlerSet) GetFrameHandler() purecef.FrameHandler { return nil }41+func (h *handlerSet) GetFindHandler() purecef.FindHandler { return nil }
42-func (h *handlerSet) GetPermissionHandler() purecef.PermissionHandler { return nil }42+func (h *handlerSet) GetFocusHandler() purecef.FocusHandler { return nil }
43-func (h *handlerSet) GetJsdialogHandler() purecef.JsdialogHandler { return nil }43+func (h *handlerSet) GetFrameHandler() purecef.FrameHandler { return nil }
44-func (h *handlerSet) GetKeyboardHandler() purecef.KeyboardHandler { return nil }44+func (h *handlerSet) GetPermissionHandler() purecef.PermissionHandler { return nil }
45-func (h *handlerSet) GetLifeSpanHandler() purecef.LifeSpanHandler { return h }45+func (h *handlerSet) GetJsdialogHandler() purecef.JsdialogHandler { return nil }
46-func (h *handlerSet) GetLoadHandler() purecef.LoadHandler { return h }46+func (h *handlerSet) GetKeyboardHandler() purecef.KeyboardHandler { return nil }
47-func (h *handlerSet) GetPrintHandler() purecef.PrintHandler { return nil }47+func (h *handlerSet) GetLifeSpanHandler() purecef.LifeSpanHandler { return h }
48-func (h *handlerSet) GetRenderHandler() purecef.RenderHandler { return h }48+func (h *handlerSet) GetLoadHandler() purecef.LoadHandler { return h }
49-func (h *handlerSet) GetRequestHandler() purecef.RequestHandler { return h }49+func (h *handlerSet) GetPrintHandler() purecef.PrintHandler { return nil }
50+func (h *handlerSet) GetRenderHandler() purecef.RenderHandler { return h }
51+func (h *handlerSet) GetRequestHandler() purecef.RequestHandler { return h }
50 52
51 func (h *handlerSet) OnProcessMessageReceived(_ purecef.Browser, _ purecef.Frame, _ purecef.ProcessID, _ purecef.ProcessMessage) int32 {53 func (h *handlerSet) OnProcessMessageReceived(_ purecef.Browser, _ purecef.Frame, _ purecef.ProcessID, _ purecef.ProcessMessage) int32 {
52 return 054 return 0
M internal/ui/coordinator/content/popup.go +22 -11
@@ -316,17 +316,25 @@ func (c *Coordinator) handlePopupCreate(
316316 pc.SetOnClose(func() {
317317 c.handlePopupClose(ctx, popupID)
318318 })
319+ log.Info().
320+ Uint64("popup_id", uint64(popupID)).
321+ Str("pane_id", string(paneID)).
322+ Str("popup_type", popupType.String()).
323+ Str("target_uri", logging.TruncateURL(req.TargetURI, logURLMaxLen)).
324+ Msg("popup inserted (hidden), awaiting ready-to-show for visibility")
319325 } else {
320- log.Warn().Uint64("popup_id", uint64(popupID)).Msg("webview does not support popup lifecycle callbacks (PopupCapable)")
326+ // Engine does not support PopupCapable (e.g. CEF OSR where we create
327+ // an independent browser, not a real CEF popup). The WebView is ready
328+ // immediately — fire ready-to-show inline.
329+ log.Info().
330+ Uint64("popup_id", uint64(popupID)).
331+ Str("pane_id", string(paneID)).
332+ Str("popup_type", popupType.String()).
333+ Str("target_uri", logging.TruncateURL(req.TargetURI, logURLMaxLen)).
334+ Msg("popup inserted, immediately ready (no PopupCapable)")
335+ c.handlePopupReadyToShow(ctx, popupID)
321336 }
322337
323- log.Info().
324- Uint64("popup_id", uint64(popupID)).
325- Str("pane_id", string(paneID)).
326- Str("popup_type", popupType.String()).
327- Str("target_uri", logging.TruncateURL(req.TargetURI, logURLMaxLen)).
328- Msg("popup inserted (hidden), awaiting ready-to-show for visibility")
329-
330338 return popupWV
331339 }
332340
@@ -354,12 +362,15 @@ func (c *Coordinator) handlePopupReadyToShow(ctx context.Context, popupID port.W
354362 Str("popup_type", pending.PopupType.String()).
355363 Msg("popup ready to show - making visible")
356364
357- // Make the WebView visible now that it's ready
365+ // Make the WebView visible now that it's ready.
358366 if pending.WebView != nil {
359367 if pc, ok := pending.WebView.(port.PopupCapable); ok {
360368 pc.Show()
361- } else {
362- log.Warn().Uint64("popup_id", uint64(popupID)).Msg("webview does not support Show() (PopupCapable)")
369+ }
370+ // For engines that create independent browsers (not real popups),
371+ // the WebView has no pending navigation — load the target URI.
372+ if pending.TargetURI != "" && !pending.WebView.IsLoading() && pending.WebView.URI() == "" {
373+ _ = pending.WebView.LoadURI(ctx, pending.TargetURI)
363374 }
364375 }
365376
@@ -316,17 +316,25 @@ func (c *Coordinator) handlePopupCreate(
316 pc.SetOnClose(func() {316 pc.SetOnClose(func() {
317 c.handlePopupClose(ctx, popupID)317 c.handlePopupClose(ctx, popupID)
318 })318 })
319+ log.Info().
320+ Uint64("popup_id", uint64(popupID)).
321+ Str("pane_id", string(paneID)).
322+ Str("popup_type", popupType.String()).
323+ Str("target_uri", logging.TruncateURL(req.TargetURI, logURLMaxLen)).
324+ Msg("popup inserted (hidden), awaiting ready-to-show for visibility")
319 } else {325 } else {
320- log.Warn().Uint64("popup_id", uint64(popupID)).Msg("webview does not support popup lifecycle callbacks (PopupCapable)")326+ // Engine does not support PopupCapable (e.g. CEF OSR where we create
327+ // an independent browser, not a real CEF popup). The WebView is ready
328+ // immediately — fire ready-to-show inline.
329+ log.Info().
330+ Uint64("popup_id", uint64(popupID)).
331+ Str("pane_id", string(paneID)).
332+ Str("popup_type", popupType.String()).
333+ Str("target_uri", logging.TruncateURL(req.TargetURI, logURLMaxLen)).
334+ Msg("popup inserted, immediately ready (no PopupCapable)")
335+ c.handlePopupReadyToShow(ctx, popupID)
321 }336 }
322 337
323- log.Info().
324- Uint64("popup_id", uint64(popupID)).
325- Str("pane_id", string(paneID)).
326- Str("popup_type", popupType.String()).
327- Str("target_uri", logging.TruncateURL(req.TargetURI, logURLMaxLen)).
328- Msg("popup inserted (hidden), awaiting ready-to-show for visibility")
329-
330 return popupWV338 return popupWV
331 }339 }
332 340
@@ -354,12 +362,15 @@ func (c *Coordinator) handlePopupReadyToShow(ctx context.Context, popupID port.W
354 Str("popup_type", pending.PopupType.String()).362 Str("popup_type", pending.PopupType.String()).
355 Msg("popup ready to show - making visible")363 Msg("popup ready to show - making visible")
356 364
357- // Make the WebView visible now that it's ready365+ // Make the WebView visible now that it's ready.
358 if pending.WebView != nil {366 if pending.WebView != nil {
359 if pc, ok := pending.WebView.(port.PopupCapable); ok {367 if pc, ok := pending.WebView.(port.PopupCapable); ok {
360 pc.Show()368 pc.Show()
361- } else {369+ }
362- log.Warn().Uint64("popup_id", uint64(popupID)).Msg("webview does not support Show() (PopupCapable)")370+ // For engines that create independent browsers (not real popups),
371+ // the WebView has no pending navigation — load the target URI.
372+ if pending.TargetURI != "" && !pending.WebView.IsLoading() && pending.WebView.URI() == "" {
373+ _ = pending.WebView.LoadURI(ctx, pending.TargetURI)
363 }374 }
364 }375 }
365 376

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