aboutsummaryrefslogtreecommitdiffstats
path: root/src/command/search.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/command/search.rs')
-rw-r--r--src/command/search.rs220
1 files changed, 220 insertions, 0 deletions
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(())
+}