Add sessions
This commit is contained in:
parent
099afe66ec
commit
440c4fc233
9 changed files with 130 additions and 37 deletions
20
Cargo.lock
generated
20
Cargo.lock
generated
|
@ -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"
|
||||||
|
|
|
@ -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.*"
|
||||||
|
|
13
README.md
13
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.
|
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.
|
||||||
</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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
12
src/main.rs
12
src/main.rs
|
@ -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(()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue