aboutsummaryrefslogtreecommitdiffstats
path: root/ui/backend/src/db.rs
diff options
context:
space:
mode:
Diffstat (limited to 'ui/backend/src/db.rs')
-rw-r--r--ui/backend/src/db.rs245
1 files changed, 245 insertions, 0 deletions
diff --git a/ui/backend/src/db.rs b/ui/backend/src/db.rs
new file mode 100644
index 00000000..c1aa4de9
--- /dev/null
+++ b/ui/backend/src/db.rs
@@ -0,0 +1,245 @@
+// Some wrappers around the Atuin history DB
+// I'll probably use this to inform changes to the "upstream" client crate
+// We also use Strings a bunch for errors. They're passed to the Tauri frontend,
+// which requires that they be serializable.
+// Can rework that in the future too, but my main concern is avoiding tauri limitations/reqs
+// ending up in the main crate.
+
+use serde::Serialize;
+use sqlx::{sqlite::SqliteRow, Row};
+use std::collections::HashMap;
+use std::path::PathBuf;
+
+use atuin_client::settings::{FilterMode, SearchMode};
+use atuin_client::{
+ database::{Context, Database, OptFilters, Sqlite},
+ history::History,
+};
+
+// useful for preprocessing data for the frontend
+#[derive(Serialize, Debug)]
+pub struct NameValue<T> {
+ pub name: String,
+ pub value: T,
+}
+
+#[derive(Serialize, Debug)]
+pub struct GlobalStats {
+ pub total_history: u64,
+
+ pub daily: Vec<NameValue<u64>>,
+
+ pub last_1d: u64,
+ pub last_7d: u64,
+ pub last_30d: u64,
+}
+
+#[derive(Serialize)]
+pub struct UIHistory {
+ pub id: String,
+ /// When the command was run.
+ pub timestamp: i128,
+ /// How long the command took to run.
+ pub duration: i64,
+ /// The exit code of the command.
+ pub exit: i64,
+ /// The command that was run.
+ pub command: String,
+ /// The current working directory when the command was run.
+ pub cwd: String,
+ /// The session ID, associated with a terminal session.
+ pub session: String,
+ /// The hostname of the machine the command was run on.
+ pub user: String,
+
+ pub host: String,
+}
+
+pub fn to_ui_history(history: History) -> UIHistory {
+ let parts: Vec<String> = history.hostname.split(':').map(str::to_string).collect();
+
+ let (host, user) = if parts.len() == 2 {
+ (parts[0].clone(), parts[1].clone())
+ } else {
+ ("no-host".to_string(), "no-user".to_string())
+ };
+
+ let mac = format!("/Users/{}", user);
+ let linux = format!("/home/{}", user);
+
+ let cwd = history.cwd.replace(mac.as_str(), "~");
+ let cwd = cwd.replace(linux.as_str(), "~");
+
+ UIHistory {
+ id: history.id.0,
+ timestamp: history.timestamp.unix_timestamp_nanos(),
+ duration: history.duration,
+ exit: history.exit,
+ command: history.command,
+ session: history.session,
+ host,
+ user,
+ cwd,
+ }
+}
+
+pub struct HistoryDB(Sqlite);
+
+impl HistoryDB {
+ pub async fn new(path: PathBuf, timeout: f64) -> Result<Self, String> {
+ let sqlite = Sqlite::new(path, timeout)
+ .await
+ .map_err(|e| e.to_string())?;
+
+ Ok(Self(sqlite))
+ }
+
+ pub async fn list(&self, limit: Option<usize>, unique: bool) -> Result<Vec<UIHistory>, String> {
+ let filters = vec![];
+
+ // bit of a hack but provide an empty context
+ // shell context makes _no sense_ in a GUI
+ let context = Context {
+ session: "".to_string(),
+ cwd: "".to_string(),
+ host_id: "".to_string(),
+ hostname: "".to_string(),
+ git_root: None,
+ };
+
+ let history = self
+ .0
+ .list(&filters, &context, limit, unique, false)
+ .await
+ .map_err(|e| e.to_string())?;
+
+ let history = history
+ .into_iter()
+ .filter(|h| h.duration > 0)
+ .map(to_ui_history)
+ .collect();
+
+ Ok(history)
+ }
+
+ pub async fn search(&self, query: &str) -> Result<Vec<UIHistory>, String> {
+ let context = Context {
+ session: "".to_string(),
+ cwd: "".to_string(),
+ host_id: "".to_string(),
+ hostname: "".to_string(),
+ git_root: None,
+ };
+
+ let filters = OptFilters {
+ limit: Some(200),
+ ..OptFilters::default()
+ };
+
+ let history = self
+ .0
+ .search(
+ SearchMode::Fuzzy,
+ FilterMode::Global,
+ &context,
+ query,
+ filters,
+ )
+ .await
+ .map_err(|e| e.to_string())?;
+
+ let history = history
+ .into_iter()
+ .filter(|h| h.duration > 0)
+ .map(to_ui_history)
+ .collect();
+
+ Ok(history)
+ }
+
+ pub async fn global_stats(&self) -> Result<GlobalStats, String> {
+ let day_ago = time::OffsetDateTime::now_utc() - time::Duration::days(1);
+ let day_ago = day_ago.unix_timestamp_nanos();
+
+ let week_ago = time::OffsetDateTime::now_utc() - time::Duration::days(7);
+ let week_ago = week_ago.unix_timestamp_nanos();
+
+ let month_ago = time::OffsetDateTime::now_utc() - time::Duration::days(30);
+ let month_ago = month_ago.unix_timestamp_nanos();
+
+ // get the last 30 days of shell history
+ let history: Vec<UIHistory> = sqlx::query("SELECT * FROM history WHERE timestamp > ?")
+ .bind(month_ago as i64)
+ .map(|row: SqliteRow| {
+ History::from_db()
+ .id(row.get("id"))
+ .timestamp(
+ time::OffsetDateTime::from_unix_timestamp_nanos(
+ row.get::<i64, _>("timestamp") as i128,
+ )
+ .unwrap(),
+ )
+ .duration(row.get("duration"))
+ .exit(row.get("exit"))
+ .command(row.get("command"))
+ .cwd(row.get("cwd"))
+ .session(row.get("session"))
+ .hostname(row.get("hostname"))
+ .deleted_at(None)
+ .build()
+ .into()
+ })
+ .map(to_ui_history)
+ .fetch_all(&self.0.pool)
+ .await
+ .map_err(|e| e.to_string())?;
+
+ let total: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM history")
+ .fetch_one(&self.0.pool)
+ .await
+ .map_err(|e| e.to_string())?;
+
+ let mut day = 0;
+ let mut week = 0;
+ let mut month = 0;
+
+ let mut daily = HashMap::new();
+ let ymd = time::format_description::parse("[year]-[month]-[day]").unwrap();
+
+ for i in history {
+ if i.timestamp > day_ago {
+ day += 1;
+ }
+
+ if i.timestamp > week_ago {
+ week += 1;
+ }
+
+ if i.timestamp > month_ago {
+ month += 1;
+
+ // get the start of the day, as a unix timestamp
+ let date = time::OffsetDateTime::from_unix_timestamp_nanos(i.timestamp)
+ .unwrap()
+ .format(&ymd)
+ .unwrap();
+
+ daily.entry(date).and_modify(|v| *v += 1).or_insert(1);
+ }
+ }
+
+ let mut daily: Vec<NameValue<u64>> = daily
+ .into_iter()
+ .map(|(k, v)| NameValue { name: k, value: v })
+ .collect();
+ daily.sort_by(|a, b| a.name.cmp(&b.name));
+
+ Ok(GlobalStats {
+ total_history: total.0 as u64,
+ last_30d: month,
+ last_7d: week,
+ last_1d: day,
+ daily,
+ })
+ }
+}