clock_cli/tui/
timer.rs

1// Copyright (C) 2020 Tianyi Shi
2//
3// This file is part of clock-cli-rs.
4//
5// clock-cli-rs is free software: you can redistribute it and/or modify
6// it under the terms of the GNU General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// clock-cli-rs is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13// GNU General Public License for more details.
14//
15// You should have received a copy of the GNU General Public License
16// along with clock-cli-rs.  If not, see <http://www.gnu.org/licenses/>.
17
18//! # Countdown Timer TUI
19//!
20//! ## Expected Behavior
21//!
22//! On start, the user sets the expected duration (HH:MM:SS). Left/right arrow keys and the TAB key move the focus. A list of frequently/recently used durations is provided. Press "Enter" to start.
23//!
24//! When the timer is running, press "Space" to pause/resume and "Enter" to cancel.
25//!
26//! When the timer finishes (when counting to 00:00:00 or cancelled), the callback set with `on_finish()` is called.
27
28use crate::utils::PrettyDuration;
29// use chrono::{DateTime, Duration, Local};
30use chrono::Duration;
31use clock_core::timer::{Timer, TimerData};
32use cursive::{
33    event::{Callback, Event, EventResult, Key},
34    theme::ColorStyle,
35    view::View,
36    Cursive, Printer, Vec2, With,
37};
38use std::rc::Rc;
39
40#[derive(Copy, Clone)]
41enum TimerViewState {
42    Config,
43    Running,
44    Finished,
45}
46
47struct TimerViewConfig {
48    h: u8,
49    m: u8,
50    s: u8,
51    focus: u8, // match focus % 3 {0 => h, 1 => m, 2 => s}
52    input_buffer: Vec<u8>,
53}
54
55pub struct TimerView {
56    timer: Timer,
57    remaining: Duration,
58    state: TimerViewState,
59    config: TimerViewConfig,
60    on_finish: Option<Rc<dyn Fn(&mut Cursive, TimerData)>>,
61}
62
63impl TimerView {
64    pub fn new(h: u8, m: u8, s: u8) -> Self {
65        let config = TimerViewConfig {
66            h,
67            m,
68            s,
69            focus: 1,
70            input_buffer: Vec::new(),
71        };
72        Self {
73            timer: Timer::new(Duration::zero()),
74            remaining: Duration::zero(),
75            config,
76            state: TimerViewState::Config,
77            on_finish: None,
78        }
79    }
80
81    pub fn start(&mut self) {
82        let seconds =
83            self.config.h as i64 * 3600 + self.config.m as i64 * 60 + self.config.s as i64;
84        self.timer = Timer::new(Duration::seconds(seconds));
85        self.state = TimerViewState::Running;
86        self.timer.pause_or_resume();
87    }
88
89    /// Sets a callback to be used when `<Enter>` is pressed or counting to 00:00:00
90    ///
91    /// The elapsed time will be given to the callback.
92    ///
93    /// See also cursive::views::select_view::SelectView::set_on_submit
94    pub fn set_on_finish<F, R>(&mut self, cb: F)
95    where
96        F: 'static + Fn(&mut Cursive, TimerData) -> R,
97    {
98        self.on_finish = Some(Rc::new(move |s, t| {
99            cb(s, t);
100        }));
101    }
102
103    pub fn on_finish<F, R>(self, cb: F) -> Self
104    where
105        F: 'static + Fn(&mut Cursive, TimerData) -> R,
106    {
107        self.with(|s| s.set_on_finish(cb))
108    }
109
110    fn finish(&mut self) -> EventResult {
111        self.state = TimerViewState::Finished;
112        let data = self.timer.stop();
113        if self.on_finish.is_some() {
114            let cb = self.on_finish.clone().unwrap();
115            EventResult::Consumed(Some(Callback::from_fn_once(move |s| cb(s, data))))
116        } else {
117            EventResult::Consumed(None)
118        }
119    }
120
121    fn draw_running(&self, printer: &Printer) {
122        printer.print((0, 0), &self.remaining.pretty());
123    }
124
125    fn draw_finished(&self, printer: &Printer) {
126        printer.print((0, 0), "FINISHED!");
127    }
128
129    fn draw_config(&self, printer: &Printer) {
130        fn format(n: u8) -> String {
131            format!("{:02}", n)
132        }
133        let (h, m, s) = (
134            format(self.config.h),
135            format(self.config.m),
136            format(self.config.s),
137        );
138
139        if self.config.focus % 3 == 0 {
140            printer.with_color(ColorStyle::highlight(), |printer| printer.print((0, 0), &h));
141        } else {
142            printer.print((0, 0), &h);
143        }
144        printer.print((2, 0), ":");
145        if self.config.focus % 3 == 1 {
146            printer.with_color(ColorStyle::highlight(), |printer| printer.print((3, 0), &m));
147        } else {
148            printer.print((3, 0), &m);
149        }
150        printer.print((5, 0), ":");
151        if self.config.focus % 3 == 2 {
152            printer.with_color(ColorStyle::highlight(), |printer| printer.print((6, 0), &s));
153        } else {
154            printer.print((6, 0), &s);
155        }
156    }
157
158    // fn get_selection(&self) -> u8 {
159    //     match self.config.focus % 3 {
160    //         0 => self.config.h,
161    //         1 => self.config.m,
162    //         2 => self.config.s,
163    //         _ => unreachable!(),
164    //     }
165    // }
166
167    fn set_selection(&mut self, v: u8) {
168        match self.config.focus % 3 {
169            0 => self.config.h = v,
170            1 => self.config.m = v,
171            2 => self.config.s = v,
172            _ => unreachable!(),
173        }
174    }
175
176    fn move_focus_right(&mut self) {
177        self.config.focus += 1;
178        self.config.input_buffer.clear();
179    }
180
181    fn move_focus_left(&mut self) {
182        self.config.focus -= 1;
183        self.config.input_buffer.clear();
184    }
185
186    fn read_buffer(&self) -> u8 {
187        let buffer = &self.config.input_buffer;
188        let n = match buffer.len() {
189            0 => 0,
190            1 => buffer[0],
191            2 => buffer[0] * 10 + buffer[1],
192            _ => unreachable!(),
193        };
194        match self.config.focus % 3 {
195            0 => n,
196            1 | 2 => {
197                if n < 60 {
198                    n
199                } else {
200                    59
201                }
202            }
203            _ => unreachable!(),
204        }
205    }
206}
207impl View for TimerView {
208    fn draw(&self, printer: &Printer) {
209        match self.state {
210            TimerViewState::Running => self.draw_running(printer),
211            TimerViewState::Config => self.draw_config(printer),
212            TimerViewState::Finished => self.draw_finished(printer),
213        }
214    }
215
216    fn required_size(&mut self, _constraint: Vec2) -> Vec2 {
217        // the required size depends on how many lap times the user want to diaplay
218        Vec2::new(12, 1) // columns, rows (width, height)
219    }
220
221    fn on_event(&mut self, event: Event) -> EventResult {
222        match self.state {
223            TimerViewState::Running => {
224                match event {
225                    // pause/resume the timer when pressing "Space"
226                    Event::Char(' ') => {
227                        self.timer.pause_or_resume();
228                    }
229                    Event::Refresh => {
230                        self.remaining = self.timer.read();
231                        if self.remaining.num_milliseconds() < 10 {
232                            return self.finish();
233                        }
234                    }
235                    // calcel
236                    Event::Key(Key::Enter) => {
237                        return self.finish();
238                    }
239                    _ => {
240                        if self.timer.data.remaining.num_milliseconds() < 10 {
241                            self.state = TimerViewState::Finished;
242                            return self.finish();
243                        }
244                    } //return EventResult::Ignored,
245                }
246            }
247            TimerViewState::Finished => match event {
248                Event::Char(' ') | Event::Key(Key::Enter) => {
249                    self.state = TimerViewState::Config;
250                }
251                _ => return EventResult::Ignored,
252            },
253            TimerViewState::Config => match event {
254                Event::Char(c) => {
255                    if c.is_numeric() {
256                        self.config.input_buffer.push(c.to_digit(10).unwrap() as u8);
257                    }
258                    self.set_selection(self.read_buffer());
259                    if self.config.input_buffer.len() == 2 {
260                        self.move_focus_right();
261                    }
262                }
263                Event::Key(Key::Right) | Event::Key(Key::Tab) => {
264                    self.move_focus_right();
265                }
266                Event::Key(Key::Left) => self.move_focus_left(),
267                Event::Key(Key::Enter) => {
268                    self.start();
269                }
270                // Event::Mouse {
271                //     offset,
272                //     position,
273                //     event,
274                // } => match event {
275                //     MouseEvent::WheelDown => self.set_selection(self.get_selection() - 1),
276                //     MouseEvent::WheelUp => self.set_selection(self.get_selection() + 1),
277                // },
278                _ => return EventResult::Ignored,
279            },
280        }
281        EventResult::Consumed(None)
282    }
283}