Support bash, resolves #3
This commit is contained in:
parent
4f16e8411e
commit
7b5c3d543d
11 changed files with 280 additions and 63 deletions
43
README.md
43
README.md
|
@ -1,7 +1,6 @@
|
|||
<h1 align="center">
|
||||
Atuin
|
||||
</h1>
|
||||
<em align="center">Magical shell history</em>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/ellie/atuin/actions?query=workflow%3ARust"><img src="https://img.shields.io/github/workflow/status/ellie/atuin/Rust?style=flat-square" /></a>
|
||||
|
@ -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?
|
||||
|
||||
|
|
79
atuin-client/src/import/bash.rs
Normal file
79
atuin-client/src/import/bash.rs
Normal file
|
@ -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<File>,
|
||||
|
||||
pub loc: u64,
|
||||
pub counter: i64,
|
||||
}
|
||||
|
||||
impl Bash {
|
||||
pub fn new(path: impl AsRef<Path>) -> Result<Self> {
|
||||
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<Result<String>> {
|
||||
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<History>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
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,
|
||||
)))
|
||||
}
|
||||
}
|
15
atuin-client/src/import/mod.rs
Normal file
15
atuin-client/src/import/mod.rs
Normal file
|
@ -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<File>) -> Result<usize> {
|
||||
let lines = buf.lines().count();
|
||||
buf.seek(SeekFrom::Start(0))?;
|
||||
|
||||
Ok(lines)
|
||||
}
|
|
@ -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<File>) -> Result<usize> {
|
||||
let lines = buf.lines().count();
|
||||
buf.seek(SeekFrom::Start(0))?;
|
||||
|
||||
Ok(lines)
|
||||
}
|
||||
|
||||
impl Zsh {
|
||||
pub fn new(path: impl AsRef<Path>) -> Result<Self> {
|
||||
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::<i64>()
|
||||
.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::<i64>().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<Result<String>> {
|
||||
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::<i64>()
|
||||
.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::<i64>().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::*;
|
|
@ -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(())
|
||||
|
|
|
@ -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"
|
|
@ -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::<History>::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(())
|
||||
}
|
||||
|
|
|
@ -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"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
30
src/shell/atuin.bash
Normal file
30
src/shell/atuin.bash
Normal file
|
@ -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'
|
|
@ -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"
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue