Skip to main content

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;