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