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"
|
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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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)?;
|
||||||
|
|
|
@ -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
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 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())
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue