Support for custom browser
Release / release (push) Successful in 26s

This commit is contained in:
Alexandru Macocian
2026-06-15 17:19:22 +02:00
parent c943b1f4fb
commit f6e8186b46
11 changed files with 107 additions and 12 deletions
+1 -1
View File
@@ -1 +1 @@
0.1.6
0.1.7
+1 -1
View File
@@ -117,7 +117,7 @@ func main() {
OnAuthURL: func(u string) {
fmt.Fprintln(os.Stderr, "gitea-mcp: opening browser for Gitea OAuth login:")
fmt.Fprintln(os.Stderr, " ", u)
if err := browser.Open(u); err != nil {
if err := browser.OpenWith(svc.OAuthBrowser, u); err != nil {
fmt.Fprintln(os.Stderr, "gitea-mcp: open browser:", err)
fmt.Fprintln(os.Stderr, "(visit the URL above manually)")
}
+1 -1
View File
@@ -114,7 +114,7 @@ func main() {
OnAuthURL: func(u string) {
fmt.Fprintln(os.Stderr, "grafana-mcp: opening browser for Grafana OAuth login:")
fmt.Fprintln(os.Stderr, " ", u)
if err := browser.Open(u); err != nil {
if err := browser.OpenWith(svc.OAuthBrowser, u); err != nil {
fmt.Fprintln(os.Stderr, "grafana-mcp: open browser:", err)
fmt.Fprintln(os.Stderr, "(visit the URL above manually)")
}
+1 -1
View File
@@ -101,7 +101,7 @@ func main() {
OnAuthURL: func(u string) {
fmt.Fprintln(os.Stderr, "gssh-mcp: opening browser for Gssh OAuth login:")
fmt.Fprintln(os.Stderr, " ", u)
if err := browser.Open(u); err != nil {
if err := browser.OpenWith(svc.OAuthBrowser, u); err != nil {
fmt.Fprintln(os.Stderr, "gssh-mcp: open browser:", err)
fmt.Fprintln(os.Stderr, "(visit the URL above manually)")
}
+1 -1
View File
@@ -95,7 +95,7 @@ func main() {
OnAuthURL: func(u string) {
fmt.Fprintln(os.Stderr, "searxng-mcp: opening browser for SearXNG OAuth login:")
fmt.Fprintln(os.Stderr, " ", u)
if err := browser.Open(u); err != nil {
if err := browser.OpenWith(svc.OAuthBrowser, u); err != nil {
fmt.Fprintln(os.Stderr, "searxng-mcp: open browser:", err)
fmt.Fprintln(os.Stderr, "(visit the URL above manually)")
}
+6
View File
@@ -1,5 +1,11 @@
# sherlock conifugration - lives in $XDG_CONFIG_HOME/sherlock/config.toml
# ── OAuth login ───────────────────────────────────────────────────────
# Optional browser executable/name for OAuth login URLs. Leave unset to
# use the OS default browser (xdg-open/open/rundll32).
# [oauth]
# browser = "firefox"
# ── Providers ────────────────────────────────────────────────────────
# A [providers.<name>] block is a reusable OAuth/OIDC client. Services
# point at one via `provider = "<name>"`. Define a provider once and
+4
View File
@@ -19,6 +19,10 @@ Resolved in order:
## Shape
`[oauth]` is optional. Set `browser = "<executable>"` to open OAuth login URLs
with a specific browser, for example `browser = "firefox"`. Leave it unset to
use the OS default browser.
`[providers.<name>]` defines a reusable OAuth/OIDC identity: `issuer`,
`client_id`, optional `client_secret`.
+23 -7
View File
@@ -7,6 +7,7 @@ package browser
import (
"os/exec"
"runtime"
"strings"
)
// Open starts the OS's default browser pointed at url, then returns
@@ -15,9 +16,29 @@ import (
// itself reports — there's no portable way to know whether the user
// actually loaded the page.
func Open(url string) error {
return OpenWith("", url)
}
// OpenWith starts browser pointed at url, then returns immediately. If
// browser is empty, it falls back to the OS default helper used by Open.
func OpenWith(browser, url string) error {
bin, args := commandFor(browser, url, runtime.GOOS)
cmd := exec.Command(bin, args...)
if err := cmd.Start(); err != nil {
return err
}
go func() { _ = cmd.Wait() }()
return nil
}
func commandFor(browser, url, goos string) (string, []string) {
if browser = strings.TrimSpace(browser); browser != "" {
return browser, []string{url}
}
var bin string
var args []string
switch runtime.GOOS {
switch goos {
case "darwin":
bin = "open"
args = []string{url}
@@ -28,10 +49,5 @@ func Open(url string) error {
bin = "xdg-open"
args = []string{url}
}
cmd := exec.Command(bin, args...)
if err := cmd.Start(); err != nil {
return err
}
go func() { _ = cmd.Wait() }()
return nil
return bin, args
}
+36
View File
@@ -0,0 +1,36 @@
package browser
import (
"reflect"
"testing"
)
func TestCommandFor_Defaults(t *testing.T) {
tests := []struct {
name string
goos string
wantBin string
wantArgs []string
}{
{name: "linux", goos: "linux", wantBin: "xdg-open", wantArgs: []string{"https://example.com"}},
{name: "darwin", goos: "darwin", wantBin: "open", wantArgs: []string{"https://example.com"}},
{name: "windows", goos: "windows", wantBin: "rundll32", wantArgs: []string{"url.dll,FileProtocolHandler", "https://example.com"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotBin, gotArgs := commandFor("", "https://example.com", tt.goos)
if gotBin != tt.wantBin || !reflect.DeepEqual(gotArgs, tt.wantArgs) {
t.Fatalf("commandFor = %q %v, want %q %v", gotBin, gotArgs, tt.wantBin, tt.wantArgs)
}
})
}
}
func TestCommandFor_CustomBrowser(t *testing.T) {
gotBin, gotArgs := commandFor(" firefox ", "https://example.com", "linux")
wantArgs := []string{"https://example.com"}
if gotBin != "firefox" || !reflect.DeepEqual(gotArgs, wantArgs) {
t.Fatalf("commandFor = %q %v, want firefox %v", gotBin, gotArgs, wantArgs)
}
}
+13
View File
@@ -9,6 +9,9 @@
//
// Shape:
//
// [oauth]
// browser = "firefox" # optional; default OS browser when empty
//
// [providers.sherlock-cli] # a reusable OAuth provider
// issuer = "https://id.example/application/o/sherlock-cli/"
// client_id = "abc123"
@@ -66,8 +69,16 @@ type Service struct {
BaseURL string `toml:"base_url"`
}
// OAuth contains global OAuth-flow preferences.
type OAuth struct {
// Browser is an optional executable name or path used to open OAuth
// login URLs. Empty means the OS default browser.
Browser string `toml:"browser"`
}
// Config is the whole parsed file.
type Config struct {
OAuth OAuth `toml:"oauth"`
Providers map[string]Provider `toml:"providers"`
Services map[string]Service `toml:"services"`
@@ -83,6 +94,7 @@ type Resolved struct {
ClientID string
ClientSecret string
BaseURL string
OAuthBrowser string
}
// DefaultPath returns the config-file path sherlock reads, honouring
@@ -155,6 +167,7 @@ func (c *Config) Service(name string) (Resolved, error) {
ClientID: svc.ClientID,
ClientSecret: svc.ClientSecret,
BaseURL: svc.BaseURL,
OAuthBrowser: c.OAuth.Browser,
}
if svc.Provider != "" {
+20
View File
@@ -63,6 +63,26 @@ base_url = "https://gitea.example"
}
}
func TestService_OAuthBrowser(t *testing.T) {
path := writeConfig(t, `
[oauth]
browser = "firefox"
[services.gitea]
issuer = "https://gitea.example"
client_id = "gitea-client"
base_url = "https://gitea.example"
`)
c, _ := LoadFrom(path)
got, err := c.Service("gitea")
if err != nil {
t.Fatalf("Service: %v", err)
}
if got.OAuthBrowser != "firefox" {
t.Fatalf("OAuthBrowser = %q", got.OAuthBrowser)
}
}
func TestService_UnknownService(t *testing.T) {
path := writeConfig(t, `
[services.gitea]