Skip to content

Commit

Permalink
Fish importing (atuinsh#234)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

* respect NOBIND

Co-authored-by: PJ <[email protected]>

* fix env var setting

Co-authored-by: PJ <[email protected]>

* fix whitespace

Co-authored-by: PJ <[email protected]>

* add fish to supported shells

Co-authored-by: PJ <[email protected]>
  • Loading branch information
conradludgate and panekj authored Dec 11, 2021
1 parent 6daaeb2 commit 87df7d8
Show file tree
Hide file tree
Showing 6 changed files with 280 additions and 0 deletions.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ I wanted to. And I **really** don't want to.

- zsh
- bash
- fish

# Quickstart

Expand Down Expand Up @@ -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
Expand Down
221 changes: 221 additions & 0 deletions atuin-client/src/import/fish.rs
Original file line number Diff line number Diff line change
@@ -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"),
]
);
}
}
1 change: 1 addition & 0 deletions atuin-client/src/import/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use eyre::Result;
use crate::history::History;

pub mod bash;
pub mod fish;
pub mod resh;
pub mod zsh;

Expand Down
11 changes: 11 additions & 0 deletions src/command/import.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::{env, path::PathBuf};

use atuin_client::import::fish::Fish;
use eyre::{eyre, Result};
use structopt::StructOpt;

Expand Down Expand Up @@ -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;
Expand All @@ -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(())
Expand All @@ -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,
}
}
}
Expand Down
8 changes: 8 additions & 0 deletions src/command/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ pub enum Cmd {
Zsh,
#[structopt(about = "bash setup")]
Bash,
#[structopt(about = "fish setup")]
Fish,
}

fn init_zsh() {
Expand All @@ -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 changes: 28 additions & 0 deletions src/shell/atuin.fish
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 87df7d8

Please sign in to comment.