diff --git a/README.md b/README.md
index 5f4f1ee..8510746 100644
--- a/README.md
+++ b/README.md
@@ -274,6 +274,21 @@ Install `atuin` shell plugin in zsh, bash, or fish with [Fig](https://fig.io) in
+### Nushell
+
+Run in *Nushell*:
+
+```
+mkdir ~/.local/share/atuin/
+atuin init nu | save ~/.local/share/atuin/init.nu
+```
+
+Add to `config.nu`:
+
+```
+source ~/.local/share/atuin/init.nu
+```
+
## ...what's with the name?
Atuin is named after "The Great A'Tuin", a giant turtle from Terry Pratchett's
diff --git a/atuin-client/src/import/mod.rs b/atuin-client/src/import/mod.rs
index 65c4f41..3d38cd2 100644
--- a/atuin-client/src/import/mod.rs
+++ b/atuin-client/src/import/mod.rs
@@ -8,6 +8,8 @@ use crate::history::History;
pub mod bash;
pub mod fish;
+pub mod nu;
+pub mod nu_histdb;
pub mod resh;
pub mod zsh;
pub mod zsh_histdb;
diff --git a/atuin-client/src/import/nu.rs b/atuin-client/src/import/nu.rs
new file mode 100644
index 0000000..0f10760
--- /dev/null
+++ b/atuin-client/src/import/nu.rs
@@ -0,0 +1,76 @@
+// import old shell history!
+// automatically hoover up all that we can find
+
+use std::{fs::File, io::Read, path::PathBuf};
+
+use async_trait::async_trait;
+use directories::BaseDirs;
+use eyre::{eyre, Result};
+
+use super::{unix_byte_lines, Importer, Loader};
+use crate::history::History;
+
+#[derive(Debug)]
+pub struct Nu {
+ bytes: Vec,
+}
+
+fn get_histpath() -> Result {
+ let base = BaseDirs::new().ok_or_else(|| eyre!("could not determine data directory"))?;
+ let config_dir = base.config_dir().join("nushell");
+
+ let histpath = config_dir.join("history.txt");
+ if histpath.exists() {
+ Ok(histpath)
+ } else {
+ Err(eyre!("Could not find history file."))
+ }
+}
+
+#[async_trait]
+impl Importer for Nu {
+ const NAME: &'static str = "nu";
+
+ async fn new() -> Result {
+ let mut bytes = Vec::new();
+ let path = get_histpath()?;
+ let mut f = File::open(path)?;
+ f.read_to_end(&mut bytes)?;
+ Ok(Self { bytes })
+ }
+
+ async fn entries(&mut self) -> Result {
+ Ok(super::count_lines(&self.bytes))
+ }
+
+ async fn load(self, h: &mut impl Loader) -> Result<()> {
+ let now = chrono::Utc::now();
+
+ let mut counter = 0;
+ for b in unix_byte_lines(&self.bytes) {
+ let s = match std::str::from_utf8(b) {
+ Ok(s) => s,
+ Err(_) => continue, // we can skip past things like invalid utf8
+ };
+
+ let cmd: String = s.replace("<\\n>", "\n");
+
+ let offset = chrono::Duration::nanoseconds(counter);
+ counter += 1;
+
+ h.push(History::new(
+ now - offset, // preserve ordering
+ cmd,
+ String::from("unknown"),
+ -1,
+ -1,
+ None,
+ None,
+ None,
+ ))
+ .await?;
+ }
+
+ Ok(())
+ }
+}
diff --git a/atuin-client/src/import/nu_histdb.rs b/atuin-client/src/import/nu_histdb.rs
new file mode 100644
index 0000000..0fb5192
--- /dev/null
+++ b/atuin-client/src/import/nu_histdb.rs
@@ -0,0 +1,110 @@
+// import old shell history!
+// automatically hoover up all that we can find
+
+use std::path::PathBuf;
+
+use async_trait::async_trait;
+use chrono::{prelude::*, Utc};
+use directories::BaseDirs;
+use eyre::{eyre, Result};
+use sqlx::{sqlite::SqlitePool, Pool};
+
+use super::Importer;
+use crate::history::History;
+use crate::import::Loader;
+
+#[derive(sqlx::FromRow, Debug)]
+pub struct HistDbEntry {
+ pub id: i64,
+ pub command_line: Vec,
+ pub start_timestamp: i64,
+ pub session_id: i64,
+ pub hostname: Vec,
+ pub cwd: Vec,
+ pub duration_ms: i64,
+ pub exit_status: i64,
+ pub more_info: Vec,
+}
+
+impl From for History {
+ fn from(histdb_item: HistDbEntry) -> Self {
+ let ts_secs = histdb_item.start_timestamp / 1000;
+ let ts_ns = (histdb_item.start_timestamp % 1000) * 1_000_000;
+ History::new(
+ DateTime::from_utc(NaiveDateTime::from_timestamp(ts_secs, ts_ns as u32), Utc),
+ String::from_utf8(histdb_item.command_line).unwrap(),
+ String::from_utf8(histdb_item.cwd).unwrap(),
+ histdb_item.exit_status,
+ histdb_item.duration_ms,
+ Some(format!("{:x}", histdb_item.session_id)),
+ Some(String::from_utf8(histdb_item.hostname).unwrap()),
+ None,
+ )
+ }
+}
+
+#[derive(Debug)]
+pub struct NuHistDb {
+ 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?;
+ hist_from_db_conn(pool).await
+}
+
+async fn hist_from_db_conn(pool: Pool) -> Result> {
+ let query = r#"
+ SELECT
+ id, command_line, start_timestamp, session_id, hostname, cwd, duration_ms, exit_status,
+ more_info
+ FROM history
+ ORDER BY start_timestamp
+ "#;
+ let histdb_vec: Vec = sqlx::query_as::<_, HistDbEntry>(query)
+ .fetch_all(&pool)
+ .await?;
+ Ok(histdb_vec)
+}
+
+impl NuHistDb {
+ pub fn histpath() -> Result {
+ let base = BaseDirs::new().ok_or_else(|| eyre!("could not determine data directory"))?;
+ let config_dir = base.config_dir().join("nushell");
+
+ let histdb_path = config_dir.join("history.sqlite3");
+ if histdb_path.exists() {
+ Ok(histdb_path)
+ } else {
+ Err(eyre!("Could not find history file."))
+ }
+ }
+}
+
+#[async_trait]
+impl Importer for NuHistDb {
+ // Not sure how this is used
+ const NAME: &'static str = "nu_histdb";
+
+ /// Creates a new NuHistDb and populates the history based on the pre-populated data
+ /// structure.
+ async fn new() -> Result {
+ let dbpath = NuHistDb::histpath()?;
+ let histdb_entry_vec = hist_from_db(dbpath).await?;
+ Ok(Self {
+ histdb: histdb_entry_vec,
+ })
+ }
+
+ async fn entries(&mut self) -> Result {
+ Ok(self.histdb.len())
+ }
+
+ async fn load(self, h: &mut impl Loader) -> Result<()> {
+ for i in self.histdb {
+ h.push(i.into()).await?;
+ }
+ Ok(())
+ }
+}
diff --git a/src/command/client/import.rs b/src/command/client/import.rs
index 7d7c2ca..7abc3d4 100644
--- a/src/command/client/import.rs
+++ b/src/command/client/import.rs
@@ -9,7 +9,8 @@ use atuin_client::{
database::Database,
history::History,
import::{
- bash::Bash, fish::Fish, resh::Resh, zsh::Zsh, zsh_histdb::ZshHistDb, Importer, Loader,
+ bash::Bash, fish::Fish, nu::Nu, nu_histdb::NuHistDb, resh::Resh, zsh::Zsh,
+ zsh_histdb::ZshHistDb, Importer, Loader,
},
};
@@ -29,6 +30,10 @@ pub enum Cmd {
Resh,
/// Import history from the fish history file
Fish,
+ /// Import history from the nu history file
+ Nu,
+ /// Import history from the nu history file
+ NuHistDb,
}
const BATCH_SIZE: usize = 100;
@@ -68,6 +73,17 @@ impl Cmd {
} else if shell.ends_with("/bash") {
println!("Detected Bash");
import::(db).await
+ } else if shell.ends_with("/nu") {
+ if NuHistDb::histpath().is_ok() {
+ println!(
+ "Detected Nu-HistDb, using :{}",
+ NuHistDb::histpath().unwrap().to_str().unwrap()
+ );
+ import::(db).await
+ } else {
+ println!("Detected Nushell");
+ import::(db).await
+ }
} else {
println!("cannot import {shell} history");
Ok(())
@@ -79,6 +95,8 @@ impl Cmd {
Self::Bash => import::(db).await,
Self::Resh => import::(db).await,
Self::Fish => import::(db).await,
+ Self::Nu => import::(db).await,
+ Self::NuHistDb => import::(db).await,
}
}
}
diff --git a/src/command/init.rs b/src/command/init.rs
index 585a828..7cb4b35 100644
--- a/src/command/init.rs
+++ b/src/command/init.rs
@@ -21,6 +21,8 @@ pub enum Shell {
Bash,
/// Fish setup
Fish,
+ /// Nu setup
+ Nu,
}
impl Cmd {
@@ -90,11 +92,53 @@ bind -M insert \e\[A _atuin_bind_up";
println!("end");
}
}
+
+ fn init_nu(&self) {
+ let full = include_str!("../shell/atuin.nu");
+ println!("{full}");
+
+ if std::env::var("ATUIN_NOBIND").is_err() {
+ const BIND_CTRL_R: &str = r#"let-env config = (
+ $env.config | upsert keybindings (
+ $env.config.keybindings
+ | append {
+ name: atuin
+ modifier: control
+ keycode: char_r
+ mode: emacs
+ event: { send: executehostcommand cmd: (_atuin_search_cmd) }
+ }
+ )
+)
+"#;
+ const BIND_UP_ARROW: &str = r#"let-env config = (
+ $env.config | upsert keybindings (
+ $env.config.keybindings
+ | append {
+ name: atuin
+ modifier: none
+ keycode: up
+ mode: emacs
+ event: { send: executehostcommand cmd: (_atuin_search_cmd '--shell-up-key-binding') }
+ }
+ )
+)
+"#;
+ if !self.disable_ctrl_r {
+ println!("{BIND_CTRL_R}");
+ }
+ if !self.disable_up_arrow {
+ println!("{BIND_UP_ARROW}");
+ }
+ }
+ }
+
pub fn run(self) {
match self.shell {
Shell::Zsh => self.init_zsh(),
Shell::Bash => self.init_bash(),
Shell::Fish => self.init_fish(),
+ Shell::Nu => self.init_nu(),
}
}
}
diff --git a/src/shell/atuin.nu b/src/shell/atuin.nu
new file mode 100644
index 0000000..f8886fc
--- /dev/null
+++ b/src/shell/atuin.nu
@@ -0,0 +1,41 @@
+# Source this in your ~/.config/nushell/config.nu
+let-env ATUIN_SESSION = (atuin uuid)
+
+# Magic token to make sure we don't record commands run by keybindings
+let ATUIN_KEYBINDING_TOKEN = $"# (random uuid)"
+
+let _atuin_pre_execution = {||
+ let cmd = (commandline)
+ if not ($cmd | str starts-with $ATUIN_KEYBINDING_TOKEN) {
+ let-env ATUIN_HISTORY_ID = (atuin history start -- $cmd)
+ }
+}
+
+let _atuin_pre_prompt = {||
+ let last_exit = $env.LAST_EXIT_CODE
+ if 'ATUIN_HISTORY_ID' not-in $env {
+ return
+ }
+ with-env { RUST_LOG: error } {
+ atuin history end --exit $last_exit -- $env.ATUIN_HISTORY_ID | null
+ }
+}
+
+def _atuin_search_cmd [...flags: string] {
+ [
+ $ATUIN_KEYBINDING_TOKEN,
+ ([
+ `commandline (sh -c 'RUST_LOG=error atuin search `,
+ $flags,
+ ` -i -- "$0" 3>&1 1>&2 2>&3' (commandline))`,
+ ] | flatten | str join ''),
+ ] | str join "\n"
+}
+
+let-env config = (
+ $env.config | upsert hooks (
+ $env.config.hooks
+ | upsert pre_execution ($env.config.hooks.pre_execution | append $_atuin_pre_execution)
+ | upsert pre_prompt ($env.config.hooks.pre_prompt | append $_atuin_pre_prompt)
+ )
+)