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