aboutsummaryrefslogtreecommitdiff
path: root/apps/cassiopeia
diff options
context:
space:
mode:
authorMx Kookie <kookie@spacekookie.de>2020-12-15 19:41:48 +0000
committerMx Kookie <kookie@spacekookie.de>2020-12-21 05:19:49 +0100
commitb9c988f42504c2e4cfa0715ac8f2d2a0db591cad (patch)
tree4850aba3bc43b7b303c4a1a3b6dd64a3f85e1df5 /apps/cassiopeia
parent236cf191b90a428325c8c179d595d4b1cd36f776 (diff)
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.
Diffstat (limited to 'apps/cassiopeia')
-rw-r--r--apps/cassiopeia/src/bin/cass.rs65
-rw-r--r--apps/cassiopeia/src/data.rs7
-rw-r--r--apps/cassiopeia/src/date.rs7
-rw-r--r--apps/cassiopeia/src/format/gen.rs33
-rw-r--r--apps/cassiopeia/src/format/ir.rs41
-rw-r--r--apps/cassiopeia/src/format/mod.rs30
-rw-r--r--apps/cassiopeia/src/lib.rs37
-rw-r--r--apps/cassiopeia/src/meta.rs3
-rw-r--r--apps/cassiopeia/src/time.rs18
9 files changed, 208 insertions, 33 deletions
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<Invoice> {
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<NaiveDate> 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::<Vec<_>>()
+ .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<Item = LineCfg>) -> 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<Item = LineCfg>) -> 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<ParseOutput> {
+pub(crate) fn load_file(path: &str) -> Option<ParseOutput> {
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<ParseOutput> {
.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::<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(())
+}
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<Self> {
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)