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 HourMinSecDeci,
29 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}