diff --git a/README.md b/README.md
index 1533e3c..ff431fc 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,6 @@
Atuin
-Magical shell history
@@ -30,6 +29,7 @@
## Supported Shells
- zsh
+- bash
# Quickstart
@@ -43,12 +43,15 @@ atuin sync
## Install
-### AUR
+### Script (recommended)
-Atuin is available on the [AUR](https://aur.archlinux.org/packages/atuin/)
+The install script will help you through the setup, ensuring your shell is
+properly configured. It will also use one of the below methods, preferring the
+system package manager where possible (AUR, homebrew, etc etc).
```
-yay -S atuin # or your AUR helper of choice
+# do not run this as root, root will be asked for if required
+curl https://github.com/ellie/atuin/blob/main/install.sh | sh
```
### With cargo
@@ -60,6 +63,14 @@ toolchain, then you can run:
cargo install atuin
```
+### AUR
+
+Atuin is available on the [AUR](https://aur.archlinux.org/packages/atuin/)
+
+```
+yay -S atuin # or your AUR helper of choice
+```
+
### From source
```
@@ -68,15 +79,31 @@ cd atuin
cargo install --path .
```
-### Shell plugin
+## Shell plugin
-Once the binary is installed, the shell plugin requires installing. Add
+Once the binary is installed, the shell plugin requires installing. If you use
+the install script, this should all be done for you!
+
+### zsh
```
-eval "$(atuin init)"
+echo 'eval "$(atuin init zsh)"' >> ~/.zshrc
```
-to your `.zshrc`
+### bash
+
+We need to setup some hooks, so first install bash-preexec:
+
+```
+curl https://raw.githubusercontent.com/rcaloras/bash-preexec/master/bash-preexec.sh -o ~/.bash-preexec.sh
+echo '[[ -f ~/.bash-preexec.sh ]] && source ~/.bash-preexec.sh' >> ~/.bashrc
+```
+
+Then setup Atuin
+
+```
+echo 'eval "$(atuin init bash)"' >> ~/.bashrc
+```
## ...what's with the name?
diff --git a/atuin-client/src/import/bash.rs b/atuin-client/src/import/bash.rs
new file mode 100644
index 0000000..d5fbef4
--- /dev/null
+++ b/atuin-client/src/import/bash.rs
@@ -0,0 +1,79 @@
+use std::io::{BufRead, BufReader};
+use std::{fs::File, path::Path};
+
+use eyre::{eyre, Result};
+
+use super::count_lines;
+use crate::history::History;
+
+#[derive(Debug)]
+pub struct Bash {
+ file: BufReader,
+
+ pub loc: u64,
+ pub counter: i64,
+}
+
+impl Bash {
+ pub fn new(path: impl AsRef) -> Result {
+ let file = File::open(path)?;
+ let mut buf = BufReader::new(file);
+ let loc = count_lines(&mut buf)?;
+
+ Ok(Self {
+ file: buf,
+ loc: loc as u64,
+ counter: 0,
+ })
+ }
+
+ fn read_line(&mut self) -> Option> {
+ let mut line = String::new();
+
+ match self.file.read_line(&mut line) {
+ Ok(0) => None,
+ Ok(_) => Some(Ok(line)),
+ Err(e) => Some(Err(eyre!("failed to read line: {}", e))), // we can skip past things like invalid utf8
+ }
+ }
+}
+
+impl Iterator for Bash {
+ type Item = Result;
+
+ fn next(&mut self) -> Option {
+ let line = self.read_line()?;
+
+ if let Err(e) = line {
+ return Some(Err(e)); // :(
+ }
+
+ let mut line = line.unwrap();
+
+ while line.ends_with("\\\n") {
+ let next_line = self.read_line()?;
+
+ if next_line.is_err() {
+ break;
+ }
+
+ line.push_str(next_line.unwrap().as_str());
+ }
+
+ let time = chrono::Utc::now();
+ let offset = chrono::Duration::seconds(self.counter);
+ let time = time - offset;
+
+ self.counter += 1;
+
+ Some(Ok(History::new(
+ time,
+ line.trim_end().to_string(),
+ String::from("unknown"),
+ -1,
+ -1,
+ None,
+ None,
+ )))
+ }
+}
diff --git a/atuin-client/src/import/mod.rs b/atuin-client/src/import/mod.rs
new file mode 100644
index 0000000..3f8ea35
--- /dev/null
+++ b/atuin-client/src/import/mod.rs
@@ -0,0 +1,15 @@
+use std::fs::File;
+use std::io::{BufRead, BufReader, Seek, SeekFrom};
+
+use eyre::Result;
+
+pub mod bash;
+pub mod zsh;
+
+// this could probably be sped up
+fn count_lines(buf: &mut BufReader) -> Result {
+ let lines = buf.lines().count();
+ buf.seek(SeekFrom::Start(0))?;
+
+ Ok(lines)
+}
diff --git a/atuin-client/src/import.rs b/atuin-client/src/import/zsh.rs
similarity index 94%
rename from atuin-client/src/import.rs
rename to atuin-client/src/import/zsh.rs
index 3b0b2a6..46e9af6 100644
--- a/atuin-client/src/import.rs
+++ b/atuin-client/src/import/zsh.rs
@@ -1,7 +1,7 @@
// import old shell history!
// automatically hoover up all that we can find
-use std::io::{BufRead, BufReader, Seek, SeekFrom};
+use std::io::{BufRead, BufReader};
use std::{fs::File, path::Path};
use chrono::prelude::*;
@@ -9,7 +9,8 @@ use chrono::Utc;
use eyre::{eyre, Result};
use itertools::Itertools;
-use super::history::History;
+use super::count_lines;
+use crate::history::History;
#[derive(Debug)]
pub struct Zsh {
@@ -19,14 +20,6 @@ pub struct Zsh {
pub counter: i64,
}
-// this could probably be sped up
-fn count_lines(buf: &mut BufReader) -> Result {
- let lines = buf.lines().count();
- buf.seek(SeekFrom::Start(0))?;
-
- Ok(lines)
-}
-
impl Zsh {
pub fn new(path: impl AsRef) -> Result {
let file = File::open(path)?;
@@ -39,36 +32,7 @@ impl Zsh {
counter: 0,
})
}
-}
-fn parse_extended(line: &str, counter: i64) -> History {
- let line = line.replacen(": ", "", 2);
- let (time, duration) = line.splitn(2, ':').collect_tuple().unwrap();
- let (duration, command) = duration.splitn(2, ';').collect_tuple().unwrap();
-
- let time = time
- .parse::()
- .unwrap_or_else(|_| chrono::Utc::now().timestamp());
-
- let offset = chrono::Duration::milliseconds(counter);
- let time = Utc.timestamp(time, 0);
- let time = time + offset;
-
- let duration = duration.parse::().map_or(-1, |t| t * 1_000_000_000);
-
- // use nanos, because why the hell not? we won't display them.
- History::new(
- time,
- command.trim_end().to_string(),
- String::from("unknown"),
- 0, // assume 0, we have no way of knowing :(
- duration,
- None,
- None,
- )
-}
-
-impl Zsh {
fn read_line(&mut self) -> Option> {
let mut line = String::new();
@@ -140,6 +104,33 @@ impl Iterator for Zsh {
}
}
+fn parse_extended(line: &str, counter: i64) -> History {
+ let line = line.replacen(": ", "", 2);
+ let (time, duration) = line.splitn(2, ':').collect_tuple().unwrap();
+ let (duration, command) = duration.splitn(2, ';').collect_tuple().unwrap();
+
+ let time = time
+ .parse::()
+ .unwrap_or_else(|_| chrono::Utc::now().timestamp());
+
+ let offset = chrono::Duration::milliseconds(counter);
+ let time = Utc.timestamp(time, 0);
+ let time = time + offset;
+
+ let duration = duration.parse::().map_or(-1, |t| t * 1_000_000_000);
+
+ // use nanos, because why the hell not? we won't display them.
+ History::new(
+ time,
+ command.trim_end().to_string(),
+ String::from("unknown"),
+ 0, // assume 0, we have no way of knowing :(
+ duration,
+ None,
+ None,
+ )
+}
+
#[cfg(test)]
mod test {
use chrono::prelude::*;
diff --git a/atuin-client/src/sync.rs b/atuin-client/src/sync.rs
index 813c2ed..5c6405d 100644
--- a/atuin-client/src/sync.rs
+++ b/atuin-client/src/sync.rs
@@ -123,6 +123,8 @@ async fn sync_upload(
client.post_history(&buffer).await?;
cursor = buffer.last().unwrap().timestamp;
remote_count = client.count().await?;
+
+ debug!("upload cursor: {:?}", cursor);
}
Ok(())
diff --git a/diesel.toml b/diesel.toml
deleted file mode 100644
index 92267c8..0000000
--- a/diesel.toml
+++ /dev/null
@@ -1,5 +0,0 @@
-# For documentation on how to configure this file,
-# see diesel.rs/guides/configuring-diesel-cli
-
-[print_schema]
-file = "src/schema.rs"
diff --git a/src/command/import.rs b/src/command/import.rs
index 931e7af..09df583 100644
--- a/src/command/import.rs
+++ b/src/command/import.rs
@@ -7,7 +7,7 @@ use structopt::StructOpt;
use atuin_client::database::Database;
use atuin_client::history::History;
-use atuin_client::import::Zsh;
+use atuin_client::import::{bash::Bash, zsh::Zsh};
use indicatif::ProgressBar;
#[derive(StructOpt)]
@@ -23,11 +23,17 @@ pub enum Cmd {
aliases=&["z", "zs"],
)]
Zsh,
+
+ #[structopt(
+ about="import history from the bash history file",
+ aliases=&["b", "ba", "bas"],
+ )]
+ Bash,
}
impl Cmd {
pub async fn run(&self, db: &mut (impl Database + Send + Sync)) -> Result<()> {
- println!(" A'Tuin ");
+ println!(" Atuin ");
println!("======================");
println!(" \u{1f30d} ");
println!(" \u{1f418}\u{1f418}\u{1f418}\u{1f418} ");
@@ -49,6 +55,7 @@ impl Cmd {
}
Self::Zsh => import_zsh(db).await,
+ Self::Bash => import_bash(db).await,
}
}
}
@@ -120,3 +127,61 @@ async fn import_zsh(db: &mut (impl Database + Send + Sync)) -> Result<()> {
Ok(())
}
+
+// TODO: don't just copy paste this lol
+async fn import_bash(db: &mut (impl Database + Send + Sync)) -> Result<()> {
+ // oh-my-zsh sets HISTFILE=~/.zhistory
+ // zsh has no default value for this var, but uses ~/.zhistory.
+ // we could maybe be smarter about this in the future :)
+
+ let histpath = env::var("HISTFILE");
+
+ let histpath = if let Ok(p) = histpath {
+ let histpath = PathBuf::from(p);
+
+ if !histpath.exists() {
+ return Err(eyre!(
+ "Could not find history file {:?}. try updating $HISTFILE",
+ histpath
+ ));
+ }
+
+ histpath
+ } else {
+ let user_dirs = UserDirs::new().unwrap();
+ let home_dir = user_dirs.home_dir();
+
+ home_dir.join(".bash_history")
+ };
+
+ let bash = Bash::new(histpath)?;
+
+ let progress = ProgressBar::new(bash.loc);
+
+ let buf_size = 100;
+ let mut buf = Vec::::with_capacity(buf_size);
+
+ for i in bash
+ .filter_map(Result::ok)
+ .filter(|x| !x.command.trim().is_empty())
+ {
+ buf.push(i);
+
+ if buf.len() == buf_size {
+ db.save_bulk(&buf).await?;
+ progress.inc(buf.len() as u64);
+
+ buf.clear();
+ }
+ }
+
+ if !buf.is_empty() {
+ db.save_bulk(&buf).await?;
+ progress.inc(buf.len() as u64);
+ }
+
+ progress.finish();
+ println!("Import complete!");
+
+ Ok(())
+}
diff --git a/src/command/init.rs b/src/command/init.rs
index 022021d..ed1555a 100644
--- a/src/command/init.rs
+++ b/src/command/init.rs
@@ -1,19 +1,32 @@
use std::env;
use eyre::{eyre, Result};
+use structopt::StructOpt;
+
+#[derive(StructOpt)]
+pub enum Cmd {
+ #[structopt(about = "zsh setup")]
+ Zsh,
+ #[structopt(about = "bash setup")]
+ Bash,
+}
fn init_zsh() {
let full = include_str!("../shell/atuin.zsh");
println!("{}", full);
}
-pub fn init() -> Result<()> {
- let shell = env::var("SHELL")?;
+fn init_bash() {
+ let full = include_str!("../shell/atuin.bash");
+ println!("{}", full);
+}
- if shell.ends_with("zsh") {
- init_zsh();
+impl Cmd {
+ pub fn run(&self) -> Result<()> {
+ match self {
+ Self::Zsh => init_zsh(),
+ Self::Bash => init_bash(),
+ }
Ok(())
- } else {
- Err(eyre!("Could not detect shell, or shell unsupported"))
}
}
diff --git a/src/command/mod.rs b/src/command/mod.rs
index 78e6402..b16aae4 100644
--- a/src/command/mod.rs
+++ b/src/command/mod.rs
@@ -37,7 +37,7 @@ pub enum AtuinCmd {
Stats(stats::Cmd),
#[structopt(about = "output shell setup")]
- Init,
+ Init(init::Cmd),
#[structopt(about = "generates a UUID")]
Uuid,
@@ -101,7 +101,7 @@ impl AtuinCmd {
Self::Import(import) => import.run(&mut db).await,
Self::Server(server) => server.run(&server_settings).await,
Self::Stats(stats) => stats.run(&mut db, &client_settings).await,
- Self::Init => init::init(),
+ Self::Init(init) => init.run(),
Self::Search {
cwd,
exit,
diff --git a/src/shell/atuin.bash b/src/shell/atuin.bash
new file mode 100644
index 0000000..43de364
--- /dev/null
+++ b/src/shell/atuin.bash
@@ -0,0 +1,30 @@
+_atuin_preexec() {
+ id=$(atuin history start "$1")
+ export ATUIN_HISTORY_ID="$id"
+}
+
+_atuin_precmd() {
+ local EXIT="$?"
+
+ [[ -z "${ATUIN_HISTORY_ID}" ]] && return
+
+
+ (RUST_LOG=error atuin history end $ATUIN_HISTORY_ID --exit $EXIT &) > /dev/null 2>&1
+}
+
+
+__atuin_history ()
+{
+ tput rmkx
+ HISTORY="$(RUST_LOG=error atuin search -i $BUFFER 3>&1 1>&2 2>&3)"
+ tput smkx
+
+ READLINE_LINE=${HISTORY}
+ READLINE_POINT=${#READLINE_LINE}
+}
+
+
+preexec_functions+=(_atuin_preexec)
+precmd_functions+=(_atuin_precmd)
+
+bind -x '"\C-r": __atuin_history'
diff --git a/src/shell/atuin.zsh b/src/shell/atuin.zsh
index cdef5e5..6a24de5 100644
--- a/src/shell/atuin.zsh
+++ b/src/shell/atuin.zsh
@@ -6,7 +6,7 @@ export ATUIN_HISTORY="atuin history list"
export ATUIN_BINDKEYS="true"
_atuin_preexec(){
- id=$(atuin history start $1)
+ id=$(atuin history start "$1")
export ATUIN_HISTORY_ID="$id"
}