diff --git a/Cargo.lock b/Cargo.lock index beba0da..75340ae4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -199,6 +199,12 @@ dependencies = [ "syn", ] +[[package]] +name = "either" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd56b59865bce947ac5958779cfa508f6c3b9497cc762b7e24a12d11ccde2c4f" + [[package]] name = "failure" version = "0.1.8" @@ -259,6 +265,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "itertools" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "0.4.6" @@ -608,6 +623,7 @@ dependencies = [ "chrono", "clap", "crossterm", + "itertools", "rand", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 4fd0043..a4edf88 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ default = ["crossterm-backend"] crossterm-backend = ["tui/crossterm", "crossterm"] [dependencies] +itertools = "0.9" serde = { version = "1", features = ["derive"] } serde_json = "1" clap = "2.33" diff --git a/src/app.rs b/src/app.rs index 675a772..b6cf447 100644 --- a/src/app.rs +++ b/src/app.rs @@ -13,6 +13,7 @@ use task_hookrs::uda::UDAValue; use chrono::{Local, NaiveDateTime, TimeZone}; +use crate::calendar::Calendar; use std::sync::{Arc, Mutex}; use std::{sync::mpsc, thread, time::Duration}; use tui::{ @@ -111,14 +112,15 @@ fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { } pub enum AppMode { - Report, - Filter, - AddTask, - AnnotateTask, - LogTask, - ModifyTask, - HelpPopup, + TaskReport, + TaskFilter, + TaskAdd, + TaskAnnotate, + TaskLog, + TaskModify, + TaskHelpPopup, TaskError, + Calendar, } pub struct TTApp { @@ -153,7 +155,7 @@ impl TTApp { command: "".to_string(), modify: "".to_string(), error: "".to_string(), - mode: AppMode::Report, + mode: AppMode::TaskReport, colors: TColorConfig::default(), }; app.get_context(); @@ -186,6 +188,32 @@ impl TTApp { } pub fn draw(&mut self, f: &mut Frame) { + match self.mode { + AppMode::TaskReport + | AppMode::TaskFilter + | AppMode::TaskAdd + | AppMode::TaskAnnotate + | AppMode::TaskError + | AppMode::TaskHelpPopup + | AppMode::TaskLog + | AppMode::TaskModify => self.draw_task(f), + AppMode::Calendar => self.draw_calendar(f), + } + } + + pub fn draw_calendar(&mut self, f: &mut Frame) { + let rects = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(0)].as_ref()) + .split(f.size()); + let c = &Calendar::new(2020).to_string()[..]; + let p = Paragraph::new(Text::from(c)) + .alignment(Alignment::Left) + .block(Block::default().borders(Borders::ALL).title("Calendar")); + f.render_widget(p, rects[0]); + } + + pub fn draw_task(&mut self, f: &mut Frame) { let tasks_is_empty = self.tasks.lock().unwrap().is_empty(); let tasks_len = self.tasks.lock().unwrap().len(); while !tasks_is_empty && self.state.selected().unwrap_or_default() >= tasks_len { @@ -210,13 +238,13 @@ impl TTApp { .unwrap_or_default() }; match self.mode { - AppMode::Report => self.draw_command(f, rects[1], &self.filter[..], "Filter Tasks"), - AppMode::Filter => { + AppMode::TaskReport => self.draw_command(f, rects[1], &self.filter[..], "Filter Tasks"), + AppMode::TaskFilter => { f.render_widget(Clear, rects[1]); f.set_cursor(rects[1].x + self.cursor_location as u16 + 1, rects[1].y + 1); self.draw_command(f, rects[1], &self.filter[..], "Filter Tasks"); } - AppMode::ModifyTask => { + AppMode::TaskModify => { f.set_cursor(rects[1].x + self.cursor_location as u16 + 1, rects[1].y + 1); f.render_widget(Clear, rects[1]); self.draw_command( @@ -226,12 +254,12 @@ impl TTApp { format!("Modify Task {}", task_id).as_str(), ); } - AppMode::LogTask => { + AppMode::TaskLog => { f.set_cursor(rects[1].x + self.cursor_location as u16 + 1, rects[1].y + 1); f.render_widget(Clear, rects[1]); self.draw_command(f, rects[1], &self.command[..], "Log Task"); } - AppMode::AnnotateTask => { + AppMode::TaskAnnotate => { f.set_cursor(rects[1].x + self.cursor_location as u16 + 1, rects[1].y + 1); f.render_widget(Clear, rects[1]); self.draw_command( @@ -241,7 +269,7 @@ impl TTApp { format!("Annotate Task {}", task_id).as_str(), ); } - AppMode::AddTask => { + AppMode::TaskAdd => { f.set_cursor(rects[1].x + self.cursor_location as u16 + 1, rects[1].y + 1); f.render_widget(Clear, rects[1]); self.draw_command(f, rects[1], &self.command[..], "Add Task"); @@ -250,10 +278,13 @@ impl TTApp { f.render_widget(Clear, rects[1]); self.draw_command(f, rects[1], &self.error[..], "Error"); } - AppMode::HelpPopup => { + AppMode::TaskHelpPopup => { self.draw_command(f, rects[1], &self.filter[..], "Filter Tasks"); self.draw_help_popup(f, f.size()); } + _ => { + panic!("Reached unreachable code. Something went wrong"); + } } } @@ -546,12 +577,21 @@ impl TTApp { match attribute { "id" => task.id().unwrap_or_default().to_string(), "due" => match task.due() { - Some(v) => vague_format_date_time(Local::now().naive_utc(), NaiveDateTime::new(v.date(), v.time())), + Some(v) => vague_format_date_time( + Local::now().naive_utc(), + NaiveDateTime::new(v.date(), v.time()), + ), None => "".to_string(), }, - "entry" => vague_format_date_time(NaiveDateTime::new(task.entry().date(), task.entry().time()), Local::now().naive_utc()), + "entry" => vague_format_date_time( + NaiveDateTime::new(task.entry().date(), task.entry().time()), + Local::now().naive_utc(), + ), "start" => match task.start() { - Some(v) => vague_format_date_time(NaiveDateTime::new(v.date(), v.time()), Local::now().naive_utc()), + Some(v) => vague_format_date_time( + NaiveDateTime::new(v.date(), v.time()), + Local::now().naive_utc(), + ), None => "".to_string(), }, "description" => task.description().to_string(), diff --git a/src/calendar.rs b/src/calendar.rs new file mode 100644 index 0000000..5ffa51e --- /dev/null +++ b/src/calendar.rs @@ -0,0 +1,206 @@ +// Based on https://play.rust-lang.org/?gist=1057364daeee4cff472a&version=nightly +// See: https://old.reddit.com/r/rust/comments/37b6oo/the_calendar_example_challenge/crlmbsg/ + +use std::fmt; + +const COL_WIDTH: usize = 21; + +use Day::*; +use Month::*; + +#[derive(Clone, Copy, Debug)] +#[repr(u8)] +pub enum Day { + Sun, + Mon, + Tue, + Wed, + Thu, + Fri, + Sat, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum Month { + Jan, + Feb, + Mar, + Apr, + May, + Jun, + Jul, + Aug, + Sep, + Oct, + Nov, + Dec, +} +impl Month { + fn len(self) -> u8 { + match self { + Jan => 31, + Feb => 28, + Mar => 31, + Apr => 30, + May => 31, + Jun => 30, + Jul => 31, + Aug => 31, + Sep => 30, + Oct => 31, + Nov => 30, + Dec => 31, + } + } + fn leap_len(self, leap_year: bool) -> u8 { + match self { + Feb => { + if leap_year { + 29 + } else { + 28 + } + } + mon => mon.len(), + } + } + fn first_day(self, year: i64) -> Day { + let y = year - 1; + let jan_first = (1 + (5 * (y % 4)) + (4 * (y % 100)) + (6 * (y % 400))) % 7; + let mut len = 0; + for m in Jan { + if m == self { + break; + } + len += m.leap_len(is_leap_year(year)) as i64; + } + match (len + jan_first) % 7 { + 0 => Sun, + 1 => Mon, + 2 => Tue, + 3 => Wed, + 4 => Thu, + 5 => Fri, + _ => Sat, + } + } +} +impl Iterator for Month { + type Item = Month; + fn next(&mut self) -> Option { + let ret = Some(*self); + *self = match *self { + Jan => Feb, + Feb => Mar, + Mar => Apr, + Apr => May, + May => Jun, + Jun => Jul, + Jul => Aug, + Aug => Sep, + Sep => Oct, + Oct => Nov, + Nov => Dec, + Dec => Jan, + }; + ret + } +} +impl fmt::Display for Month { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let name = match *self { + Jan => "January", + Feb => "February", + Mar => "March", + Apr => "April", + May => "May", + Jun => "June", + Jul => "July", + Aug => "August", + Sep => "September", + Oct => "October", + Nov => "November", + Dec => "December", + }; + let padding = COL_WIDTH - name.len(); + write!(f, "{:1$}", "", padding / 2)?; + if padding % 2 != 0 { + f.write_str(" ")?; + } + f.write_str(name)?; + write!(f, "{:1$}", "", padding / 2) + } +} + +pub struct Calendar { + pub year: i64, +} + +impl Calendar { + pub fn new(year: i64) -> Self { + Self { year } + } +} + +fn is_leap_year(year: i64) -> bool { + (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 +} + +impl fmt::Display for Calendar { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let cols = f.width().unwrap_or(3); + let year = self.year; + let leap_year = is_leap_year(year); + let months = [Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec]; + let mut dates = [ + 0..Jan.len(), + 0..Feb.leap_len(leap_year), + 0..Mar.len(), + 0..Apr.len(), + 0..May.len(), + 0..Jun.len(), + 0..Jul.len(), + 0..Aug.len(), + 0..Sep.len(), + 0..Oct.len(), + 0..Nov.len(), + 0..Dec.len(), + ]; + let chunks = dates.chunks_mut(cols).zip(months.chunks(cols)); + for (days_chunk, months) in chunks { + for month in months { + write!(f, "{:>1$} ", month, COL_WIDTH)?; + } + f.write_str("\n")?; + for month in months { + write!(f, "{:>1$} ", " S M T W T F S", COL_WIDTH)?; + } + f.write_str("\n")?; + for (days, mon) in days_chunk.iter_mut().zip(months.iter()) { + let first_day = mon.first_day(year) as u8; + for _ in 0..(first_day) { + f.write_str(" ")?; + } + for _ in 0..(7 - first_day) { + write!(f, "{:>3}", days.next().unwrap() + 1)?; + } + f.write_str(" ")?; + } + f.write_str("\n")?; + while !days_chunk.iter().all(|r| r.start == r.end) { + for days in days_chunk.iter_mut() { + for _ in 0..7 { + match days.next() { + Some(s) => write!(f, "{:>3}", s + 1)?, + None => f.write_str(" ")?, + } + } + f.write_str(" ")?; + } + f.write_str("\n")?; + } + f.write_str("\n")?; + } + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs index 6021b08..3c67c70 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ #![allow(unused_variables)] mod app; +mod calendar; mod color; mod util; @@ -57,8 +58,11 @@ fn tui_main(_config: &str) -> Result<(), Box> { // Handle input match events.next()? { Event::Input(input) => match app.mode { - AppMode::Report => match input { + AppMode::TaskReport => match input { Key::Ctrl('c') | Key::Char('q') => app.should_quit = true, + Key::Char(']') => { + app.mode = AppMode::Calendar; + } Key::Char('r') => app.update(), Key::Down | Key::Char('j') => app.next(), Key::Up | Key::Char('k') => app.previous(), @@ -103,7 +107,7 @@ fn tui_main(_config: &str) -> Result<(), Box> { } } Key::Char('m') => { - app.mode = AppMode::ModifyTask; + app.mode = AppMode::TaskModify; match app.task_current() { Some(t) => app.modify = t.description().to_string(), None => app.modify = "".to_string(), @@ -111,35 +115,35 @@ fn tui_main(_config: &str) -> Result<(), Box> { app.cursor_location = app.modify.chars().count(); } Key::Char('l') => { - app.mode = AppMode::LogTask; + app.mode = AppMode::TaskLog; } Key::Char('a') => { - app.mode = AppMode::AddTask; + app.mode = AppMode::TaskAdd; app.cursor_location = app.command.chars().count(); } Key::Char('A') => { - app.mode = AppMode::AnnotateTask; + app.mode = AppMode::TaskAnnotate; app.cursor_location = app.command.chars().count(); } Key::Char('?') => { - app.mode = AppMode::HelpPopup; + app.mode = AppMode::TaskHelpPopup; } Key::Char('/') => { - app.mode = AppMode::Filter; + app.mode = AppMode::TaskFilter; app.cursor_location = app.filter.chars().count(); } _ => {} }, - AppMode::HelpPopup => match input { + AppMode::TaskHelpPopup => match input { Key::Esc => { - app.mode = AppMode::Report; + app.mode = AppMode::TaskReport; } _ => {} }, - AppMode::ModifyTask => match input { + AppMode::TaskModify => match input { Key::Char('\n') => match app.task_modify() { Ok(_) => { - app.mode = AppMode::Report; + app.mode = AppMode::TaskReport; app.update(); } Err(e) => { @@ -149,7 +153,7 @@ fn tui_main(_config: &str) -> Result<(), Box> { }, Key::Esc => { app.modify = "".to_string(); - app.mode = AppMode::Report; + app.mode = AppMode::TaskReport; } Key::Right => { if app.cursor_location < app.modify.chars().count() { @@ -179,10 +183,10 @@ fn tui_main(_config: &str) -> Result<(), Box> { } _ => {} }, - AppMode::LogTask => match input { + AppMode::TaskLog => match input { Key::Char('\n') => match app.task_log() { Ok(_) => { - app.mode = AppMode::Report; + app.mode = AppMode::TaskReport; app.update(); } Err(e) => { @@ -192,7 +196,7 @@ fn tui_main(_config: &str) -> Result<(), Box> { }, Key::Esc => { app.command = "".to_string(); - app.mode = AppMode::Report; + app.mode = AppMode::TaskReport; } Key::Right => { if app.cursor_location < app.command.chars().count() { @@ -222,10 +226,10 @@ fn tui_main(_config: &str) -> Result<(), Box> { } _ => {} }, - AppMode::AnnotateTask => match input { + AppMode::TaskAnnotate => match input { Key::Char('\n') => match app.task_annotate() { Ok(_) => { - app.mode = AppMode::Report; + app.mode = AppMode::TaskReport; app.update(); } Err(e) => { @@ -235,7 +239,7 @@ fn tui_main(_config: &str) -> Result<(), Box> { }, Key::Esc => { app.command = "".to_string(); - app.mode = AppMode::Report; + app.mode = AppMode::TaskReport; } Key::Right => { if app.cursor_location < app.command.chars().count() { @@ -265,10 +269,10 @@ fn tui_main(_config: &str) -> Result<(), Box> { } _ => {} }, - AppMode::AddTask => match input { + AppMode::TaskAdd => match input { Key::Char('\n') => match app.task_add() { Ok(_) => { - app.mode = AppMode::Report; + app.mode = AppMode::TaskReport; app.update(); } Err(e) => { @@ -278,7 +282,7 @@ fn tui_main(_config: &str) -> Result<(), Box> { }, Key::Esc => { app.command = "".to_string(); - app.mode = AppMode::Report; + app.mode = AppMode::TaskReport; } Key::Right => { if app.cursor_location < app.command.chars().count() { @@ -308,9 +312,9 @@ fn tui_main(_config: &str) -> Result<(), Box> { } _ => {} }, - AppMode::Filter => match input { + AppMode::TaskFilter => match input { Key::Char('\n') | Key::Esc => { - app.mode = AppMode::Report; + app.mode = AppMode::TaskReport; app.update(); } Key::Right => { @@ -343,9 +347,16 @@ fn tui_main(_config: &str) -> Result<(), Box> { }, AppMode::TaskError => match input { _ => { - app.mode = AppMode::Report; + app.mode = AppMode::TaskReport; } }, + AppMode::Calendar => match input { + Key::Char('[') => { + app.mode = AppMode::TaskReport; + } + Key::Ctrl('c') | Key::Char('q') => app.should_quit = true, + _ => {} + }, }, Event::Tick => app.update(), } @@ -355,5 +366,6 @@ fn tui_main(_config: &str) -> Result<(), Box> { break; } } + Ok(()) }