From ad3677074dd6ba7d85c27ddc15d99ee1b77c1092 Mon Sep 17 00:00:00 2001 From: Bradley Noyes Date: Mon, 2 May 2022 11:19:50 -0400 Subject: [PATCH 1/4] Attempting to implement zsh-histdb import Import compiles passes tests, but doesn't run b/c of async runtime. zsh-histdb uses sqlite, and sqlx-rs is async, but import code is sync. --- atuin-client/Cargo.toml | 1 + atuin-client/src/import/mod.rs | 1 + atuin-client/src/import/zsh-histdb.rs | 223 +++++++++++++++++++++ atuin-client/src/import/zsh_histdb.rs | 173 ++++++++++++++++ atuin-client/test_files/zsh-history.db | Bin 0 -> 45056 bytes atuin-client/test_files/zsh-history.db-shm | Bin 0 -> 32768 bytes atuin-client/test_files/zsh-history.db-wal | 0 src/command/client/import.rs | 14 +- 8 files changed, 409 insertions(+), 3 deletions(-) create mode 100644 atuin-client/src/import/zsh-histdb.rs create mode 100644 atuin-client/src/import/zsh_histdb.rs create mode 100644 atuin-client/test_files/zsh-history.db create mode 100644 atuin-client/test_files/zsh-history.db-shm create mode 100644 atuin-client/test_files/zsh-history.db-wal diff --git a/atuin-client/Cargo.toml b/atuin-client/Cargo.toml index 019187064fc..58b90c6e953 100644 --- a/atuin-client/Cargo.toml +++ b/atuin-client/Cargo.toml @@ -61,6 +61,7 @@ hex = { version = "0.4", optional = true } sha2 = { version = "0.10", optional = true } rmp-serde = { version = "1.0.0", optional = true } base64 = { version = "0.13.0", optional = true } +tokio = { version = "1", features = ["full"] } [dev-dependencies] tokio = { version = "1", features = ["full"] } diff --git a/atuin-client/src/import/mod.rs b/atuin-client/src/import/mod.rs index 8d4aa17f5b1..e969a27e052 100644 --- a/atuin-client/src/import/mod.rs +++ b/atuin-client/src/import/mod.rs @@ -11,6 +11,7 @@ pub mod bash; pub mod fish; pub mod resh; pub mod zsh; +pub mod zsh_histdb; // this could probably be sped up fn count_lines(buf: &mut BufReader) -> Result { diff --git a/atuin-client/src/import/zsh-histdb.rs b/atuin-client/src/import/zsh-histdb.rs new file mode 100644 index 00000000000..a71f522d521 --- /dev/null +++ b/atuin-client/src/import/zsh-histdb.rs @@ -0,0 +1,223 @@ +// import old shell history from zsh-histdb! +// automatically hoover up all that we can find + +use std::{ + fs::File, + io::{BufRead, BufReader, Read, Seek}, + path::{Path, PathBuf}, +}; + +use chrono::{prelude::*, Utc}; +use directories::UserDirs; +use eyre::{eyre, Result}; +use itertools::Itertools; + +use super::{count_lines, Importer}; +use crate::history::History; + +#[derive(Debug)] +pub struct ZshHistDb { + file: BufReader, + strbuf: String, + loc: usize, + counter: i64, +} + +impl ZshHistDb { + fn new(r: R) -> Result { + let mut buf = BufReader::new(r); + let loc = count_lines(&mut buf)?; + + Ok(Self { + file: buf, + strbuf: String::new(), + loc, + counter: 0, + }) + } +} + +impl Importer for ZshHistDb { + const NAME: &'static str = "zsh"; + + // if [[ -z ${HISTDB_FILE} ]]; then + // typeset -g HISTDB_FILE="${HOME}/.histdb/zsh-history.db" + fn histpath() -> Result { + // oh-my-zsh sets HISTFILE=~/.zhistory + // zsh has no default value for this var, but uses ~/.zhistory. + // we could maybe be smarter about this in the future :) + let user_dirs = UserDirs::new().unwrap(); + let home_dir = user_dirs.home_dir(); + + let mut candidates = [".zhistory", ".zsh_history"].iter(); + loop { + match candidates.next() { + Some(candidate) => { + let histpath = home_dir.join(candidate); + if histpath.exists() { + break Ok(histpath); + } + } + None => break Err(eyre!("Could not find history file. Try setting $HISTFILE")), + } + } + } + + fn parse(path: impl AsRef) -> Result { + Self::new(File::open(path)?) + } +} + +impl Iterator for ZshHistDb { + type Item = Result; + + fn next(&mut self) -> Option { + // ZSH extended history records the timestamp + command duration + // These lines begin with : + // So, if the line begins with :, parse it. Otherwise it's just + // the command + self.strbuf.clear(); + match self.file.read_line(&mut self.strbuf) { + Ok(0) => return None, + Ok(_) => (), + Err(e) => return Some(Err(eyre!("failed to read line: {}", e))), // we can skip past things like invalid utf8 + } + + self.loc -= 1; + + while self.strbuf.ends_with("\\\n") { + if self.file.read_line(&mut self.strbuf).is_err() { + // There's a chance that the last line of a command has invalid + // characters, the only safe thing to do is break :/ + // usually just invalid utf8 or smth + // however, we really need to avoid missing history, so it's + // better to have some items that should have been part of + // something else, than to miss things. So break. + break; + }; + + self.loc -= 1; + } + + // We have to handle the case where a line has escaped newlines. + // Keep reading until we have a non-escaped newline + + let extended = self.strbuf.starts_with(':'); + + if extended { + self.counter += 1; + Some(Ok(parse_extended(&self.strbuf, self.counter))) + } else { + let time = chrono::Utc::now(); + let offset = chrono::Duration::seconds(self.counter); + let time = time - offset; + + self.counter += 1; + + Some(Ok(History::new( + time, + self.strbuf.trim_end().to_string(), + String::from("unknown"), + -1, + -1, + None, + None, + ))) + } + } + + fn size_hint(&self) -> (usize, Option) { + (0, Some(self.loc)) + } +} + +fn parse_extended(line: &str, counter: i64) -> History { + let line = line.replacen(": ", "", 2); + let (time, duration) = line.splitn(2, ':').collect_tuple().unwrap(); + let (duration, command) = duration.splitn(2, ';').collect_tuple().unwrap(); + + let time = time + .parse::() + .unwrap_or_else(|_| chrono::Utc::now().timestamp()); + + let offset = chrono::Duration::milliseconds(counter); + let time = Utc.timestamp(time, 0); + let time = time + offset; + + let duration = duration.parse::().map_or(-1, |t| t * 1_000_000_000); + + // use nanos, because why the hell not? we won't display them. + History::new( + time, + command.trim_end().to_string(), + String::from("unknown"), + 0, // assume 0, we have no way of knowing :( + duration, + None, + None, + ) +} + +#[cfg(test)] +mod test { + use std::io::Cursor; + + use chrono::prelude::*; + use chrono::Utc; + + use super::*; + + #[test] + fn test_parse_extended_simple() { + let parsed = parse_extended(": 1613322469:0;cargo install atuin", 0); + + assert_eq!(parsed.command, "cargo install atuin"); + assert_eq!(parsed.duration, 0); + assert_eq!(parsed.timestamp, Utc.timestamp(1_613_322_469, 0)); + + let parsed = parse_extended(": 1613322469:10;cargo install atuin;cargo update", 0); + + assert_eq!(parsed.command, "cargo install atuin;cargo update"); + assert_eq!(parsed.duration, 10_000_000_000); + assert_eq!(parsed.timestamp, Utc.timestamp(1_613_322_469, 0)); + + let parsed = parse_extended(": 1613322469:10;cargo :b̷i̶t̴r̵o̴t̴ ̵i̷s̴ ̷r̶e̵a̸l̷", 0); + + assert_eq!(parsed.command, "cargo :b̷i̶t̴r̵o̴t̴ ̵i̷s̴ ̷r̶e̵a̸l̷"); + assert_eq!(parsed.duration, 10_000_000_000); + assert_eq!(parsed.timestamp, Utc.timestamp(1_613_322_469, 0)); + + let parsed = parse_extended(": 1613322469:10;cargo install \\n atuin\n", 0); + + assert_eq!(parsed.command, "cargo install \\n atuin"); + assert_eq!(parsed.duration, 10_000_000_000); + assert_eq!(parsed.timestamp, Utc.timestamp(1_613_322_469, 0)); + } + + #[test] + fn test_parse_file() { + let input = r": 1613322469:0;cargo install atuin +: 1613322469:10;cargo install atuin; \ +cargo update +: 1613322469:10;cargo :b̷i̶t̴r̵o̴t̴ ̵i̷s̴ ̷r̶e̵a̸l̷ +"; + + let cursor = Cursor::new(input); + let mut zsh = ZshHistDb::new(cursor).unwrap(); + assert_eq!(zsh.loc, 4); + assert_eq!(zsh.size_hint(), (0, Some(4))); + + assert_eq!(&zsh.next().unwrap().unwrap().command, "cargo install atuin"); + assert_eq!( + &zsh.next().unwrap().unwrap().command, + "cargo install atuin; \\\ncargo update" + ); + assert_eq!( + &zsh.next().unwrap().unwrap().command, + "cargo :b̷i̶t̴r̵o̴t̴ ̵i̷s̴ ̷r̶e̵a̸l̷" + ); + assert!(zsh.next().is_none()); + + assert_eq!(zsh.size_hint(), (0, Some(0))); + } +} diff --git a/atuin-client/src/import/zsh_histdb.rs b/atuin-client/src/import/zsh_histdb.rs new file mode 100644 index 00000000000..6e9a0c21bb3 --- /dev/null +++ b/atuin-client/src/import/zsh_histdb.rs @@ -0,0 +1,173 @@ +// import old shell history from zsh-histdb! +// automatically hoover up all that we can find + +// As far as i can tell there are no version numbers in the histdb sqlite DB, so we're going based +// on the schema from 2022-05-01 + +// +//select * from history left join commands on history.command_id = commands.rowid left join places on history.place_id = places.rowid limit 10; +// +//id|session|command_id|place_id|exit_status|start_time|duration|id|argv|id|host|dir +// +// +// select history.id,history.start_time,places.host,places.dir,commands.argv from history left join commands on history.command_id = commands.rowid left join places on history.place_id = places.rowid ; +// +// CREATE TABLE history (id integer primary key autoincrement, +// session int, +// command_id int references commands (id), +// place_id int references places (id), +// exit_status int, +// start_time int, +// duration int); + +use std::{ + path::{Path, PathBuf}, +}; + +use chrono::{prelude::*, Utc}; +use directories::UserDirs; +use eyre::{eyre, Result}; +use sqlx::sqlite::SqlitePool; + +use super::Importer; +use crate::history::History; + +#[derive(sqlx::FromRow, Debug)] +pub struct HistDbHistoryCount { + pub count: usize +} + +#[derive(sqlx::FromRow, Debug)] +pub struct HistDbHistory { + pub id: i64, + pub start_time: NaiveDateTime, + pub host: String, + pub dir: String, + pub argv: String, + pub duration: i64, +} + +impl From for History { + fn from(histdb_item: HistDbHistory) -> Self { + History::new ( + DateTime::from_utc(histdb_item.start_time, Utc), // must assume UTC? + histdb_item.argv.trim_end().to_string(), + histdb_item.dir, + 0, // assume 0, we have no way of knowing :( + histdb_item.duration, + None, + Some(histdb_item.host), + ) + } +} + +#[derive(Debug)] +pub struct ZshHistDb { + histdb: Vec, + counter: i64, +} + +impl ZshHistDb { + //fn new>(dbpath: P) -> Result { + fn new(dbpath: PathBuf) -> Result { + // Create the runtime + //let rt = tokio::runtime::Runtime::new().unwrap(); + let handle = tokio::runtime::Handle::current(); + + // Execute the future, blocking the current thread until completion + let histdb_vec :Vec = handle.block_on(async { + let pool = SqlitePool::connect(dbpath.to_str().unwrap()).await?; + let query = format!("select history.id,history.start_time,history.duration,places.host,places.dir,commands.argv from history left join commands on history.command_id = commands.rowid left join places on history.place_id = places.rowid"); + sqlx::query_as::<_, HistDbHistory>(&query) + .fetch_all(&pool) + .await + }).unwrap(); + + let hist : Vec = histdb_vec.into_iter().map(|x| x.into()).collect::>(); + Ok(Self { + histdb: hist, + counter: 0, + }) + } +} + +impl Importer for ZshHistDb { + // Not sure how this is used + const NAME: &'static str = "zsh_histdb"; + + fn histpath() -> Result { + + // By default histdb database is `${HOME}/.histdb/zsh-history.db` + // This can be modified by ${HISTDB_FILE} + // + // if [[ -z ${HISTDB_FILE} ]]; then + // typeset -g HISTDB_FILE="${HOME}/.histdb/zsh-history.db" + let user_dirs = UserDirs::new().unwrap(); // should catch error here? + let home_dir = user_dirs.home_dir(); + let histdb_path = std::env::var("HISTDB_FILE") + .as_ref() + .map(|x| Path::new(x).to_path_buf()) + .unwrap_or_else(|_err| home_dir.join(".histdb/zsh-history.db")); + if histdb_path.exists() { Ok(histdb_path) } + else { Err(eyre!("Could not find history file. Try setting $HISTDB_FILE")) } + } + + fn parse(path: impl AsRef) -> Result { + Self::new(path.as_ref().to_path_buf()) + } +} + +impl Iterator for ZshHistDb { + type Item = Result; + + fn next(&mut self) -> Option { + match self.histdb.pop() + { + Some(h) => { self.counter += 1; Some(Ok(h)) } + None => { None } + } + } +} + +// This was a debug function +pub async fn _print_db() -> Result<()> { + let db_path = ZshHistDb::histpath().unwrap(); + let pool = SqlitePool::connect(db_path.to_str().unwrap()).await?; + let query = format!("select history.id,history.start_time,places.host,places.dir,commands.argv from history left join commands on history.command_id = commands.rowid left join places on history.place_id = places.rowid"); + //db.query_history(&query).await?; + let a = sqlx::query_as::<_, HistDbHistory>(&query) + .fetch_one(&pool) + .await?; + println!("{:?}", a); + Ok(()) +} + +#[cfg(test)] +mod test { + + use super::*; + use std::env; + + #[test] + fn test_import() { + let test_db1 = "test_files/zsh-history.db"; + let key = "HISTDB_FILE"; + env::set_var(key, test_db1); + + // test the env got set + assert_eq!(env::var(key).unwrap(), test_db1.to_string()); + + // test histdb returns the proper db from previous step + let histdb_path = ZshHistDb::histpath(); + assert_eq!(histdb_path.unwrap().to_str().unwrap() , test_db1); + + // test histdb iterator + let histdb_path = ZshHistDb::histpath(); + let histdb = ZshHistDb::new(histdb_path.unwrap()).unwrap(); + println!("h: {:#?}", histdb.histdb); + println!("counter: {:?}", histdb.counter); + for i in histdb { + println!("{:?}", i); + } + } +} diff --git a/atuin-client/test_files/zsh-history.db b/atuin-client/test_files/zsh-history.db new file mode 100644 index 0000000000000000000000000000000000000000..8e0870d3ffa07fb18b8f833265f997e3e1c3af14 GIT binary patch literal 45056 zcmeI*?Q7dc90%|_og!I&$d!d6ToZP~Zz7x~WP|Nx+hu_j%Gg7p8)F#7zBr;Pgb+(j4K-ECR112QQFE#$Z7M%+(IOjn-f!{eq~7?J@N4|% z<>Te6%Re=LY^KdO8^@{;2?7v+00bZa0SG_<0vAWXubcKe9XeU@!l69e_mVVk)2o~m51rN$WE7HX&id@#xg(Y@?s}D2L@5N=X--xc%v|irPEt7>}zdG z@^dM<=c~bNzc`TU{ZzbxlKxiF9^*r1*u5V8EN^{TIgx57*|90Mt+M(|e_mL*7P^64 zS5=+K5;g7BRXX`1k69hwYOc*hsY+k*3>0oW`f}DHE1Ao%sTub6>S+wsj{95H5>p?s zwd`ZIr6$H75&oP%Q8Oe6KmY;|fB*y_009U<00Izz00b6Gz+{YE-gT4x*EB|YgK_MO zQ521QxuL!VFeoD{2VV{As!xnRCj2pf%Ku)h4x+UXfB*y_009U<00Izz00bZa0SKsn z@UzP_8~?Kv)}t?s|22cHSZe&=Qe%I{9}xb4|HXfU8U!E!0SG_<0uX=z1Rwwb2tWV= z|9^odWvg`h8vpA4{RU$lTHY4J>Kbd);+6=OUH?z{6QW*75P$##AOHafKmY;|fB*y_ z009Uru)r0nlWsTIJ=l7C!;c28zqua;ax;vMV*UW2tWV=5P$##AOHaf zKmY;|fWRUPT+zE-YySAZc>do*!vEyI@`sDuNwggT5P$##AOHafKmY;|fB*y_aIpkh ztVU|oQa{$mcB4nXw6r{1(=AP9?>DK+T9(SH+z-X`|DI*f|NosoyI4->Ed(F{0SG_< z0uX=z1Rwwb2teR`3DlTQv#$bJxo}@I*~$2y@;``rAwd8F5P$##AOHafKmY;|fB*y_ za9#wKS=V~${(p@B&&%OOKOg`B2tWV=5P$##AOHafKmY={KsNrLdK5r${r@rH|ES;p pJBASiAOHafKmY;|fB*y_009U<00IjyVCdAMjO8;;({;;|c%( literal 0 HcmV?d00001 diff --git a/atuin-client/test_files/zsh-history.db-shm b/atuin-client/test_files/zsh-history.db-shm new file mode 100644 index 0000000000000000000000000000000000000000..fe9ac2845eca6fe6da8a63cd096d9cf9e24ece10 GIT binary patch literal 32768 zcmeIuAr62r3, _>(db, BATCH_SIZE).await + if ZshHistDb::histpath().is_ok() { + println!("Detected Zsh-HistDb"); + //atuin_client::import::zsh_histdb::print_db().await?; + //todo!(); + import::(db, BATCH_SIZE).await + } + else { + println!("Detected ZSH"); + import::, _>(db, BATCH_SIZE).await + } } else if shell.ends_with("/fish") { println!("Detected Fish"); import::, _>(db, BATCH_SIZE).await From c545a08811393f160a11680fb32ea097d2f8732c Mon Sep 17 00:00:00 2001 From: Bradley Noyes Date: Fri, 6 May 2022 14:03:09 -0400 Subject: [PATCH 2/4] More working on importing histdb --- atuin-client/src/import/zsh-histdb.rs | 223 -------------------------- atuin-client/src/import/zsh_histdb.rs | 106 ++++++++---- src/command/client/import.rs | 8 +- 3 files changed, 75 insertions(+), 262 deletions(-) delete mode 100644 atuin-client/src/import/zsh-histdb.rs diff --git a/atuin-client/src/import/zsh-histdb.rs b/atuin-client/src/import/zsh-histdb.rs deleted file mode 100644 index a71f522d521..00000000000 --- a/atuin-client/src/import/zsh-histdb.rs +++ /dev/null @@ -1,223 +0,0 @@ -// import old shell history from zsh-histdb! -// automatically hoover up all that we can find - -use std::{ - fs::File, - io::{BufRead, BufReader, Read, Seek}, - path::{Path, PathBuf}, -}; - -use chrono::{prelude::*, Utc}; -use directories::UserDirs; -use eyre::{eyre, Result}; -use itertools::Itertools; - -use super::{count_lines, Importer}; -use crate::history::History; - -#[derive(Debug)] -pub struct ZshHistDb { - file: BufReader, - strbuf: String, - loc: usize, - counter: i64, -} - -impl ZshHistDb { - fn new(r: R) -> Result { - let mut buf = BufReader::new(r); - let loc = count_lines(&mut buf)?; - - Ok(Self { - file: buf, - strbuf: String::new(), - loc, - counter: 0, - }) - } -} - -impl Importer for ZshHistDb { - const NAME: &'static str = "zsh"; - - // if [[ -z ${HISTDB_FILE} ]]; then - // typeset -g HISTDB_FILE="${HOME}/.histdb/zsh-history.db" - fn histpath() -> Result { - // oh-my-zsh sets HISTFILE=~/.zhistory - // zsh has no default value for this var, but uses ~/.zhistory. - // we could maybe be smarter about this in the future :) - let user_dirs = UserDirs::new().unwrap(); - let home_dir = user_dirs.home_dir(); - - let mut candidates = [".zhistory", ".zsh_history"].iter(); - loop { - match candidates.next() { - Some(candidate) => { - let histpath = home_dir.join(candidate); - if histpath.exists() { - break Ok(histpath); - } - } - None => break Err(eyre!("Could not find history file. Try setting $HISTFILE")), - } - } - } - - fn parse(path: impl AsRef) -> Result { - Self::new(File::open(path)?) - } -} - -impl Iterator for ZshHistDb { - type Item = Result; - - fn next(&mut self) -> Option { - // ZSH extended history records the timestamp + command duration - // These lines begin with : - // So, if the line begins with :, parse it. Otherwise it's just - // the command - self.strbuf.clear(); - match self.file.read_line(&mut self.strbuf) { - Ok(0) => return None, - Ok(_) => (), - Err(e) => return Some(Err(eyre!("failed to read line: {}", e))), // we can skip past things like invalid utf8 - } - - self.loc -= 1; - - while self.strbuf.ends_with("\\\n") { - if self.file.read_line(&mut self.strbuf).is_err() { - // There's a chance that the last line of a command has invalid - // characters, the only safe thing to do is break :/ - // usually just invalid utf8 or smth - // however, we really need to avoid missing history, so it's - // better to have some items that should have been part of - // something else, than to miss things. So break. - break; - }; - - self.loc -= 1; - } - - // We have to handle the case where a line has escaped newlines. - // Keep reading until we have a non-escaped newline - - let extended = self.strbuf.starts_with(':'); - - if extended { - self.counter += 1; - Some(Ok(parse_extended(&self.strbuf, self.counter))) - } else { - let time = chrono::Utc::now(); - let offset = chrono::Duration::seconds(self.counter); - let time = time - offset; - - self.counter += 1; - - Some(Ok(History::new( - time, - self.strbuf.trim_end().to_string(), - String::from("unknown"), - -1, - -1, - None, - None, - ))) - } - } - - fn size_hint(&self) -> (usize, Option) { - (0, Some(self.loc)) - } -} - -fn parse_extended(line: &str, counter: i64) -> History { - let line = line.replacen(": ", "", 2); - let (time, duration) = line.splitn(2, ':').collect_tuple().unwrap(); - let (duration, command) = duration.splitn(2, ';').collect_tuple().unwrap(); - - let time = time - .parse::() - .unwrap_or_else(|_| chrono::Utc::now().timestamp()); - - let offset = chrono::Duration::milliseconds(counter); - let time = Utc.timestamp(time, 0); - let time = time + offset; - - let duration = duration.parse::().map_or(-1, |t| t * 1_000_000_000); - - // use nanos, because why the hell not? we won't display them. - History::new( - time, - command.trim_end().to_string(), - String::from("unknown"), - 0, // assume 0, we have no way of knowing :( - duration, - None, - None, - ) -} - -#[cfg(test)] -mod test { - use std::io::Cursor; - - use chrono::prelude::*; - use chrono::Utc; - - use super::*; - - #[test] - fn test_parse_extended_simple() { - let parsed = parse_extended(": 1613322469:0;cargo install atuin", 0); - - assert_eq!(parsed.command, "cargo install atuin"); - assert_eq!(parsed.duration, 0); - assert_eq!(parsed.timestamp, Utc.timestamp(1_613_322_469, 0)); - - let parsed = parse_extended(": 1613322469:10;cargo install atuin;cargo update", 0); - - assert_eq!(parsed.command, "cargo install atuin;cargo update"); - assert_eq!(parsed.duration, 10_000_000_000); - assert_eq!(parsed.timestamp, Utc.timestamp(1_613_322_469, 0)); - - let parsed = parse_extended(": 1613322469:10;cargo :b̷i̶t̴r̵o̴t̴ ̵i̷s̴ ̷r̶e̵a̸l̷", 0); - - assert_eq!(parsed.command, "cargo :b̷i̶t̴r̵o̴t̴ ̵i̷s̴ ̷r̶e̵a̸l̷"); - assert_eq!(parsed.duration, 10_000_000_000); - assert_eq!(parsed.timestamp, Utc.timestamp(1_613_322_469, 0)); - - let parsed = parse_extended(": 1613322469:10;cargo install \\n atuin\n", 0); - - assert_eq!(parsed.command, "cargo install \\n atuin"); - assert_eq!(parsed.duration, 10_000_000_000); - assert_eq!(parsed.timestamp, Utc.timestamp(1_613_322_469, 0)); - } - - #[test] - fn test_parse_file() { - let input = r": 1613322469:0;cargo install atuin -: 1613322469:10;cargo install atuin; \ -cargo update -: 1613322469:10;cargo :b̷i̶t̴r̵o̴t̴ ̵i̷s̴ ̷r̶e̵a̸l̷ -"; - - let cursor = Cursor::new(input); - let mut zsh = ZshHistDb::new(cursor).unwrap(); - assert_eq!(zsh.loc, 4); - assert_eq!(zsh.size_hint(), (0, Some(4))); - - assert_eq!(&zsh.next().unwrap().unwrap().command, "cargo install atuin"); - assert_eq!( - &zsh.next().unwrap().unwrap().command, - "cargo install atuin; \\\ncargo update" - ); - assert_eq!( - &zsh.next().unwrap().unwrap().command, - "cargo :b̷i̶t̴r̵o̴t̴ ̵i̷s̴ ̷r̶e̵a̸l̷" - ); - assert!(zsh.next().is_none()); - - assert_eq!(zsh.size_hint(), (0, Some(0))); - } -} diff --git a/atuin-client/src/import/zsh_histdb.rs b/atuin-client/src/import/zsh_histdb.rs index 6e9a0c21bb3..933f7175617 100644 --- a/atuin-client/src/import/zsh_histdb.rs +++ b/atuin-client/src/import/zsh_histdb.rs @@ -3,14 +3,25 @@ // As far as i can tell there are no version numbers in the histdb sqlite DB, so we're going based // on the schema from 2022-05-01 +// +// I have run into some histories that will not import b/c of non UTF-8 characters. +// // -//select * from history left join commands on history.command_id = commands.rowid left join places on history.place_id = places.rowid limit 10; +// An Example sqlite query for hsitdb data: // //id|session|command_id|place_id|exit_status|start_time|duration|id|argv|id|host|dir // // -// select history.id,history.start_time,places.host,places.dir,commands.argv from history left join commands on history.command_id = commands.rowid left join places on history.place_id = places.rowid ; +// select +// history.id, +// history.start_time, +// places.host, +// places.dir, +// commands.argv +// from history +// left join commands on history.command_id = commands.rowid +// left join places on history.place_id = places.rowid ; // // CREATE TABLE history (id integer primary key autoincrement, // session int, @@ -19,11 +30,14 @@ // exit_status int, // start_time int, // duration int); +// use std::{ path::{Path, PathBuf}, }; +use std::sync::RwLock; +use lazy_static::lazy_static; use chrono::{prelude::*, Utc}; use directories::UserDirs; use eyre::{eyre, Result}; @@ -32,6 +46,20 @@ use sqlx::sqlite::SqlitePool; use super::Importer; use crate::history::History; +// Using lazy_static! here is just of a hack. The issue with importing zsh-histdb data is that +// sqlx-rs is fully async, but the Importer trait is not, so it is not possible to call async +// functions fromt this trait. So as a workaround, i'm using lazy_static to hold a vector of +// History Structs, and pre-populate that before the Importer trait is used. Then the Importer +// trait can recall the vector held in lazy_static!. +// + +lazy_static! { + static ref ZSH_HISTDB_VEC: RwLock> = { + let m = Vec::new(); + RwLock::new(m) + }; +} + #[derive(sqlx::FromRow, Debug)] pub struct HistDbHistoryCount { pub count: usize @@ -67,27 +95,48 @@ pub struct ZshHistDb { counter: i64, } + +async fn hist_from_db(dbpath: PathBuf) -> Result> { + let pool = SqlitePool::connect(dbpath.to_str().unwrap()).await?; + let query = format!("select history.id,history.start_time,history.duration,places.host,places.dir,commands.argv from history left join commands on history.command_id = commands.rowid left join places on history.place_id = places.rowid order by history.start_time"); + let histdb_vec : Vec = sqlx::query_as::<_, HistDbHistory>(&query) + .fetch_all(&pool) + .await?; + let hist : Vec = histdb_vec.into_iter().map(|x| x.into()).collect::>(); + Ok(hist) +} + impl ZshHistDb { - //fn new>(dbpath: P) -> Result { - fn new(dbpath: PathBuf) -> Result { - // Create the runtime - //let rt = tokio::runtime::Runtime::new().unwrap(); - let handle = tokio::runtime::Handle::current(); - - // Execute the future, blocking the current thread until completion - let histdb_vec :Vec = handle.block_on(async { - let pool = SqlitePool::connect(dbpath.to_str().unwrap()).await?; - let query = format!("select history.id,history.start_time,history.duration,places.host,places.dir,commands.argv from history left join commands on history.command_id = commands.rowid left join places on history.place_id = places.rowid"); - sqlx::query_as::<_, HistDbHistory>(&query) - .fetch_all(&pool) - .await - }).unwrap(); - - let hist : Vec = histdb_vec.into_iter().map(|x| x.into()).collect::>(); - Ok(Self { - histdb: hist, - counter: 0, - }) + + /// Creates a new ZshHistDb and populates the history based on the pre-populated data + /// structure. + pub fn new(_dbpath: PathBuf) -> Result { + if let Ok(mut static_zsh_histdb_vec) = ZSH_HISTDB_VEC.write() + { + let mut hist_vec = Vec::with_capacity(static_zsh_histdb_vec.len()); + for i in static_zsh_histdb_vec.drain(..) { hist_vec.push(i) } + Ok(Self { + histdb: hist_vec, + counter: 0, + }) + } + else { Err(eyre!("Could not find copy history")) } + } + + /// This function is used to pre-populate a vector of readings since the Importer trait is not + /// async. + pub async fn populate(dbpath: PathBuf) { + if let Ok(mut static_zsh_histdb_vec) = ZSH_HISTDB_VEC.write() + { + let mut hist = hist_from_db(dbpath).await.unwrap(); + *static_zsh_histdb_vec = Vec::with_capacity(hist.len()); + for i in hist.drain(..) { static_zsh_histdb_vec.push(i) } + } + } + + /// get the number entries already loaded. + pub fn count() -> usize { + ZSH_HISTDB_VEC.read().and_then(|x| Ok(x.len())).unwrap_or_else(|_x| 0) } } @@ -129,19 +178,6 @@ impl Iterator for ZshHistDb { } } -// This was a debug function -pub async fn _print_db() -> Result<()> { - let db_path = ZshHistDb::histpath().unwrap(); - let pool = SqlitePool::connect(db_path.to_str().unwrap()).await?; - let query = format!("select history.id,history.start_time,places.host,places.dir,commands.argv from history left join commands on history.command_id = commands.rowid left join places on history.place_id = places.rowid"); - //db.query_history(&query).await?; - let a = sqlx::query_as::<_, HistDbHistory>(&query) - .fetch_one(&pool) - .await?; - println!("{:?}", a); - Ok(()) -} - #[cfg(test)] mod test { diff --git a/src/command/client/import.rs b/src/command/client/import.rs index f9bd3147292..04085876849 100644 --- a/src/command/client/import.rs +++ b/src/command/client/import.rs @@ -16,7 +16,7 @@ pub enum Cmd { /// Import history for the current shell Auto, - /// Import history from the zsh history file + /// Import history from the zsh history file (or zsh_histdb file) Zsh, /// Import history from the bash history file @@ -47,9 +47,9 @@ impl Cmd { if shell.ends_with("/zsh") { if ZshHistDb::histpath().is_ok() { - println!("Detected Zsh-HistDb"); - //atuin_client::import::zsh_histdb::print_db().await?; - //todo!(); + println!("Detected Zsh-HistDb, using :{}", ZshHistDb::histpath().unwrap().to_str().unwrap()); + ZshHistDb::populate(ZshHistDb::histpath().unwrap()).await; + println!("Found {} entries ", ZshHistDb::count()); import::(db, BATCH_SIZE).await } else { From 5c52392cfdf80928fd94be777ec354b40f2556ce Mon Sep 17 00:00:00 2001 From: Bradley Noyes Date: Fri, 13 May 2022 12:46:25 -0400 Subject: [PATCH 3/4] Rewriting tests and using `Vec` instead of `String` - Rewriting tests to eliminate depencency on local file system - Using `Vec` for command strings instead of `String` to eliminate the utf8 errors i was seeing previously. Seems to be working. --- atuin-client/src/import/zsh_histdb.rs | 90 +++++++++++++++++---- atuin-client/test_files/zsh-history.db | Bin 45056 -> 0 bytes atuin-client/test_files/zsh-history.db-shm | Bin 32768 -> 0 bytes atuin-client/test_files/zsh-history.db-wal | 0 4 files changed, 74 insertions(+), 16 deletions(-) delete mode 100644 atuin-client/test_files/zsh-history.db delete mode 100644 atuin-client/test_files/zsh-history.db-shm delete mode 100644 atuin-client/test_files/zsh-history.db-wal diff --git a/atuin-client/src/import/zsh_histdb.rs b/atuin-client/src/import/zsh_histdb.rs index 89a7dd6b17d..28c43f355ac 100644 --- a/atuin-client/src/import/zsh_histdb.rs +++ b/atuin-client/src/import/zsh_histdb.rs @@ -40,7 +40,7 @@ use chrono::{prelude::*, Utc}; use async_trait::async_trait; use directories::UserDirs; use eyre::{eyre, Result}; -use sqlx::sqlite::SqlitePool; +use sqlx::{Pool, sqlite::SqlitePool}; use super::Importer; use crate::history::History; @@ -57,7 +57,7 @@ pub struct HistDbEntry { pub start_time: NaiveDateTime, pub host: String, pub dir: String, - pub argv: String, + pub argv: Vec, pub duration: i64, } @@ -65,7 +65,7 @@ impl From for History { fn from(histdb_item: HistDbEntry) -> Self { History::new ( DateTime::from_utc(histdb_item.start_time, Utc), // must assume UTC? - histdb_item.argv.trim_end().to_string(), + String::from_utf8(histdb_item.argv).unwrap_or_else(|_e| String::from("")).trim_end().to_string(), histdb_item.dir, 0, // assume 0, we have no way of knowing :( histdb_item.duration, @@ -84,15 +84,19 @@ pub struct ZshHistDb { /// Read db at given file, return vector of entries. async fn hist_from_db(dbpath: PathBuf) -> Result> { let pool = SqlitePool::connect(dbpath.to_str().unwrap()).await?; - let query = format!("select history.id,history.start_time,history.duration,places.host,places.dir,commands.argv from history left join commands on history.command_id = commands.rowid left join places on history.place_id = places.rowid order by history.start_time"); - let histdb_vec : Vec = sqlx::query_as::<_, HistDbEntry>(&query) + hist_from_db_conn(pool).await +} + +async fn hist_from_db_conn(pool: Pool) -> Result> { + let query = "select history.id,history.start_time,history.duration,places.host,places.dir,commands.argv from history left join commands on history.command_id = commands.rowid left join places on history.place_id = places.rowid order by history.start_time"; + let histdb_vec : Vec = sqlx::query_as::<_, HistDbEntry>(query) .fetch_all(&pool) .await?; Ok(histdb_vec) } impl ZshHistDb { - pub fn histpath() -> Result { + pub fn histpath_candidate() -> PathBuf { // By default histdb database is `${HOME}/.histdb/zsh-history.db` // This can be modified by ${HISTDB_FILE} @@ -101,10 +105,13 @@ impl ZshHistDb { // typeset -g HISTDB_FILE="${HOME}/.histdb/zsh-history.db" let user_dirs = UserDirs::new().unwrap(); // should catch error here? let home_dir = user_dirs.home_dir(); - let histdb_path = std::env::var("HISTDB_FILE") + std::env::var("HISTDB_FILE") .as_ref() .map(|x| Path::new(x).to_path_buf()) - .unwrap_or_else(|_err| home_dir.join(".histdb/zsh-history.db")); + .unwrap_or_else(|_err| home_dir.join(".histdb/zsh-history.db")) + } + pub fn histpath() -> Result { + let histdb_path = ZshHistDb::histpath_candidate(); if histdb_path.exists() { Ok(histdb_path) } else { Err(eyre!("Could not find history file. Try setting $HISTDB_FILE")) } } @@ -140,22 +147,73 @@ mod test { use super::*; use std::env; - + use sqlx::sqlite::SqlitePoolOptions; #[tokio::test(flavor = "multi_thread")] - async fn test_import() { - let test_db1 = "test_files/zsh-history.db"; + async fn test_env_vars() { + let test_env_db = "nonstd-zsh-history.db"; let key = "HISTDB_FILE"; - env::set_var(key, test_db1); + env::set_var(key, test_env_db); // test the env got set - assert_eq!(env::var(key).unwrap(), test_db1.to_string()); + assert_eq!(env::var(key).unwrap(), test_env_db.to_string()); // test histdb returns the proper db from previous step - let histdb_path = ZshHistDb::histpath(); - assert_eq!(histdb_path.unwrap().to_str().unwrap() , test_db1); + let histdb_path = ZshHistDb::histpath_candidate(); + assert_eq!(histdb_path.to_str().unwrap() , test_env_db); + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_import() { + let pool: SqlitePool = SqlitePoolOptions::new() + .min_connections(2) + .connect(":memory:") + .await + .unwrap(); + + + // sql dump directly from a test database. + let db_sql = r#" + PRAGMA foreign_keys=OFF; + BEGIN TRANSACTION; + CREATE TABLE commands (id integer primary key autoincrement, argv text, unique(argv) on conflict ignore); + INSERT INTO commands VALUES(1,'pwd'); + INSERT INTO commands VALUES(2,'curl google.com'); + INSERT INTO commands VALUES(3,'bash'); + CREATE TABLE places (id integer primary key autoincrement, host text, dir text, unique(host, dir) on conflict ignore); + INSERT INTO places VALUES(1,'mbp16.local','/home/noyez'); + CREATE TABLE history (id integer primary key autoincrement, + session int, + command_id int references commands (id), + place_id int references places (id), + exit_status int, + start_time int, + duration int); + INSERT INTO history VALUES(1,0,1,1,0,1651497918,1); + INSERT INTO history VALUES(2,0,2,1,0,1651497923,1); + INSERT INTO history VALUES(3,0,3,1,NULL,1651497930,NULL); + DELETE FROM sqlite_sequence; + INSERT INTO sqlite_sequence VALUES('commands',3); + INSERT INTO sqlite_sequence VALUES('places',3); + INSERT INTO sqlite_sequence VALUES('history',3); + CREATE INDEX hist_time on history(start_time); + CREATE INDEX place_dir on places(dir); + CREATE INDEX place_host on places(host); + CREATE INDEX history_command_place on history(command_id, place_id); + COMMIT; "#; + + + sqlx::query(db_sql) + .execute(&pool) + .await + .unwrap(); + + + // test histdb iterator - let histdb = ZshHistDb::new().await.unwrap(); + let histdb_vec = hist_from_db_conn(pool).await.unwrap(); + let histdb = ZshHistDb { histdb: histdb_vec }; + println!("h: {:#?}", histdb.histdb); println!("counter: {:?}", histdb.histdb.len()); for i in histdb.histdb { diff --git a/atuin-client/test_files/zsh-history.db b/atuin-client/test_files/zsh-history.db deleted file mode 100644 index 8e0870d3ffa07fb18b8f833265f997e3e1c3af14..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 45056 zcmeI*?Q7dc90%|_og!I&$d!d6ToZP~Zz7x~WP|Nx+hu_j%Gg7p8)F#7zBr;Pgb+(j4K-ECR112QQFE#$Z7M%+(IOjn-f!{eq~7?J@N4|% z<>Te6%Re=LY^KdO8^@{;2?7v+00bZa0SG_<0vAWXubcKe9XeU@!l69e_mVVk)2o~m51rN$WE7HX&id@#xg(Y@?s}D2L@5N=X--xc%v|irPEt7>}zdG z@^dM<=c~bNzc`TU{ZzbxlKxiF9^*r1*u5V8EN^{TIgx57*|90Mt+M(|e_mL*7P^64 zS5=+K5;g7BRXX`1k69hwYOc*hsY+k*3>0oW`f}DHE1Ao%sTub6>S+wsj{95H5>p?s zwd`ZIr6$H75&oP%Q8Oe6KmY;|fB*y_009U<00Izz00b6Gz+{YE-gT4x*EB|YgK_MO zQ521QxuL!VFeoD{2VV{As!xnRCj2pf%Ku)h4x+UXfB*y_009U<00Izz00bZa0SKsn z@UzP_8~?Kv)}t?s|22cHSZe&=Qe%I{9}xb4|HXfU8U!E!0SG_<0uX=z1Rwwb2tWV= z|9^odWvg`h8vpA4{RU$lTHY4J>Kbd);+6=OUH?z{6QW*75P$##AOHafKmY;|fB*y_ z009Uru)r0nlWsTIJ=l7C!;c28zqua;ax;vMV*UW2tWV=5P$##AOHaf zKmY;|fWRUPT+zE-YySAZc>do*!vEyI@`sDuNwggT5P$##AOHafKmY;|fB*y_aIpkh ztVU|oQa{$mcB4nXw6r{1(=AP9?>DK+T9(SH+z-X`|DI*f|NosoyI4->Ed(F{0SG_< z0uX=z1Rwwb2teR`3DlTQv#$bJxo}@I*~$2y@;``rAwd8F5P$##AOHafKmY;|fB*y_ za9#wKS=V~${(p@B&&%OOKOg`B2tWV=5P$##AOHafKmY={KsNrLdK5r${r@rH|ES;p pJBASiAOHafKmY;|fB*y_009U<00IjyVCdAMjO8;;({;;|c%( diff --git a/atuin-client/test_files/zsh-history.db-shm b/atuin-client/test_files/zsh-history.db-shm deleted file mode 100644 index fe9ac2845eca6fe6da8a63cd096d9cf9e24ece10..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32768 zcmeIuAr62r3 Date: Mon, 16 May 2022 19:16:16 -0400 Subject: [PATCH 4/4] Running fmt --- atuin-client/src/import/zsh_histdb.rs | 76 +++++++++++++-------------- src/command/client/import.rs | 9 +++- 2 files changed, 43 insertions(+), 42 deletions(-) diff --git a/atuin-client/src/import/zsh_histdb.rs b/atuin-client/src/import/zsh_histdb.rs index 28c43f355ac..34718f89507 100644 --- a/atuin-client/src/import/zsh_histdb.rs +++ b/atuin-client/src/import/zsh_histdb.rs @@ -4,23 +4,23 @@ // As far as i can tell there are no version numbers in the histdb sqlite DB, so we're going based // on the schema from 2022-05-01 // -// I have run into some histories that will not import b/c of non UTF-8 characters. +// I have run into some histories that will not import b/c of non UTF-8 characters. // // -// An Example sqlite query for hsitdb data: +// An Example sqlite query for hsitdb data: // //id|session|command_id|place_id|exit_status|start_time|duration|id|argv|id|host|dir // // -// select +// select // history.id, // history.start_time, // places.host, // places.dir, -// commands.argv -// from history -// left join commands on history.command_id = commands.rowid +// commands.argv +// from history +// left join commands on history.command_id = commands.rowid // left join places on history.place_id = places.rowid ; // // CREATE TABLE history (id integer primary key autoincrement, @@ -32,15 +32,13 @@ // duration int); // -use std::{ - path::{Path, PathBuf}, -}; +use std::path::{Path, PathBuf}; -use chrono::{prelude::*, Utc}; use async_trait::async_trait; +use chrono::{prelude::*, Utc}; use directories::UserDirs; use eyre::{eyre, Result}; -use sqlx::{Pool, sqlite::SqlitePool}; +use sqlx::{sqlite::SqlitePool, Pool}; use super::Importer; use crate::history::History; @@ -48,7 +46,7 @@ use crate::import::Loader; #[derive(sqlx::FromRow, Debug)] pub struct HistDbEntryCount { - pub count: usize + pub count: usize, } #[derive(sqlx::FromRow, Debug)] @@ -63,9 +61,12 @@ pub struct HistDbEntry { impl From for History { fn from(histdb_item: HistDbEntry) -> Self { - History::new ( - DateTime::from_utc(histdb_item.start_time, Utc), // must assume UTC? - String::from_utf8(histdb_item.argv).unwrap_or_else(|_e| String::from("")).trim_end().to_string(), + History::new( + DateTime::from_utc(histdb_item.start_time, Utc), // must assume UTC? + String::from_utf8(histdb_item.argv) + .unwrap_or_else(|_e| String::from("")) + .trim_end() + .to_string(), histdb_item.dir, 0, // assume 0, we have no way of knowing :( histdb_item.duration, @@ -80,7 +81,6 @@ pub struct ZshHistDb { histdb: Vec, } - /// Read db at given file, return vector of entries. async fn hist_from_db(dbpath: PathBuf) -> Result> { let pool = SqlitePool::connect(dbpath.to_str().unwrap()).await?; @@ -89,15 +89,14 @@ async fn hist_from_db(dbpath: PathBuf) -> Result> { async fn hist_from_db_conn(pool: Pool) -> Result> { let query = "select history.id,history.start_time,history.duration,places.host,places.dir,commands.argv from history left join commands on history.command_id = commands.rowid left join places on history.place_id = places.rowid order by history.start_time"; - let histdb_vec : Vec = sqlx::query_as::<_, HistDbEntry>(query) - .fetch_all(&pool) - .await?; + let histdb_vec: Vec = sqlx::query_as::<_, HistDbEntry>(query) + .fetch_all(&pool) + .await?; Ok(histdb_vec) } impl ZshHistDb { pub fn histpath_candidate() -> PathBuf { - // By default histdb database is `${HOME}/.histdb/zsh-history.db` // This can be modified by ${HISTDB_FILE} // @@ -106,14 +105,19 @@ impl ZshHistDb { let user_dirs = UserDirs::new().unwrap(); // should catch error here? let home_dir = user_dirs.home_dir(); std::env::var("HISTDB_FILE") - .as_ref() - .map(|x| Path::new(x).to_path_buf()) - .unwrap_or_else(|_err| home_dir.join(".histdb/zsh-history.db")) + .as_ref() + .map(|x| Path::new(x).to_path_buf()) + .unwrap_or_else(|_err| home_dir.join(".histdb/zsh-history.db")) } pub fn histpath() -> Result { let histdb_path = ZshHistDb::histpath_candidate(); - if histdb_path.exists() { Ok(histdb_path) } - else { Err(eyre!("Could not find history file. Try setting $HISTDB_FILE")) } + if histdb_path.exists() { + Ok(histdb_path) + } else { + Err(eyre!( + "Could not find history file. Try setting $HISTDB_FILE" + )) + } } } @@ -125,8 +129,7 @@ impl Importer for ZshHistDb { /// Creates a new ZshHistDb and populates the history based on the pre-populated data /// structure. async fn new() -> Result { - - let dbpath = ZshHistDb::histpath()?; + let dbpath = ZshHistDb::histpath()?; let histdb_entry_vec = hist_from_db(dbpath).await?; Ok(Self { histdb: histdb_entry_vec, @@ -136,18 +139,19 @@ impl Importer for ZshHistDb { Ok(self.histdb.len()) } async fn load(self, h: &mut impl Loader) -> Result<()> { - for i in self.histdb { h.push(i.into()).await?; } + for i in self.histdb { + h.push(i.into()).await?; + } Ok(()) } } - #[cfg(test)] mod test { use super::*; - use std::env; use sqlx::sqlite::SqlitePoolOptions; + use std::env; #[tokio::test(flavor = "multi_thread")] async fn test_env_vars() { let test_env_db = "nonstd-zsh-history.db"; @@ -159,7 +163,7 @@ mod test { // test histdb returns the proper db from previous step let histdb_path = ZshHistDb::histpath_candidate(); - assert_eq!(histdb_path.to_str().unwrap() , test_env_db); + assert_eq!(histdb_path.to_str().unwrap(), test_env_db); } #[tokio::test(flavor = "multi_thread")] @@ -170,7 +174,6 @@ mod test { .await .unwrap(); - // sql dump directly from a test database. let db_sql = r#" PRAGMA foreign_keys=OFF; @@ -201,14 +204,7 @@ mod test { CREATE INDEX history_command_place on history(command_id, place_id); COMMIT; "#; - - sqlx::query(db_sql) - .execute(&pool) - .await - .unwrap(); - - - + sqlx::query(db_sql).execute(&pool).await.unwrap(); // test histdb iterator let histdb_vec = hist_from_db_conn(pool).await.unwrap(); diff --git a/src/command/client/import.rs b/src/command/client/import.rs index fadcbea907f..60fd536d9d1 100644 --- a/src/command/client/import.rs +++ b/src/command/client/import.rs @@ -8,7 +8,9 @@ use indicatif::ProgressBar; use atuin_client::{ database::Database, history::History, - import::{bash::Bash, fish::Fish, resh::Resh, zsh::Zsh, zsh_histdb::ZshHistDb, Importer, Loader}, + import::{ + bash::Bash, fish::Fish, resh::Resh, zsh::Zsh, zsh_histdb::ZshHistDb, Importer, Loader, + }, }; #[derive(Parser)] @@ -46,7 +48,10 @@ impl Cmd { let shell = env::var("SHELL").unwrap_or_else(|_| String::from("NO_SHELL")); if shell.ends_with("/zsh") { if ZshHistDb::histpath().is_ok() { - println!("Detected Zsh-HistDb, using :{}", ZshHistDb::histpath().unwrap().to_str().unwrap()); + println!( + "Detected Zsh-HistDb, using :{}", + ZshHistDb::histpath().unwrap().to_str().unwrap() + ); import::(db).await } else { println!("Detected ZSH");