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:
parent
6636f5878a
commit
851285225f
6 changed files with 167 additions and 17 deletions
41
Cargo.lock
generated
41
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)?;
|
||||
|
|
|
@ -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());
|
||||
|
|
101
src/command/stats.rs
Normal file
101
src/command/stats.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue