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#[allow(non_snake_case)]
320#[composable]
321pub fn ProvideLaunchArgs(args: LaunchArgsRef, content: impl FnOnce()) {
322 let local = local_launch_args();
323 CompositionLocalProvider(vec![local.provides(args)], move || {
324 content();
325 });
326}
327
328#[allow(non_snake_case)]
330#[composable]
331pub fn isDebuggable() -> bool {
332 local_launch_args().current().is_debuggable()
333}
334
335#[cfg(test)]
336mod tests {
337 use std::cell::RefCell as StdRefCell;
338
339 use super::*;
340 use crate::run_test_composition;
341
342 fn args(entries: &[(&str, LaunchArgValue)]) -> LaunchArgs {
343 LaunchArgs::new(
344 entries
345 .iter()
346 .map(|(name, value)| ((*name).to_string(), value.clone())),
347 false,
348 )
349 }
350
351 fn command_line(tokens: &[&str]) -> LaunchArgs {
352 launch_args_from_command_line(tokens.iter().map(|token| (*token).to_string()), false)
353 }
354
355 #[test]
356 fn typed_extras_read_back_in_the_type_they_arrived_in() {
357 let args = args(&[
358 ("ob_autoplay", LaunchArgValue::Bool(true)),
359 ("ob_level", LaunchArgValue::Int(7)),
360 ("ob_seed", LaunchArgValue::Long(9_000_000_000)),
361 ("ob_time_scale", LaunchArgValue::Float(0.5)),
362 ("ob_screen", LaunchArgValue::Text("lobby".to_string())),
363 ]);
364
365 assert_eq!(args.boolean("ob_autoplay"), Some(true));
366 assert_eq!(args.int("ob_level"), Some(7));
367 assert_eq!(args.long("ob_seed"), Some(9_000_000_000));
368 assert_eq!(args.float("ob_time_scale"), Some(0.5));
369 assert_eq!(args.string("ob_screen"), Some("lobby"));
370 }
371
372 #[test]
373 fn a_missing_argument_reads_as_none_for_every_type() {
374 let args = args(&[]);
375
376 assert_eq!(args.boolean("absent"), None);
377 assert_eq!(args.int("absent"), None);
378 assert_eq!(args.long("absent"), None);
379 assert_eq!(args.float("absent"), None);
380 assert_eq!(args.string("absent"), None);
381 assert!(!args.contains("absent"));
382 assert!(args.is_empty());
383 }
384
385 #[test]
386 fn text_arguments_parse_into_the_requested_number_type() {
387 let args = args(&[
388 ("level", LaunchArgValue::Text("7".to_string())),
389 ("seed", LaunchArgValue::Text("9000000000".to_string())),
390 ("scale", LaunchArgValue::Text("0.25".to_string())),
391 ("flag", LaunchArgValue::Text("ON".to_string())),
392 ]);
393
394 assert_eq!(args.int("level"), Some(7));
395 assert_eq!(args.long("seed"), Some(9_000_000_000));
396 assert_eq!(args.float("scale"), Some(0.25));
397 assert_eq!(args.boolean("flag"), Some(true));
398 assert_eq!(args.int("seed"), None, "a long that does not fit an i32");
399 assert_eq!(args.boolean("level"), None, "numbers are not truthy");
400 }
401
402 #[test]
403 fn integer_arguments_widen_but_do_not_become_text() {
404 let args = args(&[("level", LaunchArgValue::Int(7))]);
405
406 assert_eq!(args.long("level"), Some(7));
407 assert_eq!(args.float("level"), Some(7.0));
408 assert_eq!(args.string("level"), None);
409 }
410
411 #[test]
412 fn the_command_line_maps_flags_and_assignments_to_arguments() {
413 let args = command_line(&[
414 "--ob_debug",
415 "--ob_level=7",
416 "positional",
417 "--ob_screen=lobby",
418 ]);
419
420 assert_eq!(args.boolean("ob_debug"), Some(true));
421 assert_eq!(args.int("ob_level"), Some(7));
422 assert_eq!(args.string("ob_screen"), Some("lobby"));
423 assert_eq!(
424 args.len(),
425 3,
426 "positional arguments are not launch arguments"
427 );
428 }
429
430 #[test]
431 fn the_command_line_stops_at_a_bare_double_dash() {
432 let args = command_line(&["--before", "--", "--after"]);
433
434 assert!(args.contains("before"));
435 assert!(!args.contains("after"));
436 }
437
438 #[test]
439 fn the_first_value_wins_when_a_name_repeats() {
440 let args = args(&[
441 ("level", LaunchArgValue::Int(1)),
442 ("level", LaunchArgValue::Int(2)),
443 ]);
444
445 assert_eq!(args.int("level"), Some(1));
446 assert_eq!(args.len(), 1);
447 }
448
449 #[test]
450 fn the_installed_platform_snapshot_takes_precedence() {
451 clear_platform_launch_args();
452 set_platform_launch_args(Rc::new(args(&[(
453 "ob_autoplay",
454 LaunchArgValue::Bool(true),
455 )])));
456
457 assert_eq!(launch_args().boolean("ob_autoplay"), Some(true));
458
459 clear_platform_launch_args();
460 assert_eq!(launch_args().boolean("ob_autoplay"), None);
461 }
462
463 #[test]
464 fn debuggable_is_reported_by_the_snapshot() {
465 clear_platform_launch_args();
466 set_platform_launch_args(Rc::new(LaunchArgs::new(std::iter::empty(), true)));
467 assert!(is_debuggable());
468
469 set_platform_launch_args(Rc::new(LaunchArgs::new(std::iter::empty(), false)));
470 assert!(!is_debuggable());
471 clear_platform_launch_args();
472 }
473
474 #[test]
475 fn provide_launch_args_reaches_composition() {
476 let captured = Rc::new(StdRefCell::new(None));
477
478 {
479 let captured = Rc::clone(&captured);
480 run_test_composition(move || {
481 let captured = Rc::clone(&captured);
482 let provided = Rc::new(LaunchArgs::new(
483 [("ob_level".to_string(), LaunchArgValue::Int(3))],
484 true,
485 ));
486 ProvideLaunchArgs(provided, move || {
487 *captured.borrow_mut() = Some((
488 local_launch_args().current().int("ob_level"),
489 isDebuggable(),
490 ));
491 });
492 });
493 }
494
495 assert_eq!(*captured.borrow(), Some((Some(3), true)));
496 }
497}