datui_lib/app/event_pump.rs
1//! The main loop, minus the terminal.
2//!
3//! `run()` sets up the terminal and hands [`EventPump::run`] a way to draw. Keys
4//! arrive on the same channel as worker results ([`crate::app::terminal_input`]), so the
5//! loop sleeps until either arrives or a deadline passes ([`Pacer`]). Keys typed
6//! while busy are held in order and replayed one per iteration once idle, through
7//! the path a fresh key takes; a replayed key's follow-ups drain before the next is
8//! offered, so a queued Enter finishes its search first. Keys that arrive together
9//! and act at once are drawn together ([`EventPump::burst_goes_on`]).
10
11use std::collections::VecDeque;
12use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender, TryRecvError};
13use std::time::{Duration, Instant};
14
15use color_eyre::Result;
16use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers, MouseEvent};
17
18use crate::app::jobs::Hold;
19use crate::app::keys::paste_keys::PasteTarget;
20use crate::app::pointer::Pointer;
21use crate::{App, AppEvent};
22
23/// Keys held while busy. Beyond this the newest is dropped and the user told: the
24/// oldest may be the `/` that makes the rest a query rather than hotkeys.
25pub const MAX_HELD_KEYS: usize = 32;
26
27/// What a fresh key should do while the app cannot take it directly.
28enum Act {
29 /// Handle it now.
30 Now,
31 /// Hold it for replay once the app is idle.
32 Hold,
33 /// Hold this key instead: what the typed key means where it was typed.
34 HoldAs(KeyEvent),
35 /// Discard it: a bare Enter/Esc at a busy table confirms nothing.
36 Drop,
37 /// Handle it now and drop the `n`/`N` held behind it: one Esc stops every queued
38 /// find.
39 StopFind,
40}
41
42/// A key, mouse event or paste read from the terminal, kept in arrival order.
43#[derive(Debug, Clone)]
44enum Input {
45 Key(KeyEvent),
46 Mouse(MouseEvent),
47 Paste(String),
48}
49
50/// Typed input held while the app was busy, replayed in order once it is idle.
51#[derive(Debug, Clone, PartialEq, Eq)]
52enum Held {
53 Key(KeyEvent),
54 /// A paste, taken as one edit by whatever field types when it is replayed.
55 Paste(String),
56}
57
58impl Held {
59 fn key(&self) -> Option<&KeyEvent> {
60 match self {
61 Held::Key(key) => Some(key),
62 Held::Paste(_) => None,
63 }
64 }
65
66 fn is_navigation(&self) -> bool {
67 self.key().is_some_and(is_navigation)
68 }
69}
70
71/// What a pass over the channel found.
72#[derive(Debug)]
73pub enum Drained {
74 /// Keep going. `updated`: something was handled and a frame is due.
75 /// `progress_only`: all of it was progress reports ([`AppEvent::is_progress`]),
76 /// whose frame may wait.
77 Continue {
78 updated: bool,
79 progress_only: bool,
80 },
81 Exit,
82 Crash(String),
83 /// A path named at startup is not there.
84 NotFound(std::path::PathBuf),
85}
86
87/// The screen the held keys were typed at. A change means their target is gone: a
88/// modal that ended the work, the dataset left for home, or a statement's failure
89/// under the query prompt.
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91struct Screen {
92 generation: u64,
93 modal: bool,
94 inline_failures: u64,
95}
96
97/// Owns the app, its channel and the keys held while it was busy.
98pub struct EventPump {
99 pub app: App,
100 tx: Sender<AppEvent>,
101 rx: Receiver<AppEvent>,
102 held: VecDeque<Held>,
103 held_for: Screen,
104 /// Continuations a handler returned, each holding the generation. Ahead of the
105 /// channel: a follow-up is the rest of the event just handled. The hold covers the
106 /// frame drawn before it runs, so a replayed key cannot find the generation free.
107 next_up: VecDeque<(AppEvent, Hold)>,
108 /// Events that arrived before the app existed (while `run` read the settings, then
109 /// the startup open), handled first in order. Keys typed meanwhile are in
110 /// [`Self::typed`].
111 backlog: VecDeque<AppEvent>,
112 /// Keys read and not yet offered, oldest first. Each waits for what arrived on the
113 /// channel behind it, so a held-down `j` does not starve the load-ahead's answer.
114 typed: VecDeque<Input>,
115 /// Events handled since a key was last offered, bounded by [`RESULTS_PER_KEY`] so a
116 /// fast worker cannot starve the keyboard.
117 since_key: usize,
118 /// How many keys at the front of [`Self::typed`] came from the backlog. Typed
119 /// before anything was sent on the channel, they wait on none of it (a Ctrl+O
120 /// typed during startup must beat the startup open).
121 early: usize,
122}
123
124/// The most channel events handled while a typed key waits; then the key is offered.
125const RESULTS_PER_KEY: usize = 64;
126
127/// The most keys of a burst handled before a frame shows them ([`EventPump::burst_goes_on`]).
128const KEYS_PER_FRAME: usize = 64;
129
130/// The longest a burst's keys are handled before a frame shows them. Tests count
131/// frames, and a loaded test machine must not split a burst.
132const BURST_FRAME: Duration = if cfg!(test) {
133 Duration::from_secs(10)
134} else {
135 Duration::from_millis(50)
136};
137
138/// The keys handled since the last frame, in one pass over the channel.
139#[derive(Default)]
140struct Burst {
141 started: Option<Instant>,
142 keys: usize,
143}
144
145impl Burst {
146 /// Count a key handled at `now`; whether the burst has room for another before a
147 /// frame.
148 fn counted(&mut self, now: Instant) -> bool {
149 let started = *self.started.get_or_insert(now);
150 self.keys += 1;
151 self.keys < KEYS_PER_FRAME && now.saturating_duration_since(started) < BURST_FRAME
152 }
153}
154
155/// What the keys after one read from the last frame: the screen and what is over it.
156/// A key that changes it ends a burst, so the next key meets the new layout drawn.
157#[derive(Debug, PartialEq, Eq)]
158struct Layout {
159 overlay: std::mem::Discriminant<crate::Overlay>,
160 input_mode: std::mem::Discriminant<crate::InputMode>,
161 help: bool,
162 modal: bool,
163 menu: bool,
164 generation: u64,
165}
166
167impl Layout {
168 fn of(app: &App) -> Self {
169 Self {
170 overlay: std::mem::discriminant(&app.overlay),
171 input_mode: std::mem::discriminant(&app.input_mode),
172 help: app.help.is_open(),
173 modal: app.modal_showing(),
174 menu: app.context_menu.is_some(),
175 generation: app.screen_generation(),
176 }
177 }
178}
179
180impl EventPump {
181 pub fn new(app: App, tx: Sender<AppEvent>, rx: Receiver<AppEvent>) -> Self {
182 let held_for = Self::screen_of(&app);
183 Self {
184 app,
185 tx,
186 rx,
187 held: VecDeque::new(),
188 held_for,
189 next_up: VecDeque::new(),
190 backlog: VecDeque::new(),
191 typed: VecDeque::new(),
192 since_key: 0,
193 early: 0,
194 }
195 }
196
197 /// Handle `events` before the channel: they arrived first, or are the startup open
198 /// those keys were typed at. Keys among them are offered after the other events,
199 /// ahead of the channel.
200 pub fn handle_first(&mut self, events: impl IntoIterator<Item = AppEvent>) {
201 for event in events {
202 match event {
203 AppEvent::Terminal(Event::Key(key)) => {
204 self.typed.push_back(Input::Key(key));
205 self.early += 1;
206 }
207 AppEvent::Terminal(Event::Mouse(mouse)) => {
208 self.typed.push_back(Input::Mouse(mouse));
209 self.early += 1;
210 }
211 AppEvent::Terminal(Event::Paste(text)) => {
212 self.typed.push_back(Input::Paste(text));
213 self.early += 1;
214 }
215 event => self.backlog.push_back(event),
216 }
217 }
218 }
219
220 pub fn send(&self, event: AppEvent) -> Result<()> {
221 self.tx.send(event)?;
222 Ok(())
223 }
224
225 /// The keys waiting for the app to go idle, oldest first (pastes left out).
226 pub fn held_keys(&self) -> impl Iterator<Item = &KeyEvent> {
227 self.held.iter().filter_map(Held::key)
228 }
229
230 /// A key from the terminal, classified by `classify`: handled now, held behind
231 /// earlier keys, held as another key, or dropped. Returns whether the app changed.
232 pub fn terminal_key(&mut self, key: KeyEvent) -> Result<bool> {
233 self.discard_stale();
234 match self.classify(&key) {
235 Act::Now => {
236 self.dispatch(key)?;
237 Ok(true)
238 }
239 Act::StopFind => {
240 self.drop_held_finds();
241 self.dispatch(key)?;
242 Ok(true)
243 }
244 Act::Drop => Ok(false),
245 Act::Hold => {
246 self.hold(key);
247 Ok(false)
248 }
249 Act::HoldAs(meant) => {
250 self.hold(meant);
251 Ok(false)
252 }
253 }
254 }
255
256 /// A mouse event, meaning what it lands on in the last frame ([`App::pointer`]).
257 /// Never held: aimed at the screen now, it would land elsewhere later. Each acts
258 /// where the key it stands for would act at once, and is dropped where that key
259 /// would wait: the wheel as arrows, a click as ↓, a chip or menu line or tool as
260 /// its key, a dragged width as `>`, a dropped header as `L`. Returns whether the app
261 /// changed.
262 pub fn terminal_mouse(&mut self, mouse: MouseEvent) -> Result<bool> {
263 self.discard_stale();
264 let acts = |p: &Self, code: KeyCode| {
265 matches!(
266 p.classify(&KeyEvent::new(code, KeyModifiers::NONE)),
267 Act::Now
268 )
269 };
270 match self.app.pointer(&mouse, std::time::Instant::now()) {
271 Pointer::Nothing => Ok(false),
272 Pointer::Keys(keys) => self.press_now(keys),
273 Pointer::Point(target, then) => {
274 if !acts(self, KeyCode::Down) {
275 // Not a click the next one can make a double click of.
276 self.app.forget_click();
277 return Ok(false);
278 }
279 self.app.point(&target);
280 self.press_now(then)?;
281 Ok(true)
282 }
283 Pointer::Form { field, act, keys } => {
284 if !acts(self, KeyCode::Down) {
285 return Ok(false);
286 }
287 let mut then = keys;
288 if let Some(id) = field {
289 let Some(clicked) = self.app.focus_field(&id) else {
290 return Ok(false);
291 };
292 if let Some(back) = act.filter(|_| clicked.acts) {
293 then.extend(crate::app::pointer::act_key(clicked.kind, back));
294 }
295 }
296 self.press_now(then)?;
297 Ok(true)
298 }
299 Pointer::Tool(tool) => {
300 if !acts(self, KeyCode::Enter) {
301 return Ok(false);
302 }
303 self.app.point_at_tool(tool);
304 self.press_now([KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)])?;
305 Ok(true)
306 }
307 Pointer::Resize { column, x } => {
308 if !acts(self, KeyCode::Char('>')) {
309 return Ok(false);
310 }
311 self.app.start_resize(column, x);
312 Ok(false)
313 }
314 Pointer::Width { column, width } => {
315 if !self.app.in_normal_table_view() || !acts(self, KeyCode::Char('>')) {
316 return Ok(false);
317 }
318 self.app.set_dragged_width(column, width);
319 Ok(true)
320 }
321 Pointer::Drop { column, onto } => {
322 if !self.app.in_normal_table_view() || !acts(self, KeyCode::Char('L')) {
323 return Ok(false);
324 }
325 if let Some(event) = self.app.drop_column(&column, &onto) {
326 self.queue_continuation(event);
327 }
328 Ok(true)
329 }
330 Pointer::Menu(hit, at) => {
331 if !acts(self, KeyCode::Down) {
332 return Ok(false);
333 }
334 // Only on the cell the cursor landed on: rows that moved since drawing were not
335 // the ones clicked.
336 if self.app.point_for_menu(&hit) {
337 self.app.open_context_menu(at);
338 }
339 Ok(true)
340 }
341 Pointer::MenuChoose(i) => {
342 if let Some(AppEvent::Press(key)) = self.app.choose_from_menu(i) {
343 self.press_now([key])?;
344 }
345 Ok(true)
346 }
347 Pointer::Redraw => Ok(true),
348 Pointer::CloseMenu => {
349 self.app.close_context_menu();
350 Ok(true)
351 }
352 }
353 }
354
355 /// A paste, taken as one edit by the field that takes typed text
356 /// ([`App::paste_target`]); where nothing does, dropped, never read as keys. Typed
357 /// where a key would wait, it waits in its place, and goes to the field focused
358 /// when it is replayed. Returns whether the app changed.
359 pub fn terminal_paste(&mut self, text: String) -> Result<bool> {
360 self.discard_stale();
361 // Held keys (a `:` typed while busy) may open the field it is meant for.
362 if self.held.is_empty() && self.app.paste_target() == PasteTarget::Nowhere {
363 return Ok(false);
364 }
365 // Classified as the text's first character, typed.
366 let Some(first) = crate::widgets::text_input::one_line(&text).chars().next() else {
367 return Ok(false);
368 };
369 match self.classify(&KeyEvent::new(KeyCode::Char(first), KeyModifiers::NONE)) {
370 Act::Now => {
371 self.offer(AppEvent::Paste(text))?;
372 Ok(true)
373 }
374 Act::Hold | Act::HoldAs(_) => {
375 self.hold_input(Held::Paste(text));
376 Ok(false)
377 }
378 Act::Drop | Act::StopFind => Ok(false),
379 }
380 }
381
382 /// Press `keys` in order while each would act at once; the rest are dropped.
383 fn press_now(&mut self, keys: impl IntoIterator<Item = KeyEvent>) -> Result<bool> {
384 let mut acted = false;
385 for key in keys {
386 match self.classify(&key) {
387 Act::Now => {}
388 Act::StopFind => self.drop_held_finds(),
389 _ => break,
390 }
391 self.dispatch(key)?;
392 acted = true;
393 }
394 Ok(acted)
395 }
396
397 fn classify(&self, key: &KeyEvent) -> Act {
398 // Escapes jump the queue, busy or idle (Ctrl-Q/Ctrl-C quit, Ctrl-O home, a
399 // confirmation answered). First, so a Ctrl-C during replay is not queued where a
400 // modal could discard it.
401 if self.app.hard_escape_while_busy(key) {
402 if key.code == KeyCode::Esc && self.app.finding() {
403 return Act::StopFind;
404 }
405 return Act::Now;
406 }
407 // The open menu's keys read nothing and act at once; a chosen line presses its key
408 // as typed.
409 if self.app.menu_takes(key) {
410 return Act::Now;
411 }
412 let queued = !self.held.is_empty();
413 // Idle with nothing ahead: handle now.
414 if !self.app.is_busy() && !queued {
415 return Act::Now;
416 }
417 // The loading screen has nothing to type into: allowed keys act, the rest drop
418 // (one held stray would queue `q` behind it for the whole load).
419 if self.app.is_busy() && self.app.awaiting_dataset() {
420 if self.app.key_acts_while_busy(key) {
421 return Act::Now;
422 }
423 return Act::Drop;
424 }
425 // Enter with nothing to drill into is Space, held as Space so a result drillable
426 // by replay time is not drilled. Only while held keys move the cursor: after `/`
427 // it is text's Enter.
428 if self.app.is_busy()
429 && key.code == KeyCode::Enter
430 && self.app.in_normal_table_view()
431 && self.app.enter_inspects()
432 && self.held.iter().all(Held::is_navigation)
433 {
434 return Act::HoldAs(KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE));
435 }
436 // A sample draw holds only what needs every row: moving, finding and inspecting
437 // the rows on hand act at once.
438 if self.app.is_busy() && !queued && self.app.key_acts_while_sampling(key) {
439 return Act::Now;
440 }
441 // Busy at the plain table with nothing queued: harmless view keys act; a bare Enter
442 // or Esc confirms nothing and drops; everything else waits. Once anything is
443 // queued, or in a text field or modal, every key waits to keep typed order.
444 if self.app.is_busy() && !queued && self.app.in_normal_table_view() {
445 if self.app.key_acts_while_busy(key) {
446 return Act::Now;
447 }
448 if matches!(key.code, KeyCode::Enter | KeyCode::Esc) {
449 return Act::Drop;
450 }
451 }
452 Act::Hold
453 }
454
455 /// Replay the oldest held key if the app is idle. Returns whether one was replayed.
456 pub fn replay_one(&mut self) -> Result<bool> {
457 self.discard_stale();
458 if self.app.is_busy() {
459 return Ok(false);
460 }
461 let Some(input) = self.held.pop_front() else {
462 return Ok(false);
463 };
464 if self.held.is_empty() {
465 self.app.set_input_dropped(false);
466 }
467 match input {
468 Held::Key(key) => self.dispatch(key)?,
469 Held::Paste(text) => self.offer(AppEvent::Paste(text))?,
470 }
471 Ok(true)
472 }
473
474 /// The next event: a continuation, then the backlog, then the channel. `Empty` once
475 /// a typed key has waited long enough, or came from the backlog, so it is offered.
476 fn take_next(&mut self) -> Result<(AppEvent, Option<Hold>), TryRecvError> {
477 if let Some((event, lease)) = self.next_up.pop_front() {
478 return Ok((event, Some(lease)));
479 }
480 if let Some(event) = self.backlog.pop_front() {
481 return Ok((event, None));
482 }
483 if !self.typed.is_empty() && (self.early > 0 || self.since_key >= RESULTS_PER_KEY) {
484 return Err(TryRecvError::Empty);
485 }
486 self.rx.try_recv().map(|event| (event, None))
487 }
488
489 /// Handle everything waiting on the channel.
490 pub fn drain(&mut self) -> Result<Drained> {
491 let first = self.take_next();
492 self.drain_from(first)
493 }
494
495 /// Wait up to `timeout` for the next event, then handle it and everything behind
496 /// it. The run loop's only wait. `Duration::MAX` waits indefinitely.
497 pub fn wait_and_drain(&mut self, timeout: Duration) -> Result<Drained> {
498 // A continuation is waiting: do not sit on it for the whole timeout.
499 if !self.next_up.is_empty() || !self.backlog.is_empty() || !self.typed.is_empty() {
500 let first = self.take_next();
501 return self.drain_from(first);
502 }
503 let first = self
504 .rx
505 .recv_timeout(timeout)
506 .map(|event| (event, None))
507 .map_err(|e| match e {
508 RecvTimeoutError::Timeout => TryRecvError::Empty,
509 RecvTimeoutError::Disconnected => TryRecvError::Disconnected,
510 });
511 self.drain_from(first)
512 }
513
514 fn drain_from(
515 &mut self,
516 mut next: Result<(AppEvent, Option<Hold>), TryRecvError>,
517 ) -> Result<Drained> {
518 let mut updated = false;
519 let mut progress_only = true;
520 let mut burst = Burst::default();
521 loop {
522 match next {
523 Ok((AppEvent::Exit, _)) => return Ok(Drained::Exit),
524 Ok((AppEvent::Crash(msg), _)) => return Ok(Drained::Crash(msg)),
525 // A path named at startup is not there: the session ends naming it. The look only
526 // says so while current, so a user who moved on stays.
527 Ok((AppEvent::NamedPathMissing(path), _)) => {
528 return Ok(Drained::NotFound(path));
529 }
530 // Offered once what arrived behind it is handled ([`Self::typed`]). A key pressed
531 // for the user (Enter on a help line) is offered next as typed, so `classify`
532 // treats it like the key itself.
533 Ok((AppEvent::Press(key), hold)) => {
534 if hold.is_some() {
535 drop(hold);
536 self.app.let_waiting_errands_in();
537 }
538 if self.typed.is_empty() {
539 self.since_key = 0;
540 }
541 self.typed.push_front(Input::Key(key));
542 if self.early > 0 {
543 self.early += 1;
544 }
545 }
546 Ok((AppEvent::Terminal(Event::Key(key)), _)) => {
547 if self.typed.is_empty() {
548 self.since_key = 0;
549 }
550 self.typed.push_back(Input::Key(key));
551 }
552 Ok((AppEvent::Terminal(Event::Mouse(mouse)), _)) => {
553 if self.typed.is_empty() {
554 self.since_key = 0;
555 }
556 self.typed.push_back(Input::Mouse(mouse));
557 }
558 Ok((AppEvent::Terminal(Event::Paste(text)), _)) => {
559 if self.typed.is_empty() {
560 self.since_key = 0;
561 }
562 self.typed.push_back(Input::Paste(text));
563 }
564 Ok((AppEvent::Terminal(Event::Resize(cols, rows)), _)) => {
565 next = Ok((AppEvent::Resize(cols, rows), None));
566 continue;
567 }
568 Ok((AppEvent::Terminal(_), _)) => {}
569 Ok((event, mut continuation)) => {
570 updated = true;
571 progress_only &= event.is_progress();
572 self.since_key += 1;
573 self.app.pointer.changed();
574 let follow_up = match self.app.handle(event) {
575 Ok(follow_up) => follow_up,
576 Err(deferred) => {
577 self.hold(deferred);
578 None
579 }
580 };
581 self.discard_stale();
582 if let Some(follow_up) = follow_up {
583 // A follow-up defers work so the UI can show the current phase first (the `Do*`
584 // events rely on it). Break so a frame is drawn and keys are polled before it
585 // runs. Its hold is taken before this event's is released, so the generation is
586 // never free between phases.
587 self.queue_continuation(follow_up);
588 drop(continuation);
589 break;
590 }
591 // After the handler: whatever phase this event started holds the generation now.
592 // Then errands waiting on the hold get their turn.
593 if let Some(hold) = continuation.take() {
594 drop(hold);
595 self.app.let_waiting_errands_in();
596 }
597 }
598 Err(TryRecvError::Empty) => {
599 // The pointer was aimed at the frame on screen; after changes it waits for the
600 // frame showing them, requested now.
601 if matches!(self.typed.front(), Some(Input::Mouse(_)))
602 && !self.app.pointer.on_screen()
603 {
604 updated = true;
605 progress_only = false;
606 break;
607 }
608 let Some(mut input) = self.typed.pop_front() else {
609 break;
610 };
611 // A drag reports every cell crossed; only the latest position matters.
612 while let (Input::Mouse(now), Some(Input::Mouse(next))) =
613 (&input, self.typed.front())
614 && is_drag(now)
615 && is_drag(next)
616 {
617 input = Input::Mouse(*next);
618 self.typed.pop_front();
619 self.early = self.early.saturating_sub(1);
620 }
621 self.since_key = 0;
622 self.early = self.early.saturating_sub(1);
623 let layout = Layout::of(&self.app);
624 let acted = match input {
625 Input::Key(key) => self.terminal_key(key)?,
626 Input::Mouse(mouse) => self.terminal_mouse(mouse)?,
627 Input::Paste(text) => self.terminal_paste(text)?,
628 };
629 if acted {
630 updated = true;
631 progress_only = false;
632 if !self.burst_goes_on(&mut burst, &layout) {
633 break;
634 }
635 }
636 }
637 Err(TryRecvError::Disconnected) => return Ok(Drained::Exit),
638 }
639 next = self.take_next();
640 }
641 Ok(Drained::Continue {
642 updated,
643 progress_only: updated && progress_only,
644 })
645 }
646
647 /// Whether the keys typed behind one that just acted are handled before the frame
648 /// showing it. A burst (key repeat, keys queued behind a slow frame) is drawn once:
649 /// redrawing for every key only sends frames the screen replaces at once. A key
650 /// that queued a follow-up gets its frame first (the follow-up shows its phase),
651 /// as does one that made the app busy (its spinner) or held keys, and one that
652 /// changed the screen (`before`, its [`Layout`] before the key): the keys behind it
653 /// read the layout the frame records (rows on screen, a panel's height). A burst is
654 /// drawn every [`KEYS_PER_FRAME`] keys or [`BURST_FRAME`] so a held key shows
655 /// motion. A click waits for its frame anyway (see `drain_from`).
656 fn burst_goes_on(&self, burst: &mut Burst, before: &Layout) -> bool {
657 burst.counted(Instant::now())
658 && self.next_up.is_empty()
659 && self.held.is_empty()
660 && !self.app.is_busy()
661 && Layout::of(&self.app) == *before
662 }
663
664 /// The run loop: draw the first frame, then replay one held key, handle what has
665 /// arrived, sleep until something arrives or a deadline passes, and redraw, until
666 /// exit. Everything comes through one channel, so nothing waits out a poll
667 /// interval; a queued continuation runs right after the frame showing its phase.
668 pub fn run(&mut self, mut draw: impl FnMut(&mut App) -> Result<()>) -> Result<Ended> {
669 let mut pacer = Pacer::default();
670 let mut first_rows = crate::loading::first_rows_trace::FirstRowsTrace::from_env();
671 draw(&mut self.app)?;
672 self.app.frame_painted();
673 first_rows.painted(&self.app);
674 pacer.drew(Instant::now());
675 loop {
676 let mut pass = Pass::default();
677 // A replayed key's follow-up (a Search, an Export) is handled in this drain,
678 // before anything typed since can overtake it.
679 pass.updated = self.replay_one()?;
680 pass.progress_only = !pass.updated;
681 if let Some(end) = pass.add(self.drain()?) {
682 return Ok(end);
683 }
684 if !pass.updated {
685 let now = Instant::now();
686 pacer.spinning(self.app.something_is_spinning(), self.app.is_busy(), now);
687 let timeout = pacer.timeout(self.app.next_deadline(), now);
688 if let Some(end) = pass.add(self.wait_and_drain(timeout)?) {
689 return Ok(end);
690 }
691 }
692 let app = &mut self.app;
693 let now = Instant::now();
694 let mut redraw = pacer.handled(pass.updated, pass.progress_only, now);
695 // The throbber turns while busy, or while a count or anything else with a spinner
696 // is out.
697 pacer.spinning(app.something_is_spinning(), app.is_busy(), now);
698 if pacer.turn_spinner(now) {
699 app.throbber_frame = app.throbber_frame.wrapping_add(1);
700 redraw = true;
701 }
702 redraw |= app.tick_flash();
703 redraw |= app.tick_follow_clock();
704 redraw |= app.flash_background_panic();
705 redraw |= app.flash_polars_warning();
706
707 // `App::frame_work` is this and the frame below without the paint, for
708 // harnesses: keep the two in step.
709 app.request_what_the_frame_needs();
710
711 if redraw {
712 draw(app)?;
713 // Read the rows the frame needed, and start a count waiting on their paint.
714 app.frame_painted();
715 first_rows.painted(app);
716 pacer.drew(now);
717 // Ask now for what the frame drew without knowing: there may be no next pass.
718 app.request_what_the_frame_needs();
719 }
720 }
721 }
722
723 /// Hold a continuation, and the generation, until it is dispatched.
724 fn queue_continuation(&mut self, follow_up: AppEvent) {
725 let hold = self.app.hold_the_generation();
726 self.next_up.push_back((follow_up, hold));
727 }
728
729 /// Offer one key to the app as the channel drain does, then reconcile the held keys
730 /// with the screen it left.
731 fn dispatch(&mut self, key: KeyEvent) -> Result<()> {
732 self.offer(AppEvent::Key(key))
733 }
734
735 /// Offer typed input (a key or a paste) to the app, then reconcile the held keys
736 /// with the screen it left.
737 fn offer(&mut self, input: AppEvent) -> Result<()> {
738 // The input may change the screen: a click waits for the frame that shows it.
739 self.app.pointer.changed();
740 let gen_before = self.app.screen_generation();
741 match self.app.handle(input) {
742 Ok(Some(follow_up)) => self.queue_continuation(follow_up),
743 Ok(None) => {}
744 // Only if the app went busy between check and call, which nothing on this thread
745 // does; the key keeps its place either way.
746 Err(deferred) => self.held.push_front(Held::Key(deferred)),
747 }
748 if self.app.screen_generation() != gen_before {
749 // This input left the view (home, a declined download): held keys were for that
750 // screen.
751 self.held.clear();
752 self.app.set_input_dropped(false);
753 } else {
754 // A modal this key opened (an overwrite prompt, an error) is one the held keys
755 // answer, unlike one a background result brings: keep them and re-baseline so
756 // `discard_stale` does not drop them.
757 self.held_for = Self::screen_of(&self.app);
758 }
759 Ok(())
760 }
761
762 /// Hold a key. At the plain table, while every held key is navigation, repeats of
763 /// one key coalesce into a press (each would chain a collect). Once `/` or any
764 /// other key is held the run is text, so `/bookkeeper` keeps both `k`s. Column
765 /// cursor keys and `n`/`N` are each kept: they read nothing or move one match each.
766 /// At the cap the newest is dropped and the user told; never the oldest, which may
767 /// be the `/`.
768 fn hold(&mut self, key: KeyEvent) {
769 if is_navigation(&key)
770 && !replays_each_press(&key)
771 && self.held.back().and_then(Held::key) == Some(&key)
772 && self.app.in_normal_table_view()
773 && self.held.iter().all(Held::is_navigation)
774 {
775 return;
776 }
777 self.hold_input(Held::Key(key));
778 }
779
780 /// Hold typed input behind what is held, up to [`MAX_HELD_KEYS`].
781 fn hold_input(&mut self, input: Held) {
782 if self.held.is_empty() {
783 self.held_for = Self::screen_of(&self.app);
784 }
785 if self.held.len() >= MAX_HELD_KEYS {
786 self.app.set_input_dropped(true);
787 return;
788 }
789 self.held.push_back(input);
790 }
791
792 /// Drop the `n`/`N` held at the front among cursor moves; past any other key they
793 /// are text or meant for what it opens.
794 fn drop_held_finds(&mut self) {
795 let run = self.held.iter().take_while(|k| k.is_navigation()).count();
796 let rest = self.held.split_off(run);
797 self.held.retain(|k| {
798 !k.key()
799 .is_some_and(|k| matches!(k.code, KeyCode::Char('n' | 'N')))
800 });
801 self.held.extend(rest);
802 if self.held.is_empty() {
803 self.app.set_input_dropped(false);
804 }
805 }
806
807 /// Drop the held keys if their screen is gone: the view abandoned, or a modal they
808 /// were not answers to. A modal a held key opened is re-baselined in `dispatch`, so
809 /// this fires for changes the keys did not cause.
810 fn discard_stale(&mut self) {
811 if self.held.is_empty() {
812 return;
813 }
814 let now = Self::screen_of(&self.app);
815 let abandoned = now.generation != self.held_for.generation;
816 let new_modal = now.modal && !self.held_for.modal;
817 let failed_inline = now.inline_failures != self.held_for.inline_failures;
818 if abandoned || new_modal || failed_inline {
819 self.held.clear();
820 self.app.set_input_dropped(false);
821 }
822 }
823
824 fn screen_of(app: &App) -> Screen {
825 Screen {
826 generation: app.screen_generation(),
827 modal: app.modal_showing(),
828 inline_failures: app.inline_failures(),
829 }
830 }
831}
832
833/// How the run loop ended.
834#[derive(Debug, PartialEq, Eq)]
835pub enum Ended {
836 Quit,
837 Crash(String),
838 /// A path named at startup is not there.
839 NotFound(std::path::PathBuf),
840}
841
842/// What one turn of the run loop handled.
843#[derive(Default)]
844struct Pass {
845 updated: bool,
846 progress_only: bool,
847}
848
849impl Pass {
850 /// Fold one channel drain in; or say how the loop ends.
851 fn add(&mut self, drained: Drained) -> Option<Ended> {
852 match drained {
853 Drained::Continue {
854 updated,
855 progress_only,
856 } => {
857 if updated {
858 self.progress_only = progress_only && (self.progress_only || !self.updated);
859 self.updated = true;
860 }
861 None
862 }
863 Drained::Exit => Some(Ended::Quit),
864 Drained::Crash(msg) => Some(Ended::Crash(msg)),
865 Drained::NotFound(path) => Some(Ended::NotFound(path)),
866 }
867 }
868}
869
870/// How often a spinner turns while the user waits: about 30 frames a second.
871pub const SPINNER_FRAME: Duration = Duration::from_millis(33);
872
873/// How often it turns for unwaited work (a count, a read-ahead): ten frames a
874/// second, redrawing a third as often.
875pub const SPINNER_IDLE_FRAME: Duration = Duration::from_millis(100);
876
877/// The least time between frames drawn for progress reports alone; keys and
878/// results draw at once.
879pub const PROGRESS_FRAME: Duration = Duration::from_millis(33);
880
881/// When the run loop draws and how long it may sleep: never on a fixed tick, only
882/// until the spinner's next frame, an owed progress frame, or the app's own
883/// deadline (a flash expiring).
884#[derive(Debug, Default)]
885pub struct Pacer {
886 last_draw: Option<Instant>,
887 /// A frame for progress reports, held back until [`PROGRESS_FRAME`] has passed.
888 owed: bool,
889 /// The spinner's next frame, while one is on screen.
890 spin_due: Option<Instant>,
891 /// How far apart its frames are: see [`SPINNER_IDLE_FRAME`].
892 spin_every: Duration,
893}
894
895impl Pacer {
896 /// Say whether a spinner is on screen and whether the user waits on it; a new one
897 /// turns a frame later.
898 pub fn spinning(&mut self, on: bool, waited_on: bool, now: Instant) {
899 self.spin_every = if waited_on {
900 SPINNER_FRAME
901 } else {
902 SPINNER_IDLE_FRAME
903 };
904 if on {
905 let next = now + self.spin_every;
906 self.spin_due = Some(self.spin_due.map_or(next, |due| due.min(next)));
907 } else {
908 self.spin_due = None;
909 }
910 }
911
912 /// Whether the spinner's next frame is due; moves the deadline on when it is.
913 pub fn turn_spinner(&mut self, now: Instant) -> bool {
914 match self.spin_due {
915 Some(due) if now >= due => {
916 self.spin_due = Some(now + self.spin_every);
917 true
918 }
919 _ => false,
920 }
921 }
922
923 /// Whether to draw for a pass now. Progress-only passes close behind the last frame
924 /// are owed one instead.
925 pub fn handled(&mut self, updated: bool, progress_only: bool, now: Instant) -> bool {
926 let progress_due = self
927 .last_draw
928 .is_none_or(|last| now >= last + PROGRESS_FRAME);
929 if updated && !progress_only {
930 return true;
931 }
932 if updated {
933 self.owed = true;
934 }
935 self.owed && progress_due
936 }
937
938 /// A frame was drawn.
939 pub fn drew(&mut self, now: Instant) {
940 self.last_draw = Some(now);
941 self.owed = false;
942 }
943
944 /// How long the loop may sleep: until the earliest deadline, or indefinitely.
945 pub fn timeout(&self, app_deadline: Option<Instant>, now: Instant) -> Duration {
946 let owed = self
947 .owed
948 .then(|| self.last_draw.map(|last| last + PROGRESS_FRAME))
949 .flatten();
950 [self.spin_due, owed, app_deadline]
951 .into_iter()
952 .flatten()
953 .min()
954 .map_or(Duration::MAX, |at| at.saturating_duration_since(now))
955 }
956}
957
958/// Navigation keys a held run keeps every press of: the column cursor's, which read
959/// nothing, and a find's next and previous, each its own match.
960fn replays_each_press(key: &KeyEvent) -> bool {
961 matches!(
962 key.code,
963 KeyCode::Left
964 | KeyCode::Right
965 | KeyCode::Char('h')
966 | KeyCode::Char('l')
967 | KeyCode::Char('{')
968 | KeyCode::Char('}')
969 | KeyCode::Char('n')
970 | KeyCode::Char('N')
971 )
972}
973
974fn is_drag(mouse: &MouseEvent) -> bool {
975 matches!(mouse.kind, crossterm::event::MouseEventKind::Drag(_))
976}
977
978/// Keys that move the view and are often held down; column cursor keys included,
979/// so Enter behind them still inspects.
980fn is_navigation(key: &KeyEvent) -> bool {
981 let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
982 match key.code {
983 KeyCode::Up
984 | KeyCode::Down
985 | KeyCode::PageUp
986 | KeyCode::PageDown
987 | KeyCode::Home
988 | KeyCode::End
989 | KeyCode::Left
990 | KeyCode::Right
991 | KeyCode::Char('j')
992 | KeyCode::Char('k')
993 | KeyCode::Char('h')
994 | KeyCode::Char('l')
995 | KeyCode::Char('{')
996 | KeyCode::Char('}')
997 | KeyCode::Char('G')
998 // A find's next and previous move the cursor too.
999 | KeyCode::Char('n')
1000 | KeyCode::Char('N') => true,
1001 KeyCode::Char('f') | KeyCode::Char('b') | KeyCode::Char('d') | KeyCode::Char('u') => ctrl,
1002 _ => false,
1003 }
1004}
1005
1006#[cfg(test)]
1007mod tests;