summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEllie Huxtable <e@elm.sh>2021-02-14 22:12:35 +0000
committerGitHub <noreply@github.com>2021-02-14 22:12:35 +0000
commit851285225fce83bd63410d44e106df0c2a4a4733 (patch)
tree943681c16d49a464dbc1ac80441a37572c271b47
parent6636f5878ac11d6461b9958af025021486a7d58f (diff)
Add stats command (#9)
* Add stats command For example atuin stats day yesterday atuin stats day last friday atuin stats day 01/01/21 * Output tables, fix import blanks
-rw-r--r--Cargo.lock41
-rw-r--r--Cargo.toml2
-rw-r--r--src/command/import.rs15
-rw-r--r--src/command/mod.rs5
-rw-r--r--src/command/stats.rs101
-rw-r--r--src/local/database.rs20
6 files changed, 167 insertions, 17 deletions
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",
@@ -241,6 +243,17 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -256,6 +269,28 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1063,6 +1098,12 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
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::<History>::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<String> },
+}
+
+fn compute_stats(history: &[History]) -> Result<()> {
+ let mut commands = HashMap::<String, i64>::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<History>;
fn list(&self) -> Result<Vec<History>>;
- fn since(&self, date: chrono::DateTime<Utc>) -> Result<Vec<History>>;
+ fn range(&self, from: chrono::DateTime<Utc>, to: chrono::DateTime<Utc>)
+ -> Result<Vec<History>>;
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<Utc>) -> Result<Vec<History>> {
- debug!("listing history since {:?}", date);
+ fn range(
+ &self,
+ from: chrono::DateTime<Utc>,
+ to: chrono::DateTime<Utc>,
+ ) -> Result<Vec<History>> {
+ 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())
}