aboutsummaryrefslogtreecommitdiffstats
path: root/src/command/client/search/interactive.rs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/command/client/search/interactive.rs190
1 files changed, 128 insertions, 62 deletions
diff --git a/src/command/client/search/interactive.rs b/src/command/client/search/interactive.rs
index 45c532f0..c1d1c175 100644
--- a/src/command/client/search/interactive.rs
+++ b/src/command/client/search/interactive.rs
@@ -1,16 +1,10 @@
use std::{
io::{stdout, Write},
+ ops::Deref,
+ sync::Arc,
time::Duration,
};
-use crate::tui::{
- backend::{Backend, CrosstermBackend},
- layout::{Alignment, Constraint, Direction, Layout},
- style::{Color, Modifier, Style},
- text::{Span, Spans, Text},
- widgets::{Block, BorderType, Borders, Paragraph},
- Frame, Terminal,
-};
use crossterm::{
event::{self, Event, KeyCode, KeyEvent, KeyModifiers, MouseEvent},
execute, terminal,
@@ -18,12 +12,12 @@ use crossterm::{
use eyre::Result;
use futures_util::FutureExt;
use semver::Version;
+use skim::SkimItem;
use unicode_width::UnicodeWidthStr;
use atuin_client::{
- database::current_context,
- database::Context,
database::Database,
+ database::{current_context, Context},
history::History,
settings::{ExitMode, FilterMode, SearchMode, Settings},
};
@@ -32,18 +26,35 @@ use super::{
cursor::Cursor,
history_list::{HistoryList, ListState, PREFIX_LENGTH},
};
-use crate::VERSION;
+use crate::{
+ tui::{
+ backend::{Backend, CrosstermBackend},
+ layout::{Alignment, Constraint, Direction, Layout},
+ style::{Color, Modifier, Style},
+ text::{Span, Spans, Text},
+ widgets::{Block, BorderType, Borders, Paragraph},
+ Frame, Terminal,
+ },
+ VERSION,
+};
const RETURN_ORIGINAL: usize = usize::MAX;
const RETURN_QUERY: usize = usize::MAX - 1;
struct State {
history_count: i64,
- input: Cursor,
- filter_mode: FilterMode,
- results_state: ListState,
- context: Context,
update_needed: Option<Version>,
+ results_state: ListState,
+ search: SearchState,
+
+ // only allocated if using skim
+ all_history: Vec<Arc<HistoryWrapper>>,
+}
+
+pub struct SearchState {
+ pub input: Cursor,
+ pub filter_mode: FilterMode,
+ pub context: Context,
}
impl State {
@@ -51,22 +62,48 @@ impl State {
&mut self,
search_mode: SearchMode,
db: &mut impl Database,
- ) -> Result<Vec<History>> {
- let i = self.input.as_str();
+ ) -> Result<Vec<Arc<HistoryWrapper>>> {
+ let i = self.search.input.as_str();
let results = if i.is_empty() {
- db.list(self.filter_mode, &self.context, Some(200), true)
- .await?
+ db.list(
+ self.search.filter_mode,
+ &self.search.context,
+ Some(200),
+ true,
+ )
+ .await?
+ .into_iter()
+ .map(|history| HistoryWrapper { history, count: 1 })
+ .map(Arc::new)
+ .collect::<Vec<_>>()
+ } else if search_mode == SearchMode::Skim {
+ if self.all_history.is_empty() {
+ self.all_history = db
+ .all_with_count()
+ .await
+ .unwrap()
+ .into_iter()
+ .map(|(history, count)| HistoryWrapper { history, count })
+ .map(Arc::new)
+ .collect::<Vec<_>>();
+ }
+
+ super::skim_impl::fuzzy_search(&self.search, &self.all_history).await
} else {
db.search(
search_mode,
- self.filter_mode,
- &self.context,
+ self.search.filter_mode,
+ &self.search.context,
i,
Some(200),
None,
None,
)
.await?
+ .into_iter()
+ .map(|history| HistoryWrapper { history, count: 1 })
+ .map(Arc::new)
+ .collect::<Vec<_>>()
};
self.results_state.select(0);
@@ -125,47 +162,51 @@ impl State {
return Some(self.results_state.selected() + c);
}
KeyCode::Left if ctrl => self
+ .search
.input
.prev_word(&settings.word_chars, settings.word_jump_mode),
KeyCode::Left => {
- self.input.left();
+ self.search.input.left();
}
KeyCode::Char('h') if ctrl => {
- self.input.left();
+ self.search.input.left();
}
KeyCode::Right if ctrl => self
+ .search
.input
.next_word(&settings.word_chars, settings.word_jump_mode),
- KeyCode::Right => self.input.right(),
- KeyCode::Char('l') if ctrl => self.input.right(),
- KeyCode::Char('a') if ctrl => self.input.start(),
- KeyCode::Home => self.input.start(),
- KeyCode::Char('e') if ctrl => self.input.end(),
- KeyCode::End => self.input.end(),
+ KeyCode::Right => self.search.input.right(),
+ KeyCode::Char('l') if ctrl => self.search.input.right(),
+ KeyCode::Char('a') if ctrl => self.search.input.start(),
+ KeyCode::Home => self.search.input.start(),
+ KeyCode::Char('e') if ctrl => self.search.input.end(),
+ KeyCode::End => self.search.input.end(),
KeyCode::Backspace if ctrl => self
+ .search
.input
.remove_prev_word(&settings.word_chars, settings.word_jump_mode),
KeyCode::Backspace => {
- self.input.back();
+ self.search.input.back();
}
KeyCode::Delete if ctrl => self
+ .search
.input
.remove_next_word(&settings.word_chars, settings.word_jump_mode),
KeyCode::Delete => {
- self.input.remove();
+ self.search.input.remove();
}
KeyCode::Char('w') if ctrl => {
// remove the first batch of whitespace
- while matches!(self.input.back(), Some(c) if c.is_whitespace()) {}
- while self.input.left() {
- if self.input.char().unwrap().is_whitespace() {
- self.input.right(); // found whitespace, go back right
+ while matches!(self.search.input.back(), Some(c) if c.is_whitespace()) {}
+ while self.search.input.left() {
+ if self.search.input.char().unwrap().is_whitespace() {
+ self.search.input.right(); // found whitespace, go back right
break;
}
- self.input.remove();
+ self.search.input.remove();
}
}
- KeyCode::Char('u') if ctrl => self.input.clear(),
+ KeyCode::Char('u') if ctrl => self.search.input.clear(),
KeyCode::Char('r') if ctrl => {
pub static FILTER_MODES: [FilterMode; 4] = [
FilterMode::Global,
@@ -173,9 +214,9 @@ impl State {
FilterMode::Session,
FilterMode::Directory,
];
- let i = self.filter_mode as usize;
+ let i = self.search.filter_mode as usize;
let i = (i + 1) % FILTER_MODES.len();
- self.filter_mode = FILTER_MODES[i];
+ self.search.filter_mode = FILTER_MODES[i];
}
KeyCode::Down if self.results_state.selected() == 0 => return Some(RETURN_ORIGINAL),
KeyCode::Down => {
@@ -194,7 +235,7 @@ impl State {
let i = self.results_state.selected() + 1;
self.results_state.select(i.min(len - 1));
}
- KeyCode::Char(c) => self.input.insert(c),
+ KeyCode::Char(c) => self.search.input.insert(c),
KeyCode::PageDown => {
let scroll_len = self.results_state.max_entries() - settings.scroll_context_lines;
let i = self.results_state.selected().saturating_sub(scroll_len);
@@ -216,7 +257,7 @@ impl State {
fn draw<T: Backend>(
&mut self,
f: &mut Frame<'_, T>,
- results: &[History],
+ results: &[Arc<HistoryWrapper>],
compact: bool,
show_preview: bool,
) {
@@ -284,7 +325,7 @@ impl State {
let preview = self.build_preview(results, compact, preview_width, chunks[3].width.into());
f.render_widget(preview, chunks[3]);
- let extra_width = UnicodeWidthStr::width(self.input.substring());
+ let extra_width = UnicodeWidthStr::width(self.search.input.substring());
let cursor_offset = if compact { 0 } else { 1 };
f.set_cursor(
@@ -332,7 +373,7 @@ impl State {
stats
}
- fn build_results_list(compact: bool, results: &[History]) -> HistoryList {
+ fn build_results_list(compact: bool, results: &[Arc<HistoryWrapper>]) -> HistoryList {
let results_list = if compact {
HistoryList::new(results)
} else {
@@ -348,8 +389,8 @@ impl State {
fn build_input(&mut self, compact: bool, chunk_width: usize) -> Paragraph {
let input = format!(
"[{:^14}] {}",
- self.filter_mode.as_str(),
- self.input.as_str(),
+ self.search.filter_mode.as_str(),
+ self.search.input.as_str(),
);
let input = if compact {
Paragraph::new(input)
@@ -366,7 +407,7 @@ impl State {
fn build_preview(
&mut self,
- results: &[History],
+ results: &[Arc<HistoryWrapper>],
compact: bool,
preview_width: u16,
chunk_width: usize,
@@ -438,6 +479,23 @@ impl Write for Stdout {
}
}
+pub struct HistoryWrapper {
+ history: History,
+ pub count: i32,
+}
+impl Deref for HistoryWrapper {
+ type Target = History;
+
+ fn deref(&self) -> &Self::Target {
+ &self.history
+ }
+}
+impl SkimItem for HistoryWrapper {
+ fn text(&self) -> std::borrow::Cow<str> {
+ std::borrow::Cow::Borrowed(self.history.command.as_str())
+ }
+}
+
// 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
@@ -455,22 +513,28 @@ pub async fn history(
// Put the cursor at the end of the query by default
input.end();
- let update_needed = settings.needs_update().fuse();
+ let settings2 = settings.clone();
+ let update_needed = tokio::spawn(async move { settings2.needs_update().await }).fuse();
tokio::pin!(update_needed);
+ let context = current_context();
+
let mut app = State {
history_count: db.history_count().await?,
- input,
results_state: ListState::default(),
- context: current_context(),
- filter_mode: if settings.shell_up_key_binding {
- settings
- .filter_mode_shell_up_key_binding
- .unwrap_or(settings.filter_mode)
- } else {
- settings.filter_mode
- },
update_needed: None,
+ search: SearchState {
+ input,
+ context,
+ filter_mode: if settings.shell_up_key_binding {
+ settings
+ .filter_mode_shell_up_key_binding
+ .unwrap_or(settings.filter_mode)
+ } else {
+ settings.filter_mode
+ },
+ },
+ all_history: Vec::new(),
};
let mut results = app.query_results(settings.search_mode, db).await?;
@@ -485,8 +549,8 @@ pub async fn history(
};
terminal.draw(|f| app.draw(f, &results, compact, settings.show_preview))?;
- let initial_input = app.input.as_str().to_owned();
- let initial_filter_mode = app.filter_mode;
+ let initial_input = app.search.input.as_str().to_owned();
+ let initial_filter_mode = app.search.filter_mode;
let event_ready = tokio::task::spawn_blocking(|| event::poll(Duration::from_millis(250)));
@@ -504,23 +568,25 @@ pub async fn history(
}
}
update_needed = &mut update_needed => {
- app.update_needed = update_needed;
+ app.update_needed = update_needed?;
}
}
- if initial_input != app.input.as_str() || initial_filter_mode != app.filter_mode {
+ if initial_input != app.search.input.as_str()
+ || initial_filter_mode != app.search.filter_mode
+ {
results = app.query_results(settings.search_mode, db).await?;
}
};
if index < results.len() {
// index is in bounds so we return that entry
- Ok(results.swap_remove(index).command)
+ Ok(results.swap_remove(index).command.clone())
} else if index == RETURN_ORIGINAL {
Ok(String::new())
} else {
// Either:
// * index == RETURN_QUERY, in which case we should return the input
// * out of bounds -> usually implies no selected entry so we return the input
- Ok(app.input.into_inner())
+ Ok(app.search.input.into_inner())
}
}