aboutsummaryrefslogtreecommitdiffstats
path: root/src/command
diff options
context:
space:
mode:
authorEllie Huxtable <e@elm.sh>2021-03-20 00:50:31 +0000
committerGitHub <noreply@github.com>2021-03-20 00:50:31 +0000
commit716c7722cda29bf612508bb96f51822a86e0f69e (patch)
treefa3c4c192fc05b078397fcd510d39ae78e46abfa /src/command
parentAdd config file support (#15) (diff)
downloadatuin-716c7722cda29bf612508bb96f51822a86e0f69e.zip
Add TUI, resolve #19, #17, #16 (#21)
Diffstat (limited to '')
-rw-r--r--src/command/event.rs68
-rw-r--r--src/command/history.rs13
-rw-r--r--src/command/mod.rs6
-rw-r--r--src/command/search.rs220
4 files changed, 307 insertions, 0 deletions
diff --git a/src/command/event.rs b/src/command/event.rs
new file mode 100644
index 00000000..b205be70
--- /dev/null
+++ b/src/command/event.rs
@@ -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()
+ }
+}
diff --git a/src/command/history.rs b/src/command/history.rs
index bd440163..af8aef7d 100644
--- a/src/command/history.rs
+++ b/src/command/history.rs
@@ -35,6 +35,12 @@ pub enum Cmd {
#[structopt(long, short)]
session: bool,
},
+
+ #[structopt(
+ about="search for a command",
+ aliases=&["se", "sea", "sear", "searc"],
+ )]
+ Search { query: Vec<String> },
}
fn print_list(h: &[History]) {
@@ -102,6 +108,13 @@ impl Cmd {
Ok(())
}
+
+ Self::Search { query } => {
+ let history = db.prefix_search(&query.join(""))?;
+ print_list(&history);
+
+ Ok(())
+ }
}
}
}
diff --git a/src/command/mod.rs b/src/command/mod.rs
index c74b138f..3ebb92e0 100644
--- a/src/command/mod.rs
+++ b/src/command/mod.rs
@@ -5,9 +5,11 @@ use uuid::Uuid;
use crate::local::database::Database;
use crate::settings::Settings;
+mod event;
mod history;
mod import;
mod init;
+mod search;
mod server;
mod stats;
@@ -33,6 +35,9 @@ pub enum AtuinCmd {
#[structopt(about = "generates a UUID")]
Uuid,
+
+ #[structopt(about = "interactive history search")]
+ Search { query: Vec<String> },
}
pub fn uuid_v4() -> String {
@@ -47,6 +52,7 @@ impl AtuinCmd {
Self::Server(server) => server.run(),
Self::Stats(stats) => stats.run(db, settings),
Self::Init => init::init(),
+ Self::Search { query } => search::run(&query, db),
Self::Uuid => {
println!("{}", uuid_v4());
diff --git a/src/command/search.rs b/src/command/search.rs
new file mode 100644
index 00000000..d51e29ef
--- /dev/null
+++ b/src/command/search.rs
@@ -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(())
+}