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}