1use chrono::DateTime;
2use chrono::Duration;
3use chrono::Local;
4use chrono::NaiveDate;
5use chrono::NaiveDateTime;
6use chrono::NaiveTime;
7use chrono::TimeZone;
8use chrono_tz::Tz;
9use clap::Subcommand;
10use crossterm::event::KeyCode;
11use ratatui::{
12 style::{Color, Style},
13 Frame,
14};
15use regex::Regex;
16
17use self::modes::Clock;
18use self::modes::Countdown;
19use self::modes::DurationFormat;
20use self::modes::Pause;
21use self::modes::Stopwatch;
22use self::modes::Timer;
23
24pub mod modes;
25
26#[derive(Debug, Subcommand)]
27pub enum Mode {
28 Clock {
30 #[clap(short = 'z', long, value_parser=parse_timezone)]
32 timezone: Option<Tz>,
33 #[clap(short = 'D', long, action)]
35 no_date: bool,
36 #[clap(short = 'S', long, action)]
38 no_seconds: bool,
39 #[clap(short, long, action)]
41 millis: bool,
42 },
43 Timer {
45 #[clap(short, long="duration", value_parser = parse_duration, num_args = 1.., default_value = "5m")]
48 durations: Vec<Duration>,
49
50 #[clap(short, long = "title", num_args = 0..)]
52 titles: Vec<String>,
53
54 #[clap(long, short, action)]
56 repeat: bool,
57
58 #[clap(long = "no-millis", short = 'M', action)]
60 no_millis: bool,
61
62 #[clap(long = "paused", short = 'P', action)]
64 paused: bool,
65
66 #[clap(long = "quit", short = 'Q', action)]
68 auto_quit: bool,
69
70 #[clap(long, short, num_args = 1.., allow_hyphen_values = true)]
72 execute: Vec<String>,
73 },
74 Stopwatch,
76 Countdown {
78 #[clap(long, short, value_parser = parse_datetime)]
80 time: DateTime<Local>,
81
82 #[clap(long, short = 'T')]
84 title: Option<String>,
85
86 #[clap(long = "continue", short = 'c', action)]
88 continue_on_zero: bool,
89
90 #[clap(long, short, action)]
92 reverse: bool,
93
94 #[clap(short, long, action)]
96 millis: bool,
97 },
98}
99
100use crate::config::Config;
101
102#[derive(clap::Parser, Default)]
103#[clap(name = "tclock", about = "A clock app in terminal", long_about = None)]
104pub struct App {
105 #[clap(subcommand)]
106 pub mode: Option<Mode>,
107 #[clap(short, long, value_parser = parse_color)]
111 pub color: Option<Color>,
112 #[clap(short, long, value_parser)]
114 pub size: Option<u16>,
115
116 #[clap(skip)]
117 clock: Option<Clock>,
118 #[clap(skip)]
119 timer: Option<Timer>,
120 #[clap(skip)]
121 stopwatch: Option<Stopwatch>,
122 #[clap(skip)]
123 countdown: Option<Countdown>,
124}
125
126impl App {
127 pub fn set_mode(&mut self, mode: Mode) {
128 self.mode = Some(mode);
129 self.init_app();
130 }
131
132 pub fn init_app(&mut self) {
133 let config = Config::load();
135 let default_config = config.as_ref().map(|c| &c.default);
136
137 if self.mode.is_none() {
139 self.mode = default_config.map(|c| match c.mode.as_str() {
140 "timer" => {
141 let timer_config = config.as_ref().map(|c| &c.timer);
142 Mode::Timer {
143 durations: timer_config
144 .map(|c| {
145 c.durations
146 .iter()
147 .filter_map(|d| parse_duration(d).ok())
148 .collect()
149 })
150 .unwrap_or_else(|| vec![Duration::minutes(25), Duration::minutes(5)]),
151 titles: timer_config.map(|c| c.titles.clone()).unwrap_or_default(),
152 repeat: timer_config.map(|c| c.repeat).unwrap_or(false),
153 no_millis: !timer_config.map(|c| c.show_millis).unwrap_or(true),
154 paused: timer_config.map(|c| c.start_paused).unwrap_or(false),
155 auto_quit: timer_config.map(|c| c.auto_quit).unwrap_or(false),
156 execute: timer_config.map(|c| c.execute.clone()).unwrap_or_default(),
157 }
158 }
159 "stopwatch" => Mode::Stopwatch,
160 "countdown" => {
161 let countdown_config = config.as_ref().map(|c| &c.countdown);
162 Mode::Countdown {
163 time: countdown_config
164 .and_then(|c| c.time.as_ref())
165 .and_then(|t| parse_datetime(t).ok())
166 .unwrap_or_else(|| Local::now()),
167 title: countdown_config.map(|c| c.title.clone()).unwrap_or(None),
168 continue_on_zero: countdown_config
169 .map(|c| c.continue_on_zero)
170 .unwrap_or(false),
171 reverse: countdown_config.map(|c| c.reverse).unwrap_or(false),
172 millis: countdown_config.map(|c| c.show_millis).unwrap_or(false),
173 }
174 }
175 _ => {
176 let clock_config = config.as_ref().map(|c| &c.clock);
177 Mode::Clock {
178 no_date: !clock_config.map(|c| c.show_date).unwrap_or(true),
179 millis: clock_config.map(|c| c.show_millis).unwrap_or(false),
180 no_seconds: !clock_config.map(|c| c.show_seconds).unwrap_or(true),
181 timezone: clock_config.and_then(|c| c.timezone),
182 }
183 }
184 });
185 }
186
187 if self.color.is_none() {
189 self.color = default_config
190 .map(|c| parse_color(&c.color).unwrap_or(Color::Green))
191 .or(Some(Color::Green));
192 }
193 if self.size.is_none() {
194 self.size = default_config.map(|c| c.size).or(Some(1));
195 }
196
197 let style = Style::default().fg(self.color.unwrap_or(Color::Green));
198 let size = self.size.unwrap_or(1);
199
200 match self.mode.as_ref().unwrap_or(&Mode::Clock {
202 no_date: false,
203 millis: false,
204 no_seconds: false,
205 timezone: None,
206 }) {
207 Mode::Clock {
208 no_date,
209 no_seconds,
210 millis,
211 timezone,
212 } => {
213 let clock_config = config.as_ref().map(|c| &c.clock);
214 self.clock = Some(Clock {
215 size,
216 style,
217 show_date: !no_date && clock_config.map(|c| c.show_date).unwrap_or(true),
218 show_millis: *millis || clock_config.map(|c| c.show_millis).unwrap_or(false),
219 show_secs: !no_seconds && clock_config.map(|c| c.show_seconds).unwrap_or(true),
220 timezone: timezone.or_else(|| clock_config.and_then(|c| c.timezone)),
221 });
222 }
223 Mode::Timer {
224 durations,
225 titles,
226 repeat,
227 no_millis,
228 paused,
229 auto_quit,
230 execute,
231 } => {
232 let timer_config = config.as_ref().map(|c| &c.timer);
233 let format = if *no_millis {
234 DurationFormat::HourMinSec
235 } else {
236 DurationFormat::HourMinSecDeci
237 };
238 self.timer = Some(Timer::new(
239 size,
240 style,
241 durations.to_owned(),
242 titles.to_owned(),
243 *repeat || timer_config.map(|c| c.repeat).unwrap_or(false),
244 format,
245 *paused || timer_config.map(|c| c.start_paused).unwrap_or(false),
246 *auto_quit || timer_config.map(|c| c.auto_quit).unwrap_or(false),
247 execute.to_owned(),
248 ));
249 }
250 Mode::Stopwatch => {
251 self.stopwatch = Some(Stopwatch::new(size, style));
252 }
253 Mode::Countdown {
254 time,
255 title,
256 continue_on_zero,
257 reverse,
258 millis,
259 } => {
260 let countdown_config = config.as_ref().map(|c| &c.countdown);
261 self.countdown = Some(Countdown {
262 size,
263 style,
264 time: *time,
265 title: title.to_owned(),
266 continue_on_zero: *continue_on_zero
267 || countdown_config
268 .map(|c| c.continue_on_zero)
269 .unwrap_or(false),
270 reverse: *reverse || countdown_config.map(|c| c.reverse).unwrap_or(false),
271 format: if *millis || countdown_config.map(|c| c.show_millis).unwrap_or(false) {
272 DurationFormat::HourMinSecDeci
273 } else {
274 DurationFormat::HourMinSec
275 },
276 })
277 }
278 }
279 }
280
281 pub fn ui(&self, f: &mut Frame) {
282 if let Some(ref w) = self.clock {
283 f.render_widget(w, f.size());
284 } else if let Some(ref w) = self.timer {
285 f.render_widget(w, f.size());
286 } else if let Some(ref w) = self.stopwatch {
287 f.render_widget(w, f.size());
288 } else if let Some(ref w) = self.countdown {
289 f.render_widget(w, f.size());
290 }
291 }
292
293 pub fn on_key(&mut self, key: KeyCode) {
294 if let Some(_w) = self.clock.as_mut() {
295 } else if let Some(w) = self.timer.as_mut() {
296 handle_key(w, key);
297 } else if let Some(w) = self.stopwatch.as_mut() {
298 handle_key(w, key);
299 }
300 }
301
302 pub fn is_ended(&self) -> bool {
303 if let Some(ref w) = self.timer {
304 return w.is_finished();
305 }
306 false
307 }
308
309 pub fn on_exit(&self) {
310 if let Some(ref w) = self.stopwatch {
311 println!("Stopwatch time: {}", w.get_display_time());
312 }
313 }
314}
315
316fn handle_key<T: Pause>(widget: &mut T, key: KeyCode) {
317 if let KeyCode::Char(' ') = key {
318 widget.toggle_paused()
319 }
320}
321
322fn parse_duration(s: &str) -> Result<Duration, String> {
323 let reg = Regex::new(r"^(\d+)([smhdSMHD])$").unwrap();
324 let cap = reg
325 .captures(s)
326 .ok_or_else(|| format!("{} is not a valid duration", s))?;
327
328 let num = cap.get(1).unwrap().as_str().parse::<i64>().unwrap();
329 let unit = cap.get(2).unwrap().as_str().to_lowercase();
330
331 match unit.as_str() {
332 "s" => Ok(Duration::seconds(num)),
333 "m" => Ok(Duration::minutes(num)),
334 "h" => Ok(Duration::hours(num)),
335 "d" => Ok(Duration::days(num)),
336 _ => Err(format!("Invalid duration: {}", s)),
337 }
338}
339
340fn parse_color(s: &str) -> Result<Color, String> {
341 let s = s.to_lowercase();
342 let reg = Regex::new(r"^#([0-9a-f]{6})$").unwrap();
343 match s.as_str() {
344 "black" => Ok(Color::Black),
345 "red" => Ok(Color::Red),
346 "green" => Ok(Color::Green),
347 "yellow" => Ok(Color::Yellow),
348 "blue" => Ok(Color::Blue),
349 "magenta" => Ok(Color::Magenta),
350 "cyan" => Ok(Color::Cyan),
351 "gray" => Ok(Color::Gray),
352 "darkgray" => Ok(Color::DarkGray),
353 "lightred" => Ok(Color::LightRed),
354 "lightgreen" => Ok(Color::LightGreen),
355 "lightyellow" => Ok(Color::LightYellow),
356 "lightblue" => Ok(Color::LightBlue),
357 "lightmagenta" => Ok(Color::LightMagenta),
358 "lightcyan" => Ok(Color::LightCyan),
359 "white" => Ok(Color::White),
360 s => {
361 let cap = reg
362 .captures(s)
363 .ok_or_else(|| format!("Invalid color: {}", s))?;
364 let hex = cap.get(1).unwrap().as_str();
365 let r = u8::from_str_radix(&hex[0..2], 16).unwrap();
366 let g = u8::from_str_radix(&hex[2..4], 16).unwrap();
367 let b = u8::from_str_radix(&hex[4..], 16).unwrap();
368 Ok(Color::Rgb(r, g, b))
369 }
370 }
371}
372
373fn parse_datetime(s: &str) -> Result<DateTime<Local>, String> {
374 let s = s.trim();
375 let today = Local::now().date_naive();
376
377 let time = NaiveTime::parse_from_str(s, "%H:%M");
378 if let Ok(time) = time {
379 let time = NaiveDateTime::new(today, time);
380 return Ok(Local.from_local_datetime(&time).unwrap());
381 }
382
383 let time = NaiveTime::parse_from_str(s, "%H:%M:%S");
384 if let Ok(time) = time {
385 let time = NaiveDateTime::new(today, time);
386 return Ok(Local.from_local_datetime(&time).unwrap());
387 }
388
389 let date = NaiveDate::parse_from_str(s, "%Y-%m-%d");
390 if let Ok(date) = date {
391 let time = NaiveDateTime::new(date, NaiveTime::from_hms_opt(0, 0, 0).unwrap());
392 return Ok(Local.from_local_datetime(&time).unwrap());
393 }
394
395 let date_time = NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S");
396 if let Ok(date_time) = date_time {
397 return Ok(Local.from_local_datetime(&date_time).unwrap());
398 }
399
400 let rfc_time = DateTime::parse_from_rfc3339(s);
401 if let Ok(rfc_time) = rfc_time {
402 return Ok(rfc_time.with_timezone(&Local));
403 }
404
405 Err("Invalid time format".to_string())
406}
407
408fn parse_timezone(s: &str) -> Result<Tz, String> {
409 s.parse()
410}