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> {};
stdenv.mkDerivation {
name = "cassiopeia";
buildInputs = with pkgs; [
rustracer rustup clangStdenv
];
}
import <nom/rust.nix>

@ -1,4 +1,4 @@
use cassiopeia::{meta, Cassiopeia};
use cassiopeia::{error::ParseResult, meta, Cassiopeia};
use clap::{App, Arg, SubCommand};
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 mut cass = match Cassiopeia::load(cass_file) {
Some(cf) => cf,
None => {
eprintln!(
"Invalid CASS file '{}'; file not found, or unparsable.",
cass_file
);
std::process::exit(2);
Ok(cf) => cf,
Err(e) => {
eprintln!("{}", e);
std::process::exit(1);
}
};
// Parse the matches generated by clap
match cli.subcommand() {
("start", Some(ops)) => {
// This parameter turns rounding OFF
let round = ops.is_present(meta::ARG_ROUNDING);
match cass.start(!round) {
Some(()) => println!("Started session!"),
None => {
eprintln!("Failed to start session...");
std::process::exit(1);
}
}
run_command(|| cass.start(!round));
}
("stop", Some(ops)) => {
// This parameter turns rounding OFF
let round = ops.is_present(meta::ARG_ROUNDING);
match cass.stop(!round) {
Some(()) => println!("Stopped session!"),
None => {
eprintln!("Failed to stop session...");
std::process::exit(1);
}
}
run_command(|| cass.stop(!round));
}
("invoice", _) => {
println!("Invoice command only partially implemented! No generation is supported");
match cass.invoice().run() {
Some(()) => println!("Added INVOICE block"),
None => {
eprintln!("Failed to add INVOICE block...");
std::process::exit(1);
}
}
eprintln!("Invoice command only partially implemented! No generation is supported");
run_command(|| cass.invoice().run());
}
("update", _) => match cass.update() {
Some(()) => println!("Updated file to new version: {}", meta::VERSION),
None => {
eprintln!("Failed to update file...");
std::process::exit(1);
}
},
(meta::CMD_STAT, _) => match cass.stat() {
Some(s) => println!("{}", s),
None => {
eprintln!("Failed to collect time statistics...");
std::process::exit(1);
}
},
("update", _) => run_command(|| cass.update()),
(meta::CMD_STAT, _) => run_command(|| {
let stats = cass.stat()?;
println!("{}", stats);
Ok(())
}),
(_, _) => 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
use std::error::Error;
use std::fmt::{self, Display, Formatter};
use std::{error::Error, io};
/// 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
/// wrong, with an invalid sequence of events expressed
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
BadKeyword { line: usize, tokn: String },
/// A bad timestamp was found
@ -70,3 +78,14 @@ impl From<UserError> for ParseError {
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 parser::LineCfg;
use crate::TimeFile;
use crate::{
error::{ParseError, ParseResult},
TimeFile,
};
use ir::{IrItem, IrStream};
use std::{
fs::{File, OpenOptions},
io::{Read, Write},
};
/// A crate internal representation of the IR stream and timefile
#[derive(Default)]
pub(crate) struct ParseOutput {
pub(crate) ir: IrStream,
@ -22,46 +26,51 @@ pub(crate) struct ParseOutput {
}
impl ParseOutput {
fn append(mut self, ir: IrItem) -> Self {
self.tf.append(ir.clone());
fn append(mut self, ir: IrItem) -> ParseResult<Self> {
self.tf.append(ir.clone())?;
self.ir.push(ir);
self
Ok(self)
}
}
/// Load a file from disk and parse it into a
/// [`TimeFile`](crate::TimeFile)
pub(crate) fn load_file(path: &str) -> Option<ParseOutput> {
let mut f = File::open(path).ok()?;
pub(crate) fn load_file(path: &str) -> ParseResult<ParseOutput> {
// Load the raw file contents
let mut f = File::open(path)?;
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();
Some(
ir::generate_ir(
lines
.iter_mut()
.map(|line| lexer::lex(line))
.map(|lex| parser::parse(lex)),
)
// Build an iterator over parsed lines
let parsed = lines
.iter_mut()
.map(|line| lexer::lex(line))
.map(|lex| parser::parse(lex));
// Generate the IR from parse output, then build the timefile
ir::generate_ir(parsed)
.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
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);
let mut lines = ir.into_iter().map(|ir| gen::line(ir)).collect::<Vec<_>>();
lines.insert(0, gen::head_comment());
let mut f = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(path)
.ok()?;
f.write_all(lines.join("\n").as_bytes()).ok()?;
Some(())
// let mut f = OpenOptions::new()
// .write(true)
// .create(true)
// .truncate(true)
// .open(path)
// .ok()?;
// f.write_all(lines.join("\n").as_bytes()).ok()?;
Ok(())
}

@ -24,15 +24,6 @@ pub enum LineCfg {
Ignore,
}
impl LineCfg {
pub(crate) fn valid(&self) -> bool {
match self {
LineCfg::Ignore => false,
_ => true,
}
}
}
pub(crate) fn parse<'l>(lex: LineLexer<'l>) -> LineCfg {
use LineCfg::*;
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)),
// Pass empty lines through,
(Empty, _) => Empty,
(empty, _) => empty,
// Ignore everything else (which will be filtered)
_ => Ignore,

@ -10,16 +10,18 @@
mod data;
mod date;
mod error;
mod format;
pub mod meta;
mod time;
mod timeline;
pub mod error;
pub mod meta;
pub use date::Date;
pub use time::Time;
use data::{Invoice, Session, TimeFile};
use error::{ParseError, ParseResult};
use format::{
ir::{append_ir, clean_ir, IrStream, MakeIr},
ParseOutput,
@ -36,27 +38,22 @@ pub struct Cassiopeia {
impl Cassiopeia {
/// 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();
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)
pub fn start(&mut self, round: bool) -> Option<()> {
let delta = self.tf.timeline.start(Time::rounded(round)).ok()?;
pub fn start(&mut self, round: bool) -> ParseResult<()> {
let delta = self.tf.timeline.start(Time::rounded(round))?;
clean_ir(&mut self.ir);
append_ir(&mut self.ir, delta.make_ir());
format::write_file(self.path.as_str(), &mut self.ir)
}
/// Stop the existing work session (with optional 15 minute rounding)
pub fn stop(&mut self, round: bool) -> Option<()> {
let delta = self.tf.timeline.stop(Time::rounded(round)).ok()?;
pub fn stop(&mut self, round: bool) -> ParseResult<()> {
let delta = self.tf.timeline.stop(Time::rounded(round))?;
clean_ir(&mut self.ir);
append_ir(&mut self.ir, delta.make_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
pub fn update(&mut self) -> Option<()> {
pub fn update(&mut self) -> ParseResult<()> {
clean_ir(&mut self.ir);
format::write_file(self.path.as_str(), &mut self.ir)
}
/// Collect statistics on previous work sessions
pub fn stat(&self) -> Option<String> {
None
pub fn stat(&self) -> ParseResult<String> {
todo!()
}
}
@ -102,6 +99,7 @@ impl Cassiopeia {
/// .client("ACME".into())
/// .run();
/// ```
#[allow(unused)]
pub struct Invoicer<'cass> {
tf: &'cass mut Cassiopeia,
generate: bool,
@ -143,13 +141,13 @@ impl<'cass> Invoicer<'cass> {
Self { project, ..self }
}
pub fn run(mut self) -> Option<()> {
pub fn run(mut self) -> ParseResult<()> {
if self.generate {
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);
append_ir(&mut self.tf.ir, delta.make_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 (new_min, incr_hour) = match naive.minute() {
// 0-7 => (0, false)
m if m >= 0 && m < 7 => (0, false),
m if m < 7 => (0, false),
// 7-22 => (15, false)
m if m >= 7 && m < 22 => (15, false),
// 22-37 => (30, false)

Loading…
Cancel
Save