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

            
7
use cli_core::forms::{now_template, step_date};
8

            
9
use super::editor::{EditMode, Editor};
10

            
11
/// A text field for date entry.
12
#[derive(Debug, Clone)]
13
pub struct DateWidget {
14
    editor: Editor,
15
}
16

            
17
impl DateWidget {
18
    /// Create a new date widget pre-seeded with the current instant (`YYYY-MM-DDTHH:MM`).
19
    #[must_use]
20
22
    pub fn new(mode: EditMode) -> Self {
21
22
        Self {
22
22
            editor: Editor::with_buffer(mode, now_template()),
23
22
        }
24
22
    }
25

            
26
    #[must_use]
27
11
    pub fn with_value(mode: EditMode, value: impl Into<String>) -> Self {
28
11
        Self {
29
11
            editor: Editor::with_buffer(mode, value),
30
11
        }
31
11
    }
32

            
33
    /// Buffer text (the current date string).
34
    #[must_use]
35
22
    pub fn value(&self) -> &str {
36
22
        self.editor.buffer()
37
22
    }
38

            
39
    /// Direct access to the inner editor.
40
    #[must_use]
41
11
    pub fn as_editor_mut(&mut self) -> &mut Editor {
42
11
        &mut self.editor
43
11
    }
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
5
    pub fn step(&mut self, delta_days: i64) {
50
5
        if let Some(new_val) = step_date(self.value(), delta_days) {
51
5
            self.editor.replace_buffer(new_val);
52
5
        }
53
5
    }
54
}
55

            
56
#[cfg(test)]
57
mod tests {
58
    use super::*;
59

            
60
    #[test]
61
1
    fn new_buffer_matches_now_template_format() {
62
1
        let w = DateWidget::new(EditMode::Emacs);
63
1
        let v = w.value();
64
1
        assert_eq!(v.len(), 16, "YYYY-MM-DDTHH:MM is 16 chars, got: {v}");
65
1
        assert!(v.contains('T'), "must contain T separator: {v}");
66
1
        assert!(
67
1
            cli_core::forms::validate_date(v).is_ok(),
68
            "must be valid: {v}"
69
        );
70
1
    }
71

            
72
    #[test]
73
1
    fn with_value_sets_initial_buffer() {
74
1
        let w = DateWidget::with_value(EditMode::Emacs, "2024-12-31");
75
1
        assert_eq!(w.value(), "2024-12-31");
76
1
    }
77

            
78
    #[test]
79
1
    fn free_typing_appends_to_buffer() {
80
1
        let mut w = DateWidget::with_value(EditMode::Emacs, "");
81
10
        for c in "2024-01-01".chars() {
82
10
            w.as_editor_mut().insert_char(c);
83
10
        }
84
1
        assert_eq!(w.value(), "2024-01-01");
85
1
    }
86

            
87
    #[test]
88
1
    fn step_forward_advances_one_day() {
89
1
        let mut w = DateWidget::with_value(EditMode::Emacs, "2024-01-31T10:30");
90
1
        w.step(1);
91
1
        assert_eq!(w.value(), "2024-02-01T10:30");
92
1
    }
93

            
94
    #[test]
95
1
    fn step_back_retreats_one_day() {
96
1
        let mut w = DateWidget::with_value(EditMode::Emacs, "2024-03-01T08:15");
97
1
        w.step(-1);
98
1
        assert_eq!(w.value(), "2024-02-29T08:15");
99
1
    }
100

            
101
    #[test]
102
1
    fn step_forward_output_accepted_by_validate_date() {
103
1
        let mut w = DateWidget::with_value(EditMode::Emacs, "2024-06-15T12:00");
104
1
        w.step(1);
105
1
        assert!(cli_core::forms::validate_date(w.value()).is_ok());
106
1
    }
107
}