detached ede66e0

fix: simplify browser launch fallback and relay handling

commit
ede66e0f3b4bf81e8fbb0d026072735f1fb0d4a3
author
brice · · 2026-04-14 17:58 UTC
parent
b7da822
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +26 -34
M internal/infrastructure/desktop/adapter.go +13 -2
@@ -406,7 +406,18 @@ func LaunchExternalURL(uri string) {
406406
407407 // LaunchBrowserURL opens a URL in a new dumber browser window.
408408 func LaunchBrowserURL(uri string) {
409- execPath, err := getExecutablePath()
409+ launchBrowserBrowseURL(uri, getExecutablePath, startDetachedProcess)
410+}
411+
412+func launchBrowserBrowseURL(uri string, resolveExecutablePath func() (string, error), spawnDetachedProcess func(*exec.Cmd) error) {
413+ if resolveExecutablePath == nil {
414+ resolveExecutablePath = getExecutablePath
415+ }
416+ if spawnDetachedProcess == nil {
417+ spawnDetachedProcess = startDetachedProcess
418+ }
419+
420+ execPath, err := resolveExecutablePath()
410421 if err != nil {
411422 fmt.Fprintf(os.Stderr, "failed to resolve dumber executable for URL %q: %v\n", uri, err)
412423 return
@@ -414,7 +425,7 @@ func LaunchBrowserURL(uri string) {
414425
415426 cmd := exec.Command(execPath, "browse", uri)
416427 cmd.Env = sanitizedChildEnv(os.Environ())
417- if err := startDetachedProcess(cmd); err != nil {
428+ if err := spawnDetachedProcess(cmd); err != nil {
418429 fmt.Fprintf(os.Stderr, "failed to launch dumber browse for URL %q: %v\n", uri, err)
419430 }
420431 }
@@ -406,7 +406,18 @@ func LaunchExternalURL(uri string) {
406 406
407 // LaunchBrowserURL opens a URL in a new dumber browser window.407 // LaunchBrowserURL opens a URL in a new dumber browser window.
408 func LaunchBrowserURL(uri string) {408 func LaunchBrowserURL(uri string) {
409- execPath, err := getExecutablePath()409+ launchBrowserBrowseURL(uri, getExecutablePath, startDetachedProcess)
410+}
411+
412+func launchBrowserBrowseURL(uri string, resolveExecutablePath func() (string, error), spawnDetachedProcess func(*exec.Cmd) error) {
413+ if resolveExecutablePath == nil {
414+ resolveExecutablePath = getExecutablePath
415+ }
416+ if spawnDetachedProcess == nil {
417+ spawnDetachedProcess = startDetachedProcess
418+ }
419+
420+ execPath, err := resolveExecutablePath()
410 if err != nil {421 if err != nil {
411 fmt.Fprintf(os.Stderr, "failed to resolve dumber executable for URL %q: %v\n", uri, err)422 fmt.Fprintf(os.Stderr, "failed to resolve dumber executable for URL %q: %v\n", uri, err)
412 return423 return
@@ -414,7 +425,7 @@ func LaunchBrowserURL(uri string) {
414 425
415 cmd := exec.Command(execPath, "browse", uri)426 cmd := exec.Command(execPath, "browse", uri)
416 cmd.Env = sanitizedChildEnv(os.Environ())427 cmd.Env = sanitizedChildEnv(os.Environ())
417- if err := startDetachedProcess(cmd); err != nil {428+ if err := spawnDetachedProcess(cmd); err != nil {
418 fmt.Fprintf(os.Stderr, "failed to launch dumber browse for URL %q: %v\n", uri, err)429 fmt.Fprintf(os.Stderr, "failed to launch dumber browse for URL %q: %v\n", uri, err)
419 }430 }
420 }431 }
M internal/infrastructure/desktop/browser_launch_relay.go +12 -12
@@ -14,6 +14,7 @@ import (
1414 "time"
1515
1616 "github.com/bnema/dumber/internal/application/port"
17+ "github.com/bnema/dumber/internal/logging"
1718 )
1819
1920 const browserLaunchSocketName = "browser-launch.sock"
@@ -31,8 +32,7 @@ type browserLaunchRequest struct {
3132 }
3233
3334 type browserLaunchResponse struct {
34- Delivered bool `json:"delivered"`
35- Error string `json:"error,omitempty"`
35+ Error string `json:"error,omitempty"`
3636 }
3737
3838 type browserLaunchRelayListener struct {
@@ -52,13 +52,6 @@ func (r *browserLaunchRelay) DeliverOpenFreshWindow(ctx context.Context, url str
5252 return false, err
5353 }
5454
55- if _, statErr := os.Stat(socketPath); statErr != nil {
56- if os.IsNotExist(statErr) {
57- return false, nil
58- }
59- return false, statErr
60- }
61-
6255 conn, err := (&net.Dialer{}).DialContext(ctx, "unix", socketPath)
6356 if err != nil {
6457 if isMissingRelayListener(err) {
@@ -107,7 +100,7 @@ func (r *browserLaunchRelay) DeliverOpenFreshWindow(ctx context.Context, url str
107100 }
108101
109102 func isMissingRelayListener(err error) bool {
110- return os.IsNotExist(err) || errors.Is(err, syscall.ECONNREFUSED)
103+ return os.IsNotExist(err) || errors.Is(err, syscall.ECONNREFUSED) || errors.Is(err, syscall.ENOENT)
111104 }
112105
113106 func isBrowserLaunchReadTimeout(err error) bool {
@@ -220,7 +213,7 @@ func (*browserLaunchRelayListener) handleConnection(ctx context.Context, conn *n
220213 return
221214 }
222215
223- response := browserLaunchResponse{Delivered: true}
216+ response := browserLaunchResponse{}
224217 if err := opener.OpenFreshWindow(ctx, request.URL); err != nil {
225218 response.Error = err.Error()
226219 }
@@ -228,7 +221,14 @@ func (*browserLaunchRelayListener) handleConnection(ctx context.Context, conn *n
228221 if err := conn.SetDeadline(time.Now().Add(browserLaunchIOTimeout)); err != nil {
229222 return
230223 }
231- _ = json.NewEncoder(conn).Encode(response)
224+ if err := json.NewEncoder(conn).Encode(response); err != nil {
225+ log := logging.FromContext(ctx)
226+ entry := log.Warn().Err(err).Str("url", request.URL)
227+ if response.Error != "" {
228+ entry = entry.Str("response_error", response.Error)
229+ }
230+ entry.Msg("failed to encode browser launch response")
231+ }
232232 }
233233
234234 var _ port.BrowserLaunchRelay = (*browserLaunchRelay)(nil)
@@ -14,6 +14,7 @@ import (
14 "time"14 "time"
15 15
16 "github.com/bnema/dumber/internal/application/port"16 "github.com/bnema/dumber/internal/application/port"
17+ "github.com/bnema/dumber/internal/logging"
17 )18 )
18 19
19 const browserLaunchSocketName = "browser-launch.sock"20 const browserLaunchSocketName = "browser-launch.sock"
@@ -31,8 +32,7 @@ type browserLaunchRequest struct {
31 }32 }
32 33
33 type browserLaunchResponse struct {34 type browserLaunchResponse struct {
34- Delivered bool `json:"delivered"`35+ Error string `json:"error,omitempty"`
35- Error string `json:"error,omitempty"`
36 }36 }
37 37
38 type browserLaunchRelayListener struct {38 type browserLaunchRelayListener struct {
@@ -52,13 +52,6 @@ func (r *browserLaunchRelay) DeliverOpenFreshWindow(ctx context.Context, url str
52 return false, err52 return false, err
53 }53 }
54 54
55- if _, statErr := os.Stat(socketPath); statErr != nil {
56- if os.IsNotExist(statErr) {
57- return false, nil
58- }
59- return false, statErr
60- }
61-
62 conn, err := (&net.Dialer{}).DialContext(ctx, "unix", socketPath)55 conn, err := (&net.Dialer{}).DialContext(ctx, "unix", socketPath)
63 if err != nil {56 if err != nil {
64 if isMissingRelayListener(err) {57 if isMissingRelayListener(err) {
@@ -107,7 +100,7 @@ func (r *browserLaunchRelay) DeliverOpenFreshWindow(ctx context.Context, url str
107 }100 }
108 101
109 func isMissingRelayListener(err error) bool {102 func isMissingRelayListener(err error) bool {
110- return os.IsNotExist(err) || errors.Is(err, syscall.ECONNREFUSED)103+ return os.IsNotExist(err) || errors.Is(err, syscall.ECONNREFUSED) || errors.Is(err, syscall.ENOENT)
111 }104 }
112 105
113 func isBrowserLaunchReadTimeout(err error) bool {106 func isBrowserLaunchReadTimeout(err error) bool {
@@ -220,7 +213,7 @@ func (*browserLaunchRelayListener) handleConnection(ctx context.Context, conn *n
220 return213 return
221 }214 }
222 215
223- response := browserLaunchResponse{Delivered: true}216+ response := browserLaunchResponse{}
224 if err := opener.OpenFreshWindow(ctx, request.URL); err != nil {217 if err := opener.OpenFreshWindow(ctx, request.URL); err != nil {
225 response.Error = err.Error()218 response.Error = err.Error()
226 }219 }
@@ -228,7 +221,14 @@ func (*browserLaunchRelayListener) handleConnection(ctx context.Context, conn *n
228 if err := conn.SetDeadline(time.Now().Add(browserLaunchIOTimeout)); err != nil {221 if err := conn.SetDeadline(time.Now().Add(browserLaunchIOTimeout)); err != nil {
229 return222 return
230 }223 }
231- _ = json.NewEncoder(conn).Encode(response)224+ if err := json.NewEncoder(conn).Encode(response); err != nil {
225+ log := logging.FromContext(ctx)
226+ entry := log.Warn().Err(err).Str("url", request.URL)
227+ if response.Error != "" {
228+ entry = entry.Str("response_error", response.Error)
229+ }
230+ entry.Msg("failed to encode browser launch response")
231+ }
232 }232 }
233 233
234 var _ port.BrowserLaunchRelay = (*browserLaunchRelay)(nil)234 var _ port.BrowserLaunchRelay = (*browserLaunchRelay)(nil)
M internal/infrastructure/desktop/browser_launcher.go +1 -20
@@ -41,24 +41,5 @@ func (l *BrowserLauncher) LaunchURL(ctx context.Context, url string) {
4141 }
4242 }
4343
44- resolveExecutablePath := l.resolveExecutablePath
45- if resolveExecutablePath == nil {
46- resolveExecutablePath = getExecutablePath
47- }
48- execPath, err := resolveExecutablePath()
49- if err != nil {
50- fmt.Fprintf(os.Stderr, "failed to resolve dumber executable for URL %q: %v\n", url, err)
51- return
52- }
53-
54- cmd := exec.Command(execPath, "browse", url)
55- cmd.Env = sanitizedChildEnv(os.Environ())
56-
57- spawnDetachedProcess := l.startDetachedProcess
58- if spawnDetachedProcess == nil {
59- spawnDetachedProcess = startDetachedProcess
60- }
61- if err := spawnDetachedProcess(cmd); err != nil {
62- fmt.Fprintf(os.Stderr, "failed to launch dumber browse for URL %q: %v\n", url, err)
63- }
44+ launchBrowserBrowseURL(url, l.resolveExecutablePath, l.startDetachedProcess)
6445 }
@@ -41,24 +41,5 @@ func (l *BrowserLauncher) LaunchURL(ctx context.Context, url string) {
41 }41 }
42 }42 }
43 43
44- resolveExecutablePath := l.resolveExecutablePath44+ launchBrowserBrowseURL(url, l.resolveExecutablePath, l.startDetachedProcess)
45- if resolveExecutablePath == nil {
46- resolveExecutablePath = getExecutablePath
47- }
48- execPath, err := resolveExecutablePath()
49- if err != nil {
50- fmt.Fprintf(os.Stderr, "failed to resolve dumber executable for URL %q: %v\n", url, err)
51- return
52- }
53-
54- cmd := exec.Command(execPath, "browse", url)
55- cmd.Env = sanitizedChildEnv(os.Environ())
56-
57- spawnDetachedProcess := l.startDetachedProcess
58- if spawnDetachedProcess == nil {
59- spawnDetachedProcess = startDetachedProcess
60- }
61- if err := spawnDetachedProcess(cmd); err != nil {
62- fmt.Fprintf(os.Stderr, "failed to launch dumber browse for URL %q: %v\n", url, err)
63- }
64 }45 }

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