diff --git a/Cargo.lock b/Cargo.lock index 588231d6..08d193f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -109,6 +109,8 @@ name = "atuin" version = "0.2.4" dependencies = [ "chrono", + "chrono-english", + "cli-table", "directories", "eyre", "hostname", @@ -240,6 +242,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "chrono-english" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4233ee19352739cfdcb5d7c2085005b166f6170ef2845ed9eef27a8fa5f95206" +dependencies = [ + "chrono", + "scanlex", + "time", +] + [[package]] name = "clap" version = "2.33.3" @@ -255,6 +268,28 @@ dependencies = [ "vec_map", ] +[[package]] +name = "cli-table" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c568382da2369ef1fcbfc2665c6f93f1b6ec9caf585312d2034d2d2584ea68b9" +dependencies = [ + "cli-table-derive", + "termcolor", + "unicode-width", +] + +[[package]] +name = "cli-table-derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee3795f920d8cf38d4902e8bf4573e7aa9ba430e0144b5b5ee3ae4da34f819b" +dependencies = [ + "proc-macro2 1.0.24", + "quote 1.0.9", + "syn 1.0.60", +] + [[package]] name = "console" version = "0.14.0" @@ -1062,6 +1097,12 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" +[[package]] +name = "scanlex" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "088c5d71572124929ea7549a8ce98e1a6fd33d0a38367b09027b382e67c033db" + [[package]] name = "serde" version = "1.0.123" diff --git a/Cargo.toml b/Cargo.toml index 29d7df0e..7573abef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,8 @@ uuid = { version = "0.8", features = ["v4"] } indicatif = "0.15.0" hostname = "0.3.1" rocket = "0.4.7" +chrono-english = "0.1.4" +cli-table = "0.4" [dependencies.rusqlite] version = "0.24" diff --git a/src/command/import.rs b/src/command/import.rs index 5a91b6b7..88108400 100644 --- a/src/command/import.rs +++ b/src/command/import.rs @@ -96,16 +96,11 @@ fn import_zsh(db: &mut Sqlite) -> Result<()> { let buf_size = 100; let mut buf = Vec::::with_capacity(buf_size); - for i in zsh { - match i { - Ok(h) => { - buf.push(h); - } - Err(e) => { - error!("{}", e); - continue; - } - } + for i in zsh + .filter_map(Result::ok) + .filter(|x| !x.command.trim().is_empty()) + { + buf.push(i); if buf.len() == buf_size { db.save_bulk(&buf)?; diff --git a/src/command/mod.rs b/src/command/mod.rs index 2e8d4778..a5dd039e 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -7,6 +7,7 @@ use crate::local::database::Sqlite; mod history; mod import; mod server; +mod stats; #[derive(StructOpt)] pub enum AtuinCmd { @@ -22,6 +23,9 @@ pub enum AtuinCmd { #[structopt(about = "start an atuin server")] Server(server::Cmd), + #[structopt(about = "calculate statistics for your history")] + Stats(stats::Cmd), + #[structopt(about = "generates a UUID")] Uuid, } @@ -36,6 +40,7 @@ impl AtuinCmd { Self::History(history) => history.run(db), Self::Import(import) => import.run(db), Self::Server(server) => server.run(), + Self::Stats(stats) => stats.run(db), Self::Uuid => { println!("{}", uuid_v4()); diff --git a/src/command/stats.rs b/src/command/stats.rs new file mode 100644 index 00000000..ea5893f9 --- /dev/null +++ b/src/command/stats.rs @@ -0,0 +1,101 @@ +use std::collections::HashMap; + +use chrono::prelude::*; +use chrono::{Duration, Utc}; +use chrono_english::{parse_date_string, Dialect}; + +use cli_table::{format::Justify, print_stdout, Cell, Style, Table}; +use eyre::{eyre, Result}; +use structopt::StructOpt; + +use crate::local::database::{Database, Sqlite}; +use crate::local::history::History; + +#[derive(StructOpt)] +pub enum Cmd { + #[structopt( + about="compute statistics for all of time", + aliases=&["d", "da"], + )] + All, + + #[structopt( + about="compute statistics for a single day", + aliases=&["d", "da"], + )] + Day { words: Vec }, +} + +fn compute_stats(history: &[History]) -> Result<()> { + let mut commands = HashMap::::new(); + + for i in history { + *commands.entry(i.command.clone()).or_default() += 1; + } + + let most_common_command = commands.iter().max_by(|a, b| a.1.cmp(b.1)); + + if most_common_command.is_none() { + return Err(eyre!("No commands found")); + } + + let table = vec![ + vec![ + "Most used command".cell(), + most_common_command + .unwrap() + .0 + .cell() + .justify(Justify::Right), + ], + vec![ + "Commands ran".cell(), + history.len().to_string().cell().justify(Justify::Right), + ], + vec![ + "Unique commands ran".cell(), + commands.len().to_string().cell().justify(Justify::Right), + ], + ] + .table() + .title(vec![ + "Statistic".cell().bold(true), + "Value".cell().bold(true), + ]) + .bold(true); + + print_stdout(table)?; + + Ok(()) +} + +impl Cmd { + pub fn run(&self, db: &mut Sqlite) -> Result<()> { + match self { + Self::Day { words } => { + let words = if words.is_empty() { + String::from("yesterday") + } else { + words.join(" ") + }; + + let start = parse_date_string(words.as_str(), Local::now(), Dialect::Us)?; + let end = start + Duration::days(1); + + let history = db.range(start.with_timezone(&Utc), end.with_timezone(&Utc))?; + + compute_stats(&history)?; + + Ok(()) + } + + Self::All => { + let history = db.list()?; + + compute_stats(&history)?; + + Ok(()) + } + } + } +} diff --git a/src/local/database.rs b/src/local/database.rs index 8e4b00ef..5b98bb36 100644 --- a/src/local/database.rs +++ b/src/local/database.rs @@ -13,7 +13,8 @@ pub trait Database { fn save_bulk(&mut self, h: &[History]) -> Result<()>; fn load(&self, id: &str) -> Result; fn list(&self) -> Result>; - fn since(&self, date: chrono::DateTime) -> Result>; + fn range(&self, from: chrono::DateTime, to: chrono::DateTime) + -> Result>; fn update(&self, h: &History) -> Result<()>; } @@ -157,16 +158,21 @@ impl Database for Sqlite { Ok(history_iter.filter_map(Result::ok).collect()) } - fn since(&self, date: chrono::DateTime) -> Result> { - debug!("listing history since {:?}", date); + fn range( + &self, + from: chrono::DateTime, + to: chrono::DateTime, + ) -> Result> { + debug!("listing history from {:?} to {:?}", from, to); let mut stmt = self.conn.prepare( - "SELECT distinct command FROM history where timestamp > ?1 order by timestamp asc", + "SELECT * FROM history where timestamp >= ?1 and timestamp <= ?2 order by timestamp asc", )?; - let history_iter = stmt.query_map(params![date.timestamp_nanos()], |row| { - history_from_sqlite_row(None, row) - })?; + let history_iter = stmt.query_map( + params![from.timestamp_nanos(), to.timestamp_nanos()], + |row| history_from_sqlite_row(None, row), + )?; Ok(history_iter.filter_map(Result::ok).collect()) }