summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorConrad Ludgate <conradludgate@gmail.com>2021-12-11 09:48:53 +0000
committerGitHub <noreply@github.com>2021-12-11 09:48:53 +0000
commit87df7d80eca0ede9e149d1ef533e71650e4b919a (patch)
tree389e2c5b32208a8a05259d308af857f7a61cb7f9
parent6daaeb22b0124c5262286a01fd9e69be3eb19604 (diff)
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>
-rw-r--r--README.md11
-rw-r--r--atuin-client/src/import/fish.rs221
-rw-r--r--atuin-client/src/import/mod.rs1
-rw-r--r--src/command/import.rs11
-rw-r--r--src/command/init.rs8
-rw-r--r--src/shell/atuin.fish28
6 files changed, 280 insertions, 0 deletions
diff --git a/README.md b/README.md
index 5f3fe338..0edd8a44 100644
--- a/README.md
+++ b/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
diff --git a/atuin-client/src/import/fish.rs b/atuin-client/src/import/fish.rs
new file mode 100644
index 00000000..4079e122
--- /dev/null
+++ b/atuin-client/src/import/fish.rs
@@ -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"),
+ ]
+ );
+ }
+}
diff --git a/atuin-client/src/import/mod.rs b/atuin-client/src/import/mod.rs
index d73d3857..471b7f98 100644
--- a/atuin-client/src/import/mod.rs
+++ b/atuin-client/src/import/mod.rs
@@ -6,6 +6,7 @@ use eyre::Result;
use crate::history::History;
pub mod bash;
+pub mod fish;
pub mod resh;
pub mod zsh;
diff --git a/src/command/import.rs b/src/command/import.rs
index 53940abb..166fcd3e 100644
--- a/src/command/import.rs
+++ b/src/command/import.rs
@@ -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,
}
}
}
diff --git a/src/command/init.rs b/src/command/init.rs
index b6fbe4b3..5d3ffed2 100644
--- a/src/command/init.rs
+++ b/src/command/init.rs
@@ -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(),
}
}
}
diff --git a/src/shell/atuin.fish b/src/shell/atuin.fish
new file mode 100644
index 00000000..5d59d01d
--- /dev/null
+++ b/src/shell/atuin.fish
@@ -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