From 19134562012bf5c48f760c7a53ba51e66d89c18e Mon Sep 17 00:00:00 2001 From: Aly Raffauf Date: Sun, 5 Jul 2026 23:03:51 -0400 Subject: [PATCH] gtk: add launch-by-id and app-info lookup helpers --- gtk/browser.go | 22 ++++++ gtk/run.go | 28 +++---- internal/browserscan/actions.go | 3 +- internal/browserscan/browserscan.go | 53 +++++++++++++ internal/browserscan/browserscan_test.go | 96 ++++++++++++++++++++++++ 5 files changed, 184 insertions(+), 18 deletions(-) diff --git a/gtk/browser.go b/gtk/browser.go index af303cd..1a8a957 100644 --- a/gtk/browser.go +++ b/gtk/browser.go @@ -51,6 +51,28 @@ func detectBrowsers() []*Browser { return browsers } +func appInfoForID(id string) *gio.AppInfo { + for _, appInfo := range gio.AppInfoGetRecommendedForType("x-scheme-handler/http") { + if appInfo.ID() == id { + return appInfo + } + } + return nil +} + +func launchBrowserByID(id, url string) bool { + browserModel, ok := browserscan.Find(id) + if !ok { + return false + } + + launchBrowser(&Browser{ + Browser: &browserModel, + appInfo: appInfoForID(browserModel.ID), + }, url) + return true +} + func launchBrowser(b *Browser, url string) { if b.Exec == "" { fmt.Fprintf(os.Stderr, "Error: No command line for browser %s\n", b.Name) diff --git a/gtk/run.go b/gtk/run.go index caf78a5..6b0d286 100644 --- a/gtk/run.go +++ b/gtk/run.go @@ -49,17 +49,16 @@ func Run() { app.Hold() } setupApp(cfg) - browsers := detectBrowsers() if len(files) == 0 { - showSettingsWindow(app, browsers, cfg) + showSettingsWindow(app, detectBrowsers(), cfg) return } rawURL := files[0].URI() if u, err := url.Parse(rawURL); err == nil && u.Scheme == "switchyard" { - handleSwitchyardURL(app, browsers, cfg, rawURL) + handleSwitchyardURL(app, cfg, rawURL) return } @@ -71,7 +70,7 @@ func Run() { return } - handleURL(app, browsers, cfg, sanitized) + handleURL(app, cfg, sanitized) }) if code := app.Run(os.Args); code > 0 { @@ -99,7 +98,7 @@ func setupApp(cfg *Config) { } // handleSwitchyardURL processes switchyard:// URLs with browser preferences -func handleSwitchyardURL(app *adw.Application, browsers []*Browser, cfg *Config, rawURL string) { +func handleSwitchyardURL(app *adw.Application, cfg *Config, rawURL string) { targetURL, browserPrefs, err := routing.ParseSwitchyardURL(rawURL) if err != nil { // Invalid switchyard URL - ignore @@ -122,8 +121,7 @@ func handleSwitchyardURL(app *adw.Application, browsers []*Browser, cfg *Config, if !strings.HasSuffix(id, ".desktop") { id = id + ".desktop" } - if browser := findBrowserByID(browsers, id); browser != nil { - launchBrowser(browser, sanitized) + if launchBrowserByID(id, sanitized) { return } } @@ -131,34 +129,32 @@ func handleSwitchyardURL(app *adw.Application, browsers []*Browser, cfg *Config, } // No browser specified or none matched — use standard routing. - handleURL(app, browsers, cfg, sanitized) + handleURL(app, cfg, sanitized) } // handleURL routes a URL to the appropriate browser based on rules -func handleURL(app *adw.Application, browsers []*Browser, cfg *Config, urlStr string) { +func handleURL(app *adw.Application, cfg *Config, urlStr string) { browserID, alwaysAsk, matched := cfg.MatchRule(urlStr) if matched { if alwaysAsk { - showLauncherWindow(app, urlStr, browsers, cfg) + showLauncherWindow(app, urlStr, detectBrowsers(), cfg) return } - if browser := findBrowserByID(browsers, browserID); browser != nil { - launchBrowser(browser, urlStr) + if launchBrowserByID(browserID, urlStr) { return } // Rule matched but browser not found — show launcher. - showLauncherWindow(app, urlStr, browsers, cfg) + showLauncherWindow(app, urlStr, detectBrowsers(), cfg) return } // No rule matched: fall back to the favorite browser, else prompt. if !cfg.PromptOnClick && cfg.FavoriteBrowser != "" { - if browser := findBrowserByID(browsers, cfg.FavoriteBrowser); browser != nil { - launchBrowser(browser, urlStr) + if launchBrowserByID(cfg.FavoriteBrowser, urlStr) { return } } - showLauncherWindow(app, urlStr, browsers, cfg) + showLauncherWindow(app, urlStr, detectBrowsers(), cfg) } diff --git a/internal/browserscan/actions.go b/internal/browserscan/actions.go index 9f38675..cb90a09 100644 --- a/internal/browserscan/actions.go +++ b/internal/browserscan/actions.go @@ -4,7 +4,6 @@ package browserscan import ( "github.com/alyraffauf/goxdgdesktop/desktopfile" - "github.com/alyraffauf/goxdgdesktop/xdg" "github.com/alyraffauf/switchyard/internal/browser" ) @@ -13,7 +12,7 @@ func ListDesktopActions(appID string) []browser.Action { return nil } - desktopFilePath := xdg.FindDesktopFile(appID) + desktopFilePath := findDesktopFile(appID) if desktopFilePath == "" { return nil } diff --git a/internal/browserscan/browserscan.go b/internal/browserscan/browserscan.go index d6471cc..22e3953 100644 --- a/internal/browserscan/browserscan.go +++ b/internal/browserscan/browserscan.go @@ -7,6 +7,7 @@ package browserscan import ( "cmp" "io/fs" + "os" "path/filepath" "slices" "strings" @@ -72,6 +73,58 @@ func Installed() []browser.Browser { return browsers } +// Find returns a displayable HTTP(S) browser by desktop file ID. +func Find(id string) (browser.Browser, bool) { + if id == "" || strings.HasPrefix(id, selfIDPrefix) { + return browser.Browser{}, false + } + + for _, dir := range applicationsDirs() { + path, ok := desktopFilePath(dir, id) + if !ok { + continue + } + return parseBrowser(id, path) + } + + return browser.Browser{}, false +} + +func findDesktopFile(id string) string { + for _, dir := range applicationsDirs() { + if path, ok := desktopFilePath(dir, id); ok { + return path + } + } + return "" +} + +func desktopFilePath(dir, id string) (string, bool) { + path := filepath.Join(dir, id) + if _, err := os.Stat(path); err == nil { + return path, true + } + + var match string + _ = filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { + if err != nil { + if d != nil && d.IsDir() { + return fs.SkipDir + } + return nil + } + if d.IsDir() || !strings.HasSuffix(d.Name(), ".desktop") { + return nil + } + if desktopID(dir, path) != id { + return nil + } + match = path + return fs.SkipAll + }) + return match, match != "" +} + // desktopID is path's desktop-file ID relative to dir, with separators replaced // by "-" per the spec (sub/foo.desktop -> sub-foo.desktop). func desktopID(dir, path string) string { diff --git a/internal/browserscan/browserscan_test.go b/internal/browserscan/browserscan_test.go index c0e9937..9e5a5b9 100644 --- a/internal/browserscan/browserscan_test.go +++ b/internal/browserscan/browserscan_test.go @@ -273,6 +273,102 @@ MimeType=x-scheme-handler/https; } } +func TestFindBrowser(t *testing.T) { + home, _ := isolatedDirs(t) + writeDesktop(t, home, "test-browser.desktop", browserEntry) + + got, ok := Find("test-browser.desktop") + if !ok { + t.Fatal("Find returned false") + } + if got.ID != "test-browser.desktop" || got.Name != "Test Browser" || + got.Icon != "test-browser" || got.Exec != "test-browser %u" { + t.Errorf("unexpected browser: %+v", got) + } +} + +func TestFindMissingOrRejected(t *testing.T) { + tests := []struct { + name string + id string + contents string + }{ + { + name: "missing", + id: "missing.desktop", + }, + { + name: "hidden", + id: "hidden.desktop", + contents: `[Desktop Entry] +Type=Application +Name=Hidden +Hidden=true +MimeType=x-scheme-handler/http; +`, + }, + { + name: "not browser", + id: "app.desktop", + contents: `[Desktop Entry] +Type=Application +Name=App +MimeType=text/plain; +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + home, _ := isolatedDirs(t) + if tt.contents != "" { + writeDesktop(t, home, tt.id, tt.contents) + } + + if got, ok := Find(tt.id); ok { + t.Errorf("Find returned %+v, want false", got) + } + }) + } +} + +func TestFindShadowingRejectedHomeSuppressesSystem(t *testing.T) { + home, system := isolatedDirs(t) + writeDesktop(t, home, "browser.desktop", `[Desktop Entry] +Type=Application +Name=Hidden Home Browser +NoDisplay=true +MimeType=x-scheme-handler/http; +`) + writeDesktop(t, system, "browser.desktop", browserEntry) + + if got, ok := Find("browser.desktop"); ok { + t.Errorf("Find returned %+v, want false", got) + } +} + +func TestFindNestedSubdirID(t *testing.T) { + home, _ := isolatedDirs(t) + writeDesktop(t, home, "sub/foo.desktop", browserEntry) + + got, ok := Find("sub-foo.desktop") + if !ok { + t.Fatal("Find returned false") + } + if got.ID != "sub-foo.desktop" { + t.Errorf("got ID %q, want %q", got.ID, "sub-foo.desktop") + } +} + +func TestFindExcludesSwitchyardFamily(t *testing.T) { + home, _ := isolatedDirs(t) + writeDesktop(t, home, "io.github.alyraffauf.Switchyard.desktop", browserEntry) + + if got, ok := Find("io.github.alyraffauf.Switchyard.desktop"); ok { + t.Errorf("Find returned %+v, want false", got) + } +} + func BenchmarkInstalledManyDesktopFiles(b *testing.B) { home := filepath.Join(b.TempDir(), "applications") system := filepath.Join(b.TempDir(), "applications") -- 2.51.2