1use std::collections::BTreeMap;
7use std::path::Path;
8
9use chrono::{Datelike, Local, Timelike};
10
11#[derive(Clone, Debug, PartialEq, Eq)]
13pub struct Define {
14 pub name: String,
15 pub width: usize,
16 pub question: String,
17 pub default: String,
18}
19
20impl Define {
21 pub fn parse(text: &[u8]) -> Option<Define> {
22 let text = String::from_utf8_lossy(text);
23 let (head, rest) = text.split_once(':')?;
24 let (name, width) = match head.split_once(',') {
25 Some((n, w)) => (n, w.trim().parse().unwrap_or(60)),
26 None => (head, 60),
27 };
28 let name = name.trim().to_ascii_uppercase();
29 if !is_name(&name) {
30 return None;
31 }
32 let (question, default) = match rest.strip_prefix('?').and_then(|r| r.split_once('?')) {
33 Some((q, d)) => (q.to_string(), d.to_string()),
34 None => (String::new(), rest.to_string()),
35 };
36 Some(Define { name, width: width.clamp(1, 60), question, default })
37 }
38
39 pub fn prompt(&self) -> String {
41 if self.question.is_empty() { format!("Enter {}", self.name) } else { self.question.clone() }
42 }
43}
44
45pub fn is_name(s: &str) -> bool {
48 (1..=12).contains(&s.len())
49 && s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
50 && s.starts_with(|c: char| c.is_ascii_alphabetic())
51}
52
53pub fn builtin(name: &str, mouse: Option<(usize, usize)>) -> Option<String> {
56 let now = Local::now();
57 let (x, y) = mouse.unwrap_or((0, 0));
58 let hour12 = match now.hour() % 12 {
59 0 => 12,
60 h => h,
61 };
62 Some(match name {
63 "RIPVER" => String::from_utf8_lossy(crate::rip::VERSION_REPLY).into_owned(),
64 "DATE" => now.format("%m/%d/%y").to_string(),
65 "MONTH" => now.format("%B").to_string(),
66 "MONTHNUM" => format!("{:02}", now.month()),
67 "DAY" => format!("{:02}", now.day()),
68 "DOY" => format!("{:03}", now.ordinal()),
69 "YEAR" => format!("{:02}", now.year() % 100),
70 "FYEAR" => format!("{:04}", now.year()),
71 "TIME" => now.format("%H:%M:%S").to_string(),
72 "HOUR" => format!("{hour12:02}"),
73 "MHOUR" => format!("{:02}", now.hour()),
74 "MIN" => format!("{:02}", now.minute()),
75 "SEC" => format!("{:02}", now.second()),
76 "AMPM" => if now.hour() < 12 { "AM" } else { "PM" }.into(),
77 "DATETIME" => now.format("%a %b %d %H:%M:%S %Y").to_string(),
78 "TIMEZONE" => now.format("%Z").to_string(),
79 "DOW" => now.format("%A").to_string(),
80 "ADOW" => now.format("%a").to_string(),
81 "WDAY" => format!("{}", now.weekday().num_days_from_sunday()),
82 "WOY" => now.format("%U").to_string(),
83 "WOYM" => now.format("%W").to_string(),
84 "X" => format!("{x:04}"),
85 "Y" => format!("{y:04}"),
86 "XY" => format!("{x:04}:{y:04}"),
87 "XYM" => format!("{x:04}:{y:04}:000"),
88 "M" => "000".into(),
89 "MSTAT" => "YES".into(),
90 "STATBAR" | "TWIN" | "CURSOR" => "ON".into(),
91 _ => return None,
92 })
93}
94
95pub fn load(path: &Path) -> BTreeMap<String, String> {
97 std::fs::read_to_string(path)
98 .unwrap_or_default()
99 .lines()
100 .filter_map(|l| l.split_once('='))
101 .filter(|(n, _)| is_name(n))
102 .map(|(n, v)| (n.to_string(), v.to_string()))
103 .collect()
104}
105
106pub fn save(path: &Path, vars: &BTreeMap<String, String>) -> std::io::Result<()> {
107 if let Some(dir) = path.parent() {
108 std::fs::create_dir_all(dir)?;
109 }
110 let text: String = vars.iter().map(|(n, v)| format!("{n}={}\n", v.replace(['\r', '\n'], " "))).collect();
111 std::fs::write(path, text)
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 #[test]
119 fn definitions_and_builtins() {
120 let d = Define::parse(b"full_name,30:?What is your full name?John Doe").unwrap();
121 assert_eq!(
122 d,
123 Define {
124 name: "FULL_NAME".into(),
125 width: 30,
126 question: "What is your full name".into(),
127 default: "John Doe".into()
128 }
129 );
130 let d = Define::parse(b"AGE:").unwrap();
131 assert_eq!((d.width, d.prompt(), d.default.as_str()), (60, "Enter AGE".into(), ""));
132 assert!(Define::parse(b"_X:").is_none() && Define::parse(b"no colon").is_none());
133 assert_eq!(builtin("RIPVER", None).as_deref(), Some("RIPSCRIP015400"));
134 assert_eq!(builtin("XY", Some((297, 321))).as_deref(), Some("0297:0321"));
135 assert_eq!(builtin("DATE", None).unwrap().len(), 8);
136 assert!(builtin("NOPE", None).is_none());
137 }
138}