Rules-based browser launcher for TUI + GNOME. switchyard.aly.codes
tui gome bowser go
0

Configure Feed

Select the types of activity you want to include in your feed.

browserscan: add IncludeNoDisplay option to control NoDisplay entries

Aly Raffauf (Jul 10, 2026, 6:26 PM EDT) 7afc443b f5ce1a31

+119 -23
+19 -9
internal/browserscan/browserscan.go
··· 28 28 29 29 // Installed returns the installed HTTP(S) browsers, sorted by Name. Switchyard's 30 30 // own entries are excluded; filtering config-hidden browsers is the caller's job. 31 - func Installed() []browser.Browser { 31 + // By default NoDisplay=true browsers are included; pass IncludeNoDisplay(false) to 32 + // hide them. 33 + func Installed(opts ...Option) []browser.Browser { 34 + options := newScanOptions(opts) 35 + 32 36 var browsers []browser.Browser 33 37 seen := map[string]bool{} 34 38 ··· 57 61 return nil 58 62 } 59 63 60 - if parsed, ok := parseBrowser(id, path); ok { 64 + if parsed, ok := parseBrowser(id, path, options); ok { 61 65 browsers = append(browsers, parsed) 62 66 } 63 67 return nil ··· 69 73 return browsers 70 74 } 71 75 72 - // Find returns a displayable HTTP(S) browser by desktop file ID. 73 - func Find(id string) (browser.Browser, bool) { 76 + // Find returns a displayable HTTP(S) browser by desktop file ID. By default a 77 + // NoDisplay=true entry still matches; pass IncludeNoDisplay(false) to reject it. 78 + func Find(id string, opts ...Option) (browser.Browser, bool) { 74 79 if isSelf(id) { 75 80 return browser.Browser{}, false 76 81 } 77 82 83 + options := newScanOptions(opts) 84 + 78 85 for _, dir := range applicationsDirs() { 79 86 path, ok := desktopFilePath(dir, id) 80 87 if !ok { 81 88 continue 82 89 } 83 - return parseBrowser(id, path) 90 + return parseBrowser(id, path, options) 84 91 } 85 92 86 93 return browser.Browser{}, false ··· 131 138 return strings.ReplaceAll(filepath.ToSlash(rel), "/", "-") 132 139 } 133 140 134 - // parseBrowser returns the Browser at path, or ok=false if it isn't a 135 - // displayable HTTP(S) browser. 136 - func parseBrowser(id, path string) (browser.Browser, bool) { 141 + // parseBrowser returns the Browser at path, or ok=false if it isn't an HTTP(S) 142 + // browser the given options accept. 143 + func parseBrowser(id, path string, options scanOptions) (browser.Browser, bool) { 137 144 file, err := desktopfile.Read(path) 138 145 if err != nil { 139 146 return browser.Browser{}, false ··· 142 149 if typ, _ := file.Get(desktopfile.EntrySection, "Type"); typ != "Application" { 143 150 return browser.Browser{}, false 144 151 } 145 - if isTrue(file, "Hidden") || isTrue(file, "NoDisplay") { 152 + if isTrue(file, "Hidden") { 153 + return browser.Browser{}, false 154 + } 155 + if !options.includeNoDisplay && isTrue(file, "NoDisplay") { 146 156 return browser.Browser{}, false 147 157 } 148 158 if !handlesHTTP(file) {
+6 -3
internal/browserscan/browserscan_darwin.go
··· 70 70 71 71 // Installed returns the installed HTTP(S) browsers, sorted by Name. Switchyard's 72 72 // own entries are excluded; filtering config-hidden browsers is the caller's job. 73 - func Installed() []browser.Browser { 73 + // Options are accepted for cross-platform API parity; IncludeNoDisplay is a no-op 74 + // on macOS, where browsers come from LaunchServices rather than .desktop files. 75 + func Installed(opts ...Option) []browser.Browser { 74 76 browsers := scanBrowsers() 75 77 sortByName(browsers) 76 78 77 79 return browsers 78 80 } 79 81 80 - // Find returns a displayable HTTP(S) browser by bundle identifier. 81 - func Find(id string) (browser.Browser, bool) { 82 + // Find returns a displayable HTTP(S) browser by bundle identifier. Options are 83 + // accepted for cross-platform API parity but have no effect on macOS. 84 + func Find(id string, opts ...Option) (browser.Browser, bool) { 82 85 if isSelf(id) { 83 86 return browser.Browser{}, false 84 87 }
+66 -11
internal/browserscan/browserscan_test.go
··· 132 132 `, 133 133 }, 134 134 { 135 - name: "nodisplay", 136 - contents: `[Desktop Entry] 137 - Type=Application 138 - Name=Hidden Browser 139 - NoDisplay=true 140 - MimeType=x-scheme-handler/http; 141 - `, 142 - }, 143 - { 144 135 name: "hidden", 145 136 contents: `[Desktop Entry] 146 137 Type=Application ··· 216 207 } 217 208 } 218 209 210 + // TestInstalledIncludesNoDisplay guards against regressing the fix for #10: 211 + // users deliberately set NoDisplay=true on custom per-profile browser entries 212 + // to hide them from launchers while still wanting Switchyard to detect them. 213 + func TestInstalledIncludesNoDisplay(t *testing.T) { 214 + home, _ := isolatedDirs(t) 215 + writeDesktop(t, home, "browser.desktop", `[Desktop Entry] 216 + Type=Application 217 + Name=NoDisplay Browser 218 + NoDisplay=true 219 + MimeType=x-scheme-handler/http; 220 + `) 221 + 222 + got := Installed() 223 + if len(got) != 1 { 224 + t.Fatalf("got %d browsers, want 1: %v", len(got), ids(got)) 225 + } 226 + if got[0].Name != "NoDisplay Browser" { 227 + t.Errorf("got %q, want %q", got[0].Name, "NoDisplay Browser") 228 + } 229 + } 230 + 231 + // TestInstalledIncludeNoDisplayFalse covers the opt-in launcher-style behavior 232 + // for other consumers of the parser: NoDisplay=true entries are excluded while 233 + // ordinary browsers still come through. 234 + func TestInstalledIncludeNoDisplayFalse(t *testing.T) { 235 + home, _ := isolatedDirs(t) 236 + writeDesktop(t, home, "hidden.desktop", `[Desktop Entry] 237 + Type=Application 238 + Name=NoDisplay Browser 239 + NoDisplay=true 240 + MimeType=x-scheme-handler/http; 241 + `) 242 + writeDesktop(t, home, "visible.desktop", `[Desktop Entry] 243 + Type=Application 244 + Name=Visible Browser 245 + MimeType=x-scheme-handler/http; 246 + `) 247 + 248 + got := ids(Installed(IncludeNoDisplay(false))) 249 + want := []string{"visible.desktop"} 250 + if len(got) != len(want) || got[0] != want[0] { 251 + t.Errorf("got %v, want %v", got, want) 252 + } 253 + } 254 + 255 + // TestFindIncludeNoDisplayFalse rejects a NoDisplay entry only when the caller 256 + // opts out; the default still matches it. 257 + func TestFindIncludeNoDisplayFalse(t *testing.T) { 258 + home, _ := isolatedDirs(t) 259 + writeDesktop(t, home, "hidden.desktop", `[Desktop Entry] 260 + Type=Application 261 + Name=NoDisplay Browser 262 + NoDisplay=true 263 + MimeType=x-scheme-handler/http; 264 + `) 265 + 266 + if _, ok := Find("hidden.desktop"); !ok { 267 + t.Error("default Find should match a NoDisplay browser") 268 + } 269 + if got, ok := Find("hidden.desktop", IncludeNoDisplay(false)); ok { 270 + t.Errorf("Find with IncludeNoDisplay(false) returned %+v, want false", got) 271 + } 272 + } 273 + 219 274 func TestInstalledShadowingHiddenHomeSuppressesSystem(t *testing.T) { 220 275 home, system := isolatedDirs(t) 221 276 // A hidden copy in data-home shadows a valid copy in the system dir. 222 277 writeDesktop(t, home, "browser.desktop", `[Desktop Entry] 223 278 Type=Application 224 279 Name=Home Browser 225 - NoDisplay=true 280 + Hidden=true 226 281 MimeType=x-scheme-handler/http; 227 282 `) 228 283 writeDesktop(t, system, "browser.desktop", browserEntry) ··· 339 394 writeDesktop(t, home, "browser.desktop", `[Desktop Entry] 340 395 Type=Application 341 396 Name=Hidden Home Browser 342 - NoDisplay=true 397 + Hidden=true 343 398 MimeType=x-scheme-handler/http; 344 399 `) 345 400 writeDesktop(t, system, "browser.desktop", browserEntry)
+28
internal/browserscan/common.go
··· 13 13 // A browser switcher must never list itself, across all packaged variants. 14 14 const selfIDPrefix = "io.github.alyraffauf.Switchyard" 15 15 16 + // An Option configures how the scanner treats .desktop entries. Pass Options to 17 + // Installed and Find to override the defaults. 18 + type Option func(*scanOptions) 19 + 20 + // IncludeNoDisplay controls whether entries with NoDisplay=true are returned. It 21 + // defaults to true. 22 + func IncludeNoDisplay(include bool) Option { 23 + return func(options *scanOptions) { options.includeNoDisplay = include } 24 + } 25 + 26 + // scanOptions holds the parser behavior toggles set via Option values. 27 + type scanOptions struct { 28 + includeNoDisplay bool 29 + } 30 + 31 + // this package can opt out with IncludeNoDisplay(false). 32 + func defaultScanOptions() scanOptions { 33 + return scanOptions{includeNoDisplay: true} 34 + } 35 + 36 + func newScanOptions(opts []Option) scanOptions { 37 + options := defaultScanOptions() 38 + for _, opt := range opts { 39 + opt(&options) 40 + } 41 + return options 42 + } 43 + 16 44 // isSelf reports whether id belongs to Switchyard itself. 17 45 // The id is a desktop file ID on Linux and a bundle identifier on macOS. 18 46 func isSelf(id string) bool {