Skip to main content

browser_locations_core/
lib.rs

1//! Shared browser discovery types and lookup functions.
2//!
3//! This crate contains the data-driven lookup engine used by the browser-specific
4//! crates and by the umbrella `browser-locations` crate.
5
6use std::collections::{BTreeMap, BTreeSet};
7use std::env;
8use std::ffi::OsString;
9use std::fmt;
10use std::path::{Path, PathBuf};
11
12use thiserror::Error;
13
14/// Known desktop browsers supported by this workspace.
15#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
16pub enum Browser {
17    /// Arc Browser.
18    Arc,
19    /// Brave Browser.
20    Brave,
21    /// Google Chrome.
22    Chrome,
23    /// Chromium.
24    Chromium,
25    /// Microsoft Edge.
26    Edge,
27    /// Mozilla Firefox.
28    Firefox,
29    /// Floorp.
30    Floorp,
31    /// Helium.
32    Helium,
33    /// LibreWolf.
34    LibreWolf,
35    /// Opera.
36    Opera,
37    /// Vivaldi.
38    Vivaldi,
39    /// Zen Browser.
40    Zen,
41}
42
43impl Browser {
44    /// Every browser supported by the workspace.
45    pub const ALL: [Self; 12] = [
46        Self::Arc,
47        Self::Brave,
48        Self::Chrome,
49        Self::Chromium,
50        Self::Edge,
51        Self::Firefox,
52        Self::Floorp,
53        Self::Helium,
54        Self::LibreWolf,
55        Self::Opera,
56        Self::Vivaldi,
57        Self::Zen,
58    ];
59
60    const fn env_key(self) -> &'static str {
61        match self {
62            Self::Arc => "ARC",
63            Self::Brave => "BRAVE",
64            Self::Chrome => "CHROME",
65            Self::Chromium => "CHROMIUM",
66            Self::Edge => "EDGE",
67            Self::Firefox => "FIREFOX",
68            Self::Floorp => "FLOORP",
69            Self::Helium => "HELIUM",
70            Self::LibreWolf => "LIBREWOLF",
71            Self::Opera => "OPERA",
72            Self::Vivaldi => "VIVALDI",
73            Self::Zen => "ZEN",
74        }
75    }
76
77    const fn display_name(self) -> &'static str {
78        match self {
79            Self::Arc => "Arc",
80            Self::Brave => "Brave",
81            Self::Chrome => "Chrome",
82            Self::Chromium => "Chromium",
83            Self::Edge => "Edge",
84            Self::Firefox => "Firefox",
85            Self::Floorp => "Floorp",
86            Self::Helium => "Helium",
87            Self::LibreWolf => "LibreWolf",
88            Self::Opera => "Opera",
89            Self::Vivaldi => "Vivaldi",
90            Self::Zen => "Zen",
91        }
92    }
93}
94
95impl fmt::Display for Browser {
96    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
97        formatter.write_str(self.display_name())
98    }
99}
100
101/// Known release channels exposed by the workspace.
102#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
103pub enum ReleaseChannel {
104    /// Default install without a vendor-defined channel model.
105    Default,
106    /// Stable channel.
107    Stable,
108    /// Beta channel.
109    Beta,
110    /// Dev channel.
111    Dev,
112    /// Canary channel.
113    Canary,
114    /// Nightly channel.
115    Nightly,
116    /// Firefox ESR channel.
117    Esr,
118    /// Firefox Developer Edition channel.
119    DeveloperEdition,
120    /// Vivaldi Snapshot channel.
121    Snapshot,
122    /// Zen Twilight channel.
123    Twilight,
124}
125
126impl ReleaseChannel {
127    const fn env_key(self) -> &'static str {
128        match self {
129            Self::Default => "DEFAULT",
130            Self::Stable => "STABLE",
131            Self::Beta => "BETA",
132            Self::Dev => "DEV",
133            Self::Canary => "CANARY",
134            Self::Nightly => "NIGHTLY",
135            Self::Esr => "ESR",
136            Self::DeveloperEdition => "DEVELOPER_EDITION",
137            Self::Snapshot => "SNAPSHOT",
138            Self::Twilight => "TWILIGHT",
139        }
140    }
141
142    const fn display_name(self) -> &'static str {
143        match self {
144            Self::Default => "default",
145            Self::Stable => "stable",
146            Self::Beta => "beta",
147            Self::Dev => "dev",
148            Self::Canary => "canary",
149            Self::Nightly => "nightly",
150            Self::Esr => "esr",
151            Self::DeveloperEdition => "developer-edition",
152            Self::Snapshot => "snapshot",
153            Self::Twilight => "twilight",
154        }
155    }
156}
157
158impl fmt::Display for ReleaseChannel {
159    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
160        formatter.write_str(self.display_name())
161    }
162}
163
164/// Supported host platforms.
165#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
166pub enum Platform {
167    /// macOS.
168    Macos,
169    /// Windows.
170    Windows,
171    /// Linux.
172    Linux,
173}
174
175impl Platform {
176    /// Returns the platform for the current build target.
177    #[must_use]
178    pub const fn current() -> Self {
179        #[cfg(target_os = "macos")]
180        {
181            Self::Macos
182        }
183        #[cfg(target_os = "windows")]
184        {
185            Self::Windows
186        }
187        #[cfg(all(not(target_os = "macos"), not(target_os = "windows")))]
188        {
189            Self::Linux
190        }
191    }
192}
193
194impl fmt::Display for Platform {
195    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
196        formatter.write_str(match self {
197            Self::Macos => "macOS",
198            Self::Windows => "Windows",
199            Self::Linux => "Linux",
200        })
201    }
202}
203
204/// Where a browser executable path was resolved from.
205#[derive(Clone, Copy, Debug, Eq, PartialEq)]
206pub enum ProbeSource {
207    /// An explicit override environment variable.
208    Override,
209    /// A well-known installation path.
210    KnownLocation,
211    /// A PATH lookup candidate.
212    PathLookup,
213    /// A Flatpak export path.
214    Flatpak,
215    /// A Snap export path.
216    Snap,
217}
218
219/// A discovered browser executable.
220#[derive(Clone, Debug, Eq, PartialEq)]
221pub struct BrowserLocation {
222    /// Browser identity.
223    pub browser: Browser,
224    /// Release channel identity.
225    pub channel: ReleaseChannel,
226    /// Fully qualified path to the executable.
227    pub path: PathBuf,
228    /// Platform on which the lookup ran.
229    pub platform: Platform,
230    /// Discovery source used for the final match.
231    pub source: ProbeSource,
232}
233
234/// Errors returned by browser discovery functions.
235#[derive(Debug, Error)]
236pub enum LocateError {
237    /// The requested channel is not modeled for the selected browser.
238    #[error("{browser} does not model the {channel} channel")]
239    UnsupportedChannel {
240        /// Browser that was queried.
241        browser: Browser,
242        /// Requested release channel.
243        channel: ReleaseChannel,
244    },
245    /// The requested browser/channel combination is not supported on this platform.
246    #[error("{browser} {channel} is not supported on {platform}")]
247    UnsupportedPlatform {
248        /// Browser that was queried.
249        browser: Browser,
250        /// Requested release channel.
251        channel: ReleaseChannel,
252        /// Host platform.
253        platform: Platform,
254    },
255    /// No executable was found for a direct channel lookup.
256    #[error("unable to find {browser} {channel} on {platform}")]
257    NotFound {
258        /// Browser that was queried.
259        browser: Browser,
260        /// Requested release channel.
261        channel: ReleaseChannel,
262        /// Host platform.
263        platform: Platform,
264    },
265    /// No executable was found while evaluating a fallback strategy.
266    #[error("unable to find any {browser} browser for the {strategy} strategy on {platform}")]
267    NoInstalledVariant {
268        /// Browser that was queried.
269        browser: Browser,
270        /// Fallback strategy label.
271        strategy: &'static str,
272        /// Host platform.
273        platform: Platform,
274    },
275}
276
277#[derive(Clone, Copy)]
278enum CandidateKind {
279    KnownLocation,
280    PathLookup,
281    Flatpak,
282    Snap,
283}
284
285#[derive(Clone, Copy)]
286struct Candidate {
287    kind: CandidateKind,
288    value: &'static str,
289}
290
291#[derive(Clone, Copy)]
292struct ChannelDefinition {
293    channel: ReleaseChannel,
294    macos: &'static [Candidate],
295    windows: &'static [Candidate],
296    linux: &'static [Candidate],
297}
298
299impl ChannelDefinition {
300    const fn candidates_for(self, platform: Platform) -> &'static [Candidate] {
301        match platform {
302            Platform::Macos => self.macos,
303            Platform::Windows => self.windows,
304            Platform::Linux => self.linux,
305        }
306    }
307}
308
309#[derive(Clone, Copy)]
310struct BrowserDefinition {
311    channels: &'static [ChannelDefinition],
312    stable_order: &'static [ReleaseChannel],
313    latest_order: &'static [ReleaseChannel],
314}
315
316trait Environment {
317    fn current_platform(&self) -> Platform;
318    fn get_var(&self, key: &str) -> Option<OsString>;
319    fn path_exists(&self, path: &Path) -> bool;
320}
321
322struct SystemEnvironment;
323
324impl Environment for SystemEnvironment {
325    fn current_platform(&self) -> Platform {
326        Platform::current()
327    }
328
329    fn get_var(&self, key: &str) -> Option<OsString> {
330        env::var_os(key)
331    }
332
333    fn path_exists(&self, path: &Path) -> bool {
334        path.exists()
335    }
336}
337
338const fn candidate(kind: CandidateKind, value: &'static str) -> Candidate {
339    Candidate { kind, value }
340}
341
342const fn channel(
343    channel: ReleaseChannel,
344    macos: &'static [Candidate],
345    windows: &'static [Candidate],
346    linux: &'static [Candidate],
347) -> ChannelDefinition {
348    ChannelDefinition {
349        channel,
350        macos,
351        windows,
352        linux,
353    }
354}
355
356const fn browser(
357    _browser: Browser,
358    channels: &'static [ChannelDefinition],
359    stable_order: &'static [ReleaseChannel],
360    latest_order: &'static [ReleaseChannel],
361) -> BrowserDefinition {
362    BrowserDefinition {
363        channels,
364        stable_order,
365        latest_order,
366    }
367}
368
369/// Locates a browser executable for a specific browser and release channel.
370///
371/// Checks environment variable overrides first, then probes well-known
372/// installation paths, and finally falls back to `PATH` lookup.
373///
374/// # Examples
375///
376/// ```no_run
377/// use browser_locations_core::{Browser, ReleaseChannel, locate_browser};
378///
379/// let location = locate_browser(Browser::Chrome, ReleaseChannel::Stable)?;
380/// println!("Chrome found at: {}", location.path.display());
381/// # Ok::<(), browser_locations_core::LocateError>(())
382/// ```
383///
384/// # Errors
385///
386/// - [`LocateError::UnsupportedChannel`] if the channel is not modeled for the browser.
387/// - [`LocateError::UnsupportedPlatform`] if no candidates exist on this platform.
388/// - [`LocateError::NotFound`] if no executable was found at any candidate path.
389pub fn locate_browser(
390    browser: Browser,
391    channel: ReleaseChannel,
392) -> Result<BrowserLocation, LocateError> {
393    locate_browser_in_environment(browser, channel, &SystemEnvironment)
394}
395
396/// Locates a browser executable using the browser's stable-first fallback order.
397///
398/// # Examples
399///
400/// ```no_run
401/// use browser_locations_core::{Browser, locate_any_stable};
402///
403/// let location = locate_any_stable(Browser::Firefox)?;
404/// println!("Firefox found at: {}", location.path.display());
405/// # Ok::<(), browser_locations_core::LocateError>(())
406/// ```
407///
408/// # Errors
409///
410/// Returns [`LocateError::NoInstalledVariant`] if no channel produced a match.
411pub fn locate_any_stable(browser: Browser) -> Result<BrowserLocation, LocateError> {
412    locate_with_fallback(
413        browser,
414        definition(browser).stable_order,
415        "stable",
416        &SystemEnvironment,
417    )
418}
419
420/// Locates a browser executable using the browser's latest-first fallback order.
421///
422/// # Examples
423///
424/// ```no_run
425/// use browser_locations_core::{Browser, locate_any_latest};
426///
427/// let location = locate_any_latest(Browser::Chrome)?;
428/// println!("Chrome found at: {}", location.path.display());
429/// # Ok::<(), browser_locations_core::LocateError>(())
430/// ```
431///
432/// # Errors
433///
434/// Returns [`LocateError::NoInstalledVariant`] if no channel produced a match.
435pub fn locate_any_latest(browser: Browser) -> Result<BrowserLocation, LocateError> {
436    locate_with_fallback(
437        browser,
438        definition(browser).latest_order,
439        "latest",
440        &SystemEnvironment,
441    )
442}
443
444/// Discovers every installed executable modeled for a specific browser.
445///
446/// # Examples
447///
448/// ```no_run
449/// use browser_locations_core::{Browser, discover_browser};
450///
451/// for location in &discover_browser(Browser::Chrome) {
452///     println!("{} at {}", location.channel, location.path.display());
453/// }
454/// ```
455#[must_use]
456pub fn discover_browser(browser: Browser) -> Vec<BrowserLocation> {
457    discover_browser_in_environment(browser, &SystemEnvironment)
458}
459
460/// Discovers every installed executable for every modeled browser.
461///
462/// # Examples
463///
464/// ```no_run
465/// use browser_locations_core::discover_installed;
466///
467/// for location in &discover_installed() {
468///     println!("{} {} at {}", location.browser, location.channel, location.path.display());
469/// }
470/// ```
471#[must_use]
472pub fn discover_installed() -> Vec<BrowserLocation> {
473    let environment = SystemEnvironment;
474    Browser::ALL
475        .into_iter()
476        .flat_map(|browser| discover_browser_in_environment(browser, &environment))
477        .collect()
478}
479
480/// Defines a channel-specific getter that returns the executable path.
481#[macro_export]
482macro_rules! define_getter {
483    ($name:ident, $channel:expr, $doc:literal) => {
484        #[doc = $doc]
485        ///
486        /// # Errors
487        ///
488        /// Returns [`LocateError`] if the browser is not found for this channel.
489        pub fn $name() -> ::std::result::Result<::std::path::PathBuf, $crate::LocateError> {
490            locate($channel).map(|location| location.path)
491        }
492    };
493}
494
495fn locate_browser_in_environment<E: Environment>(
496    browser: Browser,
497    channel: ReleaseChannel,
498    environment: &E,
499) -> Result<BrowserLocation, LocateError> {
500    let definition = definition(browser);
501    let platform = environment.current_platform();
502    let Some(channel_definition) = definition
503        .channels
504        .iter()
505        .find(|candidate| candidate.channel == channel)
506    else {
507        return Err(LocateError::UnsupportedChannel { browser, channel });
508    };
509    let override_key = format!(
510        "BROWSER_LOCATIONS_{}_{}_PATH",
511        browser.env_key(),
512        channel.env_key()
513    );
514    if let Some(path) = environment
515        .get_var(&override_key)
516        .map(PathBuf::from)
517        .filter(|path| environment.path_exists(path))
518    {
519        return Ok(BrowserLocation {
520            browser,
521            channel,
522            path,
523            platform,
524            source: ProbeSource::Override,
525        });
526    }
527    let candidates = channel_definition.candidates_for(platform);
528    if candidates.is_empty() {
529        return Err(LocateError::UnsupportedPlatform {
530            browser,
531            channel,
532            platform,
533        });
534    }
535    let mut seen = BTreeSet::new();
536    for candidate in candidates {
537        for resolved in resolve_candidate(*candidate, environment) {
538            if seen.insert(resolved.path.clone()) && environment.path_exists(&resolved.path) {
539                return Ok(BrowserLocation {
540                    browser,
541                    channel,
542                    path: resolved.path,
543                    platform,
544                    source: resolved.source,
545                });
546            }
547        }
548    }
549    Err(LocateError::NotFound {
550        browser,
551        channel,
552        platform,
553    })
554}
555
556fn locate_with_fallback<E: Environment>(
557    browser: Browser,
558    order: &[ReleaseChannel],
559    strategy: &'static str,
560    environment: &E,
561) -> Result<BrowserLocation, LocateError> {
562    let platform = environment.current_platform();
563    for channel in order {
564        if let Ok(location) = locate_browser_in_environment(browser, *channel, environment) {
565            return Ok(location);
566        }
567    }
568    Err(LocateError::NoInstalledVariant {
569        browser,
570        strategy,
571        platform,
572    })
573}
574
575fn discover_browser_in_environment<E: Environment>(
576    browser: Browser,
577    environment: &E,
578) -> Vec<BrowserLocation> {
579    definition(browser)
580        .channels
581        .iter()
582        .filter_map(|channel| {
583            locate_browser_in_environment(browser, channel.channel, environment).ok()
584        })
585        .collect()
586}
587
588struct ResolvedCandidate {
589    path: PathBuf,
590    source: ProbeSource,
591}
592
593fn resolve_candidate<E: Environment>(
594    candidate: Candidate,
595    environment: &E,
596) -> Vec<ResolvedCandidate> {
597    match candidate.kind {
598        CandidateKind::KnownLocation | CandidateKind::Flatpak | CandidateKind::Snap => {
599            expand_template(candidate.value, environment)
600                .into_iter()
601                .map(|path| ResolvedCandidate {
602                    path,
603                    source: match candidate.kind {
604                        CandidateKind::KnownLocation => ProbeSource::KnownLocation,
605                        CandidateKind::Flatpak => ProbeSource::Flatpak,
606                        CandidateKind::Snap => ProbeSource::Snap,
607                        CandidateKind::PathLookup => ProbeSource::PathLookup,
608                    },
609                })
610                .collect()
611        }
612        CandidateKind::PathLookup => environment
613            .get_var("PATH")
614            .map(|path| env::split_paths(&path).collect::<Vec<_>>())
615            .unwrap_or_default()
616            .into_iter()
617            .map(|entry| entry.join(candidate.value))
618            .map(|path| ResolvedCandidate {
619                path,
620                source: ProbeSource::PathLookup,
621            })
622            .collect(),
623    }
624}
625
626fn expand_template<E: Environment>(template: &str, environment: &E) -> Option<PathBuf> {
627    let replacements = placeholder_values(environment);
628    let mut resolved = template.to_owned();
629    for (placeholder, value) in replacements {
630        resolved = resolved.replace(placeholder, &value);
631    }
632    if resolved.contains('{') {
633        return None;
634    }
635    Some(PathBuf::from(resolved))
636}
637
638fn placeholder_values<E: Environment>(environment: &E) -> BTreeMap<&'static str, String> {
639    let mut values = BTreeMap::new();
640    for (placeholder, env_key) in [
641        ("{HOME}", "HOME"),
642        ("{LOCALAPPDATA}", "LOCALAPPDATA"),
643        ("{PROGRAMFILES}", "PROGRAMFILES"),
644        ("{PROGRAMFILES_X86}", "PROGRAMFILES(X86)"),
645        ("{USERPROFILE}", "USERPROFILE"),
646    ] {
647        if let Some(value) = environment.get_var(env_key) {
648            values.insert(placeholder, value.to_string_lossy().into_owned());
649        }
650    }
651    values
652}
653
654fn definition(browser: Browser) -> &'static BrowserDefinition {
655    match browser {
656        Browser::Arc => &ARC,
657        Browser::Brave => &BRAVE,
658        Browser::Chrome => &CHROME,
659        Browser::Chromium => &CHROMIUM,
660        Browser::Edge => &EDGE,
661        Browser::Firefox => &FIREFOX,
662        Browser::Floorp => &FLOORP,
663        Browser::Helium => &HELIUM,
664        Browser::LibreWolf => &LIBREWOLF,
665        Browser::Opera => &OPERA,
666        Browser::Vivaldi => &VIVALDI,
667        Browser::Zen => &ZEN,
668    }
669}
670
671const NONE: [Candidate; 0] = [];
672
673const DEFAULT_ONLY: [ReleaseChannel; 1] = [ReleaseChannel::Default];
674const SNAPSHOT_STABLE_ORDER: [ReleaseChannel; 2] =
675    [ReleaseChannel::Stable, ReleaseChannel::Snapshot];
676const SNAPSHOT_LATEST_ORDER: [ReleaseChannel; 2] =
677    [ReleaseChannel::Snapshot, ReleaseChannel::Stable];
678const TWILIGHT_STABLE_ORDER: [ReleaseChannel; 2] =
679    [ReleaseChannel::Stable, ReleaseChannel::Twilight];
680const TWILIGHT_LATEST_ORDER: [ReleaseChannel; 2] =
681    [ReleaseChannel::Twilight, ReleaseChannel::Stable];
682const BRAVE_STABLE_ORDER: [ReleaseChannel; 3] = [
683    ReleaseChannel::Stable,
684    ReleaseChannel::Beta,
685    ReleaseChannel::Nightly,
686];
687const BRAVE_LATEST_ORDER: [ReleaseChannel; 3] = [
688    ReleaseChannel::Nightly,
689    ReleaseChannel::Beta,
690    ReleaseChannel::Stable,
691];
692const OPERA_STABLE_ORDER: [ReleaseChannel; 3] = [
693    ReleaseChannel::Stable,
694    ReleaseChannel::Beta,
695    ReleaseChannel::Dev,
696];
697const OPERA_LATEST_ORDER: [ReleaseChannel; 3] = [
698    ReleaseChannel::Dev,
699    ReleaseChannel::Beta,
700    ReleaseChannel::Stable,
701];
702const CHROMIUM_FAMILY_STABLE_ORDER: [ReleaseChannel; 4] = [
703    ReleaseChannel::Stable,
704    ReleaseChannel::Beta,
705    ReleaseChannel::Dev,
706    ReleaseChannel::Canary,
707];
708const CHROMIUM_FAMILY_LATEST_ORDER: [ReleaseChannel; 4] = [
709    ReleaseChannel::Canary,
710    ReleaseChannel::Dev,
711    ReleaseChannel::Beta,
712    ReleaseChannel::Stable,
713];
714const FIREFOX_STABLE_ORDER: [ReleaseChannel; 5] = [
715    ReleaseChannel::Stable,
716    ReleaseChannel::Esr,
717    ReleaseChannel::Beta,
718    ReleaseChannel::DeveloperEdition,
719    ReleaseChannel::Nightly,
720];
721const FIREFOX_LATEST_ORDER: [ReleaseChannel; 5] = [
722    ReleaseChannel::Nightly,
723    ReleaseChannel::DeveloperEdition,
724    ReleaseChannel::Beta,
725    ReleaseChannel::Stable,
726    ReleaseChannel::Esr,
727];
728
729const CHROME_STABLE_MACOS: [Candidate; 1] = [candidate(
730    CandidateKind::KnownLocation,
731    "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
732)];
733const CHROME_STABLE_WINDOWS: [Candidate; 3] = [
734    candidate(
735        CandidateKind::KnownLocation,
736        "{PROGRAMFILES}\\Google\\Chrome\\Application\\chrome.exe",
737    ),
738    candidate(
739        CandidateKind::KnownLocation,
740        "{PROGRAMFILES_X86}\\Google\\Chrome\\Application\\chrome.exe",
741    ),
742    candidate(
743        CandidateKind::KnownLocation,
744        "{LOCALAPPDATA}\\Google\\Chrome\\Application\\chrome.exe",
745    ),
746];
747const CHROME_STABLE_LINUX: [Candidate; 6] = [
748    candidate(CandidateKind::KnownLocation, "/usr/bin/google-chrome"),
749    candidate(
750        CandidateKind::KnownLocation,
751        "/usr/bin/google-chrome-stable",
752    ),
753    candidate(CandidateKind::Snap, "/snap/bin/google-chrome"),
754    candidate(
755        CandidateKind::Flatpak,
756        "{HOME}/.local/share/flatpak/exports/bin/com.google.Chrome",
757    ),
758    candidate(
759        CandidateKind::Flatpak,
760        "/var/lib/flatpak/exports/bin/com.google.Chrome",
761    ),
762    candidate(CandidateKind::PathLookup, "google-chrome"),
763];
764const CHROME_BETA_MACOS: [Candidate; 1] = [candidate(
765    CandidateKind::KnownLocation,
766    "/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta",
767)];
768const CHROME_BETA_WINDOWS: [Candidate; 3] = [
769    candidate(
770        CandidateKind::KnownLocation,
771        "{PROGRAMFILES}\\Google\\Chrome Beta\\Application\\chrome.exe",
772    ),
773    candidate(
774        CandidateKind::KnownLocation,
775        "{PROGRAMFILES_X86}\\Google\\Chrome Beta\\Application\\chrome.exe",
776    ),
777    candidate(
778        CandidateKind::KnownLocation,
779        "{LOCALAPPDATA}\\Google\\Chrome Beta\\Application\\chrome.exe",
780    ),
781];
782const CHROME_BETA_LINUX: [Candidate; 2] = [
783    candidate(CandidateKind::KnownLocation, "/usr/bin/google-chrome-beta"),
784    candidate(CandidateKind::PathLookup, "google-chrome-beta"),
785];
786const CHROME_DEV_MACOS: [Candidate; 1] = [candidate(
787    CandidateKind::KnownLocation,
788    "/Applications/Google Chrome Dev.app/Contents/MacOS/Google Chrome Dev",
789)];
790const CHROME_DEV_WINDOWS: [Candidate; 1] = [candidate(
791    CandidateKind::KnownLocation,
792    "{LOCALAPPDATA}\\Google\\Chrome Dev\\Application\\chrome.exe",
793)];
794const CHROME_DEV_LINUX: [Candidate; 2] = [
795    candidate(
796        CandidateKind::KnownLocation,
797        "/usr/bin/google-chrome-unstable",
798    ),
799    candidate(CandidateKind::PathLookup, "google-chrome-unstable"),
800];
801const CHROME_CANARY_MACOS: [Candidate; 1] = [candidate(
802    CandidateKind::KnownLocation,
803    "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",
804)];
805const CHROME_CANARY_WINDOWS: [Candidate; 1] = [candidate(
806    CandidateKind::KnownLocation,
807    "{LOCALAPPDATA}\\Google\\Chrome SxS\\Application\\chrome.exe",
808)];
809const CHROME_CHANNELS: [ChannelDefinition; 4] = [
810    channel(
811        ReleaseChannel::Stable,
812        &CHROME_STABLE_MACOS,
813        &CHROME_STABLE_WINDOWS,
814        &CHROME_STABLE_LINUX,
815    ),
816    channel(
817        ReleaseChannel::Beta,
818        &CHROME_BETA_MACOS,
819        &CHROME_BETA_WINDOWS,
820        &CHROME_BETA_LINUX,
821    ),
822    channel(
823        ReleaseChannel::Dev,
824        &CHROME_DEV_MACOS,
825        &CHROME_DEV_WINDOWS,
826        &CHROME_DEV_LINUX,
827    ),
828    channel(
829        ReleaseChannel::Canary,
830        &CHROME_CANARY_MACOS,
831        &CHROME_CANARY_WINDOWS,
832        &NONE,
833    ),
834];
835
836const CHROMIUM_STABLE_MACOS: [Candidate; 1] = [candidate(
837    CandidateKind::KnownLocation,
838    "/Applications/Chromium.app/Contents/MacOS/Chromium",
839)];
840const CHROMIUM_STABLE_WINDOWS: [Candidate; 3] = [
841    candidate(
842        CandidateKind::KnownLocation,
843        "{LOCALAPPDATA}\\Chromium\\Application\\chrome.exe",
844    ),
845    candidate(
846        CandidateKind::KnownLocation,
847        "{PROGRAMFILES}\\Chromium\\Application\\chrome.exe",
848    ),
849    candidate(
850        CandidateKind::KnownLocation,
851        "{PROGRAMFILES_X86}\\Chromium\\Application\\chrome.exe",
852    ),
853];
854const CHROMIUM_STABLE_LINUX: [Candidate; 6] = [
855    candidate(CandidateKind::KnownLocation, "/usr/bin/chromium"),
856    candidate(CandidateKind::KnownLocation, "/usr/bin/chromium-browser"),
857    candidate(CandidateKind::Snap, "/snap/bin/chromium"),
858    candidate(
859        CandidateKind::Flatpak,
860        "{HOME}/.local/share/flatpak/exports/bin/org.chromium.Chromium",
861    ),
862    candidate(
863        CandidateKind::Flatpak,
864        "/var/lib/flatpak/exports/bin/org.chromium.Chromium",
865    ),
866    candidate(CandidateKind::PathLookup, "chromium"),
867];
868const CHROMIUM_CHANNELS: [ChannelDefinition; 1] = [channel(
869    ReleaseChannel::Default,
870    &CHROMIUM_STABLE_MACOS,
871    &CHROMIUM_STABLE_WINDOWS,
872    &CHROMIUM_STABLE_LINUX,
873)];
874
875const EDGE_STABLE_MACOS: [Candidate; 1] = [candidate(
876    CandidateKind::KnownLocation,
877    "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
878)];
879const EDGE_STABLE_WINDOWS: [Candidate; 3] = [
880    candidate(
881        CandidateKind::KnownLocation,
882        "{LOCALAPPDATA}\\Microsoft\\Edge\\Application\\msedge.exe",
883    ),
884    candidate(
885        CandidateKind::KnownLocation,
886        "{PROGRAMFILES}\\Microsoft\\Edge\\Application\\msedge.exe",
887    ),
888    candidate(
889        CandidateKind::KnownLocation,
890        "{PROGRAMFILES_X86}\\Microsoft\\Edge\\Application\\msedge.exe",
891    ),
892];
893const EDGE_STABLE_LINUX: [Candidate; 5] = [
894    candidate(CandidateKind::KnownLocation, "/usr/bin/microsoft-edge"),
895    candidate(
896        CandidateKind::KnownLocation,
897        "/usr/bin/microsoft-edge-stable",
898    ),
899    candidate(
900        CandidateKind::Flatpak,
901        "{HOME}/.local/share/flatpak/exports/bin/com.microsoft.Edge",
902    ),
903    candidate(
904        CandidateKind::Flatpak,
905        "/var/lib/flatpak/exports/bin/com.microsoft.Edge",
906    ),
907    candidate(CandidateKind::PathLookup, "microsoft-edge-stable"),
908];
909const EDGE_BETA_MACOS: [Candidate; 1] = [candidate(
910    CandidateKind::KnownLocation,
911    "/Applications/Microsoft Edge Beta.app/Contents/MacOS/Microsoft Edge Beta",
912)];
913const EDGE_BETA_WINDOWS: [Candidate; 3] = [
914    candidate(
915        CandidateKind::KnownLocation,
916        "{LOCALAPPDATA}\\Microsoft\\Edge Beta\\Application\\msedge.exe",
917    ),
918    candidate(
919        CandidateKind::KnownLocation,
920        "{PROGRAMFILES}\\Microsoft\\Edge Beta\\Application\\msedge.exe",
921    ),
922    candidate(
923        CandidateKind::KnownLocation,
924        "{PROGRAMFILES_X86}\\Microsoft\\Edge Beta\\Application\\msedge.exe",
925    ),
926];
927const EDGE_BETA_LINUX: [Candidate; 2] = [
928    candidate(CandidateKind::KnownLocation, "/usr/bin/microsoft-edge-beta"),
929    candidate(CandidateKind::PathLookup, "microsoft-edge-beta"),
930];
931const EDGE_DEV_MACOS: [Candidate; 1] = [candidate(
932    CandidateKind::KnownLocation,
933    "/Applications/Microsoft Edge Dev.app/Contents/MacOS/Microsoft Edge Dev",
934)];
935const EDGE_DEV_WINDOWS: [Candidate; 3] = [
936    candidate(
937        CandidateKind::KnownLocation,
938        "{LOCALAPPDATA}\\Microsoft\\Edge Dev\\Application\\msedge.exe",
939    ),
940    candidate(
941        CandidateKind::KnownLocation,
942        "{PROGRAMFILES}\\Microsoft\\Edge Dev\\Application\\msedge.exe",
943    ),
944    candidate(
945        CandidateKind::KnownLocation,
946        "{PROGRAMFILES_X86}\\Microsoft\\Edge Dev\\Application\\msedge.exe",
947    ),
948];
949const EDGE_DEV_LINUX: [Candidate; 2] = [
950    candidate(CandidateKind::KnownLocation, "/usr/bin/microsoft-edge-dev"),
951    candidate(CandidateKind::PathLookup, "microsoft-edge-dev"),
952];
953const EDGE_CANARY_MACOS: [Candidate; 1] = [candidate(
954    CandidateKind::KnownLocation,
955    "/Applications/Microsoft Edge Canary.app/Contents/MacOS/Microsoft Edge Canary",
956)];
957const EDGE_CANARY_WINDOWS: [Candidate; 1] = [candidate(
958    CandidateKind::KnownLocation,
959    "{LOCALAPPDATA}\\Microsoft\\Edge SxS\\Application\\msedge.exe",
960)];
961const EDGE_CHANNELS: [ChannelDefinition; 4] = [
962    channel(
963        ReleaseChannel::Stable,
964        &EDGE_STABLE_MACOS,
965        &EDGE_STABLE_WINDOWS,
966        &EDGE_STABLE_LINUX,
967    ),
968    channel(
969        ReleaseChannel::Beta,
970        &EDGE_BETA_MACOS,
971        &EDGE_BETA_WINDOWS,
972        &EDGE_BETA_LINUX,
973    ),
974    channel(
975        ReleaseChannel::Dev,
976        &EDGE_DEV_MACOS,
977        &EDGE_DEV_WINDOWS,
978        &EDGE_DEV_LINUX,
979    ),
980    channel(
981        ReleaseChannel::Canary,
982        &EDGE_CANARY_MACOS,
983        &EDGE_CANARY_WINDOWS,
984        &NONE,
985    ),
986];
987
988const FIREFOX_STABLE_MACOS: [Candidate; 1] = [candidate(
989    CandidateKind::KnownLocation,
990    "/Applications/Firefox.app/Contents/MacOS/firefox",
991)];
992const FIREFOX_STABLE_WINDOWS: [Candidate; 3] = [
993    candidate(
994        CandidateKind::KnownLocation,
995        "{PROGRAMFILES}\\Mozilla Firefox\\firefox.exe",
996    ),
997    candidate(
998        CandidateKind::KnownLocation,
999        "{PROGRAMFILES_X86}\\Mozilla Firefox\\firefox.exe",
1000    ),
1001    candidate(
1002        CandidateKind::KnownLocation,
1003        "{LOCALAPPDATA}\\Mozilla Firefox\\firefox.exe",
1004    ),
1005];
1006const FIREFOX_STABLE_LINUX: [Candidate; 5] = [
1007    candidate(CandidateKind::KnownLocation, "/usr/bin/firefox"),
1008    candidate(CandidateKind::Snap, "/snap/bin/firefox"),
1009    candidate(
1010        CandidateKind::Flatpak,
1011        "{HOME}/.local/share/flatpak/exports/bin/org.mozilla.firefox",
1012    ),
1013    candidate(
1014        CandidateKind::Flatpak,
1015        "/var/lib/flatpak/exports/bin/org.mozilla.firefox",
1016    ),
1017    candidate(CandidateKind::PathLookup, "firefox"),
1018];
1019const FIREFOX_BETA_MACOS: [Candidate; 1] = [candidate(
1020    CandidateKind::KnownLocation,
1021    "/Applications/Firefox Beta.app/Contents/MacOS/firefox",
1022)];
1023const FIREFOX_BETA_WINDOWS: [Candidate; 1] = [candidate(
1024    CandidateKind::KnownLocation,
1025    "{PROGRAMFILES}\\Firefox Beta\\firefox.exe",
1026)];
1027const FIREFOX_BETA_LINUX: [Candidate; 2] = [
1028    candidate(CandidateKind::KnownLocation, "/usr/bin/firefox-beta"),
1029    candidate(CandidateKind::PathLookup, "firefox-beta"),
1030];
1031const FIREFOX_DEV_MACOS: [Candidate; 1] = [candidate(
1032    CandidateKind::KnownLocation,
1033    "/Applications/Firefox Developer Edition.app/Contents/MacOS/firefox",
1034)];
1035const FIREFOX_DEV_WINDOWS: [Candidate; 1] = [candidate(
1036    CandidateKind::KnownLocation,
1037    "{PROGRAMFILES}\\Firefox Developer Edition\\firefox.exe",
1038)];
1039const FIREFOX_DEV_LINUX: [Candidate; 2] = [
1040    candidate(
1041        CandidateKind::KnownLocation,
1042        "/usr/bin/firefox-developer-edition",
1043    ),
1044    candidate(CandidateKind::PathLookup, "firefox-developer-edition"),
1045];
1046const FIREFOX_NIGHTLY_MACOS: [Candidate; 1] = [candidate(
1047    CandidateKind::KnownLocation,
1048    "/Applications/Firefox Nightly.app/Contents/MacOS/firefox",
1049)];
1050const FIREFOX_NIGHTLY_WINDOWS: [Candidate; 1] = [candidate(
1051    CandidateKind::KnownLocation,
1052    "{PROGRAMFILES}\\Firefox Nightly\\firefox.exe",
1053)];
1054const FIREFOX_NIGHTLY_LINUX: [Candidate; 2] = [
1055    candidate(CandidateKind::KnownLocation, "/usr/bin/firefox-nightly"),
1056    candidate(CandidateKind::PathLookup, "firefox-nightly"),
1057];
1058const FIREFOX_ESR_MACOS: [Candidate; 1] = [candidate(
1059    CandidateKind::KnownLocation,
1060    "/Applications/Firefox ESR.app/Contents/MacOS/firefox",
1061)];
1062const FIREFOX_ESR_WINDOWS: [Candidate; 1] = [candidate(
1063    CandidateKind::KnownLocation,
1064    "{PROGRAMFILES}\\Mozilla Firefox ESR\\firefox.exe",
1065)];
1066const FIREFOX_ESR_LINUX: [Candidate; 2] = [
1067    candidate(CandidateKind::KnownLocation, "/usr/bin/firefox-esr"),
1068    candidate(CandidateKind::PathLookup, "firefox-esr"),
1069];
1070const FIREFOX_CHANNELS: [ChannelDefinition; 5] = [
1071    channel(
1072        ReleaseChannel::Stable,
1073        &FIREFOX_STABLE_MACOS,
1074        &FIREFOX_STABLE_WINDOWS,
1075        &FIREFOX_STABLE_LINUX,
1076    ),
1077    channel(
1078        ReleaseChannel::Beta,
1079        &FIREFOX_BETA_MACOS,
1080        &FIREFOX_BETA_WINDOWS,
1081        &FIREFOX_BETA_LINUX,
1082    ),
1083    channel(
1084        ReleaseChannel::DeveloperEdition,
1085        &FIREFOX_DEV_MACOS,
1086        &FIREFOX_DEV_WINDOWS,
1087        &FIREFOX_DEV_LINUX,
1088    ),
1089    channel(
1090        ReleaseChannel::Nightly,
1091        &FIREFOX_NIGHTLY_MACOS,
1092        &FIREFOX_NIGHTLY_WINDOWS,
1093        &FIREFOX_NIGHTLY_LINUX,
1094    ),
1095    channel(
1096        ReleaseChannel::Esr,
1097        &FIREFOX_ESR_MACOS,
1098        &FIREFOX_ESR_WINDOWS,
1099        &FIREFOX_ESR_LINUX,
1100    ),
1101];
1102
1103const BRAVE_STABLE_MACOS: [Candidate; 1] = [candidate(
1104    CandidateKind::KnownLocation,
1105    "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
1106)];
1107const BRAVE_STABLE_WINDOWS: [Candidate; 3] = [
1108    candidate(
1109        CandidateKind::KnownLocation,
1110        "{LOCALAPPDATA}\\BraveSoftware\\Brave-Browser\\Application\\brave.exe",
1111    ),
1112    candidate(
1113        CandidateKind::KnownLocation,
1114        "{PROGRAMFILES}\\BraveSoftware\\Brave-Browser\\Application\\brave.exe",
1115    ),
1116    candidate(
1117        CandidateKind::KnownLocation,
1118        "{PROGRAMFILES_X86}\\BraveSoftware\\Brave-Browser\\Application\\brave.exe",
1119    ),
1120];
1121const BRAVE_STABLE_LINUX: [Candidate; 4] = [
1122    candidate(CandidateKind::KnownLocation, "/usr/bin/brave-browser"),
1123    candidate(
1124        CandidateKind::Flatpak,
1125        "{HOME}/.local/share/flatpak/exports/bin/com.brave.Browser",
1126    ),
1127    candidate(
1128        CandidateKind::Flatpak,
1129        "/var/lib/flatpak/exports/bin/com.brave.Browser",
1130    ),
1131    candidate(CandidateKind::PathLookup, "brave-browser"),
1132];
1133const BRAVE_BETA_MACOS: [Candidate; 1] = [candidate(
1134    CandidateKind::KnownLocation,
1135    "/Applications/Brave Browser Beta.app/Contents/MacOS/Brave Browser Beta",
1136)];
1137const BRAVE_BETA_WINDOWS: [Candidate; 1] = [candidate(
1138    CandidateKind::KnownLocation,
1139    "{LOCALAPPDATA}\\BraveSoftware\\Brave-Browser-Beta\\Application\\brave.exe",
1140)];
1141const BRAVE_BETA_LINUX: [Candidate; 2] = [
1142    candidate(CandidateKind::KnownLocation, "/usr/bin/brave-browser-beta"),
1143    candidate(CandidateKind::PathLookup, "brave-browser-beta"),
1144];
1145const BRAVE_NIGHTLY_MACOS: [Candidate; 1] = [candidate(
1146    CandidateKind::KnownLocation,
1147    "/Applications/Brave Browser Nightly.app/Contents/MacOS/Brave Browser Nightly",
1148)];
1149const BRAVE_NIGHTLY_WINDOWS: [Candidate; 1] = [candidate(
1150    CandidateKind::KnownLocation,
1151    "{LOCALAPPDATA}\\BraveSoftware\\Brave-Browser-Nightly\\Application\\brave.exe",
1152)];
1153const BRAVE_NIGHTLY_LINUX: [Candidate; 2] = [
1154    candidate(
1155        CandidateKind::KnownLocation,
1156        "/usr/bin/brave-browser-nightly",
1157    ),
1158    candidate(CandidateKind::PathLookup, "brave-browser-nightly"),
1159];
1160const BRAVE_CHANNELS: [ChannelDefinition; 3] = [
1161    channel(
1162        ReleaseChannel::Stable,
1163        &BRAVE_STABLE_MACOS,
1164        &BRAVE_STABLE_WINDOWS,
1165        &BRAVE_STABLE_LINUX,
1166    ),
1167    channel(
1168        ReleaseChannel::Beta,
1169        &BRAVE_BETA_MACOS,
1170        &BRAVE_BETA_WINDOWS,
1171        &BRAVE_BETA_LINUX,
1172    ),
1173    channel(
1174        ReleaseChannel::Nightly,
1175        &BRAVE_NIGHTLY_MACOS,
1176        &BRAVE_NIGHTLY_WINDOWS,
1177        &BRAVE_NIGHTLY_LINUX,
1178    ),
1179];
1180
1181const OPERA_STABLE_MACOS: [Candidate; 1] = [candidate(
1182    CandidateKind::KnownLocation,
1183    "/Applications/Opera.app/Contents/MacOS/Opera",
1184)];
1185const OPERA_STABLE_WINDOWS: [Candidate; 2] = [
1186    candidate(
1187        CandidateKind::KnownLocation,
1188        "{LOCALAPPDATA}\\Programs\\Opera\\launcher.exe",
1189    ),
1190    candidate(
1191        CandidateKind::KnownLocation,
1192        "{PROGRAMFILES}\\Opera\\launcher.exe",
1193    ),
1194];
1195const OPERA_STABLE_LINUX: [Candidate; 2] = [
1196    candidate(CandidateKind::KnownLocation, "/usr/bin/opera"),
1197    candidate(CandidateKind::PathLookup, "opera"),
1198];
1199const OPERA_BETA_MACOS: [Candidate; 1] = [candidate(
1200    CandidateKind::KnownLocation,
1201    "/Applications/Opera Beta.app/Contents/MacOS/Opera",
1202)];
1203const OPERA_BETA_WINDOWS: [Candidate; 1] = [candidate(
1204    CandidateKind::KnownLocation,
1205    "{LOCALAPPDATA}\\Programs\\Opera beta\\launcher.exe",
1206)];
1207const OPERA_BETA_LINUX: [Candidate; 2] = [
1208    candidate(CandidateKind::KnownLocation, "/usr/bin/opera-beta"),
1209    candidate(CandidateKind::PathLookup, "opera-beta"),
1210];
1211const OPERA_DEV_MACOS: [Candidate; 1] = [candidate(
1212    CandidateKind::KnownLocation,
1213    "/Applications/Opera Developer.app/Contents/MacOS/Opera",
1214)];
1215const OPERA_DEV_WINDOWS: [Candidate; 1] = [candidate(
1216    CandidateKind::KnownLocation,
1217    "{LOCALAPPDATA}\\Programs\\Opera developer\\launcher.exe",
1218)];
1219const OPERA_DEV_LINUX: [Candidate; 2] = [
1220    candidate(CandidateKind::KnownLocation, "/usr/bin/opera-developer"),
1221    candidate(CandidateKind::PathLookup, "opera-developer"),
1222];
1223const OPERA_CHANNELS: [ChannelDefinition; 3] = [
1224    channel(
1225        ReleaseChannel::Stable,
1226        &OPERA_STABLE_MACOS,
1227        &OPERA_STABLE_WINDOWS,
1228        &OPERA_STABLE_LINUX,
1229    ),
1230    channel(
1231        ReleaseChannel::Beta,
1232        &OPERA_BETA_MACOS,
1233        &OPERA_BETA_WINDOWS,
1234        &OPERA_BETA_LINUX,
1235    ),
1236    channel(
1237        ReleaseChannel::Dev,
1238        &OPERA_DEV_MACOS,
1239        &OPERA_DEV_WINDOWS,
1240        &OPERA_DEV_LINUX,
1241    ),
1242];
1243
1244const VIVALDI_STABLE_MACOS: [Candidate; 1] = [candidate(
1245    CandidateKind::KnownLocation,
1246    "/Applications/Vivaldi.app/Contents/MacOS/Vivaldi",
1247)];
1248const VIVALDI_STABLE_WINDOWS: [Candidate; 3] = [
1249    candidate(
1250        CandidateKind::KnownLocation,
1251        "{LOCALAPPDATA}\\Vivaldi\\Application\\vivaldi.exe",
1252    ),
1253    candidate(
1254        CandidateKind::KnownLocation,
1255        "{PROGRAMFILES}\\Vivaldi\\Application\\vivaldi.exe",
1256    ),
1257    candidate(
1258        CandidateKind::KnownLocation,
1259        "{PROGRAMFILES_X86}\\Vivaldi\\Application\\vivaldi.exe",
1260    ),
1261];
1262const VIVALDI_STABLE_LINUX: [Candidate; 3] = [
1263    candidate(CandidateKind::KnownLocation, "/usr/bin/vivaldi"),
1264    candidate(CandidateKind::KnownLocation, "/usr/bin/vivaldi-stable"),
1265    candidate(CandidateKind::PathLookup, "vivaldi"),
1266];
1267const VIVALDI_SNAPSHOT_MACOS: [Candidate; 1] = [candidate(
1268    CandidateKind::KnownLocation,
1269    "/Applications/Vivaldi Snapshot.app/Contents/MacOS/Vivaldi Snapshot",
1270)];
1271const VIVALDI_SNAPSHOT_WINDOWS: [Candidate; 2] = [
1272    candidate(
1273        CandidateKind::KnownLocation,
1274        "{LOCALAPPDATA}\\Vivaldi Snapshot\\Application\\vivaldi.exe",
1275    ),
1276    candidate(
1277        CandidateKind::KnownLocation,
1278        "{PROGRAMFILES}\\Vivaldi Snapshot\\Application\\vivaldi.exe",
1279    ),
1280];
1281const VIVALDI_SNAPSHOT_LINUX: [Candidate; 2] = [
1282    candidate(CandidateKind::KnownLocation, "/usr/bin/vivaldi-snapshot"),
1283    candidate(CandidateKind::PathLookup, "vivaldi-snapshot"),
1284];
1285const VIVALDI_CHANNELS: [ChannelDefinition; 2] = [
1286    channel(
1287        ReleaseChannel::Stable,
1288        &VIVALDI_STABLE_MACOS,
1289        &VIVALDI_STABLE_WINDOWS,
1290        &VIVALDI_STABLE_LINUX,
1291    ),
1292    channel(
1293        ReleaseChannel::Snapshot,
1294        &VIVALDI_SNAPSHOT_MACOS,
1295        &VIVALDI_SNAPSHOT_WINDOWS,
1296        &VIVALDI_SNAPSHOT_LINUX,
1297    ),
1298];
1299
1300const ARC_STABLE_MACOS: [Candidate; 1] = [candidate(
1301    CandidateKind::KnownLocation,
1302    "/Applications/Arc.app/Contents/MacOS/Arc",
1303)];
1304const ARC_STABLE_WINDOWS: [Candidate; 1] = [candidate(
1305    CandidateKind::KnownLocation,
1306    "{LOCALAPPDATA}\\Programs\\Arc\\Arc.exe",
1307)];
1308const ARC_CHANNELS: [ChannelDefinition; 1] = [channel(
1309    ReleaseChannel::Default,
1310    &ARC_STABLE_MACOS,
1311    &ARC_STABLE_WINDOWS,
1312    &NONE,
1313)];
1314
1315const HELIUM_STABLE_MACOS: [Candidate; 1] = [candidate(
1316    CandidateKind::KnownLocation,
1317    "/Applications/Helium.app/Contents/MacOS/Helium",
1318)];
1319const HELIUM_STABLE_WINDOWS: [Candidate; 2] = [
1320    candidate(
1321        CandidateKind::KnownLocation,
1322        "{LOCALAPPDATA}\\Helium\\Helium.exe",
1323    ),
1324    candidate(
1325        CandidateKind::KnownLocation,
1326        "{PROGRAMFILES}\\Helium\\Helium.exe",
1327    ),
1328];
1329const HELIUM_STABLE_LINUX: [Candidate; 2] = [
1330    candidate(CandidateKind::KnownLocation, "/usr/bin/helium"),
1331    candidate(CandidateKind::PathLookup, "helium"),
1332];
1333const HELIUM_CHANNELS: [ChannelDefinition; 1] = [channel(
1334    ReleaseChannel::Default,
1335    &HELIUM_STABLE_MACOS,
1336    &HELIUM_STABLE_WINDOWS,
1337    &HELIUM_STABLE_LINUX,
1338)];
1339
1340const LIBREWOLF_STABLE_MACOS: [Candidate; 1] = [candidate(
1341    CandidateKind::KnownLocation,
1342    "/Applications/LibreWolf.app/Contents/MacOS/librewolf",
1343)];
1344const LIBREWOLF_STABLE_WINDOWS: [Candidate; 3] = [
1345    candidate(
1346        CandidateKind::KnownLocation,
1347        "{PROGRAMFILES}\\LibreWolf\\librewolf.exe",
1348    ),
1349    candidate(
1350        CandidateKind::KnownLocation,
1351        "{PROGRAMFILES_X86}\\LibreWolf\\librewolf.exe",
1352    ),
1353    candidate(
1354        CandidateKind::KnownLocation,
1355        "{LOCALAPPDATA}\\LibreWolf\\librewolf.exe",
1356    ),
1357];
1358const LIBREWOLF_STABLE_LINUX: [Candidate; 4] = [
1359    candidate(CandidateKind::KnownLocation, "/usr/bin/librewolf"),
1360    candidate(
1361        CandidateKind::Flatpak,
1362        "{HOME}/.local/share/flatpak/exports/bin/io.gitlab.librewolf-community",
1363    ),
1364    candidate(
1365        CandidateKind::Flatpak,
1366        "/var/lib/flatpak/exports/bin/io.gitlab.librewolf-community",
1367    ),
1368    candidate(CandidateKind::PathLookup, "librewolf"),
1369];
1370const LIBREWOLF_CHANNELS: [ChannelDefinition; 1] = [channel(
1371    ReleaseChannel::Default,
1372    &LIBREWOLF_STABLE_MACOS,
1373    &LIBREWOLF_STABLE_WINDOWS,
1374    &LIBREWOLF_STABLE_LINUX,
1375)];
1376
1377const FLOORP_STABLE_MACOS: [Candidate; 1] = [candidate(
1378    CandidateKind::KnownLocation,
1379    "/Applications/Floorp.app/Contents/MacOS/floorp",
1380)];
1381const FLOORP_STABLE_WINDOWS: [Candidate; 2] = [
1382    candidate(
1383        CandidateKind::KnownLocation,
1384        "{PROGRAMFILES}\\Floorp\\floorp.exe",
1385    ),
1386    candidate(
1387        CandidateKind::KnownLocation,
1388        "{LOCALAPPDATA}\\Floorp\\floorp.exe",
1389    ),
1390];
1391const FLOORP_STABLE_LINUX: [Candidate; 4] = [
1392    candidate(CandidateKind::KnownLocation, "/usr/bin/floorp"),
1393    candidate(
1394        CandidateKind::Flatpak,
1395        "{HOME}/.local/share/flatpak/exports/bin/one.ablaze.floorp",
1396    ),
1397    candidate(
1398        CandidateKind::Flatpak,
1399        "/var/lib/flatpak/exports/bin/one.ablaze.floorp",
1400    ),
1401    candidate(CandidateKind::PathLookup, "floorp"),
1402];
1403const FLOORP_CHANNELS: [ChannelDefinition; 1] = [channel(
1404    ReleaseChannel::Default,
1405    &FLOORP_STABLE_MACOS,
1406    &FLOORP_STABLE_WINDOWS,
1407    &FLOORP_STABLE_LINUX,
1408)];
1409
1410const ZEN_STABLE_MACOS: [Candidate; 1] = [candidate(
1411    CandidateKind::KnownLocation,
1412    "/Applications/Zen.app/Contents/MacOS/zen",
1413)];
1414const ZEN_STABLE_WINDOWS: [Candidate; 2] = [
1415    candidate(
1416        CandidateKind::KnownLocation,
1417        "{PROGRAMFILES}\\Zen Browser\\zen.exe",
1418    ),
1419    candidate(
1420        CandidateKind::KnownLocation,
1421        "{LOCALAPPDATA}\\Zen Browser\\zen.exe",
1422    ),
1423];
1424const ZEN_STABLE_LINUX: [Candidate; 2] = [
1425    candidate(CandidateKind::KnownLocation, "/usr/bin/zen-browser"),
1426    candidate(CandidateKind::PathLookup, "zen-browser"),
1427];
1428const ZEN_TWILIGHT_MACOS: [Candidate; 1] = [candidate(
1429    CandidateKind::KnownLocation,
1430    "/Applications/Twilight.app/Contents/MacOS/zen",
1431)];
1432const ZEN_TWILIGHT_WINDOWS: [Candidate; 2] = [
1433    candidate(
1434        CandidateKind::KnownLocation,
1435        "{PROGRAMFILES}\\Zen Twilight\\zen.exe",
1436    ),
1437    candidate(
1438        CandidateKind::KnownLocation,
1439        "{LOCALAPPDATA}\\Zen Twilight\\zen.exe",
1440    ),
1441];
1442const ZEN_TWILIGHT_LINUX: [Candidate; 2] = [
1443    candidate(
1444        CandidateKind::KnownLocation,
1445        "/usr/bin/zen-browser-twilight",
1446    ),
1447    candidate(CandidateKind::PathLookup, "zen-browser-twilight"),
1448];
1449const ZEN_CHANNELS: [ChannelDefinition; 2] = [
1450    channel(
1451        ReleaseChannel::Stable,
1452        &ZEN_STABLE_MACOS,
1453        &ZEN_STABLE_WINDOWS,
1454        &ZEN_STABLE_LINUX,
1455    ),
1456    channel(
1457        ReleaseChannel::Twilight,
1458        &ZEN_TWILIGHT_MACOS,
1459        &ZEN_TWILIGHT_WINDOWS,
1460        &ZEN_TWILIGHT_LINUX,
1461    ),
1462];
1463
1464const CHROME: BrowserDefinition = browser(
1465    Browser::Chrome,
1466    &CHROME_CHANNELS,
1467    &CHROMIUM_FAMILY_STABLE_ORDER,
1468    &CHROMIUM_FAMILY_LATEST_ORDER,
1469);
1470const CHROMIUM: BrowserDefinition = browser(
1471    Browser::Chromium,
1472    &CHROMIUM_CHANNELS,
1473    &DEFAULT_ONLY,
1474    &DEFAULT_ONLY,
1475);
1476const EDGE: BrowserDefinition = browser(
1477    Browser::Edge,
1478    &EDGE_CHANNELS,
1479    &CHROMIUM_FAMILY_STABLE_ORDER,
1480    &CHROMIUM_FAMILY_LATEST_ORDER,
1481);
1482const FIREFOX: BrowserDefinition = browser(
1483    Browser::Firefox,
1484    &FIREFOX_CHANNELS,
1485    &FIREFOX_STABLE_ORDER,
1486    &FIREFOX_LATEST_ORDER,
1487);
1488const BRAVE: BrowserDefinition = browser(
1489    Browser::Brave,
1490    &BRAVE_CHANNELS,
1491    &BRAVE_STABLE_ORDER,
1492    &BRAVE_LATEST_ORDER,
1493);
1494const OPERA: BrowserDefinition = browser(
1495    Browser::Opera,
1496    &OPERA_CHANNELS,
1497    &OPERA_STABLE_ORDER,
1498    &OPERA_LATEST_ORDER,
1499);
1500const VIVALDI: BrowserDefinition = browser(
1501    Browser::Vivaldi,
1502    &VIVALDI_CHANNELS,
1503    &SNAPSHOT_STABLE_ORDER,
1504    &SNAPSHOT_LATEST_ORDER,
1505);
1506const ARC: BrowserDefinition = browser(Browser::Arc, &ARC_CHANNELS, &DEFAULT_ONLY, &DEFAULT_ONLY);
1507const HELIUM: BrowserDefinition = browser(
1508    Browser::Helium,
1509    &HELIUM_CHANNELS,
1510    &DEFAULT_ONLY,
1511    &DEFAULT_ONLY,
1512);
1513const LIBREWOLF: BrowserDefinition = browser(
1514    Browser::LibreWolf,
1515    &LIBREWOLF_CHANNELS,
1516    &DEFAULT_ONLY,
1517    &DEFAULT_ONLY,
1518);
1519const FLOORP: BrowserDefinition = browser(
1520    Browser::Floorp,
1521    &FLOORP_CHANNELS,
1522    &DEFAULT_ONLY,
1523    &DEFAULT_ONLY,
1524);
1525const ZEN: BrowserDefinition = browser(
1526    Browser::Zen,
1527    &ZEN_CHANNELS,
1528    &TWILIGHT_STABLE_ORDER,
1529    &TWILIGHT_LATEST_ORDER,
1530);
1531
1532#[cfg(test)]
1533mod tests {
1534    use super::*;
1535
1536    struct TestEnvironment {
1537        platform: Platform,
1538        vars: BTreeMap<String, OsString>,
1539        existing_paths: BTreeSet<PathBuf>,
1540    }
1541
1542    impl TestEnvironment {
1543        fn new(platform: Platform) -> Self {
1544            Self {
1545                platform,
1546                vars: BTreeMap::new(),
1547                existing_paths: BTreeSet::new(),
1548            }
1549        }
1550
1551        fn with_var(mut self, key: &str, value: impl Into<OsString>) -> Self {
1552            self.vars.insert(key.to_owned(), value.into());
1553            self
1554        }
1555
1556        fn with_path(mut self, path: impl Into<PathBuf>) -> Self {
1557            self.existing_paths.insert(path.into());
1558            self
1559        }
1560    }
1561
1562    impl Environment for TestEnvironment {
1563        fn current_platform(&self) -> Platform {
1564            self.platform
1565        }
1566
1567        fn get_var(&self, key: &str) -> Option<OsString> {
1568            self.vars.get(key).cloned()
1569        }
1570
1571        fn path_exists(&self, path: &Path) -> bool {
1572            self.existing_paths.contains(path)
1573        }
1574    }
1575
1576    #[test]
1577    fn locate_browser_uses_override_first() {
1578        let environment = TestEnvironment::new(Platform::Macos)
1579            .with_var("BROWSER_LOCATIONS_EDGE_STABLE_PATH", "/tmp/override-edge")
1580            .with_path("/tmp/override-edge");
1581
1582        let location =
1583            locate_browser_in_environment(Browser::Edge, ReleaseChannel::Stable, &environment)
1584                .unwrap();
1585
1586        assert_eq!(location.path, PathBuf::from("/tmp/override-edge"));
1587        assert_eq!(location.source, ProbeSource::Override);
1588    }
1589
1590    #[test]
1591    fn locate_any_latest_prefers_newer_channels() {
1592        let environment = TestEnvironment::new(Platform::Macos)
1593            .with_var("BROWSER_LOCATIONS_EDGE_STABLE_PATH", "/tmp/stable-edge")
1594            .with_var("BROWSER_LOCATIONS_EDGE_CANARY_PATH", "/tmp/canary-edge")
1595            .with_path("/tmp/stable-edge")
1596            .with_path("/tmp/canary-edge");
1597
1598        let location = locate_with_fallback(
1599            Browser::Edge,
1600            definition(Browser::Edge).latest_order,
1601            "latest",
1602            &environment,
1603        )
1604        .unwrap();
1605
1606        assert_eq!(location.channel, ReleaseChannel::Canary);
1607    }
1608
1609    #[test]
1610    fn locate_browser_reports_unsupported_platform() {
1611        let environment = TestEnvironment::new(Platform::Linux);
1612
1613        let error =
1614            locate_browser_in_environment(Browser::Arc, ReleaseChannel::Default, &environment)
1615                .unwrap_err();
1616
1617        assert!(matches!(
1618            error,
1619            LocateError::UnsupportedPlatform {
1620                browser: Browser::Arc,
1621                channel: ReleaseChannel::Default,
1622                platform: Platform::Linux,
1623            }
1624        ));
1625    }
1626
1627    #[test]
1628    fn discover_browser_collects_installed_channels() {
1629        let environment = TestEnvironment::new(Platform::Linux)
1630            .with_var("BROWSER_LOCATIONS_BRAVE_STABLE_PATH", "/tmp/brave-stable")
1631            .with_var("BROWSER_LOCATIONS_BRAVE_NIGHTLY_PATH", "/tmp/brave-nightly")
1632            .with_path("/tmp/brave-stable")
1633            .with_path("/tmp/brave-nightly");
1634
1635        let discovered = discover_browser_in_environment(Browser::Brave, &environment);
1636
1637        assert_eq!(discovered.len(), 2);
1638    }
1639}