1use crate::styles::{paint, DIM, WHITE};
4
5use super::core::{Frame, Key, Prompt, RenderCtx, Step};
6use super::error::{PromptError, Result};
7use super::theme;
8
9#[derive(Clone, Copy, PartialEq, Eq)]
10enum Seg {
11 Y,
12 M,
13 D,
14}
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub struct Date {
19 pub year: i32,
21 pub month: u32,
23 pub day: u32,
25}
26
27impl Date {
28 pub fn today() -> Self {
30 let secs = std::time::SystemTime::now()
31 .duration_since(std::time::UNIX_EPOCH)
32 .map(|d| d.as_secs())
33 .unwrap_or(0);
34 from_unix_days((secs / 86_400) as i64)
35 }
36 pub fn iso(&self) -> String {
38 format!("{:04}-{:02}-{:02}", self.year, self.month, self.day)
39 }
40 fn days_in_month(&self) -> u32 {
41 match self.month {
42 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
43 4 | 6 | 9 | 11 => 30,
44 2 => {
45 if is_leap(self.year) {
46 29
47 } else {
48 28
49 }
50 }
51 _ => 30,
52 }
53 }
54 fn clamp(&mut self) {
55 if self.month < 1 {
56 self.month = 12;
57 self.year -= 1;
58 }
59 if self.month > 12 {
60 self.month = 1;
61 self.year += 1;
62 }
63 let max = self.days_in_month();
64 if self.day < 1 {
65 self.day = max;
66 }
67 if self.day > max {
68 self.day = max;
69 }
70 }
71}
72
73fn is_leap(y: i32) -> bool {
74 (y % 4 == 0 && y % 100 != 0) || y % 400 == 0
75}
76
77fn from_unix_days(mut days: i64) -> Date {
78 let mut y = 1970i32;
79 loop {
80 let in_year = if is_leap(y) { 366 } else { 365 };
81 if days < in_year as i64 {
82 break;
83 }
84 days -= in_year as i64;
85 y += 1;
86 }
87 let mut m = 1u32;
88 let months_len = |year: i32, mo: u32| -> i64 {
89 match mo {
90 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
91 4 | 6 | 9 | 11 => 30,
92 2 => {
93 if is_leap(year) {
94 29
95 } else {
96 28
97 }
98 }
99 _ => 0,
100 }
101 };
102 while days >= months_len(y, m) {
103 days -= months_len(y, m);
104 m += 1;
105 }
106 Date {
107 year: y,
108 month: m,
109 day: days as u32 + 1,
110 }
111}
112
113pub struct DatePrompt {
115 label: String,
116 value: Date,
117 min: Option<Date>,
118 max: Option<Date>,
119 seg: Seg,
120}
121
122pub fn date(label: impl Into<String>) -> DatePrompt {
132 DatePrompt {
133 label: label.into(),
134 value: Date::today(),
135 min: None,
136 max: None,
137 seg: Seg::Y,
138 }
139}
140
141impl DatePrompt {
142 pub fn initial(mut self, d: Date) -> Self {
144 self.value = d;
145 self
146 }
147 pub fn min(mut self, d: Date) -> Self {
149 self.min = Some(d);
150 self
151 }
152 pub fn max(mut self, d: Date) -> Self {
154 self.max = Some(d);
155 self
156 }
157
158 pub fn run(self) -> Result<Date> {
160 super::core::run(self)
161 }
162
163 fn within_range(&self) -> bool {
164 let iso = self.value.iso();
165 if let Some(ref m) = self.min {
166 if iso < m.iso() {
167 return false;
168 }
169 }
170 if let Some(ref m) = self.max {
171 if iso > m.iso() {
172 return false;
173 }
174 }
175 true
176 }
177}
178
179impl Prompt for DatePrompt {
180 type Output = Date;
181
182 fn handle(&mut self, key: Key) -> Step<Date> {
183 match key {
184 Key::Left => {
185 self.seg = match self.seg {
186 Seg::Y => Seg::D,
187 Seg::M => Seg::Y,
188 Seg::D => Seg::M,
189 };
190 Step::Continue
191 }
192 Key::Right => {
193 self.seg = match self.seg {
194 Seg::Y => Seg::M,
195 Seg::M => Seg::D,
196 Seg::D => Seg::Y,
197 };
198 Step::Continue
199 }
200 Key::Up => {
201 adjust(&mut self.value, self.seg, 1);
202 self.value.clamp();
203 Step::Continue
204 }
205 Key::Down => {
206 adjust(&mut self.value, self.seg, -1);
207 self.value.clamp();
208 Step::Continue
209 }
210 Key::Enter => {
211 if !self.within_range() {
212 Step::Reject("Date out of range".into())
213 } else {
214 Step::Submit(self.value)
215 }
216 }
217 Key::Escape | Key::Interrupt => Step::Cancel,
218 _ => Step::Continue,
219 }
220 }
221
222 fn render(&self, _ctx: RenderCtx) -> Frame {
223 let mut f: Frame = Vec::with_capacity(4);
224 f.push(theme::label(&self.label));
225 let seg_y = format_seg(format!("{:04}", self.value.year), self.seg == Seg::Y);
226 let seg_m = format_seg(format!("{:02}", self.value.month), self.seg == Seg::M);
227 let seg_d = format_seg(format!("{:02}", self.value.day), self.seg == Seg::D);
228 let dash = paint(DIM, "-");
229 f.push(format!(
230 "{} {}{dash}{}{dash}{}",
231 paint(DIM, "│"),
232 seg_y,
233 seg_m,
234 seg_d
235 ));
236 f.push(theme::hint("← → segment · ↑ ↓ adjust · Enter to confirm"));
237 f.push(theme::frame_bot(None));
238 let _ = WHITE;
239 f
240 }
241
242 fn render_answered(&self, value: &Date) -> Frame {
243 theme::answered(&self.label, &value.iso())
244 .split("\r\n")
245 .map(String::from)
246 .collect()
247 }
248
249 fn run_fallback(self) -> Result<Date> {
250 use std::io::Write;
251 let mut out = std::io::stderr();
252 write!(
253 out,
254 " {} [yyyy-mm-dd, default {}]: ",
255 self.label,
256 self.value.iso()
257 )
258 .map_err(PromptError::Io)?;
259 out.flush().map_err(PromptError::Io)?;
260 let line = super::engine::fallback::read_line_raw()?;
261 if line.is_empty() {
262 return Ok(self.value);
263 }
264 let parts: Vec<&str> = line.split('-').collect();
265 if parts.len() != 3 {
266 return Err(PromptError::Interrupted);
267 }
268 Ok(Date {
269 year: parts[0].parse().unwrap_or(self.value.year),
270 month: parts[1].parse().unwrap_or(self.value.month),
271 day: parts[2].parse().unwrap_or(self.value.day),
272 })
273 }
274}
275
276fn adjust(d: &mut Date, seg: Seg, by: i32) {
277 match seg {
278 Seg::Y => d.year = (d.year + by).max(1),
279 Seg::M => d.month = (d.month as i32 + by).clamp(1, 12) as u32,
280 Seg::D => d.day = (d.day as i32 + by).max(1) as u32,
281 }
282}
283
284fn format_seg(value: String, active: bool) -> String {
285 let c = super::settings::colors();
286 if active {
287 paint(c.accent, &format!("\x1b[7m{value}\x1b[27m"))
288 } else {
289 paint(c.input, &value)
290 }
291}