parent
61607e023f
commit
716c7722cd
11 changed files with 429 additions and 36 deletions
67
Cargo.lock
generated
67
Cargo.lock
generated
|
@ -106,7 +106,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atuin"
|
name = "atuin"
|
||||||
version = "0.3.3"
|
version = "0.4.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"chrono-english",
|
"chrono-english",
|
||||||
|
@ -116,6 +116,7 @@ dependencies = [
|
||||||
"eyre",
|
"eyre",
|
||||||
"hostname",
|
"hostname",
|
||||||
"indicatif",
|
"indicatif",
|
||||||
|
"itertools",
|
||||||
"log 0.4.14",
|
"log 0.4.14",
|
||||||
"pretty_env_logger",
|
"pretty_env_logger",
|
||||||
"rocket",
|
"rocket",
|
||||||
|
@ -124,6 +125,9 @@ dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
"shellexpand",
|
"shellexpand",
|
||||||
"structopt",
|
"structopt",
|
||||||
|
"termion",
|
||||||
|
"tui",
|
||||||
|
"unicode-width",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -214,6 +218,12 @@ version = "1.4.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ae44d1a3d5a19df61dd0c8beb138458ac2a53a7ac09eba97d55592540004306b"
|
checksum = "ae44d1a3d5a19df61dd0c8beb138458ac2a53a7ac09eba97d55592540004306b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cassowary"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.0.66"
|
version = "1.0.66"
|
||||||
|
@ -449,6 +459,12 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "either"
|
||||||
|
version = "1.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "encode_unicode"
|
name = "encode_unicode"
|
||||||
version = "0.3.6"
|
version = "0.3.6"
|
||||||
|
@ -682,6 +698,15 @@ dependencies = [
|
||||||
"regex",
|
"regex",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itertools"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "37d572918e350e82412fe766d24b15e6682fb2ed2bbe018280caa810397cb319"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "0.4.7"
|
version = "0.4.7"
|
||||||
|
@ -846,6 +871,12 @@ version = "0.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "17b02fc0ff9a9e4b35b3342880f48e896ebf69f2967921fe8646bf5b7125956a"
|
checksum = "17b02fc0ff9a9e4b35b3342880f48e896ebf69f2967921fe8646bf5b7125956a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "numtoa"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.5.2"
|
version = "1.5.2"
|
||||||
|
@ -1046,6 +1077,15 @@ dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_termios"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f"
|
||||||
|
dependencies = [
|
||||||
|
"redox_syscall 0.2.4",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_users"
|
name = "redox_users"
|
||||||
version = "0.3.5"
|
version = "0.3.5"
|
||||||
|
@ -1367,6 +1407,18 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "termion"
|
||||||
|
version = "1.5.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "077185e2eac69c3f8379a4298e1e07cd36beb962290d4a51199acf0fdc10607e"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"numtoa",
|
||||||
|
"redox_syscall 0.2.4",
|
||||||
|
"redox_termios",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "textwrap"
|
name = "textwrap"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
|
@ -1434,6 +1486,19 @@ version = "0.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "efd1f82c56340fdf16f2a953d7bda4f8fdffba13d93b00844c25572110b26079"
|
checksum = "efd1f82c56340fdf16f2a953d7bda4f8fdffba13d93b00844c25572110b26079"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tui"
|
||||||
|
version = "0.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ced152a8e9295a5b168adc254074525c17ac4a83c90b2716274cc38118bddc9"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"cassowary",
|
||||||
|
"termion",
|
||||||
|
"unicode-segmentation",
|
||||||
|
"unicode-width",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typeable"
|
name = "typeable"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "atuin"
|
name = "atuin"
|
||||||
version = "0.3.3"
|
version = "0.4.0"
|
||||||
authors = ["Ellie Huxtable <e@elm.sh>"]
|
authors = ["Ellie Huxtable <e@elm.sh>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
@ -23,6 +23,10 @@ cli-table = "0.4"
|
||||||
config = "0.10"
|
config = "0.10"
|
||||||
serde_derive = "1.0.124"
|
serde_derive = "1.0.124"
|
||||||
serde = "1.0.124"
|
serde = "1.0.124"
|
||||||
|
tui = "0.14"
|
||||||
|
termion = "1.5"
|
||||||
|
unicode-width = "0.1"
|
||||||
|
itertools = "0.10.0"
|
||||||
|
|
||||||
[dependencies.rusqlite]
|
[dependencies.rusqlite]
|
||||||
version = "0.24"
|
version = "0.24"
|
||||||
|
|
32
README.md
32
README.md
|
@ -29,10 +29,6 @@ As well as the expected command, A'tuin stores
|
||||||
|
|
||||||
- zsh
|
- zsh
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
- [fzf](https://github.com/junegunn/fzf)
|
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
### AUR
|
### AUR
|
||||||
|
@ -77,9 +73,9 @@ to your `.zshrc`/`.bashrc`/whatever your shell uses.
|
||||||
|
|
||||||
### History search
|
### History search
|
||||||
|
|
||||||
By default A'tuin will rebind ctrl-r to use fzf to fuzzy search your history.
|
By default A'tuin will rebind ctrl-r and the up arrow to search your history.
|
||||||
It will also rebind the up arrow to use fzf, just without sorting. You can
|
|
||||||
prevent this by putting
|
You can prevent this by putting
|
||||||
|
|
||||||
```
|
```
|
||||||
export ATUIN_BINDKEYS="false"
|
export ATUIN_BINDKEYS="false"
|
||||||
|
@ -87,28 +83,6 @@ export ATUIN_BINDKEYS="false"
|
||||||
|
|
||||||
into your shell config.
|
into your shell config.
|
||||||
|
|
||||||
You may also change the default history selection. The default behaviour will search your entire history, however
|
|
||||||
|
|
||||||
```
|
|
||||||
export ATUIN_HISTORY="atuin history list --cwd"
|
|
||||||
```
|
|
||||||
|
|
||||||
will switch to only searching history for the current directory.
|
|
||||||
|
|
||||||
Similarly,
|
|
||||||
|
|
||||||
```
|
|
||||||
export ATUIN_HISTORY="atuin history list --session"
|
|
||||||
```
|
|
||||||
|
|
||||||
will search for the current session only, and
|
|
||||||
|
|
||||||
```
|
|
||||||
export ATUIN_HISTORY="atuin history list --session --cwd"
|
|
||||||
```
|
|
||||||
|
|
||||||
will do both!
|
|
||||||
|
|
||||||
### Import history
|
### Import history
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
68
src/command/event.rs
Normal file
68
src/command/event.rs
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
use std::sync::mpsc;
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use termion::event::Key;
|
||||||
|
use termion::input::TermRead;
|
||||||
|
|
||||||
|
pub enum Event<I> {
|
||||||
|
Input(I),
|
||||||
|
Tick,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A small event handler that wrap termion input and tick events. Each event
|
||||||
|
/// type is handled in its own thread and returned to a common `Receiver`
|
||||||
|
pub struct Events {
|
||||||
|
rx: mpsc::Receiver<Event<Key>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct Config {
|
||||||
|
pub exit_key: Key,
|
||||||
|
pub tick_rate: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Config {
|
||||||
|
fn default() -> Config {
|
||||||
|
Config {
|
||||||
|
exit_key: Key::Char('q'),
|
||||||
|
tick_rate: Duration::from_millis(250),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Events {
|
||||||
|
pub fn new() -> Events {
|
||||||
|
Events::with_config(Config::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_config(config: Config) -> Events {
|
||||||
|
let (tx, rx) = mpsc::channel();
|
||||||
|
|
||||||
|
{
|
||||||
|
let tx = tx.clone();
|
||||||
|
thread::spawn(move || {
|
||||||
|
let tty = termion::get_tty().expect("Could not find tty");
|
||||||
|
for key in tty.keys().flatten() {
|
||||||
|
if let Err(err) = tx.send(Event::Input(key)) {
|
||||||
|
eprintln!("{}", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
thread::spawn(move || loop {
|
||||||
|
if tx.send(Event::Tick).is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
thread::sleep(config.tick_rate);
|
||||||
|
});
|
||||||
|
|
||||||
|
Events { rx }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next(&self) -> Result<Event<Key>, mpsc::RecvError> {
|
||||||
|
self.rx.recv()
|
||||||
|
}
|
||||||
|
}
|
|
@ -35,6 +35,12 @@ pub enum Cmd {
|
||||||
#[structopt(long, short)]
|
#[structopt(long, short)]
|
||||||
session: bool,
|
session: bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
#[structopt(
|
||||||
|
about="search for a command",
|
||||||
|
aliases=&["se", "sea", "sear", "searc"],
|
||||||
|
)]
|
||||||
|
Search { query: Vec<String> },
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_list(h: &[History]) {
|
fn print_list(h: &[History]) {
|
||||||
|
@ -102,6 +108,13 @@ impl Cmd {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Self::Search { query } => {
|
||||||
|
let history = db.prefix_search(&query.join(""))?;
|
||||||
|
print_list(&history);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,9 +5,11 @@ use uuid::Uuid;
|
||||||
use crate::local::database::Database;
|
use crate::local::database::Database;
|
||||||
use crate::settings::Settings;
|
use crate::settings::Settings;
|
||||||
|
|
||||||
|
mod event;
|
||||||
mod history;
|
mod history;
|
||||||
mod import;
|
mod import;
|
||||||
mod init;
|
mod init;
|
||||||
|
mod search;
|
||||||
mod server;
|
mod server;
|
||||||
mod stats;
|
mod stats;
|
||||||
|
|
||||||
|
@ -33,6 +35,9 @@ pub enum AtuinCmd {
|
||||||
|
|
||||||
#[structopt(about = "generates a UUID")]
|
#[structopt(about = "generates a UUID")]
|
||||||
Uuid,
|
Uuid,
|
||||||
|
|
||||||
|
#[structopt(about = "interactive history search")]
|
||||||
|
Search { query: Vec<String> },
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn uuid_v4() -> String {
|
pub fn uuid_v4() -> String {
|
||||||
|
@ -47,6 +52,7 @@ impl AtuinCmd {
|
||||||
Self::Server(server) => server.run(),
|
Self::Server(server) => server.run(),
|
||||||
Self::Stats(stats) => stats.run(db, settings),
|
Self::Stats(stats) => stats.run(db, settings),
|
||||||
Self::Init => init::init(),
|
Self::Init => init::init(),
|
||||||
|
Self::Search { query } => search::run(&query, db),
|
||||||
|
|
||||||
Self::Uuid => {
|
Self::Uuid => {
|
||||||
println!("{}", uuid_v4());
|
println!("{}", uuid_v4());
|
||||||
|
|
220
src/command/search.rs
Normal file
220
src/command/search.rs
Normal file
|
@ -0,0 +1,220 @@
|
||||||
|
use eyre::Result;
|
||||||
|
use itertools::Itertools;
|
||||||
|
use std::io::stdout;
|
||||||
|
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
|
||||||
|
use tui::{
|
||||||
|
backend::TermionBackend,
|
||||||
|
layout::{Alignment, Constraint, Corner, Direction, Layout},
|
||||||
|
style::{Color, Modifier, Style},
|
||||||
|
text::{Span, Spans, Text},
|
||||||
|
widgets::{Block, Borders, List, ListItem, ListState, Paragraph},
|
||||||
|
Terminal,
|
||||||
|
};
|
||||||
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
|
use crate::command::event::{Event, Events};
|
||||||
|
use crate::local::database::Database;
|
||||||
|
use crate::local::history::History;
|
||||||
|
|
||||||
|
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
|
struct State {
|
||||||
|
input: String,
|
||||||
|
|
||||||
|
results: Vec<History>,
|
||||||
|
|
||||||
|
results_state: ListState,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn query_results(app: &mut State, db: &mut impl Database) {
|
||||||
|
let results = match app.input.as_str() {
|
||||||
|
"" => db.list(),
|
||||||
|
i => db.prefix_search(i),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Ok(results) = results {
|
||||||
|
app.results = results.into_iter().rev().unique().collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
if app.results.is_empty() {
|
||||||
|
app.results_state.select(None);
|
||||||
|
} else {
|
||||||
|
app.results_state.select(Some(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn key_handler(input: Key, db: &mut impl Database, app: &mut State) -> Option<String> {
|
||||||
|
match input {
|
||||||
|
Key::Esc | Key::Char('\n') => {
|
||||||
|
let i = app.results_state.selected().unwrap_or(0);
|
||||||
|
|
||||||
|
return Some(app.results.get(i).unwrap().command.clone());
|
||||||
|
}
|
||||||
|
Key::Char(c) => {
|
||||||
|
app.input.push(c);
|
||||||
|
query_results(app, db);
|
||||||
|
}
|
||||||
|
Key::Backspace => {
|
||||||
|
app.input.pop();
|
||||||
|
query_results(app, db);
|
||||||
|
}
|
||||||
|
Key::Down => {
|
||||||
|
let i = match app.results_state.selected() {
|
||||||
|
Some(i) => {
|
||||||
|
if i == 0 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
i - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => 0,
|
||||||
|
};
|
||||||
|
app.results_state.select(Some(i));
|
||||||
|
}
|
||||||
|
Key::Up => {
|
||||||
|
let i = match app.results_state.selected() {
|
||||||
|
Some(i) => {
|
||||||
|
if i >= app.results.len() - 1 {
|
||||||
|
app.results.len() - 1
|
||||||
|
} else {
|
||||||
|
i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => 0,
|
||||||
|
};
|
||||||
|
app.results_state.select(Some(i));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is a big blob of horrible! clean it up!
|
||||||
|
// for now, it works. But it'd be great if it were more easily readable, and
|
||||||
|
// modular. I'd like to add some more stats and stuff at some point
|
||||||
|
#[allow(clippy::clippy::cast_possible_truncation)]
|
||||||
|
fn select_history(query: &[String], db: &mut impl Database) -> Result<String> {
|
||||||
|
let stdout = stdout().into_raw_mode()?;
|
||||||
|
let stdout = MouseTerminal::from(stdout);
|
||||||
|
let stdout = AlternateScreen::from(stdout);
|
||||||
|
let backend = TermionBackend::new(stdout);
|
||||||
|
let mut terminal = Terminal::new(backend)?;
|
||||||
|
|
||||||
|
// Setup event handlers
|
||||||
|
let events = Events::new();
|
||||||
|
|
||||||
|
let mut app = State {
|
||||||
|
input: query.join(" "),
|
||||||
|
results: Vec::new(),
|
||||||
|
results_state: ListState::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
query_results(&mut app, db);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// Handle input
|
||||||
|
if let Event::Input(input) = events.next()? {
|
||||||
|
if let Some(output) = key_handler(input, db, &mut app) {
|
||||||
|
return Ok(output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
terminal.draw(|f| {
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.margin(1)
|
||||||
|
.constraints(
|
||||||
|
[
|
||||||
|
Constraint::Length(2),
|
||||||
|
Constraint::Min(1),
|
||||||
|
Constraint::Length(3),
|
||||||
|
]
|
||||||
|
.as_ref(),
|
||||||
|
)
|
||||||
|
.split(f.size());
|
||||||
|
|
||||||
|
let top_chunks = Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||||
|
.split(chunks[0]);
|
||||||
|
|
||||||
|
let top_left_chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([Constraint::Length(1), Constraint::Length(1)].as_ref())
|
||||||
|
.split(top_chunks[0]);
|
||||||
|
|
||||||
|
let top_right_chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([Constraint::Length(1), Constraint::Length(1)].as_ref())
|
||||||
|
.split(top_chunks[1]);
|
||||||
|
|
||||||
|
let title = Paragraph::new(Text::from(Span::styled(
|
||||||
|
format!("A'tuin v{}", VERSION),
|
||||||
|
Style::default().add_modifier(Modifier::BOLD),
|
||||||
|
)));
|
||||||
|
|
||||||
|
let help = vec![
|
||||||
|
Span::raw("Press "),
|
||||||
|
Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)),
|
||||||
|
Span::raw(" to exit."),
|
||||||
|
];
|
||||||
|
|
||||||
|
let help = Text::from(Spans::from(help));
|
||||||
|
let help = Paragraph::new(help);
|
||||||
|
|
||||||
|
let input = Paragraph::new(app.input.as_ref())
|
||||||
|
.block(Block::default().borders(Borders::ALL).title("Search"));
|
||||||
|
|
||||||
|
let results: Vec<ListItem> = app
|
||||||
|
.results
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, m)| {
|
||||||
|
let mut content = Span::raw(m.command.to_string());
|
||||||
|
|
||||||
|
if let Some(selected) = app.results_state.selected() {
|
||||||
|
if selected == i {
|
||||||
|
content.style =
|
||||||
|
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ListItem::new(content)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let results = List::new(results)
|
||||||
|
.block(Block::default().borders(Borders::ALL).title("History"))
|
||||||
|
.start_corner(Corner::BottomLeft)
|
||||||
|
.highlight_symbol(">> ");
|
||||||
|
|
||||||
|
let stats = Paragraph::new(Text::from(Span::raw(format!(
|
||||||
|
"history count: {}",
|
||||||
|
db.history_count().unwrap()
|
||||||
|
))))
|
||||||
|
.alignment(Alignment::Right);
|
||||||
|
|
||||||
|
f.render_widget(title, top_left_chunks[0]);
|
||||||
|
f.render_widget(help, top_left_chunks[1]);
|
||||||
|
|
||||||
|
f.render_widget(stats, top_right_chunks[0]);
|
||||||
|
f.render_stateful_widget(results, chunks[1], &mut app.results_state);
|
||||||
|
f.render_widget(input, chunks[2]);
|
||||||
|
|
||||||
|
f.set_cursor(
|
||||||
|
// Put cursor past the end of the input text
|
||||||
|
chunks[2].x + app.input.width() as u16 + 1,
|
||||||
|
// Move one line down, from the border to the input line
|
||||||
|
chunks[2].y + 1,
|
||||||
|
);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(query: &[String], db: &mut impl Database) -> Result<()> {
|
||||||
|
let item = select_history(query, db)?;
|
||||||
|
eprintln!("{}", item);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -15,12 +15,17 @@ pub enum QueryParam {
|
||||||
pub trait Database {
|
pub trait Database {
|
||||||
fn save(&mut self, h: &History) -> Result<()>;
|
fn save(&mut self, h: &History) -> Result<()>;
|
||||||
fn save_bulk(&mut self, h: &[History]) -> Result<()>;
|
fn save_bulk(&mut self, h: &[History]) -> Result<()>;
|
||||||
|
|
||||||
fn load(&self, id: &str) -> Result<History>;
|
fn load(&self, id: &str) -> Result<History>;
|
||||||
fn list(&self) -> Result<Vec<History>>;
|
fn list(&self) -> Result<Vec<History>>;
|
||||||
fn range(&self, from: chrono::DateTime<Utc>, to: chrono::DateTime<Utc>)
|
fn range(&self, from: chrono::DateTime<Utc>, to: chrono::DateTime<Utc>)
|
||||||
-> Result<Vec<History>>;
|
-> Result<Vec<History>>;
|
||||||
fn update(&self, h: &History) -> Result<()>;
|
|
||||||
fn query(&self, query: &str, params: &[QueryParam]) -> Result<Vec<History>>;
|
fn query(&self, query: &str, params: &[QueryParam]) -> Result<Vec<History>>;
|
||||||
|
fn update(&self, h: &History) -> Result<()>;
|
||||||
|
fn history_count(&self) -> Result<i64>;
|
||||||
|
|
||||||
|
fn prefix_search(&self, query: &str) -> Result<Vec<History>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Intended for use on a developer machine and not a sync server.
|
// Intended for use on a developer machine and not a sync server.
|
||||||
|
@ -199,6 +204,21 @@ impl Database for Sqlite {
|
||||||
|
|
||||||
Ok(history_iter.filter_map(Result::ok).collect())
|
Ok(history_iter.filter_map(Result::ok).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn prefix_search(&self, query: &str) -> Result<Vec<History>> {
|
||||||
|
self.query(
|
||||||
|
"select * from history where command like ?1 || '%' order by timestamp asc",
|
||||||
|
&[QueryParam::Text(query.to_string())],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn history_count(&self) -> Result<i64> {
|
||||||
|
let res: i64 =
|
||||||
|
self.conn
|
||||||
|
.query_row_and_then("select count(1) from history;", params![], |row| row.get(0))?;
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn history_from_sqlite_row(
|
fn history_from_sqlite_row(
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
use std::env;
|
use std::env;
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
|
||||||
use crate::command::uuid_v4;
|
use crate::command::uuid_v4;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct History {
|
pub struct History {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub timestamp: i64,
|
pub timestamp: i64,
|
||||||
|
@ -42,3 +43,21 @@ impl History {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PartialEq for History {
|
||||||
|
// for the sakes of listing unique history only, we do not care about
|
||||||
|
// anything else
|
||||||
|
// obviously this does not refer to the *same* item of history, but when
|
||||||
|
// we only render the command, it looks the same
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.command == other.command
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for History {}
|
||||||
|
|
||||||
|
impl Hash for History {
|
||||||
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
|
self.command.hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -29,7 +29,7 @@ mod settings;
|
||||||
#[derive(StructOpt)]
|
#[derive(StructOpt)]
|
||||||
#[structopt(
|
#[structopt(
|
||||||
author = "Ellie Huxtable <e@elm.sh>",
|
author = "Ellie Huxtable <e@elm.sh>",
|
||||||
version = "0.3.2",
|
version = "0.4.0",
|
||||||
about = "Magical shell history"
|
about = "Magical shell history"
|
||||||
)]
|
)]
|
||||||
struct Atuin {
|
struct Atuin {
|
||||||
|
|
|
@ -20,7 +20,9 @@ _atuin_search(){
|
||||||
emulate -L zsh
|
emulate -L zsh
|
||||||
zle -I
|
zle -I
|
||||||
|
|
||||||
output=$(eval $ATUIN_HISTORY | fzf)
|
# swap stderr and stdout, so that the tui stuff works
|
||||||
|
# TODO: not this
|
||||||
|
output=$(atuin search $BUFFER 3>&1 1>&2 2>&3)
|
||||||
|
|
||||||
if [[ -n $output ]] ; then
|
if [[ -n $output ]] ; then
|
||||||
LBUFFER=$output
|
LBUFFER=$output
|
||||||
|
@ -33,7 +35,9 @@ _atuin_up_search(){
|
||||||
emulate -L zsh
|
emulate -L zsh
|
||||||
zle -I
|
zle -I
|
||||||
|
|
||||||
output=$(eval $ATUIN_HISTORY | fzf --no-sort --tac)
|
# swap stderr and stdout, so that the tui stuff works
|
||||||
|
# TODO: not this
|
||||||
|
output=$(atuin search $BUFFER 3>&1 1>&2 2>&3)
|
||||||
|
|
||||||
if [[ -n $output ]] ; then
|
if [[ -n $output ]] ; then
|
||||||
LBUFFER=$output
|
LBUFFER=$output
|
||||||
|
|
Loading…
Reference in a new issue