1use super::Interval;
2use crate::app::{
3 AppMeta, Autocomplete, AutocompleteSuggestion, CommandMatches, ContextAwareParse, Runnable,
4};
5use crate::storage::{Change, KeyValue};
6use crate::utils::CaseInsensitiveStr;
7use async_trait::async_trait;
8use std::fmt;
9use std::iter;
10
11#[derive(Clone, Debug, Eq, PartialEq)]
12pub enum TimeCommand {
13 Add { interval: Interval },
14 Now,
15 Sub { interval: Interval },
16}
17
18#[async_trait(?Send)]
19impl Runnable for TimeCommand {
20 async fn run(self, _input: &str, app_meta: &mut AppMeta) -> Result<String, String> {
21 let time = {
22 let current_time = app_meta
23 .repository
24 .get_key_value(&KeyValue::Time(None))
25 .await
26 .map_err(|_| "Storage error.".to_string())?
27 .time()
28 .unwrap_or_default();
29
30 match &self {
31 Self::Add { interval } => current_time.checked_add(interval),
32 Self::Sub { interval } => current_time.checked_sub(interval),
33 Self::Now => {
34 return Ok(format!("It is currently {}.", current_time.display_long()))
35 }
36 }
37 };
38
39 if let Some(time) = time {
40 let response = format!("It is now {}. Use `undo` to reverse.", time.display_long());
41
42 app_meta
43 .repository
44 .modify(Change::SetKeyValue {
45 key_value: KeyValue::Time(Some(time)),
46 })
47 .await
48 .map(|_| response)
49 .map_err(|_| ())
50 } else {
51 Err(())
52 }
53 .map_err(|_| match &self {
54 Self::Add { interval } => {
55 format!("Unable to advance time by {}.", interval.display_long())
56 }
57 Self::Sub { interval } => {
58 format!("Unable to rewind time by {}.", interval.display_long())
59 }
60 Self::Now => unreachable!(),
61 })
62 }
63}
64
65#[async_trait(?Send)]
66impl ContextAwareParse for TimeCommand {
67 async fn parse_input(input: &str, _app_meta: &AppMeta) -> CommandMatches<Self> {
68 if input.eq_ci("now") {
69 CommandMatches::new_canonical(Self::Now)
70 } else if input.in_ci(&["time", "date"]) {
71 CommandMatches::new_fuzzy(Self::Now)
72 } else if let Some(canonical_match) = input
73 .strip_prefix('+')
74 .and_then(|s| s.parse().ok())
75 .map(|interval| Self::Add { interval })
76 .or_else(|| {
77 input
78 .strip_prefix('-')
79 .and_then(|s| s.parse().ok())
80 .map(|interval| Self::Sub { interval })
81 })
82 {
83 CommandMatches::new_canonical(canonical_match)
84 } else {
85 CommandMatches::default()
86 }
87 }
88}
89
90#[async_trait(?Send)]
91impl Autocomplete for TimeCommand {
92 async fn autocomplete(input: &str, _app_meta: &AppMeta) -> Vec<AutocompleteSuggestion> {
93 if input.starts_with(&['+', '-'][..]) {
94 let suggest = |suffix: &str| -> Result<AutocompleteSuggestion, ()> {
95 let term = format!("{}{}", input, suffix);
96 let summary = if input.starts_with('+') {
97 format!(
98 "advance time by {}",
99 &term[1..].parse::<Interval>()?.display_long(),
100 )
101 } else {
102 format!(
103 "rewind time by {}",
104 &term[1..].parse::<Interval>()?.display_long(),
105 )
106 };
107 Ok(AutocompleteSuggestion::new(term, summary))
108 };
109
110 let suggest_all = || {
111 ["", "d", "h", "m", "s", "r"]
112 .iter()
113 .filter_map(|suffix| suggest(suffix).ok())
114 };
115
116 match input {
117 "+" | "-" => iter::once(AutocompleteSuggestion::new(
118 format!("{}[number]", input),
119 if input == "+" {
120 "advance time"
121 } else {
122 "rewind time"
123 },
124 ))
125 .chain(suggest_all())
126 .collect(),
127 _ => suggest_all().collect(),
128 }
129 } else if !input.is_empty() {
130 ["now", "time", "date"]
131 .into_iter()
132 .filter(|term| term.starts_with_ci(input))
133 .map(|term| AutocompleteSuggestion::new(term, "get the current time"))
134 .collect()
135 } else {
136 Vec::new()
137 }
138 }
139}
140
141impl fmt::Display for TimeCommand {
142 fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
143 match self {
144 Self::Add { interval } => write!(f, "+{}", interval.display_short()),
145 Self::Now => write!(f, "now"),
146 Self::Sub { interval } => write!(f, "-{}", interval.display_short()),
147 }
148 }
149}
150
151#[cfg(test)]
152mod test {
153 use super::*;
154 use crate::test_utils as test;
155
156 #[tokio::test]
157 async fn parse_input_test() {
158 let app_meta = test::app_meta();
159
160 assert_eq!(
161 CommandMatches::new_canonical(TimeCommand::Add {
162 interval: Interval::new(0, 0, 1, 0, 0),
163 }),
164 TimeCommand::parse_input("+1m", &app_meta).await,
165 );
166
167 assert_eq!(
168 CommandMatches::new_canonical(TimeCommand::Add {
169 interval: Interval::new(1, 0, 0, 0, 0),
170 }),
171 TimeCommand::parse_input("+d", &app_meta).await,
172 );
173
174 assert_eq!(
175 CommandMatches::new_canonical(TimeCommand::Sub {
176 interval: Interval::new(0, 10, 0, 0, 0),
177 }),
178 TimeCommand::parse_input("-10h", &app_meta).await,
179 );
180
181 assert_eq!(
182 CommandMatches::default(),
183 TimeCommand::parse_input("1d2h", &app_meta).await,
184 );
185 }
186
187 #[tokio::test]
188 async fn autocomplete_test() {
189 let app_meta = test::app_meta();
190
191 test::assert_empty!(TimeCommand::autocomplete("", &app_meta).await);
192
193 test::assert_autocomplete_eq!(
194 [
195 ("+[number]", "advance time"),
196 ("+d", "advance time by 1 day"),
197 ("+h", "advance time by 1 hour"),
198 ("+m", "advance time by 1 minute"),
199 ("+s", "advance time by 1 second"),
200 ("+r", "advance time by 1 round"),
201 ],
202 TimeCommand::autocomplete("+", &app_meta).await,
203 );
204
205 test::assert_autocomplete_eq!(
206 [
207 ("-[number]", "rewind time"),
208 ("-d", "rewind time by 1 day"),
209 ("-h", "rewind time by 1 hour"),
210 ("-m", "rewind time by 1 minute"),
211 ("-s", "rewind time by 1 second"),
212 ("-r", "rewind time by 1 round"),
213 ],
214 TimeCommand::autocomplete("-", &app_meta).await,
215 );
216
217 test::assert_autocomplete_eq!(
218 [
219 ("+1d", "advance time by 1 day"),
220 ("+1h", "advance time by 1 hour"),
221 ("+1m", "advance time by 1 minute"),
222 ("+1s", "advance time by 1 second"),
223 ("+1r", "advance time by 1 round"),
224 ],
225 TimeCommand::autocomplete("+1", &app_meta).await,
226 );
227
228 test::assert_autocomplete_eq!(
229 [
230 ("+10d", "advance time by 10 days"),
231 ("+10h", "advance time by 10 hours"),
232 ("+10m", "advance time by 10 minutes"),
233 ("+10s", "advance time by 10 seconds"),
234 ("+10r", "advance time by 10 rounds"),
235 ],
236 TimeCommand::autocomplete("+10", &app_meta).await,
237 );
238
239 test::assert_autocomplete_eq!(
240 [
241 ("+10d5h", "advance time by 10 days, 5 hours"),
242 ("+10d5m", "advance time by 10 days, 5 minutes"),
243 ("+10d5s", "advance time by 10 days, 5 seconds"),
244 ("+10d5r", "advance time by 10 days, 5 rounds"),
245 ],
246 TimeCommand::autocomplete("+10d5", &app_meta).await,
247 );
248
249 test::assert_autocomplete_eq!(
250 [
251 ("+10D5h", "advance time by 10 days, 5 hours"),
252 ("+10D5m", "advance time by 10 days, 5 minutes"),
253 ("+10D5s", "advance time by 10 days, 5 seconds"),
254 ("+10D5r", "advance time by 10 days, 5 rounds"),
255 ],
256 TimeCommand::autocomplete("+10D5", &app_meta).await,
257 );
258
259 test::assert_autocomplete_eq!(
260 [("+1d", "advance time by 1 day")],
261 TimeCommand::autocomplete("+1d", &app_meta).await,
262 );
263 test::assert_autocomplete_eq!(
264 [("+1D", "advance time by 1 day")],
265 TimeCommand::autocomplete("+1D", &app_meta).await,
266 );
267 test::assert_autocomplete_eq!(
268 [("+1h", "advance time by 1 hour")],
269 TimeCommand::autocomplete("+1h", &app_meta).await,
270 );
271 test::assert_autocomplete_eq!(
272 [("+1H", "advance time by 1 hour")],
273 TimeCommand::autocomplete("+1H", &app_meta).await,
274 );
275 test::assert_autocomplete_eq!(
276 [("+1m", "advance time by 1 minute")],
277 TimeCommand::autocomplete("+1m", &app_meta).await,
278 );
279 test::assert_autocomplete_eq!(
280 [("+1M", "advance time by 1 minute")],
281 TimeCommand::autocomplete("+1M", &app_meta).await,
282 );
283 test::assert_autocomplete_eq!(
284 [("+1s", "advance time by 1 second")],
285 TimeCommand::autocomplete("+1s", &app_meta).await,
286 );
287 test::assert_autocomplete_eq!(
288 [("+1S", "advance time by 1 second")],
289 TimeCommand::autocomplete("+1S", &app_meta).await,
290 );
291 test::assert_autocomplete_eq!(
292 [("+1r", "advance time by 1 round")],
293 TimeCommand::autocomplete("+1r", &app_meta).await,
294 );
295 test::assert_autocomplete_eq!(
296 [("+1R", "advance time by 1 round")],
297 TimeCommand::autocomplete("+1R", &app_meta).await,
298 );
299 }
300
301 #[tokio::test]
302 async fn display_test() {
303 let app_meta = test::app_meta();
304
305 for command in [
306 TimeCommand::Add {
307 interval: Interval::new(2, 3, 4, 5, 6),
308 },
309 TimeCommand::Now,
310 TimeCommand::Sub {
311 interval: Interval::new(2, 3, 4, 5, 6),
312 },
313 ] {
314 let command_string = command.to_string();
315 assert_ne!("", command_string);
316
317 assert_eq!(
318 CommandMatches::new_canonical(command.clone()),
319 TimeCommand::parse_input(&command_string, &app_meta).await,
320 "{}",
321 command_string,
322 );
323
324 assert_eq!(
325 CommandMatches::new_canonical(command),
326 TimeCommand::parse_input(&command_string.to_uppercase(), &app_meta).await,
327 "{}",
328 command_string.to_uppercase(),
329 );
330 }
331 }
332}