1use std::{cell::RefCell, collections::VecDeque, rc::Rc, sync::Mutex};
5static PENDING: Mutex<VecDeque<Vec<String>>> = Mutex::new(VecDeque::new());
6day_reactive::tls_slots! {
7 documents;
8 static HANDLER: RefCell<Option<Rc<dyn Fn(Vec<String>)>>> = const { RefCell::new(None) };
9}
10pub fn on_open_files(handler: impl Fn(Vec<String>) + 'static) {
13 let handler: Rc<dyn Fn(Vec<String>)> = Rc::new(handler);
14 let previous = HANDLER.with(|h| h.borrow_mut().replace(handler.clone()));
15 drop(previous);
16 day_reactive::Scope::current().on_cleanup(move || {
17 let removed = HANDLER.try_with(|h| {
19 if h.borrow()
20 .as_ref()
21 .is_some_and(|current| Rc::ptr_eq(current, &handler))
22 {
23 h.borrow_mut().take()
24 } else {
25 None
26 }
27 });
28 drop(removed);
29 });
30 if day_reactive::has_main_poster() {
31 day_reactive::on_main(drain_open_files);
32 }
33}
34pub fn request_open_files(files: Vec<String>) {
37 let files: Vec<_> = files
38 .into_iter()
39 .filter(|p| {
40 if p.split_once(':').is_some_and(|(scheme, _)| {
41 scheme.len() > 1
42 && scheme
43 .bytes()
44 .all(|b| b.is_ascii_alphanumeric() || b"+.-".contains(&b))
45 }) && !p.starts_with("file:")
46 && !std::path::Path::new(p).is_absolute()
47 {
48 crate::request_open_url(p);
49 false
50 } else {
51 !p.is_empty()
52 }
53 })
54 .collect();
55 if files.is_empty() {
56 return;
57 }
58 PENDING
59 .lock()
60 .unwrap_or_else(|e| e.into_inner())
61 .push_back(files);
62 if day_reactive::has_main_poster() {
63 day_reactive::on_main(drain_open_files);
64 }
65}
66pub(crate) fn drain_open_files() {
67 loop {
68 let Some(handler) = HANDLER.with(|h| h.borrow().clone()) else {
70 return;
71 };
72 let next = PENDING
73 .lock()
74 .unwrap_or_else(|e| e.into_inner())
75 .pop_front();
76 let Some(files) = next else { break };
77 handler(files);
78 }
79}
80pub(crate) fn launch_files(args: impl Iterator<Item = String>) -> Vec<String> {
84 let mut args = args.peekable();
85 let mut files = Vec::new();
86 while let Some(arg) = args.next() {
87 if arg == "--day-open-files" {
88 files.extend(args);
89 break;
90 }
91 if arg == "--day-open-url" {
92 if let Some(url) = args.next() {
93 files.push(url);
94 }
95 } else if !arg.starts_with('-') && arg.contains("://") {
96 files.push(arg);
97 } else if arg == "--day-open-file" {
98 if let Some(path) = args.next() {
99 files.push(path)
100 }
101 } else if !arg.starts_with('-') && std::path::Path::new(&arg).is_file() {
102 files.push(arg);
103 }
104 }
105 files
106}
107
108pub fn forward_launch_args(args: Vec<String>) {
113 request_open_files(launch_files(args.into_iter()));
114 if let Some(window) = crate::windows::focused_window().or_else(crate::windows::initial_window) {
115 window.focus();
116 }
117}
118
119pub fn launch_link(route: &str) -> String {
124 let scheme = std::env::var("DAY_APP_SCHEME")
125 .ok()
126 .filter(|s| !s.is_empty())
127 .or_else(|| option_env!("DAY_APP_SCHEME").map(str::to_owned));
128 match scheme {
129 Some(scheme) => format!("{scheme}://{route}"),
130 None => route.to_owned(),
131 }
132}
133
134#[doc(hidden)]
137pub fn take_launch_files() -> Vec<String> {
138 static TAKEN: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
139 if TAKEN.swap(true, std::sync::atomic::Ordering::Relaxed) {
140 Vec::new()
141 } else {
142 launch_files(std::env::args().skip(1))
143 }
144}
145#[cfg(test)]
146mod tests {
147 use super::*;
148 #[test]
149 fn cold_warm_reentrant_delivery_and_scope_cleanup() {
150 let received = Rc::new(RefCell::new(Vec::new()));
151 request_open_files(vec!["cold.epub".into()]);
152 drain_open_files(); let scope = day_reactive::Scope::child();
154 let seen = received.clone();
155 scope.enter(|| {
156 on_open_files(move |files| {
157 if files == ["cold.epub"] {
158 request_open_files(vec!["reentrant.epub".into()]);
159 }
160 seen.borrow_mut().extend(files);
161 })
162 });
163 drain_open_files();
164 request_open_files(vec!["warm.epub".into()]);
165 drain_open_files();
166 assert_eq!(
167 *received.borrow(),
168 ["cold.epub", "reentrant.epub", "warm.epub"]
169 );
170 scope.dispose();
171 request_open_files(vec!["after-dispose.epub".into()]);
172 drain_open_files();
173 assert_eq!(received.borrow().len(), 3);
174 let second = day_reactive::Scope::child();
175 let seen = received.clone();
176 second.enter(|| on_open_files(move |files| seen.borrow_mut().extend(files)));
177 drain_open_files();
178 assert_eq!(received.borrow().last().unwrap(), "after-dispose.epub");
179 second.dispose();
180 }
181 #[test]
182 fn association_arguments_are_explicit_and_preserve_spaces() {
183 assert_eq!(
184 launch_files(
185 [
186 "app",
187 "--day-open-file",
188 "/tmp/book name.epub",
189 "--day-open-files",
190 "a.epub",
191 "b.epub"
192 ]
193 .into_iter()
194 .map(str::to_owned)
195 ),
196 ["/tmp/book name.epub", "a.epub", "b.epub"]
197 );
198 assert_eq!(
199 launch_files(
200 ["https://example.org", "--debug"]
201 .into_iter()
202 .map(str::to_owned)
203 ),
204 ["https://example.org"]
205 );
206 assert_eq!(
207 launch_files(
208 ["--day-open-url", "feed:https://example.org/rss?q=a%26b"]
209 .into_iter()
210 .map(str::to_owned)
211 ),
212 ["feed:https://example.org/rss?q=a%26b"]
213 );
214 }
215}