aboutsummaryrefslogtreecommitdiff
path: root/apps/cassiopeia/src/lib.rs
//! Cassiopeia plain text time tracking tool
//!
//! Versions `0.1` and `0.2` were written in Ruby and are thus
//! deprecated.  Most likely you are interested in `cass(1)`, the
//! simple plain text time tracking utility, part of the kookie-office
//! suite of commandline tools!  This is the library powering it.
//!
//! For more documentation, check out:
//! https://git.spacekookie.de/kookienomicon/tree/apps/cassiopeia

mod data;
mod date;
mod format;
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,
};

/// A state handler and primary API for all cass interactions
///
///
pub struct Cassiopeia {
    path: String,
    tf: TimeFile,
    ir: IrStream,
}

impl Cassiopeia {
    /// Load a cass file from disk, parsing it into a [`TimeFile`](crate::TimeFile)
    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 })
    }

    /// Start a new work session (with optional 15 minute rounding)
    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) -> 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)
    }

    /// 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) -> 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) -> ParseResult<String> {
        todo!()
    }
}

/// An invoice generator builder
///
/// The most simple use-case of this type is to provide no parameters
/// and simply add an `INVOICE` line to the cass file.  Adittionally
/// you may provide the client and project name, which will then
/// require the `client_db` path to be set as well.
///
/// ```rust,no_run
/// # let mut cass = cassiopeia::Cassiopeia::load("").unwrap();
/// cass.invoice().run();
/// ```
///
/// Additional errors can be thrown if the client or project are not
/// known in the client db.
///
/// ```rust,no_run
/// # let mut cass = cassiopeia::Cassiopeia::load("").unwrap();
/// cass.invoice()
///     .generate()
///     .db("/home/office/clients.yml".into())
///     .client("ACME".into())
///     .run();
/// ```
#[allow(unused)]
pub struct Invoicer<'cass> {
    tf: &'cass mut Cassiopeia,
    generate: bool,
    client_db: String,
    client: String,
    project: String,
}

impl<'cass> Invoicer<'cass> {
    pub fn new(tf: &'cass mut Cassiopeia) -> Self {
        Self {
            tf,
            generate: false,
            client_db: String::new(),
            client: String::new(),
            project: String::new(),
        }
    }

    /// Enable the invoice generation feature
    pub fn generate(self) -> Self {
        Self {
            generate: true,
            ..self
        }
    }

    /// Provide the client database file (.yml format)
    pub fn db(self, client_db: String) -> Self {
        Self { client_db, ..self }
    }

    /// Provide the client to invoice
    pub fn client(self, client: String) -> Self {
        Self { client, ..self }
    }

    pub fn project(self, project: String) -> Self {
        Self { project, ..self }
    }

    pub fn run(mut self) -> ParseResult<()> {
        if self.generate {
            eprintln!("Integration with invoice(1) is currently not implemented.  Sorry :(");
            return Err(ParseError::Unknown);
        }

        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)
    }
}