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">
|
<h1 align="center">
|
||||||
Atuin
|
Atuin
|
||||||
</h1>
|
</h1>
|
||||||
<em align="center">Magical shell history</em>
|
|
||||||
|
|
||||||
<p align="center">
|
<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>
|
<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
|
## Supported Shells
|
||||||
|
|
||||||
- zsh
|
- zsh
|
||||||
|
- bash
|
||||||
|
|
||||||
# Quickstart
|
# Quickstart
|
||||||
|
|
||||||
|
@ -43,12 +43,15 @@ atuin sync
|
||||||
|
|
||||||
## Install
|
## 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
|
### With cargo
|
||||||
|
@ -60,6 +63,14 @@ toolchain, then you can run:
|
||||||
cargo install atuin
|
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
|
### From source
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@ -68,15 +79,31 @@ cd atuin
|
||||||
cargo install --path .
|
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?
|
## ...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!
|
// import old shell history!
|
||||||
// automatically hoover up all that we can find
|
// 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 std::{fs::File, path::Path};
|
||||||
|
|
||||||
use chrono::prelude::*;
|
use chrono::prelude::*;
|
||||||
|
@ -9,7 +9,8 @@ use chrono::Utc;
|
||||||
use eyre::{eyre, Result};
|
use eyre::{eyre, Result};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
|
||||||
use super::history::History;
|
use super::count_lines;
|
||||||
|
use crate::history::History;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Zsh {
|
pub struct Zsh {
|
||||||
|
@ -19,14 +20,6 @@ pub struct Zsh {
|
||||||
pub counter: i64,
|
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 {
|
impl Zsh {
|
||||||
pub fn new(path: impl AsRef<Path>) -> Result<Self> {
|
pub fn new(path: impl AsRef<Path>) -> Result<Self> {
|
||||||
let file = File::open(path)?;
|
let file = File::open(path)?;
|
||||||
|
@ -39,36 +32,7 @@ impl Zsh {
|
||||||
counter: 0,
|
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>> {
|
fn read_line(&mut self) -> Option<Result<String>> {
|
||||||
let mut line = String::new();
|
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)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use chrono::prelude::*;
|
use chrono::prelude::*;
|
|
@ -123,6 +123,8 @@ async fn sync_upload(
|
||||||
client.post_history(&buffer).await?;
|
client.post_history(&buffer).await?;
|
||||||
cursor = buffer.last().unwrap().timestamp;
|
cursor = buffer.last().unwrap().timestamp;
|
||||||
remote_count = client.count().await?;
|
remote_count = client.count().await?;
|
||||||
|
|
||||||
|
debug!("upload cursor: {:?}", cursor);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
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::database::Database;
|
||||||
use atuin_client::history::History;
|
use atuin_client::history::History;
|
||||||
use atuin_client::import::Zsh;
|
use atuin_client::import::{bash::Bash, zsh::Zsh};
|
||||||
use indicatif::ProgressBar;
|
use indicatif::ProgressBar;
|
||||||
|
|
||||||
#[derive(StructOpt)]
|
#[derive(StructOpt)]
|
||||||
|
@ -23,11 +23,17 @@ pub enum Cmd {
|
||||||
aliases=&["z", "zs"],
|
aliases=&["z", "zs"],
|
||||||
)]
|
)]
|
||||||
Zsh,
|
Zsh,
|
||||||
|
|
||||||
|
#[structopt(
|
||||||
|
about="import history from the bash history file",
|
||||||
|
aliases=&["b", "ba", "bas"],
|
||||||
|
)]
|
||||||
|
Bash,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Cmd {
|
impl Cmd {
|
||||||
pub async fn run(&self, db: &mut (impl Database + Send + Sync)) -> Result<()> {
|
pub async fn run(&self, db: &mut (impl Database + Send + Sync)) -> Result<()> {
|
||||||
println!(" A'Tuin ");
|
println!(" Atuin ");
|
||||||
println!("======================");
|
println!("======================");
|
||||||
println!(" \u{1f30d} ");
|
println!(" \u{1f30d} ");
|
||||||
println!(" \u{1f418}\u{1f418}\u{1f418}\u{1f418} ");
|
println!(" \u{1f418}\u{1f418}\u{1f418}\u{1f418} ");
|
||||||
|
@ -49,6 +55,7 @@ impl Cmd {
|
||||||
}
|
}
|
||||||
|
|
||||||
Self::Zsh => import_zsh(db).await,
|
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(())
|
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 std::env;
|
||||||
|
|
||||||
use eyre::{eyre, Result};
|
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() {
|
fn init_zsh() {
|
||||||
let full = include_str!("../shell/atuin.zsh");
|
let full = include_str!("../shell/atuin.zsh");
|
||||||
println!("{}", full);
|
println!("{}", full);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init() -> Result<()> {
|
fn init_bash() {
|
||||||
let shell = env::var("SHELL")?;
|
let full = include_str!("../shell/atuin.bash");
|
||||||
|
println!("{}", full);
|
||||||
|
}
|
||||||
|
|
||||||
if shell.ends_with("zsh") {
|
impl Cmd {
|
||||||
init_zsh();
|
pub fn run(&self) -> Result<()> {
|
||||||
|
match self {
|
||||||
|
Self::Zsh => init_zsh(),
|
||||||
|
Self::Bash => init_bash(),
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
|
||||||
Err(eyre!("Could not detect shell, or shell unsupported"))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ pub enum AtuinCmd {
|
||||||
Stats(stats::Cmd),
|
Stats(stats::Cmd),
|
||||||
|
|
||||||
#[structopt(about = "output shell setup")]
|
#[structopt(about = "output shell setup")]
|
||||||
Init,
|
Init(init::Cmd),
|
||||||
|
|
||||||
#[structopt(about = "generates a UUID")]
|
#[structopt(about = "generates a UUID")]
|
||||||
Uuid,
|
Uuid,
|
||||||
|
@ -101,7 +101,7 @@ impl AtuinCmd {
|
||||||
Self::Import(import) => import.run(&mut db).await,
|
Self::Import(import) => import.run(&mut db).await,
|
||||||
Self::Server(server) => server.run(&server_settings).await,
|
Self::Server(server) => server.run(&server_settings).await,
|
||||||
Self::Stats(stats) => stats.run(&mut db, &client_settings).await,
|
Self::Stats(stats) => stats.run(&mut db, &client_settings).await,
|
||||||
Self::Init => init::init(),
|
Self::Init(init) => init.run(),
|
||||||
Self::Search {
|
Self::Search {
|
||||||
cwd,
|
cwd,
|
||||||
exit,
|
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"
|
export ATUIN_BINDKEYS="true"
|
||||||
|
|
||||||
_atuin_preexec(){
|
_atuin_preexec(){
|
||||||
id=$(atuin history start $1)
|
id=$(atuin history start "$1")
|
||||||
export ATUIN_HISTORY_ID="$id"
|
export ATUIN_HISTORY_ID="$id"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue