Compare commits

...

80 commits

Author SHA1 Message Date
abf4d5a502 Silence remaining warnings 2024-11-21 02:14:35 +01:00
22eec53c5a Move warnings to Cargo.toml 2024-11-21 02:14:12 +01:00
4001d0653b Satisfy clippy 2024-08-03 03:31:37 +02:00
cb470d201e Update dependencies 2023-02-11 22:24:15 +01:00
4529f383fe Update chrono 2023-02-11 22:12:58 +01:00
64c41b1073 Satisfy clippy 2023-02-11 21:50:38 +01:00
f3792fae64 Add marks to show which span a reminder belongs to 2022-11-01 16:41:40 +01:00
23b0a5e5fc Use pratt parser for prefix logic 2022-11-01 15:14:30 +01:00
11d9a2f1c7 Switch to pratt parser 2022-11-01 14:55:08 +01:00
f01c3818c0 Ignore some false positives 2022-10-31 18:06:49 +01:00
c5a2e5dccb Fix some warnings 2022-10-31 17:16:27 +01:00
57da1026c2 Use cove lints 2022-10-31 17:07:10 +01:00
36d8082c9d Update dependencies 2022-10-31 17:03:00 +01:00
aaa3537e90 Update dependencies 2022-05-19 14:00:33 +02:00
bc0d2481c8 Switch from structopt to clap 2022-05-19 13:56:09 +02:00
a63529b972 Increase default --range by one day 2022-05-04 11:17:25 +02:00
972a590ba9 Satisfy clippy 2022-05-02 12:57:21 +02:00
f42cf01a15 Fix cli arg anchoring 2022-05-02 12:49:03 +02:00
74433eccbe Highlight current birthdays 2022-05-02 11:50:59 +02:00
f231fee508 Document Done fields 2022-05-02 11:37:39 +02:00
29e4e786ea Release version 0.2.0 2022-03-18 03:21:02 +01:00
a236e8d953 Update dependencies 2022-03-18 03:20:40 +01:00
6b82a2cfd2 Update changelog 2022-03-18 03:09:27 +01:00
570142c0b6 Fix weekday deltas 2022-03-17 21:32:12 +01:00
4a2f72fabf Remove unnecessary label messages 2022-01-15 01:13:31 +01:00
f82b368fe1 Use new format syntax in most places 2022-01-15 01:10:33 +01:00
a3a0e0b9cf Add more 'today new' templates 2022-01-14 23:23:34 +01:00
3befb7c773 Implement 'today new' 2022-01-14 22:41:31 +01:00
0dcb75b103 Parse arbitrary commands 2022-01-14 22:37:59 +01:00
fe1bf309b8 Prepare string editing for 'today new' 2022-01-14 22:37:55 +01:00
82affe39a1 Simplify error handling in cli module 2022-01-14 22:07:02 +01:00
371c86f10f Reword changelog slightly 2022-01-14 22:07:02 +01:00
0b4f614af6 Specify default capture file 2022-01-14 22:07:02 +01:00
039b7b73ee Reorder commands inside a file again 2022-01-09 20:25:02 +01:00
d169037b7a Don't store file content string in File 2022-01-09 20:12:06 +01:00
721718eee4 Fix line numbers in show command 2022-01-09 16:49:10 +01:00
170e291ec5 Simplify DoneDate when formatting 2022-01-09 16:44:15 +01:00
20fc4bd3cc Update changelog 2022-01-09 16:39:50 +01:00
d7a26128a0 Fix show command argument parsing 2022-01-09 16:04:42 +01:00
26479ac58d Improve format of show command 2022-01-09 16:04:34 +01:00
b430f1594f Add spans to all commands 2022-01-09 15:12:15 +01:00
e0b6c839e2 Fix doc comment 2022-01-08 04:15:22 +01:00
1721f8c456 Ignore clippy warning 2022-01-08 04:12:14 +01:00
efc97800b2 Remove empty logs 2022-01-08 04:11:55 +01:00
26c9c1faf1 Add one-letter command aliases 2022-01-08 01:02:13 +01:00
ff642cc8c8 Specify --date via CliDate 2022-01-08 01:01:49 +01:00
0484eda859 Add 'log' cli command 2022-01-08 00:49:17 +01:00
3e2fa54213 Show logs in addition to entries 2022-01-07 22:34:00 +01:00
badc0d7a9f Print parse errors with codespan-reporting 2022-01-07 21:58:45 +01:00
2f6911eeca Refactor CLI argument parsing 2022-01-07 20:04:31 +01:00
4a46e70a73 Print markers for logs and entry descriptions 2022-01-06 19:15:56 +01:00
b3d81a8d0a Fix more warnings 2022-01-06 18:34:09 +01:00
80666c2cf5 Use same path for saving files as for loading 2022-01-06 18:22:26 +01:00
a4a6726091 Fix a few warnings 2022-01-06 18:22:18 +01:00
3f5e06b113 Adapt cli code to changes 2022-01-06 18:04:34 +01:00
5e47dddd4c Make error formatting more generic
Now, some errors are generic over the FileId type, meaning I can now
pretty-print command line argument errors as well as normal errors with
the same machinery. This is accomplished via the Eprint trait, whose
generics are a tad complex but probably still fine.
2022-01-05 02:59:03 +01:00
8ae691bc3c Adapt eval code to changes
These changes include the new file representation and Files API as well
as the codespan error reporting.
2022-01-02 15:06:40 +01:00
810ec67cf7 Reenable basic cli interaction 2022-01-02 15:06:40 +01:00
ef287b9fd0 Format some errors with codespan-reporting 2022-01-02 15:06:40 +01:00
d9c1dc78e4 Integrate codespan-reporting 2022-01-02 11:47:22 +01:00
d8617ede24 Adapt Files to new file representation 2022-01-02 00:43:06 +01:00
d27812b836 Represent commands in file uniformly 2022-01-02 00:42:06 +01:00
004aa87153 Disable modules in preparation for refactoring
This way, compile errors from modules further up the dependency chain
don't obscure the important errors
2022-01-02 00:36:53 +01:00
017098da34 Parse and format logs 2022-01-01 14:43:23 +01:00
0e4ef7fef3 Allow tasks to be canceled 2021-12-31 23:57:40 +01:00
e0cb1c8f23 Don't write unchanged files 2021-12-31 02:42:31 +01:00
e01de41925 Make current day stand out more 2021-12-30 02:30:13 +01:00
8af268bd68 Switch to patched tzdir
This fixes today not respecting the TZDIR environment variable, which is
necessary since NixOS uses a nonstandard time zone data location.
2021-12-29 00:53:37 +01:00
f9dbc0db01 Format Cargo.toml 2021-12-23 17:02:41 +00:00
2b88008ba3 Align "now" time with other times 2021-12-22 17:02:51 +00:00
7a67d01e1f Fix reminders not making entries relevant 2021-12-22 16:39:13 +00:00
e2624fe64f Fix "now" not being right-aligned 2021-12-22 16:11:34 +00:00
2078c5e883 Color output 2021-12-22 15:58:10 +00:00
12c86e0d28 Fix formatting of ReminderWhile 2021-12-21 19:56:31 +01:00
07ce85982c Fix when and how reminders are calculated and displayed 2021-12-21 19:53:59 +01:00
73a44a697a Error when moving untimed entry to new time 2021-12-21 19:34:55 +01:00
1ac39c69f2 Move entries to a different time 2021-12-21 19:24:58 +01:00
05a4582f13 Display reminders correctly 2021-12-21 00:14:44 +01:00
66da16f4e3 Evaluate REMINDs 2021-12-21 00:14:44 +01:00
fe22c66c5c Parse REMINDs 2021-12-21 00:14:41 +01:00
37 changed files with 3769 additions and 1636 deletions

View file

@ -2,6 +2,48 @@
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
### Added

874
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,14 +1,35 @@
[package]
name = "today"
version = "0.1.0"
version = "0.2.0"
edition = "2021"
[dependencies]
chrono = "0.4.19"
chrono = "0.4.23"
clap = { version = "4.1.4", features = ["derive"] }
codespan-reporting = "0.11.1"
colored = "2.0.0"
computus = "1.0.0"
directories = "4.0.1"
pest = "2.1.3"
pest_derive = "2.1.0"
structopt = "0.3.25"
thiserror = "1.0.30"
tzfile = "0.1.3"
edit = "0.1.4"
pest = "2.5.5"
pest_derive = "2.5.5"
promptly = "0.3.1"
termcolor = "1.2.0"
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"

View file

@ -3,56 +3,100 @@ use std::str::FromStr;
use std::{process, result};
use chrono::{NaiveDate, NaiveDateTime};
use clap::Parser;
use codespan_reporting::files::SimpleFile;
use directories::ProjectDirs;
use structopt::StructOpt;
use crate::eval::{DateRange, Entry, EntryMode, SourceInfo};
use crate::files::arguments::Range;
use crate::files::{self, Files};
use crate::eval::{self, DateRange, Entry, EntryMode};
use crate::files::cli::{CliDate, CliIdent, CliRange};
use crate::files::{self, Files, ParseError};
use self::error::Result;
use self::error::{Error, Result};
use self::layout::line::LineLayout;
mod cancel;
mod done;
mod error;
mod layout;
mod log;
mod new;
mod print;
mod show;
mod util;
#[derive(Debug, StructOpt)]
#[derive(Debug, clap::Parser)]
pub struct Opt {
/// File to load
#[structopt(short, long, parse(from_os_str))]
#[clap(short, long)]
file: Option<PathBuf>,
/// Overwrite the current date
#[structopt(short, long)]
date: Option<NaiveDate>,
/// The range days to focus on
#[structopt(short, long, default_value = "today-2d--today+13d")]
#[clap(short, long, default_value = "t")]
date: String,
/// Range of days to focus on
#[clap(short, long, default_value = "t-2d--t+2w")]
range: String,
#[structopt(subcommand)]
#[clap(subcommand)]
command: Option<Command>,
}
#[derive(Debug, StructOpt)]
#[derive(Debug, clap::Subcommand)]
pub enum Command {
#[allow(rustdoc::broken_intra_doc_links)]
/// Shows individual entries in detail
#[clap(alias = "s")]
Show {
/// Entries to show
#[structopt(required = true)]
entries: Vec<usize>,
/// Entries and days to show
#[clap(required = true)]
identifiers: Vec<String>,
},
/// Create a new entry based on a template
#[clap(alias = "n")]
New {
#[clap(subcommand)]
template: Template,
},
/// Marks one or more entries as done
#[clap(alias = "d")]
Done {
/// Entries to mark as done
#[structopt(required = true)]
#[clap(required = true)]
entries: Vec<usize>,
},
/// Reformat all loaded files
/// Marks one or more entries as canceled
#[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,
}
#[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 {
ProjectDirs::from("", "", "today")
.expect("could not determine config dir")
@ -60,18 +104,9 @@ fn default_file() -> PathBuf {
.join("main.today")
}
fn load_files(opt: &Opt) -> result::Result<Files, files::Error> {
fn load_files(opt: &Opt, files: &mut Files) -> result::Result<(), files::Error> {
let file = opt.file.clone().unwrap_or_else(default_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
}
files.load(&file)
}
fn find_entries(files: &Files, range: DateRange) -> Result<Vec<Entry>> {
@ -87,6 +122,37 @@ fn find_layout(
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<()> {
match &opt.command {
None => {
@ -94,11 +160,25 @@ fn run_command(opt: &Opt, files: &mut Files, range: DateRange, now: NaiveDateTim
let layout = find_layout(files, &entries, range, now);
print::print(&layout);
}
Some(Command::Show { entries: ns }) => {
Some(Command::Show { identifiers }) => {
let entries = find_entries(files, range)?;
let layout = find_layout(files, &entries, range, now);
show::show(files, &entries, &layout, ns)?;
let idents = parse_show_idents(identifiers, now.date())?;
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 }) => {
let entries = find_entries(files, range)?;
let layout = find_layout(files, &entries, range, now);
@ -107,50 +187,55 @@ fn run_command(opt: &Opt, files: &mut Files, range: DateRange, now: NaiveDateTim
let layout = find_layout(files, &entries, range, now);
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(),
}
Ok(())
}
pub fn run() {
let opt = Opt::from_args();
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 mut files = match load_files(&opt) {
Ok(result) => result,
Err(e) => {
e.print();
let range = parse_eval_arg("--range", &opt.range, |range: CliRange| {
range.eval((), now.date())
})?;
run_command(&opt, files, range, now)?;
Ok(())
}
pub fn run() {
let opt = Opt::parse();
let mut files = Files::new();
if let Err(e) = load_files(&opt, &mut files) {
crate::error::eprint_error(&files, &e);
process::exit(1);
}
};
let now = find_now(&opt, &files);
// 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());
if let Err(e) = run_with_files(opt, &mut files) {
crate::error::eprint_error(&files, &e);
process::exit(1);
}
if let Err(e) = files.save() {
e.print();
crate::error::eprint_error(&files, &e);
process::exit(1);
}
}

37
src/cli/cancel.rs Normal file
View file

@ -0,0 +1,37 @@
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))
}
}

View file

@ -3,7 +3,7 @@ use std::vec;
use chrono::NaiveDateTime;
use crate::eval::Entry;
use crate::files::commands::Done;
use crate::files::commands::{Done, DoneKind};
use crate::files::Files;
use super::error::{Error, Result};
@ -20,6 +20,7 @@ pub fn done(
for &number in numbers {
let entry = &entries[layout.look_up_number(number)?];
let done = Done {
kind: DoneKind::Done,
date: entry.dates.map(|dates| dates.into()),
done_at: now.date(),
};

View file

@ -1,23 +1,54 @@
use std::result;
use std::{io, result};
use crate::eval::{self, SourceInfo};
use chrono::NaiveDate;
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)]
pub enum Error {
#[error("{0}")]
Eval(#[from] eval::Error),
Eval(#[from] eval::Error<FileSource>),
#[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}")]
NoSuchEntry(usize),
#[error("No log for {0}")]
NoSuchLog(NaiveDate),
#[error("Not a task")]
NotATask(Vec<usize>),
#[error("No capture file found")]
NoCaptureFile,
#[error("Error editing: {0}")]
EditingIo(io::Error),
}
impl Error {
pub fn print<'a>(&self, sources: &[SourceInfo<'a>]) {
pub type Result<T> = result::Result<T, Error>;
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 {
Error::Eval(e) => e.print(sources),
Error::NoSuchEntry(n) => eprintln!("No entry with number {}", n),
Error::NotATask(ns) => {
Self::Eval(e) => e.eprint(files, config),
Self::ArgumentParse { file, error } => error.eprint(file, config),
Self::ArgumentEval { file, error } => error.eprint(file, config),
Self::NoSuchEntry(n) => eprintln!("No entry with number {n}"),
Self::NoSuchLog(date) => eprintln!("No log for {date}"),
Self::NotATask(ns) => {
if ns.is_empty() {
eprintln!("Not a task.");
} else if ns.len() == 1 {
@ -27,8 +58,11 @@ impl Error {
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>;

View file

@ -16,7 +16,7 @@ pub fn layout(
now: NaiveDateTime,
) -> LineLayout {
let mut day_layout = DayLayout::new(range, now);
day_layout.layout(files, entries);
day_layout.layout(entries);
let mut line_layout = LineLayout::new();
line_layout.render(files, entries, &day_layout);

View file

@ -8,9 +8,7 @@ use std::collections::HashMap;
use chrono::{NaiveDate, NaiveDateTime};
use crate::eval::{DateRange, Dates, Entry, EntryKind};
use crate::files::commands::Command;
use crate::files::primitives::Time;
use crate::files::Files;
#[derive(Debug)]
pub enum DayEntry {
@ -48,18 +46,13 @@ impl DayLayout {
}
}
pub fn layout(&mut self, files: &Files, entries: &[Entry]) {
pub fn layout(&mut self, entries: &[Entry]) {
self.insert(self.today, DayEntry::Now(self.time));
let mut commands = entries
.iter()
.enumerate()
.map(|(i, e)| (i, e, files.command(e.source)))
.collect::<Vec<_>>();
let mut entries = entries.iter().enumerate().collect::<Vec<_>>();
Self::sort_entries(&mut entries);
Self::sort_entries(&mut commands);
for (index, entry, _) in commands {
for (index, entry) in entries {
self.layout_entry(index, entry);
}
@ -73,7 +66,9 @@ impl DayLayout {
fn layout_entry(&mut self, index: usize, entry: &Entry) {
match entry.kind {
EntryKind::Task => self.layout_task(index, entry),
EntryKind::TaskDone(at) => self.layout_task_done(index, entry, at),
EntryKind::TaskDone(at) | EntryKind::TaskCanceled(at) => {
self.layout_task_done(index, entry, at)
}
EntryKind::Note | EntryKind::Birthday(_) => self.layout_note(index, entry),
}
}
@ -81,10 +76,17 @@ impl DayLayout {
fn layout_task(&mut self, index: usize, entry: &Entry) {
if let Some(dates) = entry.dates {
let (start, end) = dates.sorted().dates();
if self.today < start && (start - self.today).num_days() < 7 {
// TODO Make this adjustable, maybe even per-command
if self.today < self.range.from() || self.range.until() < self.today {
// 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.today && self.today < end {
let days = (end - self.today).num_days();
self.insert(self.today, DayEntry::ReminderWhile(index, days));
@ -115,7 +117,18 @@ impl DayLayout {
fn layout_note(&mut self, index: usize, entry: &Entry) {
if let Some(dates) = entry.dates {
let (start, end) = dates.sorted().dates();
if start < self.range.from() && self.range.until() < end {
if self.today < self.range.from() || self.range.until() < self.today {
// 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
// we just layout it as a dated entry, so instead we add it as a
// reminder. Since we are usually more interested in when
@ -123,9 +136,8 @@ impl DayLayout {
// the end.
let days = (end - self.today).num_days();
self.insert(self.today, DayEntry::ReminderWhile(index, days));
} else {
self.layout_dated_entry(index, dates);
}
self.layout_dated_entry(index, dates);
} else {
self.insert(self.today, DayEntry::Undated(index));
}
@ -177,7 +189,7 @@ impl DayLayout {
}
}
fn sort_entries(entries: &mut Vec<(usize, &Entry, &Command)>) {
fn sort_entries(entries: &mut [(usize, &Entry)]) {
// Entries should be sorted by these factors, in descending order of
// significance:
// 1. Their start date, if any
@ -186,28 +198,28 @@ impl DayLayout {
// 4. Their title
// 4.
entries.sort_by_key(|(_, _, c)| c.title());
entries.sort_by_key(|(_, e)| &e.title);
// 3.
entries.sort_by_key(|(_, e, _)| match e.kind {
entries.sort_by_key(|(_, e)| match e.kind {
EntryKind::Task => 0,
EntryKind::TaskDone(_) => 1,
EntryKind::TaskDone(_) | EntryKind::TaskCanceled(_) => 1,
EntryKind::Birthday(_) => 2,
EntryKind::Note => 3,
});
// 2.
entries.sort_by(|(_, e1, _), (_, e2, _)| {
entries.sort_by(|(_, e1), (_, e2)| {
let d1 = e1.dates.map(|d| d.sorted().other_with_time());
let d2 = e2.dates.map(|d| d.sorted().other_with_time());
d2.cmp(&d1) // Inverse comparison
});
// 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 Vec<DayEntry>) {
fn sort_day(day: &mut [DayEntry]) {
// In a day, entries should be sorted into these categories:
// 1. Untimed entries that end at the current day
// 2. Timed entries, based on

View file

@ -11,14 +11,44 @@ use crate::eval::{Entry, EntryKind};
use crate::files::primitives::Time;
use crate::files::Files;
use super::super::error::{Error, Result};
use super::super::error::Error;
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)]
pub enum SpanSegment {
Start,
Middle,
End,
Start(SpanStyle),
Middle(SpanStyle),
Mark(SpanStyle),
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)]
@ -28,10 +58,21 @@ pub enum Times {
FromTo(Time, Time),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LineKind {
Task,
Done,
Canceled,
Note,
Birthday,
}
pub enum LineEntry {
Day {
spans: Vec<Option<SpanSegment>>,
date: NaiveDate,
today: bool,
has_log: bool,
},
Now {
spans: Vec<Option<SpanSegment>>,
@ -40,8 +81,12 @@ pub enum LineEntry {
Entry {
number: Option<usize>,
spans: Vec<Option<SpanSegment>>,
today: bool,
time: Times,
kind: LineKind,
text: String,
has_desc: bool,
extra: Option<String>,
},
}
@ -79,12 +124,18 @@ impl LineLayout {
self.step_spans();
for day in layout.range.days() {
let today = day == layout.today;
let spans = self.spans_for_line();
self.line(LineEntry::Day { spans, date: day });
self.line(LineEntry::Day {
spans,
date: day,
today,
has_log: files.log(day).is_some(),
});
let layout_entries = layout.days.get(&day).expect("got nonexisting day");
for layout_entry in layout_entries {
self.render_layout_entry(files, entries, layout_entry);
self.render_layout_entry(entries, layout_entry, today);
}
}
}
@ -101,7 +152,7 @@ impl LineLayout {
&self.lines
}
pub fn look_up_number(&self, number: usize) -> Result<usize> {
pub fn look_up_number(&self, number: usize) -> Result<usize, Error> {
self.numbers
.iter()
.filter(|(_, n)| **n == number)
@ -110,12 +161,11 @@ impl LineLayout {
.ok_or(Error::NoSuchEntry(number))
}
fn render_layout_entry(&mut self, files: &Files, entries: &[Entry], l_entry: &DayEntry) {
fn render_layout_entry(&mut self, entries: &[Entry], l_entry: &DayEntry, today: bool) {
match l_entry {
DayEntry::End(i) => {
self.stop_span(*i);
let text = Self::format_entry(files, entries, *i);
self.line_entry(Some(*i), Times::Untimed, text);
self.line_entry(entries, *i, today, Times::Untimed, None);
}
DayEntry::Now(t) => self.line(LineEntry::Now {
spans: self.spans_for_line(),
@ -123,89 +173,95 @@ impl LineLayout {
}),
DayEntry::TimedEnd(i, t) => {
self.stop_span(*i);
let text = Self::format_entry(files, entries, *i);
self.line_entry(Some(*i), Times::At(*t), text);
self.line_entry(entries, *i, today, Times::At(*t), None);
}
DayEntry::TimedAt(i, t, t2) => {
let time = t2
.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);
let time = t2.map(|t2| Times::FromTo(*t, t2)).unwrap_or(Times::At(*t));
self.line_entry(entries, *i, today, time, None);
}
DayEntry::TimedStart(i, t) => {
self.start_span(*i);
let text = Self::format_entry(files, entries, *i);
self.line_entry(Some(*i), Times::At(*t), text);
self.line_entry(entries, *i, today, Times::At(*t), None);
}
DayEntry::ReminderSince(i, d) => {
let text = Self::format_entry(files, entries, *i);
let text = if *d == 1 {
format!("{} (yesterday)", text)
let extra = if *d == 1 {
"yesterday".to_string()
} else {
format!("{} ({} days ago)", text, d)
format!("{d} days ago")
};
self.line_entry(Some(*i), Times::Untimed, text);
self.line_entry(entries, *i, today, Times::Untimed, Some(extra));
}
DayEntry::At(i) => {
let text = Self::format_entry(files, entries, *i);
self.line_entry(Some(*i), Times::Untimed, text);
self.line_entry(entries, *i, today, Times::Untimed, None);
}
DayEntry::ReminderWhile(i, d) => {
let text = Self::format_entry(files, entries, *i);
let plural = if *d == 1 { "" } else { "s" };
let text = format!("{} ({} day{} left)", text, i, plural);
self.line_entry(Some(*i), Times::Untimed, text);
let extra = format!("{d} day{plural} left");
self.mark_span(*i);
self.line_entry(entries, *i, today, Times::Untimed, Some(extra));
}
DayEntry::Undated(i) => {
let text = Self::format_entry(files, entries, *i);
self.line_entry(Some(*i), Times::Untimed, text);
self.line_entry(entries, *i, today, Times::Untimed, None);
}
DayEntry::Start(i) => {
self.start_span(*i);
let text = Self::format_entry(files, entries, *i);
self.line_entry(Some(*i), Times::Untimed, text);
self.line_entry(entries, *i, today, Times::Untimed, None);
}
DayEntry::ReminderUntil(i, d) => {
let text = Self::format_entry(files, entries, *i);
let text = if *d == 1 {
format!("{} (tomorrow)", text)
let extra = if *d == 1 {
"tomorrow".to_string()
} else {
format!("{} (in {} days)", text, d)
format!("in {d} days")
};
self.line_entry(Some(*i), Times::Untimed, text);
self.line_entry(entries, *i, today, Times::Untimed, Some(extra));
}
}
}
fn format_entry(files: &Files, entries: &[Entry], index: usize) -> String {
let entry = entries[index];
let command = files.command(entry.source);
pub fn entry_kind(entry: &Entry) -> LineKind {
match entry.kind {
EntryKind::Task => format!("T {}", command.title()),
EntryKind::TaskDone(_) => format!("D {}", command.title()),
EntryKind::Note => format!("N {}", command.title()),
EntryKind::Birthday(Some(age)) => format!("B {} ({})", command.title(), age),
EntryKind::Birthday(None) => format!("B {}", command.title()),
EntryKind::Task => LineKind::Task,
EntryKind::TaskDone(_) => LineKind::Done,
EntryKind::TaskCanceled(_) => LineKind::Canceled,
EntryKind::Note => LineKind::Note,
EntryKind::Birthday(_) => LineKind::Birthday,
}
}
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) {
for span in self.spans.iter_mut() {
for (i, span) in self.spans.iter_mut().enumerate() {
if span.is_none() {
*span = Some((index, SpanSegment::Start));
let style = SpanStyle::from_indentation(i);
*span = Some((index, SpanSegment::Start(style)));
return;
}
}
// Not enough space, we need another column
self.spans.push(Some((index, SpanSegment::Start)));
let style = SpanStyle::from_indentation(self.spans.len());
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) {
for span in self.spans.iter_mut() {
match span {
Some((i, s)) if *i == index => *s = SpanSegment::End,
Some((i, s)) if *i == index => *s = SpanSegment::End(s.style()),
_ => {}
}
}
@ -214,8 +270,10 @@ impl LineLayout {
fn step_spans(&mut self) {
for span in self.spans.iter_mut() {
match span {
Some((_, s @ SpanSegment::Start)) => *s = SpanSegment::Middle,
Some((_, SpanSegment::End)) => *span = None,
Some((_, s @ (SpanSegment::Start(_) | SpanSegment::Mark(_)))) => {
*s = SpanSegment::Middle(s.style())
}
Some((_, SpanSegment::End(_))) => *span = None,
_ => {}
}
}
@ -233,24 +291,34 @@ impl LineLayout {
self.step_spans();
}
fn line_entry(&mut self, index: Option<usize>, time: Times, text: String) {
let number = match index {
Some(index) => Some(match self.numbers.get(&index) {
fn line_entry(
&mut self,
entries: &[Entry],
index: usize,
today: bool,
time: Times,
extra: Option<String>,
) {
let entry = &entries[index];
let number = match self.numbers.get(&index) {
Some(number) => *number,
None => {
self.last_number += 1;
self.numbers.insert(index, self.last_number);
self.last_number
}
}),
None => None,
};
self.line(LineEntry::Entry {
number,
number: Some(number),
spans: self.spans_for_line(),
today,
time,
text,
kind: Self::entry_kind(entry),
text: Self::entry_title(entry),
has_desc: entry.has_description,
extra,
});
}
}

24
src/cli/log.rs Normal file
View file

@ -0,0 +1,24 @@
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 Normal file
View file

@ -0,0 +1,114 @@
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)
}

View file

@ -1,10 +1,12 @@
use std::cmp;
use chrono::{Datelike, NaiveDate};
use colored::{ColoredString, Colorize};
use crate::files::primitives::{Time, Weekday};
use super::layout::line::{LineEntry, LineLayout, SpanSegment, Times};
use super::layout::line::{LineEntry, LineKind, LineLayout, SpanSegment, SpanStyle, Times};
use super::util;
struct ShowLines {
num_width: usize,
@ -23,86 +25,156 @@ impl ShowLines {
fn display_line(&mut self, line: &LineEntry) {
match line {
LineEntry::Day { spans, date } => self.display_line_date(spans, *date),
LineEntry::Day {
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::Entry {
number,
spans,
today,
time,
kind,
text,
} => self.display_line_entry(*number, spans, *time, text),
has_desc,
extra,
} => self
.display_line_entry(*number, spans, *today, *time, *kind, text, *has_desc, extra),
}
}
fn display_line_date(&mut self, spans: &[Option<SpanSegment>], date: NaiveDate) {
fn display_line_date(
&mut self,
spans: &[Option<SpanSegment>],
date: NaiveDate,
today: bool,
has_log: bool,
) {
let weekday: Weekday = date.weekday().into();
let weekday = weekday.full_name();
self.push(&format!(
"{:=>nw$}={:=<sw$}=== {:9} {} ==={:=<sw$}={:=>nw$}\n",
let styled = |s: &str| {
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$}",
"",
Self::display_spans(spans, '='),
weekday,
date,
"",
"",
nw = self.num_width,
sw = self.span_width
w = self.num_width + self.span_width
));
self.push(&format!("{p1}{p2}{p3}{p4}{p5}\n"));
}
fn display_line_now(&mut self, spans: &[Option<SpanSegment>], time: Time) {
self.push(&format!(
"{:<nw$} {:sw$} {}\n",
"now",
Self::display_spans(spans, ' '),
time,
"{:>nw$} {} {}\n",
"now".bright_cyan().bold(),
self.display_spans(spans, " ".into()),
Self::display_time(Times::At(time)),
nw = self.num_width,
sw = self.span_width
));
}
#[allow(clippy::too_many_arguments)]
fn display_line_entry(
&mut self,
number: Option<usize>,
spans: &[Option<SpanSegment>],
today: bool,
time: Times,
kind: LineKind,
text: &str,
has_desc: bool,
extra: &Option<String>,
) {
let num = match number {
Some(n) => format!("{}", n),
Some(n) => format!("{n}"),
None => "".to_string(),
};
let time = match time {
Times::Untimed => "".to_string(),
Times::At(t) => format!("{} ", t),
Times::FromTo(t1, t2) => format!("{}--{} ", t1, t2),
let text = if kind == LineKind::Birthday && today {
util::display_current_birthday_text(text)
} else {
text.into()
};
self.push(&format!(
"{:>nw$} {:sw$} {}{}\n",
num,
Self::display_spans(spans, ' '),
time,
"{:>nw$} {} {}{} {}{}{}\n",
num.bright_black(),
self.display_spans(spans, " ".into()),
util::display_kind(kind),
Self::display_time(time),
text,
Self::display_marker(has_desc, ""),
Self::display_extra(extra),
nw = self.num_width,
sw = self.span_width
))
}
fn display_spans(spans: &[Option<SpanSegment>], empty: char) -> String {
fn display_spans(&self, spans: &[Option<SpanSegment>], empty: ColoredString) -> String {
let mut result = String::new();
for segment in spans {
result.push(match segment {
Some(SpanSegment::Start) => '┌',
Some(SpanSegment::Middle) => '│',
Some(SpanSegment::End) => '└',
None => empty,
});
for i in 0..self.span_width {
if let Some(Some(segment)) = spans.get(i) {
let colored_str = match segment {
SpanSegment::Start(_) => "".bright_black(),
SpanSegment::Middle(SpanStyle::Solid) => "".bright_black(),
SpanSegment::Middle(SpanStyle::Dashed) => "".bright_black(),
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
}
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) {
self.result.push_str(line);
}

View file

@ -1,61 +1,108 @@
use crate::eval::{Entry, EntryKind};
use crate::files::Files;
use chrono::NaiveDate;
use codespan_reporting::files::Files as CsFiles;
use colored::Colorize;
use super::error::Result;
use crate::eval::{Entry, EntryKind};
use crate::files::commands::{Command, Log};
use crate::files::primitives::Spanned;
use crate::files::{Files, Sourced};
use super::error::Error;
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) => &note.desc,
Command::Log(log) => &log.desc,
_ => &[],
};
if !desc.is_empty() {
println!();
for line in desc {
println!("{}", line);
}
}
}
fn show_entry(files: &Files, entry: &Entry) {
let command = files.command(entry.source);
match entry.kind {
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 kind = util::display_kind(LineLayout::entry_kind(entry));
println!("{} {} {}", "Title:".bright_black(), kind, entry.title);
if let Some(dates) = entry.dates {
println!("DATE {}", dates.sorted());
} else {
println!("NO DATE");
}
let what = match entry.kind {
EntryKind::Task => "Task".to_string(),
EntryKind::TaskDone(date) => format!("Task, done {date}"),
EntryKind::TaskCanceled(date) => format!("Task, canceled {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);
for line in command.desc() {
println!("# {}", line);
let when = match entry.dates {
None => "no date".to_string(),
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)),
},
}
}
pub fn show(
files: &Files,
entries: &[Entry],
layout: &LineLayout,
numbers: &[usize],
) -> Result<()> {
if numbers.is_empty() {
#[derive(Debug, Clone, Copy)]
pub enum Ident {
Number(usize),
Date(NaiveDate),
}
pub fn show(files: &Files, entries: &[Entry], layout: &LineLayout, idents: &[Ident]) {
if idents.is_empty() {
// Nothing to do
return Ok(());
return;
}
let indices = numbers
.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) {
show_ident(files, entries, layout, idents[0]);
for &ident in idents.iter().skip(1) {
println!();
show_entry(files, &entries[index]);
println!();
println!();
show_ident(files, entries, layout, ident);
}
Ok(())
}

28
src/cli/util.rs Normal file
View file

@ -0,0 +1,28 @@
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 Normal file
View file

@ -0,0 +1,31 @@
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);
}

View file

@ -1,14 +1,14 @@
use chrono::NaiveDate;
use crate::files::arguments::{Range, RangeDate};
use crate::files::Files;
use crate::files::cli::{CliDate, CliDatum, CliRange};
use crate::files::{FileSource, Files};
use self::command::CommandState;
use self::command::{CommandState, EvalCommand};
pub use self::date::Dates;
use self::delta::Delta;
use self::entry::Entries;
pub use self::entry::{Entry, EntryKind, EntryMode};
pub use self::error::{Error, Result, SourceInfo};
pub use self::error::Error;
pub use self::range::DateRange;
mod command;
@ -20,22 +20,41 @@ mod range;
mod util;
impl Files {
pub fn eval(&self, mode: EntryMode, range: DateRange) -> Result<Vec<Entry>> {
pub fn eval(&self, mode: EntryMode, range: DateRange) -> Result<Vec<Entry>, Error<FileSource>> {
let mut entries = Entries::new(mode, range);
for command in self.commands() {
for entry in CommandState::new(command, range).eval()?.entries() {
let source = command.source;
if let Some(command) = EvalCommand::new(&command.value.value) {
for entry in CommandState::new(command, source, range).eval()?.entries() {
entries.add(entry);
}
}
}
Ok(entries.entries())
}
}
impl Range {
pub fn eval(&self, index: usize, today: NaiveDate) -> Result<DateRange> {
impl CliDate {
pub fn eval<S: Copy>(&self, index: S, today: NaiveDate) -> Result<NaiveDate, Error<S>> {
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 {
RangeDate::Date(d) => d,
RangeDate::Today => today,
CliDatum::Date(d) => d,
CliDatum::Today => today,
};
if let Some(delta) = &self.start_delta {
@ -46,8 +65,8 @@ impl Range {
let mut end = start;
match self.end {
Some(RangeDate::Date(d)) => end = d,
Some(RangeDate::Today) => end = today,
Some(CliDatum::Date(d)) => end = d,
Some(CliDatum::Today) => end = today,
None => {}
}

View file

@ -1,31 +1,100 @@
use std::collections::HashMap;
use chrono::NaiveDate;
use chrono::{Duration, NaiveDate};
use crate::files::commands::{BirthdaySpec, Command, Done, DoneDate, Note, Spec, Statement, Task};
use crate::files::primitives::Span;
use crate::files::SourcedCommand;
use crate::files::commands::{
self, BirthdaySpec, Command, Done, DoneDate, DoneKind, Note, Spec, Statement, Task,
};
use crate::files::primitives::{Span, Spanned, Time};
use crate::files::{FileSource, Source};
use super::date::Dates;
use super::{DateRange, Entry, EntryKind, Error, Result};
use super::delta::Delta;
use super::{DateRange, Entry, EntryKind, Error};
mod birthday;
mod date;
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) => &note.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> {
command: SourcedCommand<'a>,
command: EvalCommand<'a>,
source: Source,
range: DateRange,
from: Option<NaiveDate>,
until: Option<NaiveDate>,
remind: Option<Spanned<Delta>>,
dated: HashMap<NaiveDate, Entry>,
undated: Vec<Entry>,
}
impl<'a> CommandState<'a> {
pub fn new(command: SourcedCommand<'a>, mut range: DateRange) -> Self {
pub fn new(command: EvalCommand<'a>, source: Source, mut range: DateRange) -> Self {
// 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
// chains thereof) may move an initially out-of-range entry into range.
@ -33,26 +102,28 @@ impl<'a> CommandState<'a> {
// 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
// issue (if ever), it's probably fine.
for statement in command.command.statements() {
for statement in command.statements() {
if let Statement::Move { from, .. } = statement {
range = range.containing(*from)
}
}
Self {
range,
command,
source,
range,
from: None,
until: None,
remind: None,
dated: HashMap::new(),
undated: Vec::new(),
}
}
pub fn eval(mut self) -> Result<Self> {
match self.command.command {
Command::Task(task) => self.eval_task(task)?,
Command::Note(note) => self.eval_note(note)?,
pub fn eval(mut self) -> Result<Self, Error<FileSource>> {
match self.command {
EvalCommand::Task(task) => self.eval_task(task)?,
EvalCommand::Note(note) => self.eval_note(note)?,
}
Ok(self)
}
@ -66,30 +137,10 @@ impl<'a> CommandState<'a> {
// Helper functions
fn kind(&self) -> EntryKind {
match self.command.command {
Command::Task(_) => EntryKind::Task,
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,
fn range_with_remind(&self) -> DateRange {
match &self.remind {
None => self.range,
Some(delta) => self.range.expand_by(&delta.value),
}
}
@ -113,11 +164,42 @@ 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
/// overwrite existing entries if a root date is specified.
fn add(&mut self, kind: EntryKind, dates: Option<Dates>) {
let entry = Entry::new(self.command.source, kind, dates);
if let Some(dates) = dates {
fn add(&mut self, entry: Entry) {
if let Some(dates) = entry.dates {
let root = dates.root();
if let Some(from) = self.from {
if root < from {
@ -137,9 +219,8 @@ impl<'a> CommandState<'a> {
/// Add an entry, ignoring [`Self::from`] and [`Self::until`]. Always
/// overwrites existing entries if a root date is specified.
fn add_forced(&mut self, kind: EntryKind, dates: Option<Dates>) {
let entry = Entry::new(self.command.source, kind, dates);
if let Some(dates) = dates {
fn add_forced(&mut self, entry: Entry) {
if let Some(dates) = entry.dates {
self.dated.insert(dates.root(), entry);
} else {
self.undated.push(entry);
@ -154,47 +235,53 @@ impl<'a> CommandState<'a> {
.any(|s| matches!(s, Statement::Date(_) | Statement::BDate(_)))
}
fn eval_task(&mut self, task: &Task) -> Result<()> {
fn eval_task(&mut self, task: &Task) -> Result<(), Error<FileSource>> {
if Self::has_date_stmt(&task.statements) {
for statement in &task.statements {
self.eval_statement(statement)?;
}
} else if task.done.is_empty() {
self.add(self.kind(), None);
self.add(self.entry_with_remind(self.command.kind(), None)?);
}
for done in &task.done {
self.eval_done(done);
self.eval_done(done)?;
}
Ok(())
}
fn eval_note(&mut self, note: &Note) -> Result<()> {
fn eval_note(&mut self, note: &Note) -> Result<(), Error<FileSource>> {
if Self::has_date_stmt(&note.statements) {
for statement in &note.statements {
self.eval_statement(statement)?;
}
} else {
self.add(self.kind(), None);
self.add(self.entry_with_remind(self.command.kind(), None)?);
}
Ok(())
}
fn eval_statement(&mut self, statement: &Statement) -> Result<()> {
fn eval_statement(&mut self, statement: &Statement) -> Result<(), Error<FileSource>> {
match statement {
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::Until(date) => self.until = *date,
Statement::Except(date) => self.eval_except(*date),
Statement::Move { span, from, to } => self.eval_move(*span, *from, *to)?,
Statement::Move {
span,
from,
to,
to_time,
} => self.eval_move(*span, *from, *to, *to_time)?,
Statement::Remind(delta) => self.eval_remind(delta),
}
Ok(())
}
fn eval_date(&mut self, spec: &Spec) -> Result<()> {
fn eval_date(&mut self, spec: &Spec) -> Result<(), Error<FileSource>> {
match spec {
Spec::Date(spec) => self.eval_date_spec(spec.into()),
Spec::Weekday(spec) => self.eval_formula_spec(spec.into()),
@ -202,34 +289,69 @@ impl<'a> CommandState<'a> {
}
}
fn eval_bdate(&mut self, spec: &BirthdaySpec) {
self.eval_birthday_spec(spec);
fn eval_bdate(&mut self, spec: &BirthdaySpec) -> Result<(), Error<FileSource>> {
self.eval_birthday_spec(spec)
}
fn eval_except(&mut self, date: NaiveDate) {
// TODO Error if nothing is removed?
self.dated.remove(&date);
}
fn eval_move(&mut self, span: Span, from: NaiveDate, to: NaiveDate) -> Result<()> {
fn eval_move(
&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(dates) = entry.dates {
let delta = to - from;
entry.dates = Some(dates.move_by(delta));
let mut dates = entry.dates.expect("comes from self.dated");
// Determine delta
let mut delta = Duration::zero();
if let Some(to) = to {
delta = delta + (to - dates.root());
}
self.dated.insert(to, entry);
if let Some(to_time) = to_time {
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(())
} else {
Err(Error::MoveWithoutSource {
index: self.command.source.file(),
index: self.source.file(),
span,
})
}
}
fn eval_done(&mut self, done: &Done) {
self.add_forced(
EntryKind::TaskDone(done.done_at),
done.date.map(|date| date.into()),
);
fn eval_remind(&mut self, delta: &Option<Spanned<commands::Delta>>) {
if let Some(delta) = delta {
self.remind = Some(Spanned::new(delta.span, (&delta.value).into()));
} else {
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(())
}
}

View file

@ -1,16 +1,18 @@
use chrono::{Datelike, NaiveDate};
use crate::files::commands::BirthdaySpec;
use crate::files::FileSource;
use super::super::command::CommandState;
use super::super::date::Dates;
use super::super::error::Error;
use super::super::EntryKind;
impl<'a> CommandState<'a> {
pub fn eval_birthday_spec(&mut self, spec: &BirthdaySpec) {
let range = match self.limit_from_until(self.range) {
impl CommandState<'_> {
pub fn eval_birthday_spec(&mut self, spec: &BirthdaySpec) -> Result<(), Error<FileSource>> {
let range = match self.limit_from_until(self.range_with_remind()) {
Some(range) => range,
None => return,
None => return Ok(()),
};
for year in range.years() {
@ -26,15 +28,19 @@ impl<'a> CommandState<'a> {
let kind = EntryKind::Birthday(age);
if let Some(date) = spec.date.with_year(year) {
self.add(EntryKind::Birthday(age), Some(Dates::new(date, date)));
self.add(
self.entry_with_remind(EntryKind::Birthday(age), Some(Dates::new(date, date)))?,
);
} else {
assert_eq!(spec.date.month(), 2);
assert_eq!(spec.date.day(), 29);
let first = NaiveDate::from_ymd(year, 2, 28);
let second = NaiveDate::from_ymd(year, 3, 1);
self.add(kind, Some(Dates::new(first, second)));
let first = NaiveDate::from_ymd_opt(year, 2, 28).unwrap();
let second = NaiveDate::from_ymd_opt(year, 3, 1).unwrap();
self.add(self.entry_with_remind(kind, Some(Dates::new(first, second)))?);
}
}
Ok(())
}
}

View file

@ -1,12 +1,14 @@
use chrono::NaiveDate;
use crate::files::commands::{self, Command};
use crate::files::commands;
use crate::files::primitives::{Spanned, Time};
use crate::files::FileSource;
use super::super::command::CommandState;
use super::super::date::Dates;
use super::super::delta::{Delta, DeltaStep};
use super::super::{DateRange, Error, Result};
use super::super::{DateRange, Error};
use super::EvalCommand;
pub struct DateSpec {
pub start: NaiveDate,
@ -72,28 +74,30 @@ impl DateSpec {
/// `start` date itself should be skipped (and thus not result in an entry).
/// This may be necessary if [`Self::start_at_done`] is set.
fn start_and_range(&self, s: &CommandState<'_>) -> Option<(NaiveDate, bool, DateRange)> {
let (start, skip, range) = match s.command.command {
Command::Task(_) => {
let (start, skip, range) = match s.command {
EvalCommand::Task(_) => {
let (start, skip) = s
.command
.last_done_completion()
.map(|start| (start, true))
.filter(|_| self.start_at_done)
.unwrap_or((self.start, false));
let range_from = s
.command
.last_done_root()
.map(|date| date.succ())
.map(|date| date.succ_opt().unwrap())
.unwrap_or(self.start);
let range = s
.range
.range_with_remind()
.expand_by(&self.end_delta)
.move_by(&self.start_delta)
.with_from(range_from)?;
(start, skip, range)
}
Command::Note(_) => {
EvalCommand::Note(_) => {
let start = self.start;
let range = s
.range
.range_with_remind()
.expand_by(&self.end_delta)
.move_by(&self.start_delta);
(start, false, range)
@ -103,7 +107,11 @@ impl DateSpec {
Some((start, skip, range))
}
fn step(index: usize, from: NaiveDate, repeat: &Spanned<Delta>) -> Result<NaiveDate> {
fn step(
index: FileSource,
from: NaiveDate,
repeat: &Spanned<Delta>,
) -> Result<NaiveDate, Error<FileSource>> {
let to = repeat.value.apply_date(index, from)?;
if to > from {
Ok(to)
@ -117,7 +125,7 @@ impl DateSpec {
}
}
fn dates(&self, index: usize, start: NaiveDate) -> Result<Dates> {
fn dates(&self, index: FileSource, start: NaiveDate) -> Result<Dates, Error<FileSource>> {
let root = self.start_delta.apply_date(index, start)?;
Ok(if let Some(root_time) = self.start_time {
let (other, other_time) = self.end_delta.apply_date_time(index, root, root_time)?;
@ -129,9 +137,9 @@ impl DateSpec {
}
}
impl<'a> CommandState<'a> {
pub fn eval_date_spec(&mut self, spec: DateSpec) -> Result<()> {
let index = self.command.source.file();
impl CommandState<'_> {
pub fn eval_date_spec(&mut self, spec: DateSpec) -> Result<(), Error<FileSource>> {
let index = self.source.file();
if let Some(repeat) = &spec.repeat {
if let Some((mut start, skip, range)) = spec.start_and_range(self) {
if skip {
@ -142,13 +150,13 @@ impl<'a> CommandState<'a> {
}
while start <= range.until() {
let dates = spec.dates(index, start)?;
self.add(self.kind(), Some(dates));
self.add(self.entry_with_remind(self.command.kind(), Some(dates))?);
start = DateSpec::step(index, start, repeat)?;
}
}
} else {
let dates = spec.dates(index, spec.start)?;
self.add(self.kind(), Some(dates));
self.add(self.entry_with_remind(self.command.kind(), Some(dates))?);
}
Ok(())
}

File diff suppressed because it is too large Load diff

View file

@ -103,11 +103,24 @@ impl Dates {
}
pub fn move_by(&self, delta: Duration) -> Self {
Self {
root: self.root + delta,
other: self.other + delta,
times: self.times,
let mut result = *self;
// Modify dates
result.root += delta;
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
}
}
@ -136,32 +149,16 @@ impl From<DoneDate> for Dates {
impl From<Dates> for DoneDate {
fn from(dates: Dates) -> Self {
if dates.root == dates.other {
match dates.times {
Some(times) if times.root == times.other => Self::DateTime {
root: dates.root,
root_time: times.root,
let (root, other) = dates.dates();
match dates.times() {
Some((root_time, other_time)) => Self::DateTimeToDateTime {
root,
root_time,
other,
other_time,
},
Some(times) => Self::DateTimeToTime {
root: dates.root,
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,
},
}
None => Self::DateToDate { root, other },
}
.simplified()
}
}

View file

@ -5,7 +5,7 @@ use chrono::{Datelike, Duration, NaiveDate};
use crate::files::commands;
use crate::files::primitives::{Span, Spanned, Time, Weekday};
use super::{util, Error, Result};
use super::{util, Error};
/// Like [`commands::DeltaStep`] but includes a new constructor,
/// [`DeltaStep::Time`].
@ -43,84 +43,84 @@ impl DeltaStep {
/// A lower bound on days
fn lower_bound(&self) -> i32 {
match self {
DeltaStep::Year(n) => {
Self::Year(n) => {
if *n < 0 {
*n * 366
} else {
*n * 365
}
}
DeltaStep::Month(n) | DeltaStep::MonthReverse(n) => {
Self::Month(n) | Self::MonthReverse(n) => {
if *n < 0 {
*n * 31
} else {
*n * 28
}
}
DeltaStep::Day(n) => *n,
DeltaStep::Week(n) => *n * 7,
DeltaStep::Hour(n) => {
Self::Day(n) => *n,
Self::Week(n) => *n * 7,
Self::Hour(n) => {
if *n < 0 {
*n / 24 + (*n % 24).signum()
} else {
*n / 24
}
}
DeltaStep::Minute(n) => {
Self::Minute(n) => {
if *n < 0 {
*n / (24 * 60) + (*n % (24 * 60)).signum()
} else {
*n / (24 * 60)
}
}
DeltaStep::Weekday(n, _) => match n.cmp(&0) {
Self::Weekday(n, _) => match n.cmp(&0) {
Ordering::Less => *n * 7 - 1,
Ordering::Equal => 0,
Ordering::Greater => *n * 7 - 7,
},
DeltaStep::Time(_) => 0,
Self::Time(_) => 0,
}
}
/// An upper bound on days
fn upper_bound(&self) -> i32 {
match self {
DeltaStep::Year(n) => {
Self::Year(n) => {
if *n > 0 {
*n * 366
} else {
*n * 365
}
}
DeltaStep::Month(n) | DeltaStep::MonthReverse(n) => {
Self::Month(n) | Self::MonthReverse(n) => {
if *n > 0 {
*n * 31
} else {
*n * 28
}
}
DeltaStep::Day(n) => *n,
DeltaStep::Week(n) => *n * 7,
DeltaStep::Hour(n) => {
Self::Day(n) => *n,
Self::Week(n) => *n * 7,
Self::Hour(n) => {
if *n > 0 {
*n / 24 + (*n % 24).signum()
} else {
*n / 24
}
}
DeltaStep::Minute(n) => {
Self::Minute(n) => {
if *n > 0 {
*n / (24 * 60) + (*n % (24 * 60)).signum()
} else {
*n / (24 * 60)
}
}
DeltaStep::Weekday(n, _) => match n.cmp(&0) {
Self::Weekday(n, _) => match n.cmp(&0) {
Ordering::Less => *n * 7 - 7,
Ordering::Equal => 0,
Ordering::Greater => *n * 7 - 1,
},
DeltaStep::Time(_) => 1,
Self::Time(_) => 1,
}
}
}
@ -142,16 +142,16 @@ impl From<&commands::Delta> for Delta {
}
}
struct DeltaEval {
index: usize,
struct DeltaEval<I> {
index: I,
start: NaiveDate,
start_time: Option<Time>,
curr: NaiveDate,
curr_time: Option<Time>,
}
impl DeltaEval {
fn new(index: usize, start: NaiveDate, start_time: Option<Time>) -> Self {
impl<S: Copy> DeltaEval<S> {
fn new(index: S, start: NaiveDate, start_time: Option<Time>) -> Self {
Self {
index,
start,
@ -161,7 +161,7 @@ impl DeltaEval {
}
}
fn err_step(&self, span: Span) -> Error {
fn err_step(&self, span: Span) -> Error<S> {
Error::DeltaInvalidStep {
index: self.index,
span,
@ -172,7 +172,7 @@ impl DeltaEval {
}
}
fn err_time(&self, span: Span) -> Error {
fn err_time(&self, span: Span) -> Error<S> {
Error::DeltaNoTime {
index: self.index,
span,
@ -181,7 +181,7 @@ impl DeltaEval {
}
}
fn apply(&mut self, step: &Spanned<DeltaStep>) -> Result<()> {
fn apply(&mut self, step: &Spanned<DeltaStep>) -> Result<(), Error<S>> {
match step.value {
DeltaStep::Year(n) => self.step_year(step.span, n)?,
DeltaStep::Month(n) => self.step_month(step.span, n)?,
@ -196,7 +196,7 @@ impl DeltaEval {
Ok(())
}
fn step_year(&mut self, span: Span, amount: i32) -> Result<()> {
fn step_year(&mut self, span: Span, amount: i32) -> Result<(), Error<S>> {
let year = self.curr.year() + amount;
match NaiveDate::from_ymd_opt(year, self.curr.month(), self.curr.day()) {
None => Err(self.err_step(span)),
@ -207,7 +207,7 @@ impl DeltaEval {
}
}
fn step_month(&mut self, span: Span, amount: i32) -> Result<()> {
fn step_month(&mut self, span: Span, amount: i32) -> Result<(), Error<S>> {
let (year, month) = util::add_months(self.curr.year(), self.curr.month(), amount);
match NaiveDate::from_ymd_opt(year, month, self.curr.day()) {
None => Err(self.err_step(span)),
@ -218,7 +218,7 @@ impl DeltaEval {
}
}
fn step_month_reverse(&mut self, span: Span, amount: i32) -> Result<()> {
fn step_month_reverse(&mut self, span: Span, amount: i32) -> Result<(), Error<S>> {
// Calculate offset from the last day of the month
let month_length = util::month_length(self.curr.year(), self.curr.month()) as i32;
let end_offset = self.curr.day() as i32 - month_length;
@ -252,26 +252,26 @@ impl DeltaEval {
self.curr += delta;
}
fn step_hour(&mut self, span: Span, amount: i32) -> Result<()> {
fn step_hour(&mut self, span: Span, amount: i32) -> Result<(), Error<S>> {
let time = match self.curr_time {
Some(time) => time,
None => return Err(self.err_time(span)),
};
let (days, time) = time.add_hours(amount);
self.curr += Duration::days(days.into());
let (days, time) = time.add_hours(amount.into());
self.curr += Duration::days(days);
self.curr_time = Some(time);
Ok(())
}
fn step_minute(&mut self, span: Span, amount: i32) -> Result<()> {
fn step_minute(&mut self, span: Span, amount: i32) -> Result<(), Error<S>> {
let time = match self.curr_time {
Some(time) => time,
None => return Err(self.err_time(span)),
};
let (days, time) = time.add_minutes(amount);
self.curr += Duration::days(days.into());
let (days, time) = time.add_minutes(amount.into());
self.curr += Duration::days(days);
self.curr_time = Some(time);
Ok(())
}
@ -284,20 +284,21 @@ impl DeltaEval {
let days = rest + (amount - 1) * 7;
self.curr += Duration::days(days.into());
} else if amount < 0 {
let amount = -amount;
let rest: i32 = weekday.until(curr_wd).into();
let days = rest + (amount - 1) * 7;
self.curr -= Duration::days(days.into());
}
}
fn step_time(&mut self, span: Span, time: Time) -> Result<()> {
fn step_time(&mut self, span: Span, time: Time) -> Result<(), Error<S>> {
let curr_time = match self.curr_time {
Some(time) => time,
None => return Err(self.err_time(span)),
};
if time < curr_time {
self.curr = self.curr.succ();
self.curr = self.curr.succ_opt().unwrap();
}
self.curr_time = Some(time);
Ok(())
@ -313,11 +314,11 @@ impl Delta {
self.steps.iter().map(|step| step.value.upper_bound()).sum()
}
fn apply(
fn apply<S: Copy>(
&self,
index: usize,
index: S,
start: (NaiveDate, Option<Time>),
) -> Result<(NaiveDate, Option<Time>)> {
) -> Result<(NaiveDate, Option<Time>), Error<S>> {
let mut eval = DeltaEval::new(index, start.0, start.1);
for step in &self.steps {
eval.apply(step)?;
@ -325,16 +326,16 @@ impl Delta {
Ok((eval.curr, eval.curr_time))
}
pub fn apply_date(&self, index: usize, date: NaiveDate) -> Result<NaiveDate> {
pub fn apply_date<S: Copy>(&self, index: S, date: NaiveDate) -> Result<NaiveDate, Error<S>> {
Ok(self.apply(index, (date, None))?.0)
}
pub fn apply_date_time(
pub fn apply_date_time<S: Copy>(
&self,
index: usize,
index: S,
date: NaiveDate,
time: Time,
) -> Result<(NaiveDate, Time)> {
) -> Result<(NaiveDate, Time), Error<S>> {
let (date, time) = self.apply(index, (date, Some(time)))?;
Ok((date, time.expect("time was not preserved")))
}
@ -346,7 +347,7 @@ mod tests {
use crate::files::primitives::{Span, Spanned, Time};
use super::super::Result;
use super::super::Error;
use super::{Delta, DeltaStep as Step};
const SPAN: Span = Span { start: 12, end: 34 };
@ -357,21 +358,24 @@ mod tests {
}
}
fn apply_d(step: Step, from: (i32, u32, u32)) -> Result<NaiveDate> {
delta(step).apply_date(0, NaiveDate::from_ymd(from.0, from.1, from.2))
fn apply_d(step: Step, from: (i32, u32, u32)) -> Result<NaiveDate, Error<()>> {
delta(step).apply_date((), NaiveDate::from_ymd_opt(from.0, from.1, from.2).unwrap())
}
fn test_d(step: Step, from: (i32, u32, u32), expected: (i32, u32, u32)) {
assert_eq!(
apply_d(step, from).unwrap(),
NaiveDate::from_ymd(expected.0, expected.1, expected.2)
NaiveDate::from_ymd_opt(expected.0, expected.1, expected.2).unwrap()
);
}
fn apply_dt(step: Step, from: (i32, u32, u32, u32, u32)) -> Result<(NaiveDate, Time)> {
fn apply_dt(
step: Step,
from: (i32, u32, u32, u32, u32),
) -> Result<(NaiveDate, Time), Error<()>> {
delta(step).apply_date_time(
0,
NaiveDate::from_ymd(from.0, from.1, from.2),
(),
NaiveDate::from_ymd_opt(from.0, from.1, from.2).unwrap(),
Time::new(from.3, from.4),
)
}
@ -381,7 +385,7 @@ mod tests {
assert_eq!(
apply_dt(step, from).unwrap(),
(
NaiveDate::from_ymd(expected.0, expected.1, expected.2),
NaiveDate::from_ymd_opt(expected.0, expected.1, expected.2).unwrap(),
Time::new(expected.3, expected.4)
)
);
@ -563,6 +567,43 @@ mod tests {
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]
fn delta_time() {
test_dt(

View file

@ -9,24 +9,48 @@ use super::range::DateRange;
pub enum EntryKind {
Task,
TaskDone(NaiveDate),
TaskCanceled(NaiveDate),
Note,
Birthday(Option<i32>),
}
/// A single instance of a command.
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone)]
pub struct Entry {
pub source: Source,
pub kind: EntryKind,
pub title: String,
pub has_description: bool,
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 {
pub fn new(source: Source, kind: EntryKind, dates: Option<Dates>) -> Self {
pub fn new(
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 {
source,
kind,
title,
has_description,
dates,
remind,
}
}
@ -37,6 +61,7 @@ impl Entry {
/// Mode that determines how entries are filtered when they are added to
/// an [`Entries`].
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EntryMode {
/// The entry's root date must be contained in the range.
@ -93,8 +118,17 @@ impl Entries {
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
if let EntryKind::TaskDone(done) = entry.kind {
if let EntryKind::TaskDone(done) | EntryKind::TaskCanceled(done) = entry.kind {
if self.range.contains(done) {
return true;
}

View file

@ -1,15 +1,17 @@
use std::result;
use chrono::NaiveDate;
use codespan_reporting::diagnostic::{Diagnostic, Label};
use codespan_reporting::files::Files;
use codespan_reporting::term::Config;
use crate::error::Eprint;
use crate::files::primitives::{Span, Time};
#[derive(Debug, thiserror::Error)]
pub enum Error {
pub enum Error<S> {
/// A delta step resulted in an invalid date.
#[error("delta step resulted in invalid date")]
DeltaInvalidStep {
index: usize,
index: S,
span: Span,
start: NaiveDate,
start_time: Option<Time>,
@ -19,7 +21,7 @@ pub enum Error {
/// A time-based delta step was applied to a date without time.
#[error("time-based delta step applied to date without time")]
DeltaNoTime {
index: usize,
index: S,
span: Span,
start: NaiveDate,
prev: NaiveDate,
@ -29,7 +31,17 @@ pub enum Error {
/// in time (`to < from`).
#[error("repeat delta did not move forwards")]
RepeatDidNotMoveForwards {
index: usize,
index: S,
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,
from: NaiveDate,
to: NaiveDate,
@ -37,60 +49,48 @@ pub enum Error {
/// A `MOVE a TO b` statement was executed, but there was no entry at the
/// date `a`.
#[error("tried to move nonexisting entry")]
MoveWithoutSource { index: usize, span: Span },
MoveWithoutSource { index: S, 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.
#[error("tried to divide by zero")]
DivByZero {
index: usize,
index: S,
span: Span,
date: NaiveDate,
},
/// A modulo operation by zero has occurred.
#[error("tried to modulo by zero")]
ModByZero {
index: usize,
index: S,
span: Span,
date: NaiveDate,
},
/// Easter calculation failed.
#[error("easter calculation failed")]
Easter {
index: usize,
index: S,
span: Span,
date: NaiveDate,
msg: &'static str,
},
}
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);
}
impl<S> Error<S> {
fn fmt_date_time(date: NaiveDate, time: Option<Time>) -> String {
match time {
None => format!("{}", date),
Some(time) => format!("{} {}", date, time),
}
}
}
pub fn print<'a>(&self, sources: &[SourceInfo<'a>]) {
match self {
impl<'a, F: Files<'a>> Eprint<'a, F> for Error<F::FileId> {
#[allow(single_use_lifetimes)]
fn eprint<'f: 'a>(&self, files: &'f F, config: &Config) {
let diagnostic = match self {
Error::DeltaInvalidStep {
index,
span,
@ -99,78 +99,70 @@ impl Error {
prev,
prev_time,
} => {
let msg = format!(
"Delta step resulted in invalid date\
\nInitial start: {}\
\nPrevious step: {}",
Self::fmt_date_time(*start, *start_time),
Self::fmt_date_time(*prev, *prev_time),
);
Self::print_at(sources, index, span, msg);
let start_str = Self::fmt_date_time(*start, *start_time);
let prev_str = Self::fmt_date_time(*prev, *prev_time);
Diagnostic::error()
.with_message("Delta step resulted in invalid date")
.with_labels(vec![Label::primary(*index, span)])
.with_notes(vec![
format!("Date before applying delta: {start_str}"),
format!("Date before applying this step: {prev_str}"),
])
}
Error::DeltaNoTime {
index,
span,
start,
prev,
} => {
let msg = format!(
"Time-based delta step applied to date without time\
\nInitial start: {}\
\nPrevious step: {}",
start, prev
);
Self::print_at(sources, index, span, msg);
}
} => Diagnostic::error()
.with_message("Time-based delta step applied to date without time")
.with_labels(vec![Label::primary(*index, span)])
.with_notes(vec![
format!("Date before applying delta: {start}"),
format!("Date before applying this step: {prev}"),
]),
Error::RepeatDidNotMoveForwards {
index,
span,
from,
to,
} => {
let msg = format!(
"Repeat delta did not move forwards\
\nMoved from {} to {}",
from, to
);
Self::print_at(sources, index, span, msg);
}
Error::MoveWithoutSource { index, span } => {
let msg = "Tried to move nonexisting entry".to_string();
Self::print_at(sources, index, span, msg);
}
Error::DivByZero { index, span, date } => {
let msg = format!(
"Tried to divide by zero\
\nAt date: {}",
date
);
Self::print_at(sources, index, span, msg);
}
Error::ModByZero { index, span, date } => {
let msg = format!(
"Tried to modulo by zero\
\nAt date: {}",
date
);
Self::print_at(sources, index, span, msg);
}
} => Diagnostic::error()
.with_message("Repeat delta did not move forwards")
.with_labels(vec![Label::primary(*index, span)])
.with_notes(vec![format!("Moved from {from} to {to}")]),
Error::RemindDidNotMoveBackwards {
index,
span,
from,
to,
} => Diagnostic::error()
.with_message("Remind delta did not move backwards")
.with_labels(vec![Label::primary(*index, span)])
.with_notes(vec![format!("Moved from {from} to {to}")]),
Error::MoveWithoutSource { index, span } => Diagnostic::error()
.with_message("Tried to move nonexistent entry")
.with_labels(vec![Label::primary(*index, span)]),
Error::TimedMoveWithoutTime { index, span } => Diagnostic::error()
.with_message("Tried to move un-timed entry to new time")
.with_labels(vec![Label::primary(*index, span)]),
Error::DivByZero { index, span, date } => Diagnostic::error()
.with_message("Tried to divide by zero")
.with_labels(vec![Label::primary(*index, span)])
.with_notes(vec![format!("At date: {date}")]),
Error::ModByZero { index, span, date } => Diagnostic::error()
.with_message("Tried to modulo by zero")
.with_labels(vec![Label::primary(*index, span)])
.with_notes(vec![format!("At date: {date}")]),
Error::Easter {
index,
span,
date,
msg,
} => {
let msg = format!(
"Failed to calculate easter\
\nAt date: {}\
\nReason: {}",
date, msg
);
Self::print_at(sources, index, span, msg);
}
}
} => Diagnostic::error()
.with_message("Failed to calculate easter")
.with_labels(vec![Label::primary(*index, span)])
.with_notes(vec![format!("At date: {date}"), format!("Reason: {msg}")]),
};
Self::eprint_diagnostic(files, config, &diagnostic);
}
}
pub type Result<T> = result::Result<T, Error>;

View file

@ -37,6 +37,7 @@ impl DateRange {
/// Return a new range with its [`Self::until`] set to a new value.
///
/// Returns [`None`] if the new value is earlier than [`Self::from`].
#[allow(dead_code)]
pub fn with_until(&self, until: NaiveDate) -> Option<Self> {
if self.from <= until {
Some(Self::new(self.from, until))
@ -75,7 +76,7 @@ impl DateRange {
pub fn days(&self) -> impl Iterator<Item = NaiveDate> {
(self.from.num_days_from_ce()..=self.until.num_days_from_ce())
.map(NaiveDate::from_num_days_from_ce)
.map(|days| NaiveDate::from_num_days_from_ce_opt(days).unwrap())
}
pub fn years(&self) -> RangeInclusive<i32> {

View file

@ -9,13 +9,14 @@ pub fn is_iso_leap_year(year: i32) -> bool {
}
pub fn year_length(year: i32) -> u32 {
NaiveDate::from_ymd(year, 12, 31).ordinal()
NaiveDate::from_ymd_opt(year, 12, 31).unwrap().ordinal()
}
pub fn month_length(year: i32, month: u32) -> u32 {
NaiveDate::from_ymd_opt(year, month + 1, 1)
.unwrap_or_else(|| NaiveDate::from_ymd(year + 1, 1, 1))
.pred()
.unwrap_or_else(|| NaiveDate::from_ymd_opt(year + 1, 1, 1).unwrap())
.pred_opt()
.unwrap()
.day()
}

View file

@ -1,40 +1,48 @@
use std::collections::HashMap;
use std::fs;
use std::collections::hash_map::Entry;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::{fs, result};
use chrono::{DateTime, Utc};
use chrono::{DateTime, NaiveDate, Utc};
use codespan_reporting::files::SimpleFiles;
use tzfile::Tz;
use crate::eval::SourceInfo;
use self::commands::{Command, Done, File, Log};
pub use self::error::{Error, ParseError, Result};
use self::primitives::Spanned;
use self::commands::{Command, Done, File};
pub use self::error::{Error, Result};
pub mod arguments;
pub mod cli;
pub mod commands;
mod error;
mod format;
mod parse;
pub mod primitives;
// TODO Move file content from `File` to `LoadedFile`
#[derive(Debug)]
struct LoadedFile {
/// Canonical path for this file
path: PathBuf,
// User-readable path for this file
/// User-readable path for this file.
name: PathBuf,
/// Identifier for codespan-reporting.
cs_id: usize,
file: File,
/// Whether this file has been changed
/// Whether this file has been changed.
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 {
pub fn new(path: PathBuf, name: PathBuf, file: File) -> Self {
pub fn new(name: PathBuf, cs_id: usize, file: File) -> Self {
Self {
path,
name,
cs_id,
file,
dirty: false,
removed: HashSet::new(),
}
}
}
@ -45,43 +53,122 @@ pub struct Source {
command: usize,
}
// TODO Rename to `SourceFile`?
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FileSource(usize);
impl Source {
pub fn file(&self) -> usize {
self.file
pub fn new(file: usize, command: usize) -> Self {
Self { file, command }
}
pub fn file(&self) -> FileSource {
FileSource(self.file)
}
}
#[derive(Debug)]
pub struct SourcedCommand<'a> {
pub struct Sourced<'a, T> {
pub source: Source,
pub command: &'a Command,
pub value: &'a T,
}
impl<'a, T> Sourced<'a, T> {
fn new(source: Source, value: &'a T) -> Self {
Self { source, value }
}
}
#[derive(Debug)]
pub struct Files {
files: Vec<LoadedFile>,
timezone: Tz,
/// Codespan-reporting file database.
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 {
pub fn load(path: &Path) -> Result<Self> {
let mut paths = HashMap::new();
let mut files = vec![];
Self::load_file(&mut paths, &mut files, path)?;
let timezone = Self::determine_timezone(&files)?;
Ok(Self { files, timezone })
/* Loading */
pub fn new() -> Self {
Self {
files: vec![],
cs_files: SimpleFiles::new(),
timezone: None,
capture: None,
logs: HashMap::new(),
}
}
fn load_file(
paths: &mut HashMap<PathBuf, usize>,
files: &mut Vec<LoadedFile>,
name: &Path,
) -> Result<()> {
/// Load a file and all its includes.
///
/// # Warning
///
/// - This function must be called before all other functions.
/// - 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 {
path: name.to_path_buf(),
error: e,
})?;
if paths.contains_key(&path) {
if loaded.contains(&path) {
// We've already loaded this exact file.
return Ok(());
}
@ -90,90 +177,263 @@ impl Files {
file: path.clone(),
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.
let file = parse::parse(name, &content)?;
let includes = file.includes.clone();
let file = match parse::parse(name, &content) {
Ok(file) => file,
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,
});
}
};
paths.insert(path.clone(), files.len());
files.push(LoadedFile::new(path, name.to_owned(), file));
let includes = file
.commands
.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 {
// Since we've successfully opened the file, its name can't be the
// root directory or empty string and must thus have a parent.
let include_path = name.parent().unwrap().join(include);
Self::load_file(paths, files, &include_path)?;
// root directory or empty string and it must thus have a parent.
let include_path = name.parent().unwrap().join(include.value);
self.load_file(loaded, &include_path)?;
}
Ok(())
}
fn determine_timezone(files: &[LoadedFile]) -> Result<Tz> {
let mut found: Option<(PathBuf, String)> = None;
fn determine_timezone(&mut self) -> Result<()> {
assert_eq!(self.timezone, None);
for file in files {
if let Some(file_tz) = &file.file.timezone {
if let Some((found_name, found_tz)) = &found {
if found_tz != file_tz {
let mut found: Option<(Source, Spanned<String>)> = None;
for command in self.commands() {
if let Command::Timezone(tz) = &command.value.value {
if let Some((found_source, found_tz)) = &found {
if tz.value != found_tz.value {
return Err(Error::TzConflict {
file1: found_name.clone(),
tz1: found_tz.clone(),
file2: file.name.clone(),
tz2: file_tz.clone(),
file1: found_source.file(),
span1: found_tz.span,
tz1: found_tz.value.clone(),
file2: command.source.file(),
span2: tz.span,
tz2: tz.value.clone(),
});
}
} else {
found = Some((file.name.clone(), file_tz.clone()));
found = Some((command.source, tz.clone()));
}
}
}
Ok(if let Some((_, tz)) = found {
Tz::named(&tz).map_err(|e| Error::ResolveTz {
timezone: tz,
error: e,
let timezone = if let Some((source, tz)) = found {
Tz::named(&tz.value).map_err(|error| Error::ResolveTz {
file: source.file(),
span: tz.span,
tz: tz.value,
error,
})?
} else {
Tz::local().map_err(|e| Error::LocalTz { error: e })?
})
Tz::local().map_err(|error| Error::LocalTz { error })?
};
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<()> {
for file in &self.files {
if file.dirty {
println!("Saving file {:?}", file.path);
Self::save_file(&file.path, &file.file)?;
self.save_file(file)?;
}
}
Ok(())
}
fn save_file(path: &Path, file: &File) -> Result<()> {
fs::write(path, &format!("{}", file)).map_err(|e| Error::WriteFile {
file: path.to_path_buf(),
fn save_file(&self, file: &LoadedFile) -> Result<()> {
// TODO Sort commands within file
let previous = self
.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(())
}
/* 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) {
for file in self.files.iter_mut() {
file.dirty = true;
}
}
pub fn command(&self, source: Source) -> &Command {
&self.files[source.file].file.commands[source.command]
fn modify(&mut self, source: Source, edit: impl FnOnce(&mut Command)) {
let file = &mut self.files[source.file];
edit(&mut file.file.commands[source.command].value);
file.dirty = true;
}
pub fn sources(&self) -> Vec<SourceInfo<'_>> {
self.files
.iter()
.map(|f| SourceInfo {
name: Some(f.name.to_string_lossy().to_string()),
content: &f.file.contents,
})
.collect()
pub fn insert(&mut self, file: FileSource, command: Command) {
let file = &mut self.files[file.0];
file.file.commands.push(Spanned::dummy(command));
file.dirty = true;
}
fn remove(&mut self, source: Source) {
let file = &mut self.files[source.file];
file.removed.insert(source.command);
file.dirty = true;
}
/// Add a [`Done`] statement to the task identified by `source`.
@ -183,29 +443,41 @@ impl Files {
#[must_use]
pub fn add_done(&mut self, source: Source, done: Done) -> bool {
let file = &mut self.files[source.file];
match &mut file.file.commands[source.command] {
match &mut file.file.commands[source.command].value {
Command::Task(t) => t.done.push(done),
Command::Note(_) => return false,
_ => return false,
}
file.dirty = true;
true
}
pub fn commands(&self) -> Vec<SourcedCommand<'_>> {
let mut result = vec![];
for (file_index, file) in self.files.iter().enumerate() {
for (command_index, command) in file.file.commands.iter().enumerate() {
let source = Source {
file: file_index,
command: command_index,
};
result.push(SourcedCommand { source, command });
pub fn set_log(&mut self, date: NaiveDate, desc: Vec<String>) {
if let Some(source) = self.logs.get(&date).cloned() {
if desc.is_empty() {
self.remove(source);
} else {
self.modify(source, |command| match command {
Command::Log(log) => log.desc = desc,
_ => unreachable!(),
});
}
} 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
}
pub fn now(&self) -> DateTime<&Tz> {
Utc::now().with_timezone(&&self.timezone)
/* Errors */
fn cs_id(&self, file: FileSource) -> usize {
self.files[file.0].cs_id
}
}

View file

@ -1,96 +0,0 @@
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 Normal file
View file

@ -0,0 +1,194 @@
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)
}
}

View file

@ -27,27 +27,27 @@ pub enum DeltaStep {
impl DeltaStep {
pub fn amount(&self) -> i32 {
match self {
DeltaStep::Year(i) => *i,
DeltaStep::Month(i) => *i,
DeltaStep::MonthReverse(i) => *i,
DeltaStep::Day(i) => *i,
DeltaStep::Week(i) => *i,
DeltaStep::Hour(i) => *i,
DeltaStep::Minute(i) => *i,
DeltaStep::Weekday(i, _) => *i,
Self::Year(i) => *i,
Self::Month(i) => *i,
Self::MonthReverse(i) => *i,
Self::Day(i) => *i,
Self::Week(i) => *i,
Self::Hour(i) => *i,
Self::Minute(i) => *i,
Self::Weekday(i, _) => *i,
}
}
pub fn name(&self) -> &'static str {
match self {
DeltaStep::Year(_) => "y",
DeltaStep::Month(_) => "m",
DeltaStep::MonthReverse(_) => "M",
DeltaStep::Day(_) => "d",
DeltaStep::Week(_) => "w",
DeltaStep::Hour(_) => "h",
DeltaStep::Minute(_) => "min",
DeltaStep::Weekday(_, wd) => wd.name(),
Self::Year(_) => "y",
Self::Month(_) => "m",
Self::MonthReverse(_) => "M",
Self::Day(_) => "d",
Self::Week(_) => "w",
Self::Hour(_) => "h",
Self::Minute(_) => "min",
Self::Weekday(_, wd) => wd.name(),
}
}
}
@ -168,39 +168,39 @@ impl Var {
pub fn name(&self) -> &'static str {
match self {
// Constants
Var::True => "true",
Var::False => "false",
Var::Monday => "mon",
Var::Tuesday => "tue",
Var::Wednesday => "wed",
Var::Thursday => "thu",
Var::Friday => "fri",
Var::Saturday => "sat",
Var::Sunday => "sun",
Self::True => "true",
Self::False => "false",
Self::Monday => "mon",
Self::Tuesday => "tue",
Self::Wednesday => "wed",
Self::Thursday => "thu",
Self::Friday => "fri",
Self::Saturday => "sat",
Self::Sunday => "sun",
// Variables
Var::JulianDay => "j",
Var::Year => "y",
Var::YearLength => "yl",
Var::YearDay => "yd",
Var::YearDayReverse => "yD",
Var::YearWeek => "yw",
Var::YearWeekReverse => "yW",
Var::Month => "m",
Var::MonthLength => "ml",
Var::MonthWeek => "mw",
Var::MonthWeekReverse => "mW",
Var::Day => "d",
Var::DayReverse => "D",
Var::IsoYear => "iy",
Var::IsoYearLength => "iyl",
Var::IsoWeek => "iw",
Var::Weekday => "wd",
Var::Easter => "e",
Self::JulianDay => "j",
Self::Year => "y",
Self::YearLength => "yl",
Self::YearDay => "yd",
Self::YearDayReverse => "yD",
Self::YearWeek => "yw",
Self::YearWeekReverse => "yW",
Self::Month => "m",
Self::MonthLength => "ml",
Self::MonthWeek => "mw",
Self::MonthWeekReverse => "mW",
Self::Day => "d",
Self::DayReverse => "D",
Self::IsoYear => "iy",
Self::IsoYearLength => "iyl",
Self::IsoWeek => "iw",
Self::Weekday => "wd",
Self::Easter => "e",
// Variables with "boolean" values
Var::IsWeekday => "isWeekday",
Var::IsWeekend => "isWeekend",
Var::IsLeapYear => "isLeapYear",
Var::IsIsoLeapYear => "isIsoLeapYear",
Self::IsWeekday => "isWeekday",
Self::IsWeekend => "isWeekend",
Self::IsLeapYear => "isLeapYear",
Self::IsIsoLeapYear => "isIsoLeapYear",
}
}
}
@ -265,10 +265,13 @@ pub enum Statement {
Move {
span: Span,
from: NaiveDate,
to: NaiveDate,
to: Option<NaiveDate>,
to_time: Option<Spanned<Time>>,
},
Remind(Option<Spanned<Delta>>),
}
#[allow(clippy::enum_variant_names)]
#[derive(Debug, Clone, Copy)]
pub enum DoneDate {
Date {
@ -298,18 +301,54 @@ pub enum DoneDate {
impl DoneDate {
pub fn root(self) -> NaiveDate {
match self {
DoneDate::Date { root } => root,
DoneDate::DateTime { root, .. } => root,
DoneDate::DateToDate { root, .. } => root,
DoneDate::DateTimeToTime { root, .. } => root,
DoneDate::DateTimeToDateTime { root, .. } => root,
Self::Date { root } => root,
Self::DateTime { root, .. } => root,
Self::DateToDate { root, .. } => root,
Self::DateTimeToTime { root, .. } => root,
Self::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)]
pub struct Done {
pub kind: DoneKind,
/// The date of the task the DONE refers to.
pub date: Option<DoneDate>,
/// When the task was actually completed.
pub done_at: NaiveDate,
}
@ -329,38 +368,30 @@ pub struct Note {
}
#[derive(Debug)]
pub enum Command {
Task(Task),
Note(Note),
pub struct Log {
pub date: Spanned<NaiveDate>,
pub desc: Vec<String>,
}
impl Command {
pub fn title(&self) -> &str {
match self {
Self::Task(task) => &task.title,
Self::Note(note) => &note.title,
}
}
pub fn desc(&self) -> &[String] {
match self {
Self::Task(task) => &task.desc,
Self::Note(note) => &note.desc,
}
}
pub fn statements(&self) -> &[Statement] {
match self {
Self::Task(task) => &task.statements,
Self::Note(note) => &note.statements,
}
}
#[derive(Debug)]
pub enum Command {
Include(Spanned<String>),
Timezone(Spanned<String>),
Capture, // TODO Set capture file by template?
Task(Task),
Note(Note),
Log(Log),
}
#[derive(Debug)]
pub struct File {
pub contents: String,
pub includes: Vec<String>,
pub timezone: Option<String>,
pub commands: Vec<Command>,
pub commands: Vec<Spanned<Command>>,
}
impl File {
/// Create an empty dummy file. This file should only be used as a
/// placeholder value.
pub fn dummy() -> Self {
Self { commands: vec![] }
}
}

View file

@ -1,7 +1,88 @@
use std::path::PathBuf;
use std::{io, result};
use super::parse;
use chrono::NaiveDate;
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)]
pub enum Error {
@ -11,54 +92,132 @@ pub enum Error {
ReadFile { file: PathBuf, error: io::Error },
#[error("Could not write {file}: {error}")]
WriteFile { file: PathBuf, error: io::Error },
#[error("Could not resolve timezone {timezone}: {error}")]
ResolveTz { timezone: String, error: io::Error },
#[error("Could not resolve timezone {tz}: {error}")]
ResolveTz {
file: FileSource,
span: Span,
tz: String,
error: io::Error,
},
#[error("Could not determine local timezone: {error}")]
LocalTz { error: io::Error },
#[error("{0}")]
Parse(#[from] parse::Error),
#[error("{file1} has time zone {tz1} but {file2} has time zone {tz2}")]
#[error("{error}")]
Parse {
file: FileSource,
error: Box<parse::Error>,
},
#[error("Conflicting time zones {tz1} and {tz2}")]
TzConflict {
file1: PathBuf,
file1: FileSource,
span1: Span,
tz1: String,
file2: PathBuf,
file2: FileSource,
span2: Span,
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 Error {
pub fn print(&self) {
impl<'a> Eprint<'a, Files> for Error {
#[allow(single_use_lifetimes)]
fn eprint<'f: 'a>(&self, files: &'f Files, config: &Config) {
match self {
Error::ResolvePath { path, error } => {
eprintln!("Could not resolve path {:?}:", path);
eprintln!(" {}", error);
Self::ResolvePath { path, error } => {
eprintln!("Could not resolve path {path:?}:");
eprintln!(" {error}");
}
Error::ReadFile { file, error } => {
eprintln!("Could not read file {:?}:", file);
eprintln!(" {}", error);
Self::ReadFile { file, error } => {
eprintln!("Could not read file {file:?}:");
eprintln!(" {error}");
}
Error::WriteFile { file, error } => {
eprintln!("Could not write file {:?}:", file);
eprintln!(" {}", error);
Self::WriteFile { file, error } => {
eprintln!("Could not write file {file:?}:");
eprintln!(" {error}");
}
Error::ResolveTz { timezone, error } => {
eprintln!("Could not resolve time zone {}:", timezone);
eprintln!(" {}", error);
Self::ResolveTz {
file,
span,
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);
}
Error::LocalTz { error } => {
Self::LocalTz { error } => {
eprintln!("Could not determine local timezone:");
eprintln!(" {}", error);
eprintln!(" {error}");
}
Error::Parse(error) => eprintln!("{}", error),
Error::TzConflict {
Self::Parse { file, error } => {
ParseError::new(*file, error.clone()).eprint(files, config)
}
Self::TzConflict {
file1,
span1,
tz1,
file2,
span2,
tz2,
} => {
eprintln!("Time zone conflict:");
eprintln!(" {:?} has time zone {}", file1, tz1);
eprintln!(" {:?} has time zone {}", file2, tz2);
let diagnostic = Diagnostic::error()
.with_message(format!("Time zone conflict between {tz1} and {tz2}"))
.with_labels(vec![
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);
}
}
}

View file

@ -1,10 +1,13 @@
use std::collections::HashSet;
use std::fmt;
use chrono::Datelike;
use crate::files::commands::DoneKind;
use super::commands::{
BirthdaySpec, Command, DateSpec, Delta, DeltaStep, Done, DoneDate, Expr, File, FormulaSpec,
Note, Repeat, Spec, Statement, Task, Var, WeekdaySpec,
Log, Note, Repeat, Spec, Statement, Task, Var, WeekdaySpec,
};
use super::primitives::{Spanned, Time, Weekday};
@ -19,7 +22,7 @@ fn format_desc(f: &mut fmt::Formatter<'_>, desc: &[String]) -> fmt::Result {
if line.is_empty() {
writeln!(f, "#")?;
} else {
writeln!(f, "# {}", line)?;
writeln!(f, "# {line}")?;
}
}
Ok(())
@ -72,30 +75,30 @@ impl fmt::Display for DateSpec {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Start
write!(f, "{}", self.start)?;
for delta in &self.start_delta {
write!(f, " {}", delta)?;
if let Some(delta) = &self.start_delta {
write!(f, " {delta}")?;
}
for time in &self.start_time {
write!(f, " {}", time)?;
if let Some(time) = &self.start_time {
write!(f, " {time}")?;
}
// End
if self.end.is_some() || self.end_delta.is_some() || self.end_time.is_some() {
write!(f, " --")?;
if let Some(date) = self.end {
write!(f, " {}", date)?;
write!(f, " {date}")?;
}
if let Some(delta) = &self.end_delta {
write!(f, " {}", delta)?;
write!(f, " {delta}")?;
}
if let Some(time) = &self.end_time {
write!(f, " {}", time)?;
write!(f, " {time}")?;
}
}
// Repeat
if let Some(repeat) = &self.repeat {
write!(f, "; {}", repeat)?;
write!(f, "; {repeat}")?;
}
Ok(())
@ -106,21 +109,21 @@ impl fmt::Display for WeekdaySpec {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Start
write!(f, "{}", self.start)?;
for time in &self.start_time {
write!(f, " {}", time)?;
if let Some(time) = &self.start_time {
write!(f, " {time}")?;
}
// End
if self.end.is_some() || self.end_delta.is_some() || self.end_time.is_some() {
write!(f, " --")?;
if let Some(wd) = self.end {
write!(f, " {}", wd)?;
write!(f, " {wd}")?;
}
if let Some(delta) = &self.end_delta {
write!(f, " {}", delta)?;
write!(f, " {delta}")?;
}
if let Some(time) = &self.end_time {
write!(f, " {}", time)?;
write!(f, " {time}")?;
}
}
@ -137,25 +140,25 @@ impl fmt::Display for Var {
impl fmt::Display for Expr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Expr::Lit(i) => write!(f, "{}", i),
Expr::Var(v) => write!(f, "{}", v),
Expr::Paren(e) => write!(f, "({})", e),
Expr::Neg(e) => write!(f, "-{}", e),
Expr::Add(a, b) => write!(f, "{} + {}", a, b),
Expr::Sub(a, b) => write!(f, "{} - {}", a, b),
Expr::Mul(a, b) => write!(f, "{} * {}", a, b),
Expr::Div(a, b) => write!(f, "{} / {}", a, b),
Expr::Mod(a, b) => write!(f, "{} % {}", a, b),
Expr::Eq(a, b) => write!(f, "{} = {}", a, b),
Expr::Neq(a, b) => write!(f, "{} != {}", a, b),
Expr::Lt(a, b) => write!(f, "{} < {}", a, b),
Expr::Lte(a, b) => write!(f, "{} <= {}", a, b),
Expr::Gt(a, b) => write!(f, "{} > {}", a, b),
Expr::Gte(a, b) => write!(f, "{} >= {}", a, b),
Expr::Not(e) => write!(f, "!{}", e),
Expr::And(a, b) => write!(f, "{} & {}", a, b),
Expr::Or(a, b) => write!(f, "{} | {}", a, b),
Expr::Xor(a, b) => write!(f, "{} ^ {}", a, b),
Self::Lit(i) => write!(f, "{i}"),
Self::Var(v) => write!(f, "{v}"),
Self::Paren(e) => write!(f, "({e})"),
Self::Neg(e) => write!(f, "-{e}"),
Self::Add(a, b) => write!(f, "{a} + {b}"),
Self::Sub(a, b) => write!(f, "{a} - {b}"),
Self::Mul(a, b) => write!(f, "{a} * {b}"),
Self::Div(a, b) => write!(f, "{a} / {b}"),
Self::Mod(a, b) => write!(f, "{a} % {b}"),
Self::Eq(a, b) => write!(f, "{a} = {b}"),
Self::Neq(a, b) => write!(f, "{a} != {b}"),
Self::Lt(a, b) => write!(f, "{a} < {b}"),
Self::Lte(a, b) => write!(f, "{a} <= {b}"),
Self::Gt(a, b) => write!(f, "{a} > {b}"),
Self::Gte(a, b) => write!(f, "{a} >= {b}"),
Self::Not(e) => write!(f, "!{e}"),
Self::And(a, b) => write!(f, "{a} & {b}"),
Self::Or(a, b) => write!(f, "{a} | {b}"),
Self::Xor(a, b) => write!(f, "{a} ^ {b}"),
}
}
}
@ -164,25 +167,25 @@ impl fmt::Display for FormulaSpec {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Start
if let Some(expr) = &self.start {
write!(f, "({})", expr)?;
write!(f, "({expr})")?;
} else {
write!(f, "*")?;
}
for delta in &self.start_delta {
write!(f, " {}", delta)?;
if let Some(delta) = &self.start_delta {
write!(f, " {delta}")?;
}
for time in &self.start_time {
write!(f, " {}", time)?;
if let Some(time) = &self.start_time {
write!(f, " {time}")?;
}
// End
if self.end_delta.is_some() || self.end_time.is_some() {
write!(f, " --")?;
if let Some(delta) = &self.end_delta {
write!(f, " {}", delta)?;
write!(f, " {delta}")?;
}
if let Some(time) = &self.end_time {
write!(f, " {}", time)?;
write!(f, " {time}")?;
}
}
@ -193,9 +196,9 @@ impl fmt::Display for FormulaSpec {
impl fmt::Display for Spec {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Spec::Date(spec) => write!(f, "{}", spec),
Spec::Weekday(spec) => write!(f, "{}", spec),
Spec::Formula(spec) => write!(f, "{}", spec),
Self::Date(spec) => write!(f, "{spec}"),
Self::Weekday(spec) => write!(f, "{spec}"),
Self::Formula(spec) => write!(f, "{spec}"),
}
}
}
@ -213,45 +216,57 @@ impl fmt::Display for BirthdaySpec {
impl fmt::Display for Statement {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Statement::Date(spec) => writeln!(f, "DATE {}", spec),
Statement::BDate(spec) => writeln!(f, "BDATE {}", spec),
Statement::From(Some(date)) => writeln!(f, "FROM {}", date),
Statement::From(None) => writeln!(f, "FROM *"),
Statement::Until(Some(date)) => writeln!(f, "UNTIL {}", date),
Statement::Until(None) => writeln!(f, "UNTIL *"),
Statement::Except(date) => writeln!(f, "EXCEPT {}", date),
Statement::Move { from, to, .. } => writeln!(f, "MOVE {} TO {}", from, to),
Self::Date(spec) => writeln!(f, "DATE {spec}"),
Self::BDate(spec) => writeln!(f, "BDATE {spec}"),
Self::From(Some(date)) => writeln!(f, "FROM {date}"),
Self::From(None) => writeln!(f, "FROM *"),
Self::Until(Some(date)) => writeln!(f, "UNTIL {date}"),
Self::Until(None) => writeln!(f, "UNTIL *"),
Self::Except(date) => writeln!(f, "EXCEPT {date}"),
Self::Move {
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 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// TODO Remove redundant dates
match self {
DoneDate::Date { root } => write!(f, "{}", root),
DoneDate::DateTime { root, root_time } => write!(f, "{} {}", root, root_time),
DoneDate::DateToDate { root, other } => write!(f, "{} -- {}", root, other),
DoneDate::DateTimeToTime {
match self.simplified() {
Self::Date { root } => write!(f, "{root}"),
Self::DateTime { root, root_time } => write!(f, "{root} {root_time}"),
Self::DateToDate { root, other } => write!(f, "{root} -- {other}"),
Self::DateTimeToTime {
root,
root_time,
other_time,
} => write!(f, "{} {} -- {}", root, root_time, other_time),
DoneDate::DateTimeToDateTime {
} => write!(f, "{root} {root_time} -- {other_time}"),
Self::DateTimeToDateTime {
root,
root_time,
other,
other_time,
} => write!(f, "{} {} -- {} {}", root, root_time, other, other_time),
} => write!(f, "{root} {root_time} -- {other} {other_time}"),
}
}
}
impl fmt::Display for Done {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "DONE [{}]", self.done_at)?;
let kind = match self.kind {
DoneKind::Done => "DONE",
DoneKind::Canceled => "CANCELED",
};
write!(f, "{kind} [{}]", self.done_at)?;
if let Some(date) = &self.date {
write!(f, " {}", date)?;
write!(f, " {date}")?;
}
writeln!(f)
}
@ -261,10 +276,10 @@ impl fmt::Display for Task {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "TASK {}", self.title)?;
for statement in &self.statements {
write!(f, "{}", statement)?;
write!(f, "{statement}")?;
}
for done in &self.done {
write!(f, "{}", done)?;
write!(f, "{done}")?;
}
format_desc(f, &self.desc)?;
Ok(())
@ -275,46 +290,97 @@ impl fmt::Display for Note {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "NOTE {}", self.title)?;
for statement in &self.statements {
write!(f, "{}", statement)?;
write!(f, "{statement}")?;
}
format_desc(f, &self.desc)?;
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 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Command::Task(task) => write!(f, "{}", task),
Command::Note(note) => write!(f, "{}", note),
Self::Include(name) => writeln!(f, "INCLUDE {name}"),
Self::Timezone(name) => writeln!(f, "TIMEZONE {name}"),
Self::Capture => writeln!(f, "CAPTURE"),
Self::Task(task) => write!(f, "{task}"),
Self::Note(note) => write!(f, "{note}"),
Self::Log(log) => write!(f, "{log}"),
}
}
}
impl fmt::Display for File {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut empty = true;
for include in &self.includes {
writeln!(f, "INCLUDE {}", include)?;
empty = false;
impl File {
fn sort(commands: &mut [&Command]) {
// Order of commands in a file:
// 1. Imports, sorted alphabetically
// 2. Time zone(s)
// 3. Captures
// 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
// their order.
// 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,
});
}
if let Some(tz) = &self.timezone {
if !empty {
writeln!(f)?;
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;
}
for command in &self.commands {
if !empty {
writeln!(f)?;
}
write!(f, "{}", command)?;
empty = false;
}
Ok(())
result
}
}

View file

@ -5,6 +5,7 @@ rest_any = { (!eol ~ ANY)* }
include = { "INCLUDE" ~ WHITESPACE ~ rest_some ~ eol }
timezone = { "TIMEZONE" ~ WHITESPACE ~ rest_some ~ eol }
capture = { "CAPTURE" ~ eol }
number = @{ ASCII_DIGIT{1,9} } // Fits into an i32
@ -58,34 +59,33 @@ variable = {
| "e"
}
unop_neg = { "-" }
unop_not = { "!" }
unop = _{ unop_neg | unop_not }
prefix_neg = { "-" }
prefix_not = { "!" }
prefix = _{ prefix_neg | prefix_not }
op_add = { "+" }
op_sub = { "-" }
op_mul = { "*" }
op_div = { "/" }
op_mod = { "%" }
op_eq = { "=" }
op_neq = { "!=" }
op_lt = { "<" }
op_lte = { "<=" }
op_gt = { ">" }
op_gte = { ">=" }
op_and = { "&" }
op_or = { "|" }
op_xor = { "^" }
op = _{
op_add | op_sub | op_mul | op_div | op_mod
| op_eq | op_neq | op_lt | op_lte | op_gt | op_gte
| op_and | op_or | op_xor
infix_add = { "+" }
infix_sub = { "-" }
infix_mul = { "*" }
infix_div = { "/" }
infix_mod = { "%" }
infix_eq = { "=" }
infix_neq = { "!=" }
infix_lt = { "<" }
infix_lte = { "<=" }
infix_gt = { ">" }
infix_gte = { ">=" }
infix_and = { "&" }
infix_or = { "|" }
infix_xor = { "^" }
infix = _{
infix_add | infix_sub | infix_mul | infix_div | infix_mod
| infix_eq | infix_neq | infix_lt | infix_lte | infix_gt | infix_gte
| infix_and | infix_or | infix_xor
}
paren_expr = { "(" ~ expr ~ ")" }
unop_expr = { unop ~ expr }
term = { number | boolean | variable | paren_expr | unop_expr }
expr = { term ~ (op ~ term)* }
term = { number | boolean | variable | paren_expr }
expr = { prefix* ~ term ~ (infix ~ prefix* ~ term)* }
date_fixed_start = { datum ~ delta? ~ time? }
date_fixed_end = { datum ~ delta? ~ time? | delta ~ time? | time }
@ -106,9 +106,10 @@ stmt_bdate = !{ "BDATE" ~ bdatum ~ eol }
stmt_from = !{ "FROM" ~ (datum | "*") ~ eol }
stmt_until = !{ "UNTIL" ~ (datum | "*") ~ eol }
stmt_except = !{ "EXCEPT" ~ datum ~ eol }
stmt_move = !{ "MOVE" ~ datum ~ "TO" ~ datum ~ eol }
stmt_move = !{ "MOVE" ~ datum ~ "TO" ~ (datum ~ time? | time) ~ eol }
stmt_remind = !{ "REMIND" ~ (delta | "*") ~ eol }
statements = { (stmt_date | stmt_bdate | stmt_from | stmt_until | stmt_except | stmt_move)* }
statements = { (stmt_date | stmt_bdate | stmt_from | stmt_until | stmt_except | stmt_move | stmt_remind)* }
donedate = {
datum ~ time ~ "--" ~ datum ~ time
@ -117,7 +118,8 @@ donedate = {
| datum ~ "--" ~ datum
| datum
}
done = !{ "DONE" ~ "[" ~ datum ~ "]" ~ donedate? ~ eol }
done_kind = { "DONE" | "CANCELED" }
done = !{ done_kind ~ "[" ~ datum ~ "]" ~ donedate? ~ eol }
dones = { done* }
desc_line = { "#" ~ (" " ~ rest_any)? ~ eol }
@ -138,12 +140,24 @@ note = {
~ description
}
log_head = !{ "LOG" ~ datum ~ eol }
log = { log_head ~ description }
empty_line = _{ WHITESPACE* ~ NEWLINE }
command = { include | timezone | task | note }
command = { include | timezone | capture | task | note | log }
file = ${ SOI ~ (empty_line* ~ command)* ~ empty_line* ~ WHITESPACE* ~ EOI }
today = { "today" }
range_start = { (datum | today) ~ delta? }
range_end = { (datum | today) ~ delta? | delta }
range = { SOI ~ range_start ~ ("--" ~ range_end)? ~ EOI}
today = { "today" | "t" }
cli_datum = { datum | today }
cli_date = { cli_datum ~ delta? }
cli_ident = { cli_date | number }
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 }

View file

@ -4,12 +4,12 @@ use std::result;
use chrono::NaiveDate;
use pest::error::ErrorVariant;
use pest::iterators::Pair;
use pest::prec_climber::{Assoc, Operator, PrecClimber};
use pest::pratt_parser::{Assoc, Op, PrattParser};
use pest::{Parser, Span};
use super::commands::{
BirthdaySpec, Command, DateSpec, Delta, DeltaStep, Done, DoneDate, Expr, File, FormulaSpec,
Note, Repeat, Spec, Statement, Task, Var, WeekdaySpec,
BirthdaySpec, Command, DateSpec, Delta, DeltaStep, Done, DoneDate, DoneKind, Expr, File,
FormulaSpec, Log, Note, Repeat, Spec, Statement, Task, Var, WeekdaySpec,
};
use super::primitives::{Spanned, Time, Weekday};
@ -18,7 +18,7 @@ use super::primitives::{Spanned, Time, Weekday};
pub struct TodayfileParser;
pub type Error = pest::error::Error<Rule>;
pub type Result<T> = result::Result<T, Error>;
pub type Result<T> = result::Result<T, Box<Error>>;
fn error<S: Into<String>>(span: Span<'_>, message: S) -> Error {
Error::new_from_span(
@ -30,20 +30,26 @@ fn error<S: Into<String>>(span: Span<'_>, message: S) -> Error {
}
fn fail<S: Into<String>, T>(span: Span<'_>, message: S) -> Result<T> {
Err(error(span, message))
Err(Box::new(error(span, message)))
}
fn parse_include(p: Pair<'_, Rule>) -> String {
fn parse_include(p: Pair<'_, Rule>) -> Spanned<String> {
assert_eq!(p.as_rule(), Rule::include);
p.into_inner().next().unwrap().as_str().to_string()
let p = p.into_inner().next().unwrap();
let span = (&p.as_span()).into();
let name = p.as_str().to_string();
Spanned::new(span, name)
}
fn parse_timezone(p: Pair<'_, Rule>) -> String {
fn parse_timezone(p: Pair<'_, Rule>) -> Spanned<String> {
assert_eq!(p.as_rule(), Rule::timezone);
p.into_inner().next().unwrap().as_str().trim().to_string()
let p = p.into_inner().next().unwrap();
let span = (&p.as_span()).into();
let name = p.as_str().to_string();
Spanned::new(span, name)
}
fn parse_number(p: Pair<'_, Rule>) -> i32 {
pub fn parse_number(p: Pair<'_, Rule>) -> i32 {
assert_eq!(p.as_rule(), Rule::number);
p.as_str().parse().unwrap()
}
@ -287,7 +293,7 @@ fn parse_date_fixed(p: Pair<'_, Rule>) -> Result<DateSpec> {
assert_eq!(p.as_rule(), Rule::date_fixed);
let mut spec = DateSpec {
start: NaiveDate::from_ymd(0, 1, 1),
start: NaiveDate::from_ymd_opt(0, 1, 1).unwrap(),
start_delta: None,
start_time: None,
end: None,
@ -353,24 +359,6 @@ 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> {
assert_eq!(p.as_rule(), Rule::paren_expr);
let span = (&p.as_span()).into();
@ -386,34 +374,43 @@ fn parse_term(p: Pair<'_, Rule>) -> Spanned<Expr> {
Rule::number => Spanned::new(span, Expr::Lit(parse_number(p).into())),
Rule::boolean => Spanned::new(span, Expr::Var(parse_boolean(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),
_ => unreachable!(),
}
}
fn parse_op(l: Spanned<Expr>, p: Pair<'_, Rule>, r: Spanned<Expr>) -> Spanned<Expr> {
fn parse_prefix(p: Pair<'_, Rule>, s: 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 expr = match p.as_rule() {
// Integer-y operations
Rule::op_add => Expr::Add(Box::new(l), Box::new(r)),
Rule::op_sub => Expr::Sub(Box::new(l), Box::new(r)),
Rule::op_mul => Expr::Mul(Box::new(l), Box::new(r)),
Rule::op_div => Expr::Div(Box::new(l), Box::new(r)),
Rule::op_mod => Expr::Mod(Box::new(l), Box::new(r)),
Rule::infix_add => Expr::Add(Box::new(l), Box::new(r)),
Rule::infix_sub => Expr::Sub(Box::new(l), Box::new(r)),
Rule::infix_mul => Expr::Mul(Box::new(l), Box::new(r)),
Rule::infix_div => Expr::Div(Box::new(l), Box::new(r)),
Rule::infix_mod => Expr::Mod(Box::new(l), Box::new(r)),
// Comparisons
Rule::op_eq => Expr::Eq(Box::new(l), Box::new(r)),
Rule::op_neq => Expr::Neq(Box::new(l), Box::new(r)),
Rule::op_lt => Expr::Lt(Box::new(l), Box::new(r)),
Rule::op_lte => Expr::Lte(Box::new(l), Box::new(r)),
Rule::op_gt => Expr::Gt(Box::new(l), Box::new(r)),
Rule::op_gte => Expr::Gte(Box::new(l), Box::new(r)),
Rule::infix_eq => Expr::Eq(Box::new(l), Box::new(r)),
Rule::infix_neq => Expr::Neq(Box::new(l), Box::new(r)),
Rule::infix_lt => Expr::Lt(Box::new(l), Box::new(r)),
Rule::infix_lte => Expr::Lte(Box::new(l), Box::new(r)),
Rule::infix_gt => Expr::Gt(Box::new(l), Box::new(r)),
Rule::infix_gte => Expr::Gte(Box::new(l), Box::new(r)),
// Boolean-y operations
Rule::op_and => Expr::And(Box::new(l), Box::new(r)),
Rule::op_or => Expr::Or(Box::new(l), Box::new(r)),
Rule::op_xor => Expr::Xor(Box::new(l), Box::new(r)),
Rule::infix_and => Expr::And(Box::new(l), Box::new(r)),
Rule::infix_or => Expr::Or(Box::new(l), Box::new(r)),
Rule::infix_xor => Expr::Xor(Box::new(l), Box::new(r)),
_ => unreachable!(),
};
@ -423,21 +420,23 @@ fn parse_op(l: Spanned<Expr>, p: Pair<'_, Rule>, r: Spanned<Expr>) -> Spanned<Ex
fn parse_expr(p: Pair<'_, Rule>) -> Spanned<Expr> {
assert_eq!(p.as_rule(), Rule::expr);
fn op(rule: Rule) -> Operator<Rule> {
Operator::new(rule, Assoc::Left)
}
let climber = PrecClimber::new(vec![
// Precedence from low to high
op(Rule::op_or) | op(Rule::op_xor),
op(Rule::op_and),
op(Rule::op_eq) | op(Rule::op_neq),
op(Rule::op_lt) | op(Rule::op_lte) | op(Rule::op_gt) | op(Rule::op_gte),
op(Rule::op_mul) | op(Rule::op_div) | op(Rule::op_mod),
op(Rule::op_add) | op(Rule::op_sub),
]);
climber.climb(p.into_inner(), parse_term, parse_op)
PrattParser::new()
.op(Op::infix(Rule::infix_or, Assoc::Left) | Op::infix(Rule::infix_xor, 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)
| Op::infix(Rule::infix_lte, Assoc::Left)
| Op::infix(Rule::infix_gt, Assoc::Left)
| Op::infix(Rule::infix_gte, Assoc::Left))
.op(Op::infix(Rule::infix_mul, Assoc::Left)
| Op::infix(Rule::infix_div, Assoc::Left)
| Op::infix(Rule::infix_mod, Assoc::Left))
.op(Op::infix(Rule::infix_add, Assoc::Left) | Op::infix(Rule::infix_sub, Assoc::Left))
.op(Op::prefix(Rule::prefix_neg) | Op::prefix(Rule::prefix_not))
.map_primary(parse_term)
.map_prefix(parse_prefix)
.map_infix(parse_infix)
.parse(p.into_inner())
}
fn parse_date_expr_start(p: Pair<'_, Rule>, spec: &mut FormulaSpec) -> Result<()> {
@ -618,9 +617,34 @@ fn parse_stmt_move(p: Pair<'_, Rule>) -> Result<Statement> {
let span = (&p.as_span()).into();
let mut p = p.into_inner();
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);
Ok(Statement::Move { span, from, to })
Ok(Statement::Remind(delta))
}
fn parse_statements(p: Pair<'_, Rule>, task: bool) -> Result<Vec<Statement>> {
@ -635,6 +659,7 @@ fn parse_statements(p: Pair<'_, Rule>, task: bool) -> Result<Vec<Statement>> {
Rule::stmt_until => parse_stmt_until(p)?,
Rule::stmt_except => parse_stmt_except(p)?,
Rule::stmt_move => parse_stmt_move(p)?,
Rule::stmt_remind => parse_stmt_remind(p)?,
_ => unreachable!(),
});
}
@ -678,10 +703,20 @@ 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> {
assert_eq!(p.as_rule(), Rule::done);
let mut p = p.into_inner();
let kind = parse_done_kind(p.next().unwrap());
let done_at = parse_datum(p.next().unwrap())?.value;
let date = if let Some(p) = p.next() {
Some(parse_donedate(p)?)
@ -691,7 +726,11 @@ fn parse_done(p: Pair<'_, Rule>) -> Result<Done> {
assert_eq!(p.next(), None);
Ok(Done { date, done_at })
Ok(Done {
kind,
date,
done_at,
})
}
fn parse_dones(p: Pair<'_, Rule>) -> Result<Vec<Done>> {
@ -755,44 +794,54 @@ fn parse_note(p: Pair<'_, Rule>) -> Result<Note> {
})
}
fn parse_command(p: Pair<'_, Rule>, file: &mut File) -> Result<()> {
fn parse_log_head(p: Pair<'_, Rule>) -> Result<Spanned<NaiveDate>> {
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);
let p = p.into_inner().next().unwrap();
match p.as_rule() {
Rule::include => file.includes.push(parse_include(p)),
Rule::timezone => match file.timezone {
None => file.timezone = Some(parse_timezone(p)),
Some(_) => fail(p.as_span(), "cannot set timezone multiple times")?,
},
Rule::task => file.commands.push(Command::Task(parse_task(p)?)),
Rule::note => file.commands.push(Command::Note(parse_note(p)?)),
let span = (&p.as_span()).into();
let command = match p.as_rule() {
Rule::include => Command::Include(parse_include(p)),
Rule::timezone => Command::Timezone(parse_timezone(p)),
Rule::capture => Command::Capture,
Rule::task => Command::Task(parse_task(p)?),
Rule::note => Command::Note(parse_note(p)?),
Rule::log => Command::Log(parse_log(p)?),
_ => unreachable!(),
}
Ok(())
};
Ok(Spanned::new(span, command))
}
pub fn parse_file(p: Pair<'_, Rule>, contents: String) -> Result<File> {
pub fn parse_file(p: Pair<'_, Rule>) -> Result<File> {
assert_eq!(p.as_rule(), Rule::file);
let mut file = File {
contents,
includes: vec![],
timezone: None,
commands: vec![],
};
let mut commands = vec![];
for p in p.into_inner() {
// For some reason, the EOI in `file` always gets captured
if p.as_rule() == Rule::EOI {
break;
}
parse_command(p, &mut file)?;
commands.push(parse_command(p)?);
}
Ok(file)
Ok(File { commands })
}
pub fn parse(path: &Path, input: &str) -> Result<File> {
@ -802,5 +851,5 @@ pub fn parse(path: &Path, input: &str) -> Result<File> {
let file_pair = pairs.next().unwrap();
assert_eq!(pairs.next(), None);
parse_file(file_pair, input.to_string()).map_err(|e| e.with_path(&pathstr))
parse_file(file_pair).map_err(|e| Box::new(e.with_path(&pathstr)))
}

View file

@ -1,5 +1,5 @@
use std::cmp::{self, Ordering};
use std::fmt;
use std::{fmt, ops};
use chrono::{NaiveTime, Timelike};
@ -18,6 +18,12 @@ 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 {
pub fn join(self, other: Self) -> Self {
Self {
@ -25,6 +31,10 @@ impl Span {
end: cmp::max(self.end, other.end),
}
}
fn dummy() -> Self {
Self { start: 0, end: 0 }
}
}
#[derive(Clone, Copy)]
@ -43,6 +53,10 @@ impl<T> Spanned<T> {
pub fn new(span: Span, value: T) -> Self {
Self { span, value }
}
pub fn dummy(value: T) -> Self {
Self::new(Span::dummy(), value)
}
}
// I don't know how one would write this. It works as a polymorphic standalone
@ -98,10 +112,15 @@ impl Time {
true
}
pub fn add_minutes(&self, amount: i32) -> (i32, Self) {
/// How many minutes into the day this time is.
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) {
Ordering::Less => {
let mut mins = (self.hour as i32) * 60 + (self.min as i32) + amount;
let mut mins = self.minutes() + amount;
let days = mins.div_euclid(60 * 24);
mins = mins.rem_euclid(60 * 24);
@ -111,7 +130,7 @@ impl Time {
(days, Self::new(hour, min))
}
Ordering::Greater => {
let mut mins = (self.hour as i32) * 60 + (self.min as i32) + amount;
let mut mins = self.minutes() + amount;
let mut days = mins.div_euclid(60 * 24);
mins = mins.rem_euclid(60 * 24);
@ -130,9 +149,18 @@ impl Time {
}
}
pub fn add_hours(&self, amount: i32) -> (i32, Self) {
pub fn add_hours(&self, amount: i64) -> (i64, Self) {
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)]
@ -178,13 +206,13 @@ impl Weekday {
/// `Saturday`, `Sunday`).
pub fn full_name(self) -> &'static str {
match self {
Weekday::Monday => "Monday",
Weekday::Tuesday => "Tuesday",
Weekday::Wednesday => "Wednesday",
Weekday::Thursday => "Thursday",
Weekday::Friday => "Friday",
Weekday::Saturday => "Saturday",
Weekday::Sunday => "Sunday",
Self::Monday => "Monday",
Self::Tuesday => "Tuesday",
Self::Wednesday => "Wednesday",
Self::Thursday => "Thursday",
Self::Friday => "Friday",
Self::Saturday => "Saturday",
Self::Sunday => "Sunday",
}
}

View file

@ -1,9 +1,5 @@
#![warn(future_incompatible)]
#![warn(rust_2018_idioms)]
#![warn(clippy::all)]
#![warn(clippy::use_self)]
mod cli;
mod error;
mod eval;
mod files;