Add sessions

This commit is contained in:
Ellie Huxtable 2021-02-13 20:21:49 +00:00
parent 099afe66ec
commit 440c4fc233
9 changed files with 130 additions and 37 deletions

20
Cargo.lock generated
View file

@ -49,12 +49,13 @@ dependencies = [
[[package]] [[package]]
name = "atuin" name = "atuin"
version = "0.2.0" version = "0.2.1"
dependencies = [ dependencies = [
"chrono", "chrono",
"directories", "directories",
"eyre", "eyre",
"home", "home",
"hostname",
"indicatif", "indicatif",
"log", "log",
"pretty_env_logger", "pretty_env_logger",
@ -314,6 +315,17 @@ dependencies = [
"winapi", "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]] [[package]]
name = "humantime" name = "humantime"
version = "1.3.0" version = "1.3.0"
@ -373,6 +385,12 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "match_cfg"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.3.4" version = "2.3.4"

View file

@ -1,6 +1,6 @@
[package] [package]
name = "atuin" name = "atuin"
version = "0.2.0" version = "0.2.1"
authors = ["Ellie Huxtable <e@elm.sh>"] authors = ["Ellie Huxtable <e@elm.sh>"]
edition = "2018" edition = "2018"
license = "MIT" license = "MIT"
@ -17,6 +17,7 @@ directories = "3.*"
uuid = { version = "0.8", features = ["serde", "v4"] } uuid = { version = "0.8", features = ["serde", "v4"] }
home = "0.5.3" home = "0.5.3"
indicatif = "0.15.0" indicatif = "0.15.0"
hostname = "0.3.1"
[dependencies.rusqlite] [dependencies.rusqlite]
version = "0.24.*" version = "0.24.*"

View file

@ -5,4 +5,15 @@
Through the fathomless deeps of space swims the star turtle Great ATuin, bearing on its back the four giant elephants who carry on their shoulders the mass of the Discworld. Through the fathomless deeps of space swims the star turtle Great ATuin, bearing on its back the four giant elephants who carry on their shoulders the mass of the Discworld.
</blockquote> </blockquote>
`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

View file

@ -32,13 +32,21 @@ pub enum HistoryCmd {
} }
impl HistoryCmd { impl HistoryCmd {
pub fn run(&self, db: SqliteDatabase) -> Result<()> { pub fn run(&self, db: &mut SqliteDatabase) -> Result<()> {
match self { match self {
HistoryCmd::Start { command: words } => { HistoryCmd::Start { command: words } => {
let command = words.join(" "); let command = words.join(" ");
let cwd = env::current_dir()?.display().to_string(); 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 // print the ID
// we use this as the key for calling end // we use this as the key for calling end

View file

@ -87,12 +87,23 @@ impl ImportCmd {
} }
pub fn run(&self, db: &mut SqliteDatabase) -> Result<()> { pub fn run(&self, db: &mut SqliteDatabase) -> Result<()> {
println!(" A'Tuin ");
println!("=====================");
println!(" 🌍 ");
println!(" 🐘🐘🐘🐘 ");
println!(" 🐢 ");
println!("=====================");
println!("Importing history...");
match self { match self {
ImportCmd::Auto => { ImportCmd::Auto => {
let shell = env::var("SHELL").unwrap_or(String::from("NO_SHELL")); let shell = env::var("SHELL").unwrap_or(String::from("NO_SHELL"));
match shell.as_str() { match shell.as_str() {
"/bin/zsh" => self.import_zsh(db), "/bin/zsh" => {
println!("Detected ZSH");
self.import_zsh(db)
}
_ => { _ => {
println!("cannot import {} history", shell); println!("cannot import {} history", shell);

View file

@ -8,7 +8,7 @@ use rusqlite::{params, Connection};
use crate::History; use crate::History;
pub trait Database { pub trait Database {
fn save(&self, h: History) -> Result<()>; fn save(&mut self, h: History) -> Result<()>;
fn save_bulk(&mut self, h: &Vec<History>) -> Result<()>; fn save_bulk(&mut self, h: &Vec<History>) -> Result<()>;
fn load(&self, id: &str) -> Result<History>; fn load(&self, id: &str) -> Result<History>;
fn list(&self) -> Result<()>; fn list(&self) -> Result<()>;
@ -53,6 +53,8 @@ impl SqliteDatabase {
exit integer not null, exit integer not null,
command text not null, command text not null,
cwd text not null, cwd text not null,
session text not null,
hostname text not null,
unique(timestamp, cwd, command) unique(timestamp, cwd, command)
)", )",
@ -64,22 +66,11 @@ impl SqliteDatabase {
} }
impl Database for SqliteDatabase { impl Database for SqliteDatabase {
fn save(&self, h: History) -> Result<()> { fn save(&mut self, h: History) -> Result<()> {
debug!("saving history to sqlite"); debug!("saving history to sqlite");
let v = vec![h];
self.conn.execute( self.save_bulk(&v)
"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(())
} }
fn save_bulk(&mut self, h: &Vec<History>) -> Result<()> { fn save_bulk(&mut self, h: &Vec<History>) -> Result<()> {
@ -95,9 +86,20 @@ impl Database for SqliteDatabase {
duration, duration,
exit, exit,
command, command,
cwd cwd,
) values (?1, ?2, ?3, ?4, ?5, ?6)", session,
params![i.id, i.timestamp, i.duration, i.exit, i.command, i.cwd], 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"); debug!("loading history item");
let mut stmt = self.conn.prepare( 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", where id = ?1",
)?; )?;
@ -122,6 +124,8 @@ impl Database for SqliteDatabase {
exit: row.get(3)?, exit: row.get(3)?,
command: row.get(4)?, command: row.get(4)?,
cwd: row.get(5)?, cwd: row.get(5)?,
session: row.get(6)?,
hostname: row.get(7)?,
}) })
})?; })?;
@ -137,9 +141,9 @@ impl Database for SqliteDatabase {
self.conn.execute( self.conn.execute(
"update history "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", 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(()) Ok(())
@ -148,9 +152,9 @@ impl Database for SqliteDatabase {
fn list(&self) -> Result<()> { fn list(&self) -> Result<()> {
debug!("listing history"); debug!("listing history");
let mut stmt = self let mut stmt = self.conn.prepare(
.conn "SELECT id, timestamp, duration, exit, command, cwd, session, hostname FROM history",
.prepare("SELECT id, timestamp, duration, exit, command, cwd FROM history")?; )?;
let history_iter = stmt.query_map(params![], |row| { let history_iter = stmt.query_map(params![], |row| {
Ok(History { Ok(History {
@ -160,6 +164,8 @@ impl Database for SqliteDatabase {
exit: row.get(3)?, exit: row.get(3)?,
command: row.get(4)?, command: row.get(4)?,
cwd: row.get(5)?, cwd: row.get(5)?,
session: row.get(6)?,
hostname: row.get(7)?,
}) })
})?; })?;
@ -167,8 +173,8 @@ impl Database for SqliteDatabase {
let h = h.unwrap(); let h = h.unwrap();
println!( 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
); );
} }

View file

@ -1,4 +1,6 @@
use chrono; use std::env;
use hostname;
use uuid::Uuid; use uuid::Uuid;
#[derive(Debug)] #[derive(Debug)]
@ -9,10 +11,32 @@ pub struct History {
pub exit: i64, pub exit: i64,
pub command: String, pub command: String,
pub cwd: String, pub cwd: String,
pub session: String,
pub hostname: String,
} }
impl History { 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<String>,
hostname: Option<String>,
) -> 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 { History {
id: Uuid::new_v4().to_simple().to_string(), id: Uuid::new_v4().to_simple().to_string(),
timestamp, timestamp,
@ -20,6 +44,8 @@ impl History {
cwd, cwd,
exit, exit,
duration, duration,
session,
hostname,
} }
} }
} }

View file

@ -70,11 +70,13 @@ fn parse_extended(line: String) -> History {
// use nanos, because why the hell not? we won't display them. // use nanos, because why the hell not? we won't display them.
History::new( History::new(
Utc.timestamp(time, 0).timestamp_nanos(), time * 1_000_000_000,
trim_newline(command), trim_newline(command),
String::from("unknown"), String::from("unknown"),
-1, -1,
duration * 1_000_000_000, duration * 1_000_000_000,
None,
None,
) )
} }
@ -104,6 +106,8 @@ impl Iterator for ImportZsh {
String::from("unknown"), String::from("unknown"),
-1, -1,
-1, -1,
None,
None,
))) )))
} }
} }

View file

@ -3,6 +3,7 @@ use std::path::PathBuf;
use directories::ProjectDirs; use directories::ProjectDirs;
use eyre::{eyre, Result}; use eyre::{eyre, Result};
use structopt::StructOpt; use structopt::StructOpt;
use uuid::Uuid;
#[macro_use] #[macro_use]
extern crate log; extern crate log;
@ -40,8 +41,11 @@ enum AtuinCmd {
#[structopt(about = "import shell history from file")] #[structopt(about = "import shell history from file")]
Import(ImportCmd), Import(ImportCmd),
#[structopt(about = "start a atuin server")] #[structopt(about = "start an atuin server")]
Server, Server,
#[structopt(about = "generates a UUID")]
Uuid,
} }
impl Atuin { impl Atuin {
@ -66,8 +70,12 @@ impl Atuin {
let mut db = SqliteDatabase::new(db_path)?; let mut db = SqliteDatabase::new(db_path)?;
match self.atuin { match self.atuin {
AtuinCmd::History(history) => history.run(db), AtuinCmd::History(history) => history.run(&mut db),
AtuinCmd::Import(import) => import.run(&mut db), AtuinCmd::Import(import) => import.run(&mut db),
AtuinCmd::Uuid => {
println!("{}", Uuid::new_v4().to_simple().to_string());
Ok(())
}
_ => Ok(()), _ => Ok(()),
} }
} }