aboutsummaryrefslogtreecommitdiff
path: root/apps/cassiopeia
diff options
context:
space:
mode:
authorMx Kookie <kookie@spacekookie.de>2020-12-11 17:58:39 +0000
committerMx Kookie <kookie@spacekookie.de>2020-12-21 05:19:49 +0100
commitd40e014aebc778939ae8d9afae225b7d4f6cc949 (patch)
treefa79d54a901c757149b90f8bdf329e954b432bc7 /apps/cassiopeia
parent717c5ccff4a03c3f459456586bcbc28cd88f6709 (diff)
cassiopeia: adding more types to the time file abstraction
Diffstat (limited to 'apps/cassiopeia')
-rw-r--r--apps/cassiopeia/Cargo.lock188
-rw-r--r--apps/cassiopeia/Cargo.toml10
-rw-r--r--apps/cassiopeia/README.md64
-rw-r--r--apps/cassiopeia/src/data.rs66
-rw-r--r--apps/cassiopeia/src/format/mod.rs7
-rw-r--r--apps/cassiopeia/src/format/parser.rs22
-rw-r--r--apps/cassiopeia/src/main.rs3
7 files changed, 336 insertions, 24 deletions
diff --git a/apps/cassiopeia/Cargo.lock b/apps/cassiopeia/Cargo.lock
index be14f5554b85..2987b5639928 100644
--- a/apps/cassiopeia/Cargo.lock
+++ b/apps/cassiopeia/Cargo.lock
@@ -1,6 +1,35 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
+name = "aho-corasick"
+version = "0.7.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "ansi_term"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "winapi",
+]
+
+[[package]]
name = "autocfg"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -13,14 +42,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "474a626a67200bd107d44179bb3d4fc61891172d11696609264589be6a0e6a43"
[[package]]
+name = "bitflags"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
+
+[[package]]
name = "cassiopeia"
-version = "0.1.0"
+version = "0.3.0"
dependencies = [
"chrono",
+ "clap",
+ "env_logger",
+ "log",
"logos",
]
[[package]]
+name = "cfg-if"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
+
+[[package]]
name = "chrono"
version = "0.4.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -34,18 +78,77 @@ dependencies = [
]
[[package]]
+name = "clap"
+version = "2.33.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
+dependencies = [
+ "ansi_term",
+ "atty",
+ "bitflags",
+ "strsim",
+ "term_size",
+ "textwrap",
+ "unicode-width",
+ "vec_map",
+]
+
+[[package]]
+name = "env_logger"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26ecb66b4bdca6c1409b40fb255eefc2bd4f6d135dab3c3124f80ffa2a9661e"
+dependencies = [
+ "atty",
+ "humantime",
+ "log",
+ "regex",
+ "termcolor",
+]
+
+[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
+name = "hermit-abi"
+version = "0.1.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5aca5565f760fb5b220e499d72710ed156fdb74e631659e99377d9ebfbd13ae8"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "humantime"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c1ad908cc71012b7bea4d0c53ba96a8cba9962f048fa68d143376143d863b7a"
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
name = "libc"
version = "0.2.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1482821306169ec4d07f6aca392a4681f66c75c9918aa49641a2595db64053cb"
[[package]]
+name = "log"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
name = "logos"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -70,6 +173,12 @@ dependencies = [
]
[[package]]
+name = "memchr"
+version = "2.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525"
+
+[[package]]
name = "num-integer"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -107,12 +216,30 @@ dependencies = [
]
[[package]]
+name = "regex"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38cf2c13ed4745de91a5eb834e11c00bcc3709e773173b2ce4c56c9fbde04b9c"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+ "thread_local",
+]
+
+[[package]]
name = "regex-syntax"
version = "0.6.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b181ba2dcf07aaccad5448e8ead58db5b742cf85dfe035e2227f137a539a189"
[[package]]
+name = "strsim"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
+
+[[package]]
name = "syn"
version = "1.0.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -124,6 +251,44 @@ dependencies = [
]
[[package]]
+name = "term_size"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e4129646ca0ed8f45d09b929036bafad5377103edd06e50bf574b353d2b08d9"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "textwrap"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
+dependencies = [
+ "term_size",
+ "unicode-width",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
name = "time"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -135,6 +300,12 @@ dependencies = [
]
[[package]]
+name = "unicode-width"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
+
+[[package]]
name = "unicode-xid"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -147,6 +318,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ae116fef2b7fea257ed6440d3cfcff7f190865f170cdad00bb6465bf18ecba"
[[package]]
+name = "vec_map"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
+
+[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -169,6 +346,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/apps/cassiopeia/Cargo.toml b/apps/cassiopeia/Cargo.toml
index 2901f8199c00..9840eb3629be 100644
--- a/apps/cassiopeia/Cargo.toml
+++ b/apps/cassiopeia/Cargo.toml
@@ -1,9 +1,15 @@
[package]
name = "cassiopeia"
-version = "0.1.0"
+version = "0.3.0"
+description = "Simple, low effort time tracking tool for the kookie-office ecosystem"
authors = ["Mx Kookie <kookie@spacekookie.de>"]
edition = "2018"
+license = "GPL-3.0-or-later"
+repository = "https://git.spacekookie.de/kookienomicon/tree/apps/cassiopeia"
[dependencies]
-chrono = "*"
+chrono = "0.4"
+clap = { version = "2.0", features = ["wrap_help", "suggestions", "color"] }
+env_logger = "0.8"
+log = "0.4"
logos = "0.11" \ No newline at end of file
diff --git a/apps/cassiopeia/README.md b/apps/cassiopeia/README.md
new file mode 100644
index 000000000000..d6456bf7f403
--- /dev/null
+++ b/apps/cassiopeia/README.md
@@ -0,0 +1,64 @@
+# cassiopeia
+
+A simple time tracking tool for the kookie office ecosystem.
+
+The kookie office ecosystem is a set of free software, plain text
+tools that I use to run my business. The other tool you may want to
+look at is called [invoice]!
+
+[invoice]: https://git.spacekookie.de/kookienomicon/tree/infra/libkookie/overlays/kookie/invoice
+
+## How to use
+
+Each time file only tracks a single customer or project that you are
+working on. The time file itself is simply a set of commands,
+followed by associated data. The following four keywords exist:
+
+* `HEADER`: define metadata about the client, and store the time file
+ version to avoid parsing with an incompatible version in the future
+* `START`: opens a work session with a local timezone timestamp
+* `STOP`: closes a work session with a local timezone timestamp
+* `INVOICE`: mark the previous section of sessions as "billed". This
+ is useful if a project is long-running and you want to bill parts of
+ the work that you have been doing.
+
+It is recommended not to write this file by hand, although you
+definitely can. Lines starting with `;;` are comments and will be
+ignored. Careful though: they will be stripped from the file next
+time that cassiopeia writes it out. Comment pass-through is on the
+roadmap, but not yet implemented!
+
+`cass(1)` comes with a few commands
+
+- `start` will start tracking for an account
+- `stop` will stop tracking and error if no open slot exists
+- `invoice` will add an invoice block to the time file
+
+All commands require a `-f` (or `--file`) parameter to know which time
+file they are operating on. If none is provided, the `time.cass` file
+in the current working directory is tried. If this does not exist,
+the program exits with an error.
+
+By default time values are rounded to the next 15 minutes. To disable
+this, pass `-r` to any command.
+
+
+## Interaction with invoice
+
+cassiopeia is designed to output `.yml` configuration that can be used
+to generate invoices for your clients. For this, it needs access to
+your client database, and uses the `client` and `project` keys in your
+time file header to look them up. If none are present, you can pass
+this data to the `invoice` command manually with `--client` and
+`--project`. You provide your client database path with `--client-db`
+
+The following command will generate an invoice for a project done for
+the `ACME inc.` client:
+
+```
+$ cass invoice --client-db /home/office/clients.yml --client acme --project world-domination
+```
+
+Contrutions welcome via my [public inbox]
+
+[public-inbox]: https://lists.sr.ht/~spacekookie/public-inbox
diff --git a/apps/cassiopeia/src/data.rs b/apps/cassiopeia/src/data.rs
index 8ebc67f016c5..3911345109ca 100644
--- a/apps/cassiopeia/src/data.rs
+++ b/apps/cassiopeia/src/data.rs
@@ -5,25 +5,79 @@
//! analysis tasks.
use crate::format::LineCfg;
-use chrono::{Date, DateTime, FixedOffset as Offset};
+use chrono::{DateTime, Duration, FixedOffset as Offset, NaiveDate};
use std::collections::BTreeMap;
-#[derive(Default)]
+#[derive(Debug, Default)]
pub struct TimeFile {
+ /// Raw line buffers to echo back into the file
+ lines: Vec<LineCfg>,
+ /// A parsed header structure
header: BTreeMap<String, String>,
+ /// A parsed session structure
sessions: Vec<Session>,
- invoices: Vec<Date<Offset>>,
+ /// A parsed invoice list
+ invoices: Vec<Invoice>,
}
impl TimeFile {
- pub(crate) fn append(self, line: LineCfg) -> Self {
- println!("{:?}", line);
+ pub(crate) fn append(mut self, line: LineCfg) -> Self {
+ let lo = self.lines.len();
+ match line {
+ LineCfg::Header(ref header) => self.header = header.clone(),
+ LineCfg::Start(Some(time)) => self.sessions.push(Session::start(time, lo)),
+ LineCfg::Stop(Some(time)) => self.get_last_session().stop(time, lo),
+ LineCfg::Invoice(Some(date)) => self.invoices.push(Invoice::new(date, lo)),
+ _ => {}
+ }
+ self.lines.push(line);
self
}
+
+ fn get_last_session(&mut self) -> &mut Session {
+ self.sessions.last_mut().unwrap()
+ }
}
+#[derive(Debug)]
pub struct Session {
start: DateTime<Offset>,
- stop: DateTime<Offset>,
+ stop: Option<DateTime<Offset>>,
+ /// Track the lines this session took place in
+ lines: (usize, usize),
+}
+
+impl Session {
+ /// Create a new session with a start time
+ fn start(start: DateTime<Offset>, line: usize) -> Self {
+ Self {
+ start,
+ stop: None,
+ lines: (line, 0),
+ }
+ }
+
+ /// Finalise a session with a stop time
+ fn stop(&mut self, stop: DateTime<Offset>, line: usize) {
+ self.stop = Some(stop);
+ self.lines.1 = line;
+ }
+
+ /// Get the length of the session, if it was already finished
+ pub fn length(&self) -> Option<Duration> {
+ self.stop.map(|stop| stop - self.start)
+ }
+}
+
+#[derive(Debug)]
+pub struct Invoice {
+ date: NaiveDate,
+ line: usize,
+}
+
+impl Invoice {
+ fn new(date: NaiveDate, line: usize) -> Self {
+ Self { date, line }
+ }
}
diff --git a/apps/cassiopeia/src/format/mod.rs b/apps/cassiopeia/src/format/mod.rs
index beab2f7aac66..bac0445d8387 100644
--- a/apps/cassiopeia/src/format/mod.rs
+++ b/apps/cassiopeia/src/format/mod.rs
@@ -9,7 +9,10 @@ pub(crate) use parser::LineCfg;
use crate::TimeFile;
use std::{fs::File, io::Read};
-pub(crate) fn load_file(path: &str) {
+/// The cassiopeia parser/generator version to be written back into the file
+pub const CASS_VERSION: &str = env!("CARGO_PKG_VERSION");
+
+pub(crate) fn load_file(path: &str) -> TimeFile {
let mut f = File::open(path).unwrap();
let mut content = String::new();
f.read_to_string(&mut content).unwrap();
@@ -21,5 +24,5 @@ pub(crate) fn load_file(path: &str) {
.map(|line| lexer::lex(line))
.map(|lex| parser::parse(lex))
.filter(|line| line.valid())
- .fold(TimeFile::default(), |file, line| file.append(line));
+ .fold(TimeFile::default(), |file, line| file.append(line))
}
diff --git a/apps/cassiopeia/src/format/parser.rs b/apps/cassiopeia/src/format/parser.rs
index cc4b1b7c77df..bb2c56be0f33 100644
--- a/apps/cassiopeia/src/format/parser.rs
+++ b/apps/cassiopeia/src/format/parser.rs
@@ -4,8 +4,7 @@
//! parsed time file.
use crate::format::{LineLexer, LineToken, Token};
-use chrono::{DateTime, FixedOffset as Offset};
-use logos::Lexer;
+use chrono::{NaiveDate, DateTime, FixedOffset as Offset};
use std::collections::BTreeMap;
use std::iter::Iterator;
@@ -19,9 +18,7 @@ pub enum LineCfg {
/// A session stop line with a date and time
Stop(Option<DateTime<Offset>>),
/// An invoice line with a date
- Invoice(Option<DateTime<Offset>>),
- /// An empty line
- Empty,
+ Invoice(Option<NaiveDate>),
/// A temporary value that is invalid
#[doc(hidden)]
Ignore,
@@ -39,7 +36,7 @@ impl LineCfg {
pub(crate) fn parse<'l>(lex: LineLexer<'l>) -> LineCfg {
use LineCfg::*;
use Token as T;
-
+
#[cfg_attr(rustfmt, rustfmt_skip)]
lex.get_all().into_iter().fold(Ignore, |cfg, tok| match (cfg, tok) {
// If the first token is a comment, we ignore it
@@ -52,13 +49,10 @@ pub(crate) fn parse<'l>(lex: LineLexer<'l>) -> LineCfg {
// If the first token _was_ a keyword, fill in the data
(Header(map), LineToken { tt: T::HeaderData, slice }) => Header(append_data(map, slice)),
- (Start(_), LineToken { tt: T::Date, slice }) => Start(parse_date(slice)),
- (Stop(_), LineToken { tt: T::Date, slice }) => Stop(parse_date(slice)),
+ (Start(_), LineToken { tt: T::Date, slice }) => Start(parse_datetime(slice)),
+ (Stop(_), LineToken { tt: T::Date, slice }) => Stop(parse_datetime(slice)),
(Invoice(_), LineToken { tt: T::Date, slice }) => Invoice(parse_date(slice)),
- // Pass empty lines through,
- (Empty, _) => Empty,
-
// Ignore everything else (which will be filtered)
_ => Ignore,
})
@@ -70,9 +64,13 @@ fn append_data(mut map: BTreeMap<String, String>, slice: &str) -> BTreeMap<Strin
map
}
-fn parse_date(slice: &str) -> Option<DateTime<Offset>> {
+fn parse_datetime(slice: &str) -> Option<DateTime<Offset>> {
Some(
DateTime::parse_from_str(slice, "%Y-%m-%d %H:%M:%S%:z")
.expect("Failed to parse date; invalid format!"),
)
}
+
+fn parse_date(slice: &str) -> Option<NaiveDate> {
+ Some(NaiveDate::parse_from_str(slice, "%Y-%m-%d").expect("Failed to parse date; invalid format!"))
+}
diff --git a/apps/cassiopeia/src/main.rs b/apps/cassiopeia/src/main.rs
index b28f3c3438f7..ff942731c3d5 100644
--- a/apps/cassiopeia/src/main.rs
+++ b/apps/cassiopeia/src/main.rs
@@ -4,5 +4,6 @@ mod data;
pub use data::{TimeFile, Session};
fn main() {
- format::load_file("/home/projects/clients/nyantec-nix-workshops/time.cass")
+ let file = format::load_file("/home/projects/clients/nyantec-nix-workshops/time.cass");
+
}