···28282929// Installed returns the installed HTTP(S) browsers, sorted by Name. Switchyard's
3030// own entries are excluded; filtering config-hidden browsers is the caller's job.
3131-func Installed() []browser.Browser {
3131+// By default NoDisplay=true browsers are included; pass IncludeNoDisplay(false) to
3232+// hide them.
3333+func Installed(opts ...Option) []browser.Browser {
3434+ options := newScanOptions(opts)
3535+3236 var browsers []browser.Browser
3337 seen := map[string]bool{}
3438···5761 return nil
5862 }
59636060- if parsed, ok := parseBrowser(id, path); ok {
6464+ if parsed, ok := parseBrowser(id, path, options); ok {
6165 browsers = append(browsers, parsed)
6266 }
6367 return nil
···6973 return browsers
7074}
71757272-// Find returns a displayable HTTP(S) browser by desktop file ID.
7373-func Find(id string) (browser.Browser, bool) {
7676+// Find returns a displayable HTTP(S) browser by desktop file ID. By default a
7777+// NoDisplay=true entry still matches; pass IncludeNoDisplay(false) to reject it.
7878+func Find(id string, opts ...Option) (browser.Browser, bool) {
7479 if isSelf(id) {
7580 return browser.Browser{}, false
7681 }
77828383+ options := newScanOptions(opts)
8484+7885 for _, dir := range applicationsDirs() {
7986 path, ok := desktopFilePath(dir, id)
8087 if !ok {
8188 continue
8289 }
8383- return parseBrowser(id, path)
9090+ return parseBrowser(id, path, options)
8491 }
85928693 return browser.Browser{}, false
···131138 return strings.ReplaceAll(filepath.ToSlash(rel), "/", "-")
132139}
133140134134-// parseBrowser returns the Browser at path, or ok=false if it isn't a
135135-// displayable HTTP(S) browser.
136136-func parseBrowser(id, path string) (browser.Browser, bool) {
141141+// parseBrowser returns the Browser at path, or ok=false if it isn't an HTTP(S)
142142+// browser the given options accept.
143143+func parseBrowser(id, path string, options scanOptions) (browser.Browser, bool) {
137144 file, err := desktopfile.Read(path)
138145 if err != nil {
139146 return browser.Browser{}, false
···142149 if typ, _ := file.Get(desktopfile.EntrySection, "Type"); typ != "Application" {
143150 return browser.Browser{}, false
144151 }
145145- if isTrue(file, "Hidden") || isTrue(file, "NoDisplay") {
152152+ if isTrue(file, "Hidden") {
153153+ return browser.Browser{}, false
154154+ }
155155+ if !options.includeNoDisplay && isTrue(file, "NoDisplay") {
146156 return browser.Browser{}, false
147157 }
148158 if !handlesHTTP(file) {
+6-3
internal/browserscan/browserscan_darwin.go
···70707171// Installed returns the installed HTTP(S) browsers, sorted by Name. Switchyard's
7272// own entries are excluded; filtering config-hidden browsers is the caller's job.
7373-func Installed() []browser.Browser {
7373+// Options are accepted for cross-platform API parity; IncludeNoDisplay is a no-op
7474+// on macOS, where browsers come from LaunchServices rather than .desktop files.
7575+func Installed(opts ...Option) []browser.Browser {
7476 browsers := scanBrowsers()
7577 sortByName(browsers)
76787779 return browsers
7880}
79818080-// Find returns a displayable HTTP(S) browser by bundle identifier.
8181-func Find(id string) (browser.Browser, bool) {
8282+// Find returns a displayable HTTP(S) browser by bundle identifier. Options are
8383+// accepted for cross-platform API parity but have no effect on macOS.
8484+func Find(id string, opts ...Option) (browser.Browser, bool) {
8285 if isSelf(id) {
8386 return browser.Browser{}, false
8487 }
+66-11
internal/browserscan/browserscan_test.go
···132132`,
133133 },
134134 {
135135- name: "nodisplay",
136136- contents: `[Desktop Entry]
137137-Type=Application
138138-Name=Hidden Browser
139139-NoDisplay=true
140140-MimeType=x-scheme-handler/http;
141141-`,
142142- },
143143- {
144135 name: "hidden",
145136 contents: `[Desktop Entry]
146137Type=Application
···216207 }
217208}
218209210210+// TestInstalledIncludesNoDisplay guards against regressing the fix for #10:
211211+// users deliberately set NoDisplay=true on custom per-profile browser entries
212212+// to hide them from launchers while still wanting Switchyard to detect them.
213213+func TestInstalledIncludesNoDisplay(t *testing.T) {
214214+ home, _ := isolatedDirs(t)
215215+ writeDesktop(t, home, "browser.desktop", `[Desktop Entry]
216216+Type=Application
217217+Name=NoDisplay Browser
218218+NoDisplay=true
219219+MimeType=x-scheme-handler/http;
220220+`)
221221+222222+ got := Installed()
223223+ if len(got) != 1 {
224224+ t.Fatalf("got %d browsers, want 1: %v", len(got), ids(got))
225225+ }
226226+ if got[0].Name != "NoDisplay Browser" {
227227+ t.Errorf("got %q, want %q", got[0].Name, "NoDisplay Browser")
228228+ }
229229+}
230230+231231+// TestInstalledIncludeNoDisplayFalse covers the opt-in launcher-style behavior
232232+// for other consumers of the parser: NoDisplay=true entries are excluded while
233233+// ordinary browsers still come through.
234234+func TestInstalledIncludeNoDisplayFalse(t *testing.T) {
235235+ home, _ := isolatedDirs(t)
236236+ writeDesktop(t, home, "hidden.desktop", `[Desktop Entry]
237237+Type=Application
238238+Name=NoDisplay Browser
239239+NoDisplay=true
240240+MimeType=x-scheme-handler/http;
241241+`)
242242+ writeDesktop(t, home, "visible.desktop", `[Desktop Entry]
243243+Type=Application
244244+Name=Visible Browser
245245+MimeType=x-scheme-handler/http;
246246+`)
247247+248248+ got := ids(Installed(IncludeNoDisplay(false)))
249249+ want := []string{"visible.desktop"}
250250+ if len(got) != len(want) || got[0] != want[0] {
251251+ t.Errorf("got %v, want %v", got, want)
252252+ }
253253+}
254254+255255+// TestFindIncludeNoDisplayFalse rejects a NoDisplay entry only when the caller
256256+// opts out; the default still matches it.
257257+func TestFindIncludeNoDisplayFalse(t *testing.T) {
258258+ home, _ := isolatedDirs(t)
259259+ writeDesktop(t, home, "hidden.desktop", `[Desktop Entry]
260260+Type=Application
261261+Name=NoDisplay Browser
262262+NoDisplay=true
263263+MimeType=x-scheme-handler/http;
264264+`)
265265+266266+ if _, ok := Find("hidden.desktop"); !ok {
267267+ t.Error("default Find should match a NoDisplay browser")
268268+ }
269269+ if got, ok := Find("hidden.desktop", IncludeNoDisplay(false)); ok {
270270+ t.Errorf("Find with IncludeNoDisplay(false) returned %+v, want false", got)
271271+ }
272272+}
273273+219274func TestInstalledShadowingHiddenHomeSuppressesSystem(t *testing.T) {
220275 home, system := isolatedDirs(t)
221276 // A hidden copy in data-home shadows a valid copy in the system dir.
222277 writeDesktop(t, home, "browser.desktop", `[Desktop Entry]
223278Type=Application
224279Name=Home Browser
225225-NoDisplay=true
280280+Hidden=true
226281MimeType=x-scheme-handler/http;
227282`)
228283 writeDesktop(t, system, "browser.desktop", browserEntry)
···339394 writeDesktop(t, home, "browser.desktop", `[Desktop Entry]
340395Type=Application
341396Name=Hidden Home Browser
342342-NoDisplay=true
397397+Hidden=true
343398MimeType=x-scheme-handler/http;
344399`)
345400 writeDesktop(t, system, "browser.desktop", browserEntry)
+28
internal/browserscan/common.go
···1313// A browser switcher must never list itself, across all packaged variants.
1414const selfIDPrefix = "io.github.alyraffauf.Switchyard"
15151616+// An Option configures how the scanner treats .desktop entries. Pass Options to
1717+// Installed and Find to override the defaults.
1818+type Option func(*scanOptions)
1919+2020+// IncludeNoDisplay controls whether entries with NoDisplay=true are returned. It
2121+// defaults to true.
2222+func IncludeNoDisplay(include bool) Option {
2323+ return func(options *scanOptions) { options.includeNoDisplay = include }
2424+}
2525+2626+// scanOptions holds the parser behavior toggles set via Option values.
2727+type scanOptions struct {
2828+ includeNoDisplay bool
2929+}
3030+3131+// this package can opt out with IncludeNoDisplay(false).
3232+func defaultScanOptions() scanOptions {
3333+ return scanOptions{includeNoDisplay: true}
3434+}
3535+3636+func newScanOptions(opts []Option) scanOptions {
3737+ options := defaultScanOptions()
3838+ for _, opt := range opts {
3939+ opt(&options)
4040+ }
4141+ return options
4242+}
4343+1644// isSelf reports whether id belongs to Switchyard itself.
1745// The id is a desktop file ID on Linux and a bundle identifier on macOS.
1846func isSelf(id string) bool {