feat: add *Nushell* support (#788)
* feat: add *Nushell* support * refactor: use `sh` to swap `STDOUT` and `STDERR` instead of using a temporary file * feat: include both keybindings, with the current REPL buffer passed to *Atuin*'s * feat: don't record commands run by keybindings
This commit is contained in:
parent
b911d17800
commit
a7cb21a51b
7 changed files with 307 additions and 1 deletions
15
README.md
15
README.md
|
@ -274,6 +274,21 @@ Install `atuin` shell plugin in zsh, bash, or fish with [Fig](https://fig.io) in
|
|||
|
||||
<a href="https://fig.io/plugins/other/atuin" target="_blank"><img src="https://fig.io/badges/install-with-fig.svg" /></a>
|
||||
|
||||
### 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
|
||||
|
|
|
@ -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;
|
||||
|
|
76
atuin-client/src/import/nu.rs
Normal file
76
atuin-client/src/import/nu.rs
Normal file
|
@ -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<u8>,
|
||||
}
|
||||
|
||||
fn get_histpath() -> Result<PathBuf> {
|
||||
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<Self> {
|
||||
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<usize> {
|
||||
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(())
|
||||
}
|
||||
}
|
110
atuin-client/src/import/nu_histdb.rs
Normal file
110
atuin-client/src/import/nu_histdb.rs
Normal file
|
@ -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<u8>,
|
||||
pub start_timestamp: i64,
|
||||
pub session_id: i64,
|
||||
pub hostname: Vec<u8>,
|
||||
pub cwd: Vec<u8>,
|
||||
pub duration_ms: i64,
|
||||
pub exit_status: i64,
|
||||
pub more_info: Vec<u8>,
|
||||
}
|
||||
|
||||
impl From<HistDbEntry> 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<HistDbEntry>,
|
||||
}
|
||||
|
||||
/// Read db at given file, return vector of entries.
|
||||
async fn hist_from_db(dbpath: PathBuf) -> Result<Vec<HistDbEntry>> {
|
||||
let pool = SqlitePool::connect(dbpath.to_str().unwrap()).await?;
|
||||
hist_from_db_conn(pool).await
|
||||
}
|
||||
|
||||
async fn hist_from_db_conn(pool: Pool<sqlx::Sqlite>) -> Result<Vec<HistDbEntry>> {
|
||||
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<HistDbEntry> = sqlx::query_as::<_, HistDbEntry>(query)
|
||||
.fetch_all(&pool)
|
||||
.await?;
|
||||
Ok(histdb_vec)
|
||||
}
|
||||
|
||||
impl NuHistDb {
|
||||
pub fn histpath() -> Result<PathBuf> {
|
||||
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<Self> {
|
||||
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<usize> {
|
||||
Ok(self.histdb.len())
|
||||
}
|
||||
|
||||
async fn load(self, h: &mut impl Loader) -> Result<()> {
|
||||
for i in self.histdb {
|
||||
h.push(i.into()).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -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::<Bash, DB>(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::<NuHistDb, DB>(db).await
|
||||
} else {
|
||||
println!("Detected Nushell");
|
||||
import::<Nu, DB>(db).await
|
||||
}
|
||||
} else {
|
||||
println!("cannot import {shell} history");
|
||||
Ok(())
|
||||
|
@ -79,6 +95,8 @@ impl Cmd {
|
|||
Self::Bash => import::<Bash, DB>(db).await,
|
||||
Self::Resh => import::<Resh, DB>(db).await,
|
||||
Self::Fish => import::<Fish, DB>(db).await,
|
||||
Self::Nu => import::<Nu, DB>(db).await,
|
||||
Self::NuHistDb => import::<NuHistDb, DB>(db).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
41
src/shell/atuin.nu
Normal file
41
src/shell/atuin.nu
Normal file
|
@ -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)
|
||||
)
|
||||
)
|
Loading…
Reference in a new issue