Compare commits
No commits in common. "master" and "v0.1.0" have entirely different histories.
37 changed files with 1634 additions and 3767 deletions
42
CHANGELOG.md
42
CHANGELOG.md
|
|
@ -2,48 +2,6 @@
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
|
||||||
## Unreleased
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- Marks to show which span a reminder belongs to
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Birthdays for current day are now highlighted
|
|
||||||
- Default value for `--range` argument
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- `--date` accepting incomplete expressions
|
|
||||||
|
|
||||||
## 0.2.0 - 2022-03-18
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- `LOG` command and `today log` CLI command
|
|
||||||
- `CAPTURE` command and `today new` CLI command
|
|
||||||
- `REMIND` statement
|
|
||||||
- `CANCEL` statement for tasks
|
|
||||||
- One-letter aliases for `show`, `log`, `done` and `cancel` CLI commands
|
|
||||||
- `MOVE` can now move entries to a different time
|
|
||||||
- `--date` now accepts expressions like `today-3d`
|
|
||||||
- In `--range` and `--date`, `t` can be used as abbreviation for `today`
|
|
||||||
- `*` markers in output for days with logs and entries with descriptions
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Output is now colored
|
|
||||||
- Better error messages
|
|
||||||
- Overhauled `today show` format
|
|
||||||
- It can now show log entries for days
|
|
||||||
- It now displays the source command (file and line) of the entry
|
|
||||||
- When saving...
|
|
||||||
- Unchanged files are no longer overwritten
|
|
||||||
- Imports are now sorted alphabetically
|
|
||||||
- Done and cancel dates are now simplified where possible
|
|
||||||
- Always prints import-based path, not absolute path
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Alignment in output
|
|
||||||
- Respect `TZDIR` environment variable
|
|
||||||
- Negative weekday deltas
|
|
||||||
|
|
||||||
## 0.1.0 - 2021-12-20
|
## 0.1.0 - 2021-12-20
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
872
Cargo.lock
generated
872
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
35
Cargo.toml
35
Cargo.toml
|
|
@ -1,35 +1,14 @@
|
||||||
[package]
|
[package]
|
||||||
name = "today"
|
name = "today"
|
||||||
version = "0.2.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
chrono = "0.4.23"
|
chrono = "0.4.19"
|
||||||
clap = { version = "4.1.4", features = ["derive"] }
|
|
||||||
codespan-reporting = "0.11.1"
|
|
||||||
colored = "2.0.0"
|
|
||||||
computus = "1.0.0"
|
computus = "1.0.0"
|
||||||
directories = "4.0.1"
|
directories = "4.0.1"
|
||||||
edit = "0.1.4"
|
pest = "2.1.3"
|
||||||
pest = "2.5.5"
|
pest_derive = "2.1.0"
|
||||||
pest_derive = "2.5.5"
|
structopt = "0.3.25"
|
||||||
promptly = "0.3.1"
|
thiserror = "1.0.30"
|
||||||
termcolor = "1.2.0"
|
tzfile = "0.1.3"
|
||||||
thiserror = "1.0.38"
|
|
||||||
tzfile = { git = "https://github.com/Garmelon/tzfile.git", branch = "tzdir" }
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
rust.unsafe_code = { level = "forbid", priority = 1 }
|
|
||||||
rust.future_incompatible = "warn"
|
|
||||||
rust.rust_2018_idioms = "warn"
|
|
||||||
rust.noop_method_call = "warn"
|
|
||||||
rust.single_use_lifetimes = "warn"
|
|
||||||
rust.trivial_numeric_casts = "warn"
|
|
||||||
rust.unused = "warn"
|
|
||||||
rust.unused_crate_dependencies = "warn"
|
|
||||||
rust.unused_extern_crates = "warn"
|
|
||||||
rust.unused_import_braces = "warn"
|
|
||||||
rust.unused_lifetimes = "warn"
|
|
||||||
rust.unused_qualifications = "warn"
|
|
||||||
clippy.all = "warn"
|
|
||||||
clippy.use_self = "warn"
|
|
||||||
|
|
|
||||||
213
src/cli.rs
213
src/cli.rs
|
|
@ -3,100 +3,56 @@ use std::str::FromStr;
|
||||||
use std::{process, result};
|
use std::{process, result};
|
||||||
|
|
||||||
use chrono::{NaiveDate, NaiveDateTime};
|
use chrono::{NaiveDate, NaiveDateTime};
|
||||||
use clap::Parser;
|
|
||||||
use codespan_reporting::files::SimpleFile;
|
|
||||||
use directories::ProjectDirs;
|
use directories::ProjectDirs;
|
||||||
|
use structopt::StructOpt;
|
||||||
|
|
||||||
use crate::eval::{self, DateRange, Entry, EntryMode};
|
use crate::eval::{DateRange, Entry, EntryMode, SourceInfo};
|
||||||
use crate::files::cli::{CliDate, CliIdent, CliRange};
|
use crate::files::arguments::Range;
|
||||||
use crate::files::{self, Files, ParseError};
|
use crate::files::{self, Files};
|
||||||
|
|
||||||
use self::error::{Error, Result};
|
use self::error::Result;
|
||||||
use self::layout::line::LineLayout;
|
use self::layout::line::LineLayout;
|
||||||
|
|
||||||
mod cancel;
|
|
||||||
mod done;
|
mod done;
|
||||||
mod error;
|
mod error;
|
||||||
mod layout;
|
mod layout;
|
||||||
mod log;
|
|
||||||
mod new;
|
|
||||||
mod print;
|
mod print;
|
||||||
mod show;
|
mod show;
|
||||||
mod util;
|
|
||||||
|
|
||||||
#[derive(Debug, clap::Parser)]
|
#[derive(Debug, StructOpt)]
|
||||||
pub struct Opt {
|
pub struct Opt {
|
||||||
/// File to load
|
/// File to load
|
||||||
#[clap(short, long)]
|
#[structopt(short, long, parse(from_os_str))]
|
||||||
file: Option<PathBuf>,
|
file: Option<PathBuf>,
|
||||||
/// Overwrite the current date
|
/// Overwrite the current date
|
||||||
#[clap(short, long, default_value = "t")]
|
#[structopt(short, long)]
|
||||||
date: String,
|
date: Option<NaiveDate>,
|
||||||
/// Range of days to focus on
|
/// The range days to focus on
|
||||||
#[clap(short, long, default_value = "t-2d--t+2w")]
|
#[structopt(short, long, default_value = "today-2d--today+13d")]
|
||||||
range: String,
|
range: String,
|
||||||
#[clap(subcommand)]
|
#[structopt(subcommand)]
|
||||||
command: Option<Command>,
|
command: Option<Command>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, clap::Subcommand)]
|
#[derive(Debug, StructOpt)]
|
||||||
pub enum Command {
|
pub enum Command {
|
||||||
|
#[allow(rustdoc::broken_intra_doc_links)]
|
||||||
/// Shows individual entries in detail
|
/// Shows individual entries in detail
|
||||||
#[clap(alias = "s")]
|
|
||||||
Show {
|
Show {
|
||||||
/// Entries and days to show
|
/// Entries to show
|
||||||
#[clap(required = true)]
|
#[structopt(required = true)]
|
||||||
identifiers: Vec<String>,
|
entries: Vec<usize>,
|
||||||
},
|
|
||||||
/// Create a new entry based on a template
|
|
||||||
#[clap(alias = "n")]
|
|
||||||
New {
|
|
||||||
#[clap(subcommand)]
|
|
||||||
template: Template,
|
|
||||||
},
|
},
|
||||||
/// Marks one or more entries as done
|
/// Marks one or more entries as done
|
||||||
#[clap(alias = "d")]
|
|
||||||
Done {
|
Done {
|
||||||
/// Entries to mark as done
|
/// Entries to mark as done
|
||||||
#[clap(required = true)]
|
#[structopt(required = true)]
|
||||||
entries: Vec<usize>,
|
entries: Vec<usize>,
|
||||||
},
|
},
|
||||||
/// Marks one or more entries as canceled
|
/// Reformat all loaded files
|
||||||
#[clap(alias = "c")]
|
|
||||||
Cancel {
|
|
||||||
/// Entries to mark as done
|
|
||||||
#[clap(required = true)]
|
|
||||||
entries: Vec<usize>,
|
|
||||||
},
|
|
||||||
/// Edits or creates a log entry
|
|
||||||
#[clap(alias = "l")]
|
|
||||||
Log {
|
|
||||||
#[clap(default_value = "t")]
|
|
||||||
date: String,
|
|
||||||
},
|
|
||||||
/// Reformats all loaded files
|
|
||||||
Fmt,
|
Fmt,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, clap::Subcommand)]
|
|
||||||
pub enum Template {
|
|
||||||
/// Adds a task
|
|
||||||
#[clap(alias = "t")]
|
|
||||||
Task {
|
|
||||||
/// If specified, the task is dated to this date
|
|
||||||
date: Option<String>,
|
|
||||||
},
|
|
||||||
/// Adds a note
|
|
||||||
#[clap(alias = "n")]
|
|
||||||
Note {
|
|
||||||
/// If specified, the note is dated to this date
|
|
||||||
date: Option<String>,
|
|
||||||
},
|
|
||||||
/// Adds an undated task marked as done today
|
|
||||||
#[clap(alias = "d")]
|
|
||||||
Done,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_file() -> PathBuf {
|
fn default_file() -> PathBuf {
|
||||||
ProjectDirs::from("", "", "today")
|
ProjectDirs::from("", "", "today")
|
||||||
.expect("could not determine config dir")
|
.expect("could not determine config dir")
|
||||||
|
|
@ -104,9 +60,18 @@ fn default_file() -> PathBuf {
|
||||||
.join("main.today")
|
.join("main.today")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_files(opt: &Opt, files: &mut Files) -> result::Result<(), files::Error> {
|
fn load_files(opt: &Opt) -> result::Result<Files, files::Error> {
|
||||||
let file = opt.file.clone().unwrap_or_else(default_file);
|
let file = opt.file.clone().unwrap_or_else(default_file);
|
||||||
files.load(&file)
|
Files::load(&file)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_now(opt: &Opt, files: &Files) -> NaiveDateTime {
|
||||||
|
let now = files.now().naive_local();
|
||||||
|
if let Some(date) = opt.date {
|
||||||
|
date.and_time(now.time())
|
||||||
|
} else {
|
||||||
|
now
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_entries(files: &Files, range: DateRange) -> Result<Vec<Entry>> {
|
fn find_entries(files: &Files, range: DateRange) -> Result<Vec<Entry>> {
|
||||||
|
|
@ -122,37 +87,6 @@ fn find_layout(
|
||||||
layout::layout(files, entries, range, now)
|
layout::layout(files, entries, range, now)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_eval_arg<T, E, R>(name: &str, text: &str, eval: E) -> Result<R>
|
|
||||||
where
|
|
||||||
T: FromStr<Err = ParseError<()>>,
|
|
||||||
E: FnOnce(T) -> result::Result<R, eval::Error<()>>,
|
|
||||||
{
|
|
||||||
let value = T::from_str(text).map_err(|error| Error::ArgumentParse {
|
|
||||||
file: SimpleFile::new(name.to_string(), text.to_string()),
|
|
||||||
error,
|
|
||||||
})?;
|
|
||||||
eval(value).map_err(|error| Error::ArgumentEval {
|
|
||||||
file: SimpleFile::new(name.to_string(), text.to_string()),
|
|
||||||
error,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_eval_date(name: &str, text: &str, today: NaiveDate) -> Result<NaiveDate> {
|
|
||||||
parse_eval_arg(name, text, |date: CliDate| date.eval((), today))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_show_idents(identifiers: &[String], today: NaiveDate) -> Result<Vec<show::Ident>> {
|
|
||||||
let mut idents = vec![];
|
|
||||||
for ident in identifiers {
|
|
||||||
let ident = parse_eval_arg("identifier", ident, |ident: CliIdent| match ident {
|
|
||||||
CliIdent::Number(n) => Ok(show::Ident::Number(n)),
|
|
||||||
CliIdent::Date(d) => Ok(show::Ident::Date(d.eval((), today)?)),
|
|
||||||
})?;
|
|
||||||
idents.push(ident);
|
|
||||||
}
|
|
||||||
Ok(idents)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_command(opt: &Opt, files: &mut Files, range: DateRange, now: NaiveDateTime) -> Result<()> {
|
fn run_command(opt: &Opt, files: &mut Files, range: DateRange, now: NaiveDateTime) -> Result<()> {
|
||||||
match &opt.command {
|
match &opt.command {
|
||||||
None => {
|
None => {
|
||||||
|
|
@ -160,25 +94,11 @@ fn run_command(opt: &Opt, files: &mut Files, range: DateRange, now: NaiveDateTim
|
||||||
let layout = find_layout(files, &entries, range, now);
|
let layout = find_layout(files, &entries, range, now);
|
||||||
print::print(&layout);
|
print::print(&layout);
|
||||||
}
|
}
|
||||||
Some(Command::Show { identifiers }) => {
|
Some(Command::Show { entries: ns }) => {
|
||||||
let entries = find_entries(files, range)?;
|
let entries = find_entries(files, range)?;
|
||||||
let layout = find_layout(files, &entries, range, now);
|
let layout = find_layout(files, &entries, range, now);
|
||||||
let idents = parse_show_idents(identifiers, now.date())?;
|
show::show(files, &entries, &layout, ns)?;
|
||||||
show::show(files, &entries, &layout, &idents);
|
|
||||||
}
|
}
|
||||||
Some(Command::New { template }) => match template {
|
|
||||||
Template::Task { date: Some(date) } => {
|
|
||||||
let date = parse_eval_date("date", date, now.date())?;
|
|
||||||
new::task(files, Some(date))?
|
|
||||||
}
|
|
||||||
Template::Task { date: None } => new::task(files, None)?,
|
|
||||||
Template::Note { date: Some(date) } => {
|
|
||||||
let date = parse_eval_date("date", date, now.date())?;
|
|
||||||
new::note(files, Some(date))?
|
|
||||||
}
|
|
||||||
Template::Note { date: None } => new::note(files, None)?,
|
|
||||||
Template::Done => new::done(files, now.date())?,
|
|
||||||
},
|
|
||||||
Some(Command::Done { entries: ns }) => {
|
Some(Command::Done { entries: ns }) => {
|
||||||
let entries = find_entries(files, range)?;
|
let entries = find_entries(files, range)?;
|
||||||
let layout = find_layout(files, &entries, range, now);
|
let layout = find_layout(files, &entries, range, now);
|
||||||
|
|
@ -187,55 +107,50 @@ fn run_command(opt: &Opt, files: &mut Files, range: DateRange, now: NaiveDateTim
|
||||||
let layout = find_layout(files, &entries, range, now);
|
let layout = find_layout(files, &entries, range, now);
|
||||||
print::print(&layout);
|
print::print(&layout);
|
||||||
}
|
}
|
||||||
Some(Command::Cancel { entries: ns }) => {
|
|
||||||
let entries = find_entries(files, range)?;
|
|
||||||
let layout = find_layout(files, &entries, range, now);
|
|
||||||
cancel::cancel(files, &entries, &layout, ns, now)?;
|
|
||||||
let entries = find_entries(files, range)?;
|
|
||||||
let layout = find_layout(files, &entries, range, now);
|
|
||||||
print::print(&layout);
|
|
||||||
}
|
|
||||||
Some(Command::Log { date }) => {
|
|
||||||
let date = parse_eval_arg("date", date, |date: CliDate| date.eval((), now.date()))?;
|
|
||||||
log::log(files, date)?
|
|
||||||
}
|
|
||||||
Some(Command::Fmt) => files.mark_all_dirty(),
|
Some(Command::Fmt) => files.mark_all_dirty(),
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_with_files(opt: Opt, files: &mut Files) -> Result<()> {
|
|
||||||
let now = files.now().naive_local();
|
|
||||||
let today = parse_eval_arg("--date", &opt.date, |date: CliDate| {
|
|
||||||
date.eval((), now.date())
|
|
||||||
})?;
|
|
||||||
let now = today.and_time(now.time());
|
|
||||||
|
|
||||||
let range = parse_eval_arg("--range", &opt.range, |range: CliRange| {
|
|
||||||
range.eval((), now.date())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
run_command(&opt, files, range, now)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
let opt = Opt::parse();
|
let opt = Opt::from_args();
|
||||||
|
|
||||||
let mut files = Files::new();
|
let mut files = match load_files(&opt) {
|
||||||
if let Err(e) = load_files(&opt, &mut files) {
|
Ok(result) => result,
|
||||||
crate::error::eprint_error(&files, &e);
|
Err(e) => {
|
||||||
process::exit(1);
|
e.print();
|
||||||
}
|
process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if let Err(e) = run_with_files(opt, &mut files) {
|
let now = find_now(&opt, &files);
|
||||||
crate::error::eprint_error(&files, &e);
|
|
||||||
|
// Kinda ugly, but it can stay for now (until it grows at least).
|
||||||
|
let range = match Range::from_str(&opt.range) {
|
||||||
|
Ok(range) => match range.eval(0, now.date()) {
|
||||||
|
Ok(range) => range,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to evaluate --range:");
|
||||||
|
e.print(&[SourceInfo {
|
||||||
|
name: Some("--range".to_string()),
|
||||||
|
content: &opt.range,
|
||||||
|
}]);
|
||||||
|
process::exit(1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to parse --range:\n{}", e.with_path("--range"));
|
||||||
|
process::exit(1)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = run_command(&opt, &mut files, range, now) {
|
||||||
|
e.print(&files.sources());
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Err(e) = files.save() {
|
if let Err(e) = files.save() {
|
||||||
crate::error::eprint_error(&files, &e);
|
e.print();
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
use std::vec;
|
|
||||||
|
|
||||||
use chrono::NaiveDateTime;
|
|
||||||
|
|
||||||
use crate::eval::Entry;
|
|
||||||
use crate::files::commands::{Done, DoneKind};
|
|
||||||
use crate::files::Files;
|
|
||||||
|
|
||||||
use super::error::{Error, Result};
|
|
||||||
use super::layout::line::LineLayout;
|
|
||||||
|
|
||||||
pub fn cancel(
|
|
||||||
files: &mut Files,
|
|
||||||
entries: &[Entry],
|
|
||||||
layout: &LineLayout,
|
|
||||||
numbers: &[usize],
|
|
||||||
now: NaiveDateTime,
|
|
||||||
) -> Result<()> {
|
|
||||||
let mut not_tasks = vec![];
|
|
||||||
for &number in numbers {
|
|
||||||
let entry = &entries[layout.look_up_number(number)?];
|
|
||||||
let done = Done {
|
|
||||||
kind: DoneKind::Canceled,
|
|
||||||
date: entry.dates.map(|dates| dates.into()),
|
|
||||||
done_at: now.date(),
|
|
||||||
};
|
|
||||||
if !files.add_done(entry.source, done) {
|
|
||||||
not_tasks.push(number);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if not_tasks.is_empty() {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(Error::NotATask(not_tasks))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -3,7 +3,7 @@ use std::vec;
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
|
|
||||||
use crate::eval::Entry;
|
use crate::eval::Entry;
|
||||||
use crate::files::commands::{Done, DoneKind};
|
use crate::files::commands::Done;
|
||||||
use crate::files::Files;
|
use crate::files::Files;
|
||||||
|
|
||||||
use super::error::{Error, Result};
|
use super::error::{Error, Result};
|
||||||
|
|
@ -20,7 +20,6 @@ pub fn done(
|
||||||
for &number in numbers {
|
for &number in numbers {
|
||||||
let entry = &entries[layout.look_up_number(number)?];
|
let entry = &entries[layout.look_up_number(number)?];
|
||||||
let done = Done {
|
let done = Done {
|
||||||
kind: DoneKind::Done,
|
|
||||||
date: entry.dates.map(|dates| dates.into()),
|
date: entry.dates.map(|dates| dates.into()),
|
||||||
done_at: now.date(),
|
done_at: now.date(),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,54 +1,23 @@
|
||||||
use std::{io, result};
|
use std::result;
|
||||||
|
|
||||||
use chrono::NaiveDate;
|
use crate::eval::{self, SourceInfo};
|
||||||
use codespan_reporting::files::{Files, SimpleFile};
|
|
||||||
use codespan_reporting::term::Config;
|
|
||||||
|
|
||||||
use crate::error::Eprint;
|
|
||||||
use crate::files::FileSource;
|
|
||||||
use crate::{eval, files};
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("{0}")]
|
#[error("{0}")]
|
||||||
Eval(#[from] eval::Error<FileSource>),
|
Eval(#[from] eval::Error),
|
||||||
#[error("{error}")]
|
|
||||||
ArgumentParse {
|
|
||||||
file: SimpleFile<String, String>,
|
|
||||||
error: files::ParseError<()>,
|
|
||||||
},
|
|
||||||
#[error("{error}")]
|
|
||||||
ArgumentEval {
|
|
||||||
file: SimpleFile<String, String>,
|
|
||||||
error: eval::Error<()>,
|
|
||||||
},
|
|
||||||
#[error("No entry with number {0}")]
|
#[error("No entry with number {0}")]
|
||||||
NoSuchEntry(usize),
|
NoSuchEntry(usize),
|
||||||
#[error("No log for {0}")]
|
|
||||||
NoSuchLog(NaiveDate),
|
|
||||||
#[error("Not a task")]
|
#[error("Not a task")]
|
||||||
NotATask(Vec<usize>),
|
NotATask(Vec<usize>),
|
||||||
#[error("No capture file found")]
|
|
||||||
NoCaptureFile,
|
|
||||||
#[error("Error editing: {0}")]
|
|
||||||
EditingIo(io::Error),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Result<T> = result::Result<T, Error>;
|
impl Error {
|
||||||
|
pub fn print<'a>(&self, sources: &[SourceInfo<'a>]) {
|
||||||
impl<'a, F> Eprint<'a, F> for Error
|
|
||||||
where
|
|
||||||
F: Files<'a, FileId = FileSource>,
|
|
||||||
{
|
|
||||||
#[allow(single_use_lifetimes)]
|
|
||||||
fn eprint<'f: 'a>(&self, files: &'f F, config: &Config) {
|
|
||||||
match self {
|
match self {
|
||||||
Self::Eval(e) => e.eprint(files, config),
|
Error::Eval(e) => e.print(sources),
|
||||||
Self::ArgumentParse { file, error } => error.eprint(file, config),
|
Error::NoSuchEntry(n) => eprintln!("No entry with number {}", n),
|
||||||
Self::ArgumentEval { file, error } => error.eprint(file, config),
|
Error::NotATask(ns) => {
|
||||||
Self::NoSuchEntry(n) => eprintln!("No entry with number {n}"),
|
|
||||||
Self::NoSuchLog(date) => eprintln!("No log for {date}"),
|
|
||||||
Self::NotATask(ns) => {
|
|
||||||
if ns.is_empty() {
|
if ns.is_empty() {
|
||||||
eprintln!("Not a task.");
|
eprintln!("Not a task.");
|
||||||
} else if ns.len() == 1 {
|
} else if ns.len() == 1 {
|
||||||
|
|
@ -58,11 +27,8 @@ where
|
||||||
eprintln!("{} are not tasks.", ns.join(", "));
|
eprintln!("{} are not tasks.", ns.join(", "));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Self::NoCaptureFile => eprintln!("No capture file found"),
|
|
||||||
Self::EditingIo(error) => {
|
|
||||||
eprintln!("Error while editing:");
|
|
||||||
eprintln!(" {error}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub type Result<T> = result::Result<T, Error>;
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ pub fn layout(
|
||||||
now: NaiveDateTime,
|
now: NaiveDateTime,
|
||||||
) -> LineLayout {
|
) -> LineLayout {
|
||||||
let mut day_layout = DayLayout::new(range, now);
|
let mut day_layout = DayLayout::new(range, now);
|
||||||
day_layout.layout(entries);
|
day_layout.layout(files, entries);
|
||||||
|
|
||||||
let mut line_layout = LineLayout::new();
|
let mut line_layout = LineLayout::new();
|
||||||
line_layout.render(files, entries, &day_layout);
|
line_layout.render(files, entries, &day_layout);
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,9 @@ use std::collections::HashMap;
|
||||||
use chrono::{NaiveDate, NaiveDateTime};
|
use chrono::{NaiveDate, NaiveDateTime};
|
||||||
|
|
||||||
use crate::eval::{DateRange, Dates, Entry, EntryKind};
|
use crate::eval::{DateRange, Dates, Entry, EntryKind};
|
||||||
|
use crate::files::commands::Command;
|
||||||
use crate::files::primitives::Time;
|
use crate::files::primitives::Time;
|
||||||
|
use crate::files::Files;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum DayEntry {
|
pub enum DayEntry {
|
||||||
|
|
@ -46,13 +48,18 @@ impl DayLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn layout(&mut self, entries: &[Entry]) {
|
pub fn layout(&mut self, files: &Files, entries: &[Entry]) {
|
||||||
self.insert(self.today, DayEntry::Now(self.time));
|
self.insert(self.today, DayEntry::Now(self.time));
|
||||||
|
|
||||||
let mut entries = entries.iter().enumerate().collect::<Vec<_>>();
|
let mut commands = entries
|
||||||
Self::sort_entries(&mut entries);
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, e)| (i, e, files.command(e.source)))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
for (index, entry) in entries {
|
Self::sort_entries(&mut commands);
|
||||||
|
|
||||||
|
for (index, entry, _) in commands {
|
||||||
self.layout_entry(index, entry);
|
self.layout_entry(index, entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -66,9 +73,7 @@ impl DayLayout {
|
||||||
fn layout_entry(&mut self, index: usize, entry: &Entry) {
|
fn layout_entry(&mut self, index: usize, entry: &Entry) {
|
||||||
match entry.kind {
|
match entry.kind {
|
||||||
EntryKind::Task => self.layout_task(index, entry),
|
EntryKind::Task => self.layout_task(index, entry),
|
||||||
EntryKind::TaskDone(at) | EntryKind::TaskCanceled(at) => {
|
EntryKind::TaskDone(at) => self.layout_task_done(index, entry, at),
|
||||||
self.layout_task_done(index, entry, at)
|
|
||||||
}
|
|
||||||
EntryKind::Note | EntryKind::Birthday(_) => self.layout_note(index, entry),
|
EntryKind::Note | EntryKind::Birthday(_) => self.layout_note(index, entry),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -76,17 +81,10 @@ impl DayLayout {
|
||||||
fn layout_task(&mut self, index: usize, entry: &Entry) {
|
fn layout_task(&mut self, index: usize, entry: &Entry) {
|
||||||
if let Some(dates) = entry.dates {
|
if let Some(dates) = entry.dates {
|
||||||
let (start, end) = dates.sorted().dates();
|
let (start, end) = dates.sorted().dates();
|
||||||
if self.today < self.range.from() || self.range.until() < self.today {
|
if self.today < start && (start - self.today).num_days() < 7 {
|
||||||
// If `self.today` is not in range, reminders won't be displayed
|
// TODO Make this adjustable, maybe even per-command
|
||||||
// (since they're always displayed on `self.today`) so there's
|
let days = (start - self.today).num_days();
|
||||||
// no need to calculate them.
|
self.insert(self.today, DayEntry::ReminderUntil(index, days));
|
||||||
} else if self.today < start {
|
|
||||||
if let Some(remind) = entry.remind {
|
|
||||||
if remind <= self.today {
|
|
||||||
let days = (start - self.today).num_days();
|
|
||||||
self.insert(self.today, DayEntry::ReminderUntil(index, days));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if start < self.today && self.today < end {
|
} else if start < self.today && self.today < end {
|
||||||
let days = (end - self.today).num_days();
|
let days = (end - self.today).num_days();
|
||||||
self.insert(self.today, DayEntry::ReminderWhile(index, days));
|
self.insert(self.today, DayEntry::ReminderWhile(index, days));
|
||||||
|
|
@ -117,18 +115,7 @@ impl DayLayout {
|
||||||
fn layout_note(&mut self, index: usize, entry: &Entry) {
|
fn layout_note(&mut self, index: usize, entry: &Entry) {
|
||||||
if let Some(dates) = entry.dates {
|
if let Some(dates) = entry.dates {
|
||||||
let (start, end) = dates.sorted().dates();
|
let (start, end) = dates.sorted().dates();
|
||||||
if self.today < self.range.from() || self.range.until() < self.today {
|
if start < self.range.from() && self.range.until() < end {
|
||||||
// if `self.today` is not in range, reminders won't be displayed
|
|
||||||
// (since they're always displayed on `self.today`) so there's
|
|
||||||
// no need to calculate them.
|
|
||||||
} else if self.today < start {
|
|
||||||
if let Some(remind) = entry.remind {
|
|
||||||
if remind <= self.today {
|
|
||||||
let days = (start - self.today).num_days();
|
|
||||||
self.insert(self.today, DayEntry::ReminderUntil(index, days));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if start < self.range.from() && self.range.until() < end {
|
|
||||||
// This note applies to the current day, but it won't appear if
|
// This note applies to the current day, but it won't appear if
|
||||||
// we just layout it as a dated entry, so instead we add it as a
|
// we just layout it as a dated entry, so instead we add it as a
|
||||||
// reminder. Since we are usually more interested in when
|
// reminder. Since we are usually more interested in when
|
||||||
|
|
@ -136,8 +123,9 @@ impl DayLayout {
|
||||||
// the end.
|
// the end.
|
||||||
let days = (end - self.today).num_days();
|
let days = (end - self.today).num_days();
|
||||||
self.insert(self.today, DayEntry::ReminderWhile(index, days));
|
self.insert(self.today, DayEntry::ReminderWhile(index, days));
|
||||||
|
} else {
|
||||||
|
self.layout_dated_entry(index, dates);
|
||||||
}
|
}
|
||||||
self.layout_dated_entry(index, dates);
|
|
||||||
} else {
|
} else {
|
||||||
self.insert(self.today, DayEntry::Undated(index));
|
self.insert(self.today, DayEntry::Undated(index));
|
||||||
}
|
}
|
||||||
|
|
@ -189,7 +177,7 @@ impl DayLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sort_entries(entries: &mut [(usize, &Entry)]) {
|
fn sort_entries(entries: &mut Vec<(usize, &Entry, &Command)>) {
|
||||||
// Entries should be sorted by these factors, in descending order of
|
// Entries should be sorted by these factors, in descending order of
|
||||||
// significance:
|
// significance:
|
||||||
// 1. Their start date, if any
|
// 1. Their start date, if any
|
||||||
|
|
@ -198,28 +186,28 @@ impl DayLayout {
|
||||||
// 4. Their title
|
// 4. Their title
|
||||||
|
|
||||||
// 4.
|
// 4.
|
||||||
entries.sort_by_key(|(_, e)| &e.title);
|
entries.sort_by_key(|(_, _, c)| c.title());
|
||||||
|
|
||||||
// 3.
|
// 3.
|
||||||
entries.sort_by_key(|(_, e)| match e.kind {
|
entries.sort_by_key(|(_, e, _)| match e.kind {
|
||||||
EntryKind::Task => 0,
|
EntryKind::Task => 0,
|
||||||
EntryKind::TaskDone(_) | EntryKind::TaskCanceled(_) => 1,
|
EntryKind::TaskDone(_) => 1,
|
||||||
EntryKind::Birthday(_) => 2,
|
EntryKind::Birthday(_) => 2,
|
||||||
EntryKind::Note => 3,
|
EntryKind::Note => 3,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2.
|
// 2.
|
||||||
entries.sort_by(|(_, e1), (_, e2)| {
|
entries.sort_by(|(_, e1, _), (_, e2, _)| {
|
||||||
let d1 = e1.dates.map(|d| d.sorted().other_with_time());
|
let d1 = e1.dates.map(|d| d.sorted().other_with_time());
|
||||||
let d2 = e2.dates.map(|d| d.sorted().other_with_time());
|
let d2 = e2.dates.map(|d| d.sorted().other_with_time());
|
||||||
d2.cmp(&d1) // Inverse comparison
|
d2.cmp(&d1) // Inverse comparison
|
||||||
});
|
});
|
||||||
|
|
||||||
// 1.
|
// 1.
|
||||||
entries.sort_by_key(|(_, e)| e.dates.map(|d| d.sorted().root_with_time()));
|
entries.sort_by_key(|(_, e, _)| e.dates.map(|d| d.sorted().root_with_time()));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sort_day(day: &mut [DayEntry]) {
|
fn sort_day(day: &mut Vec<DayEntry>) {
|
||||||
// In a day, entries should be sorted into these categories:
|
// In a day, entries should be sorted into these categories:
|
||||||
// 1. Untimed entries that end at the current day
|
// 1. Untimed entries that end at the current day
|
||||||
// 2. Timed entries, based on
|
// 2. Timed entries, based on
|
||||||
|
|
|
||||||
|
|
@ -11,44 +11,14 @@ use crate::eval::{Entry, EntryKind};
|
||||||
use crate::files::primitives::Time;
|
use crate::files::primitives::Time;
|
||||||
use crate::files::Files;
|
use crate::files::Files;
|
||||||
|
|
||||||
use super::super::error::Error;
|
use super::super::error::{Error, Result};
|
||||||
use super::day::{DayEntry, DayLayout};
|
use super::day::{DayEntry, DayLayout};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub enum SpanStyle {
|
|
||||||
Solid,
|
|
||||||
Dashed,
|
|
||||||
Dotted,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SpanStyle {
|
|
||||||
fn from_indentation(index: usize) -> Self {
|
|
||||||
match index % 3 {
|
|
||||||
0 => Self::Solid,
|
|
||||||
1 => Self::Dashed,
|
|
||||||
2 => Self::Dotted,
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub enum SpanSegment {
|
pub enum SpanSegment {
|
||||||
Start(SpanStyle),
|
Start,
|
||||||
Middle(SpanStyle),
|
Middle,
|
||||||
Mark(SpanStyle),
|
End,
|
||||||
End(SpanStyle),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SpanSegment {
|
|
||||||
fn style(&self) -> SpanStyle {
|
|
||||||
match self {
|
|
||||||
Self::Start(s) => *s,
|
|
||||||
Self::Middle(s) => *s,
|
|
||||||
Self::Mark(s) => *s,
|
|
||||||
Self::End(s) => *s,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
|
@ -58,21 +28,10 @@ pub enum Times {
|
||||||
FromTo(Time, Time),
|
FromTo(Time, Time),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum LineKind {
|
|
||||||
Task,
|
|
||||||
Done,
|
|
||||||
Canceled,
|
|
||||||
Note,
|
|
||||||
Birthday,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum LineEntry {
|
pub enum LineEntry {
|
||||||
Day {
|
Day {
|
||||||
spans: Vec<Option<SpanSegment>>,
|
spans: Vec<Option<SpanSegment>>,
|
||||||
date: NaiveDate,
|
date: NaiveDate,
|
||||||
today: bool,
|
|
||||||
has_log: bool,
|
|
||||||
},
|
},
|
||||||
Now {
|
Now {
|
||||||
spans: Vec<Option<SpanSegment>>,
|
spans: Vec<Option<SpanSegment>>,
|
||||||
|
|
@ -81,12 +40,8 @@ pub enum LineEntry {
|
||||||
Entry {
|
Entry {
|
||||||
number: Option<usize>,
|
number: Option<usize>,
|
||||||
spans: Vec<Option<SpanSegment>>,
|
spans: Vec<Option<SpanSegment>>,
|
||||||
today: bool,
|
|
||||||
time: Times,
|
time: Times,
|
||||||
kind: LineKind,
|
|
||||||
text: String,
|
text: String,
|
||||||
has_desc: bool,
|
|
||||||
extra: Option<String>,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -124,18 +79,12 @@ impl LineLayout {
|
||||||
self.step_spans();
|
self.step_spans();
|
||||||
|
|
||||||
for day in layout.range.days() {
|
for day in layout.range.days() {
|
||||||
let today = day == layout.today;
|
|
||||||
let spans = self.spans_for_line();
|
let spans = self.spans_for_line();
|
||||||
self.line(LineEntry::Day {
|
self.line(LineEntry::Day { spans, date: day });
|
||||||
spans,
|
|
||||||
date: day,
|
|
||||||
today,
|
|
||||||
has_log: files.log(day).is_some(),
|
|
||||||
});
|
|
||||||
|
|
||||||
let layout_entries = layout.days.get(&day).expect("got nonexisting day");
|
let layout_entries = layout.days.get(&day).expect("got nonexisting day");
|
||||||
for layout_entry in layout_entries {
|
for layout_entry in layout_entries {
|
||||||
self.render_layout_entry(entries, layout_entry, today);
|
self.render_layout_entry(files, entries, layout_entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -152,7 +101,7 @@ impl LineLayout {
|
||||||
&self.lines
|
&self.lines
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn look_up_number(&self, number: usize) -> Result<usize, Error> {
|
pub fn look_up_number(&self, number: usize) -> Result<usize> {
|
||||||
self.numbers
|
self.numbers
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(_, n)| **n == number)
|
.filter(|(_, n)| **n == number)
|
||||||
|
|
@ -161,11 +110,12 @@ impl LineLayout {
|
||||||
.ok_or(Error::NoSuchEntry(number))
|
.ok_or(Error::NoSuchEntry(number))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_layout_entry(&mut self, entries: &[Entry], l_entry: &DayEntry, today: bool) {
|
fn render_layout_entry(&mut self, files: &Files, entries: &[Entry], l_entry: &DayEntry) {
|
||||||
match l_entry {
|
match l_entry {
|
||||||
DayEntry::End(i) => {
|
DayEntry::End(i) => {
|
||||||
self.stop_span(*i);
|
self.stop_span(*i);
|
||||||
self.line_entry(entries, *i, today, Times::Untimed, None);
|
let text = Self::format_entry(files, entries, *i);
|
||||||
|
self.line_entry(Some(*i), Times::Untimed, text);
|
||||||
}
|
}
|
||||||
DayEntry::Now(t) => self.line(LineEntry::Now {
|
DayEntry::Now(t) => self.line(LineEntry::Now {
|
||||||
spans: self.spans_for_line(),
|
spans: self.spans_for_line(),
|
||||||
|
|
@ -173,95 +123,89 @@ impl LineLayout {
|
||||||
}),
|
}),
|
||||||
DayEntry::TimedEnd(i, t) => {
|
DayEntry::TimedEnd(i, t) => {
|
||||||
self.stop_span(*i);
|
self.stop_span(*i);
|
||||||
self.line_entry(entries, *i, today, Times::At(*t), None);
|
let text = Self::format_entry(files, entries, *i);
|
||||||
|
self.line_entry(Some(*i), Times::At(*t), text);
|
||||||
}
|
}
|
||||||
DayEntry::TimedAt(i, t, t2) => {
|
DayEntry::TimedAt(i, t, t2) => {
|
||||||
let time = t2.map(|t2| Times::FromTo(*t, t2)).unwrap_or(Times::At(*t));
|
let time = t2
|
||||||
self.line_entry(entries, *i, today, time, None);
|
.map(|t2| Times::FromTo(*t, t2))
|
||||||
|
.unwrap_or_else(|| Times::At(*t));
|
||||||
|
let text = Self::format_entry(files, entries, *i);
|
||||||
|
self.line_entry(Some(*i), time, text);
|
||||||
}
|
}
|
||||||
DayEntry::TimedStart(i, t) => {
|
DayEntry::TimedStart(i, t) => {
|
||||||
self.start_span(*i);
|
self.start_span(*i);
|
||||||
self.line_entry(entries, *i, today, Times::At(*t), None);
|
let text = Self::format_entry(files, entries, *i);
|
||||||
|
self.line_entry(Some(*i), Times::At(*t), text);
|
||||||
}
|
}
|
||||||
DayEntry::ReminderSince(i, d) => {
|
DayEntry::ReminderSince(i, d) => {
|
||||||
let extra = if *d == 1 {
|
let text = Self::format_entry(files, entries, *i);
|
||||||
"yesterday".to_string()
|
let text = if *d == 1 {
|
||||||
|
format!("{} (yesterday)", text)
|
||||||
} else {
|
} else {
|
||||||
format!("{d} days ago")
|
format!("{} ({} days ago)", text, d)
|
||||||
};
|
};
|
||||||
self.line_entry(entries, *i, today, Times::Untimed, Some(extra));
|
self.line_entry(Some(*i), Times::Untimed, text);
|
||||||
}
|
}
|
||||||
DayEntry::At(i) => {
|
DayEntry::At(i) => {
|
||||||
self.line_entry(entries, *i, today, Times::Untimed, None);
|
let text = Self::format_entry(files, entries, *i);
|
||||||
|
self.line_entry(Some(*i), Times::Untimed, text);
|
||||||
}
|
}
|
||||||
DayEntry::ReminderWhile(i, d) => {
|
DayEntry::ReminderWhile(i, d) => {
|
||||||
|
let text = Self::format_entry(files, entries, *i);
|
||||||
let plural = if *d == 1 { "" } else { "s" };
|
let plural = if *d == 1 { "" } else { "s" };
|
||||||
let extra = format!("{d} day{plural} left");
|
let text = format!("{} ({} day{} left)", text, i, plural);
|
||||||
self.mark_span(*i);
|
self.line_entry(Some(*i), Times::Untimed, text);
|
||||||
self.line_entry(entries, *i, today, Times::Untimed, Some(extra));
|
|
||||||
}
|
}
|
||||||
DayEntry::Undated(i) => {
|
DayEntry::Undated(i) => {
|
||||||
self.line_entry(entries, *i, today, Times::Untimed, None);
|
let text = Self::format_entry(files, entries, *i);
|
||||||
|
self.line_entry(Some(*i), Times::Untimed, text);
|
||||||
}
|
}
|
||||||
DayEntry::Start(i) => {
|
DayEntry::Start(i) => {
|
||||||
self.start_span(*i);
|
self.start_span(*i);
|
||||||
self.line_entry(entries, *i, today, Times::Untimed, None);
|
let text = Self::format_entry(files, entries, *i);
|
||||||
|
self.line_entry(Some(*i), Times::Untimed, text);
|
||||||
}
|
}
|
||||||
DayEntry::ReminderUntil(i, d) => {
|
DayEntry::ReminderUntil(i, d) => {
|
||||||
let extra = if *d == 1 {
|
let text = Self::format_entry(files, entries, *i);
|
||||||
"tomorrow".to_string()
|
let text = if *d == 1 {
|
||||||
|
format!("{} (tomorrow)", text)
|
||||||
} else {
|
} else {
|
||||||
format!("in {d} days")
|
format!("{} (in {} days)", text, d)
|
||||||
};
|
};
|
||||||
self.line_entry(entries, *i, today, Times::Untimed, Some(extra));
|
self.line_entry(Some(*i), Times::Untimed, text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn entry_kind(entry: &Entry) -> LineKind {
|
fn format_entry(files: &Files, entries: &[Entry], index: usize) -> String {
|
||||||
|
let entry = entries[index];
|
||||||
|
let command = files.command(entry.source);
|
||||||
match entry.kind {
|
match entry.kind {
|
||||||
EntryKind::Task => LineKind::Task,
|
EntryKind::Task => format!("T {}", command.title()),
|
||||||
EntryKind::TaskDone(_) => LineKind::Done,
|
EntryKind::TaskDone(_) => format!("D {}", command.title()),
|
||||||
EntryKind::TaskCanceled(_) => LineKind::Canceled,
|
EntryKind::Note => format!("N {}", command.title()),
|
||||||
EntryKind::Note => LineKind::Note,
|
EntryKind::Birthday(Some(age)) => format!("B {} ({})", command.title(), age),
|
||||||
EntryKind::Birthday(_) => LineKind::Birthday,
|
EntryKind::Birthday(None) => format!("B {}", command.title()),
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn entry_title(entry: &Entry) -> String {
|
|
||||||
match entry.kind {
|
|
||||||
EntryKind::Birthday(Some(age)) => format!("{} ({})", entry.title, age),
|
|
||||||
_ => entry.title.clone(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start_span(&mut self, index: usize) {
|
fn start_span(&mut self, index: usize) {
|
||||||
for (i, span) in self.spans.iter_mut().enumerate() {
|
for span in self.spans.iter_mut() {
|
||||||
if span.is_none() {
|
if span.is_none() {
|
||||||
let style = SpanStyle::from_indentation(i);
|
*span = Some((index, SpanSegment::Start));
|
||||||
*span = Some((index, SpanSegment::Start(style)));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not enough space, we need another column
|
// Not enough space, we need another column
|
||||||
let style = SpanStyle::from_indentation(self.spans.len());
|
self.spans.push(Some((index, SpanSegment::Start)));
|
||||||
self.spans.push(Some((index, SpanSegment::Start(style))));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mark_span(&mut self, index: usize) {
|
|
||||||
for span in self.spans.iter_mut() {
|
|
||||||
match span {
|
|
||||||
Some((i, s)) if *i == index => *s = SpanSegment::Mark(s.style()),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stop_span(&mut self, index: usize) {
|
fn stop_span(&mut self, index: usize) {
|
||||||
for span in self.spans.iter_mut() {
|
for span in self.spans.iter_mut() {
|
||||||
match span {
|
match span {
|
||||||
Some((i, s)) if *i == index => *s = SpanSegment::End(s.style()),
|
Some((i, s)) if *i == index => *s = SpanSegment::End,
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -270,10 +214,8 @@ impl LineLayout {
|
||||||
fn step_spans(&mut self) {
|
fn step_spans(&mut self) {
|
||||||
for span in self.spans.iter_mut() {
|
for span in self.spans.iter_mut() {
|
||||||
match span {
|
match span {
|
||||||
Some((_, s @ (SpanSegment::Start(_) | SpanSegment::Mark(_)))) => {
|
Some((_, s @ SpanSegment::Start)) => *s = SpanSegment::Middle,
|
||||||
*s = SpanSegment::Middle(s.style())
|
Some((_, SpanSegment::End)) => *span = None,
|
||||||
}
|
|
||||||
Some((_, SpanSegment::End(_))) => *span = None,
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -291,34 +233,24 @@ impl LineLayout {
|
||||||
self.step_spans();
|
self.step_spans();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn line_entry(
|
fn line_entry(&mut self, index: Option<usize>, time: Times, text: String) {
|
||||||
&mut self,
|
let number = match index {
|
||||||
entries: &[Entry],
|
Some(index) => Some(match self.numbers.get(&index) {
|
||||||
index: usize,
|
Some(number) => *number,
|
||||||
today: bool,
|
None => {
|
||||||
time: Times,
|
self.last_number += 1;
|
||||||
extra: Option<String>,
|
self.numbers.insert(index, self.last_number);
|
||||||
) {
|
self.last_number
|
||||||
let entry = &entries[index];
|
}
|
||||||
|
}),
|
||||||
let number = match self.numbers.get(&index) {
|
None => None,
|
||||||
Some(number) => *number,
|
|
||||||
None => {
|
|
||||||
self.last_number += 1;
|
|
||||||
self.numbers.insert(index, self.last_number);
|
|
||||||
self.last_number
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
self.line(LineEntry::Entry {
|
self.line(LineEntry::Entry {
|
||||||
number: Some(number),
|
number,
|
||||||
spans: self.spans_for_line(),
|
spans: self.spans_for_line(),
|
||||||
today,
|
|
||||||
time,
|
time,
|
||||||
kind: Self::entry_kind(entry),
|
text,
|
||||||
text: Self::entry_title(entry),
|
|
||||||
has_desc: entry.has_description,
|
|
||||||
extra,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
use chrono::NaiveDate;
|
|
||||||
|
|
||||||
use crate::files::Files;
|
|
||||||
|
|
||||||
use super::error::Error;
|
|
||||||
use super::util;
|
|
||||||
|
|
||||||
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 edited = util::edit_with_suffix(&desc, ".md")?;
|
|
||||||
|
|
||||||
let edited = edited
|
|
||||||
.lines()
|
|
||||||
.map(|line| line.to_string())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
files.set_log(date, edited);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
114
src/cli/new.rs
114
src/cli/new.rs
|
|
@ -1,114 +0,0 @@
|
||||||
use std::result;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use chrono::NaiveDate;
|
|
||||||
use codespan_reporting::files::SimpleFile;
|
|
||||||
|
|
||||||
use crate::files::cli::CliCommand;
|
|
||||||
use crate::files::commands::{Command, DateSpec, Done, DoneKind, Note, Spec, Statement, Task};
|
|
||||||
use crate::files::{Files, ParseError};
|
|
||||||
|
|
||||||
use super::error::{Error, Result};
|
|
||||||
use super::util;
|
|
||||||
|
|
||||||
fn edit<R, F>(name: &str, mut text: String, validate: F) -> Result<Option<R>>
|
|
||||||
where
|
|
||||||
R: FromStr<Err = ParseError<()>>,
|
|
||||||
F: Fn(&R) -> result::Result<(), &str>,
|
|
||||||
{
|
|
||||||
Ok(loop {
|
|
||||||
text = util::edit(&text)?;
|
|
||||||
match text.parse() {
|
|
||||||
Ok(command) => match validate(&command) {
|
|
||||||
Ok(()) => break Some(command),
|
|
||||||
Err(msg) => eprintln!("{msg}"),
|
|
||||||
},
|
|
||||||
Err(e) => crate::error::eprint_error(&SimpleFile::new(name, &text), &e),
|
|
||||||
}
|
|
||||||
if !matches!(
|
|
||||||
promptly::prompt_default("Continue editing?", true),
|
|
||||||
Ok(true)
|
|
||||||
) {
|
|
||||||
println!("Aborting");
|
|
||||||
break None;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_task_or_note(command: &CliCommand) -> result::Result<(), &str> {
|
|
||||||
match command.0 {
|
|
||||||
Command::Task(_) | Command::Note(_) => Ok(()),
|
|
||||||
_ => Err("Only TASK and NOTE are allowed"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn new_command(files: &mut Files, command: Command) -> Result<()> {
|
|
||||||
let capture = files.capture().ok_or(Error::NoCaptureFile)?;
|
|
||||||
|
|
||||||
let command = edit("new command", format!("{command}"), is_task_or_note)?;
|
|
||||||
if let Some(command) = command {
|
|
||||||
files.insert(capture, command.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn task(files: &mut Files, date: Option<NaiveDate>) -> Result<()> {
|
|
||||||
let statements = match date {
|
|
||||||
Some(date) => vec![Statement::Date(Spec::Date(DateSpec {
|
|
||||||
start: date,
|
|
||||||
start_delta: None,
|
|
||||||
start_time: None,
|
|
||||||
end: None,
|
|
||||||
end_delta: None,
|
|
||||||
end_time: None,
|
|
||||||
repeat: None,
|
|
||||||
}))],
|
|
||||||
None => vec![],
|
|
||||||
};
|
|
||||||
let command = Command::Task(Task {
|
|
||||||
title: String::new(),
|
|
||||||
statements,
|
|
||||||
done: vec![],
|
|
||||||
desc: vec![],
|
|
||||||
});
|
|
||||||
|
|
||||||
new_command(files, command)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn note(files: &mut Files, date: Option<NaiveDate>) -> Result<()> {
|
|
||||||
let statements = match date {
|
|
||||||
Some(date) => vec![Statement::Date(Spec::Date(DateSpec {
|
|
||||||
start: date,
|
|
||||||
start_delta: None,
|
|
||||||
start_time: None,
|
|
||||||
end: None,
|
|
||||||
end_delta: None,
|
|
||||||
end_time: None,
|
|
||||||
repeat: None,
|
|
||||||
}))],
|
|
||||||
None => vec![],
|
|
||||||
};
|
|
||||||
let command = Command::Note(Note {
|
|
||||||
title: String::new(),
|
|
||||||
statements,
|
|
||||||
desc: vec![],
|
|
||||||
});
|
|
||||||
|
|
||||||
new_command(files, command)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn done(files: &mut Files, date: NaiveDate) -> Result<()> {
|
|
||||||
let command = Command::Task(Task {
|
|
||||||
title: String::new(),
|
|
||||||
statements: vec![],
|
|
||||||
done: vec![Done {
|
|
||||||
kind: DoneKind::Done,
|
|
||||||
date: None,
|
|
||||||
done_at: date,
|
|
||||||
}],
|
|
||||||
desc: vec![],
|
|
||||||
});
|
|
||||||
|
|
||||||
new_command(files, command)
|
|
||||||
}
|
|
||||||
144
src/cli/print.rs
144
src/cli/print.rs
|
|
@ -1,12 +1,10 @@
|
||||||
use std::cmp;
|
use std::cmp;
|
||||||
|
|
||||||
use chrono::{Datelike, NaiveDate};
|
use chrono::{Datelike, NaiveDate};
|
||||||
use colored::{ColoredString, Colorize};
|
|
||||||
|
|
||||||
use crate::files::primitives::{Time, Weekday};
|
use crate::files::primitives::{Time, Weekday};
|
||||||
|
|
||||||
use super::layout::line::{LineEntry, LineKind, LineLayout, SpanSegment, SpanStyle, Times};
|
use super::layout::line::{LineEntry, LineLayout, SpanSegment, Times};
|
||||||
use super::util;
|
|
||||||
|
|
||||||
struct ShowLines {
|
struct ShowLines {
|
||||||
num_width: usize,
|
num_width: usize,
|
||||||
|
|
@ -25,156 +23,86 @@ impl ShowLines {
|
||||||
|
|
||||||
fn display_line(&mut self, line: &LineEntry) {
|
fn display_line(&mut self, line: &LineEntry) {
|
||||||
match line {
|
match line {
|
||||||
LineEntry::Day {
|
LineEntry::Day { spans, date } => self.display_line_date(spans, *date),
|
||||||
spans,
|
|
||||||
date,
|
|
||||||
today,
|
|
||||||
has_log,
|
|
||||||
} => self.display_line_date(spans, *date, *today, *has_log),
|
|
||||||
LineEntry::Now { spans, time } => self.display_line_now(spans, *time),
|
LineEntry::Now { spans, time } => self.display_line_now(spans, *time),
|
||||||
LineEntry::Entry {
|
LineEntry::Entry {
|
||||||
number,
|
number,
|
||||||
spans,
|
spans,
|
||||||
today,
|
|
||||||
time,
|
time,
|
||||||
kind,
|
|
||||||
text,
|
text,
|
||||||
has_desc,
|
} => self.display_line_entry(*number, spans, *time, text),
|
||||||
extra,
|
|
||||||
} => self
|
|
||||||
.display_line_entry(*number, spans, *today, *time, *kind, text, *has_desc, extra),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn display_line_date(
|
fn display_line_date(&mut self, spans: &[Option<SpanSegment>], date: NaiveDate) {
|
||||||
&mut self,
|
|
||||||
spans: &[Option<SpanSegment>],
|
|
||||||
date: NaiveDate,
|
|
||||||
today: bool,
|
|
||||||
has_log: bool,
|
|
||||||
) {
|
|
||||||
let weekday: Weekday = date.weekday().into();
|
let weekday: Weekday = date.weekday().into();
|
||||||
let weekday = weekday.full_name();
|
let weekday = weekday.full_name();
|
||||||
|
self.push(&format!(
|
||||||
let styled = |s: &str| {
|
"{:=>nw$}={:=<sw$}=== {:9} {} ==={:=<sw$}={:=>nw$}\n",
|
||||||
if today {
|
|
||||||
s.bright_cyan().bold()
|
|
||||||
} else {
|
|
||||||
s.cyan()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// '=' symbols before the spans start
|
|
||||||
let p1 = styled(&format!("{:=<w$}=", "", w = self.num_width));
|
|
||||||
|
|
||||||
// Spans and filler '=' symbols
|
|
||||||
let p2 = self.display_spans(spans, styled("="));
|
|
||||||
|
|
||||||
// The rest of the line until after the date
|
|
||||||
let p3 = styled(&format!("=== {weekday:9} {date}"));
|
|
||||||
|
|
||||||
// The "has log" marker (if any)
|
|
||||||
let p4 = Self::display_marker(has_log, " ");
|
|
||||||
|
|
||||||
// The rest of the line
|
|
||||||
let p5 = styled(&format!(
|
|
||||||
" ===={:=<w$}",
|
|
||||||
"",
|
"",
|
||||||
w = self.num_width + self.span_width
|
Self::display_spans(spans, '='),
|
||||||
|
weekday,
|
||||||
|
date,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
nw = self.num_width,
|
||||||
|
sw = self.span_width
|
||||||
));
|
));
|
||||||
|
|
||||||
self.push(&format!("{p1}{p2}{p3}{p4}{p5}\n"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn display_line_now(&mut self, spans: &[Option<SpanSegment>], time: Time) {
|
fn display_line_now(&mut self, spans: &[Option<SpanSegment>], time: Time) {
|
||||||
self.push(&format!(
|
self.push(&format!(
|
||||||
"{:>nw$} {} {}\n",
|
"{:<nw$} {:sw$} {}\n",
|
||||||
"now".bright_cyan().bold(),
|
"now",
|
||||||
self.display_spans(spans, " ".into()),
|
Self::display_spans(spans, ' '),
|
||||||
Self::display_time(Times::At(time)),
|
time,
|
||||||
nw = self.num_width,
|
nw = self.num_width,
|
||||||
|
sw = self.span_width
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn display_line_entry(
|
fn display_line_entry(
|
||||||
&mut self,
|
&mut self,
|
||||||
number: Option<usize>,
|
number: Option<usize>,
|
||||||
spans: &[Option<SpanSegment>],
|
spans: &[Option<SpanSegment>],
|
||||||
today: bool,
|
|
||||||
time: Times,
|
time: Times,
|
||||||
kind: LineKind,
|
|
||||||
text: &str,
|
text: &str,
|
||||||
has_desc: bool,
|
|
||||||
extra: &Option<String>,
|
|
||||||
) {
|
) {
|
||||||
let num = match number {
|
let num = match number {
|
||||||
Some(n) => format!("{n}"),
|
Some(n) => format!("{}", n),
|
||||||
None => "".to_string(),
|
None => "".to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let text = if kind == LineKind::Birthday && today {
|
let time = match time {
|
||||||
util::display_current_birthday_text(text)
|
Times::Untimed => "".to_string(),
|
||||||
} else {
|
Times::At(t) => format!("{} ", t),
|
||||||
text.into()
|
Times::FromTo(t1, t2) => format!("{}--{} ", t1, t2),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.push(&format!(
|
self.push(&format!(
|
||||||
"{:>nw$} {} {}{} {}{}{}\n",
|
"{:>nw$} {:sw$} {}{}\n",
|
||||||
num.bright_black(),
|
num,
|
||||||
self.display_spans(spans, " ".into()),
|
Self::display_spans(spans, ' '),
|
||||||
util::display_kind(kind),
|
time,
|
||||||
Self::display_time(time),
|
|
||||||
text,
|
text,
|
||||||
Self::display_marker(has_desc, ""),
|
|
||||||
Self::display_extra(extra),
|
|
||||||
nw = self.num_width,
|
nw = self.num_width,
|
||||||
|
sw = self.span_width
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn display_spans(&self, spans: &[Option<SpanSegment>], empty: ColoredString) -> String {
|
fn display_spans(spans: &[Option<SpanSegment>], empty: char) -> String {
|
||||||
let mut result = String::new();
|
let mut result = String::new();
|
||||||
for i in 0..self.span_width {
|
for segment in spans {
|
||||||
if let Some(Some(segment)) = spans.get(i) {
|
result.push(match segment {
|
||||||
let colored_str = match segment {
|
Some(SpanSegment::Start) => '┌',
|
||||||
SpanSegment::Start(_) => "┌".bright_black(),
|
Some(SpanSegment::Middle) => '│',
|
||||||
SpanSegment::Middle(SpanStyle::Solid) => "│".bright_black(),
|
Some(SpanSegment::End) => '└',
|
||||||
SpanSegment::Middle(SpanStyle::Dashed) => "╎".bright_black(),
|
None => empty,
|
||||||
SpanSegment::Middle(SpanStyle::Dotted) => "┊".bright_black(),
|
});
|
||||||
SpanSegment::Mark(_) => "┝".bright_black(),
|
|
||||||
SpanSegment::End(_) => "└".bright_black(),
|
|
||||||
};
|
|
||||||
result.push_str(&format!("{colored_str}"));
|
|
||||||
} else {
|
|
||||||
result.push_str(&format!("{empty}"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
fn display_time(time: Times) -> ColoredString {
|
|
||||||
match time {
|
|
||||||
Times::Untimed => "".into(),
|
|
||||||
Times::At(t) => format!(" {t}").bright_black(),
|
|
||||||
Times::FromTo(t1, t2) => format!(" {t1}--{t2}").bright_black(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn display_marker(marker: bool, otherwise: &str) -> ColoredString {
|
|
||||||
if marker {
|
|
||||||
"*".bright_yellow()
|
|
||||||
} else {
|
|
||||||
otherwise.into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn display_extra(extra: &Option<String>) -> ColoredString {
|
|
||||||
match extra {
|
|
||||||
None => "".into(),
|
|
||||||
Some(extra) => format!(" ({extra})").bright_black(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn push(&mut self, line: &str) {
|
fn push(&mut self, line: &str) {
|
||||||
self.result.push_str(line);
|
self.result.push_str(line);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
133
src/cli/show.rs
133
src/cli/show.rs
|
|
@ -1,108 +1,61 @@
|
||||||
use chrono::NaiveDate;
|
|
||||||
use codespan_reporting::files::Files as CsFiles;
|
|
||||||
use colored::Colorize;
|
|
||||||
|
|
||||||
use crate::eval::{Entry, EntryKind};
|
use crate::eval::{Entry, EntryKind};
|
||||||
use crate::files::commands::{Command, Log};
|
use crate::files::Files;
|
||||||
use crate::files::primitives::Spanned;
|
|
||||||
use crate::files::{Files, Sourced};
|
|
||||||
|
|
||||||
use super::error::Error;
|
use super::error::Result;
|
||||||
use super::layout::line::LineLayout;
|
use super::layout::line::LineLayout;
|
||||||
use super::util;
|
|
||||||
|
|
||||||
fn fmt_where(files: &Files, command: &Sourced<'_, Spanned<Command>>) -> String {
|
|
||||||
let name = files.name(command.source.file()).expect("file exists");
|
|
||||||
let line = files
|
|
||||||
.line_index(command.source.file(), command.value.span.start)
|
|
||||||
.expect("file exists and line is valid");
|
|
||||||
let line = line + 1; // 1-indexed for human consumption
|
|
||||||
format!("Line {line} in {name}")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_desc(command: &Sourced<'_, Spanned<Command>>) {
|
|
||||||
let desc: &[String] = match &command.value.value {
|
|
||||||
Command::Task(task) => &task.desc,
|
|
||||||
Command::Note(note) => ¬e.desc,
|
|
||||||
Command::Log(log) => &log.desc,
|
|
||||||
_ => &[],
|
|
||||||
};
|
|
||||||
if !desc.is_empty() {
|
|
||||||
println!();
|
|
||||||
for line in desc {
|
|
||||||
println!("{}", line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn show_entry(files: &Files, entry: &Entry) {
|
fn show_entry(files: &Files, entry: &Entry) {
|
||||||
let command = files.command(entry.source);
|
let command = files.command(entry.source);
|
||||||
|
|
||||||
let kind = util::display_kind(LineLayout::entry_kind(entry));
|
match entry.kind {
|
||||||
println!("{} {} {}", "Title:".bright_black(), kind, entry.title);
|
EntryKind::Task => println!("TASK {}", command.title()),
|
||||||
|
EntryKind::TaskDone(when) => {
|
||||||
|
println!("DONE {}", command.title());
|
||||||
|
println!("DONE AT {}", when);
|
||||||
|
}
|
||||||
|
EntryKind::Note => println!("NOTE {}", command.title()),
|
||||||
|
EntryKind::Birthday(Some(age)) => {
|
||||||
|
println!("BIRTHDAY {}", command.title());
|
||||||
|
println!("AGE {}", age);
|
||||||
|
}
|
||||||
|
EntryKind::Birthday(None) => {
|
||||||
|
println!("BIRTHDAY {}", command.title());
|
||||||
|
println!("AGE UNKNOWN");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let what = match entry.kind {
|
if let Some(dates) = entry.dates {
|
||||||
EntryKind::Task => "Task".to_string(),
|
println!("DATE {}", dates.sorted());
|
||||||
EntryKind::TaskDone(date) => format!("Task, done {date}"),
|
} else {
|
||||||
EntryKind::TaskCanceled(date) => format!("Task, canceled {date}"),
|
println!("NO DATE");
|
||||||
EntryKind::Note => "Note".to_string(),
|
}
|
||||||
EntryKind::Birthday(None) => "Birthday, age unknown".to_string(),
|
|
||||||
EntryKind::Birthday(Some(age)) => format!("Birthday, age {age}"),
|
|
||||||
};
|
|
||||||
println!("{} {}", "What:".bright_black(), what);
|
|
||||||
|
|
||||||
let when = match entry.dates {
|
for line in command.desc() {
|
||||||
None => "no date".to_string(),
|
println!("# {}", line);
|
||||||
Some(date) => format!("{}", date.sorted()),
|
|
||||||
};
|
|
||||||
println!("{} {}", "When:".bright_black(), when);
|
|
||||||
|
|
||||||
println!("{} {}", "Where:".bright_black(), fmt_where(files, &command));
|
|
||||||
|
|
||||||
print_desc(&command);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn show_log(files: &Files, log: Sourced<'_, Log>) {
|
|
||||||
let command = files.command(log.source);
|
|
||||||
|
|
||||||
println!("{} Log entry", "What:".bright_black());
|
|
||||||
println!("{} {}", "When:".bright_black(), log.value.date);
|
|
||||||
|
|
||||||
println!("{} {}", "Where:".bright_black(), fmt_where(files, &command));
|
|
||||||
|
|
||||||
print_desc(&command);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn show_ident(files: &Files, entries: &[Entry], layout: &LineLayout, ident: Ident) {
|
|
||||||
match ident {
|
|
||||||
Ident::Number(n) => match layout.look_up_number(n) {
|
|
||||||
Ok(index) => show_entry(files, &entries[index]),
|
|
||||||
Err(e) => println!("{e}"),
|
|
||||||
},
|
|
||||||
Ident::Date(date) => match files.log(date) {
|
|
||||||
Some(log) => show_log(files, log),
|
|
||||||
None => println!("{}", Error::NoSuchLog(date)),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
pub fn show(
|
||||||
pub enum Ident {
|
files: &Files,
|
||||||
Number(usize),
|
entries: &[Entry],
|
||||||
Date(NaiveDate),
|
layout: &LineLayout,
|
||||||
}
|
numbers: &[usize],
|
||||||
|
) -> Result<()> {
|
||||||
pub fn show(files: &Files, entries: &[Entry], layout: &LineLayout, idents: &[Ident]) {
|
if numbers.is_empty() {
|
||||||
if idents.is_empty() {
|
|
||||||
// Nothing to do
|
// Nothing to do
|
||||||
return;
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
show_ident(files, entries, layout, idents[0]);
|
let indices = numbers
|
||||||
for &ident in idents.iter().skip(1) {
|
.iter()
|
||||||
|
.map(|n| layout.look_up_number(*n))
|
||||||
|
.collect::<Result<Vec<usize>>>()?;
|
||||||
|
|
||||||
|
show_entry(files, &entries[indices[0]]);
|
||||||
|
for &index in indices.iter().skip(1) {
|
||||||
println!();
|
println!();
|
||||||
println!();
|
show_entry(files, &entries[index]);
|
||||||
println!();
|
|
||||||
show_ident(files, entries, layout, ident);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
use colored::{ColoredString, Colorize};
|
|
||||||
|
|
||||||
use super::error::{Error, Result};
|
|
||||||
use super::layout::line::LineKind;
|
|
||||||
|
|
||||||
pub fn display_kind(kind: LineKind) -> ColoredString {
|
|
||||||
match kind {
|
|
||||||
LineKind::Task => "T".magenta().bold(),
|
|
||||||
LineKind::Done => "D".green().bold(),
|
|
||||||
LineKind::Canceled => "C".red().bold(),
|
|
||||||
LineKind::Note => "N".blue().bold(),
|
|
||||||
LineKind::Birthday => "B".yellow().bold(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn display_current_birthday_text(text: &str) -> ColoredString {
|
|
||||||
text.yellow()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn edit(input: &str) -> Result<String> {
|
|
||||||
edit::edit(input).map_err(Error::EditingIo)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn edit_with_suffix(input: &str, suffix: &str) -> Result<String> {
|
|
||||||
let mut builder = edit::Builder::new();
|
|
||||||
builder.suffix(suffix);
|
|
||||||
edit::edit_with_builder(input, &builder).map_err(Error::EditingIo)
|
|
||||||
}
|
|
||||||
31
src/error.rs
31
src/error.rs
|
|
@ -1,31 +0,0 @@
|
||||||
use codespan_reporting::diagnostic::Diagnostic;
|
|
||||||
use codespan_reporting::files::Files;
|
|
||||||
use codespan_reporting::term::{self, Config};
|
|
||||||
use termcolor::StandardStream;
|
|
||||||
|
|
||||||
pub trait Eprint<'a, F: Files<'a>> {
|
|
||||||
#[allow(single_use_lifetimes)]
|
|
||||||
fn eprint_diagnostic<'f: 'a>(
|
|
||||||
files: &'f F,
|
|
||||||
config: &Config,
|
|
||||||
diagnostic: &Diagnostic<F::FileId>,
|
|
||||||
) {
|
|
||||||
let mut out = StandardStream::stderr(termcolor::ColorChoice::Auto);
|
|
||||||
if let Err(e) = term::emit(&mut out, config, files, diagnostic) {
|
|
||||||
panic!("Error while reporting error: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(single_use_lifetimes)]
|
|
||||||
fn eprint<'f: 'a>(&self, files: &'f F, config: &Config);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(single_use_lifetimes)]
|
|
||||||
pub fn eprint_error<'a, 'f: 'a, F, E>(files: &'f F, e: &E)
|
|
||||||
where
|
|
||||||
F: Files<'a>,
|
|
||||||
E: Eprint<'a, F>,
|
|
||||||
{
|
|
||||||
let config = Config::default();
|
|
||||||
e.eprint(files, &config);
|
|
||||||
}
|
|
||||||
45
src/eval.rs
45
src/eval.rs
|
|
@ -1,14 +1,14 @@
|
||||||
use chrono::NaiveDate;
|
use chrono::NaiveDate;
|
||||||
|
|
||||||
use crate::files::cli::{CliDate, CliDatum, CliRange};
|
use crate::files::arguments::{Range, RangeDate};
|
||||||
use crate::files::{FileSource, Files};
|
use crate::files::Files;
|
||||||
|
|
||||||
use self::command::{CommandState, EvalCommand};
|
use self::command::CommandState;
|
||||||
pub use self::date::Dates;
|
pub use self::date::Dates;
|
||||||
use self::delta::Delta;
|
use self::delta::Delta;
|
||||||
use self::entry::Entries;
|
use self::entry::Entries;
|
||||||
pub use self::entry::{Entry, EntryKind, EntryMode};
|
pub use self::entry::{Entry, EntryKind, EntryMode};
|
||||||
pub use self::error::Error;
|
pub use self::error::{Error, Result, SourceInfo};
|
||||||
pub use self::range::DateRange;
|
pub use self::range::DateRange;
|
||||||
|
|
||||||
mod command;
|
mod command;
|
||||||
|
|
@ -20,41 +20,22 @@ mod range;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
impl Files {
|
impl Files {
|
||||||
pub fn eval(&self, mode: EntryMode, range: DateRange) -> Result<Vec<Entry>, Error<FileSource>> {
|
pub fn eval(&self, mode: EntryMode, range: DateRange) -> Result<Vec<Entry>> {
|
||||||
let mut entries = Entries::new(mode, range);
|
let mut entries = Entries::new(mode, range);
|
||||||
for command in self.commands() {
|
for command in self.commands() {
|
||||||
let source = command.source;
|
for entry in CommandState::new(command, range).eval()?.entries() {
|
||||||
if let Some(command) = EvalCommand::new(&command.value.value) {
|
entries.add(entry);
|
||||||
for entry in CommandState::new(command, source, range).eval()?.entries() {
|
|
||||||
entries.add(entry);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(entries.entries())
|
Ok(entries.entries())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CliDate {
|
impl Range {
|
||||||
pub fn eval<S: Copy>(&self, index: S, today: NaiveDate) -> Result<NaiveDate, Error<S>> {
|
pub fn eval(&self, index: usize, today: NaiveDate) -> Result<DateRange> {
|
||||||
let mut date = match self.datum {
|
|
||||||
CliDatum::Date(d) => d,
|
|
||||||
CliDatum::Today => today,
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(delta) = &self.delta {
|
|
||||||
let delta: Delta = delta.into();
|
|
||||||
date = delta.apply_date(index, date)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(date)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CliRange {
|
|
||||||
pub fn eval<S: Copy>(&self, index: S, today: NaiveDate) -> Result<DateRange, Error<S>> {
|
|
||||||
let mut start = match self.start {
|
let mut start = match self.start {
|
||||||
CliDatum::Date(d) => d,
|
RangeDate::Date(d) => d,
|
||||||
CliDatum::Today => today,
|
RangeDate::Today => today,
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(delta) = &self.start_delta {
|
if let Some(delta) = &self.start_delta {
|
||||||
|
|
@ -65,8 +46,8 @@ impl CliRange {
|
||||||
let mut end = start;
|
let mut end = start;
|
||||||
|
|
||||||
match self.end {
|
match self.end {
|
||||||
Some(CliDatum::Date(d)) => end = d,
|
Some(RangeDate::Date(d)) => end = d,
|
||||||
Some(CliDatum::Today) => end = today,
|
Some(RangeDate::Today) => end = today,
|
||||||
None => {}
|
None => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,100 +1,31 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use chrono::{Duration, NaiveDate};
|
use chrono::NaiveDate;
|
||||||
|
|
||||||
use crate::files::commands::{
|
use crate::files::commands::{BirthdaySpec, Command, Done, DoneDate, Note, Spec, Statement, Task};
|
||||||
self, BirthdaySpec, Command, Done, DoneDate, DoneKind, Note, Spec, Statement, Task,
|
use crate::files::primitives::Span;
|
||||||
};
|
use crate::files::SourcedCommand;
|
||||||
use crate::files::primitives::{Span, Spanned, Time};
|
|
||||||
use crate::files::{FileSource, Source};
|
|
||||||
|
|
||||||
use super::date::Dates;
|
use super::date::Dates;
|
||||||
use super::delta::Delta;
|
use super::{DateRange, Entry, EntryKind, Error, Result};
|
||||||
use super::{DateRange, Entry, EntryKind, Error};
|
|
||||||
|
|
||||||
mod birthday;
|
mod birthday;
|
||||||
mod date;
|
mod date;
|
||||||
mod formula;
|
mod formula;
|
||||||
|
|
||||||
/// A command that can be evaluated.
|
|
||||||
pub enum EvalCommand<'a> {
|
|
||||||
Task(&'a Task),
|
|
||||||
Note(&'a Note),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> EvalCommand<'a> {
|
|
||||||
pub fn new(command: &'a Command) -> Option<Self> {
|
|
||||||
match command {
|
|
||||||
Command::Task(task) => Some(Self::Task(task)),
|
|
||||||
Command::Note(note) => Some(Self::Note(note)),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn statements(&self) -> &[Statement] {
|
|
||||||
match self {
|
|
||||||
Self::Task(task) => &task.statements,
|
|
||||||
Self::Note(note) => ¬e.statements,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn kind(&self) -> EntryKind {
|
|
||||||
match self {
|
|
||||||
Self::Task(_) => EntryKind::Task,
|
|
||||||
Self::Note(_) => EntryKind::Note,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn title(&self) -> String {
|
|
||||||
match self {
|
|
||||||
Self::Task(task) => task.title.clone(),
|
|
||||||
Self::Note(note) => note.title.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn has_description(&self) -> bool {
|
|
||||||
match self {
|
|
||||||
Self::Task(task) => !task.desc.is_empty(),
|
|
||||||
Self::Note(note) => !note.desc.is_empty(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Last root date mentioned in any `DONE`.
|
|
||||||
fn last_done_root(&self) -> Option<NaiveDate> {
|
|
||||||
match self {
|
|
||||||
Self::Task(task) => task
|
|
||||||
.done
|
|
||||||
.iter()
|
|
||||||
.filter_map(|done| done.date.map(DoneDate::root))
|
|
||||||
.max(),
|
|
||||||
Self::Note(_) => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Last completion date mentioned in any `DONE`.
|
|
||||||
fn last_done_completion(&self) -> Option<NaiveDate> {
|
|
||||||
match self {
|
|
||||||
Self::Task(task) => task.done.iter().map(|done| done.done_at).max(),
|
|
||||||
Self::Note(_) => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct CommandState<'a> {
|
pub struct CommandState<'a> {
|
||||||
command: EvalCommand<'a>,
|
command: SourcedCommand<'a>,
|
||||||
source: Source,
|
|
||||||
range: DateRange,
|
range: DateRange,
|
||||||
|
|
||||||
from: Option<NaiveDate>,
|
from: Option<NaiveDate>,
|
||||||
until: Option<NaiveDate>,
|
until: Option<NaiveDate>,
|
||||||
remind: Option<Spanned<Delta>>,
|
|
||||||
|
|
||||||
dated: HashMap<NaiveDate, Entry>,
|
dated: HashMap<NaiveDate, Entry>,
|
||||||
undated: Vec<Entry>,
|
undated: Vec<Entry>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> CommandState<'a> {
|
impl<'a> CommandState<'a> {
|
||||||
pub fn new(command: EvalCommand<'a>, source: Source, mut range: DateRange) -> Self {
|
pub fn new(command: SourcedCommand<'a>, mut range: DateRange) -> Self {
|
||||||
// If we don't calculate entries for the source of the move command, it
|
// If we don't calculate entries for the source of the move command, it
|
||||||
// fails even though the user did nothing wrong. Also, move commands (or
|
// fails even though the user did nothing wrong. Also, move commands (or
|
||||||
// chains thereof) may move an initially out-of-range entry into range.
|
// chains thereof) may move an initially out-of-range entry into range.
|
||||||
|
|
@ -102,28 +33,26 @@ impl<'a> CommandState<'a> {
|
||||||
// To fix this, we just expand the range to contain all move command
|
// To fix this, we just expand the range to contain all move command
|
||||||
// sources. This is a quick fix, but until it becomes a performance
|
// sources. This is a quick fix, but until it becomes a performance
|
||||||
// issue (if ever), it's probably fine.
|
// issue (if ever), it's probably fine.
|
||||||
for statement in command.statements() {
|
for statement in command.command.statements() {
|
||||||
if let Statement::Move { from, .. } = statement {
|
if let Statement::Move { from, .. } = statement {
|
||||||
range = range.containing(*from)
|
range = range.containing(*from)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
command,
|
|
||||||
source,
|
|
||||||
range,
|
range,
|
||||||
|
command,
|
||||||
from: None,
|
from: None,
|
||||||
until: None,
|
until: None,
|
||||||
remind: None,
|
|
||||||
dated: HashMap::new(),
|
dated: HashMap::new(),
|
||||||
undated: Vec::new(),
|
undated: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn eval(mut self) -> Result<Self, Error<FileSource>> {
|
pub fn eval(mut self) -> Result<Self> {
|
||||||
match self.command {
|
match self.command.command {
|
||||||
EvalCommand::Task(task) => self.eval_task(task)?,
|
Command::Task(task) => self.eval_task(task)?,
|
||||||
EvalCommand::Note(note) => self.eval_note(note)?,
|
Command::Note(note) => self.eval_note(note)?,
|
||||||
}
|
}
|
||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
|
|
@ -137,10 +66,30 @@ impl<'a> CommandState<'a> {
|
||||||
|
|
||||||
// Helper functions
|
// Helper functions
|
||||||
|
|
||||||
fn range_with_remind(&self) -> DateRange {
|
fn kind(&self) -> EntryKind {
|
||||||
match &self.remind {
|
match self.command.command {
|
||||||
None => self.range,
|
Command::Task(_) => EntryKind::Task,
|
||||||
Some(delta) => self.range.expand_by(&delta.value),
|
Command::Note(_) => EntryKind::Note,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Last root date mentioned in any `DONE`.
|
||||||
|
fn last_done_root(&self) -> Option<NaiveDate> {
|
||||||
|
match self.command.command {
|
||||||
|
Command::Task(task) => task
|
||||||
|
.done
|
||||||
|
.iter()
|
||||||
|
.filter_map(|done| done.date.map(DoneDate::root))
|
||||||
|
.max(),
|
||||||
|
Command::Note(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Last completion date mentioned in any `DONE`.
|
||||||
|
fn last_done_completion(&self) -> Option<NaiveDate> {
|
||||||
|
match self.command.command {
|
||||||
|
Command::Task(task) => task.done.iter().map(|done| done.done_at).max(),
|
||||||
|
Command::Note(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -164,42 +113,11 @@ impl<'a> CommandState<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn entry_with_remind(
|
|
||||||
&self,
|
|
||||||
kind: EntryKind,
|
|
||||||
dates: Option<Dates>,
|
|
||||||
) -> Result<Entry, Error<FileSource>> {
|
|
||||||
let remind = if let (Some(dates), Some(delta)) = (dates, &self.remind) {
|
|
||||||
let index = self.source.file();
|
|
||||||
let start = dates.sorted().root();
|
|
||||||
let remind = delta.value.apply_date(index, dates.sorted().root())?;
|
|
||||||
if remind >= start {
|
|
||||||
return Err(Error::RemindDidNotMoveBackwards {
|
|
||||||
index,
|
|
||||||
span: delta.span,
|
|
||||||
from: start,
|
|
||||||
to: remind,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Some(remind)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Entry::new(
|
|
||||||
self.source,
|
|
||||||
kind,
|
|
||||||
self.command.title(),
|
|
||||||
self.command.has_description(),
|
|
||||||
dates,
|
|
||||||
remind,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add an entry, respecting [`Self::from`] and [`Self::until`]. Does not
|
/// Add an entry, respecting [`Self::from`] and [`Self::until`]. Does not
|
||||||
/// overwrite existing entries if a root date is specified.
|
/// overwrite existing entries if a root date is specified.
|
||||||
fn add(&mut self, entry: Entry) {
|
fn add(&mut self, kind: EntryKind, dates: Option<Dates>) {
|
||||||
if let Some(dates) = entry.dates {
|
let entry = Entry::new(self.command.source, kind, dates);
|
||||||
|
if let Some(dates) = dates {
|
||||||
let root = dates.root();
|
let root = dates.root();
|
||||||
if let Some(from) = self.from {
|
if let Some(from) = self.from {
|
||||||
if root < from {
|
if root < from {
|
||||||
|
|
@ -219,8 +137,9 @@ impl<'a> CommandState<'a> {
|
||||||
|
|
||||||
/// Add an entry, ignoring [`Self::from`] and [`Self::until`]. Always
|
/// Add an entry, ignoring [`Self::from`] and [`Self::until`]. Always
|
||||||
/// overwrites existing entries if a root date is specified.
|
/// overwrites existing entries if a root date is specified.
|
||||||
fn add_forced(&mut self, entry: Entry) {
|
fn add_forced(&mut self, kind: EntryKind, dates: Option<Dates>) {
|
||||||
if let Some(dates) = entry.dates {
|
let entry = Entry::new(self.command.source, kind, dates);
|
||||||
|
if let Some(dates) = dates {
|
||||||
self.dated.insert(dates.root(), entry);
|
self.dated.insert(dates.root(), entry);
|
||||||
} else {
|
} else {
|
||||||
self.undated.push(entry);
|
self.undated.push(entry);
|
||||||
|
|
@ -235,53 +154,47 @@ impl<'a> CommandState<'a> {
|
||||||
.any(|s| matches!(s, Statement::Date(_) | Statement::BDate(_)))
|
.any(|s| matches!(s, Statement::Date(_) | Statement::BDate(_)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn eval_task(&mut self, task: &Task) -> Result<(), Error<FileSource>> {
|
fn eval_task(&mut self, task: &Task) -> Result<()> {
|
||||||
if Self::has_date_stmt(&task.statements) {
|
if Self::has_date_stmt(&task.statements) {
|
||||||
for statement in &task.statements {
|
for statement in &task.statements {
|
||||||
self.eval_statement(statement)?;
|
self.eval_statement(statement)?;
|
||||||
}
|
}
|
||||||
} else if task.done.is_empty() {
|
} else if task.done.is_empty() {
|
||||||
self.add(self.entry_with_remind(self.command.kind(), None)?);
|
self.add(self.kind(), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
for done in &task.done {
|
for done in &task.done {
|
||||||
self.eval_done(done)?;
|
self.eval_done(done);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn eval_note(&mut self, note: &Note) -> Result<(), Error<FileSource>> {
|
fn eval_note(&mut self, note: &Note) -> Result<()> {
|
||||||
if Self::has_date_stmt(¬e.statements) {
|
if Self::has_date_stmt(¬e.statements) {
|
||||||
for statement in ¬e.statements {
|
for statement in ¬e.statements {
|
||||||
self.eval_statement(statement)?;
|
self.eval_statement(statement)?;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.add(self.entry_with_remind(self.command.kind(), None)?);
|
self.add(self.kind(), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn eval_statement(&mut self, statement: &Statement) -> Result<(), Error<FileSource>> {
|
fn eval_statement(&mut self, statement: &Statement) -> Result<()> {
|
||||||
match statement {
|
match statement {
|
||||||
Statement::Date(spec) => self.eval_date(spec)?,
|
Statement::Date(spec) => self.eval_date(spec)?,
|
||||||
Statement::BDate(spec) => self.eval_bdate(spec)?,
|
Statement::BDate(spec) => self.eval_bdate(spec),
|
||||||
Statement::From(date) => self.from = *date,
|
Statement::From(date) => self.from = *date,
|
||||||
Statement::Until(date) => self.until = *date,
|
Statement::Until(date) => self.until = *date,
|
||||||
Statement::Except(date) => self.eval_except(*date),
|
Statement::Except(date) => self.eval_except(*date),
|
||||||
Statement::Move {
|
Statement::Move { span, from, to } => self.eval_move(*span, *from, *to)?,
|
||||||
span,
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
to_time,
|
|
||||||
} => self.eval_move(*span, *from, *to, *to_time)?,
|
|
||||||
Statement::Remind(delta) => self.eval_remind(delta),
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn eval_date(&mut self, spec: &Spec) -> Result<(), Error<FileSource>> {
|
fn eval_date(&mut self, spec: &Spec) -> Result<()> {
|
||||||
match spec {
|
match spec {
|
||||||
Spec::Date(spec) => self.eval_date_spec(spec.into()),
|
Spec::Date(spec) => self.eval_date_spec(spec.into()),
|
||||||
Spec::Weekday(spec) => self.eval_formula_spec(spec.into()),
|
Spec::Weekday(spec) => self.eval_formula_spec(spec.into()),
|
||||||
|
|
@ -289,69 +202,34 @@ impl<'a> CommandState<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn eval_bdate(&mut self, spec: &BirthdaySpec) -> Result<(), Error<FileSource>> {
|
fn eval_bdate(&mut self, spec: &BirthdaySpec) {
|
||||||
self.eval_birthday_spec(spec)
|
self.eval_birthday_spec(spec);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn eval_except(&mut self, date: NaiveDate) {
|
fn eval_except(&mut self, date: NaiveDate) {
|
||||||
// TODO Error if nothing is removed?
|
|
||||||
self.dated.remove(&date);
|
self.dated.remove(&date);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn eval_move(
|
fn eval_move(&mut self, span: Span, from: NaiveDate, to: NaiveDate) -> Result<()> {
|
||||||
&mut self,
|
|
||||||
span: Span,
|
|
||||||
from: NaiveDate,
|
|
||||||
to: Option<NaiveDate>,
|
|
||||||
to_time: Option<Spanned<Time>>,
|
|
||||||
) -> Result<(), Error<FileSource>> {
|
|
||||||
if let Some(mut entry) = self.dated.remove(&from) {
|
if let Some(mut entry) = self.dated.remove(&from) {
|
||||||
let mut dates = entry.dates.expect("comes from self.dated");
|
if let Some(dates) = entry.dates {
|
||||||
|
let delta = to - from;
|
||||||
// Determine delta
|
entry.dates = Some(dates.move_by(delta));
|
||||||
let mut delta = Duration::zero();
|
|
||||||
if let Some(to) = to {
|
|
||||||
delta = delta + (to - dates.root());
|
|
||||||
}
|
}
|
||||||
if let Some(to_time) = to_time {
|
self.dated.insert(to, entry);
|
||||||
if let Some((root, _)) = dates.times() {
|
|
||||||
delta = delta + Duration::minutes(root.minutes_to(to_time.value));
|
|
||||||
} else {
|
|
||||||
return Err(Error::TimedMoveWithoutTime {
|
|
||||||
index: self.source.file(),
|
|
||||||
span: to_time.span,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dates = dates.move_by(delta);
|
|
||||||
entry.dates = Some(dates);
|
|
||||||
self.dated.insert(dates.root(), entry);
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err(Error::MoveWithoutSource {
|
Err(Error::MoveWithoutSource {
|
||||||
index: self.source.file(),
|
index: self.command.source.file(),
|
||||||
span,
|
span,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn eval_remind(&mut self, delta: &Option<Spanned<commands::Delta>>) {
|
fn eval_done(&mut self, done: &Done) {
|
||||||
if let Some(delta) = delta {
|
self.add_forced(
|
||||||
self.remind = Some(Spanned::new(delta.span, (&delta.value).into()));
|
EntryKind::TaskDone(done.done_at),
|
||||||
} else {
|
done.date.map(|date| date.into()),
|
||||||
self.remind = None;
|
);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn eval_done(&mut self, done: &Done) -> Result<(), Error<FileSource>> {
|
|
||||||
let kind = match done.kind {
|
|
||||||
DoneKind::Done => EntryKind::TaskDone(done.done_at),
|
|
||||||
DoneKind::Canceled => EntryKind::TaskCanceled(done.done_at),
|
|
||||||
};
|
|
||||||
let dates = done.date.map(|date| date.into());
|
|
||||||
self.add_forced(self.entry_with_remind(kind, dates)?);
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,16 @@
|
||||||
use chrono::{Datelike, NaiveDate};
|
use chrono::{Datelike, NaiveDate};
|
||||||
|
|
||||||
use crate::files::commands::BirthdaySpec;
|
use crate::files::commands::BirthdaySpec;
|
||||||
use crate::files::FileSource;
|
|
||||||
|
|
||||||
use super::super::command::CommandState;
|
use super::super::command::CommandState;
|
||||||
use super::super::date::Dates;
|
use super::super::date::Dates;
|
||||||
use super::super::error::Error;
|
|
||||||
use super::super::EntryKind;
|
use super::super::EntryKind;
|
||||||
|
|
||||||
impl CommandState<'_> {
|
impl<'a> CommandState<'a> {
|
||||||
pub fn eval_birthday_spec(&mut self, spec: &BirthdaySpec) -> Result<(), Error<FileSource>> {
|
pub fn eval_birthday_spec(&mut self, spec: &BirthdaySpec) {
|
||||||
let range = match self.limit_from_until(self.range_with_remind()) {
|
let range = match self.limit_from_until(self.range) {
|
||||||
Some(range) => range,
|
Some(range) => range,
|
||||||
None => return Ok(()),
|
None => return,
|
||||||
};
|
};
|
||||||
|
|
||||||
for year in range.years() {
|
for year in range.years() {
|
||||||
|
|
@ -28,19 +26,15 @@ impl CommandState<'_> {
|
||||||
let kind = EntryKind::Birthday(age);
|
let kind = EntryKind::Birthday(age);
|
||||||
|
|
||||||
if let Some(date) = spec.date.with_year(year) {
|
if let Some(date) = spec.date.with_year(year) {
|
||||||
self.add(
|
self.add(EntryKind::Birthday(age), Some(Dates::new(date, date)));
|
||||||
self.entry_with_remind(EntryKind::Birthday(age), Some(Dates::new(date, date)))?,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
assert_eq!(spec.date.month(), 2);
|
assert_eq!(spec.date.month(), 2);
|
||||||
assert_eq!(spec.date.day(), 29);
|
assert_eq!(spec.date.day(), 29);
|
||||||
|
|
||||||
let first = NaiveDate::from_ymd_opt(year, 2, 28).unwrap();
|
let first = NaiveDate::from_ymd(year, 2, 28);
|
||||||
let second = NaiveDate::from_ymd_opt(year, 3, 1).unwrap();
|
let second = NaiveDate::from_ymd(year, 3, 1);
|
||||||
self.add(self.entry_with_remind(kind, Some(Dates::new(first, second)))?);
|
self.add(kind, Some(Dates::new(first, second)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,12 @@
|
||||||
use chrono::NaiveDate;
|
use chrono::NaiveDate;
|
||||||
|
|
||||||
use crate::files::commands;
|
use crate::files::commands::{self, Command};
|
||||||
use crate::files::primitives::{Spanned, Time};
|
use crate::files::primitives::{Spanned, Time};
|
||||||
use crate::files::FileSource;
|
|
||||||
|
|
||||||
use super::super::command::CommandState;
|
use super::super::command::CommandState;
|
||||||
use super::super::date::Dates;
|
use super::super::date::Dates;
|
||||||
use super::super::delta::{Delta, DeltaStep};
|
use super::super::delta::{Delta, DeltaStep};
|
||||||
use super::super::{DateRange, Error};
|
use super::super::{DateRange, Error, Result};
|
||||||
use super::EvalCommand;
|
|
||||||
|
|
||||||
pub struct DateSpec {
|
pub struct DateSpec {
|
||||||
pub start: NaiveDate,
|
pub start: NaiveDate,
|
||||||
|
|
@ -74,30 +72,28 @@ impl DateSpec {
|
||||||
/// `start` date itself should be skipped (and thus not result in an entry).
|
/// `start` date itself should be skipped (and thus not result in an entry).
|
||||||
/// This may be necessary if [`Self::start_at_done`] is set.
|
/// This may be necessary if [`Self::start_at_done`] is set.
|
||||||
fn start_and_range(&self, s: &CommandState<'_>) -> Option<(NaiveDate, bool, DateRange)> {
|
fn start_and_range(&self, s: &CommandState<'_>) -> Option<(NaiveDate, bool, DateRange)> {
|
||||||
let (start, skip, range) = match s.command {
|
let (start, skip, range) = match s.command.command {
|
||||||
EvalCommand::Task(_) => {
|
Command::Task(_) => {
|
||||||
let (start, skip) = s
|
let (start, skip) = s
|
||||||
.command
|
|
||||||
.last_done_completion()
|
.last_done_completion()
|
||||||
.map(|start| (start, true))
|
.map(|start| (start, true))
|
||||||
.filter(|_| self.start_at_done)
|
.filter(|_| self.start_at_done)
|
||||||
.unwrap_or((self.start, false));
|
.unwrap_or((self.start, false));
|
||||||
let range_from = s
|
let range_from = s
|
||||||
.command
|
|
||||||
.last_done_root()
|
.last_done_root()
|
||||||
.map(|date| date.succ_opt().unwrap())
|
.map(|date| date.succ())
|
||||||
.unwrap_or(self.start);
|
.unwrap_or(self.start);
|
||||||
let range = s
|
let range = s
|
||||||
.range_with_remind()
|
.range
|
||||||
.expand_by(&self.end_delta)
|
.expand_by(&self.end_delta)
|
||||||
.move_by(&self.start_delta)
|
.move_by(&self.start_delta)
|
||||||
.with_from(range_from)?;
|
.with_from(range_from)?;
|
||||||
(start, skip, range)
|
(start, skip, range)
|
||||||
}
|
}
|
||||||
EvalCommand::Note(_) => {
|
Command::Note(_) => {
|
||||||
let start = self.start;
|
let start = self.start;
|
||||||
let range = s
|
let range = s
|
||||||
.range_with_remind()
|
.range
|
||||||
.expand_by(&self.end_delta)
|
.expand_by(&self.end_delta)
|
||||||
.move_by(&self.start_delta);
|
.move_by(&self.start_delta);
|
||||||
(start, false, range)
|
(start, false, range)
|
||||||
|
|
@ -107,11 +103,7 @@ impl DateSpec {
|
||||||
Some((start, skip, range))
|
Some((start, skip, range))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn step(
|
fn step(index: usize, from: NaiveDate, repeat: &Spanned<Delta>) -> Result<NaiveDate> {
|
||||||
index: FileSource,
|
|
||||||
from: NaiveDate,
|
|
||||||
repeat: &Spanned<Delta>,
|
|
||||||
) -> Result<NaiveDate, Error<FileSource>> {
|
|
||||||
let to = repeat.value.apply_date(index, from)?;
|
let to = repeat.value.apply_date(index, from)?;
|
||||||
if to > from {
|
if to > from {
|
||||||
Ok(to)
|
Ok(to)
|
||||||
|
|
@ -125,7 +117,7 @@ impl DateSpec {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dates(&self, index: FileSource, start: NaiveDate) -> Result<Dates, Error<FileSource>> {
|
fn dates(&self, index: usize, start: NaiveDate) -> Result<Dates> {
|
||||||
let root = self.start_delta.apply_date(index, start)?;
|
let root = self.start_delta.apply_date(index, start)?;
|
||||||
Ok(if let Some(root_time) = self.start_time {
|
Ok(if let Some(root_time) = self.start_time {
|
||||||
let (other, other_time) = self.end_delta.apply_date_time(index, root, root_time)?;
|
let (other, other_time) = self.end_delta.apply_date_time(index, root, root_time)?;
|
||||||
|
|
@ -137,9 +129,9 @@ impl DateSpec {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CommandState<'_> {
|
impl<'a> CommandState<'a> {
|
||||||
pub fn eval_date_spec(&mut self, spec: DateSpec) -> Result<(), Error<FileSource>> {
|
pub fn eval_date_spec(&mut self, spec: DateSpec) -> Result<()> {
|
||||||
let index = self.source.file();
|
let index = self.command.source.file();
|
||||||
if let Some(repeat) = &spec.repeat {
|
if let Some(repeat) = &spec.repeat {
|
||||||
if let Some((mut start, skip, range)) = spec.start_and_range(self) {
|
if let Some((mut start, skip, range)) = spec.start_and_range(self) {
|
||||||
if skip {
|
if skip {
|
||||||
|
|
@ -150,13 +142,13 @@ impl CommandState<'_> {
|
||||||
}
|
}
|
||||||
while start <= range.until() {
|
while start <= range.until() {
|
||||||
let dates = spec.dates(index, start)?;
|
let dates = spec.dates(index, start)?;
|
||||||
self.add(self.entry_with_remind(self.command.kind(), Some(dates))?);
|
self.add(self.kind(), Some(dates));
|
||||||
start = DateSpec::step(index, start, repeat)?;
|
start = DateSpec::step(index, start, repeat)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let dates = spec.dates(index, spec.start)?;
|
let dates = spec.dates(index, spec.start)?;
|
||||||
self.add(self.entry_with_remind(self.command.kind(), Some(dates))?);
|
self.add(self.kind(), Some(dates));
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -103,24 +103,11 @@ impl Dates {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn move_by(&self, delta: Duration) -> Self {
|
pub fn move_by(&self, delta: Duration) -> Self {
|
||||||
let mut result = *self;
|
Self {
|
||||||
|
root: self.root + delta,
|
||||||
// Modify dates
|
other: self.other + delta,
|
||||||
result.root += delta;
|
times: self.times,
|
||||||
result.other += delta;
|
|
||||||
|
|
||||||
// Modify times if necessary (may further modify dates)
|
|
||||||
const MINUTES_PER_DAY: i64 = 24 * 60;
|
|
||||||
let minutes = delta.num_minutes() % MINUTES_PER_DAY; // May be negative
|
|
||||||
if let Some(times) = self.times {
|
|
||||||
let (root_days, root) = times.root.add_minutes(minutes);
|
|
||||||
let (other_days, other) = times.other.add_minutes(minutes);
|
|
||||||
result.root += Duration::days(root_days);
|
|
||||||
result.other += Duration::days(other_days);
|
|
||||||
result.times = Some(Times { root, other });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
result
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -149,16 +136,32 @@ impl From<DoneDate> for Dates {
|
||||||
|
|
||||||
impl From<Dates> for DoneDate {
|
impl From<Dates> for DoneDate {
|
||||||
fn from(dates: Dates) -> Self {
|
fn from(dates: Dates) -> Self {
|
||||||
let (root, other) = dates.dates();
|
if dates.root == dates.other {
|
||||||
match dates.times() {
|
match dates.times {
|
||||||
Some((root_time, other_time)) => Self::DateTimeToDateTime {
|
Some(times) if times.root == times.other => Self::DateTime {
|
||||||
root,
|
root: dates.root,
|
||||||
root_time,
|
root_time: times.root,
|
||||||
other,
|
},
|
||||||
other_time,
|
Some(times) => Self::DateTimeToTime {
|
||||||
},
|
root: dates.root,
|
||||||
None => Self::DateToDate { root, other },
|
root_time: times.root,
|
||||||
|
other_time: times.other,
|
||||||
|
},
|
||||||
|
None => Self::Date { root: dates.root },
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match dates.times {
|
||||||
|
Some(times) => Self::DateTimeToDateTime {
|
||||||
|
root: dates.root,
|
||||||
|
root_time: times.root,
|
||||||
|
other: dates.other,
|
||||||
|
other_time: times.other,
|
||||||
|
},
|
||||||
|
None => Self::DateToDate {
|
||||||
|
root: dates.root,
|
||||||
|
other: dates.other,
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.simplified()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use chrono::{Datelike, Duration, NaiveDate};
|
||||||
use crate::files::commands;
|
use crate::files::commands;
|
||||||
use crate::files::primitives::{Span, Spanned, Time, Weekday};
|
use crate::files::primitives::{Span, Spanned, Time, Weekday};
|
||||||
|
|
||||||
use super::{util, Error};
|
use super::{util, Error, Result};
|
||||||
|
|
||||||
/// Like [`commands::DeltaStep`] but includes a new constructor,
|
/// Like [`commands::DeltaStep`] but includes a new constructor,
|
||||||
/// [`DeltaStep::Time`].
|
/// [`DeltaStep::Time`].
|
||||||
|
|
@ -43,84 +43,84 @@ impl DeltaStep {
|
||||||
/// A lower bound on days
|
/// A lower bound on days
|
||||||
fn lower_bound(&self) -> i32 {
|
fn lower_bound(&self) -> i32 {
|
||||||
match self {
|
match self {
|
||||||
Self::Year(n) => {
|
DeltaStep::Year(n) => {
|
||||||
if *n < 0 {
|
if *n < 0 {
|
||||||
*n * 366
|
*n * 366
|
||||||
} else {
|
} else {
|
||||||
*n * 365
|
*n * 365
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Self::Month(n) | Self::MonthReverse(n) => {
|
DeltaStep::Month(n) | DeltaStep::MonthReverse(n) => {
|
||||||
if *n < 0 {
|
if *n < 0 {
|
||||||
*n * 31
|
*n * 31
|
||||||
} else {
|
} else {
|
||||||
*n * 28
|
*n * 28
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Self::Day(n) => *n,
|
DeltaStep::Day(n) => *n,
|
||||||
Self::Week(n) => *n * 7,
|
DeltaStep::Week(n) => *n * 7,
|
||||||
Self::Hour(n) => {
|
DeltaStep::Hour(n) => {
|
||||||
if *n < 0 {
|
if *n < 0 {
|
||||||
*n / 24 + (*n % 24).signum()
|
*n / 24 + (*n % 24).signum()
|
||||||
} else {
|
} else {
|
||||||
*n / 24
|
*n / 24
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Self::Minute(n) => {
|
DeltaStep::Minute(n) => {
|
||||||
if *n < 0 {
|
if *n < 0 {
|
||||||
*n / (24 * 60) + (*n % (24 * 60)).signum()
|
*n / (24 * 60) + (*n % (24 * 60)).signum()
|
||||||
} else {
|
} else {
|
||||||
*n / (24 * 60)
|
*n / (24 * 60)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Self::Weekday(n, _) => match n.cmp(&0) {
|
DeltaStep::Weekday(n, _) => match n.cmp(&0) {
|
||||||
Ordering::Less => *n * 7 - 1,
|
Ordering::Less => *n * 7 - 1,
|
||||||
Ordering::Equal => 0,
|
Ordering::Equal => 0,
|
||||||
Ordering::Greater => *n * 7 - 7,
|
Ordering::Greater => *n * 7 - 7,
|
||||||
},
|
},
|
||||||
Self::Time(_) => 0,
|
DeltaStep::Time(_) => 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An upper bound on days
|
/// An upper bound on days
|
||||||
fn upper_bound(&self) -> i32 {
|
fn upper_bound(&self) -> i32 {
|
||||||
match self {
|
match self {
|
||||||
Self::Year(n) => {
|
DeltaStep::Year(n) => {
|
||||||
if *n > 0 {
|
if *n > 0 {
|
||||||
*n * 366
|
*n * 366
|
||||||
} else {
|
} else {
|
||||||
*n * 365
|
*n * 365
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Self::Month(n) | Self::MonthReverse(n) => {
|
DeltaStep::Month(n) | DeltaStep::MonthReverse(n) => {
|
||||||
if *n > 0 {
|
if *n > 0 {
|
||||||
*n * 31
|
*n * 31
|
||||||
} else {
|
} else {
|
||||||
*n * 28
|
*n * 28
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Self::Day(n) => *n,
|
DeltaStep::Day(n) => *n,
|
||||||
Self::Week(n) => *n * 7,
|
DeltaStep::Week(n) => *n * 7,
|
||||||
Self::Hour(n) => {
|
DeltaStep::Hour(n) => {
|
||||||
if *n > 0 {
|
if *n > 0 {
|
||||||
*n / 24 + (*n % 24).signum()
|
*n / 24 + (*n % 24).signum()
|
||||||
} else {
|
} else {
|
||||||
*n / 24
|
*n / 24
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Self::Minute(n) => {
|
DeltaStep::Minute(n) => {
|
||||||
if *n > 0 {
|
if *n > 0 {
|
||||||
*n / (24 * 60) + (*n % (24 * 60)).signum()
|
*n / (24 * 60) + (*n % (24 * 60)).signum()
|
||||||
} else {
|
} else {
|
||||||
*n / (24 * 60)
|
*n / (24 * 60)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Self::Weekday(n, _) => match n.cmp(&0) {
|
DeltaStep::Weekday(n, _) => match n.cmp(&0) {
|
||||||
Ordering::Less => *n * 7 - 7,
|
Ordering::Less => *n * 7 - 7,
|
||||||
Ordering::Equal => 0,
|
Ordering::Equal => 0,
|
||||||
Ordering::Greater => *n * 7 - 1,
|
Ordering::Greater => *n * 7 - 1,
|
||||||
},
|
},
|
||||||
Self::Time(_) => 1,
|
DeltaStep::Time(_) => 1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -142,16 +142,16 @@ impl From<&commands::Delta> for Delta {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DeltaEval<I> {
|
struct DeltaEval {
|
||||||
index: I,
|
index: usize,
|
||||||
start: NaiveDate,
|
start: NaiveDate,
|
||||||
start_time: Option<Time>,
|
start_time: Option<Time>,
|
||||||
curr: NaiveDate,
|
curr: NaiveDate,
|
||||||
curr_time: Option<Time>,
|
curr_time: Option<Time>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S: Copy> DeltaEval<S> {
|
impl DeltaEval {
|
||||||
fn new(index: S, start: NaiveDate, start_time: Option<Time>) -> Self {
|
fn new(index: usize, start: NaiveDate, start_time: Option<Time>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
index,
|
index,
|
||||||
start,
|
start,
|
||||||
|
|
@ -161,7 +161,7 @@ impl<S: Copy> DeltaEval<S> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn err_step(&self, span: Span) -> Error<S> {
|
fn err_step(&self, span: Span) -> Error {
|
||||||
Error::DeltaInvalidStep {
|
Error::DeltaInvalidStep {
|
||||||
index: self.index,
|
index: self.index,
|
||||||
span,
|
span,
|
||||||
|
|
@ -172,7 +172,7 @@ impl<S: Copy> DeltaEval<S> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn err_time(&self, span: Span) -> Error<S> {
|
fn err_time(&self, span: Span) -> Error {
|
||||||
Error::DeltaNoTime {
|
Error::DeltaNoTime {
|
||||||
index: self.index,
|
index: self.index,
|
||||||
span,
|
span,
|
||||||
|
|
@ -181,7 +181,7 @@ impl<S: Copy> DeltaEval<S> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply(&mut self, step: &Spanned<DeltaStep>) -> Result<(), Error<S>> {
|
fn apply(&mut self, step: &Spanned<DeltaStep>) -> Result<()> {
|
||||||
match step.value {
|
match step.value {
|
||||||
DeltaStep::Year(n) => self.step_year(step.span, n)?,
|
DeltaStep::Year(n) => self.step_year(step.span, n)?,
|
||||||
DeltaStep::Month(n) => self.step_month(step.span, n)?,
|
DeltaStep::Month(n) => self.step_month(step.span, n)?,
|
||||||
|
|
@ -196,7 +196,7 @@ impl<S: Copy> DeltaEval<S> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn step_year(&mut self, span: Span, amount: i32) -> Result<(), Error<S>> {
|
fn step_year(&mut self, span: Span, amount: i32) -> Result<()> {
|
||||||
let year = self.curr.year() + amount;
|
let year = self.curr.year() + amount;
|
||||||
match NaiveDate::from_ymd_opt(year, self.curr.month(), self.curr.day()) {
|
match NaiveDate::from_ymd_opt(year, self.curr.month(), self.curr.day()) {
|
||||||
None => Err(self.err_step(span)),
|
None => Err(self.err_step(span)),
|
||||||
|
|
@ -207,7 +207,7 @@ impl<S: Copy> DeltaEval<S> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn step_month(&mut self, span: Span, amount: i32) -> Result<(), Error<S>> {
|
fn step_month(&mut self, span: Span, amount: i32) -> Result<()> {
|
||||||
let (year, month) = util::add_months(self.curr.year(), self.curr.month(), amount);
|
let (year, month) = util::add_months(self.curr.year(), self.curr.month(), amount);
|
||||||
match NaiveDate::from_ymd_opt(year, month, self.curr.day()) {
|
match NaiveDate::from_ymd_opt(year, month, self.curr.day()) {
|
||||||
None => Err(self.err_step(span)),
|
None => Err(self.err_step(span)),
|
||||||
|
|
@ -218,7 +218,7 @@ impl<S: Copy> DeltaEval<S> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn step_month_reverse(&mut self, span: Span, amount: i32) -> Result<(), Error<S>> {
|
fn step_month_reverse(&mut self, span: Span, amount: i32) -> Result<()> {
|
||||||
// Calculate offset from the last day of the month
|
// Calculate offset from the last day of the month
|
||||||
let month_length = util::month_length(self.curr.year(), self.curr.month()) as i32;
|
let month_length = util::month_length(self.curr.year(), self.curr.month()) as i32;
|
||||||
let end_offset = self.curr.day() as i32 - month_length;
|
let end_offset = self.curr.day() as i32 - month_length;
|
||||||
|
|
@ -252,26 +252,26 @@ impl<S: Copy> DeltaEval<S> {
|
||||||
self.curr += delta;
|
self.curr += delta;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn step_hour(&mut self, span: Span, amount: i32) -> Result<(), Error<S>> {
|
fn step_hour(&mut self, span: Span, amount: i32) -> Result<()> {
|
||||||
let time = match self.curr_time {
|
let time = match self.curr_time {
|
||||||
Some(time) => time,
|
Some(time) => time,
|
||||||
None => return Err(self.err_time(span)),
|
None => return Err(self.err_time(span)),
|
||||||
};
|
};
|
||||||
|
|
||||||
let (days, time) = time.add_hours(amount.into());
|
let (days, time) = time.add_hours(amount);
|
||||||
self.curr += Duration::days(days);
|
self.curr += Duration::days(days.into());
|
||||||
self.curr_time = Some(time);
|
self.curr_time = Some(time);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn step_minute(&mut self, span: Span, amount: i32) -> Result<(), Error<S>> {
|
fn step_minute(&mut self, span: Span, amount: i32) -> Result<()> {
|
||||||
let time = match self.curr_time {
|
let time = match self.curr_time {
|
||||||
Some(time) => time,
|
Some(time) => time,
|
||||||
None => return Err(self.err_time(span)),
|
None => return Err(self.err_time(span)),
|
||||||
};
|
};
|
||||||
|
|
||||||
let (days, time) = time.add_minutes(amount.into());
|
let (days, time) = time.add_minutes(amount);
|
||||||
self.curr += Duration::days(days);
|
self.curr += Duration::days(days.into());
|
||||||
self.curr_time = Some(time);
|
self.curr_time = Some(time);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -284,21 +284,20 @@ impl<S: Copy> DeltaEval<S> {
|
||||||
let days = rest + (amount - 1) * 7;
|
let days = rest + (amount - 1) * 7;
|
||||||
self.curr += Duration::days(days.into());
|
self.curr += Duration::days(days.into());
|
||||||
} else if amount < 0 {
|
} else if amount < 0 {
|
||||||
let amount = -amount;
|
|
||||||
let rest: i32 = weekday.until(curr_wd).into();
|
let rest: i32 = weekday.until(curr_wd).into();
|
||||||
let days = rest + (amount - 1) * 7;
|
let days = rest + (amount - 1) * 7;
|
||||||
self.curr -= Duration::days(days.into());
|
self.curr -= Duration::days(days.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn step_time(&mut self, span: Span, time: Time) -> Result<(), Error<S>> {
|
fn step_time(&mut self, span: Span, time: Time) -> Result<()> {
|
||||||
let curr_time = match self.curr_time {
|
let curr_time = match self.curr_time {
|
||||||
Some(time) => time,
|
Some(time) => time,
|
||||||
None => return Err(self.err_time(span)),
|
None => return Err(self.err_time(span)),
|
||||||
};
|
};
|
||||||
|
|
||||||
if time < curr_time {
|
if time < curr_time {
|
||||||
self.curr = self.curr.succ_opt().unwrap();
|
self.curr = self.curr.succ();
|
||||||
}
|
}
|
||||||
self.curr_time = Some(time);
|
self.curr_time = Some(time);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -314,11 +313,11 @@ impl Delta {
|
||||||
self.steps.iter().map(|step| step.value.upper_bound()).sum()
|
self.steps.iter().map(|step| step.value.upper_bound()).sum()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply<S: Copy>(
|
fn apply(
|
||||||
&self,
|
&self,
|
||||||
index: S,
|
index: usize,
|
||||||
start: (NaiveDate, Option<Time>),
|
start: (NaiveDate, Option<Time>),
|
||||||
) -> Result<(NaiveDate, Option<Time>), Error<S>> {
|
) -> Result<(NaiveDate, Option<Time>)> {
|
||||||
let mut eval = DeltaEval::new(index, start.0, start.1);
|
let mut eval = DeltaEval::new(index, start.0, start.1);
|
||||||
for step in &self.steps {
|
for step in &self.steps {
|
||||||
eval.apply(step)?;
|
eval.apply(step)?;
|
||||||
|
|
@ -326,16 +325,16 @@ impl Delta {
|
||||||
Ok((eval.curr, eval.curr_time))
|
Ok((eval.curr, eval.curr_time))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn apply_date<S: Copy>(&self, index: S, date: NaiveDate) -> Result<NaiveDate, Error<S>> {
|
pub fn apply_date(&self, index: usize, date: NaiveDate) -> Result<NaiveDate> {
|
||||||
Ok(self.apply(index, (date, None))?.0)
|
Ok(self.apply(index, (date, None))?.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn apply_date_time<S: Copy>(
|
pub fn apply_date_time(
|
||||||
&self,
|
&self,
|
||||||
index: S,
|
index: usize,
|
||||||
date: NaiveDate,
|
date: NaiveDate,
|
||||||
time: Time,
|
time: Time,
|
||||||
) -> Result<(NaiveDate, Time), Error<S>> {
|
) -> Result<(NaiveDate, Time)> {
|
||||||
let (date, time) = self.apply(index, (date, Some(time)))?;
|
let (date, time) = self.apply(index, (date, Some(time)))?;
|
||||||
Ok((date, time.expect("time was not preserved")))
|
Ok((date, time.expect("time was not preserved")))
|
||||||
}
|
}
|
||||||
|
|
@ -347,7 +346,7 @@ mod tests {
|
||||||
|
|
||||||
use crate::files::primitives::{Span, Spanned, Time};
|
use crate::files::primitives::{Span, Spanned, Time};
|
||||||
|
|
||||||
use super::super::Error;
|
use super::super::Result;
|
||||||
use super::{Delta, DeltaStep as Step};
|
use super::{Delta, DeltaStep as Step};
|
||||||
|
|
||||||
const SPAN: Span = Span { start: 12, end: 34 };
|
const SPAN: Span = Span { start: 12, end: 34 };
|
||||||
|
|
@ -358,24 +357,21 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_d(step: Step, from: (i32, u32, u32)) -> Result<NaiveDate, Error<()>> {
|
fn apply_d(step: Step, from: (i32, u32, u32)) -> Result<NaiveDate> {
|
||||||
delta(step).apply_date((), NaiveDate::from_ymd_opt(from.0, from.1, from.2).unwrap())
|
delta(step).apply_date(0, NaiveDate::from_ymd(from.0, from.1, from.2))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_d(step: Step, from: (i32, u32, u32), expected: (i32, u32, u32)) {
|
fn test_d(step: Step, from: (i32, u32, u32), expected: (i32, u32, u32)) {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
apply_d(step, from).unwrap(),
|
apply_d(step, from).unwrap(),
|
||||||
NaiveDate::from_ymd_opt(expected.0, expected.1, expected.2).unwrap()
|
NaiveDate::from_ymd(expected.0, expected.1, expected.2)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_dt(
|
fn apply_dt(step: Step, from: (i32, u32, u32, u32, u32)) -> Result<(NaiveDate, Time)> {
|
||||||
step: Step,
|
|
||||||
from: (i32, u32, u32, u32, u32),
|
|
||||||
) -> Result<(NaiveDate, Time), Error<()>> {
|
|
||||||
delta(step).apply_date_time(
|
delta(step).apply_date_time(
|
||||||
(),
|
0,
|
||||||
NaiveDate::from_ymd_opt(from.0, from.1, from.2).unwrap(),
|
NaiveDate::from_ymd(from.0, from.1, from.2),
|
||||||
Time::new(from.3, from.4),
|
Time::new(from.3, from.4),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -385,7 +381,7 @@ mod tests {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
apply_dt(step, from).unwrap(),
|
apply_dt(step, from).unwrap(),
|
||||||
(
|
(
|
||||||
NaiveDate::from_ymd_opt(expected.0, expected.1, expected.2).unwrap(),
|
NaiveDate::from_ymd(expected.0, expected.1, expected.2),
|
||||||
Time::new(expected.3, expected.4)
|
Time::new(expected.3, expected.4)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
@ -567,43 +563,6 @@ mod tests {
|
||||||
assert!(apply_d(Step::Minute(0), (2021, 7, 3)).is_err());
|
assert!(apply_d(Step::Minute(0), (2021, 7, 3)).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn delta_weekday() {
|
|
||||||
use crate::files::primitives::Weekday::*;
|
|
||||||
|
|
||||||
test_d(Step::Weekday(-1, Friday), (2022, 3, 17), (2022, 3, 11));
|
|
||||||
test_d(Step::Weekday(-1, Saturday), (2022, 3, 17), (2022, 3, 12));
|
|
||||||
test_d(Step::Weekday(-1, Sunday), (2022, 3, 17), (2022, 3, 13));
|
|
||||||
test_d(Step::Weekday(-1, Monday), (2022, 3, 17), (2022, 3, 14));
|
|
||||||
test_d(Step::Weekday(-1, Tuesday), (2022, 3, 17), (2022, 3, 15));
|
|
||||||
test_d(Step::Weekday(-1, Wednesday), (2022, 3, 17), (2022, 3, 16));
|
|
||||||
test_d(Step::Weekday(-1, Thursday), (2022, 3, 17), (2022, 3, 17));
|
|
||||||
|
|
||||||
test_d(Step::Weekday(1, Thursday), (2022, 3, 17), (2022, 3, 17));
|
|
||||||
test_d(Step::Weekday(1, Friday), (2022, 3, 17), (2022, 3, 18));
|
|
||||||
test_d(Step::Weekday(1, Saturday), (2022, 3, 17), (2022, 3, 19));
|
|
||||||
test_d(Step::Weekday(1, Sunday), (2022, 3, 17), (2022, 3, 20));
|
|
||||||
test_d(Step::Weekday(1, Monday), (2022, 3, 17), (2022, 3, 21));
|
|
||||||
test_d(Step::Weekday(1, Tuesday), (2022, 3, 17), (2022, 3, 22));
|
|
||||||
test_d(Step::Weekday(1, Wednesday), (2022, 3, 17), (2022, 3, 23));
|
|
||||||
|
|
||||||
test_d(Step::Weekday(2, Thursday), (2022, 3, 17), (2022, 3, 24));
|
|
||||||
test_d(Step::Weekday(2, Friday), (2022, 3, 17), (2022, 3, 25));
|
|
||||||
test_d(Step::Weekday(2, Saturday), (2022, 3, 17), (2022, 3, 26));
|
|
||||||
test_d(Step::Weekday(2, Sunday), (2022, 3, 17), (2022, 3, 27));
|
|
||||||
test_d(Step::Weekday(2, Monday), (2022, 3, 17), (2022, 3, 28));
|
|
||||||
test_d(Step::Weekday(2, Tuesday), (2022, 3, 17), (2022, 3, 29));
|
|
||||||
test_d(Step::Weekday(2, Wednesday), (2022, 3, 17), (2022, 3, 30));
|
|
||||||
|
|
||||||
test_d(Step::Weekday(3, Thursday), (2022, 3, 17), (2022, 3, 31));
|
|
||||||
test_d(Step::Weekday(3, Friday), (2022, 3, 17), (2022, 4, 1));
|
|
||||||
test_d(Step::Weekday(3, Saturday), (2022, 3, 17), (2022, 4, 2));
|
|
||||||
test_d(Step::Weekday(3, Sunday), (2022, 3, 17), (2022, 4, 3));
|
|
||||||
test_d(Step::Weekday(3, Monday), (2022, 3, 17), (2022, 4, 4));
|
|
||||||
test_d(Step::Weekday(3, Tuesday), (2022, 3, 17), (2022, 4, 5));
|
|
||||||
test_d(Step::Weekday(3, Wednesday), (2022, 3, 17), (2022, 4, 6));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn delta_time() {
|
fn delta_time() {
|
||||||
test_dt(
|
test_dt(
|
||||||
|
|
|
||||||
|
|
@ -9,48 +9,24 @@ use super::range::DateRange;
|
||||||
pub enum EntryKind {
|
pub enum EntryKind {
|
||||||
Task,
|
Task,
|
||||||
TaskDone(NaiveDate),
|
TaskDone(NaiveDate),
|
||||||
TaskCanceled(NaiveDate),
|
|
||||||
Note,
|
Note,
|
||||||
Birthday(Option<i32>),
|
Birthday(Option<i32>),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A single instance of a command.
|
/// A single instance of a command.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub struct Entry {
|
pub struct Entry {
|
||||||
pub source: Source,
|
pub source: Source,
|
||||||
pub kind: EntryKind,
|
pub kind: EntryKind,
|
||||||
pub title: String,
|
|
||||||
pub has_description: bool,
|
|
||||||
pub dates: Option<Dates>,
|
pub dates: Option<Dates>,
|
||||||
/// Remind the user of an entry before it occurs. This date should always be
|
|
||||||
/// before the entry's start date, or `None` if there is no start date.
|
|
||||||
pub remind: Option<NaiveDate>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Entry {
|
impl Entry {
|
||||||
pub fn new(
|
pub fn new(source: Source, kind: EntryKind, dates: Option<Dates>) -> Self {
|
||||||
source: Source,
|
|
||||||
kind: EntryKind,
|
|
||||||
title: String,
|
|
||||||
has_description: bool,
|
|
||||||
dates: Option<Dates>,
|
|
||||||
remind: Option<NaiveDate>,
|
|
||||||
) -> Self {
|
|
||||||
if let Some(dates) = dates {
|
|
||||||
if let Some(remind) = remind {
|
|
||||||
assert!(remind < dates.sorted().root());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
assert!(remind.is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
source,
|
source,
|
||||||
kind,
|
kind,
|
||||||
title,
|
|
||||||
has_description,
|
|
||||||
dates,
|
dates,
|
||||||
remind,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,7 +37,6 @@ impl Entry {
|
||||||
|
|
||||||
/// Mode that determines how entries are filtered when they are added to
|
/// Mode that determines how entries are filtered when they are added to
|
||||||
/// an [`Entries`].
|
/// an [`Entries`].
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum EntryMode {
|
pub enum EntryMode {
|
||||||
/// The entry's root date must be contained in the range.
|
/// The entry's root date must be contained in the range.
|
||||||
|
|
@ -118,17 +93,8 @@ impl Entries {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let (Some(remind), Some(dates)) = (entry.remind, entry.dates) {
|
|
||||||
let (_, end) = dates.sorted().dates();
|
|
||||||
let remind_before = remind <= self.range.until();
|
|
||||||
let entry_before = end < self.range.from();
|
|
||||||
if remind_before && !entry_before {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tasks that were finished inside the range
|
// Tasks that were finished inside the range
|
||||||
if let EntryKind::TaskDone(done) | EntryKind::TaskCanceled(done) = entry.kind {
|
if let EntryKind::TaskDone(done) = entry.kind {
|
||||||
if self.range.contains(done) {
|
if self.range.contains(done) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,15 @@
|
||||||
use chrono::NaiveDate;
|
use std::result;
|
||||||
use codespan_reporting::diagnostic::{Diagnostic, Label};
|
|
||||||
use codespan_reporting::files::Files;
|
use chrono::NaiveDate;
|
||||||
use codespan_reporting::term::Config;
|
|
||||||
|
|
||||||
use crate::error::Eprint;
|
|
||||||
use crate::files::primitives::{Span, Time};
|
use crate::files::primitives::{Span, Time};
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum Error<S> {
|
pub enum Error {
|
||||||
/// A delta step resulted in an invalid date.
|
/// A delta step resulted in an invalid date.
|
||||||
#[error("delta step resulted in invalid date")]
|
#[error("delta step resulted in invalid date")]
|
||||||
DeltaInvalidStep {
|
DeltaInvalidStep {
|
||||||
index: S,
|
index: usize,
|
||||||
span: Span,
|
span: Span,
|
||||||
start: NaiveDate,
|
start: NaiveDate,
|
||||||
start_time: Option<Time>,
|
start_time: Option<Time>,
|
||||||
|
|
@ -21,7 +19,7 @@ pub enum Error<S> {
|
||||||
/// A time-based delta step was applied to a date without time.
|
/// A time-based delta step was applied to a date without time.
|
||||||
#[error("time-based delta step applied to date without time")]
|
#[error("time-based delta step applied to date without time")]
|
||||||
DeltaNoTime {
|
DeltaNoTime {
|
||||||
index: S,
|
index: usize,
|
||||||
span: Span,
|
span: Span,
|
||||||
start: NaiveDate,
|
start: NaiveDate,
|
||||||
prev: NaiveDate,
|
prev: NaiveDate,
|
||||||
|
|
@ -31,17 +29,7 @@ pub enum Error<S> {
|
||||||
/// in time (`to < from`).
|
/// in time (`to < from`).
|
||||||
#[error("repeat delta did not move forwards")]
|
#[error("repeat delta did not move forwards")]
|
||||||
RepeatDidNotMoveForwards {
|
RepeatDidNotMoveForwards {
|
||||||
index: S,
|
index: usize,
|
||||||
span: Span,
|
|
||||||
from: NaiveDate,
|
|
||||||
to: NaiveDate,
|
|
||||||
},
|
|
||||||
/// A `REMIND`'s delta did not move backwards in time from the entry's start
|
|
||||||
/// date. Instead, it either remained at the start date (`to == from`) or
|
|
||||||
/// moved forwards in time (`from < to`).
|
|
||||||
#[error("remind delta did not move backwards")]
|
|
||||||
RemindDidNotMoveBackwards {
|
|
||||||
index: S,
|
|
||||||
span: Span,
|
span: Span,
|
||||||
from: NaiveDate,
|
from: NaiveDate,
|
||||||
to: NaiveDate,
|
to: NaiveDate,
|
||||||
|
|
@ -49,48 +37,60 @@ pub enum Error<S> {
|
||||||
/// A `MOVE a TO b` statement was executed, but there was no entry at the
|
/// A `MOVE a TO b` statement was executed, but there was no entry at the
|
||||||
/// date `a`.
|
/// date `a`.
|
||||||
#[error("tried to move nonexisting entry")]
|
#[error("tried to move nonexisting entry")]
|
||||||
MoveWithoutSource { index: S, span: Span },
|
MoveWithoutSource { index: usize, span: Span },
|
||||||
/// A `MOVE a TO b` statement was executed where `b` contains a time but `a`
|
|
||||||
/// doesn't was executed.
|
|
||||||
#[error("tried to move un-timed entry to new time")]
|
|
||||||
TimedMoveWithoutTime { index: S, span: Span },
|
|
||||||
/// A division by zero has occurred.
|
/// A division by zero has occurred.
|
||||||
#[error("tried to divide by zero")]
|
#[error("tried to divide by zero")]
|
||||||
DivByZero {
|
DivByZero {
|
||||||
index: S,
|
index: usize,
|
||||||
span: Span,
|
span: Span,
|
||||||
date: NaiveDate,
|
date: NaiveDate,
|
||||||
},
|
},
|
||||||
/// A modulo operation by zero has occurred.
|
/// A modulo operation by zero has occurred.
|
||||||
#[error("tried to modulo by zero")]
|
#[error("tried to modulo by zero")]
|
||||||
ModByZero {
|
ModByZero {
|
||||||
index: S,
|
index: usize,
|
||||||
span: Span,
|
span: Span,
|
||||||
date: NaiveDate,
|
date: NaiveDate,
|
||||||
},
|
},
|
||||||
/// Easter calculation failed.
|
/// Easter calculation failed.
|
||||||
#[error("easter calculation failed")]
|
#[error("easter calculation failed")]
|
||||||
Easter {
|
Easter {
|
||||||
index: S,
|
index: usize,
|
||||||
span: Span,
|
span: Span,
|
||||||
date: NaiveDate,
|
date: NaiveDate,
|
||||||
msg: &'static str,
|
msg: &'static str,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S> Error<S> {
|
pub struct SourceInfo<'a> {
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub content: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error {
|
||||||
|
fn print_at<'a>(sources: &[SourceInfo<'a>], index: &usize, span: &Span, message: String) {
|
||||||
|
use pest::error as pe;
|
||||||
|
|
||||||
|
let source = sources.get(*index).expect("index is valid");
|
||||||
|
let span = pest::Span::new(source.content, span.start, span.end).expect("span is valid");
|
||||||
|
let variant = pe::ErrorVariant::<()>::CustomError { message };
|
||||||
|
let mut error = pe::Error::new_from_span(variant, span);
|
||||||
|
if let Some(name) = &source.name {
|
||||||
|
error = error.with_path(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
eprintln!("{}", error);
|
||||||
|
}
|
||||||
|
|
||||||
fn fmt_date_time(date: NaiveDate, time: Option<Time>) -> String {
|
fn fmt_date_time(date: NaiveDate, time: Option<Time>) -> String {
|
||||||
match time {
|
match time {
|
||||||
None => format!("{}", date),
|
None => format!("{}", date),
|
||||||
Some(time) => format!("{} {}", date, time),
|
Some(time) => format!("{} {}", date, time),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, F: Files<'a>> Eprint<'a, F> for Error<F::FileId> {
|
pub fn print<'a>(&self, sources: &[SourceInfo<'a>]) {
|
||||||
#[allow(single_use_lifetimes)]
|
match self {
|
||||||
fn eprint<'f: 'a>(&self, files: &'f F, config: &Config) {
|
|
||||||
let diagnostic = match self {
|
|
||||||
Error::DeltaInvalidStep {
|
Error::DeltaInvalidStep {
|
||||||
index,
|
index,
|
||||||
span,
|
span,
|
||||||
|
|
@ -99,70 +99,78 @@ impl<'a, F: Files<'a>> Eprint<'a, F> for Error<F::FileId> {
|
||||||
prev,
|
prev,
|
||||||
prev_time,
|
prev_time,
|
||||||
} => {
|
} => {
|
||||||
let start_str = Self::fmt_date_time(*start, *start_time);
|
let msg = format!(
|
||||||
let prev_str = Self::fmt_date_time(*prev, *prev_time);
|
"Delta step resulted in invalid date\
|
||||||
Diagnostic::error()
|
\nInitial start: {}\
|
||||||
.with_message("Delta step resulted in invalid date")
|
\nPrevious step: {}",
|
||||||
.with_labels(vec![Label::primary(*index, span)])
|
Self::fmt_date_time(*start, *start_time),
|
||||||
.with_notes(vec![
|
Self::fmt_date_time(*prev, *prev_time),
|
||||||
format!("Date before applying delta: {start_str}"),
|
);
|
||||||
format!("Date before applying this step: {prev_str}"),
|
Self::print_at(sources, index, span, msg);
|
||||||
])
|
|
||||||
}
|
}
|
||||||
Error::DeltaNoTime {
|
Error::DeltaNoTime {
|
||||||
index,
|
index,
|
||||||
span,
|
span,
|
||||||
start,
|
start,
|
||||||
prev,
|
prev,
|
||||||
} => Diagnostic::error()
|
} => {
|
||||||
.with_message("Time-based delta step applied to date without time")
|
let msg = format!(
|
||||||
.with_labels(vec![Label::primary(*index, span)])
|
"Time-based delta step applied to date without time\
|
||||||
.with_notes(vec![
|
\nInitial start: {}\
|
||||||
format!("Date before applying delta: {start}"),
|
\nPrevious step: {}",
|
||||||
format!("Date before applying this step: {prev}"),
|
start, prev
|
||||||
]),
|
);
|
||||||
|
Self::print_at(sources, index, span, msg);
|
||||||
|
}
|
||||||
Error::RepeatDidNotMoveForwards {
|
Error::RepeatDidNotMoveForwards {
|
||||||
index,
|
index,
|
||||||
span,
|
span,
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
} => Diagnostic::error()
|
} => {
|
||||||
.with_message("Repeat delta did not move forwards")
|
let msg = format!(
|
||||||
.with_labels(vec![Label::primary(*index, span)])
|
"Repeat delta did not move forwards\
|
||||||
.with_notes(vec![format!("Moved from {from} to {to}")]),
|
\nMoved from {} to {}",
|
||||||
Error::RemindDidNotMoveBackwards {
|
from, to
|
||||||
index,
|
);
|
||||||
span,
|
Self::print_at(sources, index, span, msg);
|
||||||
from,
|
}
|
||||||
to,
|
Error::MoveWithoutSource { index, span } => {
|
||||||
} => Diagnostic::error()
|
let msg = "Tried to move nonexisting entry".to_string();
|
||||||
.with_message("Remind delta did not move backwards")
|
Self::print_at(sources, index, span, msg);
|
||||||
.with_labels(vec![Label::primary(*index, span)])
|
}
|
||||||
.with_notes(vec![format!("Moved from {from} to {to}")]),
|
Error::DivByZero { index, span, date } => {
|
||||||
Error::MoveWithoutSource { index, span } => Diagnostic::error()
|
let msg = format!(
|
||||||
.with_message("Tried to move nonexistent entry")
|
"Tried to divide by zero\
|
||||||
.with_labels(vec![Label::primary(*index, span)]),
|
\nAt date: {}",
|
||||||
Error::TimedMoveWithoutTime { index, span } => Diagnostic::error()
|
date
|
||||||
.with_message("Tried to move un-timed entry to new time")
|
);
|
||||||
.with_labels(vec![Label::primary(*index, span)]),
|
Self::print_at(sources, index, span, msg);
|
||||||
Error::DivByZero { index, span, date } => Diagnostic::error()
|
}
|
||||||
.with_message("Tried to divide by zero")
|
Error::ModByZero { index, span, date } => {
|
||||||
.with_labels(vec![Label::primary(*index, span)])
|
let msg = format!(
|
||||||
.with_notes(vec![format!("At date: {date}")]),
|
"Tried to modulo by zero\
|
||||||
Error::ModByZero { index, span, date } => Diagnostic::error()
|
\nAt date: {}",
|
||||||
.with_message("Tried to modulo by zero")
|
date
|
||||||
.with_labels(vec![Label::primary(*index, span)])
|
);
|
||||||
.with_notes(vec![format!("At date: {date}")]),
|
Self::print_at(sources, index, span, msg);
|
||||||
|
}
|
||||||
Error::Easter {
|
Error::Easter {
|
||||||
index,
|
index,
|
||||||
span,
|
span,
|
||||||
date,
|
date,
|
||||||
msg,
|
msg,
|
||||||
} => Diagnostic::error()
|
} => {
|
||||||
.with_message("Failed to calculate easter")
|
let msg = format!(
|
||||||
.with_labels(vec![Label::primary(*index, span)])
|
"Failed to calculate easter\
|
||||||
.with_notes(vec![format!("At date: {date}"), format!("Reason: {msg}")]),
|
\nAt date: {}\
|
||||||
};
|
\nReason: {}",
|
||||||
Self::eprint_diagnostic(files, config, &diagnostic);
|
date, msg
|
||||||
|
);
|
||||||
|
Self::print_at(sources, index, span, msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub type Result<T> = result::Result<T, Error>;
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,6 @@ impl DateRange {
|
||||||
/// Return a new range with its [`Self::until`] set to a new value.
|
/// Return a new range with its [`Self::until`] set to a new value.
|
||||||
///
|
///
|
||||||
/// Returns [`None`] if the new value is earlier than [`Self::from`].
|
/// Returns [`None`] if the new value is earlier than [`Self::from`].
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn with_until(&self, until: NaiveDate) -> Option<Self> {
|
pub fn with_until(&self, until: NaiveDate) -> Option<Self> {
|
||||||
if self.from <= until {
|
if self.from <= until {
|
||||||
Some(Self::new(self.from, until))
|
Some(Self::new(self.from, until))
|
||||||
|
|
@ -76,7 +75,7 @@ impl DateRange {
|
||||||
|
|
||||||
pub fn days(&self) -> impl Iterator<Item = NaiveDate> {
|
pub fn days(&self) -> impl Iterator<Item = NaiveDate> {
|
||||||
(self.from.num_days_from_ce()..=self.until.num_days_from_ce())
|
(self.from.num_days_from_ce()..=self.until.num_days_from_ce())
|
||||||
.map(|days| NaiveDate::from_num_days_from_ce_opt(days).unwrap())
|
.map(NaiveDate::from_num_days_from_ce)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn years(&self) -> RangeInclusive<i32> {
|
pub fn years(&self) -> RangeInclusive<i32> {
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,13 @@ pub fn is_iso_leap_year(year: i32) -> bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn year_length(year: i32) -> u32 {
|
pub fn year_length(year: i32) -> u32 {
|
||||||
NaiveDate::from_ymd_opt(year, 12, 31).unwrap().ordinal()
|
NaiveDate::from_ymd(year, 12, 31).ordinal()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn month_length(year: i32, month: u32) -> u32 {
|
pub fn month_length(year: i32, month: u32) -> u32 {
|
||||||
NaiveDate::from_ymd_opt(year, month + 1, 1)
|
NaiveDate::from_ymd_opt(year, month + 1, 1)
|
||||||
.unwrap_or_else(|| NaiveDate::from_ymd_opt(year + 1, 1, 1).unwrap())
|
.unwrap_or_else(|| NaiveDate::from_ymd(year + 1, 1, 1))
|
||||||
.pred_opt()
|
.pred()
|
||||||
.unwrap()
|
|
||||||
.day()
|
.day()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
444
src/files.rs
444
src/files.rs
|
|
@ -1,48 +1,40 @@
|
||||||
use std::collections::hash_map::Entry;
|
use std::collections::HashMap;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::{fs, result};
|
|
||||||
|
|
||||||
use chrono::{DateTime, NaiveDate, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use codespan_reporting::files::SimpleFiles;
|
|
||||||
use tzfile::Tz;
|
use tzfile::Tz;
|
||||||
|
|
||||||
use self::commands::{Command, Done, File, Log};
|
use crate::eval::SourceInfo;
|
||||||
pub use self::error::{Error, ParseError, Result};
|
|
||||||
use self::primitives::Spanned;
|
|
||||||
|
|
||||||
pub mod cli;
|
use self::commands::{Command, Done, File};
|
||||||
|
pub use self::error::{Error, Result};
|
||||||
|
|
||||||
|
pub mod arguments;
|
||||||
pub mod commands;
|
pub mod commands;
|
||||||
mod error;
|
mod error;
|
||||||
mod format;
|
mod format;
|
||||||
mod parse;
|
mod parse;
|
||||||
pub mod primitives;
|
pub mod primitives;
|
||||||
|
|
||||||
// TODO Move file content from `File` to `LoadedFile`
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct LoadedFile {
|
struct LoadedFile {
|
||||||
/// User-readable path for this file.
|
/// Canonical path for this file
|
||||||
|
path: PathBuf,
|
||||||
|
// User-readable path for this file
|
||||||
name: PathBuf,
|
name: PathBuf,
|
||||||
/// Identifier for codespan-reporting.
|
|
||||||
cs_id: usize,
|
|
||||||
file: File,
|
file: File,
|
||||||
/// Whether this file has been changed.
|
/// Whether this file has been changed
|
||||||
dirty: bool,
|
dirty: bool,
|
||||||
/// Commands that have been removed and are to be skipped during formatting.
|
|
||||||
///
|
|
||||||
/// They are not directly removed from the list of commands in order not to
|
|
||||||
/// change other commands' indices.
|
|
||||||
removed: HashSet<usize>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LoadedFile {
|
impl LoadedFile {
|
||||||
pub fn new(name: PathBuf, cs_id: usize, file: File) -> Self {
|
pub fn new(path: PathBuf, name: PathBuf, file: File) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
path,
|
||||||
name,
|
name,
|
||||||
cs_id,
|
|
||||||
file,
|
file,
|
||||||
dirty: false,
|
dirty: false,
|
||||||
removed: HashSet::new(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -53,122 +45,43 @@ pub struct Source {
|
||||||
command: usize,
|
command: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Rename to `SourceFile`?
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub struct FileSource(usize);
|
|
||||||
|
|
||||||
impl Source {
|
impl Source {
|
||||||
pub fn new(file: usize, command: usize) -> Self {
|
pub fn file(&self) -> usize {
|
||||||
Self { file, command }
|
self.file
|
||||||
}
|
|
||||||
|
|
||||||
pub fn file(&self) -> FileSource {
|
|
||||||
FileSource(self.file)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Sourced<'a, T> {
|
pub struct SourcedCommand<'a> {
|
||||||
pub source: Source,
|
pub source: Source,
|
||||||
pub value: &'a T,
|
pub command: &'a Command,
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, T> Sourced<'a, T> {
|
|
||||||
fn new(source: Source, value: &'a T) -> Self {
|
|
||||||
Self { source, value }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Files {
|
pub struct Files {
|
||||||
files: Vec<LoadedFile>,
|
files: Vec<LoadedFile>,
|
||||||
/// Codespan-reporting file database.
|
timezone: Tz,
|
||||||
cs_files: SimpleFiles<String, String>,
|
|
||||||
timezone: Option<Tz>,
|
|
||||||
capture: Option<usize>,
|
|
||||||
logs: HashMap<NaiveDate, Source>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> codespan_reporting::files::Files<'a> for Files {
|
|
||||||
type FileId = FileSource;
|
|
||||||
type Name = String;
|
|
||||||
type Source = &'a str;
|
|
||||||
|
|
||||||
fn name(
|
|
||||||
&'a self,
|
|
||||||
id: Self::FileId,
|
|
||||||
) -> result::Result<Self::Name, codespan_reporting::files::Error> {
|
|
||||||
self.cs_files.name(self.cs_id(id))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn source(
|
|
||||||
&'a self,
|
|
||||||
id: Self::FileId,
|
|
||||||
) -> result::Result<Self::Source, codespan_reporting::files::Error> {
|
|
||||||
self.cs_files.source(self.cs_id(id))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn line_index(
|
|
||||||
&'a self,
|
|
||||||
id: Self::FileId,
|
|
||||||
byte_index: usize,
|
|
||||||
) -> result::Result<usize, codespan_reporting::files::Error> {
|
|
||||||
self.cs_files.line_index(self.cs_id(id), byte_index)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn line_range(
|
|
||||||
&'a self,
|
|
||||||
id: Self::FileId,
|
|
||||||
line_index: usize,
|
|
||||||
) -> result::Result<std::ops::Range<usize>, codespan_reporting::files::Error> {
|
|
||||||
self.cs_files.line_range(self.cs_id(id), line_index)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Files {
|
impl Files {
|
||||||
/* Loading */
|
pub fn load(path: &Path) -> Result<Self> {
|
||||||
|
let mut paths = HashMap::new();
|
||||||
pub fn new() -> Self {
|
let mut files = vec![];
|
||||||
Self {
|
Self::load_file(&mut paths, &mut files, path)?;
|
||||||
files: vec![],
|
let timezone = Self::determine_timezone(&files)?;
|
||||||
cs_files: SimpleFiles::new(),
|
Ok(Self { files, timezone })
|
||||||
timezone: None,
|
|
||||||
capture: None,
|
|
||||||
logs: HashMap::new(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load a file and all its includes.
|
fn load_file(
|
||||||
///
|
paths: &mut HashMap<PathBuf, usize>,
|
||||||
/// # Warning
|
files: &mut Vec<LoadedFile>,
|
||||||
///
|
name: &Path,
|
||||||
/// - This function must be called before all other functions.
|
) -> Result<()> {
|
||||||
/// - This function must only be called once.
|
|
||||||
/// - If this function fails,
|
|
||||||
/// - it is safe to print the error using the [`codespan_reporting::files::Files`] instance and
|
|
||||||
/// - no other functions may be called.
|
|
||||||
pub fn load(&mut self, path: &Path) -> Result<()> {
|
|
||||||
if !self.files.is_empty() {
|
|
||||||
panic!("Files::load called multiple times");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track already loaded files by their normalized paths
|
|
||||||
let mut loaded = HashSet::new();
|
|
||||||
|
|
||||||
self.load_file(&mut loaded, path)?;
|
|
||||||
self.determine_timezone()?;
|
|
||||||
self.determine_capture()?;
|
|
||||||
self.collect_logs()?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_file(&mut self, loaded: &mut HashSet<PathBuf>, name: &Path) -> Result<()> {
|
|
||||||
let path = name.canonicalize().map_err(|e| Error::ResolvePath {
|
let path = name.canonicalize().map_err(|e| Error::ResolvePath {
|
||||||
path: name.to_path_buf(),
|
path: name.to_path_buf(),
|
||||||
error: e,
|
error: e,
|
||||||
})?;
|
})?;
|
||||||
if loaded.contains(&path) {
|
if paths.contains_key(&path) {
|
||||||
// We've already loaded this exact file.
|
// We've already loaded this exact file.
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
@ -177,263 +90,90 @@ impl Files {
|
||||||
file: path.clone(),
|
file: path.clone(),
|
||||||
error: e,
|
error: e,
|
||||||
})?;
|
})?;
|
||||||
let cs_id = self
|
|
||||||
.cs_files
|
|
||||||
.add(name.to_string_lossy().to_string(), content.clone());
|
|
||||||
|
|
||||||
// Using `name` instead of `path` for the unwrap below.
|
// Using `name` instead of `path` for the unwrap below.
|
||||||
let file = match parse::parse(name, &content) {
|
let file = parse::parse(name, &content)?;
|
||||||
Ok(file) => file,
|
let includes = file.includes.clone();
|
||||||
Err(error) => {
|
|
||||||
// Using a dummy file. This should be fine since we return an
|
|
||||||
// error immediately after and the user must never call `load`
|
|
||||||
// twice. Otherwise, we run the danger of overwriting a file
|
|
||||||
// with empty content.
|
|
||||||
self.files
|
|
||||||
.push(LoadedFile::new(name.to_owned(), cs_id, File::dummy()));
|
|
||||||
return Err(Error::Parse {
|
|
||||||
file: FileSource(self.files.len() - 1),
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let includes = file
|
paths.insert(path.clone(), files.len());
|
||||||
.commands
|
files.push(LoadedFile::new(path, name.to_owned(), file));
|
||||||
.iter()
|
|
||||||
.filter_map(|c| match &c.value {
|
|
||||||
Command::Include(path) => Some(path.clone()),
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
loaded.insert(path);
|
|
||||||
self.files
|
|
||||||
.push(LoadedFile::new(name.to_owned(), cs_id, file));
|
|
||||||
|
|
||||||
for include in includes {
|
for include in includes {
|
||||||
// Since we've successfully opened the file, its name can't be the
|
// Since we've successfully opened the file, its name can't be the
|
||||||
// root directory or empty string and it must thus have a parent.
|
// root directory or empty string and must thus have a parent.
|
||||||
let include_path = name.parent().unwrap().join(include.value);
|
let include_path = name.parent().unwrap().join(include);
|
||||||
self.load_file(loaded, &include_path)?;
|
Self::load_file(paths, files, &include_path)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn determine_timezone(&mut self) -> Result<()> {
|
fn determine_timezone(files: &[LoadedFile]) -> Result<Tz> {
|
||||||
assert_eq!(self.timezone, None);
|
let mut found: Option<(PathBuf, String)> = None;
|
||||||
|
|
||||||
let mut found: Option<(Source, Spanned<String>)> = None;
|
for file in files {
|
||||||
|
if let Some(file_tz) = &file.file.timezone {
|
||||||
for command in self.commands() {
|
if let Some((found_name, found_tz)) = &found {
|
||||||
if let Command::Timezone(tz) = &command.value.value {
|
if found_tz != file_tz {
|
||||||
if let Some((found_source, found_tz)) = &found {
|
|
||||||
if tz.value != found_tz.value {
|
|
||||||
return Err(Error::TzConflict {
|
return Err(Error::TzConflict {
|
||||||
file1: found_source.file(),
|
file1: found_name.clone(),
|
||||||
span1: found_tz.span,
|
tz1: found_tz.clone(),
|
||||||
tz1: found_tz.value.clone(),
|
file2: file.name.clone(),
|
||||||
file2: command.source.file(),
|
tz2: file_tz.clone(),
|
||||||
span2: tz.span,
|
|
||||||
tz2: tz.value.clone(),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
found = Some((command.source, tz.clone()));
|
found = Some((file.name.clone(), file_tz.clone()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let timezone = if let Some((source, tz)) = found {
|
Ok(if let Some((_, tz)) = found {
|
||||||
Tz::named(&tz.value).map_err(|error| Error::ResolveTz {
|
Tz::named(&tz).map_err(|e| Error::ResolveTz {
|
||||||
file: source.file(),
|
timezone: tz,
|
||||||
span: tz.span,
|
error: e,
|
||||||
tz: tz.value,
|
|
||||||
error,
|
|
||||||
})?
|
})?
|
||||||
} else {
|
} else {
|
||||||
Tz::local().map_err(|error| Error::LocalTz { error })?
|
Tz::local().map_err(|e| Error::LocalTz { error: e })?
|
||||||
};
|
})
|
||||||
self.timezone = Some(timezone);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn determine_capture(&mut self) -> Result<()> {
|
|
||||||
assert_eq!(self.capture, None);
|
|
||||||
|
|
||||||
let mut found: Option<Source> = None;
|
|
||||||
|
|
||||||
for command in self.commands() {
|
|
||||||
if let Command::Capture = &command.value.value {
|
|
||||||
if let Some(found) = &found {
|
|
||||||
let found_cmd = self.command(*found);
|
|
||||||
return Err(Error::MultipleCapture {
|
|
||||||
file1: found.file(),
|
|
||||||
span1: found_cmd.value.span,
|
|
||||||
file2: command.source.file(),
|
|
||||||
span2: command.value.span,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
found = Some(command.source);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.capture = found.map(|s| s.file);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn collect_logs(&mut self) -> Result<()> {
|
|
||||||
for command in Self::commands_of_files(&self.files) {
|
|
||||||
if let Command::Log(log) = &command.value.value {
|
|
||||||
match self.logs.entry(log.date.value) {
|
|
||||||
Entry::Vacant(e) => {
|
|
||||||
e.insert(command.source);
|
|
||||||
}
|
|
||||||
Entry::Occupied(e) => {
|
|
||||||
let other_cmd = Self::command_of_files(&self.files, *e.get());
|
|
||||||
let other_span = match &other_cmd.value.value {
|
|
||||||
Command::Log(log) => log.date.span,
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
return Err(Error::LogConflict {
|
|
||||||
file1: other_cmd.source.file(),
|
|
||||||
span1: other_span,
|
|
||||||
file2: command.source.file(),
|
|
||||||
span2: log.date.span,
|
|
||||||
date: log.date.value,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Saving */
|
|
||||||
|
|
||||||
pub fn save(&self) -> Result<()> {
|
pub fn save(&self) -> Result<()> {
|
||||||
for file in &self.files {
|
for file in &self.files {
|
||||||
if file.dirty {
|
if file.dirty {
|
||||||
self.save_file(file)?;
|
println!("Saving file {:?}", file.path);
|
||||||
|
Self::save_file(&file.path, &file.file)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save_file(&self, file: &LoadedFile) -> Result<()> {
|
fn save_file(path: &Path, file: &File) -> Result<()> {
|
||||||
// TODO Sort commands within file
|
fs::write(path, &format!("{}", file)).map_err(|e| Error::WriteFile {
|
||||||
|
file: path.to_path_buf(),
|
||||||
let previous = self
|
error: e,
|
||||||
.cs_files
|
})?;
|
||||||
.get(file.cs_id)
|
|
||||||
.expect("cs id is valid")
|
|
||||||
.source();
|
|
||||||
|
|
||||||
let formatted = file.file.format(&file.removed);
|
|
||||||
|
|
||||||
if previous == &formatted {
|
|
||||||
println!("Unchanged file {:?}", file.name);
|
|
||||||
} else {
|
|
||||||
println!("Saving file {:?}", file.name);
|
|
||||||
fs::write(&file.name, &formatted).map_err(|e| Error::WriteFile {
|
|
||||||
file: file.name.to_path_buf(),
|
|
||||||
error: e,
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Querying */
|
|
||||||
|
|
||||||
fn commands_of_files(files: &[LoadedFile]) -> Vec<Sourced<'_, Spanned<Command>>> {
|
|
||||||
let mut result = vec![];
|
|
||||||
for (file_index, file) in files.iter().enumerate() {
|
|
||||||
for (command_index, command) in file.file.commands.iter().enumerate() {
|
|
||||||
let source = Source::new(file_index, command_index);
|
|
||||||
result.push(Sourced::new(source, command));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn commands(&self) -> Vec<Sourced<'_, Spanned<Command>>> {
|
|
||||||
Self::commands_of_files(&self.files)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn command_of_files(files: &[LoadedFile], source: Source) -> Sourced<'_, Spanned<Command>> {
|
|
||||||
let command = &files[source.file].file.commands[source.command];
|
|
||||||
Sourced::new(source, command)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn command(&self, source: Source) -> Sourced<'_, Spanned<Command>> {
|
|
||||||
Self::command_of_files(&self.files, source)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn log(&self, date: NaiveDate) -> Option<Sourced<'_, Log>> {
|
|
||||||
let source = *self.logs.get(&date)?;
|
|
||||||
match &self.command(source).value.value {
|
|
||||||
Command::Log(log) => Some(Sourced::new(source, log)),
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 capture(&self) -> Option<FileSource> {
|
|
||||||
self.capture.map(FileSource)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn now(&self) -> DateTime<&Tz> {
|
|
||||||
if let Some(tz) = &self.timezone {
|
|
||||||
Utc::now().with_timezone(&tz)
|
|
||||||
} else {
|
|
||||||
panic!("Called Files::now before Files::load");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Updating */
|
|
||||||
|
|
||||||
pub fn mark_all_dirty(&mut self) {
|
pub fn mark_all_dirty(&mut self) {
|
||||||
for file in self.files.iter_mut() {
|
for file in self.files.iter_mut() {
|
||||||
file.dirty = true;
|
file.dirty = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn modify(&mut self, source: Source, edit: impl FnOnce(&mut Command)) {
|
pub fn command(&self, source: Source) -> &Command {
|
||||||
let file = &mut self.files[source.file];
|
&self.files[source.file].file.commands[source.command]
|
||||||
edit(&mut file.file.commands[source.command].value);
|
|
||||||
file.dirty = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert(&mut self, file: FileSource, command: Command) {
|
pub fn sources(&self) -> Vec<SourceInfo<'_>> {
|
||||||
let file = &mut self.files[file.0];
|
self.files
|
||||||
file.file.commands.push(Spanned::dummy(command));
|
.iter()
|
||||||
file.dirty = true;
|
.map(|f| SourceInfo {
|
||||||
}
|
name: Some(f.name.to_string_lossy().to_string()),
|
||||||
|
content: &f.file.contents,
|
||||||
fn remove(&mut self, source: Source) {
|
})
|
||||||
let file = &mut self.files[source.file];
|
.collect()
|
||||||
file.removed.insert(source.command);
|
|
||||||
file.dirty = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a [`Done`] statement to the task identified by `source`.
|
/// Add a [`Done`] statement to the task identified by `source`.
|
||||||
|
|
@ -443,41 +183,29 @@ impl Files {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn add_done(&mut self, source: Source, done: Done) -> bool {
|
pub fn add_done(&mut self, source: Source, done: Done) -> bool {
|
||||||
let file = &mut self.files[source.file];
|
let file = &mut self.files[source.file];
|
||||||
match &mut file.file.commands[source.command].value {
|
match &mut file.file.commands[source.command] {
|
||||||
Command::Task(t) => t.done.push(done),
|
Command::Task(t) => t.done.push(done),
|
||||||
_ => return false,
|
Command::Note(_) => return false,
|
||||||
}
|
}
|
||||||
file.dirty = true;
|
file.dirty = true;
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_log(&mut self, date: NaiveDate, desc: Vec<String>) {
|
pub fn commands(&self) -> Vec<SourcedCommand<'_>> {
|
||||||
if let Some(source) = self.logs.get(&date).cloned() {
|
let mut result = vec![];
|
||||||
if desc.is_empty() {
|
for (file_index, file) in self.files.iter().enumerate() {
|
||||||
self.remove(source);
|
for (command_index, command) in file.file.commands.iter().enumerate() {
|
||||||
} else {
|
let source = Source {
|
||||||
self.modify(source, |command| match command {
|
file: file_index,
|
||||||
Command::Log(log) => log.desc = desc,
|
command: command_index,
|
||||||
_ => unreachable!(),
|
};
|
||||||
});
|
result.push(SourcedCommand { source, command });
|
||||||
}
|
}
|
||||||
} else if !desc.is_empty() {
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Errors */
|
pub fn now(&self) -> DateTime<&Tz> {
|
||||||
|
Utc::now().with_timezone(&&self.timezone)
|
||||||
fn cs_id(&self, file: FileSource) -> usize {
|
|
||||||
self.files[file.0].cs_id
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
96
src/files/arguments.rs
Normal file
96
src/files/arguments.rs
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use chrono::NaiveDate;
|
||||||
|
use pest::iterators::Pair;
|
||||||
|
use pest::Parser;
|
||||||
|
|
||||||
|
use super::commands::Delta;
|
||||||
|
use super::parse::{self, Error, Result, Rule, TodayfileParser};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum RangeDate {
|
||||||
|
Date(NaiveDate),
|
||||||
|
Today,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Range {
|
||||||
|
pub start: RangeDate,
|
||||||
|
pub start_delta: Option<Delta>,
|
||||||
|
pub end: Option<RangeDate>,
|
||||||
|
pub end_delta: Option<Delta>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Parsing */
|
||||||
|
|
||||||
|
fn parse_range_date(p: Pair<'_, Rule>) -> Result<RangeDate> {
|
||||||
|
assert!(matches!(p.as_rule(), Rule::datum | Rule::today));
|
||||||
|
Ok(match p.as_rule() {
|
||||||
|
Rule::datum => RangeDate::Date(parse::parse_datum(p)?.value),
|
||||||
|
Rule::today => RangeDate::Today,
|
||||||
|
_ => unreachable!(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_range_start(p: Pair<'_, Rule>) -> Result<(RangeDate, Option<Delta>)> {
|
||||||
|
assert_eq!(p.as_rule(), Rule::range_start);
|
||||||
|
let mut p = p.into_inner();
|
||||||
|
|
||||||
|
let start = parse_range_date(p.next().unwrap())?;
|
||||||
|
let start_delta = match p.next() {
|
||||||
|
None => None,
|
||||||
|
Some(p) => Some(parse::parse_delta(p)?.value),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(p.next(), None);
|
||||||
|
|
||||||
|
Ok((start, start_delta))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_range_end(p: Pair<'_, Rule>) -> Result<(Option<RangeDate>, Option<Delta>)> {
|
||||||
|
assert_eq!(p.as_rule(), Rule::range_end);
|
||||||
|
|
||||||
|
let mut end = None;
|
||||||
|
let mut end_delta = None;
|
||||||
|
|
||||||
|
for p in p.into_inner() {
|
||||||
|
match p.as_rule() {
|
||||||
|
Rule::datum | Rule::today => end = Some(parse_range_date(p)?),
|
||||||
|
Rule::delta => end_delta = Some(parse::parse_delta(p)?.value),
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((end, end_delta))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_range(p: Pair<'_, Rule>) -> Result<Range> {
|
||||||
|
assert_eq!(p.as_rule(), Rule::range);
|
||||||
|
let mut p = p.into_inner();
|
||||||
|
|
||||||
|
let (start, start_delta) = parse_range_start(p.next().unwrap())?;
|
||||||
|
let (end, end_delta) = match p.next() {
|
||||||
|
// For some reason, the EOI gets captured but the SOI doesn't.
|
||||||
|
Some(p) if p.as_rule() != Rule::EOI => parse_range_end(p)?,
|
||||||
|
_ => (None, None),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Range {
|
||||||
|
start,
|
||||||
|
start_delta,
|
||||||
|
end,
|
||||||
|
end_delta,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Range {
|
||||||
|
type Err = Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
let mut pairs = TodayfileParser::parse(Rule::range, s)?;
|
||||||
|
let p = pairs.next().unwrap();
|
||||||
|
assert_eq!(pairs.next(), None);
|
||||||
|
|
||||||
|
parse_range(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
194
src/files/cli.rs
194
src/files/cli.rs
|
|
@ -1,194 +0,0 @@
|
||||||
use std::result;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use chrono::NaiveDate;
|
|
||||||
use pest::iterators::Pair;
|
|
||||||
use pest::Parser;
|
|
||||||
|
|
||||||
use super::commands::{Command, Delta};
|
|
||||||
use super::parse::{self, Result, Rule, TodayfileParser};
|
|
||||||
use super::ParseError;
|
|
||||||
|
|
||||||
fn from_str_via_parse<P, R>(s: &str, rule: Rule, parse: P) -> result::Result<R, ParseError<()>>
|
|
||||||
where
|
|
||||||
P: FnOnce(Pair<'_, Rule>) -> Result<R>,
|
|
||||||
{
|
|
||||||
let mut pairs =
|
|
||||||
TodayfileParser::parse(rule, s).map_err(|e| ParseError::new((), Box::new(e)))?;
|
|
||||||
let p = pairs.next().unwrap();
|
|
||||||
assert_eq!(pairs.next(), None);
|
|
||||||
|
|
||||||
parse(p).map_err(|e| ParseError::new((), e))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum CliDatum {
|
|
||||||
Date(NaiveDate),
|
|
||||||
Today,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_cli_datum(p: Pair<'_, Rule>) -> Result<CliDatum> {
|
|
||||||
assert_eq!(p.as_rule(), Rule::cli_datum);
|
|
||||||
let p = p.into_inner().next().unwrap();
|
|
||||||
Ok(match p.as_rule() {
|
|
||||||
Rule::datum => CliDatum::Date(parse::parse_datum(p)?.value),
|
|
||||||
Rule::today => CliDatum::Today,
|
|
||||||
_ => unreachable!(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct CliDate {
|
|
||||||
pub datum: CliDatum,
|
|
||||||
pub delta: Option<Delta>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_cli_date(p: Pair<'_, Rule>) -> Result<CliDate> {
|
|
||||||
assert_eq!(p.as_rule(), Rule::cli_date);
|
|
||||||
let mut p = p.into_inner();
|
|
||||||
|
|
||||||
let datum = parse_cli_datum(p.next().unwrap())?;
|
|
||||||
let delta = match p.next() {
|
|
||||||
Some(p) => Some(parse::parse_delta(p)?.value),
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
assert_eq!(p.next(), None);
|
|
||||||
|
|
||||||
Ok(CliDate { datum, delta })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_cli_date_arg(p: Pair<'_, Rule>) -> Result<CliDate> {
|
|
||||||
assert_eq!(p.as_rule(), Rule::cli_date_arg);
|
|
||||||
let p = p.into_inner().next().unwrap();
|
|
||||||
parse_cli_date(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for CliDate {
|
|
||||||
type Err = ParseError<()>;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> result::Result<Self, ParseError<()>> {
|
|
||||||
from_str_via_parse(s, Rule::cli_date_arg, parse_cli_date_arg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum CliIdent {
|
|
||||||
Number(usize),
|
|
||||||
Date(CliDate),
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_cli_ident(p: Pair<'_, Rule>) -> Result<CliIdent> {
|
|
||||||
assert_eq!(p.as_rule(), Rule::cli_ident);
|
|
||||||
let p = p.into_inner().next().unwrap();
|
|
||||||
Ok(match p.as_rule() {
|
|
||||||
Rule::number => CliIdent::Number(parse::parse_number(p) as usize),
|
|
||||||
Rule::cli_date => CliIdent::Date(parse_cli_date(p)?),
|
|
||||||
_ => unreachable!(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_cli_ident_arg(p: Pair<'_, Rule>) -> Result<CliIdent> {
|
|
||||||
assert_eq!(p.as_rule(), Rule::cli_ident_arg);
|
|
||||||
let p = p.into_inner().next().unwrap();
|
|
||||||
parse_cli_ident(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for CliIdent {
|
|
||||||
type Err = ParseError<()>;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> result::Result<Self, ParseError<()>> {
|
|
||||||
from_str_via_parse(s, Rule::cli_ident_arg, parse_cli_ident_arg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct CliRange {
|
|
||||||
pub start: CliDatum,
|
|
||||||
pub start_delta: Option<Delta>,
|
|
||||||
pub end: Option<CliDatum>,
|
|
||||||
pub end_delta: Option<Delta>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_cli_range_start(p: Pair<'_, Rule>) -> Result<(CliDatum, Option<Delta>)> {
|
|
||||||
assert_eq!(p.as_rule(), Rule::cli_range_start);
|
|
||||||
let mut p = p.into_inner();
|
|
||||||
|
|
||||||
let start = parse_cli_datum(p.next().unwrap())?;
|
|
||||||
let start_delta = match p.next() {
|
|
||||||
None => None,
|
|
||||||
Some(p) => Some(parse::parse_delta(p)?.value),
|
|
||||||
};
|
|
||||||
|
|
||||||
assert_eq!(p.next(), None);
|
|
||||||
|
|
||||||
Ok((start, start_delta))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_cli_range_end(p: Pair<'_, Rule>) -> Result<(Option<CliDatum>, Option<Delta>)> {
|
|
||||||
assert_eq!(p.as_rule(), Rule::cli_range_end);
|
|
||||||
|
|
||||||
let mut end = None;
|
|
||||||
let mut end_delta = None;
|
|
||||||
|
|
||||||
for p in p.into_inner() {
|
|
||||||
match p.as_rule() {
|
|
||||||
Rule::cli_datum => end = Some(parse_cli_datum(p)?),
|
|
||||||
Rule::delta => end_delta = Some(parse::parse_delta(p)?.value),
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok((end, end_delta))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_cli_range(p: Pair<'_, Rule>) -> Result<CliRange> {
|
|
||||||
assert_eq!(p.as_rule(), Rule::cli_range);
|
|
||||||
let mut p = p.into_inner();
|
|
||||||
|
|
||||||
let (start, start_delta) = parse_cli_range_start(p.next().unwrap())?;
|
|
||||||
let (end, end_delta) = match p.next() {
|
|
||||||
Some(p) => parse_cli_range_end(p)?,
|
|
||||||
None => (None, None),
|
|
||||||
};
|
|
||||||
|
|
||||||
assert_eq!(p.next(), None);
|
|
||||||
|
|
||||||
Ok(CliRange {
|
|
||||||
start,
|
|
||||||
start_delta,
|
|
||||||
end,
|
|
||||||
end_delta,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_cli_range_arg(p: Pair<'_, Rule>) -> Result<CliRange> {
|
|
||||||
assert_eq!(p.as_rule(), Rule::cli_range_arg);
|
|
||||||
let p = p.into_inner().next().unwrap();
|
|
||||||
parse_cli_range(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for CliRange {
|
|
||||||
type Err = ParseError<()>;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> result::Result<Self, ParseError<()>> {
|
|
||||||
from_str_via_parse(s, Rule::cli_range_arg, parse_cli_range_arg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct CliCommand(pub Command);
|
|
||||||
|
|
||||||
fn parse_cli_command(p: Pair<'_, Rule>) -> Result<CliCommand> {
|
|
||||||
assert_eq!(p.as_rule(), Rule::cli_command);
|
|
||||||
let p = p.into_inner().next().unwrap();
|
|
||||||
Ok(CliCommand(parse::parse_command(p)?.value))
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for CliCommand {
|
|
||||||
type Err = ParseError<()>;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> result::Result<Self, ParseError<()>> {
|
|
||||||
from_str_via_parse(s, Rule::cli_command, parse_cli_command)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -27,27 +27,27 @@ pub enum DeltaStep {
|
||||||
impl DeltaStep {
|
impl DeltaStep {
|
||||||
pub fn amount(&self) -> i32 {
|
pub fn amount(&self) -> i32 {
|
||||||
match self {
|
match self {
|
||||||
Self::Year(i) => *i,
|
DeltaStep::Year(i) => *i,
|
||||||
Self::Month(i) => *i,
|
DeltaStep::Month(i) => *i,
|
||||||
Self::MonthReverse(i) => *i,
|
DeltaStep::MonthReverse(i) => *i,
|
||||||
Self::Day(i) => *i,
|
DeltaStep::Day(i) => *i,
|
||||||
Self::Week(i) => *i,
|
DeltaStep::Week(i) => *i,
|
||||||
Self::Hour(i) => *i,
|
DeltaStep::Hour(i) => *i,
|
||||||
Self::Minute(i) => *i,
|
DeltaStep::Minute(i) => *i,
|
||||||
Self::Weekday(i, _) => *i,
|
DeltaStep::Weekday(i, _) => *i,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn name(&self) -> &'static str {
|
pub fn name(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::Year(_) => "y",
|
DeltaStep::Year(_) => "y",
|
||||||
Self::Month(_) => "m",
|
DeltaStep::Month(_) => "m",
|
||||||
Self::MonthReverse(_) => "M",
|
DeltaStep::MonthReverse(_) => "M",
|
||||||
Self::Day(_) => "d",
|
DeltaStep::Day(_) => "d",
|
||||||
Self::Week(_) => "w",
|
DeltaStep::Week(_) => "w",
|
||||||
Self::Hour(_) => "h",
|
DeltaStep::Hour(_) => "h",
|
||||||
Self::Minute(_) => "min",
|
DeltaStep::Minute(_) => "min",
|
||||||
Self::Weekday(_, wd) => wd.name(),
|
DeltaStep::Weekday(_, wd) => wd.name(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -168,39 +168,39 @@ impl Var {
|
||||||
pub fn name(&self) -> &'static str {
|
pub fn name(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
// Constants
|
// Constants
|
||||||
Self::True => "true",
|
Var::True => "true",
|
||||||
Self::False => "false",
|
Var::False => "false",
|
||||||
Self::Monday => "mon",
|
Var::Monday => "mon",
|
||||||
Self::Tuesday => "tue",
|
Var::Tuesday => "tue",
|
||||||
Self::Wednesday => "wed",
|
Var::Wednesday => "wed",
|
||||||
Self::Thursday => "thu",
|
Var::Thursday => "thu",
|
||||||
Self::Friday => "fri",
|
Var::Friday => "fri",
|
||||||
Self::Saturday => "sat",
|
Var::Saturday => "sat",
|
||||||
Self::Sunday => "sun",
|
Var::Sunday => "sun",
|
||||||
// Variables
|
// Variables
|
||||||
Self::JulianDay => "j",
|
Var::JulianDay => "j",
|
||||||
Self::Year => "y",
|
Var::Year => "y",
|
||||||
Self::YearLength => "yl",
|
Var::YearLength => "yl",
|
||||||
Self::YearDay => "yd",
|
Var::YearDay => "yd",
|
||||||
Self::YearDayReverse => "yD",
|
Var::YearDayReverse => "yD",
|
||||||
Self::YearWeek => "yw",
|
Var::YearWeek => "yw",
|
||||||
Self::YearWeekReverse => "yW",
|
Var::YearWeekReverse => "yW",
|
||||||
Self::Month => "m",
|
Var::Month => "m",
|
||||||
Self::MonthLength => "ml",
|
Var::MonthLength => "ml",
|
||||||
Self::MonthWeek => "mw",
|
Var::MonthWeek => "mw",
|
||||||
Self::MonthWeekReverse => "mW",
|
Var::MonthWeekReverse => "mW",
|
||||||
Self::Day => "d",
|
Var::Day => "d",
|
||||||
Self::DayReverse => "D",
|
Var::DayReverse => "D",
|
||||||
Self::IsoYear => "iy",
|
Var::IsoYear => "iy",
|
||||||
Self::IsoYearLength => "iyl",
|
Var::IsoYearLength => "iyl",
|
||||||
Self::IsoWeek => "iw",
|
Var::IsoWeek => "iw",
|
||||||
Self::Weekday => "wd",
|
Var::Weekday => "wd",
|
||||||
Self::Easter => "e",
|
Var::Easter => "e",
|
||||||
// Variables with "boolean" values
|
// Variables with "boolean" values
|
||||||
Self::IsWeekday => "isWeekday",
|
Var::IsWeekday => "isWeekday",
|
||||||
Self::IsWeekend => "isWeekend",
|
Var::IsWeekend => "isWeekend",
|
||||||
Self::IsLeapYear => "isLeapYear",
|
Var::IsLeapYear => "isLeapYear",
|
||||||
Self::IsIsoLeapYear => "isIsoLeapYear",
|
Var::IsIsoLeapYear => "isIsoLeapYear",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -265,13 +265,10 @@ pub enum Statement {
|
||||||
Move {
|
Move {
|
||||||
span: Span,
|
span: Span,
|
||||||
from: NaiveDate,
|
from: NaiveDate,
|
||||||
to: Option<NaiveDate>,
|
to: NaiveDate,
|
||||||
to_time: Option<Spanned<Time>>,
|
|
||||||
},
|
},
|
||||||
Remind(Option<Spanned<Delta>>),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::enum_variant_names)]
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub enum DoneDate {
|
pub enum DoneDate {
|
||||||
Date {
|
Date {
|
||||||
|
|
@ -301,54 +298,18 @@ pub enum DoneDate {
|
||||||
impl DoneDate {
|
impl DoneDate {
|
||||||
pub fn root(self) -> NaiveDate {
|
pub fn root(self) -> NaiveDate {
|
||||||
match self {
|
match self {
|
||||||
Self::Date { root } => root,
|
DoneDate::Date { root } => root,
|
||||||
Self::DateTime { root, .. } => root,
|
DoneDate::DateTime { root, .. } => root,
|
||||||
Self::DateToDate { root, .. } => root,
|
DoneDate::DateToDate { root, .. } => root,
|
||||||
Self::DateTimeToTime { root, .. } => root,
|
DoneDate::DateTimeToTime { root, .. } => root,
|
||||||
Self::DateTimeToDateTime { root, .. } => root,
|
DoneDate::DateTimeToDateTime { root, .. } => root,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove redundancies like the same date or time specified twice.
|
|
||||||
pub fn simplified(self) -> Self {
|
|
||||||
let result = match self {
|
|
||||||
Self::DateToDate { root, other } if root == other => Self::Date { root },
|
|
||||||
Self::DateTimeToDateTime {
|
|
||||||
root,
|
|
||||||
root_time,
|
|
||||||
other,
|
|
||||||
other_time,
|
|
||||||
} if root == other => Self::DateTimeToTime {
|
|
||||||
root,
|
|
||||||
root_time,
|
|
||||||
other_time,
|
|
||||||
},
|
|
||||||
other => other,
|
|
||||||
};
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Self::DateTimeToTime {
|
|
||||||
root,
|
|
||||||
root_time,
|
|
||||||
other_time,
|
|
||||||
} if root_time == other_time => Self::DateTime { root, root_time },
|
|
||||||
other => other,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum DoneKind {
|
|
||||||
Done,
|
|
||||||
Canceled,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Done {
|
pub struct Done {
|
||||||
pub kind: DoneKind,
|
|
||||||
/// The date of the task the DONE refers to.
|
|
||||||
pub date: Option<DoneDate>,
|
pub date: Option<DoneDate>,
|
||||||
/// When the task was actually completed.
|
|
||||||
pub done_at: NaiveDate,
|
pub done_at: NaiveDate,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -367,31 +328,39 @@ pub struct Note {
|
||||||
pub desc: Vec<String>,
|
pub desc: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Log {
|
|
||||||
pub date: Spanned<NaiveDate>,
|
|
||||||
pub desc: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Command {
|
pub enum Command {
|
||||||
Include(Spanned<String>),
|
|
||||||
Timezone(Spanned<String>),
|
|
||||||
Capture, // TODO Set capture file by template?
|
|
||||||
Task(Task),
|
Task(Task),
|
||||||
Note(Note),
|
Note(Note),
|
||||||
Log(Log),
|
}
|
||||||
|
|
||||||
|
impl Command {
|
||||||
|
pub fn title(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Self::Task(task) => &task.title,
|
||||||
|
Self::Note(note) => ¬e.title,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn desc(&self) -> &[String] {
|
||||||
|
match self {
|
||||||
|
Self::Task(task) => &task.desc,
|
||||||
|
Self::Note(note) => ¬e.desc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn statements(&self) -> &[Statement] {
|
||||||
|
match self {
|
||||||
|
Self::Task(task) => &task.statements,
|
||||||
|
Self::Note(note) => ¬e.statements,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct File {
|
pub struct File {
|
||||||
pub commands: Vec<Spanned<Command>>,
|
pub contents: String,
|
||||||
}
|
pub includes: Vec<String>,
|
||||||
|
pub timezone: Option<String>,
|
||||||
impl File {
|
pub commands: Vec<Command>,
|
||||||
/// Create an empty dummy file. This file should only be used as a
|
|
||||||
/// placeholder value.
|
|
||||||
pub fn dummy() -> Self {
|
|
||||||
Self { commands: vec![] }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,88 +1,7 @@
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::{io, result};
|
use std::{io, result};
|
||||||
|
|
||||||
use chrono::NaiveDate;
|
use super::parse;
|
||||||
use codespan_reporting::diagnostic::{Diagnostic, Label};
|
|
||||||
use codespan_reporting::term::Config;
|
|
||||||
use pest::error::{ErrorVariant, InputLocation};
|
|
||||||
|
|
||||||
use crate::error::Eprint;
|
|
||||||
|
|
||||||
use super::primitives::Span;
|
|
||||||
use super::{parse, FileSource, Files};
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
#[error("{error}")]
|
|
||||||
pub struct ParseError<S> {
|
|
||||||
file: S,
|
|
||||||
error: Box<parse::Error>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S> ParseError<S> {
|
|
||||||
pub fn new(file: S, error: Box<parse::Error>) -> Self {
|
|
||||||
Self { file, error }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rule_name(rule: parse::Rule) -> String {
|
|
||||||
// TODO Rename rules to be more readable?
|
|
||||||
format!("{:?}", rule)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn enumerate(rules: &[parse::Rule]) -> String {
|
|
||||||
match rules.len() {
|
|
||||||
0 => "something".to_string(),
|
|
||||||
1 => Self::rule_name(rules[0]),
|
|
||||||
n => {
|
|
||||||
let except_last = rules
|
|
||||||
.iter()
|
|
||||||
.take(n - 1)
|
|
||||||
.map(|rule| Self::rule_name(*rule))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(", ");
|
|
||||||
let last = Self::rule_name(rules[n - 1]);
|
|
||||||
format!("{except_last} or {last}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn notes(&self) -> Vec<String> {
|
|
||||||
match &self.error.variant {
|
|
||||||
ErrorVariant::ParsingError {
|
|
||||||
positives,
|
|
||||||
negatives,
|
|
||||||
} => {
|
|
||||||
let mut notes = vec![];
|
|
||||||
if !positives.is_empty() {
|
|
||||||
notes.push(format!("expected {}", Self::enumerate(positives)))
|
|
||||||
}
|
|
||||||
if !negatives.is_empty() {
|
|
||||||
notes.push(format!("unexpected {}", Self::enumerate(negatives)))
|
|
||||||
}
|
|
||||||
notes
|
|
||||||
}
|
|
||||||
ErrorVariant::CustomError { message } => vec![message.clone()],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, F> Eprint<'a, F> for ParseError<F::FileId>
|
|
||||||
where
|
|
||||||
F: codespan_reporting::files::Files<'a>,
|
|
||||||
{
|
|
||||||
#[allow(single_use_lifetimes)]
|
|
||||||
fn eprint<'f: 'a>(&self, files: &'f F, config: &Config) {
|
|
||||||
let range = match self.error.location {
|
|
||||||
InputLocation::Pos(at) => at..at,
|
|
||||||
InputLocation::Span((from, to)) => from..to,
|
|
||||||
};
|
|
||||||
let name = files.name(self.file).expect("file exists");
|
|
||||||
let diagnostic = Diagnostic::error()
|
|
||||||
.with_message(format!("Could not parse {name}"))
|
|
||||||
.with_labels(vec![Label::primary(self.file, range)])
|
|
||||||
.with_notes(self.notes());
|
|
||||||
Self::eprint_diagnostic(files, config, &diagnostic);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
|
|
@ -92,132 +11,54 @@ pub enum Error {
|
||||||
ReadFile { file: PathBuf, error: io::Error },
|
ReadFile { file: PathBuf, error: io::Error },
|
||||||
#[error("Could not write {file}: {error}")]
|
#[error("Could not write {file}: {error}")]
|
||||||
WriteFile { file: PathBuf, error: io::Error },
|
WriteFile { file: PathBuf, error: io::Error },
|
||||||
#[error("Could not resolve timezone {tz}: {error}")]
|
#[error("Could not resolve timezone {timezone}: {error}")]
|
||||||
ResolveTz {
|
ResolveTz { timezone: String, error: io::Error },
|
||||||
file: FileSource,
|
|
||||||
span: Span,
|
|
||||||
tz: String,
|
|
||||||
error: io::Error,
|
|
||||||
},
|
|
||||||
#[error("Could not determine local timezone: {error}")]
|
#[error("Could not determine local timezone: {error}")]
|
||||||
LocalTz { error: io::Error },
|
LocalTz { error: io::Error },
|
||||||
#[error("{error}")]
|
#[error("{0}")]
|
||||||
Parse {
|
Parse(#[from] parse::Error),
|
||||||
file: FileSource,
|
#[error("{file1} has time zone {tz1} but {file2} has time zone {tz2}")]
|
||||||
error: Box<parse::Error>,
|
|
||||||
},
|
|
||||||
#[error("Conflicting time zones {tz1} and {tz2}")]
|
|
||||||
TzConflict {
|
TzConflict {
|
||||||
file1: FileSource,
|
file1: PathBuf,
|
||||||
span1: Span,
|
|
||||||
tz1: String,
|
tz1: String,
|
||||||
file2: FileSource,
|
file2: PathBuf,
|
||||||
span2: Span,
|
|
||||||
tz2: String,
|
tz2: String,
|
||||||
},
|
},
|
||||||
#[error("Multiple capture commands")]
|
|
||||||
MultipleCapture {
|
|
||||||
file1: FileSource,
|
|
||||||
span1: Span,
|
|
||||||
file2: FileSource,
|
|
||||||
span2: Span,
|
|
||||||
},
|
|
||||||
#[error("Duplicate logs for {date}")]
|
|
||||||
LogConflict {
|
|
||||||
file1: FileSource,
|
|
||||||
span1: Span,
|
|
||||||
file2: FileSource,
|
|
||||||
span2: Span,
|
|
||||||
date: NaiveDate,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Eprint<'a, Files> for Error {
|
impl Error {
|
||||||
#[allow(single_use_lifetimes)]
|
pub fn print(&self) {
|
||||||
fn eprint<'f: 'a>(&self, files: &'f Files, config: &Config) {
|
|
||||||
match self {
|
match self {
|
||||||
Self::ResolvePath { path, error } => {
|
Error::ResolvePath { path, error } => {
|
||||||
eprintln!("Could not resolve path {path:?}:");
|
eprintln!("Could not resolve path {:?}:", path);
|
||||||
eprintln!(" {error}");
|
eprintln!(" {}", error);
|
||||||
}
|
}
|
||||||
Self::ReadFile { file, error } => {
|
Error::ReadFile { file, error } => {
|
||||||
eprintln!("Could not read file {file:?}:");
|
eprintln!("Could not read file {:?}:", file);
|
||||||
eprintln!(" {error}");
|
eprintln!(" {}", error);
|
||||||
}
|
}
|
||||||
Self::WriteFile { file, error } => {
|
Error::WriteFile { file, error } => {
|
||||||
eprintln!("Could not write file {file:?}:");
|
eprintln!("Could not write file {:?}:", file);
|
||||||
eprintln!(" {error}");
|
eprintln!(" {}", error);
|
||||||
}
|
}
|
||||||
Self::ResolveTz {
|
Error::ResolveTz { timezone, error } => {
|
||||||
file,
|
eprintln!("Could not resolve time zone {}:", timezone);
|
||||||
span,
|
eprintln!(" {}", error);
|
||||||
tz,
|
|
||||||
error,
|
|
||||||
} => {
|
|
||||||
let diagnostic = Diagnostic::error()
|
|
||||||
.with_message(format!("Could not resolve time zone {tz}"))
|
|
||||||
.with_labels(vec![Label::primary(*file, span)])
|
|
||||||
.with_notes(vec![format!("{error}")]);
|
|
||||||
Self::eprint_diagnostic(files, config, &diagnostic);
|
|
||||||
}
|
}
|
||||||
Self::LocalTz { error } => {
|
Error::LocalTz { error } => {
|
||||||
eprintln!("Could not determine local timezone:");
|
eprintln!("Could not determine local timezone:");
|
||||||
eprintln!(" {error}");
|
eprintln!(" {}", error);
|
||||||
}
|
}
|
||||||
Self::Parse { file, error } => {
|
Error::Parse(error) => eprintln!("{}", error),
|
||||||
ParseError::new(*file, error.clone()).eprint(files, config)
|
Error::TzConflict {
|
||||||
}
|
|
||||||
Self::TzConflict {
|
|
||||||
file1,
|
file1,
|
||||||
span1,
|
|
||||||
tz1,
|
tz1,
|
||||||
file2,
|
file2,
|
||||||
span2,
|
|
||||||
tz2,
|
tz2,
|
||||||
} => {
|
} => {
|
||||||
let diagnostic = Diagnostic::error()
|
eprintln!("Time zone conflict:");
|
||||||
.with_message(format!("Time zone conflict between {tz1} and {tz2}"))
|
eprintln!(" {:?} has time zone {}", file1, tz1);
|
||||||
.with_labels(vec![
|
eprintln!(" {:?} has time zone {}", file2, tz2);
|
||||||
Label::primary(*file1, span1),
|
|
||||||
Label::primary(*file2, span2),
|
|
||||||
])
|
|
||||||
.with_notes(vec![
|
|
||||||
"All TIMEZONE commands must set the same time zone.".to_string()
|
|
||||||
]);
|
|
||||||
Self::eprint_diagnostic(files, config, &diagnostic);
|
|
||||||
}
|
|
||||||
Self::MultipleCapture {
|
|
||||||
file1,
|
|
||||||
span1,
|
|
||||||
file2,
|
|
||||||
span2,
|
|
||||||
} => {
|
|
||||||
let diagnostic = Diagnostic::error()
|
|
||||||
.with_message("Multiple capture commands")
|
|
||||||
.with_labels(vec![
|
|
||||||
Label::primary(*file1, span1),
|
|
||||||
Label::primary(*file2, span2),
|
|
||||||
])
|
|
||||||
.with_notes(vec![
|
|
||||||
"There must be at most one CAPTURE command.".to_string()
|
|
||||||
]);
|
|
||||||
Self::eprint_diagnostic(files, config, &diagnostic);
|
|
||||||
}
|
|
||||||
Self::LogConflict {
|
|
||||||
file1,
|
|
||||||
span1,
|
|
||||||
file2,
|
|
||||||
span2,
|
|
||||||
date,
|
|
||||||
} => {
|
|
||||||
let diagnostic = Diagnostic::error()
|
|
||||||
.with_message(format!("Duplicate log entries for {date}"))
|
|
||||||
.with_labels(vec![
|
|
||||||
Label::primary(*file1, span1),
|
|
||||||
Label::primary(*file2, span2),
|
|
||||||
])
|
|
||||||
.with_notes(vec!["A day can have at most one LOG entry.".to_string()]);
|
|
||||||
Self::eprint_diagnostic(files, config, &diagnostic);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,10 @@
|
||||||
use std::collections::HashSet;
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
use chrono::Datelike;
|
use chrono::Datelike;
|
||||||
|
|
||||||
use crate::files::commands::DoneKind;
|
|
||||||
|
|
||||||
use super::commands::{
|
use super::commands::{
|
||||||
BirthdaySpec, Command, DateSpec, Delta, DeltaStep, Done, DoneDate, Expr, File, FormulaSpec,
|
BirthdaySpec, Command, DateSpec, Delta, DeltaStep, Done, DoneDate, Expr, File, FormulaSpec,
|
||||||
Log, Note, Repeat, Spec, Statement, Task, Var, WeekdaySpec,
|
Note, Repeat, Spec, Statement, Task, Var, WeekdaySpec,
|
||||||
};
|
};
|
||||||
use super::primitives::{Spanned, Time, Weekday};
|
use super::primitives::{Spanned, Time, Weekday};
|
||||||
|
|
||||||
|
|
@ -22,7 +19,7 @@ fn format_desc(f: &mut fmt::Formatter<'_>, desc: &[String]) -> fmt::Result {
|
||||||
if line.is_empty() {
|
if line.is_empty() {
|
||||||
writeln!(f, "#")?;
|
writeln!(f, "#")?;
|
||||||
} else {
|
} else {
|
||||||
writeln!(f, "# {line}")?;
|
writeln!(f, "# {}", line)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -75,30 +72,30 @@ impl fmt::Display for DateSpec {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
// Start
|
// Start
|
||||||
write!(f, "{}", self.start)?;
|
write!(f, "{}", self.start)?;
|
||||||
if let Some(delta) = &self.start_delta {
|
for delta in &self.start_delta {
|
||||||
write!(f, " {delta}")?;
|
write!(f, " {}", delta)?;
|
||||||
}
|
}
|
||||||
if let Some(time) = &self.start_time {
|
for time in &self.start_time {
|
||||||
write!(f, " {time}")?;
|
write!(f, " {}", time)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// End
|
// End
|
||||||
if self.end.is_some() || self.end_delta.is_some() || self.end_time.is_some() {
|
if self.end.is_some() || self.end_delta.is_some() || self.end_time.is_some() {
|
||||||
write!(f, " --")?;
|
write!(f, " --")?;
|
||||||
if let Some(date) = self.end {
|
if let Some(date) = self.end {
|
||||||
write!(f, " {date}")?;
|
write!(f, " {}", date)?;
|
||||||
}
|
}
|
||||||
if let Some(delta) = &self.end_delta {
|
if let Some(delta) = &self.end_delta {
|
||||||
write!(f, " {delta}")?;
|
write!(f, " {}", delta)?;
|
||||||
}
|
}
|
||||||
if let Some(time) = &self.end_time {
|
if let Some(time) = &self.end_time {
|
||||||
write!(f, " {time}")?;
|
write!(f, " {}", time)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Repeat
|
// Repeat
|
||||||
if let Some(repeat) = &self.repeat {
|
if let Some(repeat) = &self.repeat {
|
||||||
write!(f, "; {repeat}")?;
|
write!(f, "; {}", repeat)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -109,21 +106,21 @@ impl fmt::Display for WeekdaySpec {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
// Start
|
// Start
|
||||||
write!(f, "{}", self.start)?;
|
write!(f, "{}", self.start)?;
|
||||||
if let Some(time) = &self.start_time {
|
for time in &self.start_time {
|
||||||
write!(f, " {time}")?;
|
write!(f, " {}", time)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// End
|
// End
|
||||||
if self.end.is_some() || self.end_delta.is_some() || self.end_time.is_some() {
|
if self.end.is_some() || self.end_delta.is_some() || self.end_time.is_some() {
|
||||||
write!(f, " --")?;
|
write!(f, " --")?;
|
||||||
if let Some(wd) = self.end {
|
if let Some(wd) = self.end {
|
||||||
write!(f, " {wd}")?;
|
write!(f, " {}", wd)?;
|
||||||
}
|
}
|
||||||
if let Some(delta) = &self.end_delta {
|
if let Some(delta) = &self.end_delta {
|
||||||
write!(f, " {delta}")?;
|
write!(f, " {}", delta)?;
|
||||||
}
|
}
|
||||||
if let Some(time) = &self.end_time {
|
if let Some(time) = &self.end_time {
|
||||||
write!(f, " {time}")?;
|
write!(f, " {}", time)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -140,25 +137,25 @@ impl fmt::Display for Var {
|
||||||
impl fmt::Display for Expr {
|
impl fmt::Display for Expr {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Lit(i) => write!(f, "{i}"),
|
Expr::Lit(i) => write!(f, "{}", i),
|
||||||
Self::Var(v) => write!(f, "{v}"),
|
Expr::Var(v) => write!(f, "{}", v),
|
||||||
Self::Paren(e) => write!(f, "({e})"),
|
Expr::Paren(e) => write!(f, "({})", e),
|
||||||
Self::Neg(e) => write!(f, "-{e}"),
|
Expr::Neg(e) => write!(f, "-{}", e),
|
||||||
Self::Add(a, b) => write!(f, "{a} + {b}"),
|
Expr::Add(a, b) => write!(f, "{} + {}", a, b),
|
||||||
Self::Sub(a, b) => write!(f, "{a} - {b}"),
|
Expr::Sub(a, b) => write!(f, "{} - {}", a, b),
|
||||||
Self::Mul(a, b) => write!(f, "{a} * {b}"),
|
Expr::Mul(a, b) => write!(f, "{} * {}", a, b),
|
||||||
Self::Div(a, b) => write!(f, "{a} / {b}"),
|
Expr::Div(a, b) => write!(f, "{} / {}", a, b),
|
||||||
Self::Mod(a, b) => write!(f, "{a} % {b}"),
|
Expr::Mod(a, b) => write!(f, "{} % {}", a, b),
|
||||||
Self::Eq(a, b) => write!(f, "{a} = {b}"),
|
Expr::Eq(a, b) => write!(f, "{} = {}", a, b),
|
||||||
Self::Neq(a, b) => write!(f, "{a} != {b}"),
|
Expr::Neq(a, b) => write!(f, "{} != {}", a, b),
|
||||||
Self::Lt(a, b) => write!(f, "{a} < {b}"),
|
Expr::Lt(a, b) => write!(f, "{} < {}", a, b),
|
||||||
Self::Lte(a, b) => write!(f, "{a} <= {b}"),
|
Expr::Lte(a, b) => write!(f, "{} <= {}", a, b),
|
||||||
Self::Gt(a, b) => write!(f, "{a} > {b}"),
|
Expr::Gt(a, b) => write!(f, "{} > {}", a, b),
|
||||||
Self::Gte(a, b) => write!(f, "{a} >= {b}"),
|
Expr::Gte(a, b) => write!(f, "{} >= {}", a, b),
|
||||||
Self::Not(e) => write!(f, "!{e}"),
|
Expr::Not(e) => write!(f, "!{}", e),
|
||||||
Self::And(a, b) => write!(f, "{a} & {b}"),
|
Expr::And(a, b) => write!(f, "{} & {}", a, b),
|
||||||
Self::Or(a, b) => write!(f, "{a} | {b}"),
|
Expr::Or(a, b) => write!(f, "{} | {}", a, b),
|
||||||
Self::Xor(a, b) => write!(f, "{a} ^ {b}"),
|
Expr::Xor(a, b) => write!(f, "{} ^ {}", a, b),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -167,25 +164,25 @@ impl fmt::Display for FormulaSpec {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
// Start
|
// Start
|
||||||
if let Some(expr) = &self.start {
|
if let Some(expr) = &self.start {
|
||||||
write!(f, "({expr})")?;
|
write!(f, "({})", expr)?;
|
||||||
} else {
|
} else {
|
||||||
write!(f, "*")?;
|
write!(f, "*")?;
|
||||||
}
|
}
|
||||||
if let Some(delta) = &self.start_delta {
|
for delta in &self.start_delta {
|
||||||
write!(f, " {delta}")?;
|
write!(f, " {}", delta)?;
|
||||||
}
|
}
|
||||||
if let Some(time) = &self.start_time {
|
for time in &self.start_time {
|
||||||
write!(f, " {time}")?;
|
write!(f, " {}", time)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// End
|
// End
|
||||||
if self.end_delta.is_some() || self.end_time.is_some() {
|
if self.end_delta.is_some() || self.end_time.is_some() {
|
||||||
write!(f, " --")?;
|
write!(f, " --")?;
|
||||||
if let Some(delta) = &self.end_delta {
|
if let Some(delta) = &self.end_delta {
|
||||||
write!(f, " {delta}")?;
|
write!(f, " {}", delta)?;
|
||||||
}
|
}
|
||||||
if let Some(time) = &self.end_time {
|
if let Some(time) = &self.end_time {
|
||||||
write!(f, " {time}")?;
|
write!(f, " {}", time)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -196,9 +193,9 @@ impl fmt::Display for FormulaSpec {
|
||||||
impl fmt::Display for Spec {
|
impl fmt::Display for Spec {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Date(spec) => write!(f, "{spec}"),
|
Spec::Date(spec) => write!(f, "{}", spec),
|
||||||
Self::Weekday(spec) => write!(f, "{spec}"),
|
Spec::Weekday(spec) => write!(f, "{}", spec),
|
||||||
Self::Formula(spec) => write!(f, "{spec}"),
|
Spec::Formula(spec) => write!(f, "{}", spec),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -216,57 +213,45 @@ impl fmt::Display for BirthdaySpec {
|
||||||
impl fmt::Display for Statement {
|
impl fmt::Display for Statement {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Date(spec) => writeln!(f, "DATE {spec}"),
|
Statement::Date(spec) => writeln!(f, "DATE {}", spec),
|
||||||
Self::BDate(spec) => writeln!(f, "BDATE {spec}"),
|
Statement::BDate(spec) => writeln!(f, "BDATE {}", spec),
|
||||||
Self::From(Some(date)) => writeln!(f, "FROM {date}"),
|
Statement::From(Some(date)) => writeln!(f, "FROM {}", date),
|
||||||
Self::From(None) => writeln!(f, "FROM *"),
|
Statement::From(None) => writeln!(f, "FROM *"),
|
||||||
Self::Until(Some(date)) => writeln!(f, "UNTIL {date}"),
|
Statement::Until(Some(date)) => writeln!(f, "UNTIL {}", date),
|
||||||
Self::Until(None) => writeln!(f, "UNTIL *"),
|
Statement::Until(None) => writeln!(f, "UNTIL *"),
|
||||||
Self::Except(date) => writeln!(f, "EXCEPT {date}"),
|
Statement::Except(date) => writeln!(f, "EXCEPT {}", date),
|
||||||
Self::Move {
|
Statement::Move { from, to, .. } => writeln!(f, "MOVE {} TO {}", from, to),
|
||||||
from, to, to_time, ..
|
|
||||||
} => match (to, to_time) {
|
|
||||||
(None, None) => unreachable!(),
|
|
||||||
(Some(to), None) => writeln!(f, "MOVE {from} TO {to}"),
|
|
||||||
(None, Some(to_time)) => writeln!(f, "MOVE {from} TO {to_time}"),
|
|
||||||
(Some(to), Some(to_time)) => writeln!(f, "MOVE {from} TO {to} {to_time}"),
|
|
||||||
},
|
|
||||||
Self::Remind(Some(delta)) => writeln!(f, "REMIND {delta}"),
|
|
||||||
Self::Remind(None) => writeln!(f, "REMIND *"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for DoneDate {
|
impl fmt::Display for DoneDate {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self.simplified() {
|
// TODO Remove redundant dates
|
||||||
Self::Date { root } => write!(f, "{root}"),
|
match self {
|
||||||
Self::DateTime { root, root_time } => write!(f, "{root} {root_time}"),
|
DoneDate::Date { root } => write!(f, "{}", root),
|
||||||
Self::DateToDate { root, other } => write!(f, "{root} -- {other}"),
|
DoneDate::DateTime { root, root_time } => write!(f, "{} {}", root, root_time),
|
||||||
Self::DateTimeToTime {
|
DoneDate::DateToDate { root, other } => write!(f, "{} -- {}", root, other),
|
||||||
|
DoneDate::DateTimeToTime {
|
||||||
root,
|
root,
|
||||||
root_time,
|
root_time,
|
||||||
other_time,
|
other_time,
|
||||||
} => write!(f, "{root} {root_time} -- {other_time}"),
|
} => write!(f, "{} {} -- {}", root, root_time, other_time),
|
||||||
Self::DateTimeToDateTime {
|
DoneDate::DateTimeToDateTime {
|
||||||
root,
|
root,
|
||||||
root_time,
|
root_time,
|
||||||
other,
|
other,
|
||||||
other_time,
|
other_time,
|
||||||
} => write!(f, "{root} {root_time} -- {other} {other_time}"),
|
} => write!(f, "{} {} -- {} {}", root, root_time, other, other_time),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for Done {
|
impl fmt::Display for Done {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
let kind = match self.kind {
|
write!(f, "DONE [{}]", self.done_at)?;
|
||||||
DoneKind::Done => "DONE",
|
|
||||||
DoneKind::Canceled => "CANCELED",
|
|
||||||
};
|
|
||||||
write!(f, "{kind} [{}]", self.done_at)?;
|
|
||||||
if let Some(date) = &self.date {
|
if let Some(date) = &self.date {
|
||||||
write!(f, " {date}")?;
|
write!(f, " {}", date)?;
|
||||||
}
|
}
|
||||||
writeln!(f)
|
writeln!(f)
|
||||||
}
|
}
|
||||||
|
|
@ -276,10 +261,10 @@ impl fmt::Display for Task {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
writeln!(f, "TASK {}", self.title)?;
|
writeln!(f, "TASK {}", self.title)?;
|
||||||
for statement in &self.statements {
|
for statement in &self.statements {
|
||||||
write!(f, "{statement}")?;
|
write!(f, "{}", statement)?;
|
||||||
}
|
}
|
||||||
for done in &self.done {
|
for done in &self.done {
|
||||||
write!(f, "{done}")?;
|
write!(f, "{}", done)?;
|
||||||
}
|
}
|
||||||
format_desc(f, &self.desc)?;
|
format_desc(f, &self.desc)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -290,97 +275,46 @@ impl fmt::Display for Note {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
writeln!(f, "NOTE {}", self.title)?;
|
writeln!(f, "NOTE {}", self.title)?;
|
||||||
for statement in &self.statements {
|
for statement in &self.statements {
|
||||||
write!(f, "{statement}")?;
|
write!(f, "{}", statement)?;
|
||||||
}
|
}
|
||||||
format_desc(f, &self.desc)?;
|
format_desc(f, &self.desc)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for Log {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
writeln!(f, "LOG {}", self.date)?;
|
|
||||||
format_desc(f, &self.desc)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for Command {
|
impl fmt::Display for Command {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Include(name) => writeln!(f, "INCLUDE {name}"),
|
Command::Task(task) => write!(f, "{}", task),
|
||||||
Self::Timezone(name) => writeln!(f, "TIMEZONE {name}"),
|
Command::Note(note) => write!(f, "{}", note),
|
||||||
Self::Capture => writeln!(f, "CAPTURE"),
|
|
||||||
Self::Task(task) => write!(f, "{task}"),
|
|
||||||
Self::Note(note) => write!(f, "{note}"),
|
|
||||||
Self::Log(log) => write!(f, "{log}"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl File {
|
impl fmt::Display for File {
|
||||||
fn sort(commands: &mut [&Command]) {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
// Order of commands in a file:
|
let mut empty = true;
|
||||||
// 1. Imports, sorted alphabetically
|
for include in &self.includes {
|
||||||
// 2. Time zone(s)
|
writeln!(f, "INCLUDE {}", include)?;
|
||||||
// 3. Captures
|
empty = false;
|
||||||
// 4. Log entries, sorted by date (ascending)
|
}
|
||||||
// 5. Tasks and notes, in original order
|
|
||||||
|
|
||||||
// There should always be at most one time zone, so we don't care about
|
if let Some(tz) = &self.timezone {
|
||||||
// their order.
|
if !empty {
|
||||||
|
writeln!(f)?;
|
||||||
// In the individual steps we must use a stable sort so the order of 4.
|
|
||||||
// is not lost.
|
|
||||||
|
|
||||||
// Order imports alphabetically
|
|
||||||
commands.sort_by_key(|c| match c {
|
|
||||||
Command::Include(path) => Some(&path.value),
|
|
||||||
_ => None,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Order log entries by date
|
|
||||||
commands.sort_by_key(|c| match c {
|
|
||||||
Command::Log(Log { date, .. }) => Some(date.value),
|
|
||||||
_ => None,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Order by type
|
|
||||||
commands.sort_by_key(|c| match c {
|
|
||||||
Command::Include(_) => 0,
|
|
||||||
Command::Timezone(_) => 1,
|
|
||||||
Command::Capture => 2,
|
|
||||||
Command::Log(_) => 3,
|
|
||||||
Command::Task(_) | Command::Note(_) => 4,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn format(&self, removed: &HashSet<usize>) -> String {
|
|
||||||
let mut result = String::new();
|
|
||||||
|
|
||||||
let mut commands = self
|
|
||||||
.commands
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.filter(|(i, _)| !removed.contains(i))
|
|
||||||
.map(|(_, c)| &c.value)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
Self::sort(&mut commands);
|
|
||||||
|
|
||||||
for i in 0..commands.len() {
|
|
||||||
let curr = &commands[i];
|
|
||||||
let next = commands.get(i + 1);
|
|
||||||
|
|
||||||
result.push_str(&format!("{curr}"));
|
|
||||||
|
|
||||||
match (curr, next) {
|
|
||||||
(Command::Include(_), Some(Command::Include(_))) => {}
|
|
||||||
(_, None) => {}
|
|
||||||
_ => result.push('\n'),
|
|
||||||
}
|
}
|
||||||
|
writeln!(f, "TIMEZONE {}", tz)?;
|
||||||
|
empty = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
result
|
for command in &self.commands {
|
||||||
|
if !empty {
|
||||||
|
writeln!(f)?;
|
||||||
|
}
|
||||||
|
write!(f, "{}", command)?;
|
||||||
|
empty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ rest_any = { (!eol ~ ANY)* }
|
||||||
|
|
||||||
include = { "INCLUDE" ~ WHITESPACE ~ rest_some ~ eol }
|
include = { "INCLUDE" ~ WHITESPACE ~ rest_some ~ eol }
|
||||||
timezone = { "TIMEZONE" ~ WHITESPACE ~ rest_some ~ eol }
|
timezone = { "TIMEZONE" ~ WHITESPACE ~ rest_some ~ eol }
|
||||||
capture = { "CAPTURE" ~ eol }
|
|
||||||
|
|
||||||
number = @{ ASCII_DIGIT{1,9} } // Fits into an i32
|
number = @{ ASCII_DIGIT{1,9} } // Fits into an i32
|
||||||
|
|
||||||
|
|
@ -59,33 +58,34 @@ variable = {
|
||||||
| "e"
|
| "e"
|
||||||
}
|
}
|
||||||
|
|
||||||
prefix_neg = { "-" }
|
unop_neg = { "-" }
|
||||||
prefix_not = { "!" }
|
unop_not = { "!" }
|
||||||
prefix = _{ prefix_neg | prefix_not }
|
unop = _{ unop_neg | unop_not }
|
||||||
|
|
||||||
infix_add = { "+" }
|
op_add = { "+" }
|
||||||
infix_sub = { "-" }
|
op_sub = { "-" }
|
||||||
infix_mul = { "*" }
|
op_mul = { "*" }
|
||||||
infix_div = { "/" }
|
op_div = { "/" }
|
||||||
infix_mod = { "%" }
|
op_mod = { "%" }
|
||||||
infix_eq = { "=" }
|
op_eq = { "=" }
|
||||||
infix_neq = { "!=" }
|
op_neq = { "!=" }
|
||||||
infix_lt = { "<" }
|
op_lt = { "<" }
|
||||||
infix_lte = { "<=" }
|
op_lte = { "<=" }
|
||||||
infix_gt = { ">" }
|
op_gt = { ">" }
|
||||||
infix_gte = { ">=" }
|
op_gte = { ">=" }
|
||||||
infix_and = { "&" }
|
op_and = { "&" }
|
||||||
infix_or = { "|" }
|
op_or = { "|" }
|
||||||
infix_xor = { "^" }
|
op_xor = { "^" }
|
||||||
infix = _{
|
op = _{
|
||||||
infix_add | infix_sub | infix_mul | infix_div | infix_mod
|
op_add | op_sub | op_mul | op_div | op_mod
|
||||||
| infix_eq | infix_neq | infix_lt | infix_lte | infix_gt | infix_gte
|
| op_eq | op_neq | op_lt | op_lte | op_gt | op_gte
|
||||||
| infix_and | infix_or | infix_xor
|
| op_and | op_or | op_xor
|
||||||
}
|
}
|
||||||
|
|
||||||
paren_expr = { "(" ~ expr ~ ")" }
|
paren_expr = { "(" ~ expr ~ ")" }
|
||||||
term = { number | boolean | variable | paren_expr }
|
unop_expr = { unop ~ expr }
|
||||||
expr = { prefix* ~ term ~ (infix ~ prefix* ~ term)* }
|
term = { number | boolean | variable | paren_expr | unop_expr }
|
||||||
|
expr = { term ~ (op ~ term)* }
|
||||||
|
|
||||||
date_fixed_start = { datum ~ delta? ~ time? }
|
date_fixed_start = { datum ~ delta? ~ time? }
|
||||||
date_fixed_end = { datum ~ delta? ~ time? | delta ~ time? | time }
|
date_fixed_end = { datum ~ delta? ~ time? | delta ~ time? | time }
|
||||||
|
|
@ -106,10 +106,9 @@ stmt_bdate = !{ "BDATE" ~ bdatum ~ eol }
|
||||||
stmt_from = !{ "FROM" ~ (datum | "*") ~ eol }
|
stmt_from = !{ "FROM" ~ (datum | "*") ~ eol }
|
||||||
stmt_until = !{ "UNTIL" ~ (datum | "*") ~ eol }
|
stmt_until = !{ "UNTIL" ~ (datum | "*") ~ eol }
|
||||||
stmt_except = !{ "EXCEPT" ~ datum ~ eol }
|
stmt_except = !{ "EXCEPT" ~ datum ~ eol }
|
||||||
stmt_move = !{ "MOVE" ~ datum ~ "TO" ~ (datum ~ time? | time) ~ eol }
|
stmt_move = !{ "MOVE" ~ datum ~ "TO" ~ datum ~ eol }
|
||||||
stmt_remind = !{ "REMIND" ~ (delta | "*") ~ eol }
|
|
||||||
|
|
||||||
statements = { (stmt_date | stmt_bdate | stmt_from | stmt_until | stmt_except | stmt_move | stmt_remind)* }
|
statements = { (stmt_date | stmt_bdate | stmt_from | stmt_until | stmt_except | stmt_move)* }
|
||||||
|
|
||||||
donedate = {
|
donedate = {
|
||||||
datum ~ time ~ "--" ~ datum ~ time
|
datum ~ time ~ "--" ~ datum ~ time
|
||||||
|
|
@ -118,8 +117,7 @@ donedate = {
|
||||||
| datum ~ "--" ~ datum
|
| datum ~ "--" ~ datum
|
||||||
| datum
|
| datum
|
||||||
}
|
}
|
||||||
done_kind = { "DONE" | "CANCELED" }
|
done = !{ "DONE" ~ "[" ~ datum ~ "]" ~ donedate? ~ eol }
|
||||||
done = !{ done_kind ~ "[" ~ datum ~ "]" ~ donedate? ~ eol }
|
|
||||||
dones = { done* }
|
dones = { done* }
|
||||||
|
|
||||||
desc_line = { "#" ~ (" " ~ rest_any)? ~ eol }
|
desc_line = { "#" ~ (" " ~ rest_any)? ~ eol }
|
||||||
|
|
@ -140,24 +138,12 @@ note = {
|
||||||
~ description
|
~ description
|
||||||
}
|
}
|
||||||
|
|
||||||
log_head = !{ "LOG" ~ datum ~ eol }
|
|
||||||
log = { log_head ~ description }
|
|
||||||
|
|
||||||
empty_line = _{ WHITESPACE* ~ NEWLINE }
|
empty_line = _{ WHITESPACE* ~ NEWLINE }
|
||||||
command = { include | timezone | capture | task | note | log }
|
command = { include | timezone | task | note }
|
||||||
|
|
||||||
file = ${ SOI ~ (empty_line* ~ command)* ~ empty_line* ~ WHITESPACE* ~ EOI }
|
file = ${ SOI ~ (empty_line* ~ command)* ~ empty_line* ~ WHITESPACE* ~ EOI }
|
||||||
|
|
||||||
today = { "today" | "t" }
|
today = { "today" }
|
||||||
cli_datum = { datum | today }
|
range_start = { (datum | today) ~ delta? }
|
||||||
cli_date = { cli_datum ~ delta? }
|
range_end = { (datum | today) ~ delta? | delta }
|
||||||
cli_ident = { cli_date | number }
|
range = { SOI ~ range_start ~ ("--" ~ range_end)? ~ EOI}
|
||||||
cli_range_start = { cli_datum ~ delta? }
|
|
||||||
cli_range_end = { cli_datum ~ delta? | delta }
|
|
||||||
cli_range = { cli_range_start ~ ("--" ~ cli_range_end)? }
|
|
||||||
|
|
||||||
cli_date_arg = { SOI ~ cli_date ~ EOI }
|
|
||||||
cli_ident_arg = { SOI ~ cli_ident ~ EOI }
|
|
||||||
cli_range_arg = { SOI ~ cli_range ~ EOI }
|
|
||||||
|
|
||||||
cli_command = ${ SOI ~ empty_line* ~ command ~ empty_line* ~ WHITESPACE* ~ EOI }
|
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,12 @@ use std::result;
|
||||||
use chrono::NaiveDate;
|
use chrono::NaiveDate;
|
||||||
use pest::error::ErrorVariant;
|
use pest::error::ErrorVariant;
|
||||||
use pest::iterators::Pair;
|
use pest::iterators::Pair;
|
||||||
use pest::pratt_parser::{Assoc, Op, PrattParser};
|
use pest::prec_climber::{Assoc, Operator, PrecClimber};
|
||||||
use pest::{Parser, Span};
|
use pest::{Parser, Span};
|
||||||
|
|
||||||
use super::commands::{
|
use super::commands::{
|
||||||
BirthdaySpec, Command, DateSpec, Delta, DeltaStep, Done, DoneDate, DoneKind, Expr, File,
|
BirthdaySpec, Command, DateSpec, Delta, DeltaStep, Done, DoneDate, Expr, File, FormulaSpec,
|
||||||
FormulaSpec, Log, Note, Repeat, Spec, Statement, Task, Var, WeekdaySpec,
|
Note, Repeat, Spec, Statement, Task, Var, WeekdaySpec,
|
||||||
};
|
};
|
||||||
use super::primitives::{Spanned, Time, Weekday};
|
use super::primitives::{Spanned, Time, Weekday};
|
||||||
|
|
||||||
|
|
@ -18,7 +18,7 @@ use super::primitives::{Spanned, Time, Weekday};
|
||||||
pub struct TodayfileParser;
|
pub struct TodayfileParser;
|
||||||
|
|
||||||
pub type Error = pest::error::Error<Rule>;
|
pub type Error = pest::error::Error<Rule>;
|
||||||
pub type Result<T> = result::Result<T, Box<Error>>;
|
pub type Result<T> = result::Result<T, Error>;
|
||||||
|
|
||||||
fn error<S: Into<String>>(span: Span<'_>, message: S) -> Error {
|
fn error<S: Into<String>>(span: Span<'_>, message: S) -> Error {
|
||||||
Error::new_from_span(
|
Error::new_from_span(
|
||||||
|
|
@ -30,26 +30,20 @@ fn error<S: Into<String>>(span: Span<'_>, message: S) -> Error {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fail<S: Into<String>, T>(span: Span<'_>, message: S) -> Result<T> {
|
fn fail<S: Into<String>, T>(span: Span<'_>, message: S) -> Result<T> {
|
||||||
Err(Box::new(error(span, message)))
|
Err(error(span, message))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_include(p: Pair<'_, Rule>) -> Spanned<String> {
|
fn parse_include(p: Pair<'_, Rule>) -> String {
|
||||||
assert_eq!(p.as_rule(), Rule::include);
|
assert_eq!(p.as_rule(), Rule::include);
|
||||||
let p = p.into_inner().next().unwrap();
|
p.into_inner().next().unwrap().as_str().to_string()
|
||||||
let span = (&p.as_span()).into();
|
|
||||||
let name = p.as_str().to_string();
|
|
||||||
Spanned::new(span, name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_timezone(p: Pair<'_, Rule>) -> Spanned<String> {
|
fn parse_timezone(p: Pair<'_, Rule>) -> String {
|
||||||
assert_eq!(p.as_rule(), Rule::timezone);
|
assert_eq!(p.as_rule(), Rule::timezone);
|
||||||
let p = p.into_inner().next().unwrap();
|
p.into_inner().next().unwrap().as_str().trim().to_string()
|
||||||
let span = (&p.as_span()).into();
|
|
||||||
let name = p.as_str().to_string();
|
|
||||||
Spanned::new(span, name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_number(p: Pair<'_, Rule>) -> i32 {
|
fn parse_number(p: Pair<'_, Rule>) -> i32 {
|
||||||
assert_eq!(p.as_rule(), Rule::number);
|
assert_eq!(p.as_rule(), Rule::number);
|
||||||
p.as_str().parse().unwrap()
|
p.as_str().parse().unwrap()
|
||||||
}
|
}
|
||||||
|
|
@ -293,7 +287,7 @@ fn parse_date_fixed(p: Pair<'_, Rule>) -> Result<DateSpec> {
|
||||||
assert_eq!(p.as_rule(), Rule::date_fixed);
|
assert_eq!(p.as_rule(), Rule::date_fixed);
|
||||||
|
|
||||||
let mut spec = DateSpec {
|
let mut spec = DateSpec {
|
||||||
start: NaiveDate::from_ymd_opt(0, 1, 1).unwrap(),
|
start: NaiveDate::from_ymd(0, 1, 1),
|
||||||
start_delta: None,
|
start_delta: None,
|
||||||
start_time: None,
|
start_time: None,
|
||||||
end: None,
|
end: None,
|
||||||
|
|
@ -359,6 +353,24 @@ fn parse_variable(p: Pair<'_, Rule>) -> Var {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_unop_expr(p: Pair<'_, Rule>) -> Spanned<Expr> {
|
||||||
|
assert_eq!(p.as_rule(), Rule::unop_expr);
|
||||||
|
let span = (&p.as_span()).into();
|
||||||
|
|
||||||
|
let mut p = p.into_inner();
|
||||||
|
let p_op = p.next().unwrap();
|
||||||
|
let p_expr = p.next().unwrap();
|
||||||
|
assert_eq!(p.next(), None);
|
||||||
|
|
||||||
|
let inner = parse_expr(p_expr);
|
||||||
|
let expr = match p_op.as_rule() {
|
||||||
|
Rule::unop_neg => Expr::Neg(Box::new(inner)),
|
||||||
|
Rule::unop_not => Expr::Not(Box::new(inner)),
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
Spanned::new(span, expr)
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_paren_expr(p: Pair<'_, Rule>) -> Spanned<Expr> {
|
fn parse_paren_expr(p: Pair<'_, Rule>) -> Spanned<Expr> {
|
||||||
assert_eq!(p.as_rule(), Rule::paren_expr);
|
assert_eq!(p.as_rule(), Rule::paren_expr);
|
||||||
let span = (&p.as_span()).into();
|
let span = (&p.as_span()).into();
|
||||||
|
|
@ -374,43 +386,34 @@ fn parse_term(p: Pair<'_, Rule>) -> Spanned<Expr> {
|
||||||
Rule::number => Spanned::new(span, Expr::Lit(parse_number(p).into())),
|
Rule::number => Spanned::new(span, Expr::Lit(parse_number(p).into())),
|
||||||
Rule::boolean => Spanned::new(span, Expr::Var(parse_boolean(p))),
|
Rule::boolean => Spanned::new(span, Expr::Var(parse_boolean(p))),
|
||||||
Rule::variable => Spanned::new(span, Expr::Var(parse_variable(p))),
|
Rule::variable => Spanned::new(span, Expr::Var(parse_variable(p))),
|
||||||
|
Rule::unop_expr => parse_unop_expr(p),
|
||||||
Rule::paren_expr => parse_paren_expr(p),
|
Rule::paren_expr => parse_paren_expr(p),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_prefix(p: Pair<'_, Rule>, s: Spanned<Expr>) -> Spanned<Expr> {
|
fn parse_op(l: Spanned<Expr>, p: Pair<'_, Rule>, r: Spanned<Expr>) -> Spanned<Expr> {
|
||||||
let span = s.span.join((&p.as_span()).into());
|
|
||||||
let expr = match p.as_rule() {
|
|
||||||
Rule::prefix_neg => Expr::Neg(Box::new(s)),
|
|
||||||
Rule::prefix_not => Expr::Not(Box::new(s)),
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
Spanned::new(span, expr)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_infix(l: Spanned<Expr>, p: Pair<'_, Rule>, r: Spanned<Expr>) -> Spanned<Expr> {
|
|
||||||
let span = l.span.join(r.span);
|
let span = l.span.join(r.span);
|
||||||
let expr = match p.as_rule() {
|
let expr = match p.as_rule() {
|
||||||
// Integer-y operations
|
// Integer-y operations
|
||||||
Rule::infix_add => Expr::Add(Box::new(l), Box::new(r)),
|
Rule::op_add => Expr::Add(Box::new(l), Box::new(r)),
|
||||||
Rule::infix_sub => Expr::Sub(Box::new(l), Box::new(r)),
|
Rule::op_sub => Expr::Sub(Box::new(l), Box::new(r)),
|
||||||
Rule::infix_mul => Expr::Mul(Box::new(l), Box::new(r)),
|
Rule::op_mul => Expr::Mul(Box::new(l), Box::new(r)),
|
||||||
Rule::infix_div => Expr::Div(Box::new(l), Box::new(r)),
|
Rule::op_div => Expr::Div(Box::new(l), Box::new(r)),
|
||||||
Rule::infix_mod => Expr::Mod(Box::new(l), Box::new(r)),
|
Rule::op_mod => Expr::Mod(Box::new(l), Box::new(r)),
|
||||||
|
|
||||||
// Comparisons
|
// Comparisons
|
||||||
Rule::infix_eq => Expr::Eq(Box::new(l), Box::new(r)),
|
Rule::op_eq => Expr::Eq(Box::new(l), Box::new(r)),
|
||||||
Rule::infix_neq => Expr::Neq(Box::new(l), Box::new(r)),
|
Rule::op_neq => Expr::Neq(Box::new(l), Box::new(r)),
|
||||||
Rule::infix_lt => Expr::Lt(Box::new(l), Box::new(r)),
|
Rule::op_lt => Expr::Lt(Box::new(l), Box::new(r)),
|
||||||
Rule::infix_lte => Expr::Lte(Box::new(l), Box::new(r)),
|
Rule::op_lte => Expr::Lte(Box::new(l), Box::new(r)),
|
||||||
Rule::infix_gt => Expr::Gt(Box::new(l), Box::new(r)),
|
Rule::op_gt => Expr::Gt(Box::new(l), Box::new(r)),
|
||||||
Rule::infix_gte => Expr::Gte(Box::new(l), Box::new(r)),
|
Rule::op_gte => Expr::Gte(Box::new(l), Box::new(r)),
|
||||||
|
|
||||||
// Boolean-y operations
|
// Boolean-y operations
|
||||||
Rule::infix_and => Expr::And(Box::new(l), Box::new(r)),
|
Rule::op_and => Expr::And(Box::new(l), Box::new(r)),
|
||||||
Rule::infix_or => Expr::Or(Box::new(l), Box::new(r)),
|
Rule::op_or => Expr::Or(Box::new(l), Box::new(r)),
|
||||||
Rule::infix_xor => Expr::Xor(Box::new(l), Box::new(r)),
|
Rule::op_xor => Expr::Xor(Box::new(l), Box::new(r)),
|
||||||
|
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
@ -420,23 +423,21 @@ fn parse_infix(l: Spanned<Expr>, p: Pair<'_, Rule>, r: Spanned<Expr>) -> Spanned
|
||||||
fn parse_expr(p: Pair<'_, Rule>) -> Spanned<Expr> {
|
fn parse_expr(p: Pair<'_, Rule>) -> Spanned<Expr> {
|
||||||
assert_eq!(p.as_rule(), Rule::expr);
|
assert_eq!(p.as_rule(), Rule::expr);
|
||||||
|
|
||||||
PrattParser::new()
|
fn op(rule: Rule) -> Operator<Rule> {
|
||||||
.op(Op::infix(Rule::infix_or, Assoc::Left) | Op::infix(Rule::infix_xor, Assoc::Left))
|
Operator::new(rule, Assoc::Left)
|
||||||
.op(Op::infix(Rule::infix_and, Assoc::Left))
|
}
|
||||||
.op(Op::infix(Rule::infix_eq, Assoc::Left) | Op::infix(Rule::infix_neq, Assoc::Left))
|
|
||||||
.op(Op::infix(Rule::infix_lt, Assoc::Left)
|
let climber = PrecClimber::new(vec![
|
||||||
| Op::infix(Rule::infix_lte, Assoc::Left)
|
// Precedence from low to high
|
||||||
| Op::infix(Rule::infix_gt, Assoc::Left)
|
op(Rule::op_or) | op(Rule::op_xor),
|
||||||
| Op::infix(Rule::infix_gte, Assoc::Left))
|
op(Rule::op_and),
|
||||||
.op(Op::infix(Rule::infix_mul, Assoc::Left)
|
op(Rule::op_eq) | op(Rule::op_neq),
|
||||||
| Op::infix(Rule::infix_div, Assoc::Left)
|
op(Rule::op_lt) | op(Rule::op_lte) | op(Rule::op_gt) | op(Rule::op_gte),
|
||||||
| Op::infix(Rule::infix_mod, Assoc::Left))
|
op(Rule::op_mul) | op(Rule::op_div) | op(Rule::op_mod),
|
||||||
.op(Op::infix(Rule::infix_add, Assoc::Left) | Op::infix(Rule::infix_sub, Assoc::Left))
|
op(Rule::op_add) | op(Rule::op_sub),
|
||||||
.op(Op::prefix(Rule::prefix_neg) | Op::prefix(Rule::prefix_not))
|
]);
|
||||||
.map_primary(parse_term)
|
|
||||||
.map_prefix(parse_prefix)
|
climber.climb(p.into_inner(), parse_term, parse_op)
|
||||||
.map_infix(parse_infix)
|
|
||||||
.parse(p.into_inner())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_date_expr_start(p: Pair<'_, Rule>, spec: &mut FormulaSpec) -> Result<()> {
|
fn parse_date_expr_start(p: Pair<'_, Rule>, spec: &mut FormulaSpec) -> Result<()> {
|
||||||
|
|
@ -617,34 +618,9 @@ fn parse_stmt_move(p: Pair<'_, Rule>) -> Result<Statement> {
|
||||||
let span = (&p.as_span()).into();
|
let span = (&p.as_span()).into();
|
||||||
let mut p = p.into_inner();
|
let mut p = p.into_inner();
|
||||||
let from = parse_datum(p.next().unwrap())?.value;
|
let from = parse_datum(p.next().unwrap())?.value;
|
||||||
|
let to = parse_datum(p.next().unwrap())?.value;
|
||||||
let mut to = None;
|
|
||||||
let mut to_time = None;
|
|
||||||
for p in p {
|
|
||||||
match p.as_rule() {
|
|
||||||
Rule::datum => to = Some(parse_datum(p)?.value),
|
|
||||||
Rule::time => to_time = Some(parse_time(p)?),
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Statement::Move {
|
|
||||||
span,
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
to_time,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_stmt_remind(p: Pair<'_, Rule>) -> Result<Statement> {
|
|
||||||
assert_eq!(p.as_rule(), Rule::stmt_remind);
|
|
||||||
let mut p = p.into_inner();
|
|
||||||
let delta = match p.next() {
|
|
||||||
Some(p) => Some(parse_delta(p)?),
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
assert_eq!(p.next(), None);
|
assert_eq!(p.next(), None);
|
||||||
Ok(Statement::Remind(delta))
|
Ok(Statement::Move { span, from, to })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_statements(p: Pair<'_, Rule>, task: bool) -> Result<Vec<Statement>> {
|
fn parse_statements(p: Pair<'_, Rule>, task: bool) -> Result<Vec<Statement>> {
|
||||||
|
|
@ -659,7 +635,6 @@ fn parse_statements(p: Pair<'_, Rule>, task: bool) -> Result<Vec<Statement>> {
|
||||||
Rule::stmt_until => parse_stmt_until(p)?,
|
Rule::stmt_until => parse_stmt_until(p)?,
|
||||||
Rule::stmt_except => parse_stmt_except(p)?,
|
Rule::stmt_except => parse_stmt_except(p)?,
|
||||||
Rule::stmt_move => parse_stmt_move(p)?,
|
Rule::stmt_move => parse_stmt_move(p)?,
|
||||||
Rule::stmt_remind => parse_stmt_remind(p)?,
|
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -703,20 +678,10 @@ fn parse_donedate(p: Pair<'_, Rule>) -> Result<DoneDate> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_done_kind(p: Pair<'_, Rule>) -> DoneKind {
|
|
||||||
assert_eq!(p.as_rule(), Rule::done_kind);
|
|
||||||
match p.as_str() {
|
|
||||||
"DONE" => DoneKind::Done,
|
|
||||||
"CANCELED" => DoneKind::Canceled,
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_done(p: Pair<'_, Rule>) -> Result<Done> {
|
fn parse_done(p: Pair<'_, Rule>) -> Result<Done> {
|
||||||
assert_eq!(p.as_rule(), Rule::done);
|
assert_eq!(p.as_rule(), Rule::done);
|
||||||
let mut p = p.into_inner();
|
let mut p = p.into_inner();
|
||||||
|
|
||||||
let kind = parse_done_kind(p.next().unwrap());
|
|
||||||
let done_at = parse_datum(p.next().unwrap())?.value;
|
let done_at = parse_datum(p.next().unwrap())?.value;
|
||||||
let date = if let Some(p) = p.next() {
|
let date = if let Some(p) = p.next() {
|
||||||
Some(parse_donedate(p)?)
|
Some(parse_donedate(p)?)
|
||||||
|
|
@ -726,11 +691,7 @@ fn parse_done(p: Pair<'_, Rule>) -> Result<Done> {
|
||||||
|
|
||||||
assert_eq!(p.next(), None);
|
assert_eq!(p.next(), None);
|
||||||
|
|
||||||
Ok(Done {
|
Ok(Done { date, done_at })
|
||||||
kind,
|
|
||||||
date,
|
|
||||||
done_at,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_dones(p: Pair<'_, Rule>) -> Result<Vec<Done>> {
|
fn parse_dones(p: Pair<'_, Rule>) -> Result<Vec<Done>> {
|
||||||
|
|
@ -794,54 +755,44 @@ fn parse_note(p: Pair<'_, Rule>) -> Result<Note> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_log_head(p: Pair<'_, Rule>) -> Result<Spanned<NaiveDate>> {
|
fn parse_command(p: Pair<'_, Rule>, file: &mut File) -> Result<()> {
|
||||||
assert_eq!(p.as_rule(), Rule::log_head);
|
|
||||||
parse_datum(p.into_inner().next().unwrap())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_log(p: Pair<'_, Rule>) -> Result<Log> {
|
|
||||||
assert_eq!(p.as_rule(), Rule::log);
|
|
||||||
let mut p = p.into_inner();
|
|
||||||
|
|
||||||
let date = parse_log_head(p.next().unwrap())?;
|
|
||||||
let desc = parse_description(p.next().unwrap())?;
|
|
||||||
|
|
||||||
assert_eq!(p.next(), None);
|
|
||||||
|
|
||||||
Ok(Log { date, desc })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse_command(p: Pair<'_, Rule>) -> Result<Spanned<Command>> {
|
|
||||||
assert_eq!(p.as_rule(), Rule::command);
|
assert_eq!(p.as_rule(), Rule::command);
|
||||||
|
|
||||||
let p = p.into_inner().next().unwrap();
|
let p = p.into_inner().next().unwrap();
|
||||||
let span = (&p.as_span()).into();
|
match p.as_rule() {
|
||||||
let command = match p.as_rule() {
|
Rule::include => file.includes.push(parse_include(p)),
|
||||||
Rule::include => Command::Include(parse_include(p)),
|
Rule::timezone => match file.timezone {
|
||||||
Rule::timezone => Command::Timezone(parse_timezone(p)),
|
None => file.timezone = Some(parse_timezone(p)),
|
||||||
Rule::capture => Command::Capture,
|
Some(_) => fail(p.as_span(), "cannot set timezone multiple times")?,
|
||||||
Rule::task => Command::Task(parse_task(p)?),
|
},
|
||||||
Rule::note => Command::Note(parse_note(p)?),
|
Rule::task => file.commands.push(Command::Task(parse_task(p)?)),
|
||||||
Rule::log => Command::Log(parse_log(p)?),
|
Rule::note => file.commands.push(Command::Note(parse_note(p)?)),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
}
|
||||||
Ok(Spanned::new(span, command))
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_file(p: Pair<'_, Rule>) -> Result<File> {
|
pub fn parse_file(p: Pair<'_, Rule>, contents: String) -> Result<File> {
|
||||||
assert_eq!(p.as_rule(), Rule::file);
|
assert_eq!(p.as_rule(), Rule::file);
|
||||||
|
|
||||||
let mut commands = vec![];
|
let mut file = File {
|
||||||
|
contents,
|
||||||
|
includes: vec![],
|
||||||
|
timezone: None,
|
||||||
|
commands: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
for p in p.into_inner() {
|
for p in p.into_inner() {
|
||||||
// For some reason, the EOI in `file` always gets captured
|
// For some reason, the EOI in `file` always gets captured
|
||||||
if p.as_rule() == Rule::EOI {
|
if p.as_rule() == Rule::EOI {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
commands.push(parse_command(p)?);
|
parse_command(p, &mut file)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(File { commands })
|
Ok(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse(path: &Path, input: &str) -> Result<File> {
|
pub fn parse(path: &Path, input: &str) -> Result<File> {
|
||||||
|
|
@ -851,5 +802,5 @@ pub fn parse(path: &Path, input: &str) -> Result<File> {
|
||||||
let file_pair = pairs.next().unwrap();
|
let file_pair = pairs.next().unwrap();
|
||||||
assert_eq!(pairs.next(), None);
|
assert_eq!(pairs.next(), None);
|
||||||
|
|
||||||
parse_file(file_pair).map_err(|e| Box::new(e.with_path(&pathstr)))
|
parse_file(file_pair, input.to_string()).map_err(|e| e.with_path(&pathstr))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use std::cmp::{self, Ordering};
|
use std::cmp::{self, Ordering};
|
||||||
use std::{fmt, ops};
|
use std::fmt;
|
||||||
|
|
||||||
use chrono::{NaiveTime, Timelike};
|
use chrono::{NaiveTime, Timelike};
|
||||||
|
|
||||||
|
|
@ -18,12 +18,6 @@ impl<'a> From<&pest::Span<'a>> for Span {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&Span> for ops::Range<usize> {
|
|
||||||
fn from(span: &Span) -> Self {
|
|
||||||
span.start..span.end
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Span {
|
impl Span {
|
||||||
pub fn join(self, other: Self) -> Self {
|
pub fn join(self, other: Self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -31,10 +25,6 @@ impl Span {
|
||||||
end: cmp::max(self.end, other.end),
|
end: cmp::max(self.end, other.end),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dummy() -> Self {
|
|
||||||
Self { start: 0, end: 0 }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
|
|
@ -53,10 +43,6 @@ impl<T> Spanned<T> {
|
||||||
pub fn new(span: Span, value: T) -> Self {
|
pub fn new(span: Span, value: T) -> Self {
|
||||||
Self { span, value }
|
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
|
// I don't know how one would write this. It works as a polymorphic standalone
|
||||||
|
|
@ -112,15 +98,10 @@ impl Time {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
/// How many minutes into the day this time is.
|
pub fn add_minutes(&self, amount: i32) -> (i32, Self) {
|
||||||
fn minutes(&self) -> i64 {
|
|
||||||
(self.hour as i64) * 60 + (self.min as i64)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_minutes(&self, amount: i64) -> (i64, Self) {
|
|
||||||
match amount.cmp(&0) {
|
match amount.cmp(&0) {
|
||||||
Ordering::Less => {
|
Ordering::Less => {
|
||||||
let mut mins = self.minutes() + amount;
|
let mut mins = (self.hour as i32) * 60 + (self.min as i32) + amount;
|
||||||
|
|
||||||
let days = mins.div_euclid(60 * 24);
|
let days = mins.div_euclid(60 * 24);
|
||||||
mins = mins.rem_euclid(60 * 24);
|
mins = mins.rem_euclid(60 * 24);
|
||||||
|
|
@ -130,7 +111,7 @@ impl Time {
|
||||||
(days, Self::new(hour, min))
|
(days, Self::new(hour, min))
|
||||||
}
|
}
|
||||||
Ordering::Greater => {
|
Ordering::Greater => {
|
||||||
let mut mins = self.minutes() + amount;
|
let mut mins = (self.hour as i32) * 60 + (self.min as i32) + amount;
|
||||||
|
|
||||||
let mut days = mins.div_euclid(60 * 24);
|
let mut days = mins.div_euclid(60 * 24);
|
||||||
mins = mins.rem_euclid(60 * 24);
|
mins = mins.rem_euclid(60 * 24);
|
||||||
|
|
@ -149,18 +130,9 @@ impl Time {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_hours(&self, amount: i64) -> (i64, Self) {
|
pub fn add_hours(&self, amount: i32) -> (i32, Self) {
|
||||||
self.add_minutes(amount * 60)
|
self.add_minutes(amount * 60)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `a.minutes_to(b)` returns the minutes from `a` to `b`, meaning it is
|
|
||||||
/// greater than 0 if `a` is earlier than `b`.
|
|
||||||
///
|
|
||||||
/// May return weird amounts if [`Self::in_normal_range`] is not true for
|
|
||||||
/// both.
|
|
||||||
pub fn minutes_to(&self, other: Self) -> i64 {
|
|
||||||
other.minutes() - self.minutes()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
|
@ -206,13 +178,13 @@ impl Weekday {
|
||||||
/// `Saturday`, `Sunday`).
|
/// `Saturday`, `Sunday`).
|
||||||
pub fn full_name(self) -> &'static str {
|
pub fn full_name(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::Monday => "Monday",
|
Weekday::Monday => "Monday",
|
||||||
Self::Tuesday => "Tuesday",
|
Weekday::Tuesday => "Tuesday",
|
||||||
Self::Wednesday => "Wednesday",
|
Weekday::Wednesday => "Wednesday",
|
||||||
Self::Thursday => "Thursday",
|
Weekday::Thursday => "Thursday",
|
||||||
Self::Friday => "Friday",
|
Weekday::Friday => "Friday",
|
||||||
Self::Saturday => "Saturday",
|
Weekday::Saturday => "Saturday",
|
||||||
Self::Sunday => "Sunday",
|
Weekday::Sunday => "Sunday",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
|
#![warn(future_incompatible)]
|
||||||
|
#![warn(rust_2018_idioms)]
|
||||||
|
#![warn(clippy::all)]
|
||||||
|
#![warn(clippy::use_self)]
|
||||||
|
|
||||||
mod cli;
|
mod cli;
|
||||||
mod error;
|
|
||||||
mod eval;
|
mod eval;
|
||||||
mod files;
|
mod files;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue