detached 74e124a

fix: forward cef relaunches to fresh browser windows

commit
74e124a1e2d22602f0cd1a89532f677bfe0b69c2
author
brice · · 2026-04-14 16:17 UTC
parent
1147e01
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +112 -8
M cmd/dumber/main.go +8 -0
@@ -18,6 +18,7 @@ import (
1818 "github.com/bnema/dumber/internal/domain/entity"
1919 "github.com/bnema/dumber/internal/domain/repository"
2020 "github.com/bnema/dumber/internal/infrastructure/cache"
21+ cefinfra "github.com/bnema/dumber/internal/infrastructure/cef"
2122 "github.com/bnema/dumber/internal/infrastructure/clipboard"
2223 "github.com/bnema/dumber/internal/infrastructure/config"
2324 "github.com/bnema/dumber/internal/infrastructure/deps"
@@ -205,6 +206,13 @@ func runGUI() int {
205206 log.Error().Err(err).Msg("failed to create application")
206207 return 1
207208 }
209+ if cefEngine, ok := engine.(*cefinfra.Engine); ok {
210+ cefEngine.SetAlreadyRunningAppRelaunchHandler(func(url string) {
211+ if err := app.OpenFreshWindow(ctx, url); err != nil {
212+ log.Warn().Err(err).Str("url", url).Msg("failed to open relaunch browser window")
213+ }
214+ })
215+ }
208216 timer.Mark("ui_deps")
209217 timer.Log(ctx)
210218
@@ -18,6 +18,7 @@ import (
18 "github.com/bnema/dumber/internal/domain/entity"18 "github.com/bnema/dumber/internal/domain/entity"
19 "github.com/bnema/dumber/internal/domain/repository"19 "github.com/bnema/dumber/internal/domain/repository"
20 "github.com/bnema/dumber/internal/infrastructure/cache"20 "github.com/bnema/dumber/internal/infrastructure/cache"
21+ cefinfra "github.com/bnema/dumber/internal/infrastructure/cef"
21 "github.com/bnema/dumber/internal/infrastructure/clipboard"22 "github.com/bnema/dumber/internal/infrastructure/clipboard"
22 "github.com/bnema/dumber/internal/infrastructure/config"23 "github.com/bnema/dumber/internal/infrastructure/config"
23 "github.com/bnema/dumber/internal/infrastructure/deps"24 "github.com/bnema/dumber/internal/infrastructure/deps"
@@ -205,6 +206,13 @@ func runGUI() int {
205 log.Error().Err(err).Msg("failed to create application")206 log.Error().Err(err).Msg("failed to create application")
206 return 1207 return 1
207 }208 }
209+ if cefEngine, ok := engine.(*cefinfra.Engine); ok {
210+ cefEngine.SetAlreadyRunningAppRelaunchHandler(func(url string) {
211+ if err := app.OpenFreshWindow(ctx, url); err != nil {
212+ log.Warn().Err(err).Str("url", url).Msg("failed to open relaunch browser window")
213+ }
214+ })
215+ }
208 timer.Mark("ui_deps")216 timer.Mark("ui_deps")
209 timer.Log(ctx)217 timer.Log(ctx)
210 218
M internal/infrastructure/cef/app.go +32 -3
@@ -1,6 +1,8 @@
11 package cef
22
33 import (
4+ "strings"
5+
46 purecef "github.com/bnema/purego-cef/cef"
57
68 "github.com/bnema/dumber/internal/logging"
@@ -125,9 +127,36 @@ func (h *dumberBPH) OnBeforeChildProcessLaunch(commandLine purecef.CommandLine)
125127 }
126128 h.engine.recordChildProcessLaunch(processType, useAngle, ozonePlatform, commandLineString)
127129 }
128-func (h *dumberBPH) OnAlreadyRunningAppRelaunch(_ purecef.CommandLine, _ string) int32 { return 0 }
129-func (h *dumberBPH) GetDefaultClient() purecef.Client { return nil }
130-func (h *dumberBPH) GetDefaultRequestContextHandler() purecef.RequestContextHandler { return nil }
130+func parseBrowseURLFromRelaunchCommandLine(commandLine purecef.CommandLine) string {
131+ if commandLine == nil {
132+ return ""
133+ }
134+
135+ fields := strings.Fields(commandLine.GetCommandLineString())
136+ for i, field := range fields {
137+ if field != "browse" {
138+ continue
139+ }
140+ if i+1 < len(fields) {
141+ return fields[i+1]
142+ }
143+ return ""
144+ }
145+
146+ return ""
147+}
148+
149+func (h *dumberBPH) OnAlreadyRunningAppRelaunch(commandLine purecef.CommandLine, _ string) int32 {
150+ if h != nil && h.engine != nil {
151+ if browseURL := parseBrowseURLFromRelaunchCommandLine(commandLine); browseURL != "" && h.engine.alreadyRunningAppRelaunchHandler != nil {
152+ h.engine.alreadyRunningAppRelaunchHandler(browseURL)
153+ }
154+ }
155+
156+ return 1
157+}
158+func (h *dumberBPH) GetDefaultClient() purecef.Client { return nil }
159+func (h *dumberBPH) GetDefaultRequestContextHandler() purecef.RequestContextHandler { return nil }
131160
132161 // OnScheduleMessagePumpWork is a no-op — multi-threaded message loop drives
133162 // its own pump. Required by the BrowserProcessHandler interface.
@@ -1,6 +1,8 @@
1 package cef1 package cef
2 2
3 import (3 import (
4+ "strings"
5+
4 purecef "github.com/bnema/purego-cef/cef"6 purecef "github.com/bnema/purego-cef/cef"
5 7
6 "github.com/bnema/dumber/internal/logging"8 "github.com/bnema/dumber/internal/logging"
@@ -125,9 +127,36 @@ func (h *dumberBPH) OnBeforeChildProcessLaunch(commandLine purecef.CommandLine)
125 }127 }
126 h.engine.recordChildProcessLaunch(processType, useAngle, ozonePlatform, commandLineString)128 h.engine.recordChildProcessLaunch(processType, useAngle, ozonePlatform, commandLineString)
127 }129 }
128-func (h *dumberBPH) OnAlreadyRunningAppRelaunch(_ purecef.CommandLine, _ string) int32 { return 0 }130+func parseBrowseURLFromRelaunchCommandLine(commandLine purecef.CommandLine) string {
129-func (h *dumberBPH) GetDefaultClient() purecef.Client { return nil }131+ if commandLine == nil {
130-func (h *dumberBPH) GetDefaultRequestContextHandler() purecef.RequestContextHandler { return nil }132+ return ""
133+ }
134+
135+ fields := strings.Fields(commandLine.GetCommandLineString())
136+ for i, field := range fields {
137+ if field != "browse" {
138+ continue
139+ }
140+ if i+1 < len(fields) {
141+ return fields[i+1]
142+ }
143+ return ""
144+ }
145+
146+ return ""
147+}
148+
149+func (h *dumberBPH) OnAlreadyRunningAppRelaunch(commandLine purecef.CommandLine, _ string) int32 {
150+ if h != nil && h.engine != nil {
151+ if browseURL := parseBrowseURLFromRelaunchCommandLine(commandLine); browseURL != "" && h.engine.alreadyRunningAppRelaunchHandler != nil {
152+ h.engine.alreadyRunningAppRelaunchHandler(browseURL)
153+ }
154+ }
155+
156+ return 1
157+}
158+func (h *dumberBPH) GetDefaultClient() purecef.Client { return nil }
159+func (h *dumberBPH) GetDefaultRequestContextHandler() purecef.RequestContextHandler { return nil }
131 160
132 // OnScheduleMessagePumpWork is a no-op — multi-threaded message loop drives161 // OnScheduleMessagePumpWork is a no-op — multi-threaded message loop drives
133 // its own pump. Required by the BrowserProcessHandler interface.162 // its own pump. Required by the BrowserProcessHandler interface.
M internal/infrastructure/cef/app_test.go +60 -0
@@ -2,6 +2,7 @@ package cef
22
33 import (
44 "testing"
5+ "unsafe"
56
67 purecef "github.com/bnema/purego-cef/cef"
78 )
@@ -19,6 +20,32 @@ func (s *schemeRegistrarStub) AddCustomScheme(schemeName string, options int32)
1920 return 1
2021 }
2122
23+type relaunchCommandLineStub struct {
24+ commandLineString string
25+}
26+
27+func (c relaunchCommandLineStub) IsValid() bool { return true }
28+func (c relaunchCommandLineStub) IsReadOnly() bool { return true }
29+func (c relaunchCommandLineStub) Copy() purecef.CommandLine { return c }
30+func (c relaunchCommandLineStub) InitFromArgv(int32, unsafe.Pointer) {}
31+func (c relaunchCommandLineStub) InitFromString(string) {}
32+func (c relaunchCommandLineStub) Reset() {}
33+func (c relaunchCommandLineStub) GetArgv(uintptr) {}
34+func (c relaunchCommandLineStub) GetCommandLineString() string { return c.commandLineString }
35+func (c relaunchCommandLineStub) GetProgram() string { return "dumber" }
36+func (c relaunchCommandLineStub) SetProgram(string) {}
37+func (c relaunchCommandLineStub) HasSwitches() bool { return false }
38+func (c relaunchCommandLineStub) HasSwitch(string) bool { return false }
39+func (c relaunchCommandLineStub) GetSwitchValue(string) string { return "" }
40+func (c relaunchCommandLineStub) GetSwitches(uintptr) {}
41+func (c relaunchCommandLineStub) AppendSwitch(string) {}
42+func (c relaunchCommandLineStub) AppendSwitchWithValue(string, string) {}
43+func (c relaunchCommandLineStub) HasArguments() bool { return true }
44+func (c relaunchCommandLineStub) GetArguments(uintptr) {}
45+func (c relaunchCommandLineStub) AppendArgument(string) {}
46+func (c relaunchCommandLineStub) PrependWrapper(string) {}
47+func (c relaunchCommandLineStub) RemoveSwitch(string) {}
48+
2249 func TestRegisterDumbScheme(t *testing.T) {
2350 t.Parallel()
2451
@@ -41,3 +68,36 @@ func TestRegisterDumbScheme(t *testing.T) {
4168 t.Fatalf("options = %d, want %d", stub.options, wantOptions)
4269 }
4370 }
71+
72+func TestParseBrowseURLFromRelaunchCommandLine(t *testing.T) {
73+ t.Parallel()
74+
75+ got := parseBrowseURLFromRelaunchCommandLine(relaunchCommandLineStub{
76+ commandLineString: "dumber browse https://example.com",
77+ })
78+
79+ if got != "https://example.com" {
80+ t.Fatalf("parsed browse url = %q, want %q", got, "https://example.com")
81+ }
82+}
83+
84+func TestDumberBPH_OnAlreadyRunningAppRelaunch_ForwardsBrowseURLAndReturns1(t *testing.T) {
85+ t.Parallel()
86+
87+ gotURL := ""
88+ eng := &Engine{}
89+ eng.SetAlreadyRunningAppRelaunchHandler(func(url string) {
90+ gotURL = url
91+ })
92+
93+ ret := (&dumberBPH{engine: eng}).OnAlreadyRunningAppRelaunch(relaunchCommandLineStub{
94+ commandLineString: "dumber browse https://example.com",
95+ }, "")
96+
97+ if ret != 1 {
98+ t.Fatalf("OnAlreadyRunningAppRelaunch returned %d, want 1", ret)
99+ }
100+ if gotURL != "https://example.com" {
101+ t.Fatalf("forwarded browse url = %q, want %q", gotURL, "https://example.com")
102+ }
103+}
@@ -2,6 +2,7 @@ package cef
2 2
3 import (3 import (
4 "testing"4 "testing"
5+ "unsafe"
5 6
6 purecef "github.com/bnema/purego-cef/cef"7 purecef "github.com/bnema/purego-cef/cef"
7 )8 )
@@ -19,6 +20,32 @@ func (s *schemeRegistrarStub) AddCustomScheme(schemeName string, options int32)
19 return 120 return 1
20 }21 }
21 22
23+type relaunchCommandLineStub struct {
24+ commandLineString string
25+}
26+
27+func (c relaunchCommandLineStub) IsValid() bool { return true }
28+func (c relaunchCommandLineStub) IsReadOnly() bool { return true }
29+func (c relaunchCommandLineStub) Copy() purecef.CommandLine { return c }
30+func (c relaunchCommandLineStub) InitFromArgv(int32, unsafe.Pointer) {}
31+func (c relaunchCommandLineStub) InitFromString(string) {}
32+func (c relaunchCommandLineStub) Reset() {}
33+func (c relaunchCommandLineStub) GetArgv(uintptr) {}
34+func (c relaunchCommandLineStub) GetCommandLineString() string { return c.commandLineString }
35+func (c relaunchCommandLineStub) GetProgram() string { return "dumber" }
36+func (c relaunchCommandLineStub) SetProgram(string) {}
37+func (c relaunchCommandLineStub) HasSwitches() bool { return false }
38+func (c relaunchCommandLineStub) HasSwitch(string) bool { return false }
39+func (c relaunchCommandLineStub) GetSwitchValue(string) string { return "" }
40+func (c relaunchCommandLineStub) GetSwitches(uintptr) {}
41+func (c relaunchCommandLineStub) AppendSwitch(string) {}
42+func (c relaunchCommandLineStub) AppendSwitchWithValue(string, string) {}
43+func (c relaunchCommandLineStub) HasArguments() bool { return true }
44+func (c relaunchCommandLineStub) GetArguments(uintptr) {}
45+func (c relaunchCommandLineStub) AppendArgument(string) {}
46+func (c relaunchCommandLineStub) PrependWrapper(string) {}
47+func (c relaunchCommandLineStub) RemoveSwitch(string) {}
48+
22 func TestRegisterDumbScheme(t *testing.T) {49 func TestRegisterDumbScheme(t *testing.T) {
23 t.Parallel()50 t.Parallel()
24 51
@@ -41,3 +68,36 @@ func TestRegisterDumbScheme(t *testing.T) {
41 t.Fatalf("options = %d, want %d", stub.options, wantOptions)68 t.Fatalf("options = %d, want %d", stub.options, wantOptions)
42 }69 }
43 }70 }
71+
72+func TestParseBrowseURLFromRelaunchCommandLine(t *testing.T) {
73+ t.Parallel()
74+
75+ got := parseBrowseURLFromRelaunchCommandLine(relaunchCommandLineStub{
76+ commandLineString: "dumber browse https://example.com",
77+ })
78+
79+ if got != "https://example.com" {
80+ t.Fatalf("parsed browse url = %q, want %q", got, "https://example.com")
81+ }
82+}
83+
84+func TestDumberBPH_OnAlreadyRunningAppRelaunch_ForwardsBrowseURLAndReturns1(t *testing.T) {
85+ t.Parallel()
86+
87+ gotURL := ""
88+ eng := &Engine{}
89+ eng.SetAlreadyRunningAppRelaunchHandler(func(url string) {
90+ gotURL = url
91+ })
92+
93+ ret := (&dumberBPH{engine: eng}).OnAlreadyRunningAppRelaunch(relaunchCommandLineStub{
94+ commandLineString: "dumber browse https://example.com",
95+ }, "")
96+
97+ if ret != 1 {
98+ t.Fatalf("OnAlreadyRunningAppRelaunch returned %d, want 1", ret)
99+ }
100+ if gotURL != "https://example.com" {
101+ t.Fatalf("forwarded browse url = %q, want %q", gotURL, "https://example.com")
102+ }
103+}
M internal/infrastructure/cef/engine.go +12 -5
@@ -27,11 +27,12 @@ type Engine struct {
2727 schemeHandler *dumbSchemeHandler
2828 contentInj *contentInjector
2929
30- registerHandlers HandlerRegistrar
31- registerAccentHandlers AccentHandlerRegistrar
32- currentConfigPayload func() ([]byte, error)
33- defaultConfigPayload func() ([]byte, error)
34- mediaClassifier MediaClassifier
30+ registerHandlers HandlerRegistrar
31+ registerAccentHandlers AccentHandlerRegistrar
32+ currentConfigPayload func() ([]byte, error)
33+ defaultConfigPayload func() ([]byte, error)
34+ mediaClassifier MediaClassifier
35+ alreadyRunningAppRelaunchHandler func(string)
3536
3637 // activeWebViews tracks all live webviews for CSS broadcast.
3738 activeWebViews sync.Map // map[port.WebViewID]*WebView
@@ -110,6 +111,12 @@ func (e *Engine) SetColorResolver(resolver port.ColorSchemeResolver) {
110111 }
111112 }
112113
114+// SetAlreadyRunningAppRelaunchHandler configures the callback invoked when CEF
115+// relaunches an already-running app instance.
116+func (e *Engine) SetAlreadyRunningAppRelaunchHandler(handler func(string)) {
117+ e.alreadyRunningAppRelaunchHandler = handler
118+}
119+
113120 // registerWebView adds a webview to the active tracking map.
114121 func (e *Engine) registerWebView(wv *WebView) {
115122 e.activeWebViews.Store(wv.id, wv)
@@ -27,11 +27,12 @@ type Engine struct {
27 schemeHandler *dumbSchemeHandler27 schemeHandler *dumbSchemeHandler
28 contentInj *contentInjector28 contentInj *contentInjector
29 29
30- registerHandlers HandlerRegistrar30+ registerHandlers HandlerRegistrar
31- registerAccentHandlers AccentHandlerRegistrar31+ registerAccentHandlers AccentHandlerRegistrar
32- currentConfigPayload func() ([]byte, error)32+ currentConfigPayload func() ([]byte, error)
33- defaultConfigPayload func() ([]byte, error)33+ defaultConfigPayload func() ([]byte, error)
34- mediaClassifier MediaClassifier34+ mediaClassifier MediaClassifier
35+ alreadyRunningAppRelaunchHandler func(string)
35 36
36 // activeWebViews tracks all live webviews for CSS broadcast.37 // activeWebViews tracks all live webviews for CSS broadcast.
37 activeWebViews sync.Map // map[port.WebViewID]*WebView38 activeWebViews sync.Map // map[port.WebViewID]*WebView
@@ -110,6 +111,12 @@ func (e *Engine) SetColorResolver(resolver port.ColorSchemeResolver) {
110 }111 }
111 }112 }
112 113
114+// SetAlreadyRunningAppRelaunchHandler configures the callback invoked when CEF
115+// relaunches an already-running app instance.
116+func (e *Engine) SetAlreadyRunningAppRelaunchHandler(handler func(string)) {
117+ e.alreadyRunningAppRelaunchHandler = handler
118+}
119+
113 // registerWebView adds a webview to the active tracking map.120 // registerWebView adds a webview to the active tracking map.
114 func (e *Engine) registerWebView(wv *WebView) {121 func (e *Engine) registerWebView(wv *WebView) {
115 e.activeWebViews.Store(wv.id, wv)122 e.activeWebViews.Store(wv.id, wv)

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