Add initial calendar support

This commit is contained in:
Dheepak Krishnamurthy 2020-09-01 01:14:56 -06:00
parent eb8e5d376e
commit 2513642aa3
5 changed files with 317 additions and 42 deletions

16
Cargo.lock generated
View file

@ -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",

View file

@ -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"

View file

@ -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<impl Backend>) {
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<impl Backend>) {
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<impl Backend>) {
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(),

206
src/calendar.rs Normal file
View file

@ -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<Month> {
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(())
}
}

View file

@ -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<dyn Error>> {
// 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<dyn Error>> {
}
}
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<dyn Error>> {
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<dyn Error>> {
},
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<dyn Error>> {
}
_ => {}
},
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<dyn Error>> {
},
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<dyn Error>> {
}
_ => {}
},
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<dyn Error>> {
},
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<dyn Error>> {
}
_ => {}
},
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<dyn Error>> {
},
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<dyn Error>> {
}
_ => {}
},
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<dyn Error>> {
},
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<dyn Error>> {
break;
}
}
Ok(())
}