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:
Steven Xu 2023-03-27 01:44:06 +11:00 committed by GitHub
parent b911d17800
commit a7cb21a51b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 307 additions and 1 deletions

View file

@ -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

View file

@ -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;

View 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(())
}
}

View 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(())
}
}

View file

@ -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,
}
}
}

View file

@ -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
View 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)
)
)