1use argh::FromArgs;
16
17use crate::scenes;
18
19const PROGRAM: &str = "unlit3d-examples";
21
22fn default_scene() -> String {
24 scenes::default().id.to_owned()
25}
26
27#[derive(FromArgs, Debug, Clone, PartialEq)]
29#[argh(
30 help_triggers("-h", "--help"),
31 description = "a windowed example with selectable scenes and an egui overlay"
32)]
33pub struct Args {
34 #[argh(option, default = "default_scene()")]
36 pub scene: String,
37
38 #[argh(switch)]
40 pub list_scenes: bool,
41
42 #[argh(option, from_str_fn(parse_size))]
44 pub size: Option<(u32, u32)>,
45
46 #[argh(switch)]
49 pub mcp: bool,
50}
51
52impl Default for Args {
53 fn default() -> Self {
54 Self {
55 scene: default_scene(),
56 list_scenes: false,
57 size: None,
58 mcp: false,
59 }
60 }
61}
62
63#[derive(Debug, Clone, PartialEq)]
65pub enum Parsed {
66 Run(Args),
68 Help(String),
70}
71
72pub fn parse<I>(args: I) -> Result<Parsed, String>
74where
75 I: IntoIterator,
76 I::Item: Into<String>,
77{
78 let args: Vec<String> = args.into_iter().map(Into::into).collect();
79 let refs: Vec<&str> = args.iter().map(String::as_str).collect();
80
81 match Args::from_args(&[PROGRAM], &refs) {
82 Ok(parsed) => {
83 parsed.validate()?;
84 Ok(Parsed::Run(parsed))
85 }
86 Err(exit) if exit.status.is_ok() => Ok(Parsed::Help(usage())),
90 Err(exit) => Err(exit.output),
91 }
92}
93
94pub fn usage() -> String {
99 let options = Args::from_args(&[PROGRAM], &["--help"])
100 .expect_err("`--help` exits early")
101 .output;
102 format!("{options}\n{}", scenes::list_text())
103}
104
105impl Args {
106 fn validate(&self) -> Result<(), String> {
111 if scenes::by_id(&self.scene).is_none() {
115 return Err(format!(
116 "unknown scene `{}`\n{}",
117 self.scene,
118 scenes::list_text()
119 ));
120 }
121
122 Ok(())
123 }
124}
125
126fn parse_size(text: &str) -> Result<(u32, u32), String> {
128 let invalid = || format!("`{text}` is not a `WxH` size");
129 let (width, height) = text.split_once(['x', 'X']).ok_or_else(invalid)?;
130 let width: u32 = width.trim().parse().map_err(|_| invalid())?;
131 let height: u32 = height.trim().parse().map_err(|_| invalid())?;
132 if width == 0 || height == 0 {
133 return Err(format!("`{text}` has a zero dimension"));
134 }
135 Ok((width, height))
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 fn run(args: &[&str]) -> Result<Args, String> {
144 match parse(args.iter().copied())? {
145 Parsed::Run(parsed) => Ok(parsed),
146 Parsed::Help(_) => panic!("expected options, got --help"),
147 }
148 }
149
150 #[test]
151 fn no_arguments_start_the_default_scene_at_its_own_size() {
152 let args = run(&[]).expect("no arguments parse");
153 assert_eq!(args.scene, scenes::default().id);
154 assert!(!args.list_scenes);
155 assert_eq!(args.size, None);
156 }
157
158 #[test]
159 fn the_window_options_are_read() {
160 let args = run(&["--scene", "skinned", "--size", "320x240", "--list-scenes"])
161 .expect("the options parse");
162 assert_eq!(args.scene, "skinned");
163 assert_eq!(args.size, Some((320, 240)));
164 assert!(args.list_scenes);
165 assert!(!args.mcp);
166 }
167
168 #[test]
169 fn the_mcp_switch_is_read() {
170 let args = run(&["--mcp"]).expect("the switch parses");
171 assert!(args.mcp);
172 assert_eq!(args.scene, scenes::default().id);
173 }
174
175 #[test]
176 fn help_is_reported_as_its_own_outcome() {
177 let help = match parse(["--help"]).expect("`--help` parses") {
178 Parsed::Help(text) => text,
179 Parsed::Run(_) => panic!("`--help` must not run"),
180 };
181 assert!(help.contains("--scene"), "help names the options: {help}");
182 assert!(parse(["-h"]).is_ok(), "`-h` is a help trigger too");
183 }
184
185 #[test]
186 fn an_unknown_scene_is_rejected() {
187 assert!(run(&["--scene", "nonsense"]).is_err());
188 }
189
190 #[test]
191 fn malformed_values_are_rejected() {
192 assert!(run(&["--size", "wide"]).is_err());
193 assert!(run(&["--size", "0x240"]).is_err());
194 assert!(run(&["--size"]).is_err());
195 assert!(run(&["--nonsense"]).is_err());
196 }
197}