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
|
||||
- bash
|
||||
- fish
|
||||
|
||||
# Quickstart
|
||||
|
||||
|
@ -166,6 +167,16 @@ Then setup Atuin
|
|||
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?
|
||||
|
||||
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;
|
||||
|
||||
pub mod bash;
|
||||
pub mod fish;
|
||||
pub mod resh;
|
||||
pub mod zsh;
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use std::{env, path::PathBuf};
|
||||
|
||||
use atuin_client::import::fish::Fish;
|
||||
use eyre::{eyre, Result};
|
||||
use structopt::StructOpt;
|
||||
|
||||
|
@ -33,6 +34,12 @@ pub enum Cmd {
|
|||
aliases=&["r", "re", "res"],
|
||||
)]
|
||||
Resh,
|
||||
|
||||
#[structopt(
|
||||
about="import history from the fish history file",
|
||||
aliases=&["f", "fi", "fis"],
|
||||
)]
|
||||
Fish,
|
||||
}
|
||||
|
||||
const BATCH_SIZE: usize = 100;
|
||||
|
@ -54,6 +61,9 @@ impl Cmd {
|
|||
if shell.ends_with("/zsh") {
|
||||
println!("Detected ZSH");
|
||||
import::<Zsh<_>, _>(db, BATCH_SIZE).await
|
||||
} else if shell.ends_with("/fish") {
|
||||
println!("Detected Fish");
|
||||
import::<Fish<_>, _>(db, BATCH_SIZE).await
|
||||
} else {
|
||||
println!("cannot import {} history", shell);
|
||||
Ok(())
|
||||
|
@ -63,6 +73,7 @@ impl Cmd {
|
|||
Self::Zsh => import::<Zsh<_>, _>(db, BATCH_SIZE).await,
|
||||
Self::Bash => import::<Bash<_>, _>(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,
|
||||
#[structopt(about = "bash setup")]
|
||||
Bash,
|
||||
#[structopt(about = "fish setup")]
|
||||
Fish,
|
||||
}
|
||||
|
||||
fn init_zsh() {
|
||||
|
@ -18,11 +20,17 @@ fn init_bash() {
|
|||
println!("{}", full);
|
||||
}
|
||||
|
||||
fn init_fish() {
|
||||
let full = include_str!("../shell/atuin.fish");
|
||||
println!("{}", full);
|
||||
}
|
||||
|
||||
impl Cmd {
|
||||
pub fn run(&self) {
|
||||
match self {
|
||||
Self::Zsh => init_zsh(),
|
||||
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