aboutsummaryrefslogtreecommitdiff
path: root/apps/cassiopeia
diff options
context:
space:
mode:
authorMx Kookie <kookie@spacekookie.de>2021-01-07 23:48:07 +0100
committerMx Kookie <kookie@spacekookie.de>2021-01-07 23:48:07 +0100
commit4fd6c87c43af132a2871982282d2f9ecd244b892 (patch)
tree747fafa1d7a5d9403080079113ce1c62d837197a /apps/cassiopeia
parent581bf56eeff8a8fdd403377c813b72ef9077bb14 (diff)
cassiopeia: update CLI handling and add new commands
Diffstat (limited to 'apps/cassiopeia')
-rw-r--r--apps/cassiopeia/.envrc1
-rw-r--r--apps/cassiopeia/shell.nix9
-rw-r--r--apps/cassiopeia/src/bin/cass.rs77
-rw-r--r--apps/cassiopeia/src/error.rs21
-rw-r--r--apps/cassiopeia/src/format/mod.rs59
-rw-r--r--apps/cassiopeia/src/format/parser.rs11
-rw-r--r--apps/cassiopeia/src/lib.rs34
-rw-r--r--apps/cassiopeia/src/time.rs2
8 files changed, 104 insertions, 110 deletions
diff --git a/apps/cassiopeia/.envrc b/apps/cassiopeia/.envrc
deleted file mode 100644
index 051d09d292a8..000000000000
--- a/apps/cassiopeia/.envrc
+++ /dev/null
@@ -1 +0,0 @@
-eval "$(lorri direnv)"
diff --git a/apps/cassiopeia/shell.nix b/apps/cassiopeia/shell.nix
index a365404a622b..ca6afad60654 100644
--- a/apps/cassiopeia/shell.nix
+++ b/apps/cassiopeia/shell.nix
@@ -1,8 +1 @@
-with import <nixpkgs> {};
-
-stdenv.mkDerivation {
- name = "cassiopeia";
- buildInputs = with pkgs; [
- rustracer rustup clangStdenv
- ];
-}
+import <nom/rust.nix>
diff --git a/apps/cassiopeia/src/bin/cass.rs b/apps/cassiopeia/src/bin/cass.rs
index 8bceddc911a4..3fea64380623 100644
--- a/apps/cassiopeia/src/bin/cass.rs
+++ b/apps/cassiopeia/src/bin/cass.rs
@@ -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);
+ }
+ }
+}
diff --git a/apps/cassiopeia/src/error.rs b/apps/cassiopeia/src/error.rs
index 31f5414c4f86..24bbb4965494 100644
--- a/apps/cassiopeia/src/error.rs
+++ b/apps/cassiopeia/src/error.rs
@@ -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)),
+ }
+ }
+}
diff --git a/apps/cassiopeia/src/format/mod.rs b/apps/cassiopeia/src/format/mod.rs
index 89f3a6ccb466..65c2264d6829 100644
--- a/apps/cassiopeia/src/format/mod.rs
+++ b/apps/cassiopeia/src/format/mod.rs
@@ -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(())
}
diff --git a/apps/cassiopeia/src/format/parser.rs b/apps/cassiopeia/src/format/parser.rs
index d1f0bcdebc68..78433c0f5cec 100644
--- a/apps/cassiopeia/src/format/parser.rs
+++ b/apps/cassiopeia/src/format/parser.rs
@@ -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,
diff --git a/apps/cassiopeia/src/lib.rs b/apps/cassiopeia/src/lib.rs
index 895110fe218d..7b4409c9689f 100644
--- a/apps/cassiopeia/src/lib.rs
+++ b/apps/cassiopeia/src/lib.rs
@@ -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)
diff --git a/apps/cassiopeia/src/time.rs b/apps/cassiopeia/src/time.rs
index a5147b93aaac..4cda0718d922 100644
--- a/apps/cassiopeia/src/time.rs
+++ b/apps/cassiopeia/src/time.rs
@@ -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)