diff options
author | Ellie Huxtable <ellie@elliehuxtable.com> | 2024-01-12 16:02:08 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-01-12 16:02:08 +0000 |
commit | 99249ea319fca96ace8f3f4962534dc7a4bc5923 (patch) | |
tree | 61103701633c6e4afec21d1108faddbc20e90d9e /atuin-client | |
parent | a60d8934c5dfcf0679dfa5f4de5c2080dfd32a5f (diff) |
feat: Add interactive command inspector (#1296)
* Begin work on command inspector
This is a separate pane in the interactive mode that allows for
exploration and inspecting of specific commands.
I've restructured things a bit. It made logical sense that things
were nested under commands, however the whole point of `atuin` is to
provide commands. Breaking things out like this enables a bit less
crazy nesting as we add more functionality to things like interactive
search. I'd like to add a few more interactive things and it was
starting to feel very cluttered
* Some vague tab things
* functioning inspector with stats
* add interactive delete to inspector
* things
* clippy
* borders
* sus
* revert restructure for another pr
* Revert "sus"
This reverts commit d4bae8cf614d93b728621f7985cf4e387b6dc113.
Diffstat (limited to 'atuin-client')
-rw-r--r-- | atuin-client/src/database.rs | 121 | ||||
-rw-r--r-- | atuin-client/src/history.rs | 20 |
2 files changed, 141 insertions, 0 deletions
diff --git a/atuin-client/src/database.rs b/atuin-client/src/database.rs index 6a6f59911..376c7b753 100644 --- a/atuin-client/src/database.rs +++ b/atuin-client/src/database.rs @@ -18,6 +18,8 @@ use sqlx::{ }; use time::OffsetDateTime; +use crate::history::HistoryStats; + use super::{ history::History, ordering, @@ -109,6 +111,8 @@ pub trait Database: Send + Sync + 'static { async fn query_history(&self, query: &str) -> Result<Vec<History>>; async fn all_with_count(&self) -> Result<Vec<(History, i32)>>; + + async fn stats(&self, h: &History) -> Result<HistoryStats>; } // Intended for use on a developer machine and not a sync server. @@ -562,6 +566,123 @@ impl Database for Sqlite { Ok(()) } + + async fn stats(&self, h: &History) -> Result<HistoryStats> { + // We select the previous in the session by time + let mut prev = SqlBuilder::select_from("history"); + prev.field("*") + .and_where("timestamp < ?1") + .and_where("session = ?2") + .order_by("timestamp", true) + .limit(1); + + let mut next = SqlBuilder::select_from("history"); + next.field("*") + .and_where("timestamp > ?1") + .and_where("session = ?2") + .order_by("timestamp", false) + .limit(1); + + let mut total = SqlBuilder::select_from("history"); + total.field("count(1)").and_where("command = ?1"); + + let mut average = SqlBuilder::select_from("history"); + average.field("avg(duration)").and_where("command = ?1"); + + let mut exits = SqlBuilder::select_from("history"); + exits + .fields(&["exit", "count(1) as count"]) + .and_where("command = ?1") + .group_by("exit"); + + // rewrite the following with sqlbuilder + let mut day_of_week = SqlBuilder::select_from("history"); + day_of_week + .fields(&[ + "strftime('%w', ROUND(timestamp / 1000000000), 'unixepoch') AS day_of_week", + "count(1) as count", + ]) + .and_where("command = ?1") + .group_by("day_of_week"); + + // Intentionally format the string with 01 hardcoded. We want the average runtime for the + // _entire month_, but will later parse it as a datetime for sorting + // Sqlite has no datetime so we cannot do it there, and otherwise sorting will just be a + // string sort, which won't be correct. + let mut duration_over_time = SqlBuilder::select_from("history"); + duration_over_time + .fields(&[ + "strftime('01-%m-%Y', ROUND(timestamp / 1000000000), 'unixepoch') AS month_year", + "avg(duration) as duration", + ]) + .and_where("command = ?1") + .group_by("month_year") + .having("duration > 0"); + + let prev = prev.sql().expect("issue in stats previous query"); + let next = next.sql().expect("issue in stats next query"); + let total = total.sql().expect("issue in stats average query"); + let average = average.sql().expect("issue in stats previous query"); + let exits = exits.sql().expect("issue in stats exits query"); + let day_of_week = day_of_week.sql().expect("issue in stats day of week query"); + let duration_over_time = duration_over_time + .sql() + .expect("issue in stats duration over time query"); + + let prev = sqlx::query(&prev) + .bind(h.timestamp.unix_timestamp_nanos() as i64) + .bind(&h.session) + .map(Self::query_history) + .fetch_optional(&self.pool) + .await?; + + let next = sqlx::query(&next) + .bind(h.timestamp.unix_timestamp_nanos() as i64) + .bind(&h.session) + .map(Self::query_history) + .fetch_optional(&self.pool) + .await?; + + let total: (i64,) = sqlx::query_as(&total) + .bind(&h.command) + .fetch_one(&self.pool) + .await?; + + let average: (f64,) = sqlx::query_as(&average) + .bind(&h.command) + .fetch_one(&self.pool) + .await?; + + let exits: Vec<(i64, i64)> = sqlx::query_as(&exits) + .bind(&h.command) + .fetch_all(&self.pool) + .await?; + + let day_of_week: Vec<(String, i64)> = sqlx::query_as(&day_of_week) + .bind(&h.command) + .fetch_all(&self.pool) + .await?; + + let duration_over_time: Vec<(String, f64)> = sqlx::query_as(&duration_over_time) + .bind(&h.command) + .fetch_all(&self.pool) + .await?; + + let duration_over_time = duration_over_time + .iter() + .map(|f| (f.0.clone(), f.1.round() as i64)) + .collect(); + + Ok(HistoryStats { + next, + previous: prev, + total: total.0 as u64, + average_duration: average.0 as u64, + exits, + day_of_week, + duration_over_time, + }) + } } #[cfg(test)] diff --git a/atuin-client/src/history.rs b/atuin-client/src/history.rs index 8c312dc21..0147e25bc 100644 --- a/atuin-client/src/history.rs +++ b/atuin-client/src/history.rs @@ -71,6 +71,26 @@ pub struct History { pub deleted_at: Option<OffsetDateTime>, } +#[derive(Debug, Clone, PartialEq, Eq, sqlx::FromRow)] +pub struct HistoryStats { + /// The command that was ran after this one in the session + pub next: Option<History>, + /// + /// The command that was ran before this one in the session + pub previous: Option<History>, + + /// How many times has this command been ran? + pub total: u64, + + pub average_duration: u64, + + pub exits: Vec<(i64, i64)>, + + pub day_of_week: Vec<(String, i64)>, + + pub duration_over_time: Vec<(String, i64)>, +} + impl History { #[allow(clippy::too_many_arguments)] fn new( |