1#![deny(clippy::use_self, unsafe_op_in_unsafe_fn)]
14
15use std::{
16 cell::RefCell,
17 env::Args,
18 fmt::Display,
19 io::{Write, stderr, stdin},
20};
21
22thread_local! {
23 static ARGV: RefCell<Args> = {
24 let mut args = std::env::args();
25 args.next();
26 RefCell::new(args)
27 }
28}
29
30pub fn optional<S, E: Display>(
31 prompt: &str,
32 mut parser: impl for<'a> FnMut(&'a str) -> Result<S, E>,
33) -> Option<S> {
34 raw(&format!("{prompt} (optional)"), |x| {
35 if x.is_empty() {
36 Ok(None)
37 } else {
38 parser(x).map(Some)
39 }
40 })
41}
42
43pub fn with_default<S, E: Display>(
44 prompt: &str,
45 default: &str,
46 mut parser: impl for<'a> FnMut(&'a str) -> Result<S, E>,
47) -> S {
48 raw(&format!("{prompt} (default: {default})"), |x| {
49 if x.is_empty() {
50 parser(default)
51 } else {
52 parser(x)
53 }
54 })
55}
56
57pub fn yes_no(prompt: &str) -> bool {
58 with_default(prompt, "y", |response| {
59 if response.starts_with('y') || response.starts_with('n') {
60 Ok(response.starts_with('y'))
61 } else {
62 Err(format!(
63 "unrecognized response '{response}'. Should be '(y)es' or '(n)o'"
64 ))
65 }
66 })
67}
68
69pub fn raw<S, E: Display>(
70 prompt: &str,
71 mut parser: impl for<'a> FnMut(&'a str) -> Result<S, E>,
72) -> S {
73 let cli: Option<(String, Result<S, E>)> = ARGV.with(|argv| {
74 let arg = argv.borrow_mut().next()?;
75 let result = parser(&arg);
76 Some((arg, result))
77 });
78
79 match cli {
80 Some((arg, Ok(res))) => {
81 eprintln!("{prompt}: {arg}");
82 return res;
83 }
84 Some((arg, Err(e))) => {
85 eprintln!("{prompt}: {arg}");
86 eprintln!("{e:#}");
87 std::process::exit(1);
88 }
89 None => (),
90 }
91
92 loop {
93 eprint!("{prompt}: ");
94 stderr().flush().unwrap();
95 let mut input = String::new();
96 stdin()
97 .read_line(&mut input)
98 .unwrap_or_else(|_| panic!("Error reading for prompt: {prompt}"));
99 let trimmed = input.trim();
100 match parser(trimmed) {
101 Ok(res) => {
102 return res;
103 }
104 Err(e) => {
105 eprintln!("{e:#}\n\nTry again");
106 }
107 }
108 }
109}
110
111pub fn vector(prompt: &str, len: usize) -> Vec<u32> {
112 raw(prompt, |s| {
113 let v = s[1..s.len() - 1]
114 .split(',')
115 .map(|x| x.trim().parse::<u32>().map_err(|e| e.to_string()))
116 .collect::<Result<Vec<_>, String>>()?;
117 if v.len() != len {
118 return Err(format!(
119 "Target has dimension {} but {} coordinates supplied",
120 len,
121 v.len()
122 ));
123 }
124 Ok(v)
125 })
126}