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.request_what_the_frame_needs();
571
572 if redraw {
573 draw(app)?;
574 // Read the rows the frame needed, and start a count waiting on their paint.
575 app.frame_painted();
576 first_rows.painted(app);
577 pacer.drew(now);
578 // Ask now for what the frame drew without knowing: there may be no next pass.
579 app.request_what_the_frame_needs();
580 }
581 }
582 }
583
584 /// Hold a continuation, and the generation, until it is dispatched.
585 fn queue_continuation(&mut self, follow_up: AppEvent) {
586 let hold = self.app.hold_the_generation();
587 self.next_up.push_back((follow_up, hold));
588 }
589
590 /// Offer one key to the app as the channel drain does, then reconcile the held keys
591 /// with the screen it left.
592 fn dispatch(&mut self, key: KeyEvent) -> Result<()> {
593 // The key may change the screen: a click waits for the frame that shows it.
594 self.app.pointer.changed();
595 let gen_before = self.app.screen_generation();
596 match self.app.handle(AppEvent::Key(key)) {
597 Ok(Some(follow_up)) => self.queue_continuation(follow_up),
598 Ok(None) => {}
599 // Only if the app went busy between check and call, which nothing on this thread
600 // does; the key keeps its place either way.
601 Err(deferred) => self.held.push_front(deferred),
602 }
603 if self.app.screen_generation() != gen_before {
604 // This key left the view (home, a declined download): held keys were for that
605 // screen.
606 self.held.clear();
607 self.app.set_input_dropped(false);
608 } else {
609 // A modal this key opened (an overwrite prompt, an error) is one the held keys
610 // answer, unlike one a background result brings: keep them and re-baseline so
611 // `discard_stale` does not drop them.
612 self.held_for = Self::screen_of(&self.app);
613 }
614 Ok(())
615 }
616
617 /// Hold a key. At the plain table, while every held key is navigation, repeats of
618 /// one key coalesce into a press (each would chain a collect). Once `/` or any
619 /// other key is held the run is text, so `/bookkeeper` keeps both `k`s. Column
620 /// cursor keys and `n`/`N` are each kept: they read nothing or move one match each.
621 /// At the cap the newest is dropped and the user told; never the oldest, which may
622 /// be the `/`.
623 fn hold(&mut self, key: KeyEvent) {
624 if self.held.is_empty() {
625 self.held_for = Self::screen_of(&self.app);
626 }
627 if is_navigation(&key)
628 && !replays_each_press(&key)
629 && self.held.back() == Some(&key)
630 && self.app.in_normal_table_view()
631 && self.held.iter().all(is_navigation)
632 {
633 return;
634 }
635 if self.held.len() >= MAX_HELD_KEYS {
636 self.app.set_input_dropped(true);
637 return;
638 }
639 self.held.push_back(key);
640 }
641
642 /// Drop the `n`/`N` held at the front among cursor moves; past any other key they
643 /// are text or meant for what it opens.
644 fn drop_held_finds(&mut self) {
645 let run = self.held.iter().take_while(|k| is_navigation(k)).count();
646 let rest = self.held.split_off(run);
647 self.held
648 .retain(|k| !matches!(k.code, KeyCode::Char('n' | 'N')));
649 self.held.extend(rest);
650 if self.held.is_empty() {
651 self.app.set_input_dropped(false);
652 }
653 }
654
655 /// Drop the held keys if their screen is gone: the view abandoned, or a modal they
656 /// were not answers to. A modal a held key opened is re-baselined in `dispatch`, so
657 /// this fires for changes the keys did not cause.
658 fn discard_stale(&mut self) {
659 if self.held.is_empty() {
660 return;
661 }
662 let now = Self::screen_of(&self.app);
663 let abandoned = now.generation != self.held_for.generation;
664 let new_modal = now.modal && !self.held_for.modal;
665 let failed_inline = now.inline_failures != self.held_for.inline_failures;
666 if abandoned || new_modal || failed_inline {
667 self.held.clear();
668 self.app.set_input_dropped(false);
669 }
670 }
671
672 fn screen_of(app: &App) -> Screen {
673 Screen {
674 generation: app.screen_generation(),
675 modal: app.modal_showing(),
676 inline_failures: app.inline_failures(),
677 }
678 }
679}
680
681/// How the run loop ended.
682#[derive(Debug, PartialEq, Eq)]
683pub enum Ended {
684 Quit,
685 Crash(String),
686 /// A path named at startup is not there.
687 NotFound(std::path::PathBuf),
688}
689
690/// What one turn of the run loop handled.
691#[derive(Default)]
692struct Pass {
693 updated: bool,
694 progress_only: bool,
695}
696
697impl Pass {
698 /// Fold one channel drain in; or say how the loop ends.
699 fn add(&mut self, drained: Drained) -> Option<Ended> {
700 match drained {
701 Drained::Continue {
702 updated,
703 progress_only,
704 } => {
705 if updated {
706 self.progress_only = progress_only && (self.progress_only || !self.updated);
707 self.updated = true;
708 }
709 None
710 }
711 Drained::Exit => Some(Ended::Quit),
712 Drained::Crash(msg) => Some(Ended::Crash(msg)),
713 Drained::NotFound(path) => Some(Ended::NotFound(path)),
714 }
715 }
716}
717
718/// How often a spinner turns while the user waits: about 30 frames a second.
719pub const SPINNER_FRAME: Duration = Duration::from_millis(33);
720
721/// How often it turns for unwaited work (a count, a read-ahead): ten frames a
722/// second, redrawing a third as often.
723pub const SPINNER_IDLE_FRAME: Duration = Duration::from_millis(100);
724
725/// The least time between frames drawn for progress reports alone; keys and
726/// results draw at once.
727pub const PROGRESS_FRAME: Duration = Duration::from_millis(33);
728
729/// When the run loop draws and how long it may sleep: never on a fixed tick, only
730/// until the spinner's next frame, an owed progress frame, or the app's own
731/// deadline (a flash expiring).
732#[derive(Debug, Default)]
733pub struct Pacer {
734 last_draw: Option<Instant>,
735 /// A frame for progress reports, held back until [`PROGRESS_FRAME`] has passed.
736 owed: bool,
737 /// The spinner's next frame, while one is on screen.
738 spin_due: Option<Instant>,
739 /// How far apart its frames are: see [`SPINNER_IDLE_FRAME`].
740 spin_every: Duration,
741}
742
743impl Pacer {
744 /// Say whether a spinner is on screen and whether the user waits on it; a new one
745 /// turns a frame later.
746 pub fn spinning(&mut self, on: bool, waited_on: bool, now: Instant) {
747 self.spin_every = if waited_on {
748 SPINNER_FRAME
749 } else {
750 SPINNER_IDLE_FRAME
751 };
752 if on {
753 let next = now + self.spin_every;
754 self.spin_due = Some(self.spin_due.map_or(next, |due| due.min(next)));
755 } else {
756 self.spin_due = None;
757 }
758 }
759
760 /// Whether the spinner's next frame is due; moves the deadline on when it is.
761 pub fn turn_spinner(&mut self, now: Instant) -> bool {
762 match self.spin_due {
763 Some(due) if now >= due => {
764 self.spin_due = Some(now + self.spin_every);
765 true
766 }
767 _ => false,
768 }
769 }
770
771 /// Whether to draw for a pass now. Progress-only passes close behind the last frame
772 /// are owed one instead.
773 pub fn handled(&mut self, updated: bool, progress_only: bool, now: Instant) -> bool {
774 let progress_due = self
775 .last_draw
776 .is_none_or(|last| now >= last + PROGRESS_FRAME);
777 if updated && !progress_only {
778 return true;
779 }
780 if updated {
781 self.owed = true;
782 }
783 self.owed && progress_due
784 }
785
786 /// A frame was drawn.
787 pub fn drew(&mut self, now: Instant) {
788 self.last_draw = Some(now);
789 self.owed = false;
790 }
791
792 /// How long the loop may sleep: until the earliest deadline, or indefinitely.
793 pub fn timeout(&self, app_deadline: Option<Instant>, now: Instant) -> Duration {
794 let owed = self
795 .owed
796 .then(|| self.last_draw.map(|last| last + PROGRESS_FRAME))
797 .flatten();
798 [self.spin_due, owed, app_deadline]
799 .into_iter()
800 .flatten()
801 .min()
802 .map_or(Duration::MAX, |at| at.saturating_duration_since(now))
803 }
804}
805
806/// Navigation keys a held run keeps every press of: the column cursor's, which read
807/// nothing, and a find's next and previous, each its own match.
808fn replays_each_press(key: &KeyEvent) -> bool {
809 matches!(
810 key.code,
811 KeyCode::Left
812 | KeyCode::Right
813 | KeyCode::Char('h')
814 | KeyCode::Char('l')
815 | KeyCode::Char('{')
816 | KeyCode::Char('}')
817 | KeyCode::Char('n')
818 | KeyCode::Char('N')
819 )
820}
821
822fn is_drag(mouse: &MouseEvent) -> bool {
823 matches!(mouse.kind, crossterm::event::MouseEventKind::Drag(_))
824}
825
826/// Keys that move the view and are often held down; column cursor keys included,
827/// so Enter behind them still inspects.
828fn is_navigation(key: &KeyEvent) -> bool {
829 let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
830 match key.code {
831 KeyCode::Up
832 | KeyCode::Down
833 | KeyCode::PageUp
834 | KeyCode::PageDown
835 | KeyCode::Home
836 | KeyCode::End
837 | KeyCode::Left
838 | KeyCode::Right
839 | KeyCode::Char('j')
840 | KeyCode::Char('k')
841 | KeyCode::Char('h')
842 | KeyCode::Char('l')
843 | KeyCode::Char('{')
844 | KeyCode::Char('}')
845 | KeyCode::Char('G')
846 // A find's next and previous move the cursor too.
847 | KeyCode::Char('n')
848 | KeyCode::Char('N') => true,
849 KeyCode::Char('f') | KeyCode::Char('b') | KeyCode::Char('d') | KeyCode::Char('u') => ctrl,
850 _ => false,
851 }
852}
853
854#[cfg(test)]
855mod tests;