summaryrefslogtreecommitdiffstats
path: root/src
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 /src
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
Diffstat (limited to 'src')
-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
4 files changed, 124 insertions, 17 deletions
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())
}