use std::collections::VecDeque;
use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender, TryRecvError};
use std::time::{Duration, Instant};
use color_eyre::Result;
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers, MouseEvent};
use crate::app::jobs::Hold;
use crate::app::keys::paste_keys::PasteTarget;
use crate::app::pointer::Pointer;
use crate::{App, AppEvent};
pub const MAX_HELD_KEYS: usize = 32;
enum Act {
Now,
Hold,
HoldAs(KeyEvent),
Drop,
StopFind,
}
#[derive(Debug, Clone)]
enum Input {
Key(KeyEvent),
Mouse(MouseEvent),
Paste(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum Held {
Key(KeyEvent),
Paste(String),
}
impl Held {
fn key(&self) -> Option<&KeyEvent> {
match self {
Held::Key(key) => Some(key),
Held::Paste(_) => None,
}
}
fn is_navigation(&self) -> bool {
self.key().is_some_and(is_navigation)
}
}
#[derive(Debug)]
pub enum Drained {
Continue {
updated: bool,
progress_only: bool,
},
Exit,
Crash(String),
NotFound(std::path::PathBuf),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Screen {
generation: u64,
modal: bool,
inline_failures: u64,
}
pub struct EventPump {
pub app: App,
tx: Sender<AppEvent>,
rx: Receiver<AppEvent>,
held: VecDeque<Held>,
held_for: Screen,
next_up: VecDeque<(AppEvent, Hold)>,
backlog: VecDeque<AppEvent>,
typed: VecDeque<Input>,
since_key: usize,
early: usize,
}
const RESULTS_PER_KEY: usize = 64;
const KEYS_PER_FRAME: usize = 64;
const BURST_FRAME: Duration = if cfg!(test) {
Duration::from_secs(10)
} else {
Duration::from_millis(50)
};
#[derive(Default)]
struct Burst {
started: Option<Instant>,
keys: usize,
}
impl Burst {
fn counted(&mut self, now: Instant) -> bool {
let started = *self.started.get_or_insert(now);
self.keys += 1;
self.keys < KEYS_PER_FRAME && now.saturating_duration_since(started) < BURST_FRAME
}
}
#[derive(Debug, PartialEq, Eq)]
struct Layout {
overlay: std::mem::Discriminant<crate::Overlay>,
input_mode: std::mem::Discriminant<crate::InputMode>,
help: bool,
modal: bool,
menu: bool,
generation: u64,
}
impl Layout {
fn of(app: &App) -> Self {
Self {
overlay: std::mem::discriminant(&app.overlay),
input_mode: std::mem::discriminant(&app.input_mode),
help: app.help.is_open(),
modal: app.modal_showing(),
menu: app.context_menu.is_some(),
generation: app.screen_generation(),
}
}
}
impl EventPump {
pub fn new(app: App, tx: Sender<AppEvent>, rx: Receiver<AppEvent>) -> Self {
let held_for = Self::screen_of(&app);
Self {
app,
tx,
rx,
held: VecDeque::new(),
held_for,
next_up: VecDeque::new(),
backlog: VecDeque::new(),
typed: VecDeque::new(),
since_key: 0,
early: 0,
}
}
pub fn handle_first(&mut self, events: impl IntoIterator<Item = AppEvent>) {
for event in events {
match event {
AppEvent::Terminal(Event::Key(key)) => {
self.typed.push_back(Input::Key(key));
self.early += 1;
}
AppEvent::Terminal(Event::Mouse(mouse)) => {
self.typed.push_back(Input::Mouse(mouse));
self.early += 1;
}
AppEvent::Terminal(Event::Paste(text)) => {
self.typed.push_back(Input::Paste(text));
self.early += 1;
}
event => self.backlog.push_back(event),
}
}
}
pub fn send(&self, event: AppEvent) -> Result<()> {
self.tx.send(event)?;
Ok(())
}
pub fn held_keys(&self) -> impl Iterator<Item = &KeyEvent> {
self.held.iter().filter_map(Held::key)
}
pub fn terminal_key(&mut self, key: KeyEvent) -> Result<bool> {
self.discard_stale();
match self.classify(&key) {
Act::Now => {
self.dispatch(key)?;
Ok(true)
}
Act::StopFind => {
self.drop_held_finds();
self.dispatch(key)?;
Ok(true)
}
Act::Drop => Ok(false),
Act::Hold => {
self.hold(key);
Ok(false)
}
Act::HoldAs(meant) => {
self.hold(meant);
Ok(false)
}
}
}
pub fn terminal_mouse(&mut self, mouse: MouseEvent) -> Result<bool> {
self.discard_stale();
let acts = |p: &Self, code: KeyCode| {
matches!(
p.classify(&KeyEvent::new(code, KeyModifiers::NONE)),
Act::Now
)
};
match self.app.pointer(&mouse, std::time::Instant::now()) {
Pointer::Nothing => Ok(false),
Pointer::Keys(keys) => self.press_now(keys),
Pointer::Point(target, then) => {
if !acts(self, KeyCode::Down) {
self.app.forget_click();
return Ok(false);
}
self.app.point(&target);
self.press_now(then)?;
Ok(true)
}
Pointer::Form { field, act, keys } => {
if !acts(self, KeyCode::Down) {
return Ok(false);
}
let mut then = keys;
if let Some(id) = field {
let Some(clicked) = self.app.focus_field(&id) else {
return Ok(false);
};
if let Some(back) = act.filter(|_| clicked.acts) {
then.extend(crate::app::pointer::act_key(clicked.kind, back));
}
}
self.press_now(then)?;
Ok(true)
}
Pointer::Tool(tool) => {
if !acts(self, KeyCode::Enter) {
return Ok(false);
}
self.app.point_at_tool(tool);
self.press_now([KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)])?;
Ok(true)
}
Pointer::Resize { column, x } => {
if !acts(self, KeyCode::Char('>')) {
return Ok(false);
}
self.app.start_resize(column, x);
Ok(false)
}
Pointer::Width { column, width } => {
if !self.app.in_normal_table_view() || !acts(self, KeyCode::Char('>')) {
return Ok(false);
}
self.app.set_dragged_width(column, width);
Ok(true)
}
Pointer::Drop { column, onto } => {
if !self.app.in_normal_table_view() || !acts(self, KeyCode::Char('L')) {
return Ok(false);
}
if let Some(event) = self.app.drop_column(&column, &onto) {
self.queue_continuation(event);
}
Ok(true)
}
Pointer::Menu(hit, at) => {
if !acts(self, KeyCode::Down) {
return Ok(false);
}
if self.app.point_for_menu(&hit) {
self.app.open_context_menu(at);
}
Ok(true)
}
Pointer::MenuChoose(i) => {
if let Some(AppEvent::Press(key)) = self.app.choose_from_menu(i) {
self.press_now([key])?;
}
Ok(true)
}
Pointer::Redraw => Ok(true),
Pointer::CloseMenu => {
self.app.close_context_menu();
Ok(true)
}
}
}
pub fn terminal_paste(&mut self, text: String) -> Result<bool> {
self.discard_stale();
if self.held.is_empty() && self.app.paste_target() == PasteTarget::Nowhere {
return Ok(false);
}
let Some(first) = crate::widgets::text_input::one_line(&text).chars().next() else {
return Ok(false);
};
match self.classify(&KeyEvent::new(KeyCode::Char(first), KeyModifiers::NONE)) {
Act::Now => {
self.offer(AppEvent::Paste(text))?;
Ok(true)
}
Act::Hold | Act::HoldAs(_) => {
self.hold_input(Held::Paste(text));
Ok(false)
}
Act::Drop | Act::StopFind => Ok(false),
}
}
fn press_now(&mut self, keys: impl IntoIterator<Item = KeyEvent>) -> Result<bool> {
let mut acted = false;
for key in keys {
match self.classify(&key) {
Act::Now => {}
Act::StopFind => self.drop_held_finds(),
_ => break,
}
self.dispatch(key)?;
acted = true;
}
Ok(acted)
}
fn classify(&self, key: &KeyEvent) -> Act {
if self.app.hard_escape_while_busy(key) {
if key.code == KeyCode::Esc && self.app.finding() {
return Act::StopFind;
}
return Act::Now;
}
if self.app.menu_takes(key) {
return Act::Now;
}
let queued = !self.held.is_empty();
if !self.app.is_busy() && !queued {
return Act::Now;
}
if self.app.is_busy() && self.app.awaiting_dataset() {
if self.app.key_acts_while_busy(key) {
return Act::Now;
}
return Act::Drop;
}
if self.app.is_busy()
&& key.code == KeyCode::Enter
&& self.app.in_normal_table_view()
&& self.app.enter_inspects()
&& self.held.iter().all(Held::is_navigation)
{
return Act::HoldAs(KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE));
}
if self.app.is_busy() && !queued && self.app.key_acts_while_sampling(key) {
return Act::Now;
}
if self.app.is_busy() && !queued && self.app.in_normal_table_view() {
if self.app.key_acts_while_busy(key) {
return Act::Now;
}
if matches!(key.code, KeyCode::Enter | KeyCode::Esc) {
return Act::Drop;
}
}
Act::Hold
}
pub fn replay_one(&mut self) -> Result<bool> {
self.discard_stale();
if self.app.is_busy() {
return Ok(false);
}
let Some(input) = self.held.pop_front() else {
return Ok(false);
};
if self.held.is_empty() {
self.app.set_input_dropped(false);
}
match input {
Held::Key(key) => self.dispatch(key)?,
Held::Paste(text) => self.offer(AppEvent::Paste(text))?,
}
Ok(true)
}
fn take_next(&mut self) -> Result<(AppEvent, Option<Hold>), TryRecvError> {
if let Some((event, lease)) = self.next_up.pop_front() {
return Ok((event, Some(lease)));
}
if let Some(event) = self.backlog.pop_front() {
return Ok((event, None));
}
if !self.typed.is_empty() && (self.early > 0 || self.since_key >= RESULTS_PER_KEY) {
return Err(TryRecvError::Empty);
}
self.rx.try_recv().map(|event| (event, None))
}
pub fn drain(&mut self) -> Result<Drained> {
let first = self.take_next();
self.drain_from(first)
}
pub fn wait_and_drain(&mut self, timeout: Duration) -> Result<Drained> {
if !self.next_up.is_empty() || !self.backlog.is_empty() || !self.typed.is_empty() {
let first = self.take_next();
return self.drain_from(first);
}
let first = self
.rx
.recv_timeout(timeout)
.map(|event| (event, None))
.map_err(|e| match e {
RecvTimeoutError::Timeout => TryRecvError::Empty,
RecvTimeoutError::Disconnected => TryRecvError::Disconnected,
});
self.drain_from(first)
}
fn drain_from(
&mut self,
mut next: Result<(AppEvent, Option<Hold>), TryRecvError>,
) -> Result<Drained> {
let mut updated = false;
let mut progress_only = true;
let mut burst = Burst::default();
loop {
match next {
Ok((AppEvent::Exit, _)) => return Ok(Drained::Exit),
Ok((AppEvent::Crash(msg), _)) => return Ok(Drained::Crash(msg)),
Ok((AppEvent::NamedPathMissing(path), _)) => {
return Ok(Drained::NotFound(path));
}
Ok((AppEvent::Press(key), hold)) => {
if hold.is_some() {
drop(hold);
self.app.let_waiting_errands_in();
}
if self.typed.is_empty() {
self.since_key = 0;
}
self.typed.push_front(Input::Key(key));
if self.early > 0 {
self.early += 1;
}
}
Ok((AppEvent::Terminal(Event::Key(key)), _)) => {
if self.typed.is_empty() {
self.since_key = 0;
}
self.typed.push_back(Input::Key(key));
}
Ok((AppEvent::Terminal(Event::Mouse(mouse)), _)) => {
if self.typed.is_empty() {
self.since_key = 0;
}
self.typed.push_back(Input::Mouse(mouse));
}
Ok((AppEvent::Terminal(Event::Paste(text)), _)) => {
if self.typed.is_empty() {
self.since_key = 0;
}
self.typed.push_back(Input::Paste(text));
}
Ok((AppEvent::Terminal(Event::Resize(cols, rows)), _)) => {
next = Ok((AppEvent::Resize(cols, rows), None));
continue;
}
Ok((AppEvent::Terminal(_), _)) => {}
Ok((event, mut continuation)) => {
updated = true;
progress_only &= event.is_progress();
self.since_key += 1;
self.app.pointer.changed();
let follow_up = match self.app.handle(event) {
Ok(follow_up) => follow_up,
Err(deferred) => {
self.hold(deferred);
None
}
};
self.discard_stale();
if let Some(follow_up) = follow_up {
self.queue_continuation(follow_up);
drop(continuation);
break;
}
if let Some(hold) = continuation.take() {
drop(hold);
self.app.let_waiting_errands_in();
}
}
Err(TryRecvError::Empty) => {
if matches!(self.typed.front(), Some(Input::Mouse(_)))
&& !self.app.pointer.on_screen()
{
updated = true;
progress_only = false;
break;
}
let Some(mut input) = self.typed.pop_front() else {
break;
};
while let (Input::Mouse(now), Some(Input::Mouse(next))) =
(&input, self.typed.front())
&& is_drag(now)
&& is_drag(next)
{
input = Input::Mouse(*next);
self.typed.pop_front();
self.early = self.early.saturating_sub(1);
}
self.since_key = 0;
self.early = self.early.saturating_sub(1);
let layout = Layout::of(&self.app);
let acted = match input {
Input::Key(key) => self.terminal_key(key)?,
Input::Mouse(mouse) => self.terminal_mouse(mouse)?,
Input::Paste(text) => self.terminal_paste(text)?,
};
if acted {
updated = true;
progress_only = false;
if !self.burst_goes_on(&mut burst, &layout) {
break;
}
}
}
Err(TryRecvError::Disconnected) => return Ok(Drained::Exit),
}
next = self.take_next();
}
Ok(Drained::Continue {
updated,
progress_only: updated && progress_only,
})
}
fn burst_goes_on(&self, burst: &mut Burst, before: &Layout) -> bool {
burst.counted(Instant::now())
&& self.next_up.is_empty()
&& self.held.is_empty()
&& !self.app.is_busy()
&& Layout::of(&self.app) == *before
}
pub fn run(&mut self, mut draw: impl FnMut(&mut App) -> Result<()>) -> Result<Ended> {
let mut pacer = Pacer::default();
let mut first_rows = crate::loading::first_rows_trace::FirstRowsTrace::from_env();
draw(&mut self.app)?;
self.app.frame_painted();
first_rows.painted(&self.app);
pacer.drew(Instant::now());
loop {
let mut pass = Pass::default();
pass.updated = self.replay_one()?;
pass.progress_only = !pass.updated;
if let Some(end) = pass.add(self.drain()?) {
return Ok(end);
}
if !pass.updated {
let now = Instant::now();
pacer.spinning(self.app.something_is_spinning(), self.app.is_busy(), now);
let timeout = pacer.timeout(self.app.next_deadline(), now);
if let Some(end) = pass.add(self.wait_and_drain(timeout)?) {
return Ok(end);
}
}
let app = &mut self.app;
let now = Instant::now();
let mut redraw = pacer.handled(pass.updated, pass.progress_only, now);
pacer.spinning(app.something_is_spinning(), app.is_busy(), now);
if pacer.turn_spinner(now) {
app.throbber_frame = app.throbber_frame.wrapping_add(1);
redraw = true;
}
redraw |= app.tick_flash();
redraw |= app.tick_follow_clock();
redraw |= app.flash_background_panic();
redraw |= app.flash_polars_warning();
app.request_what_the_frame_needs();
if redraw {
draw(app)?;
app.frame_painted();
first_rows.painted(app);
pacer.drew(now);
app.request_what_the_frame_needs();
}
}
}
fn queue_continuation(&mut self, follow_up: AppEvent) {
let hold = self.app.hold_the_generation();
self.next_up.push_back((follow_up, hold));
}
fn dispatch(&mut self, key: KeyEvent) -> Result<()> {
self.offer(AppEvent::Key(key))
}
fn offer(&mut self, input: AppEvent) -> Result<()> {
self.app.pointer.changed();
let gen_before = self.app.screen_generation();
match self.app.handle(input) {
Ok(Some(follow_up)) => self.queue_continuation(follow_up),
Ok(None) => {}
Err(deferred) => self.held.push_front(Held::Key(deferred)),
}
if self.app.screen_generation() != gen_before {
self.held.clear();
self.app.set_input_dropped(false);
} else {
self.held_for = Self::screen_of(&self.app);
}
Ok(())
}
fn hold(&mut self, key: KeyEvent) {
if is_navigation(&key)
&& !replays_each_press(&key)
&& self.held.back().and_then(Held::key) == Some(&key)
&& self.app.in_normal_table_view()
&& self.held.iter().all(Held::is_navigation)
{
return;
}
self.hold_input(Held::Key(key));
}
fn hold_input(&mut self, input: Held) {
if self.held.is_empty() {
self.held_for = Self::screen_of(&self.app);
}
if self.held.len() >= MAX_HELD_KEYS {
self.app.set_input_dropped(true);
return;
}
self.held.push_back(input);
}
fn drop_held_finds(&mut self) {
let run = self.held.iter().take_while(|k| k.is_navigation()).count();
let rest = self.held.split_off(run);
self.held.retain(|k| {
!k.key()
.is_some_and(|k| matches!(k.code, KeyCode::Char('n' | 'N')))
});
self.held.extend(rest);
if self.held.is_empty() {
self.app.set_input_dropped(false);
}
}
fn discard_stale(&mut self) {
if self.held.is_empty() {
return;
}
let now = Self::screen_of(&self.app);
let abandoned = now.generation != self.held_for.generation;
let new_modal = now.modal && !self.held_for.modal;
let failed_inline = now.inline_failures != self.held_for.inline_failures;
if abandoned || new_modal || failed_inline {
self.held.clear();
self.app.set_input_dropped(false);
}
}
fn screen_of(app: &App) -> Screen {
Screen {
generation: app.screen_generation(),
modal: app.modal_showing(),
inline_failures: app.inline_failures(),
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum Ended {
Quit,
Crash(String),
NotFound(std::path::PathBuf),
}
#[derive(Default)]
struct Pass {
updated: bool,
progress_only: bool,
}
impl Pass {
fn add(&mut self, drained: Drained) -> Option<Ended> {
match drained {
Drained::Continue {
updated,
progress_only,
} => {
if updated {
self.progress_only = progress_only && (self.progress_only || !self.updated);
self.updated = true;
}
None
}
Drained::Exit => Some(Ended::Quit),
Drained::Crash(msg) => Some(Ended::Crash(msg)),
Drained::NotFound(path) => Some(Ended::NotFound(path)),
}
}
}
pub const SPINNER_FRAME: Duration = Duration::from_millis(33);
pub const SPINNER_IDLE_FRAME: Duration = Duration::from_millis(100);
pub const PROGRESS_FRAME: Duration = Duration::from_millis(33);
#[derive(Debug, Default)]
pub struct Pacer {
last_draw: Option<Instant>,
owed: bool,
spin_due: Option<Instant>,
spin_every: Duration,
}
impl Pacer {
pub fn spinning(&mut self, on: bool, waited_on: bool, now: Instant) {
self.spin_every = if waited_on {
SPINNER_FRAME
} else {
SPINNER_IDLE_FRAME
};
if on {
let next = now + self.spin_every;
self.spin_due = Some(self.spin_due.map_or(next, |due| due.min(next)));
} else {
self.spin_due = None;
}
}
pub fn turn_spinner(&mut self, now: Instant) -> bool {
match self.spin_due {
Some(due) if now >= due => {
self.spin_due = Some(now + self.spin_every);
true
}
_ => false,
}
}
pub fn handled(&mut self, updated: bool, progress_only: bool, now: Instant) -> bool {
let progress_due = self
.last_draw
.is_none_or(|last| now >= last + PROGRESS_FRAME);
if updated && !progress_only {
return true;
}
if updated {
self.owed = true;
}
self.owed && progress_due
}
pub fn drew(&mut self, now: Instant) {
self.last_draw = Some(now);
self.owed = false;
}
pub fn timeout(&self, app_deadline: Option<Instant>, now: Instant) -> Duration {
let owed = self
.owed
.then(|| self.last_draw.map(|last| last + PROGRESS_FRAME))
.flatten();
[self.spin_due, owed, app_deadline]
.into_iter()
.flatten()
.min()
.map_or(Duration::MAX, |at| at.saturating_duration_since(now))
}
}
fn replays_each_press(key: &KeyEvent) -> bool {
matches!(
key.code,
KeyCode::Left
| KeyCode::Right
| KeyCode::Char('h')
| KeyCode::Char('l')
| KeyCode::Char('{')
| KeyCode::Char('}')
| KeyCode::Char('n')
| KeyCode::Char('N')
)
}
fn is_drag(mouse: &MouseEvent) -> bool {
matches!(mouse.kind, crossterm::event::MouseEventKind::Drag(_))
}
fn is_navigation(key: &KeyEvent) -> bool {
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
match key.code {
KeyCode::Up
| KeyCode::Down
| KeyCode::PageUp
| KeyCode::PageDown
| KeyCode::Home
| KeyCode::End
| KeyCode::Left
| KeyCode::Right
| KeyCode::Char('j')
| KeyCode::Char('k')
| KeyCode::Char('h')
| KeyCode::Char('l')
| KeyCode::Char('{')
| KeyCode::Char('}')
| KeyCode::Char('G')
| KeyCode::Char('n')
| KeyCode::Char('N') => true,
KeyCode::Char('f') | KeyCode::Char('b') | KeyCode::Char('d') | KeyCode::Char('u') => ctrl,
_ => false,
}
}
#[cfg(test)]
mod tests;