diff --git a/Cargo.lock b/Cargo.lock index fcf1175..bd8b360 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -49,12 +49,13 @@ dependencies = [ [[package]] name = "atuin" -version = "0.2.0" +version = "0.2.1" dependencies = [ "chrono", "directories", "eyre", "home", + "hostname", "indicatif", "log", "pretty_env_logger", @@ -314,6 +315,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi", +] + [[package]] name = "humantime" version = "1.3.0" @@ -373,6 +385,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + [[package]] name = "memchr" version = "2.3.4" diff --git a/Cargo.toml b/Cargo.toml index 57117c2..c4b67a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "atuin" -version = "0.2.0" +version = "0.2.1" authors = ["Ellie Huxtable "] edition = "2018" license = "MIT" @@ -17,6 +17,7 @@ directories = "3.*" uuid = { version = "0.8", features = ["serde", "v4"] } home = "0.5.3" indicatif = "0.15.0" +hostname = "0.3.1" [dependencies.rusqlite] version = "0.24.*" diff --git a/README.md b/README.md index 030af67..6739500 100644 --- a/README.md +++ b/README.md @@ -5,4 +5,15 @@ Through the fathomless deeps of space swims the star turtle Great A’Tuin, bearing on its back the four giant elephants who carry on their shoulders the mass of the Discworld. - `atuin` manages and synchronizes your shell history! +`atuin` manages and synchronizes your shell history! Instead of storing +everything in a text file (such as ~/.history), `atuin` uses a sqlite database. +This lets us do all kinds of analysis on it! + +As well as the expected command, this stores + +- duration +- exit code +- working directory +- hostname +- time +- a unique session ID diff --git a/src/command/history.rs b/src/command/history.rs index 72f821c..4caf4c1 100644 --- a/src/command/history.rs +++ b/src/command/history.rs @@ -32,13 +32,21 @@ pub enum HistoryCmd { } impl HistoryCmd { - pub fn run(&self, db: SqliteDatabase) -> Result<()> { + pub fn run(&self, db: &mut SqliteDatabase) -> Result<()> { match self { HistoryCmd::Start { command: words } => { let command = words.join(" "); let cwd = env::current_dir()?.display().to_string(); - let h = History::new(chrono::Utc::now().timestamp_nanos(), command, cwd, -1, -1); + let h = History::new( + chrono::Utc::now().timestamp_nanos(), + command, + cwd, + -1, + -1, + None, + None, + ); // print the ID // we use this as the key for calling end diff --git a/src/command/import.rs b/src/command/import.rs index 5ece13a..5ce0e74 100644 --- a/src/command/import.rs +++ b/src/command/import.rs @@ -87,12 +87,23 @@ impl ImportCmd { } pub fn run(&self, db: &mut SqliteDatabase) -> Result<()> { + println!(" A'Tuin "); + println!("====================="); + println!(" 🌍 "); + println!(" 🐘🐘🐘🐘 "); + println!(" 🐒 "); + println!("====================="); + println!("Importing history..."); + match self { ImportCmd::Auto => { let shell = env::var("SHELL").unwrap_or(String::from("NO_SHELL")); match shell.as_str() { - "/bin/zsh" => self.import_zsh(db), + "/bin/zsh" => { + println!("Detected ZSH"); + self.import_zsh(db) + } _ => { println!("cannot import {} history", shell); diff --git a/src/local/database.rs b/src/local/database.rs index 2a4cc58..e9882fd 100644 --- a/src/local/database.rs +++ b/src/local/database.rs @@ -8,7 +8,7 @@ use rusqlite::{params, Connection}; use crate::History; pub trait Database { - fn save(&self, h: History) -> Result<()>; + fn save(&mut self, h: History) -> Result<()>; fn save_bulk(&mut self, h: &Vec) -> Result<()>; fn load(&self, id: &str) -> Result; fn list(&self) -> Result<()>; @@ -53,6 +53,8 @@ impl SqliteDatabase { exit integer not null, command text not null, cwd text not null, + session text not null, + hostname text not null, unique(timestamp, cwd, command) )", @@ -64,22 +66,11 @@ impl SqliteDatabase { } impl Database for SqliteDatabase { - fn save(&self, h: History) -> Result<()> { + fn save(&mut self, h: History) -> Result<()> { debug!("saving history to sqlite"); + let v = vec![h]; - self.conn.execute( - "insert or ignore into history ( - id, - timestamp, - duration, - exit, - command, - cwd - ) values (?1, ?2, ?3, ?4, ?5, ?6)", - params![h.id, h.timestamp, h.duration, h.exit, h.command, h.cwd], - )?; - - Ok(()) + self.save_bulk(&v) } fn save_bulk(&mut self, h: &Vec) -> Result<()> { @@ -95,9 +86,20 @@ impl Database for SqliteDatabase { duration, exit, command, - cwd - ) values (?1, ?2, ?3, ?4, ?5, ?6)", - params![i.id, i.timestamp, i.duration, i.exit, i.command, i.cwd], + cwd, + session, + hostname + ) values (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", + params![ + i.id, + i.timestamp, + i.duration, + i.exit, + i.command, + i.cwd, + i.session, + i.hostname + ], )?; } @@ -110,7 +112,7 @@ impl Database for SqliteDatabase { debug!("loading history item"); let mut stmt = self.conn.prepare( - "select id, timestamp, duration, exit, command, cwd from history + "select id, timestamp, duration, exit, command, cwd, session, hostname from history where id = ?1", )?; @@ -122,6 +124,8 @@ impl Database for SqliteDatabase { exit: row.get(3)?, command: row.get(4)?, cwd: row.get(5)?, + session: row.get(6)?, + hostname: row.get(7)?, }) })?; @@ -137,9 +141,9 @@ impl Database for SqliteDatabase { self.conn.execute( "update history - set timestamp = ?2, duration = ?3, exit = ?4, command = ?5, cwd = ?6 + set timestamp = ?2, duration = ?3, exit = ?4, command = ?5, cwd = ?6, session = ?7, hostname = ?8 where id = ?1", - params![h.id, h.timestamp, h.duration, h.exit, h.command, h.cwd], + params![h.id, h.timestamp, h.duration, h.exit, h.command, h.cwd, h.session, h.hostname], )?; Ok(()) @@ -148,9 +152,9 @@ impl Database for SqliteDatabase { fn list(&self) -> Result<()> { debug!("listing history"); - let mut stmt = self - .conn - .prepare("SELECT id, timestamp, duration, exit, command, cwd FROM history")?; + let mut stmt = self.conn.prepare( + "SELECT id, timestamp, duration, exit, command, cwd, session, hostname FROM history", + )?; let history_iter = stmt.query_map(params![], |row| { Ok(History { @@ -160,6 +164,8 @@ impl Database for SqliteDatabase { exit: row.get(3)?, command: row.get(4)?, cwd: row.get(5)?, + session: row.get(6)?, + hostname: row.get(7)?, }) })?; @@ -167,8 +173,8 @@ impl Database for SqliteDatabase { let h = h.unwrap(); println!( - "{} | {} | {} | {} | {}", - h.timestamp, h.cwd, h.duration, h.exit, h.command + "{} | {} | {} | {} | {} | {} | {}", + h.timestamp, h.hostname, h.session, h.cwd, h.duration, h.exit, h.command ); } diff --git a/src/local/history.rs b/src/local/history.rs index 0010962..bb8b912 100644 --- a/src/local/history.rs +++ b/src/local/history.rs @@ -1,4 +1,6 @@ -use chrono; +use std::env; + +use hostname; use uuid::Uuid; #[derive(Debug)] @@ -9,10 +11,32 @@ pub struct History { pub exit: i64, pub command: String, pub cwd: String, + pub session: String, + pub hostname: String, } impl History { - pub fn new(timestamp: i64, command: String, cwd: String, exit: i64, duration: i64) -> History { + pub fn new( + timestamp: i64, + command: String, + cwd: String, + exit: i64, + duration: i64, + session: Option, + hostname: Option, + ) -> History { + // get the current session or just generate a random string + let env_session = + env::var("ATUIN_SESSION").unwrap_or(Uuid::new_v4().to_simple().to_string()); + + // best attempt at getting the current hostname, or just unknown + let os_hostname = hostname::get().unwrap(); + let os_hostname = os_hostname.to_str().unwrap(); + let os_hostname = String::from(os_hostname); + + let session = session.unwrap_or(env_session); + let hostname = hostname.unwrap_or(os_hostname); + History { id: Uuid::new_v4().to_simple().to_string(), timestamp, @@ -20,6 +44,8 @@ impl History { cwd, exit, duration, + session, + hostname, } } } diff --git a/src/local/import.rs b/src/local/import.rs index ce141c5..0b23781 100644 --- a/src/local/import.rs +++ b/src/local/import.rs @@ -70,11 +70,13 @@ fn parse_extended(line: String) -> History { // use nanos, because why the hell not? we won't display them. History::new( - Utc.timestamp(time, 0).timestamp_nanos(), + time * 1_000_000_000, trim_newline(command), String::from("unknown"), -1, duration * 1_000_000_000, + None, + None, ) } @@ -104,6 +106,8 @@ impl Iterator for ImportZsh { String::from("unknown"), -1, -1, + None, + None, ))) } } diff --git a/src/main.rs b/src/main.rs index 19357cb..22c8b21 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ use std::path::PathBuf; use directories::ProjectDirs; use eyre::{eyre, Result}; use structopt::StructOpt; +use uuid::Uuid; #[macro_use] extern crate log; @@ -40,8 +41,11 @@ enum AtuinCmd { #[structopt(about = "import shell history from file")] Import(ImportCmd), - #[structopt(about = "start a atuin server")] + #[structopt(about = "start an atuin server")] Server, + + #[structopt(about = "generates a UUID")] + Uuid, } impl Atuin { @@ -66,8 +70,12 @@ impl Atuin { let mut db = SqliteDatabase::new(db_path)?; match self.atuin { - AtuinCmd::History(history) => history.run(db), + AtuinCmd::History(history) => history.run(&mut db), AtuinCmd::Import(import) => import.run(&mut db), + AtuinCmd::Uuid => { + println!("{}", Uuid::new_v4().to_simple().to_string()); + Ok(()) + } _ => Ok(()), } }