Fish importing (#234)
* make a start on fish * fix * test * enable fish * fmt * update histpath set up fish init script * update readme * cover edge case * fmt * fix session variables Co-authored-by: PJ <me@panekj.dev> * respect NOBIND Co-authored-by: PJ <me@panekj.dev> * fix env var setting Co-authored-by: PJ <me@panekj.dev> * fix whitespace Co-authored-by: PJ <me@panekj.dev> * add fish to supported shells Co-authored-by: PJ <me@panekj.dev>
This commit is contained in:
parent
6daaeb22b0
commit
87df7d80ec
6 changed files with 280 additions and 0 deletions
11
README.md
11
README.md
|
@ -65,6 +65,7 @@ I wanted to. And I **really** don't want to.
|
||||||
|
|
||||||
- zsh
|
- zsh
|
||||||
- bash
|
- bash
|
||||||
|
- fish
|
||||||
|
|
||||||
# Quickstart
|
# Quickstart
|
||||||
|
|
||||||
|
@ -166,6 +167,16 @@ Then setup Atuin
|
||||||
echo 'eval "$(atuin init bash)"' >> ~/.bashrc
|
echo 'eval "$(atuin init bash)"' >> ~/.bashrc
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### fish
|
||||||
|
|
||||||
|
Add
|
||||||
|
|
||||||
|
```
|
||||||
|
atuin init fish | source
|
||||||
|
```
|
||||||
|
|
||||||
|
to your `is-interactive` block in your `~/.config/fish/config.fish` file
|
||||||
|
|
||||||
## ...what's with the name?
|
## ...what's with the name?
|
||||||
|
|
||||||
Atuin is named after "The Great A'Tuin", a giant turtle from Terry Pratchett's
|
Atuin is named after "The Great A'Tuin", a giant turtle from Terry Pratchett's
|
||||||
|
|
221
atuin-client/src/import/fish.rs
Normal file
221
atuin-client/src/import/fish.rs
Normal file
|
@ -0,0 +1,221 @@
|
||||||
|
// import old shell history!
|
||||||
|
// automatically hoover up all that we can find
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
fs::File,
|
||||||
|
io::{self, BufRead, BufReader, Read, Seek},
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
use chrono::prelude::*;
|
||||||
|
use chrono::Utc;
|
||||||
|
use directories::BaseDirs;
|
||||||
|
use eyre::{eyre, Result};
|
||||||
|
|
||||||
|
use super::{count_lines, Importer};
|
||||||
|
use crate::history::History;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Fish<R> {
|
||||||
|
file: BufReader<R>,
|
||||||
|
strbuf: String,
|
||||||
|
loc: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: Read + Seek> Fish<R> {
|
||||||
|
fn new(r: R) -> Result<Self> {
|
||||||
|
let mut buf = BufReader::new(r);
|
||||||
|
let loc = count_lines(&mut buf)?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
file: buf,
|
||||||
|
strbuf: String::new(),
|
||||||
|
loc,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: Read> Fish<R> {
|
||||||
|
fn new_entry(&mut self) -> io::Result<bool> {
|
||||||
|
let inner = self.file.fill_buf()?;
|
||||||
|
Ok(inner.starts_with(b"- "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Importer for Fish<File> {
|
||||||
|
const NAME: &'static str = "fish";
|
||||||
|
|
||||||
|
/// see https://fishshell.com/docs/current/interactive.html#searchable-command-history
|
||||||
|
fn histpath() -> Result<PathBuf> {
|
||||||
|
let base = BaseDirs::new().ok_or_else(|| eyre!("could not determine data directory"))?;
|
||||||
|
let data = base.data_local_dir();
|
||||||
|
|
||||||
|
// fish supports multiple history sessions
|
||||||
|
// If `fish_history` var is missing, or set to `default`, use `fish` as the session
|
||||||
|
let session = std::env::var("fish_history").unwrap_or_else(|_| String::from("fish"));
|
||||||
|
let session = if session == "default" {
|
||||||
|
String::from("fish")
|
||||||
|
} else {
|
||||||
|
session
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut histpath = data.join("fish");
|
||||||
|
histpath.push(format!("{}_history", session));
|
||||||
|
|
||||||
|
if histpath.exists() {
|
||||||
|
Ok(histpath)
|
||||||
|
} else {
|
||||||
|
Err(eyre!("Could not find history file. Try setting $HISTFILE"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse(path: impl AsRef<Path>) -> Result<Self> {
|
||||||
|
Self::new(File::open(path)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: Read> Iterator for Fish<R> {
|
||||||
|
type Item = Result<History>;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
let mut time: Option<DateTime<Utc>> = None;
|
||||||
|
let mut cmd: Option<String> = None;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
self.strbuf.clear();
|
||||||
|
match self.file.read_line(&mut self.strbuf) {
|
||||||
|
// no more content to read
|
||||||
|
Ok(0) => break,
|
||||||
|
// bail on IO error
|
||||||
|
Err(e) => return Some(Err(e.into())),
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
// `read_line` adds the line delimeter to the string. No thanks
|
||||||
|
self.strbuf.pop();
|
||||||
|
|
||||||
|
if let Some(c) = self.strbuf.strip_prefix("- cmd: ") {
|
||||||
|
// using raw strings to avoid needing escaping.
|
||||||
|
// replaces double backslashes with single backslashes
|
||||||
|
let c = c.replace(r"\\", r"\");
|
||||||
|
// replaces escaped newlines
|
||||||
|
let c = c.replace(r"\n", "\n");
|
||||||
|
// TODO: any other escape characters?
|
||||||
|
|
||||||
|
cmd = Some(c);
|
||||||
|
} else if let Some(t) = self.strbuf.strip_prefix(" when: ") {
|
||||||
|
// if t is not an int, just ignore this line
|
||||||
|
if let Ok(t) = t.parse::<i64>() {
|
||||||
|
time = Some(Utc.timestamp(t, 0));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// ... ignore paths lines
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.new_entry() {
|
||||||
|
// next line is a new entry, so let's stop here
|
||||||
|
// only if we have found a command though
|
||||||
|
Ok(true) if cmd.is_some() => break,
|
||||||
|
// bail on IO error
|
||||||
|
Err(e) => return Some(Err(e.into())),
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let cmd = cmd?;
|
||||||
|
let time = time.unwrap_or_else(Utc::now);
|
||||||
|
|
||||||
|
Some(Ok(History::new(
|
||||||
|
time,
|
||||||
|
cmd,
|
||||||
|
"unknown".into(),
|
||||||
|
-1,
|
||||||
|
-1,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||||
|
// worst case, entry per line
|
||||||
|
(0, Some(self.loc))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use chrono::{TimeZone, Utc};
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
use super::Fish;
|
||||||
|
use crate::history::History;
|
||||||
|
|
||||||
|
// simple wrapper for fish history entry
|
||||||
|
macro_rules! fishtory {
|
||||||
|
($timestamp:literal, $command:literal) => {
|
||||||
|
History::new(
|
||||||
|
Utc.timestamp($timestamp, 0),
|
||||||
|
$command.into(),
|
||||||
|
"unknown".into(),
|
||||||
|
-1,
|
||||||
|
-1,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_complex() {
|
||||||
|
// complicated input with varying contents and escaped strings.
|
||||||
|
let input = r#"- cmd: history --help
|
||||||
|
when: 1639162832
|
||||||
|
- cmd: cat ~/.bash_history
|
||||||
|
when: 1639162851
|
||||||
|
paths:
|
||||||
|
- ~/.bash_history
|
||||||
|
- cmd: ls ~/.local/share/fish/fish_history
|
||||||
|
when: 1639162890
|
||||||
|
paths:
|
||||||
|
- ~/.local/share/fish/fish_history
|
||||||
|
- cmd: cat ~/.local/share/fish/fish_history
|
||||||
|
when: 1639162893
|
||||||
|
paths:
|
||||||
|
- ~/.local/share/fish/fish_history
|
||||||
|
ERROR
|
||||||
|
- CORRUPTED: ENTRY
|
||||||
|
CONTINUE:
|
||||||
|
- AS
|
||||||
|
- NORMAL
|
||||||
|
- cmd: echo "foo" \\\n'bar' baz
|
||||||
|
when: 1639162933
|
||||||
|
- cmd: cat ~/.local/share/fish/fish_history
|
||||||
|
when: 1639162939
|
||||||
|
paths:
|
||||||
|
- ~/.local/share/fish/fish_history
|
||||||
|
- cmd: echo "\\"" \\\\ "\\\\"
|
||||||
|
when: 1639163063
|
||||||
|
- cmd: cat ~/.local/share/fish/fish_history
|
||||||
|
when: 1639163066
|
||||||
|
paths:
|
||||||
|
- ~/.local/share/fish/fish_history
|
||||||
|
"#;
|
||||||
|
let cursor = Cursor::new(input);
|
||||||
|
let fish = Fish::new(cursor).unwrap();
|
||||||
|
|
||||||
|
let history = fish.collect::<Result<Vec<_>, _>>().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
history,
|
||||||
|
vec![
|
||||||
|
fishtory!(1639162832, "history --help"),
|
||||||
|
fishtory!(1639162851, "cat ~/.bash_history"),
|
||||||
|
fishtory!(1639162890, "ls ~/.local/share/fish/fish_history"),
|
||||||
|
fishtory!(1639162893, "cat ~/.local/share/fish/fish_history"),
|
||||||
|
fishtory!(1639162933, "echo \"foo\" \\\n'bar' baz"),
|
||||||
|
fishtory!(1639162939, "cat ~/.local/share/fish/fish_history"),
|
||||||
|
fishtory!(1639163063, r#"echo "\"" \\ "\\""#),
|
||||||
|
fishtory!(1639163066, "cat ~/.local/share/fish/fish_history"),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ use eyre::Result;
|
||||||
use crate::history::History;
|
use crate::history::History;
|
||||||
|
|
||||||
pub mod bash;
|
pub mod bash;
|
||||||
|
pub mod fish;
|
||||||
pub mod resh;
|
pub mod resh;
|
||||||
pub mod zsh;
|
pub mod zsh;
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use std::{env, path::PathBuf};
|
use std::{env, path::PathBuf};
|
||||||
|
|
||||||
|
use atuin_client::import::fish::Fish;
|
||||||
use eyre::{eyre, Result};
|
use eyre::{eyre, Result};
|
||||||
use structopt::StructOpt;
|
use structopt::StructOpt;
|
||||||
|
|
||||||
|
@ -33,6 +34,12 @@ pub enum Cmd {
|
||||||
aliases=&["r", "re", "res"],
|
aliases=&["r", "re", "res"],
|
||||||
)]
|
)]
|
||||||
Resh,
|
Resh,
|
||||||
|
|
||||||
|
#[structopt(
|
||||||
|
about="import history from the fish history file",
|
||||||
|
aliases=&["f", "fi", "fis"],
|
||||||
|
)]
|
||||||
|
Fish,
|
||||||
}
|
}
|
||||||
|
|
||||||
const BATCH_SIZE: usize = 100;
|
const BATCH_SIZE: usize = 100;
|
||||||
|
@ -54,6 +61,9 @@ impl Cmd {
|
||||||
if shell.ends_with("/zsh") {
|
if shell.ends_with("/zsh") {
|
||||||
println!("Detected ZSH");
|
println!("Detected ZSH");
|
||||||
import::<Zsh<_>, _>(db, BATCH_SIZE).await
|
import::<Zsh<_>, _>(db, BATCH_SIZE).await
|
||||||
|
} else if shell.ends_with("/fish") {
|
||||||
|
println!("Detected Fish");
|
||||||
|
import::<Fish<_>, _>(db, BATCH_SIZE).await
|
||||||
} else {
|
} else {
|
||||||
println!("cannot import {} history", shell);
|
println!("cannot import {} history", shell);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -63,6 +73,7 @@ impl Cmd {
|
||||||
Self::Zsh => import::<Zsh<_>, _>(db, BATCH_SIZE).await,
|
Self::Zsh => import::<Zsh<_>, _>(db, BATCH_SIZE).await,
|
||||||
Self::Bash => import::<Bash<_>, _>(db, BATCH_SIZE).await,
|
Self::Bash => import::<Bash<_>, _>(db, BATCH_SIZE).await,
|
||||||
Self::Resh => import::<Resh, _>(db, BATCH_SIZE).await,
|
Self::Resh => import::<Resh, _>(db, BATCH_SIZE).await,
|
||||||
|
Self::Fish => import::<Fish<_>, _>(db, BATCH_SIZE).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,8 @@ pub enum Cmd {
|
||||||
Zsh,
|
Zsh,
|
||||||
#[structopt(about = "bash setup")]
|
#[structopt(about = "bash setup")]
|
||||||
Bash,
|
Bash,
|
||||||
|
#[structopt(about = "fish setup")]
|
||||||
|
Fish,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_zsh() {
|
fn init_zsh() {
|
||||||
|
@ -18,11 +20,17 @@ fn init_bash() {
|
||||||
println!("{}", full);
|
println!("{}", full);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn init_fish() {
|
||||||
|
let full = include_str!("../shell/atuin.fish");
|
||||||
|
println!("{}", full);
|
||||||
|
}
|
||||||
|
|
||||||
impl Cmd {
|
impl Cmd {
|
||||||
pub fn run(&self) {
|
pub fn run(&self) {
|
||||||
match self {
|
match self {
|
||||||
Self::Zsh => init_zsh(),
|
Self::Zsh => init_zsh(),
|
||||||
Self::Bash => init_bash(),
|
Self::Bash => init_bash(),
|
||||||
|
Self::Fish => init_fish(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
28
src/shell/atuin.fish
Normal file
28
src/shell/atuin.fish
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
|
||||||
|
set -gx ATUIN_SESSION (atuin uuid)
|
||||||
|
set -gx ATUIN_HISTORY (atuin history list)
|
||||||
|
|
||||||
|
function _atuin_preexec --on-event fish_preexec
|
||||||
|
set -gx ATUIN_HISTORY_ID (atuin history start "$argv[1]")
|
||||||
|
end
|
||||||
|
|
||||||
|
function _atuin_postexec --on-event fish_postexec
|
||||||
|
set s $status
|
||||||
|
if test -n "$ATUIN_HISTORY_ID"
|
||||||
|
RUST_LOG=error atuin history end $ATUIN_HISTORY_ID --exit $s &; disown
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function _atuin_search
|
||||||
|
set h (RUST_LOG=error atuin search -i (commandline -b) 3>&1 1>&2 2>&3)
|
||||||
|
commandline -f repaint
|
||||||
|
if test -n "$h"
|
||||||
|
commandline -r $h
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if test -z $ATUIN_NOBIND
|
||||||
|
bind -k up '_atuin_search'
|
||||||
|
bind \eOA '_atuin_search'
|
||||||
|
bind \e\[A '_atuin_search'
|
||||||
|
end
|
Loading…
Reference in a new issue