cassiopeia: update CLI handling and add new commands

wip/yesman
Katharina Fey 3 years ago
parent 581bf56eef
commit 4fd6c87c43
  1. 1
      apps/cassiopeia/.envrc
  2. 9
      apps/cassiopeia/shell.nix
  3. 77
      apps/cassiopeia/src/bin/cass.rs
  4. 21
      apps/cassiopeia/src/error.rs
  5. 59
      apps/cassiopeia/src/format/mod.rs
  6. 11
      apps/cassiopeia/src/format/parser.rs
  7. 34
      apps/cassiopeia/src/lib.rs
  8. 2
      apps/cassiopeia/src/time.rs

@ -1 +0,0 @@
eval "$(lorri direnv)"

@ -1,8 +1 @@
with import <nixpkgs> {}; import <nom/rust.nix>
stdenv.mkDerivation {
name = "cassiopeia";
buildInputs = with pkgs; [
rustracer rustup clangStdenv
];
}

@ -1,4 +1,4 @@
use cassiopeia::{meta, Cassiopeia}; use cassiopeia::{error::ParseResult, meta, Cassiopeia};
use clap::{App, Arg, SubCommand}; use clap::{App, Arg, SubCommand};
fn main() { fn main() {
@ -69,64 +69,49 @@ If you want to report a bug, please do so on my mailing list: lists.sr.ht/~space
let cass_file = cli.value_of(meta::ARG_FILE).unwrap(); let cass_file = cli.value_of(meta::ARG_FILE).unwrap();
let mut cass = match Cassiopeia::load(cass_file) { let mut cass = match Cassiopeia::load(cass_file) {
Some(cf) => cf, Ok(cf) => cf,
None => { Err(e) => {
eprintln!( eprintln!("{}", e);
"Invalid CASS file '{}'; file not found, or unparsable.", std::process::exit(1);
cass_file
);
std::process::exit(2);
} }
}; };
// Parse the matches generated by clap // Parse the matches generated by clap
match cli.subcommand() { match cli.subcommand() {
("start", Some(ops)) => { ("start", Some(ops)) => {
// This parameter turns rounding OFF
let round = ops.is_present(meta::ARG_ROUNDING); let round = ops.is_present(meta::ARG_ROUNDING);
match cass.start(!round) { run_command(|| cass.start(!round));
Some(()) => println!("Started session!"),
None => {
eprintln!("Failed to start session...");
std::process::exit(1);
}
}
} }
("stop", Some(ops)) => { ("stop", Some(ops)) => {
// This parameter turns rounding OFF
let round = ops.is_present(meta::ARG_ROUNDING); let round = ops.is_present(meta::ARG_ROUNDING);
match cass.stop(!round) { run_command(|| cass.stop(!round));
Some(()) => println!("Stopped session!"),
None => {
eprintln!("Failed to stop session...");
std::process::exit(1);
}
}
} }
("invoice", _) => { ("invoice", _) => {
println!("Invoice command only partially implemented! No generation is supported"); eprintln!("Invoice command only partially implemented! No generation is supported");
match cass.invoice().run() { run_command(|| cass.invoice().run());
Some(()) => println!("Added INVOICE block"),
None => {
eprintln!("Failed to add INVOICE block...");
std::process::exit(1);
}
}
} }
("update", _) => match cass.update() { ("update", _) => run_command(|| cass.update()),
Some(()) => println!("Updated file to new version: {}", meta::VERSION), (meta::CMD_STAT, _) => run_command(|| {
None => { let stats = cass.stat()?;
eprintln!("Failed to update file..."); println!("{}", stats);
std::process::exit(1); Ok(())
} }),
},
(meta::CMD_STAT, _) => match cass.stat() {
Some(s) => println!("{}", s),
None => {
eprintln!("Failed to collect time statistics...");
std::process::exit(1);
}
},
(_, _) => todo!(), (_, _) => todo!(),
} }
} }
/// Run a closure and print the associated error message
///
/// Set the exit code for the program.
fn run_command<F>(mut cmd: F)
where
F: FnMut() -> ParseResult<()>,
{
match cmd() {
Ok(_) => std::process::exit(0),
Err(e) => {
eprintln!("{}", e);
std::process::exit(2);
}
}
}

@ -1,7 +1,7 @@
//! A set of error types for cassiopeia //! A set of error types for cassiopeia
use std::error::Error;
use std::fmt::{self, Display, Formatter}; use std::fmt::{self, Display, Formatter};
use std::{error::Error, io};
/// User errors that can occur when using cassiopeia /// User errors that can occur when using cassiopeia
/// ///
@ -45,6 +45,14 @@ pub enum ParseError {
/// This error means that the structure of the parsed file is /// This error means that the structure of the parsed file is
/// wrong, with an invalid sequence of events expressed /// wrong, with an invalid sequence of events expressed
User(UserError), User(UserError),
/// The requested file did not exist
NoSuchFile,
/// The file could not be read
BadPermissions,
/// The file could not be written to
FileNotWritable,
/// Other file related errors
FileUnknown(String),
/// An invalid keyword was found /// An invalid keyword was found
BadKeyword { line: usize, tokn: String }, BadKeyword { line: usize, tokn: String },
/// A bad timestamp was found /// A bad timestamp was found
@ -70,3 +78,14 @@ impl From<UserError> for ParseError {
ParseError::User(user) ParseError::User(user)
} }
} }
impl From<io::Error> for ParseError {
fn from(e: io::Error) -> Self {
use io::ErrorKind::*;
match e.kind() {
NotFound => Self::NoSuchFile,
PermissionDenied => Self::BadPermissions,
_ => Self::FileUnknown(format!("{}", e)),
}
}
}

@ -8,13 +8,17 @@ mod parser;
pub(crate) use lexer::{LineLexer, LineToken, Token}; pub(crate) use lexer::{LineLexer, LineToken, Token};
pub(crate) use parser::LineCfg; pub(crate) use parser::LineCfg;
use crate::TimeFile; use crate::{
error::{ParseError, ParseResult},
TimeFile,
};
use ir::{IrItem, IrStream}; use ir::{IrItem, IrStream};
use std::{ use std::{
fs::{File, OpenOptions}, fs::{File, OpenOptions},
io::{Read, Write}, io::{Read, Write},
}; };
/// A crate internal representation of the IR stream and timefile
#[derive(Default)] #[derive(Default)]
pub(crate) struct ParseOutput { pub(crate) struct ParseOutput {
pub(crate) ir: IrStream, pub(crate) ir: IrStream,
@ -22,46 +26,51 @@ pub(crate) struct ParseOutput {
} }
impl ParseOutput { impl ParseOutput {
fn append(mut self, ir: IrItem) -> Self { fn append(mut self, ir: IrItem) -> ParseResult<Self> {
self.tf.append(ir.clone()); self.tf.append(ir.clone())?;
self.ir.push(ir); self.ir.push(ir);
self Ok(self)
} }
} }
/// Load a file from disk and parse it into a /// Load a file from disk and parse it into a
/// [`TimeFile`](crate::TimeFile) /// [`TimeFile`](crate::TimeFile)
pub(crate) fn load_file(path: &str) -> Option<ParseOutput> { pub(crate) fn load_file(path: &str) -> ParseResult<ParseOutput> {
let mut f = File::open(path).ok()?; // Load the raw file contents
let mut f = File::open(path)?;
let mut content = String::new(); let mut content = String::new();
f.read_to_string(&mut content).ok()?; f.read_to_string(&mut content)?;
// Split the file by lines - .cass is a line based format
let mut lines: Vec<String> = content.split("\n").map(|l| l.to_owned()).collect(); let mut lines: Vec<String> = content.split("\n").map(|l| l.to_owned()).collect();
Some( // Build an iterator over parsed lines
ir::generate_ir( let parsed = lines
lines .iter_mut()
.iter_mut() .map(|line| lexer::lex(line))
.map(|line| lexer::lex(line)) .map(|lex| parser::parse(lex));
.map(|lex| parser::parse(lex)),
) // Generate the IR from parse output, then build the timefile
ir::generate_ir(parsed)
.into_iter() .into_iter()
.fold(ParseOutput::default(), |output, ir| output.append(ir)), .fold(Ok(ParseOutput::default()), |out, ir| match out {
) Ok(mut out) => out.append(ir),
e @ Err(_) => e,
})
} }
/// Write a file with the updated IR stream /// Write a file with the updated IR stream
pub(crate) fn write_file(path: &str, ir: &mut IrStream) -> Option<()> { pub(crate) fn write_file(path: &str, ir: &mut IrStream) -> ParseResult<()> {
ir::update_header(ir); ir::update_header(ir);
let mut lines = ir.into_iter().map(|ir| gen::line(ir)).collect::<Vec<_>>(); let mut lines = ir.into_iter().map(|ir| gen::line(ir)).collect::<Vec<_>>();
lines.insert(0, gen::head_comment()); lines.insert(0, gen::head_comment());
let mut f = OpenOptions::new() // let mut f = OpenOptions::new()
.write(true) // .write(true)
.create(true) // .create(true)
.truncate(true) // .truncate(true)
.open(path) // .open(path)
.ok()?; // .ok()?;
f.write_all(lines.join("\n").as_bytes()).ok()?; // f.write_all(lines.join("\n").as_bytes()).ok()?;
Some(()) Ok(())
} }

@ -24,15 +24,6 @@ pub enum LineCfg {
Ignore, Ignore,
} }
impl LineCfg {
pub(crate) fn valid(&self) -> bool {
match self {
LineCfg::Ignore => false,
_ => true,
}
}
}
pub(crate) fn parse<'l>(lex: LineLexer<'l>) -> LineCfg { pub(crate) fn parse<'l>(lex: LineLexer<'l>) -> LineCfg {
use LineCfg::*; use LineCfg::*;
use Token as T; use Token as T;
@ -54,7 +45,7 @@ pub(crate) fn parse<'l>(lex: LineLexer<'l>) -> LineCfg {
(Invoice(_), LineToken { tt: T::Date, slice }) => Invoice(parse_date(slice)), (Invoice(_), LineToken { tt: T::Date, slice }) => Invoice(parse_date(slice)),
// Pass empty lines through, // Pass empty lines through,
(Empty, _) => Empty, (empty, _) => empty,
// Ignore everything else (which will be filtered) // Ignore everything else (which will be filtered)
_ => Ignore, _ => Ignore,

@ -10,16 +10,18 @@
mod data; mod data;
mod date; mod date;
mod error;
mod format; mod format;
pub mod meta;
mod time; mod time;
mod timeline; mod timeline;
pub mod error;
pub mod meta;
pub use date::Date; pub use date::Date;
pub use time::Time; pub use time::Time;
use data::{Invoice, Session, TimeFile}; use data::{Invoice, Session, TimeFile};
use error::{ParseError, ParseResult};
use format::{ use format::{
ir::{append_ir, clean_ir, IrStream, MakeIr}, ir::{append_ir, clean_ir, IrStream, MakeIr},
ParseOutput, ParseOutput,
@ -36,27 +38,22 @@ pub struct Cassiopeia {
impl Cassiopeia { impl Cassiopeia {
/// Load a cass file from disk, parsing it into a [`TimeFile`](crate::TimeFile) /// Load a cass file from disk, parsing it into a [`TimeFile`](crate::TimeFile)
pub fn load(path: &str) -> Option<Self> { pub fn load(path: &str) -> ParseResult<Self> {
let path = path.to_owned(); let path = path.to_owned();
format::load_file(path.as_str()).map(|ParseOutput { tf, ir }| Self { path, tf, ir }) format::load_file(path.as_str()).map(|ParseOutput { tf, ir }| Self { path, tf, ir })
} }
/// Store the modified time file back to disk
pub fn store(&self) -> Option<()> {
Some(())
}
/// Start a new work session (with optional 15 minute rounding) /// Start a new work session (with optional 15 minute rounding)
pub fn start(&mut self, round: bool) -> Option<()> { pub fn start(&mut self, round: bool) -> ParseResult<()> {
let delta = self.tf.timeline.start(Time::rounded(round)).ok()?; let delta = self.tf.timeline.start(Time::rounded(round))?;
clean_ir(&mut self.ir); clean_ir(&mut self.ir);
append_ir(&mut self.ir, delta.make_ir()); append_ir(&mut self.ir, delta.make_ir());
format::write_file(self.path.as_str(), &mut self.ir) format::write_file(self.path.as_str(), &mut self.ir)
} }
/// Stop the existing work session (with optional 15 minute rounding) /// Stop the existing work session (with optional 15 minute rounding)
pub fn stop(&mut self, round: bool) -> Option<()> { pub fn stop(&mut self, round: bool) -> ParseResult<()> {
let delta = self.tf.timeline.stop(Time::rounded(round)).ok()?; let delta = self.tf.timeline.stop(Time::rounded(round))?;
clean_ir(&mut self.ir); clean_ir(&mut self.ir);
append_ir(&mut self.ir, delta.make_ir()); append_ir(&mut self.ir, delta.make_ir());
format::write_file(self.path.as_str(), &mut self.ir) format::write_file(self.path.as_str(), &mut self.ir)
@ -68,14 +65,14 @@ impl Cassiopeia {
} }
/// Write out the file IR as is, updating only the header version /// Write out the file IR as is, updating only the header version
pub fn update(&mut self) -> Option<()> { pub fn update(&mut self) -> ParseResult<()> {
clean_ir(&mut self.ir); clean_ir(&mut self.ir);
format::write_file(self.path.as_str(), &mut self.ir) format::write_file(self.path.as_str(), &mut self.ir)
} }
/// Collect statistics on previous work sessions /// Collect statistics on previous work sessions
pub fn stat(&self) -> Option<String> { pub fn stat(&self) -> ParseResult<String> {
None todo!()
} }
} }
@ -102,6 +99,7 @@ impl Cassiopeia {
/// .client("ACME".into()) /// .client("ACME".into())
/// .run(); /// .run();
/// ``` /// ```
#[allow(unused)]
pub struct Invoicer<'cass> { pub struct Invoicer<'cass> {
tf: &'cass mut Cassiopeia, tf: &'cass mut Cassiopeia,
generate: bool, generate: bool,
@ -143,13 +141,13 @@ impl<'cass> Invoicer<'cass> {
Self { project, ..self } Self { project, ..self }
} }
pub fn run(mut self) -> Option<()> { pub fn run(mut self) -> ParseResult<()> {
if self.generate { if self.generate {
eprintln!("Integration with invoice(1) is currently not implemented. Sorry :("); eprintln!("Integration with invoice(1) is currently not implemented. Sorry :(");
return None; return Err(ParseError::Unknown);
} }
let delta = self.tf.tf.timeline.invoice(Date::today()).ok()?; let delta = self.tf.tf.timeline.invoice(Date::today())?;
clean_ir(&mut self.tf.ir); clean_ir(&mut self.tf.ir);
append_ir(&mut self.tf.ir, delta.make_ir()); append_ir(&mut self.tf.ir, delta.make_ir());
format::write_file(self.tf.path.as_str(), &mut self.tf.ir) format::write_file(self.tf.path.as_str(), &mut self.tf.ir)

@ -81,7 +81,7 @@ impl Time {
let naive = self.inner.time(); let naive = self.inner.time();
let (new_min, incr_hour) = match naive.minute() { let (new_min, incr_hour) = match naive.minute() {
// 0-7 => (0, false) // 0-7 => (0, false)
m if m >= 0 && m < 7 => (0, false), m if m < 7 => (0, false),
// 7-22 => (15, false) // 7-22 => (15, false)
m if m >= 7 && m < 22 => (15, false), m if m >= 7 && m < 22 => (15, false),
// 22-37 => (30, false) // 22-37 => (30, false)

Loading…
Cancel
Save