saturn_cli/ui/
layout.rs

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                                        // we only need the first one
256                                        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
317// blatantly taken from ratatui examples
318fn 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    // I apparently hate myself
346    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    // NOTE: I apologize for making you read this code
400
401    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}