detached 314ad3d

chore: fix branch lint issues

commit
314ad3ddaa85393a88f1b9f3ed338fa8194b690c
author
brice · · 2026-04-23 06:46 UTC
parent
292d988
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
11 files · +317 -309
M internal/infrastructure/cef/engine_init.go +100 -64
@@ -179,51 +179,80 @@ func wireEngine(
179179 return nil, fmt.Errorf("GL loader: %w", err)
180180 }
181181
182- scale := detectHiDPIScale(logger)
183-
184- // Initialize the GPU transcoder when enabled in the loaded app config.
185- var mediaTranscoder port.MediaTranscoder
186- transcoderState := buildTranscoderStartupState(transcodingCfg)
187- if transcodingCfg.Enabled {
188- transcoderState.ProbeAttempted = true
189- tc := transcoderpkg.New(config.TranscodingConfig{
190- Enabled: transcodingCfg.Enabled,
191- HWAccel: transcodingCfg.HWAccel,
192- MaxConcurrent: transcodingCfg.MaxConcurrent,
193- Quality: transcodingCfg.Quality,
194- }, logger)
195- caps := tc.Capabilities()
196- transcoderState.API = caps.API
197- transcoderState.Encoders = append([]string(nil), caps.Encoders...)
198- transcoderState.Decoders = append([]string(nil), caps.Decoders...)
199- if tc.Available() {
200- mediaTranscoder = tc
201- transcoderState.Status = "available"
202- logger.Info().
203- Str("api", tc.Capabilities().API).
204- Strs("encoders", tc.Capabilities().Encoders).
205- Msg("cef: GPU transcoding available")
206- } else {
207- transcoderState.Status = "unavailable_no_compatible_gpu"
208- logger.Warn().Msg("cef: GPU transcoding enabled but no compatible GPU found — feature disabled")
209- }
210- }
211-
212- factory := newWebViewFactory(eng, gl, webViewFactoryOptions{
213- scale: scale,
182+ mediaTranscoder, transcoderState := initializeEngineTranscoder(transcodingCfg, logger)
183+ eng.gl = gl
184+ eng.factory = newWebViewFactory(eng, gl, webViewFactoryOptions{
185+ scale: detectHiDPIScale(logger),
214186 windowlessFrameRate: windowlessFrameRate,
215187 transcoder: mediaTranscoder,
216188 mediaClassifier: mediaClassifier,
217189 audioOutputFactory: audioFactory,
218190 })
219- pool := newWebViewPool(factory)
220-
221- eng.gl = gl
222- eng.factory = factory
223- eng.pool = pool
191+ eng.pool = newWebViewPool(eng.factory)
224192 eng.contentInj = newContentInjector(eng, nil)
225193 eng.transcoderState = transcoderState
226194
195+ messageRouter, schemeHandler, err := newEngineSchemeHandler(
196+ ctx,
197+ eng,
198+ logger,
199+ mediaTranscoder,
200+ currentConfigPayload,
201+ defaultConfigPayload,
202+ )
203+ if err != nil {
204+ purecef.Shutdown()
205+ return nil, err
206+ }
207+ eng.messageRouter = messageRouter
208+ eng.schemeHandler = schemeHandler
209+
210+ registerEngineSchemeFactories(logger, purecef.NewSchemeHandlerFactory(schemeHandler))
211+ return eng, nil
212+}
213+
214+func initializeEngineTranscoder(
215+ transcodingCfg TranscodingRuntimeConfig,
216+ logger *zerolog.Logger,
217+) (port.MediaTranscoder, transcoderStartupState) {
218+ transcoderState := buildTranscoderStartupState(transcodingCfg)
219+ if !transcodingCfg.Enabled {
220+ return nil, transcoderState
221+ }
222+
223+ transcoderState.ProbeAttempted = true
224+ tc := transcoderpkg.New(config.TranscodingConfig{
225+ Enabled: transcodingCfg.Enabled,
226+ HWAccel: transcodingCfg.HWAccel,
227+ MaxConcurrent: transcodingCfg.MaxConcurrent,
228+ Quality: transcodingCfg.Quality,
229+ }, logger)
230+ caps := tc.Capabilities()
231+ transcoderState.API = caps.API
232+ transcoderState.Encoders = append([]string(nil), caps.Encoders...)
233+ transcoderState.Decoders = append([]string(nil), caps.Decoders...)
234+ if !tc.Available() {
235+ transcoderState.Status = "unavailable_no_compatible_gpu"
236+ logger.Warn().Msg("cef: GPU transcoding enabled but no compatible GPU found — feature disabled")
237+ return nil, transcoderState
238+ }
239+
240+ transcoderState.Status = "available"
241+ logger.Info().
242+ Str("api", caps.API).
243+ Strs("encoders", caps.Encoders).
244+ Msg("cef: GPU transcoding available")
245+ return tc, transcoderState
246+}
247+
248+func newEngineSchemeHandler(
249+ ctx context.Context,
250+ eng *Engine,
251+ logger *zerolog.Logger,
252+ mediaTranscoder port.MediaTranscoder,
253+ currentConfigPayload func() ([]byte, error),
254+ defaultConfigPayload func() ([]byte, error),
255+) (*MessageRouter, *dumbSchemeHandler, error) {
227256 messageRouter := NewMessageRouter(ctx)
228257 schemeHandler, err := newDumbSchemeHandler(
229258 ctx,
@@ -233,15 +262,25 @@ func wireEngine(
233262 defaultConfigPayload,
234263 )
235264 if err != nil {
236- purecef.Shutdown()
237- return nil, err
265+ return nil, nil, err
238266 }
239267 schemeHandler.setAssets(assets.WebUIAssets)
268+ schemeHandler.onClipboardSet = makeGTKClipboardSetter(logger)
269+ schemeHandler.onEditableFocus = eng.handleEditableFocusBridge
270+ schemeHandler.onPopupOpen = eng.handlePopupBridgeOpen
271+ schemeHandler.onPopupNavigate = eng.handlePopupBridgeNavigate
272+ schemeHandler.onPopupClose = eng.handlePopupBridgeClose
273+ schemeHandler.onPopupOpenerNavigate = eng.handlePopupOpenerNavigate
274+ schemeHandler.onPopupOpenerPostMessage = eng.handlePopupOpenerPostMessage
275+ schemeHandler.bridgeNonceValidator = eng.validateBridgeRequest
276+ return messageRouter, schemeHandler, nil
277+}
240278
279+func makeGTKClipboardSetter(logger *zerolog.Logger) func(string) {
241280 // Bridge clipboard writes from page JS → GDK system clipboard.
242281 // The callback is invoked on the CEF IO thread, so schedule the actual GDK
243282 // write on the GTK main loop.
244- schemeHandler.onClipboardSet = func(text string) {
283+ return func(text string) {
245284 fn := glib.SourceOnceFunc(func(_ uintptr) {
246285 if display := gdk.DisplayGetDefault(); display != nil {
247286 if cb := display.GetClipboard(); cb != nil {
@@ -256,40 +295,37 @@ func wireEngine(
256295 })
257296 glib.IdleAddOnce(&fn, 0)
258297 }
298+}
259299
260- schemeHandler.onEditableFocus = eng.handleEditableFocusBridge
261- schemeHandler.onPopupOpen = eng.handlePopupBridgeOpen
262- schemeHandler.onPopupNavigate = eng.handlePopupBridgeNavigate
263- schemeHandler.onPopupClose = eng.handlePopupBridgeClose
264- schemeHandler.onPopupOpenerNavigate = eng.handlePopupOpenerNavigate
265- schemeHandler.onPopupOpenerPostMessage = eng.handlePopupOpenerPostMessage
266- schemeHandler.bridgeNonceValidator = eng.validateBridgeRequest
267-
268- schemeFactory := purecef.NewSchemeHandlerFactory(schemeHandler)
300+func registerEngineSchemeFactories(logger *zerolog.Logger, schemeFactory purecef.SchemeHandlerFactory) {
301+ registerEngineSchemeFactory(logger, "dumb", "", schemeFactory)
302+ registerEngineSchemeFactory(logger, actualInternalScheme, actualInternalHost, schemeFactory)
303+}
269304
270- result := purecef.RegisterSchemeHandlerFactory("dumb", "", schemeFactory)
271- if result != 1 {
272- logger.Error().Int32("result", result).Msg("cef: failed to register dumb:// scheme handler factory")
273- } else {
305+func registerEngineSchemeFactory(
306+ logger *zerolog.Logger,
307+ scheme, host string,
308+ schemeFactory purecef.SchemeHandlerFactory,
309+) {
310+ result := purecef.RegisterSchemeHandlerFactory(scheme, host, schemeFactory)
311+ if scheme == "dumb" && host == "" {
312+ if result != 1 {
313+ logger.Error().Int32("result", result).Msg("cef: failed to register dumb:// scheme handler factory")
314+ return
315+ }
274316 logger.Info().Msg("cef: registered dumb:// scheme handler factory")
317+ return
275318 }
276-
277- result = purecef.RegisterSchemeHandlerFactory(actualInternalScheme, actualInternalHost, schemeFactory)
278319 if result != 1 {
279320 logger.Error().
280321 Int32("result", result).
281322 Str("origin", actualInternalOrigin).
282323 Msg("cef: failed to register internal https handler factory")
283- } else {
284- logger.Info().
285- Str("origin", actualInternalOrigin).
286- Msg("cef: registered internal https handler factory")
324+ return
287325 }
288-
289- eng.messageRouter = messageRouter
290- eng.schemeHandler = schemeHandler
291-
292- return eng, nil
326+ logger.Info().
327+ Str("origin", actualInternalOrigin).
328+ Msg("cef: registered internal https handler factory")
293329 }
294330
295331 func buildTranscoderStartupState(cfg TranscodingRuntimeConfig) transcoderStartupState {
@@ -179,51 +179,80 @@ func wireEngine(
179 return nil, fmt.Errorf("GL loader: %w", err)179 return nil, fmt.Errorf("GL loader: %w", err)
180 }180 }
181 181
182- scale := detectHiDPIScale(logger)182+ mediaTranscoder, transcoderState := initializeEngineTranscoder(transcodingCfg, logger)
183-183+ eng.gl = gl
184- // Initialize the GPU transcoder when enabled in the loaded app config.184+ eng.factory = newWebViewFactory(eng, gl, webViewFactoryOptions{
185- var mediaTranscoder port.MediaTranscoder185+ scale: detectHiDPIScale(logger),
186- transcoderState := buildTranscoderStartupState(transcodingCfg)
187- if transcodingCfg.Enabled {
188- transcoderState.ProbeAttempted = true
189- tc := transcoderpkg.New(config.TranscodingConfig{
190- Enabled: transcodingCfg.Enabled,
191- HWAccel: transcodingCfg.HWAccel,
192- MaxConcurrent: transcodingCfg.MaxConcurrent,
193- Quality: transcodingCfg.Quality,
194- }, logger)
195- caps := tc.Capabilities()
196- transcoderState.API = caps.API
197- transcoderState.Encoders = append([]string(nil), caps.Encoders...)
198- transcoderState.Decoders = append([]string(nil), caps.Decoders...)
199- if tc.Available() {
200- mediaTranscoder = tc
201- transcoderState.Status = "available"
202- logger.Info().
203- Str("api", tc.Capabilities().API).
204- Strs("encoders", tc.Capabilities().Encoders).
205- Msg("cef: GPU transcoding available")
206- } else {
207- transcoderState.Status = "unavailable_no_compatible_gpu"
208- logger.Warn().Msg("cef: GPU transcoding enabled but no compatible GPU found — feature disabled")
209- }
210- }
211-
212- factory := newWebViewFactory(eng, gl, webViewFactoryOptions{
213- scale: scale,
214 windowlessFrameRate: windowlessFrameRate,186 windowlessFrameRate: windowlessFrameRate,
215 transcoder: mediaTranscoder,187 transcoder: mediaTranscoder,
216 mediaClassifier: mediaClassifier,188 mediaClassifier: mediaClassifier,
217 audioOutputFactory: audioFactory,189 audioOutputFactory: audioFactory,
218 })190 })
219- pool := newWebViewPool(factory)191+ eng.pool = newWebViewPool(eng.factory)
220-
221- eng.gl = gl
222- eng.factory = factory
223- eng.pool = pool
224 eng.contentInj = newContentInjector(eng, nil)192 eng.contentInj = newContentInjector(eng, nil)
225 eng.transcoderState = transcoderState193 eng.transcoderState = transcoderState
226 194
195+ messageRouter, schemeHandler, err := newEngineSchemeHandler(
196+ ctx,
197+ eng,
198+ logger,
199+ mediaTranscoder,
200+ currentConfigPayload,
201+ defaultConfigPayload,
202+ )
203+ if err != nil {
204+ purecef.Shutdown()
205+ return nil, err
206+ }
207+ eng.messageRouter = messageRouter
208+ eng.schemeHandler = schemeHandler
209+
210+ registerEngineSchemeFactories(logger, purecef.NewSchemeHandlerFactory(schemeHandler))
211+ return eng, nil
212+}
213+
214+func initializeEngineTranscoder(
215+ transcodingCfg TranscodingRuntimeConfig,
216+ logger *zerolog.Logger,
217+) (port.MediaTranscoder, transcoderStartupState) {
218+ transcoderState := buildTranscoderStartupState(transcodingCfg)
219+ if !transcodingCfg.Enabled {
220+ return nil, transcoderState
221+ }
222+
223+ transcoderState.ProbeAttempted = true
224+ tc := transcoderpkg.New(config.TranscodingConfig{
225+ Enabled: transcodingCfg.Enabled,
226+ HWAccel: transcodingCfg.HWAccel,
227+ MaxConcurrent: transcodingCfg.MaxConcurrent,
228+ Quality: transcodingCfg.Quality,
229+ }, logger)
230+ caps := tc.Capabilities()
231+ transcoderState.API = caps.API
232+ transcoderState.Encoders = append([]string(nil), caps.Encoders...)
233+ transcoderState.Decoders = append([]string(nil), caps.Decoders...)
234+ if !tc.Available() {
235+ transcoderState.Status = "unavailable_no_compatible_gpu"
236+ logger.Warn().Msg("cef: GPU transcoding enabled but no compatible GPU found — feature disabled")
237+ return nil, transcoderState
238+ }
239+
240+ transcoderState.Status = "available"
241+ logger.Info().
242+ Str("api", caps.API).
243+ Strs("encoders", caps.Encoders).
244+ Msg("cef: GPU transcoding available")
245+ return tc, transcoderState
246+}
247+
248+func newEngineSchemeHandler(
249+ ctx context.Context,
250+ eng *Engine,
251+ logger *zerolog.Logger,
252+ mediaTranscoder port.MediaTranscoder,
253+ currentConfigPayload func() ([]byte, error),
254+ defaultConfigPayload func() ([]byte, error),
255+) (*MessageRouter, *dumbSchemeHandler, error) {
227 messageRouter := NewMessageRouter(ctx)256 messageRouter := NewMessageRouter(ctx)
228 schemeHandler, err := newDumbSchemeHandler(257 schemeHandler, err := newDumbSchemeHandler(
229 ctx,258 ctx,
@@ -233,15 +262,25 @@ func wireEngine(
233 defaultConfigPayload,262 defaultConfigPayload,
234 )263 )
235 if err != nil {264 if err != nil {
236- purecef.Shutdown()265+ return nil, nil, err
237- return nil, err
238 }266 }
239 schemeHandler.setAssets(assets.WebUIAssets)267 schemeHandler.setAssets(assets.WebUIAssets)
268+ schemeHandler.onClipboardSet = makeGTKClipboardSetter(logger)
269+ schemeHandler.onEditableFocus = eng.handleEditableFocusBridge
270+ schemeHandler.onPopupOpen = eng.handlePopupBridgeOpen
271+ schemeHandler.onPopupNavigate = eng.handlePopupBridgeNavigate
272+ schemeHandler.onPopupClose = eng.handlePopupBridgeClose
273+ schemeHandler.onPopupOpenerNavigate = eng.handlePopupOpenerNavigate
274+ schemeHandler.onPopupOpenerPostMessage = eng.handlePopupOpenerPostMessage
275+ schemeHandler.bridgeNonceValidator = eng.validateBridgeRequest
276+ return messageRouter, schemeHandler, nil
277+}
240 278
279+func makeGTKClipboardSetter(logger *zerolog.Logger) func(string) {
241 // Bridge clipboard writes from page JS → GDK system clipboard.280 // Bridge clipboard writes from page JS → GDK system clipboard.
242 // The callback is invoked on the CEF IO thread, so schedule the actual GDK281 // The callback is invoked on the CEF IO thread, so schedule the actual GDK
243 // write on the GTK main loop.282 // write on the GTK main loop.
244- schemeHandler.onClipboardSet = func(text string) {283+ return func(text string) {
245 fn := glib.SourceOnceFunc(func(_ uintptr) {284 fn := glib.SourceOnceFunc(func(_ uintptr) {
246 if display := gdk.DisplayGetDefault(); display != nil {285 if display := gdk.DisplayGetDefault(); display != nil {
247 if cb := display.GetClipboard(); cb != nil {286 if cb := display.GetClipboard(); cb != nil {
@@ -256,40 +295,37 @@ func wireEngine(
256 })295 })
257 glib.IdleAddOnce(&fn, 0)296 glib.IdleAddOnce(&fn, 0)
258 }297 }
298+}
259 299
260- schemeHandler.onEditableFocus = eng.handleEditableFocusBridge300+func registerEngineSchemeFactories(logger *zerolog.Logger, schemeFactory purecef.SchemeHandlerFactory) {
261- schemeHandler.onPopupOpen = eng.handlePopupBridgeOpen301+ registerEngineSchemeFactory(logger, "dumb", "", schemeFactory)
262- schemeHandler.onPopupNavigate = eng.handlePopupBridgeNavigate302+ registerEngineSchemeFactory(logger, actualInternalScheme, actualInternalHost, schemeFactory)
263- schemeHandler.onPopupClose = eng.handlePopupBridgeClose303+}
264- schemeHandler.onPopupOpenerNavigate = eng.handlePopupOpenerNavigate
265- schemeHandler.onPopupOpenerPostMessage = eng.handlePopupOpenerPostMessage
266- schemeHandler.bridgeNonceValidator = eng.validateBridgeRequest
267-
268- schemeFactory := purecef.NewSchemeHandlerFactory(schemeHandler)
269 304
270- result := purecef.RegisterSchemeHandlerFactory("dumb", "", schemeFactory)305+func registerEngineSchemeFactory(
271- if result != 1 {306+ logger *zerolog.Logger,
272- logger.Error().Int32("result", result).Msg("cef: failed to register dumb:// scheme handler factory")307+ scheme, host string,
273- } else {308+ schemeFactory purecef.SchemeHandlerFactory,
309+) {
310+ result := purecef.RegisterSchemeHandlerFactory(scheme, host, schemeFactory)
311+ if scheme == "dumb" && host == "" {
312+ if result != 1 {
313+ logger.Error().Int32("result", result).Msg("cef: failed to register dumb:// scheme handler factory")
314+ return
315+ }
274 logger.Info().Msg("cef: registered dumb:// scheme handler factory")316 logger.Info().Msg("cef: registered dumb:// scheme handler factory")
317+ return
275 }318 }
276-
277- result = purecef.RegisterSchemeHandlerFactory(actualInternalScheme, actualInternalHost, schemeFactory)
278 if result != 1 {319 if result != 1 {
279 logger.Error().320 logger.Error().
280 Int32("result", result).321 Int32("result", result).
281 Str("origin", actualInternalOrigin).322 Str("origin", actualInternalOrigin).
282 Msg("cef: failed to register internal https handler factory")323 Msg("cef: failed to register internal https handler factory")
283- } else {324+ return
284- logger.Info().
285- Str("origin", actualInternalOrigin).
286- Msg("cef: registered internal https handler factory")
287 }325 }
288-326+ logger.Info().
289- eng.messageRouter = messageRouter327+ Str("origin", actualInternalOrigin).
290- eng.schemeHandler = schemeHandler328+ Msg("cef: registered internal https handler factory")
291-
292- return eng, nil
293 }329 }
294 330
295 func buildTranscoderStartupState(cfg TranscodingRuntimeConfig) transcoderStartupState {331 func buildTranscoderStartupState(cfg TranscodingRuntimeConfig) transcoderStartupState {
M internal/infrastructure/cef/popup_opener_bridge.go +14 -12
@@ -23,6 +23,8 @@ type popupOpenerNavigatePayload struct {
2323 URL string `json:"url"`
2424 }
2525
26+const popupOpenerHTTPScheme = "http"
27+
2628 func (wv *WebView) setPopupNoJavaScriptAccess(noJavaScriptAccess bool) {
2729 if wv == nil {
2830 return
@@ -96,7 +98,7 @@ func (wv *WebView) HasActivePopupOpenerBridge() bool {
9698 return active
9799 }
98100
99-func (wv *WebView) popupOpenerBridgeState() (parentURI string, active bool, blocked bool) {
101+func (wv *WebView) popupOpenerBridgeState() (parentURI string, active, blocked bool) {
100102 if wv == nil {
101103 return "", false, false
102104 }
@@ -301,8 +303,8 @@ func sanitizePopupOpenerNavigationTarget(target *neturl.URL) string {
301303 if target == nil {
302304 return ""
303305 }
304- switch strings.ToLower(strings.TrimSpace(target.Scheme)) {
305- case "http", "https":
306+ switch scheme := strings.ToLower(strings.TrimSpace(target.Scheme)); scheme {
307+ case popupOpenerHTTPScheme, actualInternalScheme:
306308 return target.String()
307309 default:
308310 return ""
@@ -346,7 +348,7 @@ func (wv *WebView) handlePopupOpenerPostMessage(payload popupOpenerPostMessagePa
346348 }
347349
348350 sourceOrigin := popupSourceOrigin(payload)
349- dataExpr := "undefined"
351+ var dataExpr string
350352 switch strings.ToLower(strings.TrimSpace(payload.DataKind)) {
351353 case "json":
352354 dataExpr = fmt.Sprintf("JSON.parse('%s')", webutil.EscapeForJSString(payload.Data))
@@ -430,17 +432,17 @@ func canonicalOrigin(rawURL string) string {
430432 if host == "" {
431433 return ""
432434 }
433- port := parsed.Port()
434- if port == "" {
435+ portStr := parsed.Port()
436+ if portStr == "" {
435437 switch scheme {
436- case "http":
437- port = "80"
438- case "https":
439- port = "443"
438+ case popupOpenerHTTPScheme:
439+ portStr = "80"
440+ case actualInternalScheme:
441+ portStr = "443"
440442 }
441443 }
442- if port == "" {
444+ if portStr == "" {
443445 return scheme + "://" + host
444446 }
445- return scheme + "://" + host + ":" + port
447+ return scheme + "://" + host + ":" + portStr
446448 }
@@ -23,6 +23,8 @@ type popupOpenerNavigatePayload struct {
23 URL string `json:"url"`23 URL string `json:"url"`
24 }24 }
25 25
26+const popupOpenerHTTPScheme = "http"
27+
26 func (wv *WebView) setPopupNoJavaScriptAccess(noJavaScriptAccess bool) {28 func (wv *WebView) setPopupNoJavaScriptAccess(noJavaScriptAccess bool) {
27 if wv == nil {29 if wv == nil {
28 return30 return
@@ -96,7 +98,7 @@ func (wv *WebView) HasActivePopupOpenerBridge() bool {
96 return active98 return active
97 }99 }
98 100
99-func (wv *WebView) popupOpenerBridgeState() (parentURI string, active bool, blocked bool) {101+func (wv *WebView) popupOpenerBridgeState() (parentURI string, active, blocked bool) {
100 if wv == nil {102 if wv == nil {
101 return "", false, false103 return "", false, false
102 }104 }
@@ -301,8 +303,8 @@ func sanitizePopupOpenerNavigationTarget(target *neturl.URL) string {
301 if target == nil {303 if target == nil {
302 return ""304 return ""
303 }305 }
304- switch strings.ToLower(strings.TrimSpace(target.Scheme)) {306+ switch scheme := strings.ToLower(strings.TrimSpace(target.Scheme)); scheme {
305- case "http", "https":307+ case popupOpenerHTTPScheme, actualInternalScheme:
306 return target.String()308 return target.String()
307 default:309 default:
308 return ""310 return ""
@@ -346,7 +348,7 @@ func (wv *WebView) handlePopupOpenerPostMessage(payload popupOpenerPostMessagePa
346 }348 }
347 349
348 sourceOrigin := popupSourceOrigin(payload)350 sourceOrigin := popupSourceOrigin(payload)
349- dataExpr := "undefined"351+ var dataExpr string
350 switch strings.ToLower(strings.TrimSpace(payload.DataKind)) {352 switch strings.ToLower(strings.TrimSpace(payload.DataKind)) {
351 case "json":353 case "json":
352 dataExpr = fmt.Sprintf("JSON.parse('%s')", webutil.EscapeForJSString(payload.Data))354 dataExpr = fmt.Sprintf("JSON.parse('%s')", webutil.EscapeForJSString(payload.Data))
@@ -430,17 +432,17 @@ func canonicalOrigin(rawURL string) string {
430 if host == "" {432 if host == "" {
431 return ""433 return ""
432 }434 }
433- port := parsed.Port()435+ portStr := parsed.Port()
434- if port == "" {436+ if portStr == "" {
435 switch scheme {437 switch scheme {
436- case "http":438+ case popupOpenerHTTPScheme:
437- port = "80"439+ portStr = "80"
438- case "https":440+ case actualInternalScheme:
439- port = "443"441+ portStr = "443"
440 }442 }
441 }443 }
442- if port == "" {444+ if portStr == "" {
443 return scheme + "://" + host445 return scheme + "://" + host
444 }446 }
445- return scheme + "://" + host + ":" + port447+ return scheme + "://" + host + ":" + portStr
446 }448 }
M internal/infrastructure/cef/popup_opener_renderer_test.go +1 -1
@@ -75,7 +75,7 @@ func (c *testPopupOpenerV8Context) Exit() int32 {
7575 return 1
7676 }
7777 func (c *testPopupOpenerV8Context) IsSame(that purecef.V8Context) bool { return c == that }
78-func (c *testPopupOpenerV8Context) Eval(code string, scriptURL string, _ int32, _, _ unsafe.Pointer) int32 {
78+func (c *testPopupOpenerV8Context) Eval(code, scriptURL string, _ int32, _, _ unsafe.Pointer) int32 {
7979 c.evalCode = code
8080 c.evalURL = scriptURL
8181 return 1
@@ -75,7 +75,7 @@ func (c *testPopupOpenerV8Context) Exit() int32 {
75 return 175 return 1
76 }76 }
77 func (c *testPopupOpenerV8Context) IsSame(that purecef.V8Context) bool { return c == that }77 func (c *testPopupOpenerV8Context) IsSame(that purecef.V8Context) bool { return c == that }
78-func (c *testPopupOpenerV8Context) Eval(code string, scriptURL string, _ int32, _, _ unsafe.Pointer) int32 {78+func (c *testPopupOpenerV8Context) Eval(code, scriptURL string, _ int32, _, _ unsafe.Pointer) int32 {
79 c.evalCode = code79 c.evalCode = code
80 c.evalURL = scriptURL80 c.evalURL = scriptURL
81 return 181 return 1
M internal/infrastructure/cef/scheme_handler.go +9 -2
@@ -252,7 +252,7 @@ func (h *dumbSchemeHandler) handleTranscodeAPI(request purecef.Request) purecef.
252252 }
253253
254254 sourceParsed, err := url.Parse(sourceURL)
255- if err != nil || sourceParsed.Host == "" || (sourceParsed.Scheme != "http" && sourceParsed.Scheme != "https") {
255+ if err != nil || sourceParsed.Host == "" || (sourceParsed.Scheme != "http" && sourceParsed.Scheme != actualInternalScheme) {
256256 return h.newAPIJSONResourceHandler(http.StatusBadRequest, map[string]string{"error": "invalid src"})
257257 }
258258
@@ -453,7 +453,14 @@ func (h *dumbSchemeHandler) handlePopupOpenerNavigate(request purecef.Request, b
453453 }
454454
455455 func (h *dumbSchemeHandler) handlePopupOpenerPostMessage(request purecef.Request, browser purecef.Browser) purecef.ResourceHandler {
456- return handlePopupBridgeRequest(h, request, browser, "popup-opener-post-message", decodePopupOpenerPostMessagePayload, h.onPopupOpenerPostMessage)
456+ return handlePopupBridgeRequest(
457+ h,
458+ request,
459+ browser,
460+ "popup-opener-post-message",
461+ decodePopupOpenerPostMessagePayload,
462+ h.onPopupOpenerPostMessage,
463+ )
457464 }
458465
459466 func (h *dumbSchemeHandler) hasTrustedBridgeNonce(request purecef.Request, browser purecef.Browser) bool {
@@ -252,7 +252,7 @@ func (h *dumbSchemeHandler) handleTranscodeAPI(request purecef.Request) purecef.
252 }252 }
253 253
254 sourceParsed, err := url.Parse(sourceURL)254 sourceParsed, err := url.Parse(sourceURL)
255- if err != nil || sourceParsed.Host == "" || (sourceParsed.Scheme != "http" && sourceParsed.Scheme != "https") {255+ if err != nil || sourceParsed.Host == "" || (sourceParsed.Scheme != "http" && sourceParsed.Scheme != actualInternalScheme) {
256 return h.newAPIJSONResourceHandler(http.StatusBadRequest, map[string]string{"error": "invalid src"})256 return h.newAPIJSONResourceHandler(http.StatusBadRequest, map[string]string{"error": "invalid src"})
257 }257 }
258 258
@@ -453,7 +453,14 @@ func (h *dumbSchemeHandler) handlePopupOpenerNavigate(request purecef.Request, b
453 }453 }
454 454
455 func (h *dumbSchemeHandler) handlePopupOpenerPostMessage(request purecef.Request, browser purecef.Browser) purecef.ResourceHandler {455 func (h *dumbSchemeHandler) handlePopupOpenerPostMessage(request purecef.Request, browser purecef.Browser) purecef.ResourceHandler {
456- return handlePopupBridgeRequest(h, request, browser, "popup-opener-post-message", decodePopupOpenerPostMessagePayload, h.onPopupOpenerPostMessage)456+ return handlePopupBridgeRequest(
457+ h,
458+ request,
459+ browser,
460+ "popup-opener-post-message",
461+ decodePopupOpenerPostMessagePayload,
462+ h.onPopupOpenerPostMessage,
463+ )
457 }464 }
458 465
459 func (h *dumbSchemeHandler) hasTrustedBridgeNonce(request purecef.Request, browser purecef.Browser) bool {466 func (h *dumbSchemeHandler) hasTrustedBridgeNonce(request purecef.Request, browser purecef.Browser) bool {
M internal/infrastructure/cef/webview.go +65 -27
@@ -794,25 +794,51 @@ func (wv *WebView) prepareNativePopup(
794794 return false
795795 }
796796
797- wv.mu.RLock()
798- if !wv.nativePopupCandidate || wv.client == nil || wv.nativePopupFallbackStarted || wv.browser != nil {
799- wv.mu.RUnlock()
797+ prep, ok := wv.nativePopupPreparationSnapshot()
798+ if !ok {
800799 return false
801800 }
802- parent := wv.nativePopupParent
803- frameRate := wv.windowlessFrameRate
804- backgroundColor := wv.backgroundColor
805- wv.mu.RUnlock()
806-
807- parentWindowHandle := popupParentWindowHandle(parent)
801+ parentWindowHandle := popupParentWindowHandle(prep.parent)
808802 if parentWindowHandle == 0 {
809803 return false
810804 }
805+ client, ok := wv.activateNativePopup(popupID, targetURL)
806+ if !ok {
807+ return false
808+ }
809+ if prep.parent != nil {
810+ prep.parent.trackPendingNativePopup(popupID, wv)
811+ }
812+
813+ configureNativePopupWindow(windowInfo, settings, parentWindowHandle, prep.frameRate, prep.backgroundColor)
814+ clientSlot.Set(client)
815+ return true
816+}
817+
818+func (wv *WebView) nativePopupPreparationSnapshot() (nativePopupPreparation, bool) {
819+ if wv == nil {
820+ return nativePopupPreparation{}, false
821+ }
822+ wv.mu.RLock()
823+ defer wv.mu.RUnlock()
824+ if !wv.canPrepareNativePopupLocked() {
825+ return nativePopupPreparation{}, false
826+ }
827+ return nativePopupPreparation{
828+ parent: wv.nativePopupParent,
829+ frameRate: wv.windowlessFrameRate,
830+ backgroundColor: wv.backgroundColor,
831+ }, true
832+}
811833
834+func (wv *WebView) activateNativePopup(popupID int32, targetURL string) (purecef.RawClient, bool) {
835+ if wv == nil {
836+ return nil, false
837+ }
812838 wv.mu.Lock()
813- if !wv.nativePopupCandidate || wv.client == nil || wv.nativePopupFallbackStarted || wv.browser != nil {
814- wv.mu.Unlock()
815- return false
839+ defer wv.mu.Unlock()
840+ if !wv.canPrepareNativePopupLocked() {
841+ return nil, false
816842 }
817843 wv.nativePopupCandidate = false
818844 wv.nativePopupID = popupID
@@ -826,28 +852,40 @@ func (wv *WebView) prepareNativePopup(
826852 wv.uri = toConceptualInternalURL(targetURL)
827853 }
828854 wv.isLoading = true
829- client := wv.client
830- wv.mu.Unlock()
855+ return wv.client, true
856+}
831857
832- if parent != nil {
833- parent.trackPendingNativePopup(popupID, wv)
834- }
858+func (wv *WebView) canPrepareNativePopupLocked() bool {
859+ return wv.nativePopupCandidate && wv.client != nil && !wv.nativePopupFallbackStarted && wv.browser == nil
860+}
835861
862+func configureNativePopupWindow(
863+ windowInfo *purecef.WindowInfo,
864+ settings *purecef.BrowserSettings,
865+ parentWindowHandle uintptr,
866+ frameRate int32,
867+ backgroundColor uint32,
868+) {
836869 purecef.SetAsWindowless(windowInfo, purecef.WindowHandle(parentWindowHandle), false)
837870 if externalBeginFrameEnabled() {
838871 windowInfo.ExternalBeginFrameEnabled = 1
839872 }
840- if settings != nil {
841- if frameRate > 0 {
842- settings.WindowlessFrameRate = frameRate
843- }
844- settings.LocalStorage = 1
845- if backgroundColor != 0 {
846- settings.BackgroundColor = backgroundColor
847- }
873+ if settings == nil {
874+ return
848875 }
849- clientSlot.Set(client)
850- return true
876+ if frameRate > 0 {
877+ settings.WindowlessFrameRate = frameRate
878+ }
879+ settings.LocalStorage = 1
880+ if backgroundColor != 0 {
881+ settings.BackgroundColor = backgroundColor
882+ }
883+}
884+
885+type nativePopupPreparation struct {
886+ parent *WebView
887+ frameRate int32
888+ backgroundColor uint32
851889 }
852890
853891 func (wv *WebView) handleNativePopupAborted() {
@@ -794,25 +794,51 @@ func (wv *WebView) prepareNativePopup(
794 return false794 return false
795 }795 }
796 796
797- wv.mu.RLock()797+ prep, ok := wv.nativePopupPreparationSnapshot()
798- if !wv.nativePopupCandidate || wv.client == nil || wv.nativePopupFallbackStarted || wv.browser != nil {798+ if !ok {
799- wv.mu.RUnlock()
800 return false799 return false
801 }800 }
802- parent := wv.nativePopupParent801+ parentWindowHandle := popupParentWindowHandle(prep.parent)
803- frameRate := wv.windowlessFrameRate
804- backgroundColor := wv.backgroundColor
805- wv.mu.RUnlock()
806-
807- parentWindowHandle := popupParentWindowHandle(parent)
808 if parentWindowHandle == 0 {802 if parentWindowHandle == 0 {
809 return false803 return false
810 }804 }
805+ client, ok := wv.activateNativePopup(popupID, targetURL)
806+ if !ok {
807+ return false
808+ }
809+ if prep.parent != nil {
810+ prep.parent.trackPendingNativePopup(popupID, wv)
811+ }
812+
813+ configureNativePopupWindow(windowInfo, settings, parentWindowHandle, prep.frameRate, prep.backgroundColor)
814+ clientSlot.Set(client)
815+ return true
816+}
817+
818+func (wv *WebView) nativePopupPreparationSnapshot() (nativePopupPreparation, bool) {
819+ if wv == nil {
820+ return nativePopupPreparation{}, false
821+ }
822+ wv.mu.RLock()
823+ defer wv.mu.RUnlock()
824+ if !wv.canPrepareNativePopupLocked() {
825+ return nativePopupPreparation{}, false
826+ }
827+ return nativePopupPreparation{
828+ parent: wv.nativePopupParent,
829+ frameRate: wv.windowlessFrameRate,
830+ backgroundColor: wv.backgroundColor,
831+ }, true
832+}
811 833
834+func (wv *WebView) activateNativePopup(popupID int32, targetURL string) (purecef.RawClient, bool) {
835+ if wv == nil {
836+ return nil, false
837+ }
812 wv.mu.Lock()838 wv.mu.Lock()
813- if !wv.nativePopupCandidate || wv.client == nil || wv.nativePopupFallbackStarted || wv.browser != nil {839+ defer wv.mu.Unlock()
814- wv.mu.Unlock()840+ if !wv.canPrepareNativePopupLocked() {
815- return false841+ return nil, false
816 }842 }
817 wv.nativePopupCandidate = false843 wv.nativePopupCandidate = false
818 wv.nativePopupID = popupID844 wv.nativePopupID = popupID
@@ -826,28 +852,40 @@ func (wv *WebView) prepareNativePopup(
826 wv.uri = toConceptualInternalURL(targetURL)852 wv.uri = toConceptualInternalURL(targetURL)
827 }853 }
828 wv.isLoading = true854 wv.isLoading = true
829- client := wv.client855+ return wv.client, true
830- wv.mu.Unlock()856+}
831 857
832- if parent != nil {858+func (wv *WebView) canPrepareNativePopupLocked() bool {
833- parent.trackPendingNativePopup(popupID, wv)859+ return wv.nativePopupCandidate && wv.client != nil && !wv.nativePopupFallbackStarted && wv.browser == nil
834- }860+}
835 861
862+func configureNativePopupWindow(
863+ windowInfo *purecef.WindowInfo,
864+ settings *purecef.BrowserSettings,
865+ parentWindowHandle uintptr,
866+ frameRate int32,
867+ backgroundColor uint32,
868+) {
836 purecef.SetAsWindowless(windowInfo, purecef.WindowHandle(parentWindowHandle), false)869 purecef.SetAsWindowless(windowInfo, purecef.WindowHandle(parentWindowHandle), false)
837 if externalBeginFrameEnabled() {870 if externalBeginFrameEnabled() {
838 windowInfo.ExternalBeginFrameEnabled = 1871 windowInfo.ExternalBeginFrameEnabled = 1
839 }872 }
840- if settings != nil {873+ if settings == nil {
841- if frameRate > 0 {874+ return
842- settings.WindowlessFrameRate = frameRate
843- }
844- settings.LocalStorage = 1
845- if backgroundColor != 0 {
846- settings.BackgroundColor = backgroundColor
847- }
848 }875 }
849- clientSlot.Set(client)876+ if frameRate > 0 {
850- return true877+ settings.WindowlessFrameRate = frameRate
878+ }
879+ settings.LocalStorage = 1
880+ if backgroundColor != 0 {
881+ settings.BackgroundColor = backgroundColor
882+ }
883+}
884+
885+type nativePopupPreparation struct {
886+ parent *WebView
887+ frameRate int32
888+ backgroundColor uint32
851 }889 }
852 890
853 func (wv *WebView) handleNativePopupAborted() {891 func (wv *WebView) handleNativePopupAborted() {
M internal/infrastructure/cef/webview_handlers.go +74 -51
@@ -759,73 +759,88 @@ func (h *handlerSet) OnAfterCreated(browser purecef.Browser) {
759759 return
760760 }
761761 browserID := browser.GetIdentifier()
762- log.Debug().
763- Int32("browser_id", browserID).
764- Msg("cef: OnAfterCreated")
762+ log.Debug().Int32("browser_id", browserID).Msg("cef: OnAfterCreated")
763+
765764 host := browser.GetHost()
766765 if host == nil {
767766 log.Warn().Msg("cef: OnAfterCreated returned nil host")
768767 return
769768 }
770769
771- shouldSyncFocus := false
772- hasPendingNavigation := false
773- nativePopupParent := (*WebView)(nil)
774- nativePopupID := int32(0)
775- duplicateBrowserID := int32(0)
776- closeDuplicate := false
770+ state := h.attachAfterCreatedBrowser(browser, host, browserID)
771+ if state.closeDuplicate {
772+ log.Warn().
773+ Int32("browser_id", browserID).
774+ Int32("existing_browser_id", state.duplicateBrowserID).
775+ Msg("cef: duplicate popup browser attached after shell already had a browser; closing duplicate")
776+ host.CloseBrowser(1)
777+ return
778+ }
779+
780+ h.finishAfterCreated(browser, host, state)
781+}
782+
783+func (h *handlerSet) attachAfterCreatedBrowser(
784+ browser purecef.Browser,
785+ host purecef.BrowserHost,
786+ browserID int32,
787+) afterCreatedState {
788+ state := afterCreatedState{}
777789 h.wv.mu.Lock()
790+ defer h.wv.mu.Unlock()
791+
778792 if existing := h.wv.browser; existing != nil {
779793 existingID := existing.GetIdentifier()
780794 if existingID != 0 && existingID != browserID {
781- duplicateBrowserID = existingID
782- closeDuplicate = true
795+ state.closeDuplicate = true
796+ state.duplicateBrowserID = existingID
797+ return state
783798 }
784799 }
785- if !closeDuplicate {
786- h.wv.browser = browser
787- h.wv.host = host
788- h.wv.pendingCreate = nil
789- h.wv.input.setHost(host)
790- if h.wv.findCtrl != nil {
791- h.wv.findCtrl.setHost(host)
792- }
793- nativePopupParent = h.wv.nativePopupParent
794- nativePopupID = h.wv.nativePopupID
795- h.wv.nativePopupParent = nil
796- h.wv.nativePopupID = 0
797- h.wv.nativePopupFallbackStarted = false
798-
799- // Mark browser as visible — CEF OSR starts in hidden state and suppresses
800- // painting/caret updates until explicitly told the browser is shown.
801- host.WasHidden(0)
802-
803- // If GTK focus entered before the browser existed, reassert browser focus
804- // now. Otherwise CEF may stay internally unfocused and suppress caret paint.
805- if h.wv.input != nil {
806- shouldSyncFocus = h.wv.input.hasGTKFocus()
807- if !shouldSyncFocus && h.wv.input.glArea != nil {
808- shouldSyncFocus = h.wv.input.glArea.HasFocus()
809- }
810- }
811- hasPendingNavigation = strings.TrimSpace(h.wv.pendingURI) != ""
800+
801+ h.wv.browser = browser
802+ h.wv.host = host
803+ h.wv.pendingCreate = nil
804+ h.wv.input.setHost(host)
805+ if h.wv.findCtrl != nil {
806+ h.wv.findCtrl.setHost(host)
807+ }
808+ state.nativePopupParent = h.wv.nativePopupParent
809+ state.nativePopupID = h.wv.nativePopupID
810+ h.wv.nativePopupParent = nil
811+ h.wv.nativePopupID = 0
812+ h.wv.nativePopupFallbackStarted = false
813+
814+ // Mark browser as visible — CEF OSR starts in hidden state and suppresses
815+ // painting/caret updates until explicitly told the browser is shown.
816+ host.WasHidden(0)
817+ state.shouldSyncFocus = h.afterCreatedShouldSyncFocus()
818+ state.hasPendingNavigation = strings.TrimSpace(h.wv.pendingURI) != ""
819+ return state
820+}
821+
822+func (h *handlerSet) afterCreatedShouldSyncFocus() bool {
823+ if h.wv.input == nil {
824+ return false
812825 }
813- h.wv.mu.Unlock()
814- if closeDuplicate {
815- log.Warn().
816- Int32("browser_id", browserID).
817- Int32("existing_browser_id", duplicateBrowserID).
818- Msg("cef: duplicate popup browser attached after shell already had a browser; closing duplicate")
819- host.CloseBrowser(1)
820- return
826+ if h.wv.input.hasGTKFocus() {
827+ return true
821828 }
829+ return h.wv.input.glArea != nil && h.wv.input.glArea.HasFocus()
830+}
831+
832+func (h *handlerSet) finishAfterCreated(
833+ browser purecef.Browser,
834+ host purecef.BrowserHost,
835+ state afterCreatedState,
836+) {
822837 if h.wv.engine != nil {
823838 h.wv.engine.recordBrowserAfterCreated(browser)
824839 h.wv.engine.registerWebView(h.wv)
825840 h.wv.engine.bindBrowserWebView(browser, h.wv)
826841 }
827842 h.wv.stopNativePopupFallbackTimer()
828- if shouldSyncFocus {
843+ if state.shouldSyncFocus {
829844 syncWindowlessBrowserFocus(host)
830845 } else {
831846 // Request an initial OSR frame even before the first real navigation
@@ -833,17 +848,25 @@ func (h *handlerSet) OnAfterCreated(browser purecef.Browser) {
833848 // startup path relied on and prevents the render pipeline from staying idle.
834849 host.Invalidate(purecef.PaintElementTypePetView)
835850 }
836- if hasPendingNavigation {
851+ if state.hasPendingNavigation {
837852 h.wv.schedulePendingNavigationReplay(0)
838853 }
839- if nativePopupParent != nil && nativePopupID != 0 {
840- nativePopupParent.clearPendingNativePopup(nativePopupID, h.wv)
854+ if state.nativePopupParent != nil && state.nativePopupID != 0 {
855+ state.nativePopupParent.clearPendingNativePopup(state.nativePopupID, h.wv)
841856 }
842-
843857 h.wv.scheduleStartBeginFrameLoop()
844858 h.wv.fireReadyToShow()
845859 }
846860
861+type afterCreatedState struct {
862+ shouldSyncFocus bool
863+ hasPendingNavigation bool
864+ nativePopupParent *WebView
865+ nativePopupID int32
866+ duplicateBrowserID int32
867+ closeDuplicate bool
868+}
869+
847870 // DoClose returns false to allow the default close behavior.
848871 func (h *handlerSet) DoClose(_ purecef.Browser) bool {
849872 return false
@@ -759,73 +759,88 @@ func (h *handlerSet) OnAfterCreated(browser purecef.Browser) {
759 return759 return
760 }760 }
761 browserID := browser.GetIdentifier()761 browserID := browser.GetIdentifier()
762- log.Debug().762+ log.Debug().Int32("browser_id", browserID).Msg("cef: OnAfterCreated")
763- Int32("browser_id", browserID).763+
764- Msg("cef: OnAfterCreated")
765 host := browser.GetHost()764 host := browser.GetHost()
766 if host == nil {765 if host == nil {
767 log.Warn().Msg("cef: OnAfterCreated returned nil host")766 log.Warn().Msg("cef: OnAfterCreated returned nil host")
768 return767 return
769 }768 }
770 769
771- shouldSyncFocus := false770+ state := h.attachAfterCreatedBrowser(browser, host, browserID)
772- hasPendingNavigation := false771+ if state.closeDuplicate {
773- nativePopupParent := (*WebView)(nil)772+ log.Warn().
774- nativePopupID := int32(0)773+ Int32("browser_id", browserID).
775- duplicateBrowserID := int32(0)774+ Int32("existing_browser_id", state.duplicateBrowserID).
776- closeDuplicate := false775+ Msg("cef: duplicate popup browser attached after shell already had a browser; closing duplicate")
776+ host.CloseBrowser(1)
777+ return
778+ }
779+
780+ h.finishAfterCreated(browser, host, state)
781+}
782+
783+func (h *handlerSet) attachAfterCreatedBrowser(
784+ browser purecef.Browser,
785+ host purecef.BrowserHost,
786+ browserID int32,
787+) afterCreatedState {
788+ state := afterCreatedState{}
777 h.wv.mu.Lock()789 h.wv.mu.Lock()
790+ defer h.wv.mu.Unlock()
791+
778 if existing := h.wv.browser; existing != nil {792 if existing := h.wv.browser; existing != nil {
779 existingID := existing.GetIdentifier()793 existingID := existing.GetIdentifier()
780 if existingID != 0 && existingID != browserID {794 if existingID != 0 && existingID != browserID {
781- duplicateBrowserID = existingID795+ state.closeDuplicate = true
782- closeDuplicate = true796+ state.duplicateBrowserID = existingID
797+ return state
783 }798 }
784 }799 }
785- if !closeDuplicate {800+
786- h.wv.browser = browser801+ h.wv.browser = browser
787- h.wv.host = host802+ h.wv.host = host
788- h.wv.pendingCreate = nil803+ h.wv.pendingCreate = nil
789- h.wv.input.setHost(host)804+ h.wv.input.setHost(host)
790- if h.wv.findCtrl != nil {805+ if h.wv.findCtrl != nil {
791- h.wv.findCtrl.setHost(host)806+ h.wv.findCtrl.setHost(host)
792- }807+ }
793- nativePopupParent = h.wv.nativePopupParent808+ state.nativePopupParent = h.wv.nativePopupParent
794- nativePopupID = h.wv.nativePopupID809+ state.nativePopupID = h.wv.nativePopupID
795- h.wv.nativePopupParent = nil810+ h.wv.nativePopupParent = nil
796- h.wv.nativePopupID = 0811+ h.wv.nativePopupID = 0
797- h.wv.nativePopupFallbackStarted = false812+ h.wv.nativePopupFallbackStarted = false
798-813+
799- // Mark browser as visible — CEF OSR starts in hidden state and suppresses814+ // Mark browser as visible — CEF OSR starts in hidden state and suppresses
800- // painting/caret updates until explicitly told the browser is shown.815+ // painting/caret updates until explicitly told the browser is shown.
801- host.WasHidden(0)816+ host.WasHidden(0)
802-817+ state.shouldSyncFocus = h.afterCreatedShouldSyncFocus()
803- // If GTK focus entered before the browser existed, reassert browser focus818+ state.hasPendingNavigation = strings.TrimSpace(h.wv.pendingURI) != ""
804- // now. Otherwise CEF may stay internally unfocused and suppress caret paint.819+ return state
805- if h.wv.input != nil {820+}
806- shouldSyncFocus = h.wv.input.hasGTKFocus()821+
807- if !shouldSyncFocus && h.wv.input.glArea != nil {822+func (h *handlerSet) afterCreatedShouldSyncFocus() bool {
808- shouldSyncFocus = h.wv.input.glArea.HasFocus()823+ if h.wv.input == nil {
809- }824+ return false
810- }
811- hasPendingNavigation = strings.TrimSpace(h.wv.pendingURI) != ""
812 }825 }
813- h.wv.mu.Unlock()826+ if h.wv.input.hasGTKFocus() {
814- if closeDuplicate {827+ return true
815- log.Warn().
816- Int32("browser_id", browserID).
817- Int32("existing_browser_id", duplicateBrowserID).
818- Msg("cef: duplicate popup browser attached after shell already had a browser; closing duplicate")
819- host.CloseBrowser(1)
820- return
821 }828 }
829+ return h.wv.input.glArea != nil && h.wv.input.glArea.HasFocus()
830+}
831+
832+func (h *handlerSet) finishAfterCreated(
833+ browser purecef.Browser,
834+ host purecef.BrowserHost,
835+ state afterCreatedState,
836+) {
822 if h.wv.engine != nil {837 if h.wv.engine != nil {
823 h.wv.engine.recordBrowserAfterCreated(browser)838 h.wv.engine.recordBrowserAfterCreated(browser)
824 h.wv.engine.registerWebView(h.wv)839 h.wv.engine.registerWebView(h.wv)
825 h.wv.engine.bindBrowserWebView(browser, h.wv)840 h.wv.engine.bindBrowserWebView(browser, h.wv)
826 }841 }
827 h.wv.stopNativePopupFallbackTimer()842 h.wv.stopNativePopupFallbackTimer()
828- if shouldSyncFocus {843+ if state.shouldSyncFocus {
829 syncWindowlessBrowserFocus(host)844 syncWindowlessBrowserFocus(host)
830 } else {845 } else {
831 // Request an initial OSR frame even before the first real navigation846 // Request an initial OSR frame even before the first real navigation
@@ -833,17 +848,25 @@ func (h *handlerSet) OnAfterCreated(browser purecef.Browser) {
833 // startup path relied on and prevents the render pipeline from staying idle.848 // startup path relied on and prevents the render pipeline from staying idle.
834 host.Invalidate(purecef.PaintElementTypePetView)849 host.Invalidate(purecef.PaintElementTypePetView)
835 }850 }
836- if hasPendingNavigation {851+ if state.hasPendingNavigation {
837 h.wv.schedulePendingNavigationReplay(0)852 h.wv.schedulePendingNavigationReplay(0)
838 }853 }
839- if nativePopupParent != nil && nativePopupID != 0 {854+ if state.nativePopupParent != nil && state.nativePopupID != 0 {
840- nativePopupParent.clearPendingNativePopup(nativePopupID, h.wv)855+ state.nativePopupParent.clearPendingNativePopup(state.nativePopupID, h.wv)
841 }856 }
842-
843 h.wv.scheduleStartBeginFrameLoop()857 h.wv.scheduleStartBeginFrameLoop()
844 h.wv.fireReadyToShow()858 h.wv.fireReadyToShow()
845 }859 }
846 860
861+type afterCreatedState struct {
862+ shouldSyncFocus bool
863+ hasPendingNavigation bool
864+ nativePopupParent *WebView
865+ nativePopupID int32
866+ duplicateBrowserID int32
867+ closeDuplicate bool
868+}
869+
847 // DoClose returns false to allow the default close behavior.870 // DoClose returns false to allow the default close behavior.
848 func (h *handlerSet) DoClose(_ purecef.Browser) bool {871 func (h *handlerSet) DoClose(_ purecef.Browser) bool {
849 return false872 return false
M internal/infrastructure/cef/webview_popup_proxy.go +0 -13
@@ -14,19 +14,6 @@ type syntheticPopupState struct {
1414 Closed bool
1515 }
1616
17-func (wv *WebView) syntheticPopupState(proxyID string) *syntheticPopupState {
18- if wv == nil || proxyID == "" {
19- return nil
20- }
21-
22- wv.syntheticPopupMu.Lock()
23- defer wv.syntheticPopupMu.Unlock()
24- if wv.syntheticPopups == nil {
25- return nil
26- }
27- return wv.syntheticPopups[proxyID]
28-}
29-
3017 // syntheticPopupStateLocked returns the synthetic popup state for proxyID.
3118 // Caller must hold wv.syntheticPopupMu.
3219 func (wv *WebView) syntheticPopupStateLocked(proxyID string) *syntheticPopupState {
@@ -14,19 +14,6 @@ type syntheticPopupState struct {
14 Closed bool14 Closed bool
15 }15 }
16 16
17-func (wv *WebView) syntheticPopupState(proxyID string) *syntheticPopupState {
18- if wv == nil || proxyID == "" {
19- return nil
20- }
21-
22- wv.syntheticPopupMu.Lock()
23- defer wv.syntheticPopupMu.Unlock()
24- if wv.syntheticPopups == nil {
25- return nil
26- }
27- return wv.syntheticPopups[proxyID]
28-}
29-
30 // syntheticPopupStateLocked returns the synthetic popup state for proxyID.17 // syntheticPopupStateLocked returns the synthetic popup state for proxyID.
31 // Caller must hold wv.syntheticPopupMu.18 // Caller must hold wv.syntheticPopupMu.
32 func (wv *WebView) syntheticPopupStateLocked(proxyID string) *syntheticPopupState {19 func (wv *WebView) syntheticPopupStateLocked(proxyID string) *syntheticPopupState {
M internal/ui/coordinator/content/coordinator.go +0 -6
@@ -322,12 +322,6 @@ func (c *Coordinator) deleteWebViewLocked(paneID entity.PaneID) port.WebView {
322322 return wv
323323 }
324324
325-func (c *Coordinator) paneIDByWebViewID(webViewID port.WebViewID) (entity.PaneID, bool) {
326- c.webViewsMu.RLock()
327- defer c.webViewsMu.RUnlock()
328- return c.paneIDByWebViewIDLocked(webViewID)
329-}
330-
331325 func (c *Coordinator) paneIDByWebViewIDLocked(webViewID port.WebViewID) (entity.PaneID, bool) {
332326 paneID, ok := c.webViewPaneIDs[webViewID]
333327 return paneID, ok
@@ -322,12 +322,6 @@ func (c *Coordinator) deleteWebViewLocked(paneID entity.PaneID) port.WebView {
322 return wv322 return wv
323 }323 }
324 324
325-func (c *Coordinator) paneIDByWebViewID(webViewID port.WebViewID) (entity.PaneID, bool) {
326- c.webViewsMu.RLock()
327- defer c.webViewsMu.RUnlock()
328- return c.paneIDByWebViewIDLocked(webViewID)
329-}
330-
331 func (c *Coordinator) paneIDByWebViewIDLocked(webViewID port.WebViewID) (entity.PaneID, bool) {325 func (c *Coordinator) paneIDByWebViewIDLocked(webViewID port.WebViewID) (entity.PaneID, bool) {
332 paneID, ok := c.webViewPaneIDs[webViewID]326 paneID, ok := c.webViewPaneIDs[webViewID]
333 return paneID, ok327 return paneID, ok
M internal/ui/coordinator/content/oauth.go +7 -16
@@ -206,10 +206,7 @@ func composeOnURIChanged(existing, next func(string)) func(string) {
206206 }
207207 }
208208
209-func composeOnLoadChanged(
210- existing func(port.LoadEvent),
211- next func(port.LoadEvent),
212-) func(port.LoadEvent) {
209+func composeOnLoadChanged(existing, next func(port.LoadEvent)) func(port.LoadEvent) {
213210 if existing == nil {
214211 return next
215212 }
@@ -402,8 +399,7 @@ func (c *Coordinator) scheduleParentPaneOAuthResume(
402399 ctx context.Context,
403400 parentPaneID entity.PaneID,
404401 popupID port.WebViewID,
405- parentURIAtOpen string,
406- callbackURI string,
402+ parentURIAtOpen, callbackURI string,
407403 ) {
408404 c.scheduleParentPaneOAuthResumeAttempt(
409405 ctx,
@@ -419,8 +415,7 @@ func (c *Coordinator) scheduleParentPaneOAuthResumeAttempt(
419415 ctx context.Context,
420416 parentPaneID entity.PaneID,
421417 popupID port.WebViewID,
422- parentURIAtOpen string,
423- callbackURI string,
418+ parentURIAtOpen, callbackURI string,
424419 remainingGrace int,
425420 ) {
426421 c.ensurePopupManager().schedulePopupRefresh(parentPaneID, oauthParentRefreshDebounce, func() {
@@ -436,8 +431,7 @@ func (c *Coordinator) resumeParentPaneAfterOAuth(
436431 ctx context.Context,
437432 parentPaneID entity.PaneID,
438433 popupID port.WebViewID,
439- parentURIAtOpen string,
440- callbackURI string,
434+ parentURIAtOpen, callbackURI string,
441435 ) {
442436 c.resumeParentPaneAfterOAuthAttempt(
443437 ctx,
@@ -453,8 +447,7 @@ func (c *Coordinator) resumeParentPaneAfterOAuthAttempt(
453447 ctx context.Context,
454448 parentPaneID entity.PaneID,
455449 popupID port.WebViewID,
456- parentURIAtOpen string,
457- callbackURI string,
450+ parentURIAtOpen, callbackURI string,
458451 remainingGrace int,
459452 ) {
460453 log := logging.FromContext(ctx)
@@ -515,9 +508,7 @@ func (c *Coordinator) resumeParentPaneAfterOAuthAttempt(
515508 }
516509
517510 func shouldGraceWaitForParentPaneOAuthResume(
518- parentURIAtOpen string,
519- currentParentURI string,
520- callbackURI string,
511+ parentURIAtOpen, currentParentURI, callbackURI string,
521512 remainingGrace int,
522513 ) bool {
523514 if remainingGrace <= 0 {
@@ -532,7 +523,7 @@ func shouldGraceWaitForParentPaneOAuthResume(
532523 return sameOAuthResumeSite(parentURIAtOpen, callbackURI)
533524 }
534525
535-func sameOAuthResumeSite(a string, b string) bool {
526+func sameOAuthResumeSite(a, b string) bool {
536527 hostA := normalizeOAuthResumeHost(a)
537528 hostB := normalizeOAuthResumeHost(b)
538529 return hostA != "" && hostA == hostB
@@ -206,10 +206,7 @@ func composeOnURIChanged(existing, next func(string)) func(string) {
206 }206 }
207 }207 }
208 208
209-func composeOnLoadChanged(209+func composeOnLoadChanged(existing, next func(port.LoadEvent)) func(port.LoadEvent) {
210- existing func(port.LoadEvent),
211- next func(port.LoadEvent),
212-) func(port.LoadEvent) {
213 if existing == nil {210 if existing == nil {
214 return next211 return next
215 }212 }
@@ -402,8 +399,7 @@ func (c *Coordinator) scheduleParentPaneOAuthResume(
402 ctx context.Context,399 ctx context.Context,
403 parentPaneID entity.PaneID,400 parentPaneID entity.PaneID,
404 popupID port.WebViewID,401 popupID port.WebViewID,
405- parentURIAtOpen string,402+ parentURIAtOpen, callbackURI string,
406- callbackURI string,
407 ) {403 ) {
408 c.scheduleParentPaneOAuthResumeAttempt(404 c.scheduleParentPaneOAuthResumeAttempt(
409 ctx,405 ctx,
@@ -419,8 +415,7 @@ func (c *Coordinator) scheduleParentPaneOAuthResumeAttempt(
419 ctx context.Context,415 ctx context.Context,
420 parentPaneID entity.PaneID,416 parentPaneID entity.PaneID,
421 popupID port.WebViewID,417 popupID port.WebViewID,
422- parentURIAtOpen string,418+ parentURIAtOpen, callbackURI string,
423- callbackURI string,
424 remainingGrace int,419 remainingGrace int,
425 ) {420 ) {
426 c.ensurePopupManager().schedulePopupRefresh(parentPaneID, oauthParentRefreshDebounce, func() {421 c.ensurePopupManager().schedulePopupRefresh(parentPaneID, oauthParentRefreshDebounce, func() {
@@ -436,8 +431,7 @@ func (c *Coordinator) resumeParentPaneAfterOAuth(
436 ctx context.Context,431 ctx context.Context,
437 parentPaneID entity.PaneID,432 parentPaneID entity.PaneID,
438 popupID port.WebViewID,433 popupID port.WebViewID,
439- parentURIAtOpen string,434+ parentURIAtOpen, callbackURI string,
440- callbackURI string,
441 ) {435 ) {
442 c.resumeParentPaneAfterOAuthAttempt(436 c.resumeParentPaneAfterOAuthAttempt(
443 ctx,437 ctx,
@@ -453,8 +447,7 @@ func (c *Coordinator) resumeParentPaneAfterOAuthAttempt(
453 ctx context.Context,447 ctx context.Context,
454 parentPaneID entity.PaneID,448 parentPaneID entity.PaneID,
455 popupID port.WebViewID,449 popupID port.WebViewID,
456- parentURIAtOpen string,450+ parentURIAtOpen, callbackURI string,
457- callbackURI string,
458 remainingGrace int,451 remainingGrace int,
459 ) {452 ) {
460 log := logging.FromContext(ctx)453 log := logging.FromContext(ctx)
@@ -515,9 +508,7 @@ func (c *Coordinator) resumeParentPaneAfterOAuthAttempt(
515 }508 }
516 509
517 func shouldGraceWaitForParentPaneOAuthResume(510 func shouldGraceWaitForParentPaneOAuthResume(
518- parentURIAtOpen string,511+ parentURIAtOpen, currentParentURI, callbackURI string,
519- currentParentURI string,
520- callbackURI string,
521 remainingGrace int,512 remainingGrace int,
522 ) bool {513 ) bool {
523 if remainingGrace <= 0 {514 if remainingGrace <= 0 {
@@ -532,7 +523,7 @@ func shouldGraceWaitForParentPaneOAuthResume(
532 return sameOAuthResumeSite(parentURIAtOpen, callbackURI)523 return sameOAuthResumeSite(parentURIAtOpen, callbackURI)
533 }524 }
534 525
535-func sameOAuthResumeSite(a string, b string) bool {526+func sameOAuthResumeSite(a, b string) bool {
536 hostA := normalizeOAuthResumeHost(a)527 hostA := normalizeOAuthResumeHost(a)
537 hostB := normalizeOAuthResumeHost(b)528 hostB := normalizeOAuthResumeHost(b)
538 return hostA != "" && hostA == hostB529 return hostA != "" && hostA == hostB
M internal/ui/coordinator/content/popup.go +0 -79
@@ -91,26 +91,6 @@ func isReusableNamedPopupFrame(frameName string) bool {
9191 return frameName != "" && frameName != "_blank"
9292 }
9393
94-func (c *Coordinator) lookupReusableNamedPopup(parentPaneID entity.PaneID, frameName string) (*namedPopupState, bool) {
95- return c.ensurePopupManager().lookupReusableNamedPopup(parentPaneID, frameName)
96-}
97-
98-func (c *Coordinator) storeReusableNamedPopup(
99- parentPaneID entity.PaneID,
100- frameName string,
101- wv port.WebView,
102-) {
103- c.ensurePopupManager().storeReusableNamedPopup(parentPaneID, frameName, wv)
104-}
105-
106-func (c *Coordinator) updatePendingPopupTarget(popupID port.WebViewID, targetURI string) {
107- c.ensurePopupManager().updatePendingPopupTarget(popupID, targetURI)
108-}
109-
110-func (c *Coordinator) clearReusableNamedPopupByWebViewID(popupID port.WebViewID) {
111- c.ensurePopupManager().clearReusableNamedPopupByWebViewID(popupID)
112-}
113-
11494 // InsertPopupInput contains the data needed to insert a popup into the workspace.
11595 type InsertPopupInput struct {
11696 // ParentPaneID is the pane that spawned this popup.
@@ -199,40 +179,6 @@ func (c *Coordinator) buildPopupCreateHandler(
199179 }
200180 }
201181
202-// createPopupPane creates a new pane entity for a popup window.
203-func (c *Coordinator) createPopupPane(
204- popupID port.WebViewID,
205- parentPaneID entity.PaneID,
206- targetURI string,
207-) (entity.PaneID, *entity.Pane) {
208- return c.ensurePopupManager().createPopupPane(popupID, parentPaneID, targetURI)
209-}
210-
211-func (c *Coordinator) relatedPopupSupportDisabled() bool {
212- return c.ensurePopupManager().relatedPopupSupportDisabled()
213-}
214-
215-func (c *Coordinator) markRelatedPopupUnsupported() {
216- c.ensurePopupManager().markRelatedPopupUnsupported()
217-}
218-
219-func (c *Coordinator) markRelatedPopupSupported() {
220- c.ensurePopupManager().markRelatedPopupSupported()
221-}
222-
223-// createPopupWebView prefers a related WebView so popups can share the
224-// parent session/context. If the engine does not support related popup views,
225-// it gracefully falls back to a regular WebView so target="_blank" and
226-// window.open() still open in a workspace pane.
227-func (c *Coordinator) createPopupWebView(
228- ctx context.Context,
229- parentID port.WebViewID,
230- targetURI string,
231- noJavaScriptAccess bool,
232-) (port.WebView, bool, error) {
233- return c.ensurePopupManager().createPopupWebView(ctx, parentID, targetURI, noJavaScriptAccess)
234-}
235-
236182 // handlePopupCreate handles a popup request from the current WebView.
237183 // Returns a WebView if popup handling is allowed, nil to block.
238184 //
@@ -248,31 +194,6 @@ func (c *Coordinator) handlePopupCreate(
248194 return c.ensurePopupManager().handlePopupCreate(ctx, c.popupHooks(), parentPaneID, parentWV, req)
249195 }
250196
251-func (c *Coordinator) reuseNamedPopup(
252- ctx context.Context,
253- parentPaneID entity.PaneID,
254- frameName string,
255- targetURI string,
256-) (port.WebView, bool) {
257- return c.ensurePopupManager().reuseNamedPopup(ctx, parentPaneID, frameName, targetURI)
258-}
259-
260-func (c *Coordinator) finishPopupCreate(ctx context.Context, create popupCreateContext) port.WebView {
261- return c.ensurePopupManager().finishPopupCreate(ctx, c.popupHooks(), create)
262-}
263-
264-// handlePopupReadyToShow handles the WebKit "ready-to-show" signal.
265-// The popup WebView was already inserted into the workspace (hidden) during
266-// the create signal. This handler just makes it visible.
267-func (c *Coordinator) handlePopupReadyToShow(ctx context.Context, popupID port.WebViewID) {
268- c.ensurePopupManager().handlePopupReadyToShow(ctx, popupID)
269-}
270-
271-// handlePopupClose handles the WebKit "close" signal for popup windows.
272-func (c *Coordinator) handlePopupClose(ctx context.Context, popupID port.WebViewID) {
273- c.ensurePopupManager().handlePopupClose(ctx, c.popupHooks(), popupID)
274-}
275-
276197 // handleLinkMiddleClick handles middle-click / Ctrl+click on links.
277198 // Opens the link in a new pane using blank_target_behavior config.
278199 func (c *Coordinator) handleLinkMiddleClick(ctx context.Context, parentPaneID entity.PaneID, uri string) bool {
@@ -91,26 +91,6 @@ func isReusableNamedPopupFrame(frameName string) bool {
91 return frameName != "" && frameName != "_blank"91 return frameName != "" && frameName != "_blank"
92 }92 }
93 93
94-func (c *Coordinator) lookupReusableNamedPopup(parentPaneID entity.PaneID, frameName string) (*namedPopupState, bool) {
95- return c.ensurePopupManager().lookupReusableNamedPopup(parentPaneID, frameName)
96-}
97-
98-func (c *Coordinator) storeReusableNamedPopup(
99- parentPaneID entity.PaneID,
100- frameName string,
101- wv port.WebView,
102-) {
103- c.ensurePopupManager().storeReusableNamedPopup(parentPaneID, frameName, wv)
104-}
105-
106-func (c *Coordinator) updatePendingPopupTarget(popupID port.WebViewID, targetURI string) {
107- c.ensurePopupManager().updatePendingPopupTarget(popupID, targetURI)
108-}
109-
110-func (c *Coordinator) clearReusableNamedPopupByWebViewID(popupID port.WebViewID) {
111- c.ensurePopupManager().clearReusableNamedPopupByWebViewID(popupID)
112-}
113-
114 // InsertPopupInput contains the data needed to insert a popup into the workspace.94 // InsertPopupInput contains the data needed to insert a popup into the workspace.
115 type InsertPopupInput struct {95 type InsertPopupInput struct {
116 // ParentPaneID is the pane that spawned this popup.96 // ParentPaneID is the pane that spawned this popup.
@@ -199,40 +179,6 @@ func (c *Coordinator) buildPopupCreateHandler(
199 }179 }
200 }180 }
201 181
202-// createPopupPane creates a new pane entity for a popup window.
203-func (c *Coordinator) createPopupPane(
204- popupID port.WebViewID,
205- parentPaneID entity.PaneID,
206- targetURI string,
207-) (entity.PaneID, *entity.Pane) {
208- return c.ensurePopupManager().createPopupPane(popupID, parentPaneID, targetURI)
209-}
210-
211-func (c *Coordinator) relatedPopupSupportDisabled() bool {
212- return c.ensurePopupManager().relatedPopupSupportDisabled()
213-}
214-
215-func (c *Coordinator) markRelatedPopupUnsupported() {
216- c.ensurePopupManager().markRelatedPopupUnsupported()
217-}
218-
219-func (c *Coordinator) markRelatedPopupSupported() {
220- c.ensurePopupManager().markRelatedPopupSupported()
221-}
222-
223-// createPopupWebView prefers a related WebView so popups can share the
224-// parent session/context. If the engine does not support related popup views,
225-// it gracefully falls back to a regular WebView so target="_blank" and
226-// window.open() still open in a workspace pane.
227-func (c *Coordinator) createPopupWebView(
228- ctx context.Context,
229- parentID port.WebViewID,
230- targetURI string,
231- noJavaScriptAccess bool,
232-) (port.WebView, bool, error) {
233- return c.ensurePopupManager().createPopupWebView(ctx, parentID, targetURI, noJavaScriptAccess)
234-}
235-
236 // handlePopupCreate handles a popup request from the current WebView.182 // handlePopupCreate handles a popup request from the current WebView.
237 // Returns a WebView if popup handling is allowed, nil to block.183 // Returns a WebView if popup handling is allowed, nil to block.
238 //184 //
@@ -248,31 +194,6 @@ func (c *Coordinator) handlePopupCreate(
248 return c.ensurePopupManager().handlePopupCreate(ctx, c.popupHooks(), parentPaneID, parentWV, req)194 return c.ensurePopupManager().handlePopupCreate(ctx, c.popupHooks(), parentPaneID, parentWV, req)
249 }195 }
250 196
251-func (c *Coordinator) reuseNamedPopup(
252- ctx context.Context,
253- parentPaneID entity.PaneID,
254- frameName string,
255- targetURI string,
256-) (port.WebView, bool) {
257- return c.ensurePopupManager().reuseNamedPopup(ctx, parentPaneID, frameName, targetURI)
258-}
259-
260-func (c *Coordinator) finishPopupCreate(ctx context.Context, create popupCreateContext) port.WebView {
261- return c.ensurePopupManager().finishPopupCreate(ctx, c.popupHooks(), create)
262-}
263-
264-// handlePopupReadyToShow handles the WebKit "ready-to-show" signal.
265-// The popup WebView was already inserted into the workspace (hidden) during
266-// the create signal. This handler just makes it visible.
267-func (c *Coordinator) handlePopupReadyToShow(ctx context.Context, popupID port.WebViewID) {
268- c.ensurePopupManager().handlePopupReadyToShow(ctx, popupID)
269-}
270-
271-// handlePopupClose handles the WebKit "close" signal for popup windows.
272-func (c *Coordinator) handlePopupClose(ctx context.Context, popupID port.WebViewID) {
273- c.ensurePopupManager().handlePopupClose(ctx, c.popupHooks(), popupID)
274-}
275-
276 // handleLinkMiddleClick handles middle-click / Ctrl+click on links.197 // handleLinkMiddleClick handles middle-click / Ctrl+click on links.
277 // Opens the link in a new pane using blank_target_behavior config.198 // Opens the link in a new pane using blank_target_behavior config.
278 func (c *Coordinator) handleLinkMiddleClick(ctx context.Context, parentPaneID entity.PaneID, uri string) bool {199 func (c *Coordinator) handleLinkMiddleClick(ctx context.Context, parentPaneID entity.PaneID, uri string) bool {
M internal/ui/coordinator/content/popup_manager.go +47 -38
@@ -665,69 +665,78 @@ func (pm *popupManager) handlePopupReadyToShow(ctx context.Context, popupID port
665665 Msg("popup now visible")
666666 }
667667
668-func (pm *popupManager) handlePopupClose(ctx context.Context, hooks popupCoordinatorHooks, popupID port.WebViewID) {
669- log := logging.FromContext(ctx)
670- fields := log.Debug().Uint64("popup_id", uint64(popupID))
668+func (pm *popupManager) logPopupCloseSignal(ctx context.Context, hooks popupCoordinatorHooks, popupID port.WebViewID) {
669+ fields := logging.FromContext(ctx).Debug().Uint64("popup_id", uint64(popupID))
671670 if hooks.findPaneByWebViewID != nil && hooks.getWebView != nil {
672671 if paneID, ok := hooks.findPaneByWebViewID(popupID); ok && paneID != "" {
673672 fields = fields.Str("pane_id", string(paneID))
674673 if wv := hooks.getWebView(paneID); wv != nil {
675674 fields = fields.
676675 Str("current_uri", logging.TruncateURL(wv.URI(), logURLMaxLen)).
677- Bool("is_loading", wv.IsLoading())
678- fields = fields.Bool("synthetic_opener_active", popupUsesSyntheticOpenerSignals(wv))
676+ Bool("is_loading", wv.IsLoading()).
677+ Bool("synthetic_opener_active", popupUsesSyntheticOpenerSignals(wv))
679678 }
680679 }
681680 }
682681 fields.Msg("popup close signal received")
682+}
683683
684- pending, wasPending := pm.takePendingPopup(popupID)
685- if wasPending && pending != nil {
686- if hooks.handlePopupOAuthClose != nil {
687- hooks.handlePopupOAuthClose(ctx, popupID)
688- }
689- if pm.onClosePane != nil {
690- if err := pm.onClosePane(ctx, pending.PaneID); err != nil {
691- log.Warn().Err(err).Str("pane_id", string(pending.PaneID)).Msg("failed to close pending popup pane")
692- }
693- }
694- pm.clearReusableNamedPopupByWebViewID(popupID)
695- if hooks.getWebView != nil && hooks.releaseWebView != nil && hooks.getWebView(pending.PaneID) != nil {
696- hooks.releaseWebView(ctx, pending.PaneID)
697- }
698- log.Debug().Str("pane_id", string(pending.PaneID)).Msg("cleaned up pending popup that was never shown")
699- return
684+func (pm *popupManager) findPopupPaneForClose(hooks popupCoordinatorHooks, popupID port.WebViewID) (entity.PaneID, bool) {
685+ if hooks.findPaneByWebViewID == nil {
686+ return "", false
700687 }
688+ paneID, ok := hooks.findPaneByWebViewID(popupID)
689+ return paneID, ok && paneID != ""
690+}
701691
702- var (
703- paneID entity.PaneID
704- ok bool
705- )
706- if hooks.findPaneByWebViewID != nil {
707- paneID, ok = hooks.findPaneByWebViewID(popupID)
708- }
709- if !ok || paneID == "" {
710- if hooks.handlePopupOAuthClose != nil {
711- hooks.handlePopupOAuthClose(ctx, popupID)
712- }
713- pm.clearReusableNamedPopupByWebViewID(popupID)
714- log.Warn().Msg("popup close: could not find pane for webview")
692+func (pm *popupManager) releasePopupWebView(ctx context.Context, hooks popupCoordinatorHooks, paneID entity.PaneID) {
693+ if paneID == "" || hooks.getWebView == nil || hooks.releaseWebView == nil {
715694 return
716695 }
696+ if hooks.getWebView(paneID) != nil {
697+ hooks.releaseWebView(ctx, paneID)
698+ }
699+}
717700
701+func (pm *popupManager) cleanupPopupClose(
702+ ctx context.Context,
703+ hooks popupCoordinatorHooks,
704+ popupID port.WebViewID,
705+ paneID entity.PaneID,
706+ closeErrMsg string,
707+) {
708+ log := logging.FromContext(ctx)
718709 if hooks.handlePopupOAuthClose != nil {
719710 hooks.handlePopupOAuthClose(ctx, popupID)
720711 }
721- if pm.onClosePane != nil {
712+ if paneID != "" && pm.onClosePane != nil {
722713 if err := pm.onClosePane(ctx, paneID); err != nil {
723- log.Warn().Err(err).Str("pane_id", string(paneID)).Msg("failed to close popup pane")
714+ log.Warn().Err(err).Str("pane_id", string(paneID)).Msg(closeErrMsg)
724715 }
725716 }
726717 pm.clearReusableNamedPopupByWebViewID(popupID)
727- if hooks.getWebView != nil && hooks.releaseWebView != nil && hooks.getWebView(paneID) != nil {
728- hooks.releaseWebView(ctx, paneID)
718+ pm.releasePopupWebView(ctx, hooks, paneID)
719+}
720+
721+func (pm *popupManager) handlePopupClose(ctx context.Context, hooks popupCoordinatorHooks, popupID port.WebViewID) {
722+ log := logging.FromContext(ctx)
723+ pm.logPopupCloseSignal(ctx, hooks, popupID)
724+
725+ pending, wasPending := pm.takePendingPopup(popupID)
726+ if wasPending && pending != nil {
727+ pm.cleanupPopupClose(ctx, hooks, popupID, pending.PaneID, "failed to close pending popup pane")
728+ log.Debug().Str("pane_id", string(pending.PaneID)).Msg("cleaned up pending popup that was never shown")
729+ return
730+ }
731+
732+ paneID, ok := pm.findPopupPaneForClose(hooks, popupID)
733+ if !ok {
734+ pm.cleanupPopupClose(ctx, hooks, popupID, "", "")
735+ log.Warn().Msg("popup close: could not find pane for webview")
736+ return
729737 }
730738
739+ pm.cleanupPopupClose(ctx, hooks, popupID, paneID, "failed to close popup pane")
731740 log.Info().Str("pane_id", string(paneID)).Msg("popup closed")
732741 }
733742
@@ -665,69 +665,78 @@ func (pm *popupManager) handlePopupReadyToShow(ctx context.Context, popupID port
665 Msg("popup now visible")665 Msg("popup now visible")
666 }666 }
667 667
668-func (pm *popupManager) handlePopupClose(ctx context.Context, hooks popupCoordinatorHooks, popupID port.WebViewID) {668+func (pm *popupManager) logPopupCloseSignal(ctx context.Context, hooks popupCoordinatorHooks, popupID port.WebViewID) {
669- log := logging.FromContext(ctx)669+ fields := logging.FromContext(ctx).Debug().Uint64("popup_id", uint64(popupID))
670- fields := log.Debug().Uint64("popup_id", uint64(popupID))
671 if hooks.findPaneByWebViewID != nil && hooks.getWebView != nil {670 if hooks.findPaneByWebViewID != nil && hooks.getWebView != nil {
672 if paneID, ok := hooks.findPaneByWebViewID(popupID); ok && paneID != "" {671 if paneID, ok := hooks.findPaneByWebViewID(popupID); ok && paneID != "" {
673 fields = fields.Str("pane_id", string(paneID))672 fields = fields.Str("pane_id", string(paneID))
674 if wv := hooks.getWebView(paneID); wv != nil {673 if wv := hooks.getWebView(paneID); wv != nil {
675 fields = fields.674 fields = fields.
676 Str("current_uri", logging.TruncateURL(wv.URI(), logURLMaxLen)).675 Str("current_uri", logging.TruncateURL(wv.URI(), logURLMaxLen)).
677- Bool("is_loading", wv.IsLoading())676+ Bool("is_loading", wv.IsLoading()).
678- fields = fields.Bool("synthetic_opener_active", popupUsesSyntheticOpenerSignals(wv))677+ Bool("synthetic_opener_active", popupUsesSyntheticOpenerSignals(wv))
679 }678 }
680 }679 }
681 }680 }
682 fields.Msg("popup close signal received")681 fields.Msg("popup close signal received")
682+}
683 683
684- pending, wasPending := pm.takePendingPopup(popupID)684+func (pm *popupManager) findPopupPaneForClose(hooks popupCoordinatorHooks, popupID port.WebViewID) (entity.PaneID, bool) {
685- if wasPending && pending != nil {685+ if hooks.findPaneByWebViewID == nil {
686- if hooks.handlePopupOAuthClose != nil {686+ return "", false
687- hooks.handlePopupOAuthClose(ctx, popupID)
688- }
689- if pm.onClosePane != nil {
690- if err := pm.onClosePane(ctx, pending.PaneID); err != nil {
691- log.Warn().Err(err).Str("pane_id", string(pending.PaneID)).Msg("failed to close pending popup pane")
692- }
693- }
694- pm.clearReusableNamedPopupByWebViewID(popupID)
695- if hooks.getWebView != nil && hooks.releaseWebView != nil && hooks.getWebView(pending.PaneID) != nil {
696- hooks.releaseWebView(ctx, pending.PaneID)
697- }
698- log.Debug().Str("pane_id", string(pending.PaneID)).Msg("cleaned up pending popup that was never shown")
699- return
700 }687 }
688+ paneID, ok := hooks.findPaneByWebViewID(popupID)
689+ return paneID, ok && paneID != ""
690+}
701 691
702- var (692+func (pm *popupManager) releasePopupWebView(ctx context.Context, hooks popupCoordinatorHooks, paneID entity.PaneID) {
703- paneID entity.PaneID693+ if paneID == "" || hooks.getWebView == nil || hooks.releaseWebView == nil {
704- ok bool
705- )
706- if hooks.findPaneByWebViewID != nil {
707- paneID, ok = hooks.findPaneByWebViewID(popupID)
708- }
709- if !ok || paneID == "" {
710- if hooks.handlePopupOAuthClose != nil {
711- hooks.handlePopupOAuthClose(ctx, popupID)
712- }
713- pm.clearReusableNamedPopupByWebViewID(popupID)
714- log.Warn().Msg("popup close: could not find pane for webview")
715 return694 return
716 }695 }
696+ if hooks.getWebView(paneID) != nil {
697+ hooks.releaseWebView(ctx, paneID)
698+ }
699+}
717 700
701+func (pm *popupManager) cleanupPopupClose(
702+ ctx context.Context,
703+ hooks popupCoordinatorHooks,
704+ popupID port.WebViewID,
705+ paneID entity.PaneID,
706+ closeErrMsg string,
707+) {
708+ log := logging.FromContext(ctx)
718 if hooks.handlePopupOAuthClose != nil {709 if hooks.handlePopupOAuthClose != nil {
719 hooks.handlePopupOAuthClose(ctx, popupID)710 hooks.handlePopupOAuthClose(ctx, popupID)
720 }711 }
721- if pm.onClosePane != nil {712+ if paneID != "" && pm.onClosePane != nil {
722 if err := pm.onClosePane(ctx, paneID); err != nil {713 if err := pm.onClosePane(ctx, paneID); err != nil {
723- log.Warn().Err(err).Str("pane_id", string(paneID)).Msg("failed to close popup pane")714+ log.Warn().Err(err).Str("pane_id", string(paneID)).Msg(closeErrMsg)
724 }715 }
725 }716 }
726 pm.clearReusableNamedPopupByWebViewID(popupID)717 pm.clearReusableNamedPopupByWebViewID(popupID)
727- if hooks.getWebView != nil && hooks.releaseWebView != nil && hooks.getWebView(paneID) != nil {718+ pm.releasePopupWebView(ctx, hooks, paneID)
728- hooks.releaseWebView(ctx, paneID)719+}
720+
721+func (pm *popupManager) handlePopupClose(ctx context.Context, hooks popupCoordinatorHooks, popupID port.WebViewID) {
722+ log := logging.FromContext(ctx)
723+ pm.logPopupCloseSignal(ctx, hooks, popupID)
724+
725+ pending, wasPending := pm.takePendingPopup(popupID)
726+ if wasPending && pending != nil {
727+ pm.cleanupPopupClose(ctx, hooks, popupID, pending.PaneID, "failed to close pending popup pane")
728+ log.Debug().Str("pane_id", string(pending.PaneID)).Msg("cleaned up pending popup that was never shown")
729+ return
730+ }
731+
732+ paneID, ok := pm.findPopupPaneForClose(hooks, popupID)
733+ if !ok {
734+ pm.cleanupPopupClose(ctx, hooks, popupID, "", "")
735+ log.Warn().Msg("popup close: could not find pane for webview")
736+ return
729 }737 }
730 738
739+ pm.cleanupPopupClose(ctx, hooks, popupID, paneID, "failed to close popup pane")
731 log.Info().Str("pane_id", string(paneID)).Msg("popup closed")740 log.Info().Str("pane_id", string(paneID)).Msg("popup closed")
732 }741 }
733 742

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