1use 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#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
16pub enum Browser {
17 Arc,
19 Brave,
21 Chrome,
23 Chromium,
25 Edge,
27 Firefox,
29 Floorp,
31 Helium,
33 LibreWolf,
35 Opera,
37 Vivaldi,
39 Zen,
41}
42
43impl Browser {
44 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#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
103pub enum ReleaseChannel {
104 Default,
106 Stable,
108 Beta,
110 Dev,
112 Canary,
114 Nightly,
116 Esr,
118 DeveloperEdition,
120 Snapshot,
122 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#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
166pub enum Platform {
167 Macos,
169 Windows,
171 Linux,
173}
174
175impl Platform {
176 #[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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
206pub enum ProbeSource {
207 Override,
209 KnownLocation,
211 PathLookup,
213 Flatpak,
215 Snap,
217}
218
219#[derive(Clone, Debug, Eq, PartialEq)]
221pub struct BrowserLocation {
222 pub browser: Browser,
224 pub channel: ReleaseChannel,
226 pub path: PathBuf,
228 pub platform: Platform,
230 pub source: ProbeSource,
232}
233
234#[derive(Debug, Error)]
236pub enum LocateError {
237 #[error("{browser} does not model the {channel} channel")]
239 UnsupportedChannel {
240 browser: Browser,
242 channel: ReleaseChannel,
244 },
245 #[error("{browser} {channel} is not supported on {platform}")]
247 UnsupportedPlatform {
248 browser: Browser,
250 channel: ReleaseChannel,
252 platform: Platform,
254 },
255 #[error("unable to find {browser} {channel} on {platform}")]
257 NotFound {
258 browser: Browser,
260 channel: ReleaseChannel,
262 platform: Platform,
264 },
265 #[error("unable to find any {browser} browser for the {strategy} strategy on {platform}")]
267 NoInstalledVariant {
268 browser: Browser,
270 strategy: &'static str,
272 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
369pub fn locate_browser(
390 browser: Browser,
391 channel: ReleaseChannel,
392) -> Result<BrowserLocation, LocateError> {
393 locate_browser_in_environment(browser, channel, &SystemEnvironment)
394}
395
396pub 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
420pub 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#[must_use]
456pub fn discover_browser(browser: Browser) -> Vec<BrowserLocation> {
457 discover_browser_in_environment(browser, &SystemEnvironment)
458}
459
460#[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#[macro_export]
482macro_rules! define_getter {
483 ($name:ident, $channel:expr, $doc:literal) => {
484 #[doc = $doc]
485 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}