From b9c988f42504c2e4cfa0715ac8f2d2a0db591cad Mon Sep 17 00:00:00 2001 From: Mx Kookie Date: Tue, 15 Dec 2020 19:41:48 +0000 Subject: cassiopeia: finishing up version 0.3.0 This commit does kind of a lot to get cass(1) over the finish line. For one it implements all the CLI functions (well, almost all) with their respective parameters, and also creates a new `gen` module which uses the IR stream to generate a new file based on the old one, while updating header fields that need to be updated (currently only `version`). This version does nothing with the actual header values, and probably has a lot of bugs. More documentation will follow in future cassiopeia commits. --- apps/cassiopeia/src/bin/cass.rs | 65 ++++++++++++++++++++++++++++++++++++--- apps/cassiopeia/src/data.rs | 7 ++--- apps/cassiopeia/src/date.rs | 7 +++++ apps/cassiopeia/src/format/gen.rs | 33 ++++++++++++++++++++ apps/cassiopeia/src/format/ir.rs | 41 ++++++++++++++++++++++-- apps/cassiopeia/src/format/mod.rs | 30 +++++++++++++++--- apps/cassiopeia/src/lib.rs | 37 ++++++++++++++-------- apps/cassiopeia/src/meta.rs | 3 ++ apps/cassiopeia/src/time.rs | 18 ++++++++--- 9 files changed, 208 insertions(+), 33 deletions(-) create mode 100644 apps/cassiopeia/src/format/gen.rs (limited to 'apps/cassiopeia') diff --git a/apps/cassiopeia/src/bin/cass.rs b/apps/cassiopeia/src/bin/cass.rs index 45b032d7d768..90f84661ae0c 100644 --- a/apps/cassiopeia/src/bin/cass.rs +++ b/apps/cassiopeia/src/bin/cass.rs @@ -1,8 +1,8 @@ -use cassiopeia::{self as cass, meta}; +use cassiopeia::{meta, Cassiopeia}; use clap::{App, Arg, SubCommand}; fn main() { - let app = App::new(meta::NAME) + let cli = App::new(meta::NAME) .version(meta::VERSION) .about(meta::ABOUT) .after_help("To learn more on how to use cassiopeia, check out the documentation \ @@ -23,15 +23,16 @@ If you want to report a bug, please do so on my mailing list: lists.sr.ht/~space .default_value("./time.cass") .takes_value(true), ) + .subcommand(SubCommand::with_name(meta::CMD_UPDATE).about(meta::CMD_UPDATE_ABOUT)) .subcommand( SubCommand::with_name(meta::CMD_START) .about(meta::CMD_START_ABOUT) - .arg(Arg::with_name(meta::ARG_ROUNDING).help(meta::ARG_ROUNDING_ABOUT)), + .arg(Arg::with_name(meta::ARG_ROUNDING).short("r").long("no-round").help(meta::ARG_ROUNDING_ABOUT)), ) .subcommand( SubCommand::with_name(meta::CMD_STOP) .about(meta::CMD_STOP_ABOUT) - .arg(Arg::with_name(meta::ARG_ROUNDING).help(meta::ARG_ROUNDING_ABOUT)), + .arg(Arg::with_name(meta::ARG_ROUNDING).short("r").long("no-round").help(meta::ARG_ROUNDING_ABOUT)), ) .subcommand( SubCommand::with_name(meta::CMD_INVOICE) @@ -65,5 +66,59 @@ If you want to report a bug, please do so on my mailing list: lists.sr.ht/~space ) .get_matches(); - let file = cass::load_file("/home/projects/clients/nyantec-nix-workshops/time.cass"); + 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); + } + }; + + // 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); + } + } + } + ("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); + } + } + } + ("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); + } + } + } + ("update", _) => match cass.update() { + Some(()) => println!("Updated file to new version: {}", meta::VERSION), + None => { + eprintln!("Failed to update file..."); + std::process::exit(1); + } + }, + (_, _) => todo!(), + } } diff --git a/apps/cassiopeia/src/data.rs b/apps/cassiopeia/src/data.rs index 47bb6e35b6e5..188c0255203d 100644 --- a/apps/cassiopeia/src/data.rs +++ b/apps/cassiopeia/src/data.rs @@ -5,7 +5,7 @@ //! analysis tasks. use crate::{ - format::{IrItem, IrType, MakeIr}, + format::ir::{IrItem, IrType, MakeIr}, Date, Time, }; use chrono::{DateTime, Duration, FixedOffset as Offset, Local, NaiveDate}; @@ -96,7 +96,7 @@ impl TimeFile { } /// Add a new invoice block to the time file - pub(crate) fn invoice(&mut self) -> Option<()> { + pub(crate) fn invoice(&mut self) -> Option { let today = Date::today(); let last_sess = self.get_last_session().cloned(); @@ -119,8 +119,7 @@ impl TimeFile { _ => {} } - self.invoices.push(Invoice::new(today)); - Some(()) + Some(Invoice::new(today)) } } diff --git a/apps/cassiopeia/src/date.rs b/apps/cassiopeia/src/date.rs index 00d8a700859a..df366f6717aa 100644 --- a/apps/cassiopeia/src/date.rs +++ b/apps/cassiopeia/src/date.rs @@ -24,3 +24,10 @@ impl From for Date { Self { inner } } } + + +impl ToString for Date { + fn to_string(&self) -> String { + format!("{}", self.inner.format("%Y-%m-%d")) + } +} diff --git a/apps/cassiopeia/src/format/gen.rs b/apps/cassiopeia/src/format/gen.rs new file mode 100644 index 000000000000..2bcd6cc724fd --- /dev/null +++ b/apps/cassiopeia/src/format/gen.rs @@ -0,0 +1,33 @@ +//! Cassiopeia line generator +//! +//! This module takes a set of IR lines, and generates strings from +//! them that are in accordance with the way that the parser of the +//! same version expects them. + +use crate::format::ir::{IrItem, IrType}; + +/// Take a line of IR and generate a string to print into a file +pub(crate) fn line(ir: &IrItem) -> String { + let IrItem { tt, lo } = ir; + match tt { + IrType::Ignore => "".into(), + IrType::Header(map) => format!( + "HEADER {}", + map.iter() + .map(|(k, v)| format!("{}={},", k, v)) + .collect::>() + .join("") + ), + IrType::Start(time) => format!("START {}", time.to_string()), + + // FIXME: find a better way to align the lines here rather + // than having to manually having to pad the 'STOP' commands + IrType::Stop(time) => format!("STOP {}", time.to_string()), + IrType::Invoice(date) => format!("INVOICE {}", date.to_string()), + } +} + +pub(crate) fn head_comment() -> String { + ";; generated by cassiopeia, be careful about editing by hand!".into() +} + diff --git a/apps/cassiopeia/src/format/ir.rs b/apps/cassiopeia/src/format/ir.rs index 32922ec079e7..09a54e810615 100644 --- a/apps/cassiopeia/src/format/ir.rs +++ b/apps/cassiopeia/src/format/ir.rs @@ -44,7 +44,7 @@ pub(crate) fn generate_ir(buf: impl Iterator) -> IrStream { buf.enumerate().fold(vec![], |mut buf, (lo, item)| { #[cfg_attr(rustfmt, rustfmt_skip)] buf.push(match item { - LineCfg::Header(map) => IrItem { tt: IrType::Header(map), lo }, + LineCfg::Header(map) => IrItem { tt: IrType::Header(map.into_iter().map(|(k, v)| (k, v.replace(",", ""))).collect()), lo }, LineCfg::Start(Some(time)) => IrItem { tt: IrType::Start(time.into()), lo }, LineCfg::Stop(Some(time)) => IrItem { tt: IrType::Stop(time.into()), lo }, LineCfg::Invoice(Some(date)) => IrItem { tt: IrType::Invoice(date.into()), lo }, @@ -56,8 +56,45 @@ pub(crate) fn generate_ir(buf: impl Iterator) -> IrStream { }) } - pub(crate) trait MakeIr { /// Make a new IR line from an object fn make_ir(&self) -> IrType; } + +pub(crate) fn clean_ir(ir: &mut IrStream) { + ir.remove(0); // FIXME: this is required to remove the leading + // comment, which will be manually re-generated at + // the moment, but which would just add more blank + // lines between the new comment, and the first line + // in this current format. This is very bad, yikes + // yikes yikes, but what can I do, I have a deadline + // (not really) lol + + // FIXME: this hack gets rid of a trailing empty line if it exists + // to make sure we don't have any gaps between work sessions. + if match ir.last() { + Some(IrItem { + tt: IrType::Ignore, .. + }) => true, + _ => false, + } { + ir.pop(); + } +} + +/// Taken an IrType and append it to an existing IR stream +pub(crate) fn append_ir(ir: &mut IrStream, tt: IrType) { + + let lo = ir.last().unwrap().lo; + ir.push(IrItem { tt, lo }); +} + +/// Search for the header that contains the version string and update it +pub(crate) fn update_header(ir: &mut IrStream) { + ir.iter_mut().for_each(|item| match item.tt { + IrType::Header(ref mut map) if map.contains_key("version") => { + map.insert("version".into(), crate::meta::VERSION.into()); + } + _ => {} + }); +} diff --git a/apps/cassiopeia/src/format/mod.rs b/apps/cassiopeia/src/format/mod.rs index 814c08656dbe..89f3a6ccb466 100644 --- a/apps/cassiopeia/src/format/mod.rs +++ b/apps/cassiopeia/src/format/mod.rs @@ -1,18 +1,22 @@ //! cassiopeia file format -mod ir; +mod gen; +pub(crate) mod ir; mod lexer; mod parser; -pub(crate) use ir::{IrItem, IrStream, IrType, MakeIr}; pub(crate) use lexer::{LineLexer, LineToken, Token}; pub(crate) use parser::LineCfg; use crate::TimeFile; -use std::{fs::File, io::Read}; +use ir::{IrItem, IrStream}; +use std::{ + fs::{File, OpenOptions}, + io::{Read, Write}, +}; #[derive(Default)] -pub struct ParseOutput { +pub(crate) struct ParseOutput { pub(crate) ir: IrStream, pub(crate) tf: TimeFile, } @@ -27,7 +31,7 @@ impl ParseOutput { /// Load a file from disk and parse it into a /// [`TimeFile`](crate::TimeFile) -pub fn load_file(path: &str) -> Option { +pub(crate) fn load_file(path: &str) -> Option { let mut f = File::open(path).ok()?; let mut content = String::new(); f.read_to_string(&mut content).ok()?; @@ -45,3 +49,19 @@ pub fn load_file(path: &str) -> Option { .fold(ParseOutput::default(), |output, ir| output.append(ir)), ) } + +/// Write a file with the updated IR stream +pub(crate) fn write_file(path: &str, ir: &mut IrStream) -> Option<()> { + ir::update_header(ir); + let mut lines = ir.into_iter().map(|ir| gen::line(ir)).collect::>(); + 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(()) +} diff --git a/apps/cassiopeia/src/lib.rs b/apps/cassiopeia/src/lib.rs index d4ebf5355bb9..e391c695237e 100644 --- a/apps/cassiopeia/src/lib.rs +++ b/apps/cassiopeia/src/lib.rs @@ -15,11 +15,13 @@ pub mod meta; mod time; pub use date::Date; -pub use format::load_file; pub use time::Time; use data::{Invoice, Session, TimeFile}; -use format::{ir, IrStream, ParseOutput}; +use format::{ + ir::{append_ir, clean_ir, IrStream, MakeIr}, + ParseOutput, +}; /// A state handler and primary API for all cass interactions /// @@ -34,7 +36,7 @@ impl Cassiopeia { /// Load a cass file from disk, parsing it into a [`TimeFile`](crate::TimeFile) pub fn load(path: &str) -> Option { let path = path.to_owned(); - 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 @@ -44,22 +46,30 @@ impl Cassiopeia { /// Start a new work session (with optional 15 minute rounding) pub fn start(&mut self, round: bool) -> Option<()> { - self.tf.start(round)?; - - Some(()) + let s = self.tf.start(round)?; + clean_ir(&mut self.ir); + append_ir(&mut self.ir, s.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<()> { - self.tf.stop(round)?; - - Some(()) + let s = self.tf.stop(round)?; + clean_ir(&mut self.ir); + append_ir(&mut self.ir, s.make_ir()); + format::write_file(self.path.as_str(), &mut self.ir) } /// Add an invoice block to the time file pub fn invoice<'slf>(&'slf mut self) -> Invoicer<'slf> { Invoicer::new(self) } + + /// Write out the file IR as is, updating only the header version + pub fn update(&mut self) -> Option<()> { + clean_ir(&mut self.ir); + format::write_file(self.path.as_str(), &mut self.ir) + } } /// An invoice generator builder @@ -126,14 +136,15 @@ impl<'cass> Invoicer<'cass> { Self { project, ..self } } - pub fn run(self) -> Option<()> { + pub fn run(mut self) -> Option<()> { if self.generate { eprintln!("Integration with invoice(1) is currently not implemented. Sorry :("); return None; } - self.tf.tf.invoice()?; - - Some(()) + let inv = self.tf.tf.invoice()?; + clean_ir(&mut self.tf.ir); + append_ir(&mut self.tf.ir, inv.make_ir()); + format::write_file(self.tf.path.as_str(), &mut self.tf.ir) } } diff --git a/apps/cassiopeia/src/meta.rs b/apps/cassiopeia/src/meta.rs index 6d74d95bfe2a..d29f8b9b30e0 100644 --- a/apps/cassiopeia/src/meta.rs +++ b/apps/cassiopeia/src/meta.rs @@ -23,6 +23,9 @@ pub const CMD_INVOICE_ABOUT: &'static str = "Create an invoice. You get to choo statement to your time file, or generating .yml configuration to build an invoice generator from. See invoice(1) \ for more detail!"; +pub const CMD_UPDATE: &'static str = "update"; +pub const CMD_UPDATE_ABOUT: &'static str = "Update the selected file to a new version"; + pub const ARG_CLIENT: &'static str = "CLIENT"; pub const ARG_CLIENT_ABOUT: &'static str = "Provide the name of the current client for invoice generation"; diff --git a/apps/cassiopeia/src/time.rs b/apps/cassiopeia/src/time.rs index 4ac4ce7db900..0161742a0c92 100644 --- a/apps/cassiopeia/src/time.rs +++ b/apps/cassiopeia/src/time.rs @@ -27,12 +27,23 @@ impl<'t> Sub for &'t Time { } } +impl ToString for Time { + fn to_string(&self) -> String { + format!("{}", self.inner.format("%Y-%m-%d %H:%M:%S%:z")) + } +} + impl Time { /// Get the current local time and pin it to a fixed Tz offset pub fn now() -> Self { let now = Local::now(); Self { - inner: build_datetime(now.time()), + inner: build_datetime( + now.time() + .with_second(0) + .and_then(|t| t.with_nanosecond(0)) + .unwrap(), + ), } } @@ -44,7 +55,7 @@ impl Time { pub fn after(&self, date: &Date) -> bool { &Date::from(self.date()) > date } - + #[cfg(test)] pub(crate) fn fixed(hour: u32, min: u32, sec: u32) -> Self { Self { @@ -59,10 +70,9 @@ impl Time { /// rounding values that were created in a different timezone. pub fn round(&self) -> Self { 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 >= 0 && m < 7 => (0, false), // 7-22 => (15, false) m if m >= 7 && m < 22 => (15, false), // 22-37 => (30, false) -- cgit v1.2.3