1use crate::{
2 record::{PresentedRecord, PresentedRecurringRecord, Record, RecurringRecord},
3 time::now,
4 ui::{
5 consts::*,
6 state::{ProtectedState, State},
7 types::*,
8 },
9};
10use anyhow::{anyhow, Result};
11use chrono::Datelike;
12use crossterm::event::{self, Event, KeyCode};
13use ratatui::{prelude::*, widgets::*};
14use std::time::Duration;
15use std::{io::Stdout, ops::Deref, sync::Arc};
16
17fn sit<T>(msg: impl std::future::Future<Output = Result<T>>) -> Result<T> {
18 let runtime = tokio::runtime::Builder::new_multi_thread()
19 .enable_all()
20 .build()?;
21
22 runtime.block_on(msg)
23}
24
25pub async fn draw_loop<'a>(
26 state: ProtectedState<'static>,
27 terminal: &mut Terminal<CrosstermBackend<Stdout>>,
28) -> Result<()> {
29 let (s, mut r) = tokio::sync::mpsc::channel(1);
30
31 let s2 = state.clone();
32 std::thread::spawn(move || sit(read_input(s2, s)));
33 let mut last_line = String::from("placeholder");
34 let mut last_draw = now() - chrono::TimeDelta::try_minutes(1).unwrap_or_default();
35
36 loop {
37 let mut lock = state.lock().await;
38 if !lock.block_ui {
39 let redraw = lock.redraw;
40
41 if redraw {
42 lock.redraw = false;
43 }
44
45 if !lock.errors.is_empty() {
46 lock.redraw = true;
47 }
48
49 let line = lock.line_buf.clone();
50 drop(lock);
51
52 if redraw
53 || line != last_line
54 || last_draw + chrono::TimeDelta::try_seconds(5).unwrap_or_default() < now()
55 {
56 let lock = state.lock().await;
57 let show = lock.show.clone();
58 let show_recurring = lock.show_recurring.clone();
59 drop(lock);
60 terminal.draw(|f| {
61 render_app(state.clone(), f, line.clone(), show, show_recurring);
62 })?;
63
64 last_line = line;
65 last_draw = now();
66 }
67
68 if r.try_recv().is_ok() {
69 break;
70 }
71 }
72 tokio::time::sleep(Duration::new(0, 100)).await;
73 }
74 Ok(())
75}
76
77fn notify_update_state(state: ProtectedState<'static>) {
78 tokio::spawn(async move {
79 state.add_notification("Updating state").await;
80 state.update_state().await
81 });
82}
83
84pub async fn read_input<'a>(
85 state: ProtectedState<'static>,
86 s: tokio::sync::mpsc::Sender<()>,
87) -> Result<()> {
88 let mut last_buf = String::new();
89
90 'input: loop {
91 let lock = state.lock().await;
92 if !lock.block_ui {
93 let mut buf = lock.line_buf.clone();
94 drop(lock);
95
96 buf = match handle_input(buf) {
97 Ok(buf) => buf,
98 Err(_) => {
99 state.add_error(anyhow!("Invalid Input")).await;
100 state.update_state().await;
101 continue 'input;
102 }
103 };
104
105 let mut lock = state.lock().await;
106 if buf != last_buf && !lock.errors.is_empty() {
107 lock.errors = Vec::new();
108 if !buf.is_empty() {
109 buf = buf[0..buf.len() - 1].to_string();
110 }
111 }
112 drop(lock);
113
114 if buf.ends_with('\n') {
115 match buf.trim() {
116 "quit" => break 'input,
117 x => {
118 if x.starts_with("s ") || x.starts_with("show ") {
119 let m = if x.starts_with("show ") {
120 x.trim_start_matches("show ")
121 } else {
122 x.trim_start_matches("s ")
123 }
124 .trim()
125 .split(' ')
126 .filter(|x| !x.is_empty())
127 .collect::<Vec<&str>>();
128 let mut lock = state.lock().await;
129 lock.show = None;
130 lock.show_recurring = None;
131 drop(lock);
132 match m[0] {
133 "all" | "a" => {
134 state.lock().await.list_type = ListType::All;
135 notify_update_state(state.clone());
136 }
137 "today" | "t" => {
138 state.lock().await.list_type = ListType::Today;
139 let state = state.clone();
140 notify_update_state(state.clone());
141 }
142 "recur" | "recurring" | "recurrence" | "r" => {
143 if m.len() == 2 {
144 if let Ok(id) = m[1].parse::<u64>() {
145 state
146 .lock()
147 .await
148 .commands
149 .push(CommandType::Show(true, id));
150 } else {
151 state
152 .add_error(anyhow!("Invalid Command '{}'", x))
153 .await
154 }
155 } else {
156 state.lock().await.list_type = ListType::Recurring;
157 }
158
159 notify_update_state(state.clone());
160 }
161 id => {
162 if let Ok(id) = id.parse::<u64>() {
163 state
164 .lock()
165 .await
166 .commands
167 .push(CommandType::Show(false, id));
168 } else {
169 state.add_error(anyhow!("Invalid Command '{}'", x)).await
170 }
171
172 notify_update_state(state.clone());
173 }
174 }
175 } else if x.starts_with("d ") || x.starts_with("delete ") {
176 let ids = if x.starts_with("delete ") {
177 x.trim_start_matches("delete ")
178 } else {
179 x.trim_start_matches("d ")
180 }
181 .split(' ')
182 .filter(|x| !x.is_empty())
183 .collect::<Vec<&str>>();
184
185 let mut v = Vec::new();
186 let mut recur = false;
187
188 for id in &ids {
189 if id.is_empty() {
190 continue;
191 }
192
193 if *id == "recur" {
194 recur = true;
195 continue;
196 }
197
198 match id.parse::<u64>() {
199 Ok(y) => v.push(y),
200 Err(_) => {
201 state.add_error(anyhow!("Invalid ID {}", id)).await;
202 }
203 };
204 }
205
206 let command = if recur {
207 CommandType::DeleteRecurring(v)
208 } else {
209 CommandType::Delete(v)
210 };
211
212 let s = state.clone();
213 tokio::spawn(async move {
214 s.lock().await.commands.push(command);
215 });
216
217 notify_update_state(state.clone());
218 } else if x.starts_with("e ") || x.starts_with("entry ") {
219 let x = x.to_string();
220
221 let state = state.clone();
222 tokio::spawn(async move {
223 state.lock().await.commands.push(CommandType::Entry(
224 if x.starts_with("entry ") {
225 x.trim_start_matches("entry ")
226 } else {
227 x.trim_start_matches("e ")
228 }
229 .to_string(),
230 ));
231 notify_update_state(state.clone());
232 });
233 } else if x.starts_with("edit ") {
234 let ids = x
235 .trim_start_matches("edit ")
236 .split(' ')
237 .filter(|x| !x.is_empty())
238 .collect::<Vec<&str>>();
239
240 let mut v = Vec::new();
241 let mut recur = false;
242
243 'ids: for id in &ids {
244 if id.is_empty() {
245 continue;
246 }
247
248 if *id == "recur" {
249 recur = true;
250 continue;
251 }
252
253 match id.parse::<u64>() {
254 Ok(y) => {
255 v.push(y);
257 break 'ids;
258 }
259 Err(_) => {
260 state.add_error(anyhow!("Invalid ID {}", id)).await;
261 }
262 };
263 }
264
265 let s = state.clone();
266 tokio::spawn(async move {
267 if v.is_empty() {
268 s.add_error(anyhow!("Edit requires an ID")).await;
269 } else {
270 s.lock().await.commands.push(CommandType::Edit(recur, v[0]));
271 }
272 });
273
274 notify_update_state(state.clone());
275 } else if x.starts_with("/ ") || x.starts_with("search") {
276 let x = x.to_string();
277
278 let state = state.clone();
279 tokio::spawn(async move {
280 state.lock().await.commands.push(CommandType::Search(
281 if x.starts_with("search ") {
282 x.trim_start_matches("search ")
283 } else {
284 x.trim_start_matches("/ ")
285 }
286 .to_string()
287 .split(" ")
288 .filter_map(|x| {
289 if x.is_empty() {
290 None
291 } else {
292 Some(x.to_string())
293 }
294 })
295 .collect(),
296 ));
297 notify_update_state(state.clone());
298 });
299 } else {
300 state.add_error(anyhow!("Invalid Command")).await;
301 }
302 }
303 }
304 buf = String::new();
305 }
306 last_buf = buf.clone();
307 state.lock().await.line_buf = buf;
308 tokio::time::sleep(Duration::new(0, 500000)).await;
309 } else {
310 tokio::time::sleep(Duration::new(1, 0)).await;
311 }
312 }
313 s.send(()).await?;
314 Ok(())
315}
316
317fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
319 let popup_layout = Layout::default()
320 .direction(Direction::Vertical)
321 .constraints(
322 [
323 Constraint::Percentage((100 - percent_y) / 2),
324 Constraint::Percentage(percent_y),
325 Constraint::Percentage((100 - percent_y) / 2),
326 ]
327 .as_ref(),
328 )
329 .split(r);
330
331 Layout::default()
332 .direction(Direction::Horizontal)
333 .constraints(
334 [
335 Constraint::Percentage((100 - percent_x) / 2),
336 Constraint::Percentage(percent_x),
337 Constraint::Percentage((100 - percent_x) / 2),
338 ]
339 .as_ref(),
340 )
341 .split(popup_layout[1])[1]
342}
343
344pub fn add_error(state: ProtectedState<'static>, e: anyhow::Error) {
345 let _ = std::thread::spawn(move || {
347 sit(async move {
348 state.lock().await.errors.push(e.to_string());
349 Ok(())
350 })
351 })
352 .join();
353}
354
355pub fn get_errors(state: ProtectedState<'static>) -> Option<Vec<String>> {
356 std::thread::spawn(move || {
357 sit(async move {
358 let errors = state.lock().await.errors.clone();
359 if errors.is_empty() {
360 Ok(None)
361 } else {
362 Ok(Some(errors))
363 }
364 })
365 })
366 .join()
367 .unwrap()
368 .unwrap()
369}
370
371pub fn render_error(
372 frame: &mut ratatui::Frame<'_, CrosstermBackend<Stdout>>,
373 layout: Rect,
374 e: String,
375) {
376 let layout = centered_rect(50, 20, layout);
377 let block = Block::default()
378 .title("Error")
379 .title_style(Style::default().fg(Color::Red))
380 .borders(Borders::ALL);
381 let area = block.inner(layout);
382
383 let paragraph = Paragraph::new(e + "\nPress any key to continue\n")
384 .style(Style::default().fg(Color::LightRed))
385 .alignment(Alignment::Center)
386 .wrap(Wrap { trim: true });
387 frame.render_widget(Clear, layout);
388 frame.render_widget(block, layout);
389 frame.render_widget(paragraph, area);
390}
391
392pub fn render_app(
393 state: ProtectedState<'static>,
394 frame: &mut ratatui::Frame<'_, CrosstermBackend<Stdout>>,
395 buf: String,
396 show: Option<Record>,
397 show_recurring: Option<RecurringRecord>,
398) {
399 let layout = Layout::default()
402 .constraints([Constraint::Length(1), Constraint::Percentage(100)].as_ref())
403 .split(frame.size());
404
405 let line_layout = Layout::default()
406 .direction(Direction::Horizontal)
407 .constraints([Constraint::Min(30), Constraint::Length(30)].as_ref())
408 .split(layout[0]);
409
410 let draw_layout = Layout::default()
411 .direction(Direction::Horizontal)
412 .constraints([Constraint::Percentage(70), Constraint::Percentage(30)].as_ref())
413 .split(layout[1]);
414
415 let s = state.clone();
416 let res = std::thread::spawn(move || sit(build_events(s))).join();
417
418 if let Ok(Ok(events)) = res {
419 let s = state.clone();
420 let res = std::thread::spawn(move || {
421 sit(async move {
422 let mut lock = s.lock().await;
423 let ret = lock.notification.clone();
424
425 if let Some(ret) = &ret {
426 if now().naive_local()
427 >= ret.1 + chrono::TimeDelta::try_seconds(1).unwrap_or_default()
428 {
429 lock.notification = None;
430 }
431 }
432
433 Ok(ret)
434 })
435 })
436 .join();
437
438 if let Ok(Ok(notification)) = res {
439 if let Some(notification) = notification {
440 frame.render_widget(
441 Paragraph::new(format!("[ {} ]", notification.0)).alignment(Alignment::Right),
442 line_layout[1],
443 );
444 }
445
446 if let Some(record) = show {
447 let s = state.clone();
448 let res = std::thread::spawn(move || sit(build_show_event(s, record))).join();
449 if let Ok(Ok(event)) = res {
450 frame.render_widget(event.deref().clone(), draw_layout[0]);
451 } else if let Ok(Err(e)) = res {
452 add_error(state.clone(), e);
453 } else {
454 add_error(
455 state.clone(),
456 anyhow!("Unknown error while showing an event"),
457 );
458 }
459 } else if let Some(record) = show_recurring {
460 let s = state.clone();
461 let res =
462 std::thread::spawn(move || sit(build_show_recurring_event(s, record))).join();
463 if let Ok(Ok(event)) = res {
464 frame.render_widget(event.deref().clone(), draw_layout[0]);
465 } else if let Ok(Err(e)) = res {
466 add_error(state.clone(), e);
467 } else {
468 add_error(
469 state.clone(),
470 anyhow!("Unknown error while showing an event"),
471 );
472 }
473 } else {
474 let s = state.clone();
475 let res = std::thread::spawn(move || sit(build_calendar(s))).join();
476 if let Ok(Ok(calendar)) = res {
477 frame.render_widget(calendar.deref().clone(), draw_layout[0]);
478 } else if let Ok(Err(e)) = res {
479 add_error(state.clone(), e);
480 } else {
481 add_error(
482 state.clone(),
483 anyhow!("Unknown error while showing calendar"),
484 );
485 }
486 }
487
488 frame.render_widget(events.deref().clone(), draw_layout[1]);
489 } else if let Ok(Err(e)) = res {
490 add_error(state.clone(), e);
491 } else {
492 add_error(
493 state.clone(),
494 anyhow!("Unknown error while polling for notifications"),
495 );
496 }
497 } else if let Ok(Err(e)) = res {
498 add_error(state.clone(), e);
499 } else {
500 add_error(state.clone(), anyhow!("Unknown error while listing events"));
501 }
502
503 if let Some(errors) = get_errors(state.clone()) {
504 render_error(frame, layout[1], errors.join("\n").to_string())
505 }
506
507 frame.render_widget(Paragraph::new(format!(">> {}", buf)), layout[0]);
508 frame.set_cursor(3 + buf.len() as u16, 0);
509}
510
511async fn get_month_name(state: ProtectedState<'static>) -> &str {
512 match chrono::Month::try_from(now().month() as u8) {
513 Ok(m) => m.name(),
514 Err(_) => {
515 state.add_error(anyhow!("Invalid Month")).await;
516 notify_update_state(state.clone());
517 ""
518 }
519 }
520}
521
522pub async fn build_show_recurring_event<'a>(
523 state: ProtectedState<'static>,
524 record: RecurringRecord,
525) -> Result<Arc<Table<'a>>> {
526 let header_cells = ["Key", "Value"]
527 .iter()
528 .map(|h| Cell::from(*h).style(*TITLE_STYLE));
529 let header = Row::new(header_cells)
530 .style(*HEADER_STYLE)
531 .height(1)
532 .bottom_margin(1);
533
534 let presented: PresentedRecurringRecord = record.clone().into();
535 let mut rows = vec![
536 Row::new(vec![
537 Cell::from("id"),
538 Cell::from(format!("{}", record.recurrence_key())),
539 ]),
540 Row::new(vec![
541 Cell::from("date"),
542 Cell::from(format!("{}", presented.record.date)),
543 ]),
544 Row::new(vec![
545 Cell::from("recurrence"),
546 Cell::from(format!("{}", presented.recurrence.to_string())),
547 ]),
548 Row::new(vec![
549 Cell::from("completed"),
550 Cell::from(format!("{}", presented.record.completed)),
551 ]),
552 Row::new(vec![
553 Cell::from("detail"),
554 Cell::from(format!("{}", presented.record.detail)),
555 ]),
556 Row::new(vec![
557 Cell::from("type"),
558 Cell::from(format!("{:?}", presented.record.typ)),
559 ]),
560 ];
561
562 match presented.record.typ {
563 crate::record::RecordType::At => rows.push(Row::new(vec![
564 Cell::from("at"),
565 Cell::from(format!("{}", presented.record.at.unwrap().format("%H:%M"))),
566 ])),
567 crate::record::RecordType::Schedule => rows.push(Row::new(vec![
568 Cell::from("scheduled"),
569 Cell::from(format!("{}", presented.record.scheduled.unwrap())),
570 ])),
571 _ => {}
572 }
573
574 let table = Arc::new(
575 Table::new(rows.clone())
576 .header(header)
577 .block(
578 Block::default()
579 .borders(Borders::ALL)
580 .title(get_month_name(state).await),
581 )
582 .widths(&[Constraint::Percentage(30), Constraint::Percentage(70)]),
583 );
584 Ok(table)
585}
586
587pub async fn build_show_event<'a>(
588 state: ProtectedState<'static>,
589 record: Record,
590) -> Result<Arc<Table<'a>>> {
591 let header_cells = ["Key", "Value"]
592 .iter()
593 .map(|h| Cell::from(*h).style(*TITLE_STYLE));
594 let header = Row::new(header_cells)
595 .style(*HEADER_STYLE)
596 .height(1)
597 .bottom_margin(1);
598
599 let presented: PresentedRecord = record.clone().into();
600 let mut rows = vec![
601 Row::new(vec![
602 Cell::from("id"),
603 Cell::from(format!("{}", record.primary_key())),
604 ]),
605 Row::new(vec![
606 Cell::from("date"),
607 Cell::from(format!("{}", presented.date)),
608 ]),
609 Row::new(vec![
610 Cell::from("completed"),
611 Cell::from(format!("{}", presented.completed)),
612 ]),
613 Row::new(vec![
614 Cell::from("detail"),
615 Cell::from(format!("{}", presented.detail)),
616 ]),
617 Row::new(vec![
618 Cell::from("type"),
619 Cell::from(format!("{:?}", presented.typ)),
620 ]),
621 Row::new(vec![
622 Cell::from("fields"),
623 Cell::from(record.fields().to_string()),
624 ]),
625 ];
626
627 match presented.typ {
628 crate::record::RecordType::At => rows.push(Row::new(vec![
629 Cell::from("at"),
630 Cell::from(format!("{}", presented.at.unwrap().format("%H:%M"))),
631 ])),
632 crate::record::RecordType::Schedule => rows.push(Row::new(vec![
633 Cell::from("scheduled"),
634 Cell::from(format!("{}", presented.scheduled.unwrap())),
635 ])),
636 _ => {}
637 }
638
639 let table = Arc::new(
640 Table::new(rows.clone())
641 .header(header)
642 .block(
643 Block::default()
644 .borders(Borders::ALL)
645 .title(get_month_name(state).await),
646 )
647 .widths(&[Constraint::Percentage(30), Constraint::Percentage(70)]),
648 );
649 Ok(table)
650}
651pub async fn build_calendar<'a>(state: ProtectedState<'static>) -> Result<Arc<Table<'a>>> {
652 if let Some(calendar) = state.lock().await.calendar.clone() {
653 if calendar.1 + chrono::TimeDelta::try_seconds(1).unwrap_or_default() > now().naive_local()
654 {
655 return Ok(calendar.0);
656 }
657 }
658
659 let header_cells = ["", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", ""]
660 .iter()
661 .map(|h| Cell::from(*h).style(*TITLE_STYLE));
662 let header = Row::new(header_cells)
663 .style(*HEADER_STYLE)
664 .height(1)
665 .bottom_margin(1);
666
667 let mut rows = Vec::new();
668 let mut last_row: Vec<(Cell<'_>, usize)> = Vec::new();
669 last_row.push((Cell::from("".to_string()), 0));
670
671 let datetime = now();
672 let date = now().date_naive();
673 let mut begin = chrono::NaiveDateTime::new(
674 chrono::NaiveDate::from_ymd_opt(
675 date.year_ce().1 as i32,
676 date.month0() + 1,
677 (date
678 - chrono::TimeDelta::try_days(datetime.weekday().num_days_from_sunday().into())
679 .unwrap_or_default())
680 .day0()
681 + 1,
682 )
683 .unwrap(),
684 chrono::NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
685 );
686
687 let mut lock = state.lock().await;
688 for x in 0..DAYS {
689 if x % DAYS_IN_WEEK == 0 && x != 0 {
690 last_row.push((Cell::from("".to_string()), 0));
691 rows.push(
692 Row::new(last_row.iter().map(|x| x.0.clone()).collect::<Vec<Cell>>()).height({
693 let res = last_row.iter().map(|res| res.1).max().unwrap_or(4) as u16;
694 if res > 4 {
695 res
696 } else {
697 4
698 }
699 }),
700 );
701 rows.push(Row::new(
702 ["", "", "", "", "", "", "", "", ""].map(Cell::from),
703 ));
704 last_row = Vec::new();
705 last_row.push((Cell::from("".to_string()), 0));
706 }
707
708 last_row.push(build_data(&mut lock, begin).await);
709 begin += chrono::TimeDelta::try_days(1).unwrap_or_default();
710 }
711 drop(lock);
712 last_row.push((Cell::from("".to_string()), 0));
713 rows.push(
714 Row::new(last_row.iter().map(|x| x.0.clone()).collect::<Vec<Cell>>()).height({
715 let res = last_row.iter().map(|x| x.1).max().unwrap_or(4) as u16;
716 if res > 4 {
717 res
718 } else {
719 4
720 }
721 }),
722 );
723
724 let table = Arc::new(
725 Table::new(rows.clone())
726 .header(header)
727 .block(
728 Block::default()
729 .borders(Borders::ALL)
730 .title(get_month_name(state.clone()).await),
731 )
732 .widths(&[
733 Constraint::Percentage(3),
734 Constraint::Percentage(12),
735 Constraint::Percentage(12),
736 Constraint::Percentage(12),
737 Constraint::Percentage(12),
738 Constraint::Percentage(12),
739 Constraint::Percentage(12),
740 Constraint::Percentage(12),
741 Constraint::Percentage(3),
742 ]),
743 );
744
745 let mut lock = state.lock().await;
746
747 if (!rows.is_empty() && lock.calendar.is_none()) || lock.calendar.is_some() {
748 lock.calendar = Some((table.clone(), now().naive_local()));
749 }
750
751 Ok(table)
752}
753
754pub async fn build_events<'a>(state: ProtectedState<'static>) -> Result<Arc<Table<'a>>> {
755 if let Some(events) = state.lock().await.events.clone() {
756 if events.1 + chrono::TimeDelta::try_seconds(1).unwrap_or_default() > now().naive_local() {
757 return Ok(events.0);
758 }
759 }
760
761 let datetime = now();
762 let date = datetime.date_naive();
763 let begin = chrono::NaiveDateTime::new(
764 chrono::NaiveDate::from_ymd_opt(
765 date.year_ce().1 as i32,
766 date.month0() + 1,
767 (date
768 - chrono::TimeDelta::try_days(datetime.weekday().num_days_from_sunday().into())
769 .unwrap_or_default())
770 .day0()
771 + 1,
772 )
773 .unwrap(),
774 chrono::NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
775 );
776
777 let header_cells = ["ID", "Time", "Summary"]
778 .iter()
779 .map(|h| Cell::from(*h).style(*TITLE_STYLE));
780 let header = Row::new(header_cells)
781 .style(*HEADER_STYLE)
782 .height(1)
783 .bottom_margin(1);
784
785 let mut inner = state.lock().await;
786 let rows = match inner.list_type {
787 ListType::All | ListType::Today | ListType::Search => inner
788 .records
789 .iter()
790 .filter_map(|r| {
791 if (r.all_day()
792 && chrono::NaiveDateTime::new(
793 r.date(),
794 chrono::NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
795 ) >= begin)
796 || r.datetime().naive_local() >= begin
797 {
798 let pk = format!("{}", r.primary_key());
799 let detail = r.detail().to_string();
800
801 let mut row = Row::new(vec![
802 Cell::from(pk),
803 if r.all_day() {
804 Cell::from(r.date().format("%m/%d [Day]").to_string())
805 } else {
806 Cell::from(r.datetime().format("%m/%d %H:%M").to_string())
807 },
808 Cell::from(detail),
809 ])
810 .style(Style::default().fg(Color::DarkGray));
811
812 if r.datetime().date_naive() == now().date_naive() {
813 row = row.style(Style::default().fg(Color::White))
814 }
815
816 if (r.all_day() && r.date() == now().date_naive())
817 || (datetime
818 > r.datetime() - chrono::TimeDelta::try_hours(1).unwrap_or_default()
819 && datetime
820 < r.datetime()
821 + chrono::TimeDelta::try_hours(1).unwrap_or_default())
822 {
823 row = row.style(Style::default().fg(Color::LightGreen))
824 }
825
826 Some(row)
827 } else {
828 None
829 }
830 })
831 .collect::<Vec<Row>>(),
832 ListType::Recurring => inner
833 .recurring_records
834 .iter()
835 .map(|r| {
836 let pk = format!("{}", r.recurrence_key());
837 let detail = r.clone().record().detail().to_string();
838 Row::new(vec![
839 Cell::from(pk),
840 Cell::from(r.recurrence().to_string()),
841 Cell::from(detail),
842 ])
843 })
844 .collect::<Vec<Row>>(),
845 };
846
847 let table = Arc::new(
848 Table::new(rows.clone())
849 .header(header)
850 .block(
851 Block::default()
852 .borders(Borders::ALL)
853 .title(match inner.list_type {
854 ListType::All => "All Events",
855 ListType::Today => "Today's Events",
856 ListType::Recurring => "Recurring Events",
857 ListType::Search => "Search Results",
858 }),
859 )
860 .widths(&[
861 Constraint::Length(5),
862 Constraint::Length(15),
863 Constraint::Percentage(100),
864 ]),
865 );
866
867 if (!rows.is_empty() && inner.events.is_none()) || inner.events.is_some() {
868 inner.events = Some((table.clone(), now().naive_local()));
869 }
870
871 Ok(table)
872}
873
874pub async fn find_dates<'a>(
875 state: &mut tokio::sync::MutexGuard<'_, State<'a>>,
876 date: chrono::NaiveDateTime,
877) -> Vec<Record> {
878 let mut v = Vec::new();
879
880 for item in state.records.clone() {
881 if item.date() == date.date() {
882 v.push(item);
883 }
884 }
885
886 v
887}
888
889pub async fn build_data<'a>(
890 state: &mut tokio::sync::MutexGuard<'_, State<'a>>,
891 date: chrono::NaiveDateTime,
892) -> (Cell<'a>, usize) {
893 let mut s = format!("{}\n", date.day());
894 for item in find_dates(state, date).await {
895 if item.all_day() {
896 s += &format!("[Day] {}\n", item.primary_key());
897 } else {
898 s += &format!(
899 "{} {}\n",
900 item.datetime().time().format("%H:%M"),
901 item.primary_key()
902 );
903 }
904 }
905
906 let style = if date.date() == now().date_naive() {
907 *TODAY_STYLE
908 } else {
909 *CELL_STYLE
910 };
911
912 (Cell::from(s.clone()).style(style), s.matches('\n').count())
913}
914
915pub fn handle_input(mut buf: String) -> Result<String> {
916 if event::poll(Duration::from_millis(250))? {
917 if let Event::Key(key) = event::read()? {
918 match key.code {
919 KeyCode::Char(x) => {
920 buf += &format!("{}", x);
921 }
922 KeyCode::Enter => {
923 buf += "\n";
924 }
925 KeyCode::Backspace => {
926 if !buf.is_empty() {
927 buf = buf[0..buf.len() - 1].to_string();
928 }
929 }
930 _ => {}
931 }
932 }
933 }
934
935 Ok(buf)
936}