From cbcf36f8c413b2ef7d15e94730efb7026be0390d Mon Sep 17 00:00:00 2001 From: Vincent Breitmoser Date: Sat, 12 Jan 2019 13:26:10 +0100 Subject: move actions into actions module --- src/actions/agenda.rs | 226 ++++++++++++++++++++++++++++++++++++++++ src/actions/cal.rs | 160 ++++++++++++++++++++++++++++ src/actions/copy.rs | 37 +++++++ src/actions/edit.rs | 28 +++++ src/actions/index/bucketable.rs | 149 ++++++++++++++++++++++++++ src/actions/index/index.rs | 152 +++++++++++++++++++++++++++ src/actions/index/indextime.rs | 38 +++++++ src/actions/index/mod.rs | 5 + src/actions/list.rs | 23 ++++ src/actions/mod.rs | 13 +++ src/actions/modify.rs | 18 ++++ src/actions/new.rs | 43 ++++++++ src/actions/prettyprint.rs | 36 +++++++ src/actions/select.rs | 76 ++++++++++++++ src/actions/seq.rs | 57 ++++++++++ src/actions/show.rs | 17 +++ src/actions/unroll.rs | 15 +++ src/agenda.rs | 226 ---------------------------------------- src/bin/khaleesi.rs | 14 +-- src/bucketable.rs | 149 -------------------------- src/cal.rs | 160 ---------------------------- src/copy.rs | 37 ------- src/edit.rs | 28 ----- src/index.rs | 152 --------------------------- src/indextime.rs | 38 ------- src/lib.rs | 16 +-- src/list.rs | 23 ---- src/modify.rs | 18 ---- src/new.rs | 43 -------- src/prettyprint.rs | 36 ------- src/select.rs | 76 -------------- src/seq.rs | 57 ---------- src/show.rs | 17 --- src/unroll.rs | 15 --- 34 files changed, 1095 insertions(+), 1103 deletions(-) create mode 100644 src/actions/agenda.rs create mode 100644 src/actions/cal.rs create mode 100644 src/actions/copy.rs create mode 100644 src/actions/edit.rs create mode 100644 src/actions/index/bucketable.rs create mode 100644 src/actions/index/index.rs create mode 100644 src/actions/index/indextime.rs create mode 100644 src/actions/index/mod.rs create mode 100644 src/actions/list.rs create mode 100644 src/actions/mod.rs create mode 100644 src/actions/modify.rs create mode 100644 src/actions/new.rs create mode 100644 src/actions/prettyprint.rs create mode 100644 src/actions/select.rs create mode 100644 src/actions/seq.rs create mode 100644 src/actions/show.rs create mode 100644 src/actions/unroll.rs delete mode 100644 src/agenda.rs delete mode 100644 src/bucketable.rs delete mode 100644 src/cal.rs delete mode 100644 src/copy.rs delete mode 100644 src/edit.rs delete mode 100644 src/index.rs delete mode 100644 src/indextime.rs delete mode 100644 src/list.rs delete mode 100644 src/modify.rs delete mode 100644 src/new.rs delete mode 100644 src/prettyprint.rs delete mode 100644 src/select.rs delete mode 100644 src/seq.rs delete mode 100644 src/show.rs delete mode 100644 src/unroll.rs diff --git a/src/actions/agenda.rs b/src/actions/agenda.rs new file mode 100644 index 0000000..5b98fb1 --- /dev/null +++ b/src/actions/agenda.rs @@ -0,0 +1,226 @@ +use chrono::{Datelike, TimeZone, Local, Date}; +use itertools::Itertools; +use yansi::{Style}; + +use icalwrap::*; +use utils::fileutil as utils; +use config::{Config,CalendarConfig}; + +pub fn show_events(config: &Config, lines: &mut Iterator) { + let cals = utils::read_calendars_from_files(lines).unwrap(); + + let mut not_over_yet: Vec<(usize, &IcalVCalendar, IcalVEvent, Option<&CalendarConfig>)> = Vec::new(); + let mut cals_iter = cals.iter() + .enumerate() + .map(|(i, cal)| (i, cal, cal.get_principal_event(), config.get_config_for_calendar(&cal))) + .peekable(); + + let start_day = match cals_iter.peek() { + Some((_, _, event, _)) => { + event + .get_dtstart() + .unwrap_or_else(|| Local.timestamp(0, 0)) + .date() + } + None => return, + }; + + let mut cur_day = start_day.pred(); + let mut last_printed_day = start_day.pred(); + while cals_iter.peek().is_some() || !not_over_yet.is_empty() { + cur_day = cur_day.succ(); + + maybe_print_date_line_header(&config, cur_day, start_day, &mut last_printed_day); + + not_over_yet.retain( |(index, _, event, cal_config)| { + maybe_print_date_line(&config, cur_day, start_day, &mut last_printed_day); + print_event_line(*cal_config, *index, &event, cur_day); + event.continues_after(cur_day) + }); + + let relevant_events = cals_iter.peeking_take_while(|(_,_,event,_)| event.starts_on(cur_day)); + for (i, cal, event, cal_config) in relevant_events { + maybe_print_date_line(&config, cur_day, start_day, &mut last_printed_day); + print_event_line(cal_config, i, &event, cur_day); + if event.continues_after(cur_day) { + not_over_yet.push((i, cal, event, cal_config)); + } + } + } +} + +fn maybe_print_week_separator(config: &Config, date: Date, start_date: Date, last_printed_date: Date) { + if !config.agenda.print_week_separator { + return; + } + if date != start_date && last_printed_date.iso_week() < date.iso_week() { + println!(); + } +} + +fn maybe_print_date_line_header(config: &Config, date: Date, start_date: Date, last_printed_date: &mut Date) { + if !config.agenda.print_empty_days { + return; + } + maybe_print_date_line(config, date, start_date, last_printed_date); +} + +fn maybe_print_date_line(config: &Config, date: Date, start_date: Date, last_printed_date: &mut Date) { + if date <= *last_printed_date { + return; + } + maybe_print_week_separator(config, date, start_date, *last_printed_date); + print_date_line(date); + *last_printed_date = date; +} + +fn print_date_line(date: Date) { + let style_heading = Style::new().bold(); + println!("{}, {}", style_heading.paint(date.format("%Y-%m-%d")), date.format("%A")); +} + +fn print_event_line(config: Option<&CalendarConfig>, index: usize, event: &IcalVEvent, date: Date) { + match event_line(config, &event, date) { + Ok(line) => println!("{:4} {}", index, line), + Err(error) => warn!("{} in {}", error, event.get_uid()) + } +} + +pub fn event_line(config: Option<&CalendarConfig>, event: &IcalVEvent, cur_day: Date) -> Result { + if !event.relevant_on(cur_day) { + return Err(format!("event is not relevant for {:?}", cur_day)); + } + + if event.is_allday() { + let mut summary = event.get_summary().ok_or("Invalid SUMMARY")?; + if let Some(config) = config { + let calendar_style = config.get_style_for_calendar(); + summary = calendar_style.paint(summary).to_string(); + } + Ok(format!(" {}", summary)) + } else { + let mut time_sep = " "; + let dtstart = event.get_dtstart().ok_or("Invalid DTSTART")?; + let start_string = if dtstart.date() != cur_day { + "".to_string() + } else { + time_sep = "-"; + format!("{}", dtstart.format("%H:%M")) + }; + + let dtend = event.get_dtend().ok_or("Invalid DTEND")?; + let end_string = if dtend.date() != cur_day { + "".to_string() + } else { + time_sep = "-"; + format!("{}", dtend.format("%H:%M")) + }; + + let mut summary = event.get_summary().ok_or("Invalid SUMMARY")?; + + if let Some(config) = config { + let calendar_style = config.get_style_for_calendar(); + summary = calendar_style.paint(summary).to_string(); + } + + Ok(format!("{:5}{}{:5} {}", start_string, time_sep, end_string, summary)) + } +} + +impl IcalVEvent { + fn starts_on(&self, date: Date) -> bool { + self.get_dtstart().unwrap().date() == date + } + + fn relevant_on(&self, date: Date) -> bool { + self.get_dtstart().map(|dtstart| dtstart.date() <= date).unwrap_or(false) && + self.get_last_relevant_date().map(|enddate| enddate >= date).unwrap_or(false) + } + + fn continues_after(&self, date: Date) -> bool { + self.get_last_relevant_date() + .map(|enddate| enddate > date) + .unwrap_or(false) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use testdata; + use chrono::{Local, TimeZone}; + + #[test] + fn test_starts_on() { + let cal = IcalVCalendar::from_str(testdata::TEST_EVENT_MULTIDAY, None).unwrap(); + let event = cal.get_principal_event(); + + let first_day = Local.ymd(2007, 6, 28); + assert!(event.starts_on(first_day)); + + let last_day = Local.ymd(2007, 7, 7); + assert!(!event.starts_on(last_day)); + } + + #[test] + fn test_continues_after_allday() { + let cal = IcalVCalendar::from_str(testdata::TEST_EVENT_MULTIDAY_ALLDAY, None).unwrap(); + let event = cal.get_principal_event(); + let first_day = Local.ymd(2007, 6, 28); + assert!(event.continues_after(first_day)); + let last_day = Local.ymd(2007, 7, 8); + assert!(!event.continues_after(last_day)); + } + + #[test] + fn test_continues_after_simple() { + let cal = IcalVCalendar::from_str(testdata::TEST_EVENT_ONE_MEETING, None).unwrap(); + let event = cal.get_principal_event(); + let date = Local.ymd(1997, 3, 24); + assert!(!event.continues_after(date)); + } + + #[test] + fn test_event_line_negative() { + let cal = IcalVCalendar::from_str(testdata::TEST_EVENT_ONE_MEETING, None).unwrap(); + let event = cal.get_principal_event(); + let date = Local.ymd(1998, 1, 1); + let event_line = event_line(None, &event, date); + assert!(event_line.is_err()) + } + + #[test] + fn test_event_line_simple() { + testdata::setup(); + let cal = IcalVCalendar::from_str(testdata::TEST_EVENT_ONE_MEETING, None).unwrap(); + let event = cal.get_principal_event(); + let date = Local.ymd(1997, 3, 24); + let event_line = event_line(None, &event, date).unwrap(); + assert_eq!("12:30-21:00 Calendaring Interoperability Planning Meeting".to_string(), event_line) + } + + #[test] + fn test_event_line_multiday() { + testdata::setup(); + let cal = IcalVCalendar::from_str(testdata::TEST_EVENT_MULTIDAY, None).unwrap(); + let event = cal.get_principal_event(); + let begin = Local.ymd(2007, 6, 28); + let middle = Local.ymd(2007, 6, 30); + let end = Local.ymd(2007, 7, 9); + let event_line_begin = event_line(None, &event, begin).unwrap(); + let event_line_middle = event_line(None, &event, middle).unwrap(); + let event_line_end = event_line(None, &event, end).unwrap(); + assert_eq!("13:29- Festival International de Jazz de Montreal".to_string(), event_line_begin); + assert_eq!(" Festival International de Jazz de Montreal".to_string(), event_line_middle); + assert_eq!(" -07:29 Festival International de Jazz de Montreal".to_string(), event_line_end); + } + + #[test] + fn test_event_line_multiday_allday() { + let cal = IcalVCalendar::from_str(testdata::TEST_EVENT_MULTIDAY_ALLDAY, None).unwrap(); + let event = cal.get_principal_event(); + let date = Local.ymd(2007, 6, 28); + let event_line = event_line(None, &event, date).unwrap(); + assert_eq!(" Festival International de Jazz de Montreal".to_string(), event_line) + } +} diff --git a/src/actions/cal.rs b/src/actions/cal.rs new file mode 100644 index 0000000..a23be91 --- /dev/null +++ b/src/actions/cal.rs @@ -0,0 +1,160 @@ +use chrono::Duration; +use chrono::prelude::*; +use yansi::{Style,Color}; + +use utils::misc; + +struct Cell { + date: NaiveDate, + content: (String,String) +} + +pub fn printcal() { + let now = Local::today(); + let a = cal_month(now); + let b = cal_month(now.with_month(now.month() + 1).unwrap()); + let c = cal_month(now.with_month(now.month() + 2).unwrap()); + + let joined = misc::joinlines(&a, &b); + let joined = misc::joinlines(&joined, &c); + println!("{}", joined); +} + +pub fn dbg() { + let begin = Local::today().naive_local(); + let end = begin + Duration::days(5); + let cells = get_cells(begin, end); + let cells = expand_cells_to_week(cells); + + let render = render_cells(&cells); + print!("{}", render); +} + +fn render_cells(cells: &[Cell]) -> String { + let mut result = String::with_capacity(50); + + let now = cells[0].date; + + result.push_str(&format!("{:>28} {:<8}\n", + now.format("%B").to_string(), + now.format("%Y").to_string() + )); + let weekdays = &[ "Mo", "Tu", "We", "Th", "Fr", "Sa", "Su" ].iter().map(|x| format!("{:8}", x)).collect::(); + result.push_str(weekdays); + result.push_str("\n"); + + let flow = render_flow(7, 8, cells); + result.push_str(&flow); + + result +} + +fn render_flow(cells_per_line: usize, cell_width: usize, cells: &[Cell]) -> String { + let mut result = String::with_capacity(50); + + let style = Style::new().bg(Color::Fixed(236)); + + let it = cells.iter(); + let mut n = 0; + while n < (cells.len() / cells_per_line) { + let line = it.clone().skip(n * cells_per_line).take(cells_per_line); + for cell in line.clone() { + let cellstr = &format!("{:width$}", &cell.content.0, width = cell_width); + let cellstr = &format!("{}", style.paint(cellstr)); + result.push_str(cellstr); + } + result.push_str("\n"); + for cell in line { + let cellstr = &format!("{:width$}", &cell.content.1, width = cell_width); + let cellstr = &format!("{}", style.paint(cellstr)); + result.push_str(cellstr); + } + let emptyline = &format!("{:width$}", "", width = cell_width * cells_per_line); + result.push_str("\n"); + result.push_str(&format!("{}\n", style.paint(emptyline))); + n += 1; + } + + result +} + +fn get_cells(date_begin: NaiveDate, date_end: NaiveDate) -> Vec { + let mut result = vec!(); + let mut date = date_begin; + while date < date_end { + let cell = cell_whatever(date); + result.push(cell); + + date += Duration::days(1); + } + result +} + +fn cell_whatever(date: NaiveDate) -> Cell { + let fst = date.format("%d").to_string(); + let snd = String::from(""); + Cell{date, content: (fst, snd)} +} + +fn cell_empty(date: NaiveDate) -> Cell { + let fst = date.format("%d").to_string(); + let snd = String::from(""); + Cell{date, content: (fst, snd)} +} + +fn expand_cells_to_week(cells: Vec) -> Vec { + let mut result = vec!(); + + let mut day = NaiveDate::from_isoywd(cells[0].date.year(), cells[0].date.iso_week().week(), Weekday::Mon); + while day < cells[0].date { + let cell = cell_empty(day); + result.push(cell); + + day += Duration::days(1); + } + + let mut day = cells[cells.len() - 1].date; + + for cell in cells { + result.push(cell); + } + + let last_date = NaiveDate::from_isoywd(day.year(), day.iso_week().week(), Weekday::Sun); + day += Duration::days(1); + while day <= last_date { + let cell = cell_empty(day); + result.push(cell); + + day += Duration::days(1); + } + + result +} + +pub fn cal_month(now: Date) -> String { + let mut result = String::with_capacity(50); + + result.push_str(&format!("{:>11} {:<8}\n", + now.format("%B").to_string(), + now.format("%Y").to_string() + )); + result.push_str("Su Mo Tu We Th Fr Sa\n"); + + let this_month = now.month(); + let mut current_day = Local.ymd(now.year(), now.month(), 1); + + let one_day = Duration::days(1); + for _ in 0..current_day.weekday().num_days_from_sunday() { + result.push_str(" "); + } + while current_day.month() == this_month { + result.push_str(&format!("{:>2} ", current_day.day())); + if current_day.weekday() == Weekday::Sat { + result.push_str("\n"); + } + current_day = current_day + one_day; + } + result.push_str("\n"); + + result +} diff --git a/src/actions/copy.rs b/src/actions/copy.rs new file mode 100644 index 0000000..5ed7aef --- /dev/null +++ b/src/actions/copy.rs @@ -0,0 +1,37 @@ +use utils::fileutil; +use utils::misc; + +pub fn do_copy(lines: &mut Iterator, _args: &[String]) { + + let lines = lines.collect::>(); + if lines.len() > 1 { + println!("copy only one event!"); + return; + }; + + let cal = match fileutil::read_khaleesi_line(&lines[0]) { + Ok(calendar) => calendar, + Err(error) => { + error!("{}", error); + return + }, + }; + let new_cal = match cal.with_uid(&misc::make_new_uid()) { + Ok(new_cal) => new_cal, + Err(error) => { + error!("{}", error); + return + }, + }; + let new_cal = new_cal.with_dtstamp_now(); + + match fileutil::write_cal(&new_cal) { + Ok(_) => info!("Successfully wrote file: {}", new_cal.get_path().unwrap().display()), + Err(error) => { + error!("{}", error); + return + }, + } + + println!("{}", new_cal.get_principal_event().get_khaleesi_line().unwrap()); +} diff --git a/src/actions/edit.rs b/src/actions/edit.rs new file mode 100644 index 0000000..e7f0afb --- /dev/null +++ b/src/actions/edit.rs @@ -0,0 +1,28 @@ +use std::env; +use std::fs; +use std::process::Command; + +use utils::dateutil; + +pub fn do_edit(filenames: &mut Iterator, _args: &[String]) { + + let mut paths: Vec = filenames.map( |line| { + let parts: Vec<&str> = line.splitn(2, ' ').collect(); + match dateutil::datetime_from_timestamp(parts[0]) { + Some(_) => parts[1].to_string(), + None => parts[0].to_string(), + } + }).collect(); + paths.sort_unstable(); + paths.dedup(); + + let editor = env::var("EDITOR").unwrap_or_else(|_| "vim".to_string()); + + if let Err(error) = Command::new(&editor) + .args(paths) + .stdin(fs::File::open("/dev/tty").unwrap()) + .status() { + error!("{} command failed to start, error: {}", editor, error); + return + }; +} diff --git a/src/actions/index/bucketable.rs b/src/actions/index/bucketable.rs new file mode 100644 index 0000000..e0f743a --- /dev/null +++ b/src/actions/index/bucketable.rs @@ -0,0 +1,149 @@ +use chrono::{Local, Date, Datelike, Duration}; +use std::collections::HashMap; +use std::{hash, cmp}; + +use icalwrap::{IcalVEvent, IcalVCalendar}; +use utils::misc; + +pub trait Bucketable { + fn get_buckets(&self) -> Result>, String>; + + fn buckets_for_interval(mut start: Date, end: Date) -> Vec { + let mut buckets = Vec::new(); + + while start.iso_week() <= end.iso_week() { + let bucket = misc::get_bucket_for_date(start); + buckets.push(bucket); + start = start.checked_add_signed(Duration::days(7)).unwrap(); + } + buckets + } +} + +impl Bucketable for IcalVEvent { + fn get_buckets(&self) -> Result>, String> { + let mut result: HashMap> = HashMap::new(); + + let start_date = self.get_dtstart_date().ok_or_else(|| format!("Invalid DTSTART in {}", self.get_uid()))?; + let mut end_date = self.get_dtend_date().unwrap_or(start_date); + + // end-dtimes are non-inclusive + // so in case of date-only events, the last day of the event is dtend-1 + if self.is_allday() { + end_date = end_date.pred(); + } + + let buckets = Self::buckets_for_interval(start_date, end_date); + for bucketid in buckets { + result + .entry(bucketid) + .and_modify(|items| items.push(self.get_khaleesi_line().unwrap())) + .or_insert_with(|| vec!(self.get_khaleesi_line().unwrap())); + } + + if self.has_recur() { + for instance in self.get_recur_instances() { + let recur_buckets = instance.get_buckets()?; + result.merge(recur_buckets) + } + } + + for vec in result.values_mut() { + vec.dedup() + } + Ok(result) + } +} + +impl Bucketable for IcalVCalendar { + fn get_buckets(&self) -> Result>, String> { + let mut result: HashMap> = HashMap::new(); + for event in self.events_iter() { + let recur_buckets = event.get_buckets()?; + result.merge(recur_buckets); + } + Ok(result) + } +} + +pub trait Merge +where K: cmp::Eq + hash::Hash +{ + fn merge(&mut self, other: HashMap>); +} + +impl Merge for HashMap, S> +where K: cmp::Eq + hash::Hash, + S: std::hash::BuildHasher +{ + fn merge(&mut self, other: HashMap>) { + for (key, mut lines) in other.into_iter() { + self + .entry(key) + .and_modify(|items| items.append(&mut lines)) + .or_insert(lines); + } + } +} + +#[test] +fn merge_test() { + let mut map_a: HashMap<&str, Vec> = HashMap::new(); + let mut map_b: HashMap<&str, Vec> = HashMap::new(); + + let key = "key"; + map_a.insert(&key, vec!["a".to_string(), "b".to_string()]); + map_b.insert(&key, vec!["c".to_string(), "d".to_string()]); + + map_a.merge(map_b); + assert_eq!(map_a.get(&key).unwrap(), &vec!["a".to_string(), "b".to_string(), "c".to_string(), "d".to_string()]); +} + +#[test] +fn buckets_multi_day_allday() { + use testdata; + use std::path::PathBuf; + + let path = PathBuf::from("test/path"); + let cal = IcalVCalendar::from_str(testdata::TEST_EVENT_MULTIDAY_ALLDAY, Some(&path)).unwrap(); + + let event_buckets = cal.get_principal_event().get_buckets().unwrap(); + + assert_eq!(2, event_buckets.len()); + + let mut bucket_names = event_buckets.keys().collect::>(); + bucket_names.sort_unstable(); + assert_eq!(vec!("2007-W26", "2007-W27"), bucket_names); + + let cal_buckets = cal.get_buckets().unwrap(); + assert_eq!(event_buckets, cal_buckets); +} + +#[test] +fn buckets_single_event() { + use testdata; + use std::path::PathBuf; + + let path = PathBuf::from("test/path"); + let cal = IcalVCalendar::from_str(testdata::TEST_EVENT_ONE_MEETING, Some(&path)).unwrap(); + + let comp_buckets = cal.get_buckets().unwrap(); + assert_eq!(vec!("1997-W13"), comp_buckets.keys().collect::>()); +} + +#[test] +fn buckets_simple_recurring_event() { + use testdata; + use std::path::PathBuf; + + let path = PathBuf::from("test/path"); + let cal = IcalVCalendar::from_str(testdata::TEST_EVENT_RECUR, Some(&path)).unwrap(); + + let event = cal.get_principal_event(); + let event_buckets = event.get_buckets().unwrap(); + let cal_buckets = cal.get_buckets().unwrap(); + assert_eq!(event_buckets, cal_buckets); + let mut cal_bucket_names = cal_buckets.keys().collect::>(); + cal_bucket_names.sort_unstable(); + assert_eq!(vec!("2018-W41", "2018-W42", "2018-W43", "2018-W44", "2018-W45", "2018-W46", "2018-W47", "2018-W48", "2018-W49", "2018-W50"), cal_bucket_names); +} diff --git a/src/actions/index/index.rs b/src/actions/index/index.rs new file mode 100644 index 0000000..96acf8a --- /dev/null +++ b/src/actions/index/index.rs @@ -0,0 +1,152 @@ +use chrono::prelude::*; +use icalwrap::*; +use std::collections::HashMap; +use std::fs; +use std::path::{Path,PathBuf}; +use std::time::SystemTime; +use walkdir::DirEntry; + +use defaults::*; +use super::indextime; +use utils::fileutil; +use utils::lock; +use utils::misc; + + +fn add_buckets_for_calendar(buckets: &mut HashMap>, cal: &IcalVCalendar) { + use super::bucketable::Bucketable; + use super::bucketable::Merge; + + match cal.get_buckets() { + Ok(cal_buckets) => buckets.merge(cal_buckets), + Err(error) => { + warn!("{}", error) + } + } +} + +pub fn index_dir(dir: &Path, reindex: bool) { + use std::time::Instant; + + let lock = lock::lock_file_exclusive(&get_indexlockfile()); + if lock.is_err() { + error!("Failed to obtain index lock!"); + return; + } + + info!("Recursively indexing '.ics' files in directory: {}", dir.to_string_lossy()); + if !dir.exists() { + error!("Directory doesn't exist: {}", dir.to_string_lossy()); + return; + } + + let now = Instant::now(); + let start_time = Utc::now(); + + let last_index_time = if reindex { + debug!("Forced reindex, indexing all files"); + None + } else { + let last_index_time = indextime::get_index_time(); + match last_index_time { + Some(time) => debug!("Previously indexed {}, indexing newer files only", time.with_timezone(&Local)), + None => debug!("No previous index time, indexing all files"), + } + last_index_time + }; + + let modified_since = last_index_time.map(|time| time.timestamp()).unwrap_or(0); + let ics_files = get_ics_files(dir, modified_since); + + let buckets = read_buckets(ics_files); + + let indexdir = get_indexdir(); + let clear_index_dir = last_index_time.is_none(); + if let Err(error) = prepare_index_dir(&indexdir, clear_index_dir) { + error!("{}", error); + return; + } + + write_index(&indexdir, &buckets); + info!("Index written in {}ms", misc::format_duration(&now.elapsed())); + + indextime::write_index_time(&start_time); +} + +pub fn get_ics_files(dir: &Path, modified_since: i64) -> impl Iterator { + use walkdir::WalkDir; + + WalkDir::new(dir).into_iter() + .filter_entry(move |entry| accept_entry(entry, modified_since)) + .filter_map(|e| e.ok()) + .filter(|e| e.file_type().is_file()) + .filter(|e| e.path().extension().map_or(false, |extension| extension == "ics")) + .map(|entry| entry.into_path()) +} + +fn accept_entry(dir_entry: &DirEntry, modified_since: i64) -> bool { + if dir_entry.path().is_dir() { + return true; + } + dir_entry.metadata() + .map_err(|err| err.into()) // transform to io::Error + .and_then(|metadata| metadata.modified()) + .map(|modified| modified.duration_since(SystemTime::UNIX_EPOCH).unwrap()) + .map(|modified| modified.as_secs() as i64) + .map(|modified| modified > modified_since) + .unwrap_or(false) +} + +fn read_buckets(ics_files: impl Iterator) -> HashMap> { + let mut buckets: HashMap> = HashMap::new(); + + let mut total_files = 0; + for file in ics_files { + debug!("Indexing file: {:?}", file); + match fileutil::read_file_to_string(&file) { + Ok(content) => { + total_files += 1; + match IcalVCalendar::from_str(&content, Some(&file)) { + Ok(mut cal) => add_buckets_for_calendar(&mut buckets, &cal), + Err(error) => error!("{:?}: {}", file, error) + } + } + Err(error) => error!("{}", error), + } + } + + info!("Loaded {} files into {} buckets", total_files, buckets.len()); + buckets +} + +fn write_index(index_dir: &Path, buckets: &HashMap>) { + for (key, val) in buckets.iter() { + let bucketfile = bucket_file(index_dir, key); + trace!("Writing bucket: {}", key); + let content = &[&val.join("\n"), "\n"].concat(); + if let Err(error) = fileutil::append_file(&bucketfile, content) { + error!("{}", error); + return; + } + } +} + +fn bucket_file(index_dir: &Path, key: &str) -> PathBuf { + let mut result = PathBuf::from(index_dir); + result.push(key); + result +} + +fn prepare_index_dir(indexdir: &Path, clear_index_dir: bool) -> Result<(), std::io::Error> { + if indexdir.exists() && clear_index_dir { + info!("Clearing index directory: {}", indexdir.to_string_lossy()); + fs::remove_dir_all(&indexdir)? + } + + if !indexdir.exists() { + info!("Creating index directory: {}", indexdir.to_string_lossy()); + fs::create_dir(&indexdir)?; + } + + Ok(()) +} diff --git a/src/actions/index/indextime.rs b/src/actions/index/indextime.rs new file mode 100644 index 0000000..b078b2f --- /dev/null +++ b/src/actions/index/indextime.rs @@ -0,0 +1,38 @@ +use std::fs; +use std::io::{Read,Write}; +use chrono::prelude::*; + +use defaults::*; + +pub fn write_index_time(index_time: &DateTime) { + let mut timefile = fs::File::create(get_indextimefile()).unwrap(); + timefile.write_all(format!("{}\n", index_time.timestamp()).as_bytes()).unwrap(); +} + +pub fn get_index_time() -> Option> { + let mut timefile = fs::File::open(get_indextimefile()).ok()?; + let mut timestamp_str = String::new(); + timefile.read_to_string(&mut timestamp_str).ok()?; + let timestamp = timestamp_str.trim().parse::().ok()?; + Some(Utc.timestamp(timestamp, 0)) +} + +#[cfg(test)] +mod tests { + use super::*; + + use testutils; + use assert_fs::prelude::*; + + #[test] + fn test_write_read() { + let testdir = testutils::prepare_testdir("testdir"); + + let timestamp = Utc.ymd(1990,01,01).and_hms(1, 1, 0); + write_index_time(×tamp); + testdir.child(".khaleesi/index-time").assert("631155660\n"); + + let indextime = get_index_time(); + assert_eq!(Some(timestamp), indextime); + } +} diff --git a/src/actions/index/mod.rs b/src/actions/index/mod.rs new file mode 100644 index 0000000..7081c0b --- /dev/null +++ b/src/actions/index/mod.rs @@ -0,0 +1,5 @@ +pub mod index; +mod indextime; +mod bucketable; + +pub use self::index::index_dir; diff --git a/src/actions/list.rs b/src/actions/list.rs new file mode 100644 index 0000000..01289f6 --- /dev/null +++ b/src/actions/list.rs @@ -0,0 +1,23 @@ +use selectors::SelectFilters; +use utils::fileutil as utils; + +pub fn list_by_args(filenames: &mut Iterator, args: &[String]) { + let filters = match SelectFilters::parse_from_args_with_range(args) { + Err(error) => { println!("{}", error); return; }, + Ok(parsed_filters) => parsed_filters, + }; + + let cals = utils::read_calendars_from_files(filenames).unwrap(); + + let events = cals.into_iter() + .map(|cal| cal.get_principal_event()) + .enumerate() + .filter(|(index, event)| filters.is_selected_index(*index, event)); + + for (_, event) in events { + if let Some(line) = event.get_khaleesi_line() { + println!("{}", line); + } + } +} + diff --git a/src/actions/mod.rs b/src/actions/mod.rs new file mode 100644 index 0000000..20d8149 --- /dev/null +++ b/src/actions/mod.rs @@ -0,0 +1,13 @@ +pub mod agenda; +pub mod cal; +pub mod copy; +pub mod edit; +pub mod index; +pub mod list; +pub mod modify; +pub mod new; +pub mod prettyprint; +pub mod select; +pub mod seq; +pub mod show; +pub mod unroll; diff --git a/src/actions/modify.rs b/src/actions/modify.rs new file mode 100644 index 0000000..449d95a --- /dev/null +++ b/src/actions/modify.rs @@ -0,0 +1,18 @@ +use utils::fileutil as utils; + +pub fn do_modify(lines: &mut Iterator, args: &[String]) { + info!("do_modify"); + + if args[0] == "removeprop" && args[1] == "xlicerror" { + let cals = utils::read_calendars_from_files(lines).unwrap(); + let output: Vec = cals.into_iter() + .map(|cal| cal.with_remove_property("X-LIC-ERROR") ) + .filter(|cal| cal.1 > 0) + .map(|cal| cal.0.to_string()) + .collect(); + println!("{}", output.join("\n")); + } else { + error!("not supported") + } + +} diff --git a/src/actions/new.rs b/src/actions/new.rs new file mode 100644 index 0000000..b834015 --- /dev/null +++ b/src/actions/new.rs @@ -0,0 +1,43 @@ +use utils::fileutil; +use utils::misc; +use icalwrap::IcalVCalendar; +use defaults; + +pub fn do_new(_lines: &mut Iterator, _args: &[String]) { + + let uid = misc::make_new_uid(); + let path = defaults::get_datafile(&(uid.clone() + ".ics")); + + let new_cal = match IcalVCalendar::from_str(TEMPLATE_EVENT, Some(&path)).unwrap().with_uid(&uid) { + Ok(new_cal) => new_cal, + Err(error) => { + error!("{}", error); + return + }, + }; + let new_cal = new_cal.with_dtstamp_now(); + match fileutil::write_cal(&new_cal) { + Ok(_) => info!("Successfully wrote file: {}", new_cal.get_path().unwrap().display()), + Err(error) => { + error!("{}", error); + return + }, + } + + println!("{}", new_cal.get_principal_event().get_khaleesi_line().unwrap()); +} + +static TEMPLATE_EVENT: &str = indoc!(" + BEGIN:VCALENDAR + VERSION:2.0 + PRODID:-//khaleesi //EN + BEGIN:VEVENT + SUMMARY:<> + LOCATION:<> + DTSTART;VALUE=DATE-TIME:20181026T133000 + DTEND;VALUE=DATE-TIME:20181026T160000 + DTSTAMP;VALUE=DATE-TIME:20181022T145405Z + UID:foo + END:VEVENT + END:VCALENDAR +"); diff --git a/src/actions/prettyprint.rs b/src/actions/prettyprint.rs new file mode 100644 index 0000000..0baef7d --- /dev/null +++ b/src/actions/prettyprint.rs @@ -0,0 +1,36 @@ +use icalwrap::{IcalComponent,IcalProperty}; +use utils::fileutil; + +pub fn prettyprint(lines: &mut Iterator) { + let cals = fileutil::read_calendars_from_files(lines).unwrap(); + for cal in cals { + let event = cal.get_principal_event(); + prettyprint_comp(&event, cal.get_path_as_string()); + } +} + +pub fn prettyprint_comp(cal: &IcalComponent, path: Option) { + let properties = cal.get_properties_all(); + if let Some(path) = path { + debug!("path: {}", path); + } + debug!("property count: {}", properties.len()); + for property in properties { + prettyprint_prop(&property); + } + println!(); +} + +fn prettyprint_prop(property: &IcalProperty) { + let name = property.get_name(); + let value = property.get_value(); + match name.as_str() { + "DTSTART" => { + let date = property.get_value_as_date(); + println!("start: {}", date.unwrap()); + }, + "DESCRIPTION" => println!("description: {}", value), + _ => println!("{} - {}", name, value), + } +} + diff --git a/src/actions/select.rs b/src/actions/select.rs new file mode 100644 index 0000000..87bca2b --- /dev/null +++ b/src/actions/select.rs @@ -0,0 +1,76 @@ +use std::path::PathBuf; + +use defaults; +use selectors::{SelectFilters,daterange::SelectFilterFrom,daterange::SelectFilterTo}; +use utils::fileutil as utils; + +impl SelectFilters { + fn predicate_path_skip_while(&self) -> impl Fn(&PathBuf) -> bool + '_ { + move |path| { + let bucketname = match path.file_name() { + Some(path_os_str) => path_os_str.to_string_lossy(), + None => panic!("{:?} not a file", path), + }; + self.from.is_bucket_before(&bucketname) + } + } + + fn predicate_path_take_while<'a>(&'a self) -> impl Fn(&PathBuf) -> bool + 'a { + move |path| { + let bucketname = match path.file_name() { + Some(path_os_str) => path_os_str.to_string_lossy(), + None => panic!("{:?} not a file", path), + }; + self.to.is_bucket_while(&bucketname) + } + } +} + +impl SelectFilterFrom { + fn is_bucket_before(&self, bucketname: &str) -> bool { + self.bucket.as_ref().map_or(false, |bucket| bucketname < bucket) + } +} + +impl SelectFilterTo { + fn is_bucket_while(&self, bucketname: &str) -> bool { + self.bucket.as_ref().map_or(true, |bucket| bucketname <= bucket) + } +} + +pub fn select_by_args(args: &[String]) { + let filters = match SelectFilters::parse_from_args(args) { + Err(error) => { println!("{}", error); return; }, + Ok(parsed_filters) => parsed_filters, + }; + + let indexdir = defaults::get_indexdir(); + + let mut buckets: Vec = utils::file_iter(&indexdir) + .collect(); + buckets.sort_unstable(); + let buckets = buckets.into_iter() + .skip_while(filters.predicate_path_skip_while()) + .take_while(filters.predicate_path_take_while()); + + let cals = buckets.map(|bucket| utils::read_lines_from_file(&bucket)) + .filter_map(|lines| lines.ok()) + .flatten() + .map(|line| utils::read_khaleesi_line(&line)) + .filter_map(|cal| cal.ok()) + .map(|cal| cal.get_principal_event()) + ; + + let mut lines: Vec = cals + .filter(|event| filters.is_selected(event)) + .map(|event| event.get_khaleesi_line()) + .flatten() + .collect(); + + lines.sort_unstable(); + lines.dedup(); + + for line in lines { + println!("{}", line); + } +} diff --git a/src/actions/seq.rs b/src/actions/seq.rs new file mode 100644 index 0000000..cbdca21 --- /dev/null +++ b/src/actions/seq.rs @@ -0,0 +1,57 @@ +extern crate atty; + +use itertools::Itertools; +use std::fs::rename; +use std::io; + +use defaults::*; +use utils::fileutil as utils; + +pub fn do_seq(_args: &[String]) { + if atty::isnt(atty::Stream::Stdin) { + write_stdin_to_seqfile() + } else { + //println!("stdin is tty") + } + + if atty::isnt(atty::Stream::Stdout) || atty::is(atty::Stream::Stdin) { + write_seqfile_to_stdout() + } +} + +fn write_stdin_to_seqfile() { + let tmpfilename = get_datafile("tmpseq"); + + let seqfile = get_seqfile(); + let mut lines; + match utils::read_lines_from_stdin() { + Ok(mut input) => lines = input.join("\n"), + Err(error) => { + error!("Error reading from stdin: {}", error); + return + } + } + lines.push_str("\n"); + if let Err(error) = utils::write_file(&tmpfilename, &lines) { + error!("Could not write seqfile: {}", error); + return + } + + if let Err(error) = rename(tmpfilename, seqfile) { + error!("{}", error) + } +} + +pub fn read_seqfile() -> io::Result> { + let seqfile = get_seqfile(); + debug!("Reading sequence file: {}", seqfile.to_string_lossy()); + utils::read_lines_from_file(&seqfile) +} + +fn write_seqfile_to_stdout() { + if let Ok(sequence) = read_seqfile() { + for line in sequence { + println!("{}", line); + } + } +} diff --git a/src/actions/show.rs b/src/actions/show.rs new file mode 100644 index 0000000..89d07e0 --- /dev/null +++ b/src/actions/show.rs @@ -0,0 +1,17 @@ +use std::path::Path; + +use utils::{fileutil,dateutil}; + +pub fn do_show(filenames: &mut Iterator, _args: &[String]) { + info!("do_show"); + + for line in filenames { + let parts: Vec<&str> = line.splitn(2, ' ').collect(); + let path = match dateutil::datetime_from_timestamp(parts[0]) { + Some(_) => Path::new(parts[1]), + None => Path::new(parts[0]), + }; + let output = fileutil::read_file_to_string(path).unwrap(); + println!("{}", output); + } +} diff --git a/src/actions/unroll.rs b/src/actions/unroll.rs new file mode 100644 index 0000000..156432f --- /dev/null +++ b/src/actions/unroll.rs @@ -0,0 +1,15 @@ +use std::path::Path; + +use utils::fileutil as utils; + +pub fn do_unroll(filepath: &Path) { + let cal = utils::read_calendar_from_path(filepath).unwrap(); + for event in cal.events_iter() { + if event.has_recur() { + let recurs = event.get_recur_datetimes(); + for datetime in recurs { + println!("{} {}", datetime.timestamp(), cal.get_path_as_string().unwrap_or_else(|| "".to_string())); + } + } + } +} diff --git a/src/agenda.rs b/src/agenda.rs deleted file mode 100644 index 5b98fb1..0000000 --- a/src/agenda.rs +++ /dev/null @@ -1,226 +0,0 @@ -use chrono::{Datelike, TimeZone, Local, Date}; -use itertools::Itertools; -use yansi::{Style}; - -use icalwrap::*; -use utils::fileutil as utils; -use config::{Config,CalendarConfig}; - -pub fn show_events(config: &Config, lines: &mut Iterator) { - let cals = utils::read_calendars_from_files(lines).unwrap(); - - let mut not_over_yet: Vec<(usize, &IcalVCalendar, IcalVEvent, Option<&CalendarConfig>)> = Vec::new(); - let mut cals_iter = cals.iter() - .enumerate() - .map(|(i, cal)| (i, cal, cal.get_principal_event(), config.get_config_for_calendar(&cal))) - .peekable(); - - let start_day = match cals_iter.peek() { - Some((_, _, event, _)) => { - event - .get_dtstart() - .unwrap_or_else(|| Local.timestamp(0, 0)) - .date() - } - None => return, - }; - - let mut cur_day = start_day.pred(); - let mut last_printed_day = start_day.pred(); - while cals_iter.peek().is_some() || !not_over_yet.is_empty() { - cur_day = cur_day.succ(); - - maybe_print_date_line_header(&config, cur_day, start_day, &mut last_printed_day); - - not_over_yet.retain( |(index, _, event, cal_config)| { - maybe_print_date_line(&config, cur_day, start_day, &mut last_printed_day); - print_event_line(*cal_config, *index, &event, cur_day); - event.continues_after(cur_day) - }); - - let relevant_events = cals_iter.peeking_take_while(|(_,_,event,_)| event.starts_on(cur_day)); - for (i, cal, event, cal_config) in relevant_events { - maybe_print_date_line(&config, cur_day, start_day, &mut last_printed_day); - print_event_line(cal_config, i, &event, cur_day); - if event.continues_after(cur_day) { - not_over_yet.push((i, cal, event, cal_config)); - } - } - } -} - -fn maybe_print_week_separator(config: &Config, date: Date, start_date: Date, last_printed_date: Date) { - if !config.agenda.print_week_separator { - return; - } - if date != start_date && last_printed_date.iso_week() < date.iso_week() { - println!(); - } -} - -fn maybe_print_date_line_header(config: &Config, date: Date, start_date: Date, last_printed_date: &mut Date) { - if !config.agenda.print_empty_days { - return; - } - maybe_print_date_line(config, date, start_date, last_printed_date); -} - -fn maybe_print_date_line(config: &Config, date: Date, start_date: Date, last_printed_date: &mut Date) { - if date <= *last_printed_date { - return; - } - maybe_print_week_separator(config, date, start_date, *last_printed_date); - print_date_line(date); - *last_printed_date = date; -} - -fn print_date_line(date: Date) { - let style_heading = Style::new().bold(); - println!("{}, {}", style_heading.paint(date.format("%Y-%m-%d")), date.format("%A")); -} - -fn print_event_line(config: Option<&CalendarConfig>, index: usize, event: &IcalVEvent, date: Date) { - match event_line(config, &event, date) { - Ok(line) => println!("{:4} {}", index, line), - Err(error) => warn!("{} in {}", error, event.get_uid()) - } -} - -pub fn event_line(config: Option<&CalendarConfig>, event: &IcalVEvent, cur_day: Date) -> Result { - if !event.relevant_on(cur_day) { - return Err(format!("event is not relevant for {:?}", cur_day)); - } - - if event.is_allday() { - let mut summary = event.get_summary().ok_or("Invalid SUMMARY")?; - if let Some(config) = config { - let calendar_style = config.get_style_for_calendar(); - summary = calendar_style.paint(summary).to_string(); - } - Ok(format!(" {}", summary)) - } else { - let mut time_sep = " "; - let dtstart = event.get_dtstart().ok_or("Invalid DTSTART")?; - let start_string = if dtstart.date() != cur_day { - "".to_string() - } else { - time_sep = "-"; - format!("{}", dtstart.format("%H:%M")) - }; - - let dtend = event.get_dtend().ok_or("Invalid DTEND")?; - let end_string = if dtend.date() != cur_day { - "".to_string() - } else { - time_sep = "-"; - format!("{}", dtend.format("%H:%M")) - }; - - let mut summary = event.get_summary().ok_or("Invalid SUMMARY")?; - - if let Some(config) = config { - let calendar_style = config.get_style_for_calendar(); - summary = calendar_style.paint(summary).to_string(); - } - - Ok(format!("{:5}{}{:5} {}", start_string, time_sep, end_string, summary)) - } -} - -impl IcalVEvent { - fn starts_on(&self, date: Date) -> bool { - self.get_dtstart().unwrap().date() == date - } - - fn relevant_on(&self, date: Date) -> bool { - self.get_dtstart().map(|dtstart| dtstart.date() <= date).unwrap_or(false) && - self.get_last_relevant_date().map(|enddate| enddate >= date).unwrap_or(false) - } - - fn continues_after(&self, date: Date) -> bool { - self.get_last_relevant_date() - .map(|enddate| enddate > date) - .unwrap_or(false) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use testdata; - use chrono::{Local, TimeZone}; - - #[test] - fn test_starts_on() { - let cal = IcalVCalendar::from_str(testdata::TEST_EVENT_MULTIDAY, None).unwrap(); - let event = cal.get_principal_event(); - - let first_day = Local.ymd(2007, 6, 28); - assert!(event.starts_on(first_day)); - - let last_day = Local.ymd(2007, 7, 7); - assert!(!event.starts_on(last_day)); - } - - #[test] - fn test_continues_after_allday() { - let cal = IcalVCalendar::from_str(testdata::TEST_EVENT_MULTIDAY_ALLDAY, None).unwrap(); - let event = cal.get_principal_event(); - let first_day = Local.ymd(2007, 6, 28); - assert!(event.continues_after(first_day)); - let last_day = Local.ymd(2007, 7, 8); - assert!(!event.continues_after(last_day)); - } - - #[test] - fn test_continues_after_simple() { - let cal = IcalVCalendar::from_str(testdata::TEST_EVENT_ONE_MEETING, None).unwrap(); - let event = cal.get_principal_event(); - let date = Local.ymd(1997, 3, 24); - assert!(!event.continues_after(date)); - } - - #[test] - fn test_event_line_negative() { - let cal = IcalVCalendar::from_str(testdata::TEST_EVENT_ONE_MEETING, None).unwrap(); - let event = cal.get_principal_event(); - let date = Local.ymd(1998, 1, 1); - let event_line = event_line(None, &event, date); - assert!(event_line.is_err()) - } - - #[test] - fn test_event_line_simple() { - testdata::setup(); - let cal = IcalVCalendar::from_str(testdata::TEST_EVENT_ONE_MEETING, None).unwrap(); - let event = cal.get_principal_event(); - let date = Local.ymd(1997, 3, 24); - let event_line = event_line(None, &event, date).unwrap(); - assert_eq!("12:30-21:00 Calendaring Interoperability Planning Meeting".to_string(), event_line) - } - - #[test] - fn test_event_line_multiday() { - testdata::setup(); - let cal = IcalVCalendar::from_str(testdata::TEST_EVENT_MULTIDAY, None).unwrap(); - let event = cal.get_principal_event(); - let begin = Local.ymd(2007, 6, 28); - let middle = Local.ymd(2007, 6, 30); - let end = Local.ymd(2007, 7, 9); - let event_line_begin = event_line(None, &event, begin).unwrap(); - let event_line_middle = event_line(None, &event, middle).unwrap(); - let event_line_end = event_line(None, &event, end).unwrap(); - assert_eq!("13:29- Festival International de Jazz de Montreal".to_string(), event_line_begin); - assert_eq!(" Festival International de Jazz de Montreal".to_string(), event_line_middle); - assert_eq!(" -07:29 Festival International de Jazz de Montreal".to_string(), event_line_end); - } - - #[test] - fn test_event_line_multiday_allday() { - let cal = IcalVCalendar::from_str(testdata::TEST_EVENT_MULTIDAY_ALLDAY, None).unwrap(); - let event = cal.get_principal_event(); - let date = Local.ymd(2007, 6, 28); - let event_line = event_line(None, &event, date).unwrap(); - assert_eq!(" Festival International de Jazz de Montreal".to_string(), event_line) - } -} diff --git a/src/bin/khaleesi.rs b/src/bin/khaleesi.rs index 07d3c4e..dd56435 100644 --- a/src/bin/khaleesi.rs +++ b/src/bin/khaleesi.rs @@ -5,21 +5,9 @@ extern crate stderrlog; #[macro_use] extern crate log; -use khaleesi::agenda; -use khaleesi::cal; -use khaleesi::copy; use khaleesi::config::Config; use khaleesi::defaults::*; -use khaleesi::edit; -use khaleesi::index; -use khaleesi::list; -use khaleesi::modify; -use khaleesi::new; -use khaleesi::prettyprint; -use khaleesi::select; -use khaleesi::seq; -use khaleesi::show; -use khaleesi::unroll; +use khaleesi::actions::*; use khaleesi::utils::fileutil as utils; use std::env; diff --git a/src/bucketable.rs b/src/bucketable.rs deleted file mode 100644 index e0f743a..0000000 --- a/src/bucketable.rs +++ /dev/null @@ -1,149 +0,0 @@ -use chrono::{Local, Date, Datelike, Duration}; -use std::collections::HashMap; -use std::{hash, cmp}; - -use icalwrap::{IcalVEvent, IcalVCalendar}; -use utils::misc; - -pub trait Bucketable { - fn get_buckets(&self) -> Result>, String>; - - fn buckets_for_interval(mut start: Date, end: Date) -> Vec { - let mut buckets = Vec::new(); - - while start.iso_week() <= end.iso_week() { - let bucket = misc::get_bucket_for_date(start); - buckets.push(bucket); - start = start.checked_add_signed(Duration::days(7)).unwrap(); - } - buckets - } -} - -impl Bucketable for IcalVEvent { - fn get_buckets(&self) -> Result>, String> { - let mut result: HashMap> = HashMap::new(); - - let start_date = self.get_dtstart_date().ok_or_else(|| format!("Invalid DTSTART in {}", self.get_uid()))?; - let mut end_date = self.get_dtend_date().unwrap_or(start_date); - - // end-dtimes are non-inclusive - // so in case of date-only events, the last day of the event is dtend-1 - if self.is_allday() { - end_date = end_date.pred(); - } - - let buckets = Self::buckets_for_interval(start_date, end_date); - for bucketid in buckets { - result - .entry(bucketid) - .and_modify(|items| items.push(self.get_khaleesi_line().unwrap())) - .or_insert_with(|| vec!(self.get_khaleesi_line().unwrap())); - } - - if self.has_recur() { - for instance in self.get_recur_instances() { - let recur_buckets = instance.get_buckets()?; - result.merge(recur_buckets) - } - } - - for vec in result.values_mut() { - vec.dedup() - } - Ok(result) - } -} - -impl Bucketable for IcalVCalendar { - fn get_buckets(&self) -> Result>, String> { - let mut result: HashMap> = HashMap::new(); - for event in self.events_iter() { - let recur_buckets = event.get_buckets()?; - result.merge(recur_buckets); - } - Ok(result) - } -} - -pub trait Merge -where K: cmp::Eq + hash::Hash -{ - fn merge(&mut self, other: HashMap>); -} - -impl Merge for HashMap, S> -where K: cmp::Eq + hash::Hash, - S: std::hash::BuildHasher -{ - fn merge(&mut self, other: HashMap>) { - for (key, mut lines) in other.into_iter() { - self - .entry(key) - .and_modify(|items| items.append(&mut lines)) - .or_insert(lines); - } - } -} - -#[test] -fn merge_test() { - let mut map_a: HashMap<&str, Vec> = HashMap::new(); - let mut map_b: HashMap<&str, Vec> = HashMap::new(); - - let key = "key"; - map_a.insert(&key, vec!["a".to_string(), "b".to_string()]); - map_b.insert(&key, vec!["c".to_string(), "d".to_string()]); - - map_a.merge(map_b); - assert_eq!(map_a.get(&key).unwrap(), &vec!["a".to_string(), "b".to_string(), "c".to_string(), "d".to_string()]); -} - -#[test] -fn buckets_multi_day_allday() { - use testdata; - use std::path::PathBuf; - - let path = PathBuf::from("test/path"); - let cal = IcalVCalendar::from_str(testdata::TEST_EVENT_MULTIDAY_ALLDAY, Some(&path)).unwrap(); - - let event_buckets = cal.get_principal_event().get_buckets().unwrap(); - - assert_eq!(2, event_buckets.len()); - - let mut bucket_names = event_buckets.keys().collect::>(); - bucket_names.sort_unstable(); - assert_eq!(vec!("2007-W26", "2007-W27"), bucket_names); - - let cal_buckets = cal.get_buckets().unwrap(); - assert_eq!(event_buckets, cal_buckets); -} - -#[test] -fn buckets_single_event() { - use testdata; - use std::path::PathBuf; - - let path = PathBuf::from("test/path"); - let cal = IcalVCalendar::from_str(testdata::TEST_EVENT_ONE_MEETING, Some(&path)).unwrap(); - - let comp_buckets = cal.get_buckets().unwrap(); - assert_eq!(vec!("1997-W13"), comp_buckets.keys().collect::>()); -} - -#[test] -fn buckets_simple_recurring_event() { - use testdata; - use std::path::PathBuf; - - let path = PathBuf::from("test/path"); - let cal = IcalVCalendar::from_str(testdata::TEST_EVENT_RECUR, Some(&path)).unwrap(); - - let event = cal.get_principal_event(); - let event_buckets = event.get_buckets().unwrap(); - let cal_buckets = cal.get_buckets().unwrap(); - assert_eq!(event_buckets, cal_buckets); - let mut cal_bucket_names = cal_buckets.keys().collect::>(); - cal_bucket_names.sort_unstable(); - assert_eq!(vec!("2018-W41", "2018-W42", "2018-W43", "2018-W44", "2018-W45", "2018-W46", "2018-W47", "2018-W48", "2018-W49", "2018-W50"), cal_bucket_names); -} diff --git a/src/cal.rs b/src/cal.rs deleted file mode 100644 index a23be91..0000000 --- a/src/cal.rs +++ /dev/null @@ -1,160 +0,0 @@ -use chrono::Duration; -use chrono::prelude::*; -use yansi::{Style,Color}; - -use utils::misc; - -struct Cell { - date: NaiveDate, - content: (String,String) -} - -pub fn printcal() { - let now = Local::today(); - let a = cal_month(now); - let b = cal_month(now.with_month(now.month() + 1).unwrap()); - let c = cal_month(now.with_month(now.month() + 2).unwrap()); - - let joined = misc::joinlines(&a, &b); - let joined = misc::joinlines(&joined, &c); - println!("{}", joined); -} - -pub fn dbg() { - let begin = Local::today().naive_local(); - let end = begin + Duration::days(5); - let cells = get_cells(begin, end); - let cells = expand_cells_to_week(cells); - - let render = render_cells(&cells); - print!("{}", render); -} - -fn render_cells(cells: &[Cell]) -> String { - let mut result = String::with_capacity(50); - - let now = cells[0].date; - - result.push_str(&format!("{:>28} {:<8}\n", - now.format("%B").to_string(), - now.format("%Y").to_string() - )); - let weekdays = &[ "Mo", "Tu", "We", "Th", "Fr", "Sa", "Su" ].iter().map(|x| format!("{:8}", x)).collect::(); - result.push_str(weekdays); - result.push_str("\n"); - - let flow = render_flow(7, 8, cells); - result.push_str(&flow); - - result -} - -fn render_flow(cells_per_line: usize, cell_width: usize, cells: &[Cell]) -> String { - let mut result = String::with_capacity(50); - - let style = Style::new().bg(Color::Fixed(236)); - - let it = cells.iter(); - let mut n = 0; - while n < (cells.len() / cells_per_line) { - let line = it.clone().skip(n * cells_per_line).take(cells_per_line); - for cell in line.clone() { - let cellstr = &format!("{:width$}", &cell.content.0, width = cell_width); - let cellstr = &format!("{}", style.paint(cellstr)); - result.push_str(cellstr); - } - result.push_str("\n"); - for cell in line { - let cellstr = &format!("{:width$}", &cell.content.1, width = cell_width); - let cellstr = &format!("{}", style.paint(cellstr)); - result.push_str(cellstr); - } - let emptyline = &format!("{:width$}", "", width = cell_width * cells_per_line); - result.push_str("\n"); - result.push_str(&format!("{}\n", style.paint(emptyline))); - n += 1; - } - - result -} - -fn get_cells(date_begin: NaiveDate, date_end: NaiveDate) -> Vec { - let mut result = vec!(); - let mut date = date_begin; - while date < date_end { - let cell = cell_whatever(date); - result.push(cell); - - date += Duration::days(1); - } - result -} - -fn cell_whatever(date: NaiveDate) -> Cell { - let fst = date.format("%d").to_string(); - let snd = String::from(""); - Cell{date, content: (fst, snd)} -} - -fn cell_empty(date: NaiveDate) -> Cell { - let fst = date.format("%d").to_string(); - let snd = String::from(""); - Cell{date, content: (fst, snd)} -} - -fn expand_cells_to_week(cells: Vec) -> Vec { - let mut result = vec!(); - - let mut day = NaiveDate::from_isoywd(cells[0].date.year(), cells[0].date.iso_week().week(), Weekday::Mon); - while day < cells[0].date { - let cell = cell_empty(day); - result.push(cell); - - day += Duration::days(1); - } - - let mut day = cells[cells.len() - 1].date; - - for cell in cells { - result.push(cell); - } - - let last_date = NaiveDate::from_isoywd(day.year(), day.iso_week().week(), Weekday::Sun); - day += Duration::days(1); - while day <= last_date { - let cell = cell_empty(day); - result.push(cell); - - day += Duration::days(1); - } - - result -} - -pub fn cal_month(now: Date) -> String { - let mut result = String::with_capacity(50); - - result.push_str(&format!("{:>11} {:<8}\n", - now.format("%B").to_string(), - now.format("%Y").to_string() - )); - result.push_str("Su Mo Tu We Th Fr Sa\n"); - - let this_month = now.month(); - let mut current_day = Local.ymd(now.year(), now.month(), 1); - - let one_day = Duration::days(1); - for _ in 0..current_day.weekday().num_days_from_sunday() { - result.push_str(" "); - } - while current_day.month() == this_month { - result.push_str(&format!("{:>2} ", current_day.day())); - if current_day.weekday() == Weekday::Sat { - result.push_str("\n"); - } - current_day = current_day + one_day; - } - result.push_str("\n"); - - result -} diff --git a/src/copy.rs b/src/copy.rs deleted file mode 100644 index 5ed7aef..0000000 --- a/src/copy.rs +++ /dev/null @@ -1,37 +0,0 @@ -use utils::fileutil; -use utils::misc; - -pub fn do_copy(lines: &mut Iterator, _args: &[String]) { - - let lines = lines.collect::>(); - if lines.len() > 1 { - println!("copy only one event!"); - return; - }; - - let cal = match fileutil::read_khaleesi_line(&lines[0]) { - Ok(calendar) => calendar, - Err(error) => { - error!("{}", error); - return - }, - }; - let new_cal = match cal.with_uid(&misc::make_new_uid()) { - Ok(new_cal) => new_cal, - Err(error) => { - error!("{}", error); - return - }, - }; - let new_cal = new_cal.with_dtstamp_now(); - - match fileutil::write_cal(&new_cal) { - Ok(_) => info!("Successfully wrote file: {}", new_cal.get_path().unwrap().display()), - Err(error) => { - error!("{}", error); - return - }, - } - - println!("{}", new_cal.get_principal_event().get_khaleesi_line().unwrap()); -} diff --git a/src/edit.rs b/src/edit.rs deleted file mode 100644 index e7f0afb..0000000 --- a/src/edit.rs +++ /dev/null @@ -1,28 +0,0 @@ -use std::env; -use std::fs; -use std::process::Command; - -use utils::dateutil; - -pub fn do_edit(filenames: &mut Iterator, _args: &[String]) { - - let mut paths: Vec = filenames.map( |line| { - let parts: Vec<&str> = line.splitn(2, ' ').collect(); - match dateutil::datetime_from_timestamp(parts[0]) { - Some(_) => parts[1].to_string(), - None => parts[0].to_string(), - } - }).collect(); - paths.sort_unstable(); - paths.dedup(); - - let editor = env::var("EDITOR").unwrap_or_else(|_| "vim".to_string()); - - if let Err(error) = Command::new(&editor) - .args(paths) - .stdin(fs::File::open("/dev/tty").unwrap()) - .status() { - error!("{} command failed to start, error: {}", editor, error); - return - }; -} diff --git a/src/index.rs b/src/index.rs deleted file mode 100644 index e3f02a7..0000000 --- a/src/index.rs +++ /dev/null @@ -1,152 +0,0 @@ -use chrono::prelude::*; -use icalwrap::*; -use std::collections::HashMap; -use std::fs; -use std::path::{Path,PathBuf}; -use std::time::SystemTime; -use walkdir::DirEntry; - -use defaults::*; -use indextime; -use utils::fileutil; -use utils::lock; -use utils::misc; - - -fn add_buckets_for_calendar(buckets: &mut HashMap>, cal: &IcalVCalendar) { - use bucketable::Bucketable; - use bucketable::Merge; - - match cal.get_buckets() { - Ok(cal_buckets) => buckets.merge(cal_buckets), - Err(error) => { - warn!("{}", error) - } - } -} - -pub fn index_dir(dir: &Path, reindex: bool) { - use std::time::Instant; - - let lock = lock::lock_file_exclusive(&get_indexlockfile()); - if lock.is_err() { - error!("Failed to obtain index lock!"); - return; - } - - info!("Recursively indexing '.ics' files in directory: {}", dir.to_string_lossy()); - if !dir.exists() { - error!("Directory doesn't exist: {}", dir.to_string_lossy()); - return; - } - - let now = Instant::now(); - let start_time = Utc::now(); - - let last_index_time = if reindex { - debug!("Forced reindex, indexing all files"); - None - } else { - let last_index_time = indextime::get_index_time(); - match last_index_time { - Some(time) => debug!("Previously indexed {}, indexing newer files only", time.with_timezone(&Local)), - None => debug!("No previous index time, indexing all files"), - } - last_index_time - }; - - let modified_since = last_index_time.map(|time| time.timestamp()).unwrap_or(0); - let ics_files = get_ics_files(dir, modified_since); - - let buckets = read_buckets(ics_files); - - let indexdir = get_indexdir(); - let clear_index_dir = last_index_time.is_none(); - if let Err(error) = prepare_index_dir(&indexdir, clear_index_dir) { - error!("{}", error); - return; - } - - write_index(&indexdir, &buckets); - info!("Index written in {}ms", misc::format_duration(&now.elapsed())); - - indextime::write_index_time(&start_time); -} - -pub fn get_ics_files(dir: &Path, modified_since: i64) -> impl Iterator { - use walkdir::WalkDir; - - WalkDir::new(dir).into_iter() - .filter_entry(move |entry| accept_entry(entry, modified_since)) - .filter_map(|e| e.ok()) - .filter(|e| e.file_type().is_file()) - .filter(|e| e.path().extension().map_or(false, |extension| extension == "ics")) - .map(|entry| entry.into_path()) -} - -fn accept_entry(dir_entry: &DirEntry, modified_since: i64) -> bool { - if dir_entry.path().is_dir() { - return true; - } - dir_entry.metadata() - .map_err(|err| err.into()) // transform to io::Error - .and_then(|metadata| metadata.modified()) - .map(|modified| modified.duration_since(SystemTime::UNIX_EPOCH).unwrap()) - .map(|modified| modified.as_secs() as i64) - .map(|modified| modified > modified_since) - .unwrap_or(false) -} - -fn read_buckets(ics_files: impl Iterator) -> HashMap> { - let mut buckets: HashMap> = HashMap::new(); - - let mut total_files = 0; - for file in ics_files { - trace!("File: {:?}", file); - match fileutil::read_file_to_string(&file) { - Ok(content) => { - total_files += 1; - match IcalVCalendar::from_str(&content, Some(&file)) { - Ok(mut cal) => add_buckets_for_calendar(&mut buckets, &cal), - Err(error) => error!("{:?}: {}", file, error) - } - } - Err(error) => error!("{}", error), - } - } - - info!("Loaded {} files into {} buckets", total_files, buckets.len()); - buckets -} - -fn write_index(index_dir: &Path, buckets: &HashMap>) { - for (key, val) in buckets.iter() { - let bucketfile = bucket_file(index_dir, key); - trace!("Writing bucket: {}", key); - let content = &[&val.join("\n"), "\n"].concat(); - if let Err(error) = fileutil::append_file(&bucketfile, content) { - error!("{}", error); - return; - } - } -} - -fn bucket_file(index_dir: &Path, key: &str) -> PathBuf { - let mut result = PathBuf::from(index_dir); - result.push(key); - result -} - -fn prepare_index_dir(indexdir: &Path, clear_index_dir: bool) -> Result<(), std::io::Error> { - if indexdir.exists() && clear_index_dir { - info!("Clearing index directory: {}", indexdir.to_string_lossy()); - fs::remove_dir_all(&indexdir)? - } - - if !indexdir.exists() { - info!("Creating index directory: {}", indexdir.to_string_lossy()); - fs::create_dir(&indexdir)?; - } - - Ok(()) -} diff --git a/src/indextime.rs b/src/indextime.rs deleted file mode 100644 index b078b2f..0000000 --- a/src/indextime.rs +++ /dev/null @@ -1,38 +0,0 @@ -use std::fs; -use std::io::{Read,Write}; -use chrono::prelude::*; - -use defaults::*; - -pub fn write_index_time(index_time: &DateTime) { - let mut timefile = fs::File::create(get_indextimefile()).unwrap(); - timefile.write_all(format!("{}\n", index_time.timestamp()).as_bytes()).unwrap(); -} - -pub fn get_index_time() -> Option> { - let mut timefile = fs::File::open(get_indextimefile()).ok()?; - let mut timestamp_str = String::new(); - timefile.read_to_string(&mut timestamp_str).ok()?; - let timestamp = timestamp_str.trim().parse::().ok()?; - Some(Utc.timestamp(timestamp, 0)) -} - -#[cfg(test)] -mod tests { - use super::*; - - use testutils; - use assert_fs::prelude::*; - - #[test] - fn test_write_read() { - let testdir = testutils::prepare_testdir("testdir"); - - let timestamp = Utc.ymd(1990,01,01).and_hms(1, 1, 0); - write_index_time(×tamp); - testdir.child(".khaleesi/index-time").assert("631155660\n"); - - let indextime = get_index_time(); - assert_eq!(Some(timestamp), indextime); - } -} diff --git a/src/lib.rs b/src/lib.rs index