Skip to main content

tui/widgets/
date.rs

1//! Free-text date entry widget.
2//!
3//! Wraps an [`Editor`] for free-text date entry (`YYYY-MM-DDTHH:MM` / RFC3339).
4//! New widgets are pre-seeded with the current instant. Up/Down day-stepping is
5//! available via [`DateWidget::step`].
6
7use cli_core::forms::{now_template, step_date};
8
9use super::editor::{EditMode, Editor};
10
11/// A text field for date entry.
12#[derive(Debug, Clone)]
13pub struct DateWidget {
14    editor: Editor,
15}
16
17impl DateWidget {
18    /// Create a new date widget pre-seeded with the current instant (`YYYY-MM-DDTHH:MM`).
19    #[must_use]
20    pub fn new(mode: EditMode) -> Self {
21        Self {
22            editor: Editor::with_buffer(mode, now_template()),
23        }
24    }
25
26    #[must_use]
27    pub fn with_value(mode: EditMode, value: impl Into<String>) -> Self {
28        Self {
29            editor: Editor::with_buffer(mode, value),
30        }
31    }
32
33    /// Buffer text (the current date string).
34    #[must_use]
35    pub fn value(&self) -> &str {
36        self.editor.buffer()
37    }
38
39    /// Direct access to the inner editor.
40    #[must_use]
41    pub fn as_editor_mut(&mut self) -> &mut Editor {
42        &mut self.editor
43    }
44
45    /// Advance the date by `delta_days` days, preserving the time-of-day component.
46    ///
47    /// On parse failure the buffer is replaced with a now-relative step. On
48    /// date-range overflow the buffer is left unchanged.
49    pub fn step(&mut self, delta_days: i64) {
50        if let Some(new_val) = step_date(self.value(), delta_days) {
51            self.editor.replace_buffer(new_val);
52        }
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59
60    #[test]
61    fn new_buffer_matches_now_template_format() {
62        let w = DateWidget::new(EditMode::Emacs);
63        let v = w.value();
64        assert_eq!(v.len(), 16, "YYYY-MM-DDTHH:MM is 16 chars, got: {v}");
65        assert!(v.contains('T'), "must contain T separator: {v}");
66        assert!(
67            cli_core::forms::validate_date(v).is_ok(),
68            "must be valid: {v}"
69        );
70    }
71
72    #[test]
73    fn with_value_sets_initial_buffer() {
74        let w = DateWidget::with_value(EditMode::Emacs, "2024-12-31");
75        assert_eq!(w.value(), "2024-12-31");
76    }
77
78    #[test]
79    fn free_typing_appends_to_buffer() {
80        let mut w = DateWidget::with_value(EditMode::Emacs, "");
81        for c in "2024-01-01".chars() {
82            w.as_editor_mut().insert_char(c);
83        }
84        assert_eq!(w.value(), "2024-01-01");
85    }
86
87    #[test]
88    fn step_forward_advances_one_day() {
89        let mut w = DateWidget::with_value(EditMode::Emacs, "2024-01-31T10:30");
90        w.step(1);
91        assert_eq!(w.value(), "2024-02-01T10:30");
92    }
93
94    #[test]
95    fn step_back_retreats_one_day() {
96        let mut w = DateWidget::with_value(EditMode::Emacs, "2024-03-01T08:15");
97        w.step(-1);
98        assert_eq!(w.value(), "2024-02-29T08:15");
99    }
100
101    #[test]
102    fn step_forward_output_accepted_by_validate_date() {
103        let mut w = DateWidget::with_value(EditMode::Emacs, "2024-06-15T12:00");
104        w.step(1);
105        assert!(cli_core::forms::validate_date(w.value()).is_ok());
106    }
107}