1use std::cell::Cell;
34use std::sync::Arc;
35
36use rich::{Segment, Style};
37
38use crate::component::{Component, Context, Flow, View};
39use crate::components::{Theme, fit, plain, question, text};
40use crate::compose::{FOCUS_NEXT, FOCUS_PREVIOUS};
41use crate::datetime::{Date, DateTime, Time};
42use crate::event::{Button, Event, Key, KeyCode, MouseKind};
43use crate::keymap::{Keymap, keys};
44use crate::policy::{LineIo, NotInteractive};
45
46pub fn date_keymap() -> Keymap {
50 Keymap::new("date")
51 .bind("pick", keys("enter"), "pick")
52 .bind("cancel", keys("escape"), "cancel")
53 .bind("left", keys("left h"), "a day earlier; the field before")
54 .bind("right", keys("right l"), "a day later; the next field")
55 .bind("up", keys("up k"), "a week earlier; the field up")
56 .bind("down", keys("down j"), "a week later; the field down")
57 .bind("previous-month", keys("pageup ["), "a month earlier")
58 .bind("next-month", keys("pagedown ]"), "a month later")
59 .bind("previous-year", keys("shift+pageup {"), "a year earlier")
60 .bind("next-year", keys("shift+pagedown }"), "a year later")
61 .bind("first-day", keys("home"), "the month's first day")
62 .bind("last-day", keys("end"), "the month's last day")
63 .bind("today", keys("t"), "today")
64 .bind(FOCUS_NEXT, keys("tab"), "the next part")
65 .bind(FOCUS_PREVIOUS, keys("shift+tab"), "the part before")
66}
67
68#[derive(Clone, Copy, Debug, PartialEq, Eq)]
69enum Mode {
70 Date,
71 Time,
72 DateTime,
73}
74
75#[derive(Clone, Copy, Debug, PartialEq, Eq)]
77enum Part {
78 Grid,
79 Hour,
80 Minute,
81 Second,
82 Half,
83}
84
85type Check = Arc<dyn Fn(DateTime) -> Result<(), String> + Send + Sync>;
86
87const WEEKS: usize = 6;
90const GRID_WIDTH: usize = 23;
93const GRID_TOP: usize = 3;
95const FIRST_DAY: Date = Date::new(1, 1, 1);
98const LAST_DAY: Date = Date::new(9999, 12, 31);
99
100struct Chooser {
102 prompt: String,
103 mode: Mode,
104 value: DateTime,
105 value_given: bool,
107 min: Option<DateTime>,
108 max: Option<DateTime>,
109 seconds: bool,
110 twelve_hour: bool,
111 today: Date,
112 part: Part,
113 typed: String,
115 message: Option<String>,
117 check: Option<Check>,
118 default: Option<DateTime>,
119 theme: Theme,
120 mouse: bool,
121 keymap: Keymap,
122 clicked: Option<Date>,
124 answer: Option<Option<String>>,
125 width: Cell<usize>,
127}
128
129impl Chooser {
130 fn new(prompt: String, mode: Mode) -> Chooser {
131 let now = DateTime::now_utc();
132 let value = match mode {
133 Mode::Date => DateTime::new(now.date, Time::MIDNIGHT),
134 Mode::Time | Mode::DateTime => {
135 DateTime::new(now.date, Time::new(now.time.hour, now.time.minute, 0))
136 }
137 };
138 Chooser {
139 prompt,
140 mode,
141 value,
142 value_given: false,
143 min: None,
144 max: None,
145 seconds: false,
146 twelve_hour: false,
147 today: now.date,
148 part: if mode == Mode::Time {
149 Part::Hour
150 } else {
151 Part::Grid
152 },
153 typed: String::new(),
154 message: None,
155 check: None,
156 default: None,
157 theme: Theme::default(),
158 mouse: false,
159 keymap: date_keymap(),
160 clicked: None,
161 answer: None,
162 width: Cell::new(80),
163 }
164 }
165
166 fn has_date(&self) -> bool {
167 self.mode != Mode::Time
168 }
169
170 fn has_time(&self) -> bool {
171 self.mode != Mode::Date
172 }
173
174 fn parts(&self) -> Vec<Part> {
176 let mut parts = Vec::new();
177 if self.has_date() {
178 parts.push(Part::Grid);
179 }
180 if self.has_time() {
181 parts.extend([Part::Hour, Part::Minute]);
182 if self.seconds {
183 parts.push(Part::Second);
184 }
185 if self.twelve_hour {
186 parts.push(Part::Half);
187 }
188 }
189 parts
190 }
191
192 fn to_the_minute(&self) -> bool {
195 self.has_time() && !self.seconds
196 }
197
198 fn shown(&self, at: DateTime) -> DateTime {
201 if !self.to_the_minute() {
202 return at;
203 }
204 DateTime::new(
205 at.date,
206 Time {
207 second: 0,
208 ..at.time
209 },
210 )
211 }
212
213 fn earliest(&self) -> Option<DateTime> {
216 let min = self.min?;
217 if !self.to_the_minute() || min.time.second == 0 {
218 return Some(min);
219 }
220 let minute = i64::from(min.time.seconds() - min.time.second);
221 let time = Time::from_seconds(minute + 60);
222 let date = if time == Time::MIDNIGHT {
223 min.date.add_days(1)
224 } else {
225 min.date
226 };
227 Some(DateTime::new(date, time))
228 }
229
230 fn latest(&self) -> Option<DateTime> {
233 self.max.map(|max| self.shown(max))
234 }
235
236 fn earliest_text(&self) -> Option<String> {
240 let (min, earliest) = (self.min?, self.earliest()?);
241 Some(if self.mode == Mode::Time && earliest.date != min.date {
242 min.time.format(true, self.twelve_hour)
243 } else {
244 self.format(earliest)
245 })
246 }
247
248 fn day_allowed(&self, date: Date) -> bool {
250 (FIRST_DAY..=LAST_DAY).contains(&date)
251 && self.earliest().is_none_or(|min| date >= min.date)
252 && self.latest().is_none_or(|max| date <= max.date)
253 }
254
255 fn clamp_day(&self, date: Date) -> Date {
257 let mut date = date;
258 if let Some(min) = self.earliest() {
259 date = date.max(min.date);
260 }
261 if let Some(max) = self.latest() {
262 date = date.min(max.date);
263 }
264 date.clamp(FIRST_DAY, LAST_DAY)
265 }
266
267 fn key_of(&self, at: DateTime) -> (Option<Date>, Option<Time>) {
270 match self.mode {
271 Mode::Date => (Some(at.date), None),
272 Mode::Time => (None, Some(at.time)),
273 Mode::DateTime => (Some(at.date), Some(at.time)),
274 }
275 }
276
277 fn format(&self, at: DateTime) -> String {
279 match self.mode {
280 Mode::Date => at.date.to_string(),
281 Mode::Time => at.time.format(self.seconds, self.twelve_hour),
282 Mode::DateTime => at.format(self.seconds, self.twelve_hour),
283 }
284 }
285
286 fn refusal(&self, at: DateTime) -> Option<String> {
289 let at = self.shown(at);
290 if let (Some(earliest), Some(latest)) = (self.earliest(), self.latest())
291 && self.key_of(earliest) > self.key_of(latest)
292 {
293 return Some(format!(
294 "the range is empty: the earliest, {}, is after the latest, {}",
295 self.earliest_text().unwrap_or_default(),
296 self.format(latest)
297 ));
298 }
299 let at_key = self.key_of(at);
300 if let Some(min) = self.min
301 && at_key < self.key_of(min)
302 {
303 let earliest = self.earliest_text().unwrap_or_default();
304 return Some(format!("the earliest is {earliest}"));
305 }
306 if let Some(max) = self.latest()
307 && at_key > self.key_of(max)
308 {
309 return Some(format!("the latest is {}", self.format(max)));
310 }
311 self.check.as_ref().and_then(|check| check(at).err())
312 }
313
314 fn set_day(&mut self, date: Date) {
315 self.value.date = self.clamp_day(date);
316 }
317
318 fn step_field(&mut self, delta: i64) {
320 let time = self.value.time;
321 let wrap =
322 |value: u32, by: i64, modulus: i64| (value as i64 + by).rem_euclid(modulus) as u32;
323 self.value.time = match self.part {
324 Part::Hour => Time {
325 hour: wrap(time.hour, delta, 24),
326 ..time
327 },
328 Part::Minute => Time {
329 minute: wrap(time.minute, delta, 60),
330 ..time
331 },
332 Part::Second => Time {
333 second: wrap(time.second, delta, 60),
334 ..time
335 },
336 Part::Half => Time {
337 hour: (time.hour + 12) % 24,
338 ..time
339 },
340 Part::Grid => time,
341 };
342 }
343
344 fn type_digit(&mut self, digit: char) {
347 self.typed.push(digit);
348 let number: u32 = self.typed.parse().unwrap_or(0);
349 let time = self.value.time;
350 let (most, full) = match self.part {
351 Part::Hour if self.twelve_hour => (12, number >= 2 || self.typed.len() == 2),
352 Part::Hour => (23, number >= 3 || self.typed.len() == 2),
353 _ => (59, number >= 6 || self.typed.len() == 2),
354 };
355 let number = number.min(most);
356 self.value.time = match self.part {
357 Part::Hour if self.twelve_hour => {
358 let pm = time.hour >= 12;
359 let hour = if self.typed.len() == 2 || full {
360 number.max(1) % 12
361 } else {
362 number % 12
363 };
364 Time {
365 hour: hour + if pm { 12 } else { 0 },
366 ..time
367 }
368 }
369 Part::Hour => Time {
370 hour: number,
371 ..time
372 },
373 Part::Minute => Time {
374 minute: number,
375 ..time
376 },
377 Part::Second => Time {
378 second: number,
379 ..time
380 },
381 _ => time,
382 };
383 if full {
384 self.typed.clear();
385 self.move_part(1);
386 }
387 }
388
389 fn part_index(&self, parts: &[Part]) -> usize {
391 parts
392 .iter()
393 .position(|part| *part == self.part)
394 .unwrap_or(0)
395 }
396
397 fn move_part(&mut self, by: isize) {
400 let parts = self.parts();
401 let count = parts.len() as isize;
402 let first = if self.has_date() { 1 } else { 0 };
403 let next = (self.part_index(&parts) as isize + by).clamp(first.min(count - 1), count - 1);
404 self.part = parts[next as usize];
405 self.typed.clear();
406 }
407
408 fn step_part(&mut self, forward: bool) -> bool {
412 let parts = self.parts();
413 let at = self.part_index(&parts);
414 let next = if forward {
415 Some(at + 1)
416 } else {
417 at.checked_sub(1)
418 };
419 match next.and_then(|next| parts.get(next)) {
420 Some(part) => {
421 self.part = *part;
422 self.typed.clear();
423 true
424 }
425 None => false,
426 }
427 }
428
429 fn enter(&mut self, forward: bool) {
432 let parts = self.parts();
433 if let Some(part) = if forward { parts.first() } else { parts.last() } {
434 self.part = *part;
435 }
436 self.typed.clear();
437 }
438
439 fn pick(&mut self) -> Flow<DateTime> {
440 let value = self.shown(self.value);
441 if let Some(why) = self.refusal(value) {
442 self.message = Some(why);
443 return Flow::Continue;
444 }
445 self.value = value;
446 self.answer = Some(Some(self.format(value)));
447 Flow::Done(value)
448 }
449
450 fn act(&mut self, action: &str) -> Flow<DateTime> {
451 let on_grid = self.part == Part::Grid;
452 let day = self.value.date;
453 match action {
454 "pick" => return self.pick(),
455 "cancel" => {
456 self.answer = Some(None);
457 return Flow::Cancel;
458 }
459 FOCUS_NEXT | FOCUS_PREVIOUS => {
463 if !self.step_part(action == FOCUS_NEXT) {
464 return Flow::Ignored;
465 }
466 }
467 "left" if on_grid => self.set_day(day.add_days(-1)),
468 "right" if on_grid => self.set_day(day.add_days(1)),
469 "up" if on_grid => self.set_day(day.add_days(-7)),
470 "down" if on_grid => self.set_day(day.add_days(7)),
471 "left" => self.move_part(-1),
472 "right" => self.move_part(1),
473 "up" => self.step_field(1),
474 "down" => self.step_field(-1),
475 "previous-month" if self.has_date() => self.set_day(day.add_months(-1)),
476 "next-month" if self.has_date() => self.set_day(day.add_months(1)),
477 "previous-year" if self.has_date() => self.set_day(day.add_months(-12)),
478 "next-year" if self.has_date() => self.set_day(day.add_months(12)),
479 "first-day" if on_grid => self.set_day(day.first_of_month()),
480 "last-day" if on_grid => self.set_day(day.last_of_month()),
481 "today" if self.has_date() => self.set_day(self.today),
482 _ => return Flow::Ignored,
483 }
484 Flow::Continue
485 }
486
487 fn key(&mut self, key: Key) -> Flow<DateTime> {
488 let plain_key = !key.modifiers.ctrl && !key.modifiers.alt;
489 if plain_key && self.part != Part::Grid {
492 match key.code {
493 KeyCode::Char(c @ '0'..='9') if self.part != Part::Half => {
494 self.type_digit(c);
495 return Flow::Continue;
496 }
497 KeyCode::Char(c @ ('a' | 'A' | 'p' | 'P')) if self.twelve_hour => {
498 let pm = c.eq_ignore_ascii_case(&'p');
499 let hour = self.value.time.hour % 12 + if pm { 12 } else { 0 };
500 self.value.time.hour = hour;
501 return Flow::Continue;
502 }
503 _ => {}
504 }
505 }
506 let Some(action) = self.keymap.action(key).map(str::to_string) else {
507 return Flow::Ignored;
508 };
509 self.typed.clear();
511 self.act(&action)
512 }
513
514 fn time_row(&self) -> usize {
516 if self.has_date() { GRID_TOP + WEEKS } else { 1 }
517 }
518
519 fn day_at(&self, column: usize, row: usize) -> Option<Date> {
521 if !self.has_date() || !(GRID_TOP..GRID_TOP + WEEKS).contains(&row) || column < 2 {
522 return None;
523 }
524 let cell = (column - 1) / 3;
525 if cell >= 7 {
526 return None;
527 }
528 let first = self.value.date.first_of_month();
529 let at = (row - GRID_TOP) * 7 + cell;
530 let day = (at + 1).checked_sub(first.weekday() as usize)?;
531 (1..=Date::days_in_month(first.year, first.month) as usize)
532 .contains(&day)
533 .then_some(Date {
534 day: day as u32,
535 ..first
536 })
537 }
538
539 fn field_at(&self, column: usize) -> Option<Part> {
541 let mut at = self.time_label().len();
542 for (part, width) in self.time_fields() {
543 if (at..at + width).contains(&column) {
544 return Some(part);
545 }
546 at += width + 1;
547 }
548 None
549 }
550
551 fn mouse_event(&mut self, kind: MouseKind, column: usize, row: usize) -> Flow<DateTime> {
552 let over_grid = self.has_date() && row < GRID_TOP + WEEKS;
553 match kind {
554 MouseKind::Down(Button::Left) => {
555 if let Some(day) = self.day_at(column, row) {
556 if !self.day_allowed(day) {
557 return Flow::Continue;
558 }
559 self.part = Part::Grid;
560 if self.clicked == Some(day) && self.value.date == day {
561 return self.pick();
562 }
563 self.value.date = day;
564 self.clicked = Some(day);
565 } else if self.has_time()
566 && row == self.time_row()
567 && let Some(part) = self.field_at(column)
568 {
569 self.part = part;
570 self.typed.clear();
571 }
572 }
573 MouseKind::ScrollUp | MouseKind::ScrollDown => {
574 let by = if kind == MouseKind::ScrollUp { -1 } else { 1 };
575 if over_grid {
576 self.set_day(self.value.date.add_months(by));
577 } else if self.has_time() && self.part != Part::Grid {
578 self.step_field(-(by as i64));
580 }
581 }
582 _ => return Flow::Ignored,
583 }
584 Flow::Continue
585 }
586
587 fn event(&mut self, event: &Event) -> Flow<DateTime> {
588 if let Some(mouse) = event.mouse() {
589 self.message = None;
590 return self.mouse_event(mouse.kind, mouse.column as usize, mouse.row as usize);
591 }
592 match event.key() {
593 Some(key) => {
594 self.message = None;
595 self.key(key)
596 }
597 None => Flow::Ignored,
598 }
599 }
600
601 fn time_label(&self) -> &'static str {
603 if self.has_date() { " Time " } else { " " }
604 }
605
606 fn time_fields(&self) -> Vec<(Part, usize)> {
609 let mut fields = vec![(Part::Hour, 2), (Part::Minute, 2)];
610 if self.seconds {
611 fields.push((Part::Second, 2));
612 }
613 if self.twelve_hour {
614 fields.push((Part::Half, 2));
615 }
616 fields
617 .into_iter()
618 .map(|(part, width)| (part, width + if part == self.part { 2 } else { 0 }))
619 .collect()
620 }
621
622 fn time_line(&self, width: usize) -> Vec<Segment> {
623 let theme = &self.theme;
624 let time = self.value.time;
625 let out = self
626 .refusal(self.value)
627 .is_some_and(|_| self.min.is_some() || self.max.is_some());
628 let reverse = Style::parse("reverse").expect("a built-in style");
629 let mut line = vec![plain(self.time_label())];
630 let (hour, pm) = time.twelve_hour();
631 for (index, (part, _)) in self.time_fields().into_iter().enumerate() {
632 if index > 0 {
633 line.push(plain(if part == Part::Half { " " } else { ":" }));
634 }
635 let shown = match part {
636 Part::Hour if self.twelve_hour => format!("{hour:02}"),
637 Part::Hour => format!("{:02}", time.hour),
638 Part::Minute => format!("{:02}", time.minute),
639 Part::Second => format!("{:02}", time.second),
640 _ => (if pm { "PM" } else { "AM" }).to_string(),
641 };
642 let shown = if part == self.part && !self.typed.is_empty() {
643 format!("{:_<2}", self.typed)
644 } else {
645 shown
646 };
647 if part == self.part {
648 line.push(text(format!("[{shown}]"), &theme.focused.combine(&reverse)));
649 } else if out {
650 line.push(text(shown, &theme.error));
651 } else {
652 line.push(plain(shown));
653 }
654 }
655 fit(line, width)
656 }
657
658 fn grid_lines(&self, width: usize) -> Vec<Vec<Segment>> {
659 let theme = &self.theme;
660 let selected = self.value.date;
661 let first = selected.first_of_month();
662 let title = format!("{} {}", selected.month_name(), selected.year);
663 let pad = GRID_WIDTH.saturating_sub(title.len()) / 2;
664 let title_style = Style::parse("bold").expect("a built-in style");
665 let mut lines = vec![
666 fit(
667 vec![plain(" ".repeat(pad)), text(title, &title_style)],
668 width,
669 ),
670 fit(vec![text(" Mo Tu We Th Fr Sa Su", &theme.hint)], width),
671 ];
672 let reverse = Style::parse("reverse").expect("a built-in style");
673 let underline = Style::parse("underline").expect("a built-in style");
674 let today_style = Style::parse("bold").expect("a built-in style");
675 let chosen = if self.part == Part::Grid {
676 theme.focused.combine(&reverse)
677 } else {
678 underline
679 };
680 let offset = first.weekday() as usize;
681 let days = Date::days_in_month(first.year, first.month) as usize;
682 for week in 0..WEEKS {
683 let mut line = vec![plain(" ")];
684 for cell in 0..7 {
685 let at = week * 7 + cell;
686 let day = (at + 1)
687 .checked_sub(offset)
688 .filter(|day| (1..=days).contains(day));
689 let is = |cell: usize| {
690 (week * 7 + cell + 1)
691 .checked_sub(offset)
692 .is_some_and(|day| day == selected.day as usize)
693 };
694 line.push(plain(if is(cell) {
697 "["
698 } else if cell > 0 && is(cell - 1) {
699 "]"
700 } else {
701 " "
702 }));
703 let Some(day) = day else {
704 line.push(plain(" "));
705 continue;
706 };
707 let date = Date {
708 day: day as u32,
709 ..first
710 };
711 let label = format!("{day:>2}");
712 line.push(if date == selected {
713 text(label, &chosen)
714 } else if !self.day_allowed(date) {
715 text(label, &theme.hint)
716 } else if date == self.today {
717 text(label, &today_style)
718 } else {
719 plain(label)
720 });
721 }
722 if is_last_selected(week, offset, selected.day) {
723 line.push(plain("]"));
724 }
725 lines.push(fit(line, width));
726 }
727 lines
728 }
729
730 fn hint(&self) -> String {
731 let mut hint = String::from(" ");
732 hint.push_str(match self.part {
733 Part::Grid => "←→↑↓ day · pgup/pgdn month · t today",
734 _ => "←→ field · ↑↓ change · type digits",
735 });
736 if self.parts().len() > 1 {
737 hint.push_str(match (self.part, self.mode) {
738 (Part::Grid, Mode::DateTime) => " · tab time",
739 (_, Mode::DateTime) => " · tab date",
740 _ => " · tab next",
741 });
742 }
743 hint.push_str(" · enter pick · esc cancel");
744 hint
745 }
746
747 fn range_line(&self) -> Option<String> {
749 let latest = self.latest().map(|at| self.format(at));
750 match (self.earliest_text(), latest) {
751 (None, None) => None,
752 (Some(min), Some(max)) => Some(format!(" from {min} to {max}")),
753 (Some(min), None) => Some(format!(" from {min}")),
754 (None, Some(max)) => Some(format!(" until {max}")),
755 }
756 }
757
758 fn render(&self, context: &Context<'_>) -> View {
759 let width = context.width;
760 self.width.set(width);
761 let theme = &self.theme;
762 let mut header = question(theme, &self.prompt);
763 if let Some(answer) = &self.answer {
764 header.push(match answer {
765 Some(answer) => text(answer.clone(), &theme.answer),
766 None => text("cancelled", &theme.hint),
767 });
768 return View::new(vec![fit(header, width)]);
769 }
770 header.push(plain(self.format(self.value)));
772 let mut lines = vec![fit(header, width)];
773 if self.has_date() {
774 lines.extend(self.grid_lines(width));
775 }
776 if self.has_time() {
777 lines.push(self.time_line(width));
778 }
779 if let Some(range) = self.range_line() {
780 lines.push(fit(vec![text(range, &theme.hint)], width));
781 }
782 lines.push(fit(vec![text(self.hint(), &theme.hint)], width));
783 if let Some(message) = &self.message {
784 lines.push(fit(
785 vec![text(format!(" ✗ {message}"), &theme.error)],
786 width,
787 ));
788 }
789 View::new(lines)
790 }
791
792 fn shape(&self) -> &'static str {
794 match (self.mode, self.seconds) {
795 (Mode::Date, _) => "YYYY-MM-DD",
796 (Mode::Time, false) => "HH:MM",
797 (Mode::Time, true) => "HH:MM:SS",
798 (Mode::DateTime, false) => "YYYY-MM-DD HH:MM",
799 (Mode::DateTime, true) => "YYYY-MM-DD HH:MM:SS",
800 }
801 }
802
803 fn parse(&self, line: &str) -> Result<DateTime, String> {
804 match self.mode {
805 Mode::Date => line.parse::<Date>().map(DateTime::from),
806 Mode::Time => line
807 .parse::<Time>()
808 .map(|time| DateTime::new(self.value.date, time)),
809 Mode::DateTime => line.parse::<DateTime>(),
810 }
811 }
812
813 fn ask(&mut self, io: &mut dyn LineIo) -> Result<Option<DateTime>, NotInteractive> {
814 let mut prompt = format!("{} ({}", self.prompt, self.shape());
815 if let Some(range) = self.range_line() {
816 prompt.push_str(&format!(", {}", range.trim()));
817 }
818 prompt.push(')');
819 if let Some(default) = self.default {
820 prompt.push_str(&format!(" [{}]", self.format(default)));
821 }
822 io.write(&format!("{prompt}: "));
823 let line = match io.read_line() {
824 Some(line) if !line.trim().is_empty() => line,
825 ended => {
828 return match self.default {
829 Some(default) => self.checked(default).map(Some),
830 None if ended.is_none() => Err(NotInteractive::Ended),
831 None => Err(NotInteractive::Invalid(format!(
832 "no answer given ({})",
833 self.shape()
834 ))),
835 };
836 }
837 };
838 let at = self.parse(&line).map_err(NotInteractive::Invalid)?;
839 self.checked(at).map(Some)
840 }
841
842 fn checked(&self, at: DateTime) -> Result<DateTime, NotInteractive> {
845 let at = self.shown(at);
846 match self.refusal(at) {
847 Some(why) => Err(NotInteractive::Invalid(format!(
848 "{}: {why}",
849 self.format(at)
850 ))),
851 None => Ok(at),
852 }
853 }
854}
855
856fn is_last_selected(week: usize, offset: usize, day: u32) -> bool {
859 (week * 7 + 7).checked_sub(offset) == Some(day as usize)
860}
861
862macro_rules! shared_builders {
864 () => {
865 pub fn theme(mut self, theme: Theme) -> Self {
866 self.0.theme = theme;
867 self
868 }
869
870 pub fn with_mouse(mut self, on: bool) -> Self {
874 self.0.mouse = on;
875 self
876 }
877
878 pub fn rebind(mut self, action: &str, keys: impl IntoIterator<Item = Key>) -> Self {
881 self.0.keymap.rebind(action, keys);
882 self
883 }
884 };
885}
886
887macro_rules! time_builders {
889 () => {
890 pub fn seconds(mut self, on: bool) -> Self {
895 self.0.seconds = on;
896 self
897 }
898
899 pub fn twelve_hour(mut self, on: bool) -> Self {
901 self.0.twelve_hour = on;
902 self
903 }
904 };
905}
906
907macro_rules! date_builders {
909 () => {
910 pub fn today(mut self, today: Date) -> Self {
913 self.0.today = today;
914 self
915 }
916 };
917}
918
919#[must_use = "a component does nothing until it is run or mounted"]
933pub struct DatePicker(Chooser);
934
935impl DatePicker {
936 pub fn new(prompt: impl Into<String>) -> DatePicker {
937 DatePicker(Chooser::new(prompt.into(), Mode::Date))
938 }
939
940 shared_builders!();
941 date_builders!();
942
943 pub fn value(mut self, date: Date) -> Self {
945 self.0.value.date = date;
946 self.0.value_given = true;
947 self
948 }
949
950 pub fn default(mut self, date: Date) -> Self {
953 if !self.0.value_given {
954 self.0.value.date = date;
955 }
956 self.0.default = Some(date.into());
957 self
958 }
959
960 pub fn min(mut self, date: Date) -> Self {
962 self.0.min = Some(date.at(Time::MIDNIGHT));
963 self
964 }
965
966 pub fn max(mut self, date: Date) -> Self {
968 self.0.max = Some(date.at(Time::LAST));
969 self
970 }
971
972 pub fn validate(
975 mut self,
976 check: impl Fn(Date) -> Result<(), String> + Send + Sync + 'static,
977 ) -> Self {
978 self.0.check = Some(Arc::new(move |at: DateTime| check(at.date)));
979 self
980 }
981
982 pub fn selected(&self) -> Date {
984 self.0.value.date
985 }
986}
987
988#[must_use = "a component does nothing until it is run or mounted"]
1001pub struct TimePicker(Chooser);
1002
1003impl TimePicker {
1004 pub fn new(prompt: impl Into<String>) -> TimePicker {
1005 TimePicker(Chooser::new(prompt.into(), Mode::Time))
1006 }
1007
1008 shared_builders!();
1009 time_builders!();
1010
1011 pub fn value(mut self, time: Time) -> Self {
1013 self.0.value.time = time;
1014 self.0.value_given = true;
1015 self
1016 }
1017
1018 pub fn default(mut self, time: Time) -> Self {
1021 if !self.0.value_given {
1022 self.0.value.time = time;
1023 }
1024 self.0.default = Some(self.0.value.date.at(time));
1025 self
1026 }
1027
1028 pub fn min(mut self, time: Time) -> Self {
1030 self.0.min = Some(self.0.value.date.at(time));
1031 self
1032 }
1033
1034 pub fn max(mut self, time: Time) -> Self {
1036 self.0.max = Some(self.0.value.date.at(time));
1037 self
1038 }
1039
1040 pub fn validate(
1043 mut self,
1044 check: impl Fn(Time) -> Result<(), String> + Send + Sync + 'static,
1045 ) -> Self {
1046 self.0.check = Some(Arc::new(move |at: DateTime| check(at.time)));
1047 self
1048 }
1049
1050 pub fn selected(&self) -> Time {
1052 self.0.shown(self.0.value).time
1053 }
1054}
1055
1056#[must_use = "a component does nothing until it is run or mounted"]
1073pub struct DateTimePicker(Chooser);
1074
1075impl DateTimePicker {
1076 pub fn new(prompt: impl Into<String>) -> DateTimePicker {
1077 DateTimePicker(Chooser::new(prompt.into(), Mode::DateTime))
1078 }
1079
1080 shared_builders!();
1081 date_builders!();
1082 time_builders!();
1083
1084 pub fn value(mut self, at: DateTime) -> Self {
1086 self.0.value = at;
1087 self.0.value_given = true;
1088 self
1089 }
1090
1091 pub fn default(mut self, at: DateTime) -> Self {
1094 if !self.0.value_given {
1095 self.0.value = at;
1096 }
1097 self.0.default = Some(at);
1098 self
1099 }
1100
1101 pub fn min(mut self, at: DateTime) -> Self {
1104 self.0.min = Some(at);
1105 self
1106 }
1107
1108 pub fn max(mut self, at: DateTime) -> Self {
1110 self.0.max = Some(at);
1111 self
1112 }
1113
1114 pub fn validate(
1117 mut self,
1118 check: impl Fn(DateTime) -> Result<(), String> + Send + Sync + 'static,
1119 ) -> Self {
1120 self.0.check = Some(Arc::new(check));
1121 self
1122 }
1123
1124 pub fn selected(&self) -> DateTime {
1126 self.0.shown(self.0.value)
1127 }
1128}
1129
1130macro_rules! picker_component {
1133 ($picker:ty, $output:ty, $from:expr) => {
1134 impl Component for $picker {
1135 type Output = $output;
1136
1137 fn start(&mut self, _context: &Context<'_>) -> Flow<$output> {
1138 let mut value = self.0.shown(self.0.value);
1141 value.date = self.0.clamp_day(value.date);
1142 self.0.value = value;
1143 Flow::Continue
1144 }
1145
1146 fn handle(&mut self, event: &Event, _context: &Context<'_>) -> Flow<$output> {
1147 match self.0.event(event) {
1148 Flow::Done(at) => Flow::Done($from(at)),
1149 Flow::Cancel => Flow::Cancel,
1150 Flow::Ignored => Flow::Ignored,
1151 _ => Flow::Continue,
1152 }
1153 }
1154
1155 fn render(&self, context: &Context<'_>) -> View {
1156 self.0.render(context)
1157 }
1158
1159 fn mouse(&self) -> bool {
1160 self.0.mouse
1161 }
1162
1163 fn keymap(&self) -> Keymap {
1164 self.0.keymap.clone()
1165 }
1166
1167 fn focus_step(&mut self, forward: bool) -> bool {
1169 self.0.step_part(forward)
1170 }
1171
1172 fn focus_enter(&mut self, forward: bool) -> bool {
1174 self.0.enter(forward);
1175 true
1176 }
1177
1178 fn default_value(&self) -> Option<$output> {
1181 self.0
1182 .default
1183 .and_then(|default| self.0.checked(default).ok())
1184 .map($from)
1185 }
1186
1187 fn prompt(&mut self, io: &mut dyn LineIo) -> Result<Option<$output>, NotInteractive> {
1188 Ok(self.0.ask(io)?.map($from))
1189 }
1190 }
1191 };
1192}
1193
1194picker_component!(DatePicker, Date, |at: DateTime| at.date);
1195picker_component!(TimePicker, Time, |at: DateTime| at.time);
1196picker_component!(DateTimePicker, DateTime, |at: DateTime| at);