Skip to content

Commit 16a3c90

Browse files
authored
fix(cli): open the docs on Windows, where the tray already knows how (#1620)
1 parent 00715b3 commit 16a3c90

14 files changed

Lines changed: 126 additions & 113 deletions

File tree

docs/CLI.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ Accepted by every command.
9999
| [`toggle-screen-share`](#neru-toggle-screen-share) | Hide overlays while sharing | Yes | macOS |
100100
| [`roles`](#neru-roles) | List the role vocabulary | No | All |
101101
| [`services`](#neru-services) | Manage the system service | No | macOS · Linux |
102-
| [`docs`](#neru-docs) | Open documentation in a browser | No | macOS · Linux |
102+
| [`docs`](#neru-docs) | Open documentation in a browser | No | All |
103103

104104
¹ Element discovery quality differs by platform: a full accessibility tree on
105105
macOS, an AT-SPI walk on Linux whose coverage depends on the application, and a
@@ -1561,7 +1561,8 @@ Open documentation in a browser.
15611561
neru docs config|cli
15621562
```
15631563
1564-
**Platforms:** macOS and Linux; other platforms return `ERR_NOT_SUPPORTED`.
1564+
**Platforms:** all. The page opens with the desktop's default handler (`open`,
1565+
`xdg-open`, or `rundll32`).
15651566
15661567
URLs point at the Git tag matching the installed version. Development builds
15671568
fall back to `main`.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
//go:build darwin
2+
3+
package platform
4+
5+
import (
6+
"context"
7+
"os/exec"
8+
9+
"github.com/y3owk1n/neru/internal/derrors"
10+
)
11+
12+
// OpenExternal opens a URL or file path with the macOS default handler.
13+
// It does not validate the target or wait for the launched app.
14+
func OpenExternal(ctx context.Context, target string) error {
15+
err := exec.CommandContext(ctx, "/usr/bin/open", target).Run()
16+
if err != nil {
17+
return derrors.Wrap(err, derrors.CodeExecFailed, "failed to launch the system open handler")
18+
}
19+
20+
return nil
21+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
//go:build linux
2+
3+
package platform
4+
5+
import (
6+
"context"
7+
"os/exec"
8+
9+
"github.com/y3owk1n/neru/internal/derrors"
10+
)
11+
12+
// OpenExternal opens a URL or file path with the Linux desktop's default
13+
// handler. It does not validate the target or wait for the launched app.
14+
func OpenExternal(ctx context.Context, target string) error {
15+
err := exec.CommandContext(ctx, "xdg-open", target).Start()
16+
if err != nil {
17+
return derrors.Wrap(err, derrors.CodeExecFailed, "failed to launch the system open handler")
18+
}
19+
20+
return nil
21+
}
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
//go:build !darwin && !linux && !windows
22

3-
package systray
3+
package platform
44

55
import (
66
"context"
77

88
"github.com/y3owk1n/neru/internal/derrors"
99
)
1010

11-
func openExternal(_ context.Context, _ string) error {
11+
// OpenExternal is the non-target fallback: no handler is known here.
12+
func OpenExternal(_ context.Context, _ string) error {
1213
return derrors.New(derrors.CodeNotSupported, "opening external targets is not supported")
1314
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package platform_test
2+
3+
import (
4+
"context"
5+
"runtime"
6+
"testing"
7+
8+
"github.com/y3owk1n/neru/internal/adapter/platform"
9+
"github.com/y3owk1n/neru/internal/derrors"
10+
)
11+
12+
// An already-canceled context makes the launch fail before anything is
13+
// spawned, so this runs everywhere without opening a browser. The shipped
14+
// platforms report the failed launch, and the fallback slot refuses.
15+
func TestOpenExternalReportsALaunchFailure(t *testing.T) {
16+
ctx, cancel := context.WithCancel(context.Background())
17+
cancel()
18+
19+
err := platform.OpenExternal(ctx, "https://example.invalid")
20+
if err == nil {
21+
t.Fatal("OpenExternal() = nil, want an error when the handler cannot launch")
22+
}
23+
24+
wantCode := derrors.CodeNotSupported
25+
26+
switch runtime.GOOS {
27+
case string(platform.Darwin), string(platform.Linux), string(platform.Windows):
28+
wantCode = derrors.CodeExecFailed
29+
}
30+
31+
if !derrors.IsCode(err, wantCode) {
32+
t.Errorf("OpenExternal() error = %v, want code %s", err, wantCode)
33+
}
34+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//go:build windows
2+
3+
package platform
4+
5+
import (
6+
"context"
7+
"os/exec"
8+
9+
"github.com/y3owk1n/neru/internal/derrors"
10+
)
11+
12+
// OpenExternal opens a URL or file path with the Windows default handler via
13+
// rundll32. It does not validate the target or wait for the launched app.
14+
func OpenExternal(ctx context.Context, target string) error {
15+
// rundll32 url.dll,FileProtocolHandler handles both URLs and file paths
16+
// using the registered default application, with no console window flash.
17+
err := exec.CommandContext(ctx, "rundll32", "url.dll,FileProtocolHandler", target).Run()
18+
if err != nil {
19+
return derrors.Wrap(err, derrors.CodeExecFailed, "failed to launch the system open handler")
20+
}
21+
22+
return nil
23+
}

internal/app/components/systray/open_darwin.go

Lines changed: 0 additions & 14 deletions
This file was deleted.

internal/app/components/systray/open_linux.go

Lines changed: 0 additions & 14 deletions
This file was deleted.

internal/app/components/systray/open_windows.go

Lines changed: 0 additions & 16 deletions
This file was deleted.

internal/app/components/systray/systray.go

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/atotto/clipboard"
88
"go.uber.org/zap"
99

10+
"github.com/y3owk1n/neru/internal/adapter/platform"
1011
"github.com/y3owk1n/neru/internal/buildinfo"
1112
"github.com/y3owk1n/neru/internal/domain"
1213
"github.com/y3owk1n/neru/internal/ports"
@@ -290,14 +291,14 @@ func (c *Component) handleEvents() {
290291
go c.handleOpenConfig()
291292
case <-c.mSourceCode.Clicked():
292293
go func() {
293-
err := openExternal(c.ctx, "https://github.com/y3owk1n/neru")
294+
err := platform.OpenExternal(c.ctx, "https://github.com/y3owk1n/neru")
294295
if err != nil {
295296
c.logger.Error("Failed to open repository", zap.Error(err))
296297
}
297298
}()
298299
case <-c.mDocsConfig.Clicked():
299300
go func() {
300-
err := openExternal(
301+
err := platform.OpenExternal(
301302
c.ctx,
302303
buildinfo.DocsURL("docs/CONFIGURATION.md", buildinfo.Version),
303304
)
@@ -307,14 +308,17 @@ func (c *Component) handleEvents() {
307308
}()
308309
case <-c.mDocsCLI.Clicked():
309310
go func() {
310-
err := openExternal(c.ctx, buildinfo.DocsURL("docs/CLI.md", buildinfo.Version))
311+
err := platform.OpenExternal(
312+
c.ctx,
313+
buildinfo.DocsURL("docs/CLI.md", buildinfo.Version),
314+
)
311315
if err != nil {
312316
c.logger.Error("Failed to open CLI docs", zap.Error(err))
313317
}
314318
}()
315319
case <-c.mFeatureRequest.Clicked():
316320
go func() {
317-
err := openExternal(
321+
err := platform.OpenExternal(
318322
c.ctx,
319323
"https://github.com/y3owk1n/neru/issues/new?template=feature_request.yml",
320324
)
@@ -324,7 +328,7 @@ func (c *Component) handleEvents() {
324328
}()
325329
case <-c.mReportBug.Clicked():
326330
go func() {
327-
err := openExternal(
331+
err := platform.OpenExternal(
328332
c.ctx,
329333
"https://github.com/y3owk1n/neru/issues/new?template=bug_report.yml",
330334
)
@@ -334,7 +338,7 @@ func (c *Component) handleEvents() {
334338
}()
335339
case <-c.mDiscuss.Clicked():
336340
go func() {
337-
err := openExternal(c.ctx, "https://github.com/y3owk1n/neru/discussions")
341+
err := platform.OpenExternal(c.ctx, "https://github.com/y3owk1n/neru/discussions")
338342
if err != nil {
339343
c.logger.Error("Failed to open community discussion", zap.Error(err))
340344
}
@@ -405,7 +409,7 @@ func (c *Component) handleOpenConfig() {
405409
return
406410
}
407411

408-
err := openExternal(c.ctx, configPath)
412+
err := platform.OpenExternal(c.ctx, configPath)
409413
if err != nil {
410414
c.logger.Error("Failed to open config file", zap.Error(err))
411415
}

0 commit comments

Comments
 (0)