1use std::{cell::RefCell, rc::Rc};
43
44use cranpose_core::{CompositionLocal, CompositionLocalProvider, compositionLocalOfWithPolicy};
45use cranpose_macros::composable;
46
47#[derive(Clone, Debug, PartialEq)]
53pub enum LaunchArgValue {
54 Bool(bool),
56 Int(i32),
58 Long(i64),
60 Float(f32),
62 Text(String),
64}
65
66#[derive(Clone, Debug, Default, PartialEq)]
72pub struct LaunchArgs {
73 entries: Vec<(Box<str>, LaunchArgValue)>,
74 debuggable: bool,
75}
76
77pub type LaunchArgsRef = Rc<LaunchArgs>;
79
80impl LaunchArgs {
81 pub fn new(
87 entries: impl IntoIterator<Item = (String, LaunchArgValue)>,
88 debuggable: bool,
89 ) -> Self {
90 let mut collected: Vec<(Box<str>, LaunchArgValue)> = Vec::new();
91 for (name, value) in entries {
92 if name.is_empty() || collected.iter().any(|(known, _)| **known == *name) {
93 continue;
94 }
95 collected.push((name.into_boxed_str(), value));
96 }
97 Self {
98 entries: collected,
99 debuggable,
100 }
101 }
102
103 pub fn is_debuggable(&self) -> bool {
111 self.debuggable
112 }
113
114 pub fn contains(&self, name: &str) -> bool {
119 self.value(name).is_some()
120 }
121
122 pub fn names(&self) -> impl Iterator<Item = &str> {
124 self.entries.iter().map(|(name, _)| &**name)
125 }
126
127 pub fn len(&self) -> usize {
129 self.entries.len()
130 }
131
132 pub fn is_empty(&self) -> bool {
134 self.entries.is_empty()
135 }
136
137 pub fn value(&self, name: &str) -> Option<&LaunchArgValue> {
139 self.entries
140 .iter()
141 .find(|(known, _)| &**known == name)
142 .map(|(_, value)| value)
143 }
144
145 pub fn boolean(&self, name: &str) -> Option<bool> {
152 match self.value(name)? {
153 LaunchArgValue::Bool(value) => Some(*value),
154 LaunchArgValue::Text(text) => parse_boolean(text),
155 _ => None,
156 }
157 }
158
159 pub fn int(&self, name: &str) -> Option<i32> {
163 match self.value(name)? {
164 LaunchArgValue::Int(value) => Some(*value),
165 LaunchArgValue::Long(value) => i32::try_from(*value).ok(),
166 LaunchArgValue::Text(text) => text.trim().parse().ok(),
167 _ => None,
168 }
169 }
170
171 pub fn long(&self, name: &str) -> Option<i64> {
176 match self.value(name)? {
177 LaunchArgValue::Long(value) => Some(*value),
178 LaunchArgValue::Int(value) => Some(i64::from(*value)),
179 LaunchArgValue::Text(text) => text.trim().parse().ok(),
180 _ => None,
181 }
182 }
183
184 pub fn float(&self, name: &str) -> Option<f32> {
188 match self.value(name)? {
189 LaunchArgValue::Float(value) => Some(*value),
190 LaunchArgValue::Int(value) => Some(*value as f32),
191 LaunchArgValue::Long(value) => Some(*value as f32),
192 LaunchArgValue::Text(text) => text.trim().parse().ok(),
193 _ => None,
194 }
195 }
196
197 pub fn string(&self, name: &str) -> Option<&str> {
203 match self.value(name)? {
204 LaunchArgValue::Text(text) => Some(text),
205 _ => None,
206 }
207 }
208}
209
210fn parse_boolean(text: &str) -> Option<bool> {
211 match text.trim().to_ascii_lowercase().as_str() {
212 "true" | "1" | "yes" | "on" => Some(true),
213 "false" | "0" | "no" | "off" => Some(false),
214 _ => None,
215 }
216}
217
218thread_local! {
219 static PLATFORM_LAUNCH_ARGS: RefCell<Option<LaunchArgsRef>> = const { RefCell::new(None) };
220 static DEFAULT_LAUNCH_ARGS: RefCell<Option<LaunchArgsRef>> = const { RefCell::new(None) };
221}
222
223pub fn set_platform_launch_args(args: LaunchArgsRef) {
230 PLATFORM_LAUNCH_ARGS.with(|cell| *cell.borrow_mut() = Some(args));
231}
232
233pub fn clear_platform_launch_args() {
235 PLATFORM_LAUNCH_ARGS.with(|cell| *cell.borrow_mut() = None);
236}
237
238pub fn launch_args() -> LaunchArgsRef {
242 if let Some(args) = PLATFORM_LAUNCH_ARGS.with(|cell| cell.borrow().clone()) {
243 return args;
244 }
245 DEFAULT_LAUNCH_ARGS.with(|cell| {
246 let mut cached = cell.borrow_mut();
247 cached
248 .get_or_insert_with(|| Rc::new(default_launch_args()))
249 .clone()
250 })
251}
252
253pub fn is_debuggable() -> bool {
256 launch_args().is_debuggable()
257}
258
259fn default_launch_args() -> LaunchArgs {
260 #[cfg(not(target_arch = "wasm32"))]
261 {
262 launch_args_from_command_line(std::env::args().skip(1), cfg!(debug_assertions))
263 }
264 #[cfg(target_arch = "wasm32")]
265 {
266 LaunchArgs::new(std::iter::empty(), cfg!(debug_assertions))
267 }
268}
269
270pub fn launch_args_from_command_line(
277 tokens: impl IntoIterator<Item = String>,
278 debuggable: bool,
279) -> LaunchArgs {
280 let mut entries = Vec::new();
281 for token in tokens {
282 if token == "--" {
283 break;
284 }
285 let Some(option) = token.strip_prefix("--") else {
286 continue;
287 };
288 match option.split_once('=') {
289 Some((name, value)) => {
290 entries.push((name.to_string(), LaunchArgValue::Text(value.to_string())));
291 }
292 None => entries.push((option.to_string(), LaunchArgValue::Bool(true))),
293 }
294 }
295 LaunchArgs::new(entries, debuggable)
296}
297
298pub fn local_launch_args() -> CompositionLocal<LaunchArgsRef> {
303 thread_local! {
304 static LOCAL_LAUNCH_ARGS: RefCell<Option<CompositionLocal<LaunchArgsRef>>> = const { RefCell::new(None) };
305 }
306
307 LOCAL_LAUNCH_ARGS.with(|cell| {
308 let mut local = cell.borrow_mut();
309 local
310 .get_or_insert_with(|| compositionLocalOfWithPolicy(launch_args, Rc::ptr_eq))
311 .clone()
312 })
313}
314
315#[composable]
320pub fn ProvideLaunchArgs(args: LaunchArgsRef, content: impl FnOnce()) {
321 let local = local_launch_args();
322 CompositionLocalProvider(vec![local.provides(args)], move || {
323 content();
324 });
325}
326
327#[composable]
329pub fn isDebuggable() -> bool {
330 local_launch_args().current().is_debuggable()
331}
332
333#[cfg(test)]
334mod tests {
335 use std::cell::RefCell as StdRefCell;
336
337 use super::*;
338 use crate::run_test_composition;
339
340 fn args(entries: &[(&str, LaunchArgValue)]) -> LaunchArgs {
341 LaunchArgs::new(
342 entries
343 .iter()
344 .map(|(name, value)| ((*name).to_string(), value.clone())),
345 false,
346 )
347 }
348
349 fn command_line(tokens: &[&str]) -> LaunchArgs {
350 launch_args_from_command_line(tokens.iter().map(|token| (*token).to_string()), false)
351 }
352
353 #[test]
354 fn typed_extras_read_back_in_the_type_they_arrived_in() {
355 let args = args(&[
356 ("ob_autoplay", LaunchArgValue::Bool(true)),
357 ("ob_level", LaunchArgValue::Int(7)),
358 ("ob_seed", LaunchArgValue::Long(9_000_000_000)),
359 ("ob_time_scale", LaunchArgValue::Float(0.5)),
360 ("ob_screen", LaunchArgValue::Text("lobby".to_string())),
361 ]);
362
363 assert_eq!(args.boolean("ob_autoplay"), Some(true));
364 assert_eq!(args.int("ob_level"), Some(7));
365 assert_eq!(args.long("ob_seed"), Some(9_000_000_000));
366 assert_eq!(args.float("ob_time_scale"), Some(0.5));
367 assert_eq!(args.string("ob_screen"), Some("lobby"));
368 }
369
370 #[test]
371 fn a_missing_argument_reads_as_none_for_every_type() {
372 let args = args(&[]);
373
374 assert_eq!(args.boolean("absent"), None);
375 assert_eq!(args.int("absent"), None);
376 assert_eq!(args.long("absent"), None);
377 assert_eq!(args.float("absent"), None);
378 assert_eq!(args.string("absent"), None);
379 assert!(!args.contains("absent"));
380 assert!(args.is_empty());
381 }
382
383 #[test]
384 fn text_arguments_parse_into_the_requested_number_type() {
385 let args = args(&[
386 ("level", LaunchArgValue::Text("7".to_string())),
387 ("seed", LaunchArgValue::Text("9000000000".to_string())),
388 ("scale", LaunchArgValue::Text("0.25".to_string())),
389 ("flag", LaunchArgValue::Text("ON".to_string())),
390 ]);
391
392 assert_eq!(args.int("level"), Some(7));
393 assert_eq!(args.long("seed"), Some(9_000_000_000));
394 assert_eq!(args.float("scale"), Some(0.25));
395 assert_eq!(args.boolean("flag"), Some(true));
396 assert_eq!(args.int("seed"), None, "a long that does not fit an i32");
397 assert_eq!(args.boolean("level"), None, "numbers are not truthy");
398 }
399
400 #[test]
401 fn integer_arguments_widen_but_do_not_become_text() {
402 let args = args(&[("level", LaunchArgValue::Int(7))]);
403
404 assert_eq!(args.long("level"), Some(7));
405 assert_eq!(args.float("level"), Some(7.0));
406 assert_eq!(args.string("level"), None);
407 }
408
409 #[test]
410 fn the_command_line_maps_flags_and_assignments_to_arguments() {
411 let args = command_line(&[
412 "--ob_debug",
413 "--ob_level=7",
414 "positional",
415 "--ob_screen=lobby",
416 ]);
417
418 assert_eq!(args.boolean("ob_debug"), Some(true));
419 assert_eq!(args.int("ob_level"), Some(7));
420 assert_eq!(args.string("ob_screen"), Some("lobby"));
421 assert_eq!(
422 args.len(),
423 3,
424 "positional arguments are not launch arguments"
425 );
426 }
427
428 #[test]
429 fn the_command_line_stops_at_a_bare_double_dash() {
430 let args = command_line(&["--before", "--", "--after"]);
431
432 assert!(args.contains("before"));
433 assert!(!args.contains("after"));
434 }
435
436 #[test]
437 fn the_first_value_wins_when_a_name_repeats() {
438 let args = args(&[
439 ("level", LaunchArgValue::Int(1)),
440 ("level", LaunchArgValue::Int(2)),
441 ]);
442
443 assert_eq!(args.int("level"), Some(1));
444 assert_eq!(args.len(), 1);
445 }
446
447 #[test]
448 fn the_installed_platform_snapshot_takes_precedence() {
449 clear_platform_launch_args();
450 set_platform_launch_args(Rc::new(args(&[(
451 "ob_autoplay",
452 LaunchArgValue::Bool(true),
453 )])));
454
455 assert_eq!(launch_args().boolean("ob_autoplay"), Some(true));
456
457 clear_platform_launch_args();
458 assert_eq!(launch_args().boolean("ob_autoplay"), None);
459 }
460
461 #[test]
462 fn debuggable_is_reported_by_the_snapshot() {
463 clear_platform_launch_args();
464 set_platform_launch_args(Rc::new(LaunchArgs::new(std::iter::empty(), true)));
465 assert!(is_debuggable());
466
467 set_platform_launch_args(Rc::new(LaunchArgs::new(std::iter::empty(), false)));
468 assert!(!is_debuggable());
469 clear_platform_launch_args();
470 }
471
472 #[test]
473 fn provide_launch_args_reaches_composition() {
474 let captured = Rc::new(StdRefCell::new(None));
475
476 {
477 let captured = Rc::clone(&captured);
478 run_test_composition(move || {
479 let captured = Rc::clone(&captured);
480 let provided = Rc::new(LaunchArgs::new(
481 [("ob_level".to_string(), LaunchArgValue::Int(3))],
482 true,
483 ));
484 ProvideLaunchArgs(provided, move || {
485 *captured.borrow_mut() = Some((
486 local_launch_args().current().int("ob_level"),
487 isDebuggable(),
488 ));
489 });
490 });
491 }
492
493 assert_eq!(*captured.borrow(), Some((Some(3), true)));
494 }
495}