Skip to main content

ui/
assoc.rs

1// assoc -- the app's file types and URL schemes at start-up (plat::assoc): the switches every
2// app gets through ui::main!
3//
4//   --register            register this program for PACKAGE filetypes= / schemes= (Windows:
5//                         HKCU\Software\Classes; Linux: a .desktop file, the own MIME types,
6//                         mimeapps.list); defaults of other programs stay and are listed; with
7//                         browser=1 also as a web browser (Windows: Default Programs)
8//   --register-replace    the same, and those defaults go to this app too (the user's own pick
9//                         in Windows' dialog always stays)
10//   --unregister          remove exactly what the registration added, put back what it replaced
11// They print what they did (a Windows GUI program: on the console it was started from) and end
12// the program: exit code 0 done, 1 failed, 2 not possible here or nothing declared.
13//
14//   desk_boot(package) -> Option<exit code>   (main!) keep the built-in PACKAGE text
15//                         (plat::assoc::package_set), run a switch; headless runs (--shot,
16//                         --script, the agent protocols) leave the system's list of recent files
17//                         alone (plat::recent::recent_os(false); jfgui_RECENT still redirects it).
18//                         Without a switch, Windows: the app id as the explicit AppUserModelID
19//                         once the setup installed the program or --register registered it
20//                         (plat::assoc::app_id_boot), before the first window
21//   assoc_switch(pkg, switch) -> Option<(exit code, text)>   one switch for pkg (None: no switch)
22//
23// (E13) A web browser (PACKAGE browser=1):
24//   ui.default_browser() -> BrowserStatus   plat::default_browser_status; headless runs: what the
25//                         script said (`system browser default|other|unknown`, at first other;
26//                         the command then sends UIC_ACTIVE 0 and 1 as a real change would)
27//   ui.default_browser_request() -> Result<(), AssocError>   plat::default_browser_request (the
28//                         system's page or question); headless runs open nothing: they print
29//                         "ui_default_browser_request" and answer Ok
30
31use alloc::format;
32use alloc::string::String;
33use core::sync::atomic::{AtomicU8, Ordering};
34
35use plat::assoc::{self, AssocError, BrowserStatus, Conflict, Package};
36
37use crate::Ui;
38
39/// the headless runs' answer of default_browser (the script's `system browser`): 0 other, 1
40/// default, 2 unknown
41static HEADLESS_BROWSER: AtomicU8 = AtomicU8::new(0);
42
43/// (auto.rs, `system browser default|other|unknown`) what default_browser answers in a headless run
44pub(crate) fn browser_script_set(s: BrowserStatus) {
45    HEADLESS_BROWSER.store(
46        match s {
47            BrowserStatus::Default => 1,
48            BrowserStatus::Unknown => 2,
49            _ => 0,
50        },
51        Ordering::Relaxed,
52    );
53}
54
55impl Ui {
56    /// (PACKAGE browser=1) is this app the system's web browser (plat::default_browser_status:
57    /// Default, NotDefault, Unknown).  Keep the answer and ask again when the app is active
58    /// again (UIC_ACTIVE 1): the user may have chosen in the system's settings.  Headless runs
59    /// answer what the script said (`system browser default|other|unknown`, at first
60    /// NotDefault; the command is followed by UIC_ACTIVE 0 and 1, as a real change would be)
61    pub fn default_browser(&self) -> BrowserStatus {
62        if self.headless {
63            return match HEADLESS_BROWSER.load(Ordering::Relaxed) {
64                1 => BrowserStatus::Default,
65                2 => BrowserStatus::Unknown,
66                _ => BrowserStatus::NotDefault,
67            };
68        }
69        plat::default_browser_status()
70    }
71
72    /// (PACKAGE browser=1) let the user make this app the web browser, from the user's click
73    /// (plat::default_browser_request: Windows' Settings at the app's page of the default apps,
74    /// Linux sets it, macOS asks).  Headless runs open nothing: they print
75    /// "ui_default_browser_request" and answer Ok
76    pub fn default_browser_request(&mut self) -> Result<(), AssocError> {
77        if self.headless {
78            plat::write_stdout(b"ui_default_browser_request\n");
79            return Ok(());
80        }
81        plat::default_browser_request()
82    }
83}
84
85fn error_code(e: &AssocError) -> i32 {
86    match e {
87        AssocError::Unavailable | AssocError::Empty => 2,
88        _ => 1,
89    }
90}
91
92/// (main!) PACKAGE, then the switches -> Some(exit code) when one of them ran
93#[doc(hidden)]
94pub fn desk_boot(package: Option<&str>) -> Option<i32> {
95    assoc::package_set(package);
96    let args = plat::args();
97    let has = |o: &str| args.iter().skip(1).any(|a| a == o);
98    // as perm.rs: the runs without a window
99    let headless = ["--shot", "--script"].iter().any(|o| plat::arg_value(o).is_some()) || has("--agent-json") || has("--agent-mcp");
100    if headless {
101        plat::recent::recent_os(false);
102    }
103    let Some(switch) = ["--unregister", "--register-replace", "--register"].into_iter().find(|s| has(s)) else {
104        assoc::app_id_boot();
105        return None;
106    };
107    let (code, text) = assoc_switch(&assoc::package(), switch)?;
108    assoc::console_print(&text);
109    Some(code)
110}
111
112/// run --register / --register-replace / --unregister for pkg -> (exit code, what it says)
113pub fn assoc_switch(pkg: &Package, switch: &str) -> Option<(i32, String)> {
114    if switch == "--unregister" {
115        return Some(match assoc::assoc_unregister() {
116            Ok(true) => (0, String::from("unregistered: the file types and URL schemes are removed\n")),
117            Ok(false) => (0, String::from("nothing to unregister\n")),
118            Err(e) => (error_code(&e), format!("error: {e}\n")),
119        });
120    }
121    let replace = match switch {
122        "--register" => false,
123        "--register-replace" => true,
124        _ => return None,
125    };
126    let mut take = |_: &Conflict| replace;
127    Some(match assoc::assoc_register(pkg, &mut take) {
128        Ok(r) => {
129            let plural = |n: usize, one: &str, many: &str| format!("{n} {}", if n == 1 { one } else { many });
130            let browser = if pkg.browser { ", as a web browser (the default stays the user's choice)" } else { "" };
131            let mut out = format!("registered {}: {}, {}{browser}\n", r.exe, plural(r.types, "file type", "file types"), plural(r.schemes, "URL scheme", "URL schemes"));
132            for c in &r.kept {
133                let why = if c.fixed { "the user's choice in Windows' settings" } else { "--register-replace takes it" };
134                out.push_str(&format!("kept: {} stays with {} ({why})\n", c.what, c.holder));
135            }
136            for n in &r.notes {
137                out.push_str(&format!("note: {n}\n"));
138            }
139            (0, out)
140        }
141        Err(e) => {
142            let mut out = format!("error: {e}\n");
143            if let AssocError::Declined(_) = e {
144                out.push_str("(--register-replace moves it aside; --unregister puts it back)\n");
145            }
146            (error_code(&e), out)
147        }
148    })
149}