diff --git a/Cargo.lock b/Cargo.lock index 13420f4..73c80f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -157,6 +157,22 @@ dependencies = [ "winapi", ] +[[package]] +name = "edit" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cdd6936f8bd9782e28932eef853bfcd8548992ce5748bb3e7e88bad613d0ee0" +dependencies = [ + "tempfile", + "which", +] + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + [[package]] name = "fake-simd" version = "0.1.2" @@ -287,6 +303,12 @@ dependencies = [ "sha-1", ] +[[package]] +name = "ppv-lite86" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -329,6 +351,46 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", + "rand_hc", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_hc" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" +dependencies = [ + "rand_core", +] + [[package]] name = "redox_syscall" version = "0.2.10" @@ -348,6 +410,15 @@ dependencies = [ "redox_syscall", ] +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + [[package]] name = "sha-1" version = "0.8.2" @@ -401,6 +472,20 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "tempfile" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" +dependencies = [ + "cfg-if", + "libc", + "rand", + "redox_syscall", + "remove_dir_all", + "winapi", +] + [[package]] name = "termcolor" version = "1.1.2" @@ -459,6 +544,7 @@ dependencies = [ "colored", "computus", "directories", + "edit", "pest", "pest_derive", "structopt", @@ -524,6 +610,17 @@ version = "0.10.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" +[[package]] +name = "which" +version = "4.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea187a8ef279bc014ec368c27a920da2024d2a711109bfbe3440585d5cf27ad9" +dependencies = [ + "either", + "lazy_static", + "libc", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 458483b..2807701 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ codespan-reporting = "0.11.1" colored = "2.0.0" computus = "1.0.0" directories = "4.0.1" +edit = "0.1.3" pest = "2.1.3" pest_derive = "2.1.0" structopt = "0.3.25" diff --git a/src/cli.rs b/src/cli.rs index 5baf98c..4bb98ac 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -8,7 +8,7 @@ use directories::ProjectDirs; use structopt::StructOpt; use crate::eval::{self, DateRange, Entry, EntryMode}; -use crate::files::arguments::{CliIdent, CliRange}; +use crate::files::arguments::{CliDate, CliIdent, CliRange}; use crate::files::{self, FileSource, Files, ParseError}; use self::error::Error; @@ -18,6 +18,7 @@ mod cancel; mod done; mod error; mod layout; +mod log; mod print; mod show; @@ -57,6 +58,11 @@ pub enum Command { #[structopt(required = true)] entries: Vec, }, + /// Edits or creates a log entry + Log { + #[structopt(default_value = "today")] + date: String, + }, /// Reformats all loaded files Fmt, } @@ -162,6 +168,12 @@ fn run_command( let layout = find_layout(files, &entries, range, now); print::print(&layout); } + Some(Command::Log { date }) => { + match parse_eval_arg("date", date, |date: CliDate| date.eval((), now.date())) { + Some(date) => log::log(files, date)?, + None => process::exit(1), + }; + } Some(Command::Fmt) => files.mark_all_dirty(), } Ok(()) diff --git a/src/cli/error.rs b/src/cli/error.rs index 7ad1b01..b4246c3 100644 --- a/src/cli/error.rs +++ b/src/cli/error.rs @@ -1,3 +1,5 @@ +use std::io; + use chrono::NaiveDate; use codespan_reporting::files::Files; use codespan_reporting::term::Config; @@ -15,6 +17,8 @@ pub enum Error { NoSuchLog(NaiveDate), #[error("Not a task")] NotATask(Vec), + #[error("Error editing log for {date}: {error}")] + EditingLog { date: NaiveDate, error: io::Error }, } impl<'a, F: Files<'a>> Eprint<'a, F> for Error { @@ -33,6 +37,10 @@ impl<'a, F: Files<'a>> Eprint<'a, F> for Error { eprintln!("{} are not tasks.", ns.join(", ")); } } + Error::EditingLog { date, error } => { + eprintln!("Error editing log for {}", date); + eprintln!(" {}", error); + } } } } diff --git a/src/cli/log.rs b/src/cli/log.rs new file mode 100644 index 0000000..6456827 --- /dev/null +++ b/src/cli/log.rs @@ -0,0 +1,26 @@ +use chrono::NaiveDate; + +use crate::files::Files; + +use super::error::Error; + +pub fn log(files: &mut Files, date: NaiveDate) -> Result<(), Error> { + let desc = files + .log(date) + .map(|log| log.value.desc.join("\n")) + .unwrap_or_default(); + + let mut builder = edit::Builder::new(); + builder.suffix(".md"); + let edited = edit::edit_with_builder(desc, &builder) + .map_err(|error| Error::EditingLog { date, error })?; + + let edited = edited + .lines() + .map(|line| line.to_string()) + .collect::>(); + + files.set_log(date, edited); + + Ok(()) +} diff --git a/src/files.rs b/src/files.rs index 3f32812..007ad63 100644 --- a/src/files.rs +++ b/src/files.rs @@ -339,6 +339,21 @@ impl Files { } } + fn latest_log(&self) -> Option<(NaiveDate, Source)> { + self.logs + .iter() + .map(|(d, s)| (*d, *s)) + .max_by_key(|(d, _)| *d) + } + + fn latest_log_before(&self, date: NaiveDate) -> Option<(NaiveDate, Source)> { + self.logs + .iter() + .map(|(d, s)| (*d, *s)) + .filter(|(d, _)| d <= &date) + .max_by_key(|(d, _)| *d) + } + pub fn now(&self) -> DateTime<&Tz> { if let Some(tz) = &self.timezone { Utc::now().with_timezone(&tz) @@ -355,6 +370,18 @@ impl Files { } } + fn modify(&mut self, source: Source, edit: impl FnOnce(&mut Command)) { + let file = &mut self.files[source.file]; + edit(&mut file.file.commands[source.command]); + file.dirty = true; + } + + fn insert(&mut self, file: FileSource, command: Command) { + let file = &mut self.files[file.0]; + file.file.commands.push(command); + file.dirty = true; + } + /// Add a [`Done`] statement to the task identified by `source`. /// /// Returns whether the addition was successful. It can fail if the entry @@ -370,6 +397,26 @@ impl Files { true } + pub fn set_log(&mut self, date: NaiveDate, desc: Vec) { + if let Some(source) = self.logs.get(&date).cloned() { + self.modify(source, |command| match command { + Command::Log(log) => log.desc = desc, + _ => unreachable!(), + }); + } else { + let file = self + .latest_log_before(date) + .or_else(|| self.latest_log()) + .map(|(_, source)| source.file()) + .unwrap_or(FileSource(0)); + + let date = Spanned::dummy(date); + let command = Command::Log(Log { date, desc }); + + self.insert(file, command); + } + } + /* Errors */ fn cs_id(&self, file: FileSource) -> usize { diff --git a/src/files/arguments.rs b/src/files/arguments.rs index 6ff4b7d..100e7f1 100644 --- a/src/files/arguments.rs +++ b/src/files/arguments.rs @@ -46,6 +46,19 @@ fn parse_cli_date(p: Pair<'_, Rule>) -> Result { Ok(CliDate { datum, delta }) } +impl FromStr for CliDate { + type Err = ParseError<()>; + + fn from_str(s: &str) -> result::Result> { + let mut pairs = + TodayfileParser::parse(Rule::cli_date, s).map_err(|e| ParseError::new((), e))?; + let p = pairs.next().unwrap(); + assert_eq!(pairs.next(), None); + + parse_cli_date(p).map_err(|e| ParseError::new((), e)) + } +} + #[derive(Debug)] pub enum CliIdent { Number(usize), diff --git a/src/files/primitives.rs b/src/files/primitives.rs index 118bfb3..e63b867 100644 --- a/src/files/primitives.rs +++ b/src/files/primitives.rs @@ -31,6 +31,10 @@ impl Span { end: cmp::max(self.end, other.end), } } + + fn dummy() -> Self { + Self { start: 0, end: 0 } + } } #[derive(Clone, Copy)] @@ -49,6 +53,10 @@ impl Spanned { pub fn new(span: Span, value: T) -> Self { Self { span, value } } + + pub fn dummy(value: T) -> Self { + Self::new(Span::dummy(), value) + } } // I don't know how one would write this. It works as a polymorphic standalone