clock_tui/app/
modes.rs

1mod clock;
2mod countdown;
3mod pause;
4mod stopwatch;
5mod timer;
6
7use std::cmp::min;
8use std::fmt::Write as _;
9
10use crate::clock_text::ClockText;
11use chrono::Duration;
12pub(crate) use clock::Clock;
13pub(crate) use countdown::Countdown;
14pub(crate) use pause::Pause;
15use ratatui::{
16    buffer::Buffer,
17    layout::Rect,
18    style::Style,
19    text::Span,
20    widgets::{Paragraph, Widget},
21};
22pub(crate) use stopwatch::Stopwatch;
23pub(crate) use timer::Timer;
24
25#[derive(Copy, Clone)]
26pub(crate) enum DurationFormat {
27    /// Hours, minutes, seconds, deciseconds
28    HourMinSecDeci,
29    /// Hours, minutes, seconds
30    HourMinSec,
31}
32
33fn format_duration(duration: Duration, format: DurationFormat) -> String {
34    let is_neg = duration < Duration::zero();
35    let duration = if is_neg { -duration } else { duration };
36
37    let millis = duration.num_milliseconds();
38    let seconds = millis / 1000;
39    let minutes = seconds / 60;
40    let hours = minutes / 60;
41    let days = hours / 24;
42    let mut result = String::new();
43
44    fn append_number(s: &mut String, num: i64) {
45        if s.is_empty() {
46            let _ = write!(s, "{}", num);
47        } else {
48            let _ = write!(s, "{:02}", num);
49        }
50    }
51
52    if days > 0 {
53        let _ = write!(result, "{}:", days);
54    }
55    if hours > 0 {
56        append_number(&mut result, hours % 24);
57        result.push(':');
58    }
59    append_number(&mut result, minutes % 60);
60    result.push(':');
61
62    if is_neg {
63        result.insert(0, '-');
64    }
65    match format {
66        DurationFormat::HourMinSecDeci => {
67            let _ = write!(result, "{:02}.{}", seconds % 60, (millis % 1000) / 100);
68        }
69        DurationFormat::HourMinSec => {
70            let _ = write!(result, "{:02}", seconds % 60);
71        }
72    }
73
74    result
75}
76
77fn render_centered(
78    area: Rect,
79    buf: &mut Buffer,
80    text: &ClockText,
81    header: Option<String>,
82    footer: Option<String>,
83) {
84    let text_size = text.size();
85    let text_area = Rect {
86        x: area.x + (area.width.saturating_sub(text_size.0)) / 2,
87        y: area.y + (area.height.saturating_sub(text_size.1)) / 2,
88        width: min(text_size.0, area.width),
89        height: min(text_size.1, area.height),
90    };
91    text.clone().render(text_area, buf);
92
93    let render_text_center = |text: &str, top: u16, buf: &mut Buffer| {
94        let text_len = text.len() as u16;
95        let paragrahp = Paragraph::new(Span::from(text)).style(Style::default());
96
97        let para_area = Rect {
98            x: area.left() + (area.width.saturating_sub(text_len)) / 2,
99            y: top,
100            width: min(text_len, area.width),
101            height: min(1, area.height),
102        };
103        paragrahp.render(para_area, buf);
104    };
105
106    if let Some(text) = header {
107        if area.top() + 2 <= text_area.top() {
108            render_text_center(text.as_str(), text_area.top() - 2, buf);
109        }
110    }
111
112    if let Some(text) = footer {
113        if area.bottom() >= text_area.bottom() + 2 {
114            render_text_center(text.as_str(), text_area.bottom() + 1, buf);
115        }
116    }
117}