diff --git a/.config/config.toml b/.config/config.toml index 14c16cb..e39cb21 100644 --- a/.config/config.toml +++ b/.config/config.toml @@ -1,3 +1,41 @@ +[keybindings.Calendar] +# App +"" = "Quit" +"" = "Quit" +"" = "Suspend" +"" = "Help" +# Tabs +"" = "TabRight" +"" = "TabRight" +"" = "TabLeft" +"" = "TabLeft" +# Scrolling +"" = "ViewUp" +"" = "ViewUp" +"" = "ViewUp" +"" = "ViewPageUp" +"" = "ViewDown" +"" = "ViewDown" +"" = "ViewDown" +"" = "ViewPageDown" +# Navigation +"" = "ReloadVault" +"" = "GotoToday" +"" = "Down" +"" = "Down" +"" = "Up" +"" = "Up" +"" = "Left" +"" = "Left" +"" = "Right" +"" = "Right" +"" = "NextMonth" +"" = "NextMonth" +"" = "PreviousMonth" +"" = "PreviousMonth" +"" = "NextYear" +"" = "PreviousYear" + [keybindings.Explorer] # App "" = "Quit" diff --git a/Cargo.lock b/Cargo.lock index 6244dd4..40be2b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2616,6 +2616,7 @@ dependencies = [ "paste", "serde", "strum", + "time", "unicode-segmentation", "unicode-truncate", "unicode-width 0.2.0", @@ -3131,9 +3132,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.36" +version = "0.3.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" dependencies = [ "deranged", "itoa", @@ -3154,9 +3155,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" dependencies = [ "num-conv", "time-core", @@ -3525,6 +3526,7 @@ dependencies = [ "strip-ansi-escapes", "strum", "strum_macros", + "time", "tokio", "tokio-util", "toml", diff --git a/Cargo.toml b/Cargo.toml index a29b984..55c574b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,14 +24,14 @@ futures = "0.3.31" human-panic = "2.0.2" lazy_static = "1.5.0" libc = "0.2.167" -ratatui = {version = "0.29.0", features = ["serde", "macros"]} +ratatui = {version = "0.29.0", features = ["serde", "macros", "widget-calendar"]} signal-hook = "0.3.17" strip-ansi-escapes = "0.2.0" tokio = {version = "1.41.1", features = ["full"]} tokio-util = "0.7.12" tracing-error = "0.2.1" tracing-subscriber = {version = "0.3.19", features = ["env-filter", "serde"]} -chrono = "0.4.38" +chrono = {version="0.4.38"} tui-widget-list = "0.13.0" tui-input = "0.11.1" edit = "0.1.5" @@ -46,6 +46,7 @@ strum_macros = "0.26.4" notify-rust = "4.11.3" lexical-sort = "0.3.1" winnow = "0.6.20" +time = "0.3.37" [dev-dependencies] insta = {version = "1.41.1", features = ["yaml"]} diff --git a/README.md b/README.md index d05f074..e3d121d 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,20 @@ -onfig Vault-tasks +# Vault-tasks `vault-tasks` is a TUI Markdown task manager. It will parse any Markdown file or vault and display the tasks it contains. +## Demo using `./test-vault` + +``` +test-vault +├── chocolate_lava_cake.md +├── daily_workout.md +├── diy_bookshelf.md +├── study_plan.md +└── test.md +``` + ![Demo](./examples/demo_full.gif) ## Why @@ -24,13 +35,13 @@ I also spend most of my writing time in the terminal (Helix) and do not rely on - priority - Navigate vault - Search through tasks (sort and filter) +- Calendar view and timeline - Edit tasks or open in default editor - Time Management tab (Pomodoro & Flowtime) ## Planned Features - `new` action in Explorer Tab to create a new child on selected entry -- A Timeline tab with a calendar and a chronological view. (I'd also like to be able to import calendar files) ## Installation @@ -162,6 +173,29 @@ Check the key map within the app with `?` ![](./examples/demo_filter.gif) +#### Calendar Tab + +##### Navigation + +| Key | Alternate Key | Action | +| --------- | ------------- | -------- | +| `h` | `←` | +1 day | +| `l` | `→` | -1 day | +| `j` | `↓` | +7 days | +| `k` | `↑` | -7 days | +| `Shift-j` | `Shift-↓` | +1 month | +| `Shift-k` | `Shift-↑` | -1 month | +| `n` | | +1 year | +| `Shift-n` | | -1 year | + +##### Commands + +| Key | Action | +| --- | ---------- | +| `t` | Goto Today | + +![](./examples/demo_calendar.gif) + #### Time Management Tab ##### Navigation @@ -192,6 +226,7 @@ vault-tasks explorer # is the default # Or vault-tasks filter vault-tasks time +vault-tasks calendar ``` You can also output the content of a vault in standard output using diff --git a/examples/demo_calendar.gif b/examples/demo_calendar.gif new file mode 100644 index 0000000..12f8647 Binary files /dev/null and b/examples/demo_calendar.gif differ diff --git a/examples/demo_calendar.mp4 b/examples/demo_calendar.mp4 new file mode 100644 index 0000000..266bb82 Binary files /dev/null and b/examples/demo_calendar.mp4 differ diff --git a/examples/demo_calendar.tape b/examples/demo_calendar.tape new file mode 100644 index 0000000..fe6f851 --- /dev/null +++ b/examples/demo_calendar.tape @@ -0,0 +1,35 @@ +Output ./demo_calendar.gif +Output ./demo_calendar.mp4 + +Set Shell zsh +Set Theme "Builtin Solarized Light" +Set TypingSpeed 80ms +Set FontSize 22 +Set Width 1800 +Set Height 1000 + +Hide +Type "vault-tasks -c ../.config/ -v ../test-vault calendar" +Enter +Show +# Calendar tab +# +Sleep 2.5s + +Right +Sleep 2s +Right@0.5s 4 +Sleep 2s + +Right@0.5s 3 +Sleep 1s +Right@1s 2 +Sleep 1s + +Shift+k +Sleep 1s +Up@0.5s 3 + +# Stop vault-tasks +Ctrl+c + diff --git a/examples/demo_explorer.gif b/examples/demo_explorer.gif index 52c3f81..f0904e2 100644 Binary files a/examples/demo_explorer.gif and b/examples/demo_explorer.gif differ diff --git a/examples/demo_explorer.mp4 b/examples/demo_explorer.mp4 index dc48eec..08af56d 100644 Binary files a/examples/demo_explorer.mp4 and b/examples/demo_explorer.mp4 differ diff --git a/examples/demo_explorer.tape b/examples/demo_explorer.tape index 428616e..a129ebb 100644 --- a/examples/demo_explorer.tape +++ b/examples/demo_explorer.tape @@ -8,10 +8,12 @@ Set Theme "Builtin Solarized Light" Set TypingSpeed 80ms Set FontSize 22 Set Width 1800 -Set Height 800 +Set Height 1000 +Hide Type "vault-tasks -c ../.config/ -v ../test-vault explorer" Enter +Show # First entry Sleep 3s diff --git a/examples/demo_filter.gif b/examples/demo_filter.gif index 3ff8047..7865bb9 100644 Binary files a/examples/demo_filter.gif and b/examples/demo_filter.gif differ diff --git a/examples/demo_filter.mp4 b/examples/demo_filter.mp4 index 5c7b098..c65b997 100644 Binary files a/examples/demo_filter.mp4 and b/examples/demo_filter.mp4 differ diff --git a/examples/demo_filter.tape b/examples/demo_filter.tape index 5dafab5..73c8032 100644 --- a/examples/demo_filter.tape +++ b/examples/demo_filter.tape @@ -6,10 +6,12 @@ Set Theme "Builtin Solarized Light" Set TypingSpeed 80ms Set FontSize 22 Set Width 1800 -Set Height 800 +Set Height 1000 +Hide Type "vault-tasks -c ../.config/ -v ../test-vault filter" Enter +Show Sleep 3s Type "#tobuy 09/29" diff --git a/examples/demo_full.gif b/examples/demo_full.gif index cfab725..8ea22f8 100644 Binary files a/examples/demo_full.gif and b/examples/demo_full.gif differ diff --git a/examples/demo_full.mp4 b/examples/demo_full.mp4 index aed870b..1176dfd 100644 Binary files a/examples/demo_full.mp4 and b/examples/demo_full.mp4 differ diff --git a/examples/demo_full.tape b/examples/demo_full.tape index a858a09..cf1da00 100644 --- a/examples/demo_full.tape +++ b/examples/demo_full.tape @@ -13,8 +13,9 @@ Set TypingSpeed 80ms Set FontSize 22 Set FontSize 22 Set Width 1800 -Set Height 800 +Set Height 1000 Source ./demo_explorer.tape Source ./demo_filter.tape +Source ./demo_calendar.tape Source ./demo_time.tape diff --git a/examples/demo_full.webm b/examples/demo_full.webm index 4c892a4..fa397d7 100644 Binary files a/examples/demo_full.webm and b/examples/demo_full.webm differ diff --git a/examples/demo_time.gif b/examples/demo_time.gif index 153dea7..471679b 100644 Binary files a/examples/demo_time.gif and b/examples/demo_time.gif differ diff --git a/examples/demo_time.mp4 b/examples/demo_time.mp4 index bbacbca..b4cbe5b 100644 Binary files a/examples/demo_time.mp4 and b/examples/demo_time.mp4 differ diff --git a/examples/demo_time.tape b/examples/demo_time.tape index d102027..8de2cf4 100644 --- a/examples/demo_time.tape +++ b/examples/demo_time.tape @@ -6,10 +6,12 @@ Set Theme "Builtin Solarized Light" Set TypingSpeed 80ms Set FontSize 22 Set Width 1800 -Set Height 800 +Set Height 1000 +Hide Type "vault-tasks -c ../.config/ -v ../test-vault time" Enter +Show # Time Management tab Sleep 1s diff --git a/examples/justfile b/examples/justfile index 3ab5dcb..fefb2cd 100644 --- a/examples/justfile +++ b/examples/justfile @@ -1,6 +1,7 @@ make-all-vhs: vhs < ./demo_explorer.tape vhs < ./demo_filter.tape + vhs < ./demo_calendar.tape vhs < ./demo_time.tape git checkout HEAD -- ../test-vault/test.md # it is edited during explorer demo vhs < ./demo_full.tape diff --git a/src/action.rs b/src/action.rs index f872faf..484f0e5 100644 --- a/src/action.rs +++ b/src/action.rs @@ -19,6 +19,11 @@ pub enum Action { Key(KeyEvent), ReloadVault, // Movements + GotoToday, + NextMonth, + PreviousMonth, + NextYear, + PreviousYear, PreviousMethod, NextMethod, NextSegment, diff --git a/src/app.rs b/src/app.rs index 1e2b247..ac1e096 100644 --- a/src/app.rs +++ b/src/app.rs @@ -9,8 +9,8 @@ use crate::{ action::Action, cli::{Cli, Commands}, components::{ - explorer_tab::ExplorerTab, filter_tab::FilterTab, fps::FpsCounter, home::Home, - time_management_tab::TimeManagementTab, Component, + calendar_tab::CalendarTab, explorer_tab::ExplorerTab, filter_tab::FilterTab, + fps::FpsCounter, home::Home, time_management_tab::TimeManagementTab, Component, }, config::Config, tui::{Event, Tui}, @@ -41,6 +41,7 @@ pub enum Mode { Explorer, Filter, TimeManagement, + Calendar, } impl App { @@ -56,6 +57,7 @@ impl App { Box::::default(), Box::new(ExplorerTab::new()), Box::new(FilterTab::new()), + Box::new(CalendarTab::new()), Box::new(TimeManagementTab::new()), ], should_quit: false, @@ -72,6 +74,7 @@ impl App { let tab = match args.command { Some(Commands::Filter) => Action::Focus(Mode::Filter), Some(Commands::TimeManagement) => Action::Focus(Mode::TimeManagement), + Some(Commands::Calendar) => Action::Focus(Mode::Calendar), Some(Commands::Explorer | Commands::GenerateConfig { path: _ }) | None => { Action::Focus(Mode::Explorer) } diff --git a/src/cli.rs b/src/cli.rs index dcee07a..a0e326b 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -37,6 +37,9 @@ pub enum Commands { /// Open Time Management view #[command(alias = "time")] TimeManagement, + /// Open Calendar view + #[command(alias = "cld")] + Calendar, /// Generates a new configuration file from the default one GenerateConfig { path: Option }, /// Write tasks to STDOUT diff --git a/src/components.rs b/src/components.rs index c6eabef..8cd3528 100644 --- a/src/components.rs +++ b/src/components.rs @@ -12,6 +12,7 @@ use crate::{ tui::{Event, Tui}, }; +pub mod calendar_tab; pub mod explorer_tab; pub mod filter_tab; pub mod fps; diff --git a/src/components/calendar_tab.rs b/src/components/calendar_tab.rs new file mode 100644 index 0000000..6037125 --- /dev/null +++ b/src/components/calendar_tab.rs @@ -0,0 +1,463 @@ +use std::collections::hash_map::Entry; + +use ::time::{Date, OffsetDateTime}; +use chrono::{Datelike, Duration, NaiveDate, NaiveTime}; +use ratatui::{ + layout::{Constraint, Layout, Rect}, + style::{Color, Modifier, Style, Stylize}, + text::{Line, Span, ToSpan}, + widgets::{calendar::CalendarEventStore, StatefulWidget, Widget}, + Frame, +}; +use time::{util::days_in_year, Weekday}; +use tracing::error; +use tui_scrollview::ScrollViewState; + +use crate::{ + action::Action, + app::Mode, + config::Config, + core::{ + filter::{filter_to_vec, Filter}, + sorter::SortingMode, + task::{DueDate, State, Task}, + vault_data::VaultData, + TaskManager, + }, + widgets::{help_menu::HelpMenu, styled_calendar::StyledCalendar, task_list::TaskList}, +}; + +use super::Component; + +/// Struct that helps with drawing the component +struct CalendarTabArea { + date: Rect, + calendar: Rect, + legend: Rect, + footer: Rect, + timeline: Rect, +} + +pub struct CalendarTab<'a> { + // Utils + config: Config, + is_focused: bool, + task_mgr: TaskManager, + // Content + tasks: Vec, + entries_list: TaskList, + events: CalendarEventStore, + selected_date: Date, + task_list_widget_state: ScrollViewState, + // Whether the help panel is open or not + show_help: bool, + help_menu_wigdet: HelpMenu<'a>, +} +impl Default for CalendarTab<'_> { + fn default() -> Self { + Self { + selected_date: OffsetDateTime::now_local().unwrap().date(), + config: Config::default(), + is_focused: false, + show_help: false, + help_menu_wigdet: HelpMenu::default(), + tasks: vec![], + task_mgr: TaskManager::default(), + task_list_widget_state: ScrollViewState::new(), + entries_list: TaskList::default(), + events: CalendarEventStore::default(), + } + } +} +impl CalendarTab<'_> { + const SELECTED: Style = Style::new() + .fg(Color::White) + .bg(Color::Red) + .add_modifier(Modifier::BOLD); + const PREVIEWED: Style = Style::new() + .fg(Color::White) + .bg(Color::Green) + .add_modifier(Modifier::BOLD); + const TASK_DONE: Style = Style::new() + .fg(Color::Green) + .add_modifier(Modifier::UNDERLINED); + const TASK_TODO: Style = Style::new() + .fg(Color::Red) + .add_modifier(Modifier::UNDERLINED); + pub fn new() -> Self { + Self::default() + } + fn split_frame(area: Rect) -> CalendarTabArea { + let [_header, content, footer, _tab_footera] = Layout::vertical([ + Constraint::Length(1), // tabs + Constraint::Min(0), // content + Constraint::Length(1), //footer + Constraint::Length(1), // home footer + ]) + .areas(area); + + let [calendar, timeline] = Layout::horizontal([ + Constraint::Length(7 * 3 + 5 + 4), // calendar + Constraint::Min(0), // timeline + ]) + .areas(content); + let [calendar, legend] = Layout::vertical([ + Constraint::Length(7 * 3 + 5), // calendar + Constraint::Min(0), // legend + ]) + .areas(calendar); + + let [date, timeline] = Layout::vertical([ + Constraint::Length(1), // date + Constraint::Min(0), // timeline + ]) + .areas(timeline); + + CalendarTabArea { + date, + calendar, + legend, + footer, + timeline, + } + } + fn render_footer(area: Rect, frame: &mut Frame) { + ratatui::widgets::Widget::render( + Line::raw("Navigate: | Month: Shift+ | Goto Today: ").centered(), + area, + frame.buffer_mut(), + ); + } + fn update_tasks(&mut self) { + self.tasks = filter_to_vec(&self.task_mgr.tasks, &Filter::default()); + self.tasks.sort_by(SortingMode::cmp_due_date); + + self.entries_list = TaskList::new( + &self.config, + &self + .tasks + .clone() + .iter() + .map(|t| VaultData::Task(t.clone())) + .collect::>(), + true, + ); + } + fn updated_date(&mut self) { + let mut index_closest_task = 0; + let mut best = Duration::max_value(); + for (i, task) in self.tasks.iter().enumerate() { + let d = match task.due_date { + DueDate::NoDate => Duration::max_value(), + DueDate::Day(naive_date) => NaiveDate::from_ymd_opt( + self.selected_date.year(), + self.selected_date.month() as u32, + u32::from(self.selected_date.day()), + ) + .unwrap() + .signed_duration_since(naive_date) + .abs(), + DueDate::DayTime(naive_date_time) => NaiveDate::from_ymd_opt( + self.selected_date.year(), + self.selected_date.month() as u32, + u32::from(self.selected_date.day()), + ) + .unwrap() + .and_time(NaiveTime::default()) + .signed_duration_since(naive_date_time) + .abs(), + }; + if d < best { + best = d; + index_closest_task = i; + } + } + self.task_list_widget_state.scroll_to_top(); + (0..self.entries_list.height_of(index_closest_task)).for_each(|_| { + self.task_list_widget_state.scroll_down(); + }); + self.tasks_to_events(self.tasks.clone().get(index_closest_task)); + } + #[allow(clippy::cast_possible_truncation)] + fn naive_date_to_date(naive_date: NaiveDate) -> Date { + Date::from_iso_week_date( + naive_date.year(), + naive_date.iso_week().week() as u8, + match naive_date.weekday() { + chrono::Weekday::Mon => Weekday::Monday, + chrono::Weekday::Tue => Weekday::Tuesday, + chrono::Weekday::Wed => Weekday::Wednesday, + chrono::Weekday::Thu => Weekday::Thursday, + chrono::Weekday::Fri => Weekday::Friday, + chrono::Weekday::Sat => Weekday::Saturday, + chrono::Weekday::Sun => Weekday::Sunday, + }, + ) + .unwrap() + } + fn tasks_to_events(&mut self, previewed_task: Option<&Task>) { + self.events = CalendarEventStore::today( + Style::default() + .add_modifier(Modifier::BOLD) + .bg(Color::Blue), + ); + // Previewed date + if let Some(t) = previewed_task { + match t.due_date { + DueDate::NoDate => (), + DueDate::Day(naive_date) => self + .events + .add(Self::naive_date_to_date(naive_date), Self::PREVIEWED), + + DueDate::DayTime(naive_date_time) => self.events.add( + Self::naive_date_to_date(naive_date_time.date()), + Self::PREVIEWED, + ), + } + } + // selected date + self.events.add(self.selected_date, Self::SELECTED); + + let mut current = None; + for task in self.tasks.clone() { + let next = match task.clone().due_date { + DueDate::NoDate => None, + DueDate::Day(naive_date) => Some(Self::naive_date_to_date(naive_date)), + + DueDate::DayTime(naive_datetime) => { + Some(Self::naive_date_to_date(naive_datetime.date())) + } + }; + let theme = match task.state { + State::ToDo | State::Incomplete => Self::TASK_TODO, + State::Done | State::Canceled => Self::TASK_DONE, + }; + if let Some(date) = next { + // Already marked as selected + if date == self.selected_date + || self + .events + .0 + .get(&date) + .is_some_and(|&t| t == Self::PREVIEWED) + { + self.events.0.insert( + date, + self.events + .0 + .get(&date) + .unwrap() + .add_modifier(Modifier::UNDERLINED), + ); + } + + // Are we on the same date as before ? + if current.is_some_and(|d: Date| d == date) { + // update if needed + if let Entry::Occupied(mut e) = self.events.0.entry(date) { + if theme == Self::TASK_TODO { + e.insert(theme); // Todo has priority over Done + } + } else { + error!("No event on this date but tasks exist"); + } + } + if self.events.0.contains_key(&date) { + error!("Calendar entry exists but no tasks were added yet"); + } else { + self.events.add(date, theme); + current = next; + } + } + } + } + fn render_legend(areas: &CalendarTabArea, frame: &mut Frame<'_>) { + let [todo, done, selected, previewed, today] = + Layout::vertical([Constraint::Length(1); 5]).areas(areas.legend); + ratatui::widgets::Widget::render( + Span::raw("Todo") + .style(Self::TASK_TODO) + .into_left_aligned_line(), + todo, + frame.buffer_mut(), + ); + ratatui::widgets::Widget::render( + Span::raw("Done") + .style(Self::TASK_DONE) + .into_left_aligned_line(), + done, + frame.buffer_mut(), + ); + ratatui::widgets::Widget::render( + Span::raw("Selected") + .style(Self::SELECTED) + .into_left_aligned_line(), + selected, + frame.buffer_mut(), + ); + ratatui::widgets::Widget::render( + Span::raw("Previewed") + .style(Self::PREVIEWED) + .into_left_aligned_line(), + previewed, + frame.buffer_mut(), + ); + ratatui::widgets::Widget::render( + Span::raw("Today") + .style( + Style::default() + .add_modifier(Modifier::BOLD) + .bg(Color::Blue), + ) + .into_left_aligned_line(), + today, + frame.buffer_mut(), + ); + } +} +impl Component for CalendarTab<'_> { + fn register_config_handler(&mut self, config: Config) -> color_eyre::eyre::Result<()> { + self.task_mgr = TaskManager::load_from_config(&config.tasks_config)?; + self.config = config; + + self.update_tasks(); + self.updated_date(); + self.help_menu_wigdet = HelpMenu::new(Mode::Calendar, &self.config); + Ok(()) + } + + fn update( + &mut self, + _tui: Option<&mut crate::tui::Tui>, + action: crate::action::Action, + ) -> color_eyre::eyre::Result> { + if !self.is_focused { + match action { + Action::ReloadVault => { + self.task_mgr.reload(&self.config.tasks_config)?; + self.update_tasks(); + self.updated_date(); + } + Action::Focus(Mode::Calendar) => self.is_focused = true, + Action::Focus(mode) if !(mode == Mode::Calendar) => self.is_focused = false, + _ => (), + } + } else if self.show_help { + match action { + Action::ViewUp | Action::Up => self.help_menu_wigdet.scroll_up(), + Action::ViewDown | Action::Down => self.help_menu_wigdet.scroll_down(), + Action::Help | Action::Escape | Action::Enter => { + self.show_help = !self.show_help; + } + _ => (), + } + } else { + match action { + Action::Focus(mode) if mode != Mode::Calendar => self.is_focused = false, + Action::Focus(Mode::Calendar) => self.is_focused = true, + Action::Help => self.show_help = !self.show_help, + Action::GotoToday => { + self.selected_date = OffsetDateTime::now_local().unwrap().date(); + self.updated_date(); + } + Action::ReloadVault => { + self.task_mgr.reload(&self.config.tasks_config)?; + self.update_tasks(); + self.updated_date(); + } + Action::Left => { + self.selected_date -= time::Duration::days(1); + + self.updated_date(); + } + Action::Down => { + self.selected_date += time::Duration::weeks(1); + self.updated_date(); + } + Action::Up => { + self.selected_date -= time::Duration::weeks(1); + self.updated_date(); + } + Action::Right => { + self.selected_date += time::Duration::days(1); + self.updated_date(); + } + Action::NextMonth => { + self.selected_date += time::Duration::days(i64::from( + self.selected_date.month().length(self.selected_date.year()), + )); + self.updated_date(); + } + Action::PreviousMonth => { + self.selected_date -= time::Duration::days(i64::from( + self.selected_date.month().length(self.selected_date.year()), + )); + self.updated_date(); + } + Action::NextYear => { + self.selected_date += time::Duration::days(i64::from(days_in_year( + self.selected_date.year() + 1, + ))); + self.updated_date(); + } + + Action::PreviousYear => { + self.selected_date -= time::Duration::days(i64::from(days_in_year( + self.selected_date.year() + 1, + ))); + self.updated_date(); + } + Action::ViewUp => self.task_list_widget_state.scroll_up(), + Action::ViewDown => self.task_list_widget_state.scroll_down(), + Action::ViewPageUp => self.task_list_widget_state.scroll_page_up(), + Action::ViewPageDown => self.task_list_widget_state.scroll_page_down(), + Action::ViewRight => self.task_list_widget_state.scroll_right(), + Action::ViewLeft => self.task_list_widget_state.scroll_left(), + _ => (), + } + } + Ok(None) + } + fn draw( + &mut self, + frame: &mut ratatui::Frame, + area: ratatui::prelude::Rect, + ) -> color_eyre::eyre::Result<()> { + if !self.is_focused { + return Ok(()); + } + + let areas = Self::split_frame(area); + + // Calendar + StyledCalendar::render_quarter(frame, areas.calendar, self.selected_date, &self.events); + + // Legend + Self::render_legend(&areas, frame); + + // Date + self.selected_date + .to_span() + .bold() + .render(areas.date, frame.buffer_mut()); + + // Timeline + self.entries_list.clone().render( + areas.timeline, + frame.buffer_mut(), + &mut self.task_list_widget_state, + ); + + // Footer + Self::render_footer(areas.footer, frame); + // Help + if self.show_help { + self.help_menu_wigdet.clone().render( + area, + frame.buffer_mut(), + &mut self.help_menu_wigdet.state, + ); + } + Ok(()) + } +} diff --git a/src/components/explorer_tab.rs b/src/components/explorer_tab.rs index 25b389b..b1235db 100644 --- a/src/components/explorer_tab.rs +++ b/src/components/explorer_tab.rs @@ -243,17 +243,11 @@ impl ExplorerTab<'_> { // If we have tasks, then render a TaskList widget match self.entries_right_view.first() { Some(VaultData::Task(_) | VaultData::Header(_, _, _)) => { - TaskList::new(&self.config, &self.entries_right_view, false) - .header_style( - *self - .config - .styles - .get(&crate::app::Mode::Explorer) - .unwrap() - .get("preview_headers") - .unwrap(), - ) - .render(area, frame.buffer_mut(), &mut self.task_list_widget_state); + TaskList::new(&self.config, &self.entries_right_view, false).render( + area, + frame.buffer_mut(), + &mut self.task_list_widget_state, + ); } // Else render a ListView widget Some(VaultData::Directory(_, _)) => Self::build_list( diff --git a/src/components/home.rs b/src/components/home.rs index 2b6c80b..f9e7f6b 100644 --- a/src/components/home.rs +++ b/src/components/home.rs @@ -24,6 +24,7 @@ impl Home { SelectedTab::Explorer => Action::Focus(Mode::Explorer), SelectedTab::Filter => Action::Focus(Mode::Filter), SelectedTab::TimeManagement => Action::Focus(Mode::TimeManagement), + SelectedTab::Calendar => Action::Focus(Mode::Calendar), }) { error!("Could not focus selected tab: {e}"); } @@ -83,6 +84,7 @@ impl Component for Home { Action::Focus(Mode::Explorer) => self.selected_tab = SelectedTab::Explorer, Action::Focus(Mode::Filter) => self.selected_tab = SelectedTab::Filter, Action::Focus(Mode::TimeManagement) => self.selected_tab = SelectedTab::TimeManagement, + Action::Focus(Mode::Calendar) => self.selected_tab = SelectedTab::Calendar, _ => (), } Ok(None) @@ -106,6 +108,8 @@ enum SelectedTab { Explorer, #[strum(to_string = "Filter")] Filter, + #[strum(to_string = "Calendar")] + Calendar, #[strum(to_string = "Time Management")] TimeManagement, } diff --git a/src/components/snapshots/vault_tasks__components__home__tests__render_home_component.snap b/src/components/snapshots/vault_tasks__components__home__tests__render_home_component.snap index a35a3bb..1cc43c2 100644 --- a/src/components/snapshots/vault_tasks__components__home__tests__render_home_component.snap +++ b/src/components/snapshots/vault_tasks__components__home__tests__render_home_component.snap @@ -2,7 +2,7 @@ source: src/components/home.rs expression: terminal.backend() --- -" Explorer Filter Time Management " +" Explorer Filter Calendar Time Management " " " " " " " diff --git a/src/core/filter.rs b/src/core/filter.rs index ed5fb89..8d2e2af 100644 --- a/src/core/filter.rs +++ b/src/core/filter.rs @@ -7,7 +7,7 @@ use super::{ vault_data::VaultData, }; -#[derive(PartialEq, Eq, Debug)] +#[derive(Default, PartialEq, Eq, Debug)] pub struct Filter { pub task: Task, state: Option, diff --git a/src/core/sorter.rs b/src/core/sorter.rs index 81c37c7..a07a293 100644 --- a/src/core/sorter.rs +++ b/src/core/sorter.rs @@ -29,7 +29,7 @@ impl SortingMode { } /// Compare two tasks by due date - fn cmp_due_date(t1: &Task, t2: &Task) -> Ordering { + pub fn cmp_due_date(t1: &Task, t2: &Task) -> Ordering { match (&t1.due_date, &t2.due_date) { (DueDate::Day(d1), DueDate::Day(d2)) => d1.cmp(d2), (DueDate::DayTime(d1), DueDate::DayTime(d2)) => d1.cmp(d2), diff --git a/src/widgets.rs b/src/widgets.rs index 588898f..ae59498 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -1,5 +1,6 @@ pub mod help_menu; pub mod input_bar; +pub mod styled_calendar; pub mod task_list; pub mod task_list_item; pub mod timer; diff --git a/src/widgets/styled_calendar.rs b/src/widgets/styled_calendar.rs new file mode 100644 index 0000000..4ffe3d7 --- /dev/null +++ b/src/widgets/styled_calendar.rs @@ -0,0 +1,80 @@ +use ratatui::{ + layout::{Constraint, Layout, Margin, Rect}, + style::{Style, Stylize}, + widgets::calendar::{CalendarEventStore, Monthly}, + Frame, +}; +use time::{Date, Month}; + +#[derive(Default, Clone, Copy)] +pub struct StyledCalendar; + +impl StyledCalendar { + // pub fn render_year(frame: &mut Frame, area: Rect, date: Date, events: &CalendarEventStore) { + // let area = area.inner(Margin { + // vertical: 1, + // horizontal: 1, + // }); + // let rows = Layout::vertical([Constraint::Ratio(1, 3); 3]).split(area); + // let areas = rows.iter().flat_map(|row| { + // Layout::horizontal([Constraint::Ratio(1, 4); 4]) + // .split(*row) + // .to_vec() + // }); + // for (i, area) in areas.enumerate() { + // let month = date + // .replace_day(1) + // .unwrap() + // .replace_month(Month::try_from(i as u8 + 1).unwrap()) + // .unwrap(); + // StyledCalendar::render_month(frame, area, month, events); + // } + // } + + pub fn render_quarter(frame: &mut Frame, area: Rect, date: Date, events: &CalendarEventStore) { + let area = area.inner(Margin { + vertical: 1, + horizontal: 1, + }); + let [pred, cur, next] = Layout::vertical([Constraint::Length(2 + 5 + 1); 3]).areas(area); + + let mut prev_date = date; + if date.month() == Month::January { + prev_date = prev_date.replace_year(date.year() - 1).unwrap(); + } + StyledCalendar::render_month( + frame, + pred, + prev_date + .replace_day(1) + .unwrap() + .replace_month(date.month().previous()) + .unwrap(), + events, + ); + StyledCalendar::render_month(frame, cur, date.replace_day(1).unwrap(), events); + let mut next_date = date; + if date.month() == Month::December { + next_date = next_date.replace_year(date.year() + 1).unwrap(); + } + StyledCalendar::render_month( + frame, + next, + next_date + .replace_day(1) + .unwrap() + .replace_month(date.month().next()) + .unwrap(), + events, + ); + } + + fn render_month(frame: &mut Frame, area: Rect, date: Date, events: &CalendarEventStore) { + let calendar = Monthly::new(date, events) + .default_style(Style::new().bold()) + .show_month_header(Style::default()) + .show_surrounding(Style::new().dim()) + .show_weekdays_header(Style::new().bold().green()); + frame.render_widget(calendar, area); + } +} diff --git a/src/widgets/task_list.rs b/src/widgets/task_list.rs index 143880e..a885823 100644 --- a/src/widgets/task_list.rs +++ b/src/widgets/task_list.rs @@ -1,4 +1,4 @@ -use crate::core::{vault_data::VaultData, PrettySymbolsConfig}; +use crate::core::vault_data::VaultData; use ratatui::prelude::*; use tui_scrollview::{ScrollView, ScrollViewState}; @@ -8,28 +8,47 @@ use super::task_list_item::TaskListItem; #[derive(Default, Clone)] pub struct TaskList { - file_content: Vec, - symbols: PrettySymbolsConfig, - not_american_format: bool, - show_relative_due_dates: bool, - display_filename: bool, - header_style: Style, + content: Vec, + constraints: Vec, + height: u16, } impl TaskList { pub fn new(config: &Config, file_content: &[VaultData], display_filename: bool) -> Self { + let content = file_content + .iter() + .map(|fc| { + TaskListItem::new( + fc.clone(), + !config.tasks_config.use_american_format, + config.tasks_config.pretty_symbols.clone(), + display_filename, + config.tasks_config.show_relative_due_dates, + ) + .header_style( + *config + .styles + .get(&crate::app::Mode::Explorer) + .unwrap() + .get("preview_headers") + .unwrap(), + ) + }) + .collect::>(); + let mut height = 0; + let mut constraints = vec![]; + for item in &content { + height += item.height; + constraints.push(Constraint::Length(item.height)); + } Self { - not_american_format: !config.tasks_config.use_american_format, - symbols: config.tasks_config.pretty_symbols.clone(), - file_content: file_content.to_vec(), - display_filename, - header_style: Style::default(), - show_relative_due_dates: config.tasks_config.show_relative_due_dates, + content, + constraints, + height, } } - pub const fn header_style(mut self, style: Style) -> Self { - self.header_style = style; - self + pub fn height_of(&mut self, i: usize) -> u16 { + (0..i).map(|i| self.content[i].height).sum() } } impl StatefulWidget for TaskList { @@ -42,46 +61,24 @@ impl StatefulWidget for TaskList { ) where Self: Sized, { - let content = self - .file_content - .iter() - .map(|fc| { - TaskListItem::new( - fc.clone(), - self.not_american_format, - self.symbols.clone(), - self.display_filename, - self.show_relative_due_dates, - ) - .header_style(self.header_style) - }) - .collect::>(); - - let mut constraints = vec![]; - let mut height = 0; - for item in &content { - height += item.height; - constraints.push(Constraint::Length(item.height)); - } - // If we need the vertical scrollbar // Then take into account that we need to draw it // // If we don't do this, the horizontal scrollbar // appears for only one character // It basically disables the horizontal scrollbar - let width = if height > area.height { + let width = if self.height > area.height { area.width - 1 } else { area.width }; - let size = Size::new(width, height); + let size = Size::new(width, self.height); let mut scroll_view = ScrollView::new(size); - let layout = Layout::vertical(constraints).split(scroll_view.area()); + let layout = Layout::vertical(self.constraints).split(scroll_view.area()); - for (i, item) in content.into_iter().enumerate() { + for (i, item) in self.content.into_iter().enumerate() { scroll_view.render_widget(item, layout[i]); } scroll_view.render(area, buf, state); diff --git a/src/widgets/task_list_item.rs b/src/widgets/task_list_item.rs index 183902b..55b74e4 100644 --- a/src/widgets/task_list_item.rs +++ b/src/widgets/task_list_item.rs @@ -12,6 +12,7 @@ use crate::core::{ PrettySymbolsConfig, }; +#[derive(Clone)] pub struct TaskListItem { item: VaultData, pub height: u16, diff --git a/test-vault/diy_bookshelf.md b/test-vault/diy_bookshelf.md index 6560ab5..e41b98a 100644 --- a/test-vault/diy_bookshelf.md +++ b/test-vault/diy_bookshelf.md @@ -1,23 +1,23 @@ # Home Improvement: DIY Bookshelf -- [x] Gather materials (Wood, screws, sandpaper, paint) 2024/09/30 #bookshelf +- [/] Gather materials (Wood, screws, sandpaper, paint) #bookshelf - - [x] Pine boards (2 @ 1x6, 4 @ 1x2, 4 @ 1x4) 2024/09/30 #toBuy - - [x] Wood screws (1.5 inch) 2024/09/30 #toBuy - - [x] Sandpaper (grit 120) 2024/09/30 #toBuy - - [x] Paint or stain of choice 2024/09/30 #toBuy + - [x] Pine boards (2 @ 1x6, 4 @ 1x2, 4 @ 1x4) 2024/12/28 #toBuy + - [x] Wood screws (1.5 inch) 2024/12/28 #toBuy + - [ ] Sandpaper (grit 120) 2024/12/28 #toBuy + - [ ] Paint or stain of choice 2024/12/29 #toBuy -- [x] Design bookshelf layout 2024/09/30 #bookshelf +- [x] Design bookshelf layout 2024/12/28 #bookshelf - - [x] Determine dimensions and placement of shelves 2024/09/30 - - [x] Sketch design on paper or digital software 2024/09/30 + - [x] Determine dimensions and placement of shelves + - [x] Sketch design on paper or digital software -- [x] Cut wood pieces to size using a circular saw or table saw 2024/10/01 #bookshelf +- [/] Cut wood pieces to size using a circular saw or table saw #bookshelf - - [x] Assemble bookshelf frame with screws and wood glue 2024/10/01 - - [x] Attach the vertical supports (1x6 boards) to the bottom and top horizontal supports (1x4 boards) 2024/10/01 - - [x] Attach the shelf dividers (1x2 boards) between the shelves 2024/10/01 + - [ ] Assemble bookshelf frame with screws and wood glue 2024/12/29 + - [ ] Attach the vertical supports (1x6 boards) to the bottom and top horizontal supports (1x4 boards) 2025/01/02 + - [ ] Attach the shelf dividers (1x2 boards) between the shelves 2025/01/02 -- [/] Sand the bookshelf for a smooth finish 2024/10/01 #bookshelf - - [x] Paint or stain the bookshelf according to your chosen color or finish 2024/10/01 - - [ ] Allow the paint/stain to dry completely before using the bookshelf 2024/10/01 +- [ ] Sand the bookshelf for a smooth finish #bookshelf + - [ ] Paint or stain the bookshelf according to your chosen color or finish 2024/12/30 + - [ ] Allow the paint/stain to dry completely before using the bookshelf 2025/01/06 diff --git a/test-vault/test.md b/test-vault/test.md index 29f9f01..0bea048 100644 --- a/test-vault/test.md +++ b/test-vault/test.md @@ -15,7 +15,7 @@ - [ ] Test b - [ ] Test c -- [ ] Test 1 -- [x] Test 2 -- [-] Test 3 -- [/] Test 4 +- [ ] Test to do +- [x] Test done +- [-] Test canceled +- [/] Test incomplete