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
This commit is contained in:
Ellie Huxtable 2021-02-14 22:12:35 +00:00 committed by GitHub
parent 6636f5878a
commit 851285225f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 167 additions and 17 deletions

41
Cargo.lock generated
View file

@ -109,6 +109,8 @@ name = "atuin"
version = "0.2.4" version = "0.2.4"
dependencies = [ dependencies = [
"chrono", "chrono",
"chrono-english",
"cli-table",
"directories", "directories",
"eyre", "eyre",
"hostname", "hostname",
@ -240,6 +242,17 @@ dependencies = [
"winapi", "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]] [[package]]
name = "clap" name = "clap"
version = "2.33.3" version = "2.33.3"
@ -255,6 +268,28 @@ dependencies = [
"vec_map", "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]] [[package]]
name = "console" name = "console"
version = "0.14.0" version = "0.14.0"
@ -1062,6 +1097,12 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
[[package]]
name = "scanlex"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "088c5d71572124929ea7549a8ce98e1a6fd33d0a38367b09027b382e67c033db"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.123" version = "1.0.123"

View file

@ -18,6 +18,8 @@ uuid = { version = "0.8", features = ["v4"] }
indicatif = "0.15.0" indicatif = "0.15.0"
hostname = "0.3.1" hostname = "0.3.1"
rocket = "0.4.7" rocket = "0.4.7"
chrono-english = "0.1.4"
cli-table = "0.4"
[dependencies.rusqlite] [dependencies.rusqlite]
version = "0.24" version = "0.24"

View file

@ -96,16 +96,11 @@ fn import_zsh(db: &mut Sqlite) -> Result<()> {
let buf_size = 100; let buf_size = 100;
let mut buf = Vec::<History>::with_capacity(buf_size); let mut buf = Vec::<History>::with_capacity(buf_size);
for i in zsh { for i in zsh
match i { .filter_map(Result::ok)
Ok(h) => { .filter(|x| !x.command.trim().is_empty())
buf.push(h); {
} buf.push(i);
Err(e) => {
error!("{}", e);
continue;
}
}
if buf.len() == buf_size { if buf.len() == buf_size {
db.save_bulk(&buf)?; db.save_bulk(&buf)?;

View file

@ -7,6 +7,7 @@ use crate::local::database::Sqlite;
mod history; mod history;
mod import; mod import;
mod server; mod server;
mod stats;
#[derive(StructOpt)] #[derive(StructOpt)]
pub enum AtuinCmd { pub enum AtuinCmd {
@ -22,6 +23,9 @@ pub enum AtuinCmd {
#[structopt(about = "start an atuin server")] #[structopt(about = "start an atuin server")]
Server(server::Cmd), Server(server::Cmd),
#[structopt(about = "calculate statistics for your history")]
Stats(stats::Cmd),
#[structopt(about = "generates a UUID")] #[structopt(about = "generates a UUID")]
Uuid, Uuid,
} }
@ -36,6 +40,7 @@ impl AtuinCmd {
Self::History(history) => history.run(db), Self::History(history) => history.run(db),
Self::Import(import) => import.run(db), Self::Import(import) => import.run(db),
Self::Server(server) => server.run(), Self::Server(server) => server.run(),
Self::Stats(stats) => stats.run(db),
Self::Uuid => { Self::Uuid => {
println!("{}", uuid_v4()); println!("{}", uuid_v4());

101
src/command/stats.rs Normal file
View file

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

View file

@ -13,7 +13,8 @@ pub trait Database {
fn save_bulk(&mut self, h: &[History]) -> Result<()>; fn save_bulk(&mut self, h: &[History]) -> Result<()>;
fn load(&self, id: &str) -> Result<History>; fn load(&self, id: &str) -> Result<History>;
fn list(&self) -> Result<Vec<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<()>; fn update(&self, h: &History) -> Result<()>;
} }
@ -157,16 +158,21 @@ impl Database for Sqlite {
Ok(history_iter.filter_map(Result::ok).collect()) Ok(history_iter.filter_map(Result::ok).collect())
} }
fn since(&self, date: chrono::DateTime<Utc>) -> Result<Vec<History>> { fn range(
debug!("listing history since {:?}", date); &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( 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| { let history_iter = stmt.query_map(
history_from_sqlite_row(None, row) params![from.timestamp_nanos(), to.timestamp_nanos()],
})?; |row| history_from_sqlite_row(None, row),
)?;
Ok(history_iter.filter_map(Result::ok).collect()) Ok(history_iter.filter_map(Result::ok).collect())
} }