Batch key handling (#448)
* Batch input events and only query once they are finished This simplifies the code a lot (no more bounded channel) and yields the same performance improvement with scroll wheel spam while fixing copy/paste * Clippy * fmt * Use blocking wait before emptying events channel This was causing a busy loop * Update query on filter mode change
This commit is contained in:
parent
23b9d34e16
commit
ab994e3c82
3 changed files with 28 additions and 27 deletions
|
@ -24,7 +24,7 @@ pub enum SearchMode {
|
||||||
Fuzzy,
|
Fuzzy,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Copy)]
|
#[derive(Clone, Debug, Deserialize, Copy, PartialEq)]
|
||||||
pub enum FilterMode {
|
pub enum FilterMode {
|
||||||
#[serde(rename = "global")]
|
#[serde(rename = "global")]
|
||||||
Global,
|
Global,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use std::{thread, time::Duration};
|
use std::{thread, time::Duration};
|
||||||
|
|
||||||
use crossbeam_channel::{bounded, TrySendError};
|
use crossbeam_channel::unbounded;
|
||||||
use termion::{event::Event as TermEvent, event::Key, input::TermRead};
|
use termion::{event::Event as TermEvent, event::Key, input::TermRead};
|
||||||
|
|
||||||
pub enum Event<I> {
|
pub enum Event<I> {
|
||||||
|
@ -35,22 +35,16 @@ impl Events {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_config(config: Config) -> Events {
|
pub fn with_config(config: Config) -> Events {
|
||||||
// Keep channel small so scroll events don't stack for ages.
|
let (tx, rx) = unbounded();
|
||||||
let (tx, rx) = bounded(1);
|
|
||||||
|
|
||||||
{
|
{
|
||||||
let tx = tx.clone();
|
let tx = tx.clone();
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
let tty = termion::get_tty().expect("Could not find tty");
|
let tty = termion::get_tty().expect("Could not find tty");
|
||||||
for event in tty.events().flatten() {
|
for event in tty.events().flatten() {
|
||||||
if let Err(err) = tx.try_send(Event::Input(event)) {
|
if let Err(err) = tx.send(Event::Input(event)) {
|
||||||
if let TrySendError::Full(_) = err {
|
eprintln!("{}", err);
|
||||||
// Silently ignore send fails when buffer is full.
|
return;
|
||||||
// This will most likely be scroll wheel spam and we can drop some events.
|
|
||||||
} else {
|
|
||||||
eprintln!("{}", err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -69,4 +63,8 @@ impl Events {
|
||||||
pub fn next(&self) -> Result<Event<TermEvent>, crossbeam_channel::RecvError> {
|
pub fn next(&self) -> Result<Event<TermEvent>, crossbeam_channel::RecvError> {
|
||||||
self.rx.recv()
|
self.rx.recv()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn try_next(&self) -> Result<Event<TermEvent>, crossbeam_channel::TryRecvError> {
|
||||||
|
self.rx.try_recv()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -307,12 +307,7 @@ fn remove_char_from_input(app: &mut State, i: usize) -> char {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_lines)]
|
#[allow(clippy::too_many_lines)]
|
||||||
async fn key_handler(
|
fn key_handler(input: &TermEvent, app: &mut State) -> Option<String> {
|
||||||
input: TermEvent,
|
|
||||||
search_mode: SearchMode,
|
|
||||||
db: &mut impl Database,
|
|
||||||
app: &mut State,
|
|
||||||
) -> Option<String> {
|
|
||||||
match input {
|
match input {
|
||||||
TermEvent::Key(Key::Esc | Key::Ctrl('c' | 'd' | 'g')) => return Some(String::from("")),
|
TermEvent::Key(Key::Esc | Key::Ctrl('c' | 'd' | 'g')) => return Some(String::from("")),
|
||||||
TermEvent::Key(Key::Char('\n')) => {
|
TermEvent::Key(Key::Char('\n')) => {
|
||||||
|
@ -324,7 +319,7 @@ async fn key_handler(
|
||||||
.map_or(app.input.clone(), |h| h.command.clone()),
|
.map_or(app.input.clone(), |h| h.command.clone()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
TermEvent::Key(Key::Alt(c)) if ('1'..='9').contains(&c) => {
|
TermEvent::Key(Key::Alt(c)) if ('1'..='9').contains(c) => {
|
||||||
let c = c.to_digit(10)? as usize;
|
let c = c.to_digit(10)? as usize;
|
||||||
let i = app.results_state.selected()? + c;
|
let i = app.results_state.selected()? + c;
|
||||||
|
|
||||||
|
@ -351,9 +346,8 @@ async fn key_handler(
|
||||||
app.cursor_index = app.input.chars().count();
|
app.cursor_index = app.input.chars().count();
|
||||||
}
|
}
|
||||||
TermEvent::Key(Key::Char(c)) => {
|
TermEvent::Key(Key::Char(c)) => {
|
||||||
insert_char_into_input(app, app.cursor_index, c);
|
insert_char_into_input(app, app.cursor_index, *c);
|
||||||
app.cursor_index += 1;
|
app.cursor_index += 1;
|
||||||
query_results(app, search_mode, db).await.unwrap();
|
|
||||||
}
|
}
|
||||||
TermEvent::Key(Key::Backspace) => {
|
TermEvent::Key(Key::Backspace) => {
|
||||||
if app.cursor_index == 0 {
|
if app.cursor_index == 0 {
|
||||||
|
@ -361,7 +355,6 @@ async fn key_handler(
|
||||||
}
|
}
|
||||||
remove_char_from_input(app, app.cursor_index);
|
remove_char_from_input(app, app.cursor_index);
|
||||||
app.cursor_index -= 1;
|
app.cursor_index -= 1;
|
||||||
query_results(app, search_mode, db).await.unwrap();
|
|
||||||
}
|
}
|
||||||
TermEvent::Key(Key::Ctrl('w')) => {
|
TermEvent::Key(Key::Ctrl('w')) => {
|
||||||
let mut stop_on_next_whitespace = false;
|
let mut stop_on_next_whitespace = false;
|
||||||
|
@ -379,12 +372,10 @@ async fn key_handler(
|
||||||
}
|
}
|
||||||
app.cursor_index -= 1;
|
app.cursor_index -= 1;
|
||||||
}
|
}
|
||||||
query_results(app, search_mode, db).await.unwrap();
|
|
||||||
}
|
}
|
||||||
TermEvent::Key(Key::Ctrl('u')) => {
|
TermEvent::Key(Key::Ctrl('u')) => {
|
||||||
app.input = String::from("");
|
app.input = String::from("");
|
||||||
app.cursor_index = 0;
|
app.cursor_index = 0;
|
||||||
query_results(app, search_mode, db).await.unwrap();
|
|
||||||
}
|
}
|
||||||
TermEvent::Key(Key::Ctrl('r')) => {
|
TermEvent::Key(Key::Ctrl('r')) => {
|
||||||
app.filter_mode = match app.filter_mode {
|
app.filter_mode = match app.filter_mode {
|
||||||
|
@ -393,8 +384,6 @@ async fn key_handler(
|
||||||
FilterMode::Session => FilterMode::Directory,
|
FilterMode::Session => FilterMode::Directory,
|
||||||
FilterMode::Directory => FilterMode::Global,
|
FilterMode::Directory => FilterMode::Global,
|
||||||
};
|
};
|
||||||
|
|
||||||
query_results(app, search_mode, db).await.unwrap();
|
|
||||||
}
|
}
|
||||||
TermEvent::Key(Key::Down | Key::Ctrl('n' | 'j'))
|
TermEvent::Key(Key::Down | Key::Ctrl('n' | 'j'))
|
||||||
| TermEvent::Mouse(MouseEvent::Press(MouseButton::WheelDown, _, _)) => {
|
| TermEvent::Mouse(MouseEvent::Press(MouseButton::WheelDown, _, _)) => {
|
||||||
|
@ -632,13 +621,27 @@ async fn select_history(
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let history_count = db.history_count().await?;
|
let history_count = db.history_count().await?;
|
||||||
|
let initial_input = app.input.clone();
|
||||||
|
let initial_filter_mode = app.filter_mode;
|
||||||
|
|
||||||
// Handle input
|
// Handle input
|
||||||
if let Event::Input(input) = events.next()? {
|
if let Event::Input(input) = events.next()? {
|
||||||
if let Some(output) = key_handler(input, search_mode, db, &mut app).await {
|
if let Some(output) = key_handler(&input, &mut app) {
|
||||||
return Ok(output);
|
return Ok(output);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// After we receive input process the whole event channel before query/render.
|
||||||
|
while let Ok(Event::Input(input)) = events.try_next() {
|
||||||
|
if let Some(output) = key_handler(&input, &mut app) {
|
||||||
|
return Ok(output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if initial_input != app.input || initial_filter_mode != app.filter_mode {
|
||||||
|
query_results(&mut app, search_mode, db).await?;
|
||||||
|
}
|
||||||
|
|
||||||
let compact = match style {
|
let compact = match style {
|
||||||
atuin_client::settings::Style::Auto => {
|
atuin_client::settings::Style::Auto => {
|
||||||
terminal.size().map(|size| size.height < 14).unwrap_or(true)
|
terminal.size().map(|size| size.height < 14).unwrap_or(true)
|
||||||
|
|
Loading…
Reference in a new issue