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