Skip to main content

datui_lib/
event_pump.rs

1//! The main loop, minus the terminal.
2//!
3//! `run()` sets the terminal up and hands [`EventPump::run`] a way to draw a frame;
4//! keys reach the pump through the same channel as worker results
5//! ([`crate::terminal_input`]), so the loop sleeps until either arrives or a deadline
6//! passes ([`Pacer`]). Everything between, including what happens to a key typed
7//! while the app is busy, lives here so it can be driven without a terminal. Keys
8//! typed while busy are held, in order, and replayed one per
9//! loop iteration once the app is idle, each through the same path a fresh key takes.
10//! Any follow-up event a replayed key produces is drained before the next held key is
11//! offered, so a queued Enter finishes its search before the key typed after it acts.
12
13use std::collections::VecDeque;
14use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender, TryRecvError};
15use std::time::{Duration, Instant};
16
17use color_eyre::Result;
18use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers, MouseEvent};
19
20use crate::jobs::Hold;
21use crate::pointer::Pointer;
22use crate::{App, AppEvent};
23
24/// Keys held while busy. Beyond this the newest is dropped, and the user told: the
25/// oldest may be the `/` that puts the rest into the query bar, and without it the
26/// tail of a typed query would replay as hotkeys.
27pub const MAX_HELD_KEYS: usize = 32;
28
29/// What a fresh key from the terminal should do while the app cannot take it directly.
30enum Act {
31    /// Handle it now.
32    Now,
33    /// Hold it for replay once the app is idle.
34    Hold,
35    /// Hold this key in its place: what it means where it was typed.
36    HoldAs(KeyEvent),
37    /// Discard it: a bare Enter/Esc at a busy table confirms nothing.
38    Drop,
39    /// Handle it now, and drop the `n` and `N` held behind it: Esc stopping a find
40    /// stops the finds typed after it too, or each would need an Esc of its own.
41    StopFind,
42}
43
44/// Something read from the terminal for the app: a key or the mouse. Kept in the
45/// order it arrived.
46#[derive(Debug, Clone, Copy)]
47enum Input {
48    Key(KeyEvent),
49    Mouse(MouseEvent),
50}
51
52/// What a pass over the channel found.
53#[derive(Debug)]
54pub enum Drained {
55    /// Keep going; `updated` says whether anything was handled and a frame is due,
56    /// and `progress_only` that everything handled was a report from work still
57    /// running ([`AppEvent::is_progress`]), whose frame may wait for the next.
58    Continue {
59        updated: bool,
60        progress_only: bool,
61    },
62    Exit,
63    Crash(String),
64    /// A path named at startup is not there.
65    NotFound(std::path::PathBuf),
66}
67
68/// The screen the held keys were typed at. A change means they were meant for
69/// something that is no longer there: the modal that ended the work and has not been
70/// seen yet, the dataset left for the home screen, or a statement's failure put
71/// under it in the query prompt.
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73struct Screen {
74    generation: u64,
75    modal: bool,
76    inline_failures: u64,
77}
78
79/// Owns the app, its channel and the keys held while it was busy.
80pub struct EventPump {
81    pub app: App,
82    tx: Sender<AppEvent>,
83    rx: Receiver<AppEvent>,
84    held: VecDeque<KeyEvent>,
85    held_for: Screen,
86    /// Continuations a handler returned, each holding the generation.
87    ///
88    /// Ahead of the channel rather than appended to it. A follow-up is the rest of the
89    /// event just handled, so it belongs before results that arrived while that handler
90    /// ran.
91    ///
92    /// The hold covers the gap the break leaves. A frame is drawn before the
93    /// continuation runs, and a key replayed then must not find the generation free.
94    next_up: VecDeque<(AppEvent, Hold)>,
95    /// Events that arrived before there was an app to take them, while `run` read the
96    /// settings, then the startup open: handled first, in that order. The keys typed
97    /// meanwhile are in [`Self::typed`].
98    backlog: VecDeque<AppEvent>,
99    /// Keys read from the terminal and not yet offered to the app, oldest first. Each
100    /// waits for what has arrived on the channel behind it, as it did when the loop
101    /// read the terminal itself: taken in channel order, a held-down `j` put the
102    /// load-ahead's answer behind every repeat, scrolled off the buffer and folded the
103    /// rest into one press.
104    typed: VecDeque<Input>,
105    /// Events handled since a key was last offered. Bounded by [`RESULTS_PER_KEY`], so
106    /// a worker reporting faster than it is handled cannot starve the keyboard.
107    since_key: usize,
108    /// How many of the keys at the front of [`Self::typed`] came from the backlog.
109    /// Typed before anything on the channel was sent, they wait on none of it: a
110    /// Ctrl+O typed while the settings were read lost to the startup open whenever
111    /// that open answered before the channel was ever found empty.
112    early: usize,
113}
114
115/// The most channel events handled while a typed key waits; then the key is offered.
116const RESULTS_PER_KEY: usize = 64;
117
118impl EventPump {
119    pub fn new(app: App, tx: Sender<AppEvent>, rx: Receiver<AppEvent>) -> Self {
120        let held_for = Self::screen_of(&app);
121        Self {
122            app,
123            tx,
124            rx,
125            held: VecDeque::new(),
126            held_for,
127            next_up: VecDeque::new(),
128            backlog: VecDeque::new(),
129            typed: VecDeque::new(),
130            since_key: 0,
131            early: 0,
132        }
133    }
134
135    /// Handle `events` before anything on the channel: they arrived first, or are the
136    /// startup open those keys were typed at. Keys among them are offered, in order,
137    /// after the other events and ahead of the channel.
138    pub fn handle_first(&mut self, events: impl IntoIterator<Item = AppEvent>) {
139        for event in events {
140            match event {
141                AppEvent::Terminal(Event::Key(key)) => {
142                    self.typed.push_back(Input::Key(key));
143                    self.early += 1;
144                }
145                AppEvent::Terminal(Event::Mouse(mouse)) => {
146                    self.typed.push_back(Input::Mouse(mouse));
147                    self.early += 1;
148                }
149                event => self.backlog.push_back(event),
150            }
151        }
152    }
153
154    pub fn send(&self, event: AppEvent) -> Result<()> {
155        self.tx.send(event)?;
156        Ok(())
157    }
158
159    /// The keys waiting for the app to go idle, oldest first.
160    pub fn held_keys(&self) -> impl Iterator<Item = &KeyEvent> {
161        self.held.iter()
162    }
163
164    /// True while held keys are waiting on an idle app: the loop should not wait on
165    /// the terminal, since each iteration replays one.
166    pub fn replaying(&self) -> bool {
167        !self.held.is_empty() && !self.app.is_busy()
168    }
169
170    /// A key read from the terminal. Handled now if it is a hard escape, or the app is
171    /// idle with nothing queued ahead of it, or it is one of the view keys that act at a
172    /// busy table; Enter with nothing to drill into waits as Space; a bare Enter/Esc at a
173    /// busy table is dropped; otherwise it waits behind whatever was typed before it.
174    /// Returns whether the app changed.
175    pub fn terminal_key(&mut self, key: KeyEvent) -> Result<bool> {
176        self.discard_stale();
177        match self.classify(&key) {
178            Act::Now => {
179                self.dispatch(key)?;
180                Ok(true)
181            }
182            Act::StopFind => {
183                self.drop_held_finds();
184                self.dispatch(key)?;
185                Ok(true)
186            }
187            Act::Drop => Ok(false),
188            Act::Hold => {
189                self.hold(key);
190                Ok(false)
191            }
192            Act::HoldAs(meant) => {
193                self.hold(meant);
194                Ok(false)
195            }
196        }
197    }
198
199    /// A mouse event read from the terminal, which means what it lands on in the last
200    /// frame ([`App::pointer`]). It is never held: aimed at what is on screen now, it
201    /// would land on something else later. The wheel's arrows and a chip's key act
202    /// where a typed key would act at once and are dropped where it would wait, so the
203    /// wheel across moves the column cursor at a busy table, as ←→ do, and the wheel
204    /// down waits for nothing. A click moves the cursor where ↓ would act at once: at
205    /// an idle table with nothing held, and on the home screen, which keeps its keys.
206    /// Returns whether the app changed.
207    ///
208    /// The rest stand for keys and act where those keys would: focusing a form's field
209    /// where ↓ would, a dragged width where `>` would, a dropped header where `L`
210    /// would, opening the context menu where ↓ would at the table, and a menu line or
211    /// a tool pressing its key as typed.
212    pub fn terminal_mouse(&mut self, mouse: MouseEvent) -> Result<bool> {
213        self.discard_stale();
214        let acts = |p: &Self, code: KeyCode| {
215            matches!(
216                p.classify(&KeyEvent::new(code, KeyModifiers::NONE)),
217                Act::Now
218            )
219        };
220        match self.app.pointer(&mouse, std::time::Instant::now()) {
221            Pointer::Nothing => Ok(false),
222            Pointer::Keys(keys) => self.press_now(keys),
223            Pointer::Point(target, then) => {
224                if !acts(self, KeyCode::Down) {
225                    // Not a click the next one can make a double click of.
226                    self.app.forget_click();
227                    return Ok(false);
228                }
229                self.app.point(&target);
230                self.press_now(then)?;
231                Ok(true)
232            }
233            Pointer::Form { field, act, keys } => {
234                if !acts(self, KeyCode::Down) {
235                    return Ok(false);
236                }
237                let mut then = keys;
238                if let Some(id) = field {
239                    let Some(clicked) = self.app.focus_field(&id) else {
240                        return Ok(false);
241                    };
242                    if let Some(back) = act.filter(|_| clicked.acts) {
243                        then.extend(crate::pointer::act_key(clicked.kind, back));
244                    }
245                }
246                self.press_now(then)?;
247                Ok(true)
248            }
249            Pointer::Tool(tool) => {
250                if !acts(self, KeyCode::Enter) {
251                    return Ok(false);
252                }
253                self.app.point_at_tool(tool);
254                self.press_now([KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)])?;
255                Ok(true)
256            }
257            Pointer::Resize { column, x } => {
258                if !acts(self, KeyCode::Char('>')) {
259                    return Ok(false);
260                }
261                self.app.start_resize(column, x);
262                Ok(false)
263            }
264            Pointer::Width { column, width } => {
265                if !self.app.in_normal_table_view() || !acts(self, KeyCode::Char('>')) {
266                    return Ok(false);
267                }
268                self.app.set_dragged_width(column, width);
269                Ok(true)
270            }
271            Pointer::Drop { column, onto } => {
272                if !self.app.in_normal_table_view() || !acts(self, KeyCode::Char('L')) {
273                    return Ok(false);
274                }
275                if let Some(event) = self.app.drop_column(&column, &onto) {
276                    self.queue_continuation(event);
277                }
278                Ok(true)
279            }
280            Pointer::Menu(hit, at) => {
281                if !acts(self, KeyCode::Down) {
282                    return Ok(false);
283                }
284                // Only on the cell the cursor landed on: rows that moved since they
285                // were drawn are not the ones clicked.
286                if self.app.point_for_menu(&hit) {
287                    self.app.open_context_menu(at);
288                }
289                Ok(true)
290            }
291            Pointer::MenuChoose(i) => {
292                if let Some(AppEvent::Press(key)) = self.app.choose_from_menu(i) {
293                    self.press_now([key])?;
294                }
295                Ok(true)
296            }
297            Pointer::Redraw => Ok(true),
298            Pointer::CloseMenu => {
299                self.app.close_context_menu();
300                Ok(true)
301            }
302        }
303    }
304
305    /// Press `keys` in order while each would act at once; the rest are dropped.
306    fn press_now(&mut self, keys: impl IntoIterator<Item = KeyEvent>) -> Result<bool> {
307        let mut acted = false;
308        for key in keys {
309            match self.classify(&key) {
310                Act::Now => {}
311                Act::StopFind => self.drop_held_finds(),
312                _ => break,
313            }
314            self.dispatch(key)?;
315            acted = true;
316        }
317        Ok(acted)
318    }
319
320    fn classify(&self, key: &KeyEvent) -> Act {
321        // Escapes jump ahead of anything queued, busy or idle: Ctrl-Q/Ctrl-C quit,
322        // Ctrl-O goes home, a confirmation modal is answered. Checked first so a
323        // Ctrl-C typed during replay is not appended behind the held keys, where a
324        // modal opening could discard it.
325        if self.app.hard_escape_while_busy(key) {
326            if key.code == KeyCode::Esc && self.app.finding() {
327                return Act::StopFind;
328            }
329            return Act::Now;
330        }
331        // The open menu reads nothing: its own keys act at once. A line chosen
332        // presses its key as typed, and that key waits as typed.
333        if self.app.menu_takes(key) {
334            return Act::Now;
335        }
336        let queued = !self.held.is_empty();
337        // Idle with nothing ahead: ordinary front-of-line handling.
338        if !self.app.is_busy() && !queued {
339            return Act::Now;
340        }
341        // The loading screen has nothing to type ahead into, so nothing is
342        // held there: the allowed keys act, everything else is dropped. Held
343        // once, a stray key would queue `q` behind it for the whole load.
344        if self.app.is_busy() && self.app.awaiting_dataset() {
345            if self.app.key_acts_while_busy(key) {
346                return Act::Now;
347            }
348            return Act::Drop;
349        }
350        // Enter with nothing to drill into is Space, and waits as Space does. Held as
351        // Space, so a result that can be drilled by the time it replays is not drilled
352        // into. Only while what is held moves the cursor: after a `/` it is text's Enter.
353        if self.app.is_busy()
354            && key.code == KeyCode::Enter
355            && self.app.in_normal_table_view()
356            && self.app.enter_inspects()
357            && self.held.iter().all(is_navigation)
358        {
359            return Act::HoldAs(KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE));
360        }
361        // A sample being drawn holds only what needs every row: moving, finding and
362        // inspecting the rows on hand act at once, the find line and inspector too.
363        if self.app.is_busy() && !queued && self.app.key_acts_while_sampling(key) {
364            return Act::Now;
365        }
366        // Busy, nothing queued yet, at the plain table view: the harmless view keys act
367        // (quit, the column cursor, help); a bare Enter that would drill, or Esc, confirms
368        // nothing and is dropped; everything else is type-ahead and waits. Once anything
369        // is queued, or the view is a text field or modal, every key waits to keep the
370        // typed order.
371        if self.app.is_busy() && !queued && self.app.in_normal_table_view() {
372            if self.app.key_acts_while_busy(key) {
373                return Act::Now;
374            }
375            if matches!(key.code, KeyCode::Enter | KeyCode::Esc) {
376                return Act::Drop;
377            }
378        }
379        Act::Hold
380    }
381
382    /// Replay the oldest held key if the app is idle. Returns whether one was replayed.
383    pub fn replay_one(&mut self) -> Result<bool> {
384        self.discard_stale();
385        if self.app.is_busy() {
386            return Ok(false);
387        }
388        let Some(key) = self.held.pop_front() else {
389            return Ok(false);
390        };
391        if self.held.is_empty() {
392            self.app.set_input_dropped(false);
393        }
394        self.dispatch(key)?;
395        Ok(true)
396    }
397
398    /// The next event to handle: a continuation first, then the backlog, then the
399    /// channel. `Empty` once a typed key has waited long enough, or came from the
400    /// backlog, so it is offered.
401    fn take_next(&mut self) -> Result<(AppEvent, Option<Hold>), TryRecvError> {
402        if let Some((event, lease)) = self.next_up.pop_front() {
403            return Ok((event, Some(lease)));
404        }
405        if let Some(event) = self.backlog.pop_front() {
406            return Ok((event, None));
407        }
408        if !self.typed.is_empty() && (self.early > 0 || self.since_key >= RESULTS_PER_KEY) {
409            return Err(TryRecvError::Empty);
410        }
411        self.rx.try_recv().map(|event| (event, None))
412    }
413
414    /// Handle everything waiting on the channel.
415    pub fn drain(&mut self) -> Result<Drained> {
416        let first = self.take_next();
417        self.drain_from(first)
418    }
419
420    /// Wait up to `timeout` for the next event, then handle it and everything behind
421    /// it. The run loop's only wait: keys, worker results and continuations all arrive
422    /// here, so whichever comes first ends it. `Duration::MAX` waits as long as it
423    /// takes.
424    pub fn wait_and_drain(&mut self, timeout: Duration) -> Result<Drained> {
425        // A continuation is already here; waiting on the channel would sit on it for the
426        // whole timeout while the errand it belongs to is halfway through.
427        if !self.next_up.is_empty() || !self.backlog.is_empty() || !self.typed.is_empty() {
428            let first = self.take_next();
429            return self.drain_from(first);
430        }
431        let first = self
432            .rx
433            .recv_timeout(timeout)
434            .map(|event| (event, None))
435            .map_err(|e| match e {
436                RecvTimeoutError::Timeout => TryRecvError::Empty,
437                RecvTimeoutError::Disconnected => TryRecvError::Disconnected,
438            });
439        self.drain_from(first)
440    }
441
442    fn drain_from(
443        &mut self,
444        mut next: Result<(AppEvent, Option<Hold>), TryRecvError>,
445    ) -> Result<Drained> {
446        let mut updated = false;
447        let mut progress_only = true;
448        loop {
449            match next {
450                Ok((AppEvent::Exit, _)) => return Ok(Drained::Exit),
451                Ok((AppEvent::Crash(msg), _)) => return Ok(Drained::Crash(msg)),
452                // A path named at startup is not there: the session ends as it always
453                // has, with the file named. The look's answer says so only while it is
454                // current, so a user who has moved on meanwhile stays.
455                Ok((AppEvent::NamedPathMissing(path), _)) => {
456                    return Ok(Drained::NotFound(path));
457                }
458                // Offered once what arrived behind it is handled ([`Self::typed`]).
459                // A key the app pressed for the user (Enter on a help line): offered
460                // next, as typed, so `classify` holds, converts or drops it as it
461                // would the key itself.
462                Ok((AppEvent::Press(key), hold)) => {
463                    if hold.is_some() {
464                        drop(hold);
465                        self.app.let_waiting_errands_in();
466                    }
467                    if self.typed.is_empty() {
468                        self.since_key = 0;
469                    }
470                    self.typed.push_front(Input::Key(key));
471                    if self.early > 0 {
472                        self.early += 1;
473                    }
474                }
475                Ok((AppEvent::Terminal(Event::Key(key)), _)) => {
476                    if self.typed.is_empty() {
477                        self.since_key = 0;
478                    }
479                    self.typed.push_back(Input::Key(key));
480                }
481                Ok((AppEvent::Terminal(Event::Mouse(mouse)), _)) => {
482                    if self.typed.is_empty() {
483                        self.since_key = 0;
484                    }
485                    self.typed.push_back(Input::Mouse(mouse));
486                }
487                Ok((AppEvent::Terminal(Event::Resize(cols, rows)), _)) => {
488                    next = Ok((AppEvent::Resize(cols, rows), None));
489                    continue;
490                }
491                Ok((AppEvent::Terminal(_), _)) => {}
492                Ok((event, mut continuation)) => {
493                    updated = true;
494                    progress_only &= event.is_progress();
495                    self.since_key += 1;
496                    self.app.pointer.changed();
497                    let follow_up = match self.app.handle(&event) {
498                        Ok(follow_up) => follow_up,
499                        Err(deferred) => {
500                            self.hold(deferred);
501                            None
502                        }
503                    };
504                    self.discard_stale();
505                    if let Some(follow_up) = follow_up {
506                        // A handler that returns a follow-up event is deferring work so
507                        // the UI can show the current phase first — the `Do*` events all
508                        // rely on this. Draining the follow-up in the same pass defeats
509                        // that: the phase label never renders and the throbber never
510                        // moves. Break so a frame is drawn and keys are polled first.
511                        //
512                        // Its hold is taken before this event's is let go, so the
513                        // generation is never free between the two phases.
514                        self.queue_continuation(follow_up);
515                        drop(continuation);
516                        break;
517                    }
518                    // After the handler, never before: whatever phase this event started
519                    // holds the generation by now. Then what waited on the hold gets its
520                    // turn, as it would after any other event.
521                    if let Some(hold) = continuation.take() {
522                        drop(hold);
523                        self.app.let_waiting_errands_in();
524                    }
525                }
526                Err(TryRecvError::Empty) => {
527                    // The pointer was aimed at the frame on screen. When something has
528                    // been handled since (a resize, a list that arrived, rows read), it
529                    // waits for the frame that shows it, which this asks for now.
530                    if matches!(self.typed.front(), Some(Input::Mouse(_)))
531                        && !self.app.pointer.on_screen()
532                    {
533                        updated = true;
534                        progress_only = false;
535                        break;
536                    }
537                    let Some(mut input) = self.typed.pop_front() else {
538                        break;
539                    };
540                    // A drag reports every cell the pointer crosses; only where it is
541                    // now matters, so the moves waiting behind it are one.
542                    while let (Input::Mouse(now), Some(Input::Mouse(next))) =
543                        (input, self.typed.front())
544                        && is_drag(&now)
545                        && is_drag(next)
546                    {
547                        input = Input::Mouse(*next);
548                        self.typed.pop_front();
549                        self.early = self.early.saturating_sub(1);
550                    }
551                    self.since_key = 0;
552                    self.early = self.early.saturating_sub(1);
553                    // One key per frame, as when the loop read the terminal itself: a
554                    // key that acted is drawn before the next is offered, and a
555                    // continuation it queued gets its frame first.
556                    let acted = match input {
557                        Input::Key(key) => self.terminal_key(key)?,
558                        Input::Mouse(mouse) => self.terminal_mouse(mouse)?,
559                    };
560                    if acted {
561                        updated = true;
562                        progress_only = false;
563                        break;
564                    }
565                }
566                Err(TryRecvError::Disconnected) => return Ok(Drained::Exit),
567            }
568            next = self.take_next();
569        }
570        Ok(Drained::Continue {
571            updated,
572            progress_only: updated && progress_only,
573        })
574    }
575
576    /// The run loop, given a way to draw a frame: draw the first one, then replay one
577    /// held key, handle what has arrived, sleep until something arrives or a deadline
578    /// passes, and redraw, until the app exits.
579    ///
580    /// Keys ([`AppEvent::Terminal`]), worker results and the app's own news all come
581    /// through the one channel, so whichever is first ends the sleep; nothing waits out
582    /// a poll interval. Whatever was handled is drawn before the loop sleeps, and a
583    /// continuation queued by it runs right after that frame.
584    pub fn run(&mut self, mut draw: impl FnMut(&mut App) -> Result<()>) -> Result<Ended> {
585        let mut pacer = Pacer::default();
586        let mut first_rows = crate::first_rows_trace::FirstRowsTrace::from_env();
587        draw(&mut self.app)?;
588        self.app.frame_painted();
589        first_rows.painted(&self.app);
590        pacer.drew(Instant::now());
591        loop {
592            let mut pass = Pass::default();
593            // A replayed key may have queued a follow-up (a Search, an Export); it is
594            // handled in this drain, before anything typed since can overtake it.
595            pass.updated = self.replay_one()?;
596            pass.progress_only = !pass.updated;
597            if let Some(end) = pass.add(self.drain()?) {
598                return Ok(end);
599            }
600            if !pass.updated {
601                let now = Instant::now();
602                pacer.spinning(self.app.something_is_spinning(), now);
603                let timeout = pacer.timeout(self.app.next_deadline(), now);
604                if let Some(end) = pass.add(self.wait_and_drain(timeout)?) {
605                    return Ok(end);
606                }
607            }
608            let app = &mut self.app;
609            let now = Instant::now();
610            let mut redraw = pacer.handled(pass.updated, pass.progress_only, now);
611            // The throbber turns while busy, and while a background row count or
612            // anything else with a spinner of its own is still out.
613            pacer.spinning(app.something_is_spinning(), now);
614            if pacer.turn_spinner(now) {
615                app.throbber_frame = app.throbber_frame.wrapping_add(1);
616                redraw = true;
617            }
618            redraw |= app.tick_flash();
619            redraw |= app.tick_follow_clock();
620            redraw |= app.flash_background_panic();
621            redraw |= app.flash_polars_warning();
622
623            app.request_what_the_frame_needs();
624
625            if redraw {
626                draw(app)?;
627                // A count waiting for the rows to be on screen starts now.
628                app.frame_painted();
629                first_rows.painted(app);
630                pacer.drew(now);
631                // The frame sets the visible row count; a change asks for a collect.
632                if let Some(state) = &mut app.data_table_state
633                    && state.needs_recollect
634                {
635                    state.needs_recollect = false;
636                    app.spawn_async_collect(App::LOADING_BUFFER);
637                }
638                // And it marks the rows it drew without knowing them. Asked now, not
639                // on the next pass: with nothing else arriving there may not be one.
640                app.request_what_the_frame_needs();
641            }
642        }
643    }
644
645    /// Hold a continuation, and the generation, until it is dispatched.
646    fn queue_continuation(&mut self, follow_up: AppEvent) {
647        let hold = self.app.hold_the_generation();
648        self.next_up.push_back((follow_up, hold));
649    }
650
651    /// Offer one key to the app, the way the channel drain does, then reconcile the
652    /// keys queued behind it with the screen it left.
653    fn dispatch(&mut self, key: KeyEvent) -> Result<()> {
654        // The key may change the screen: a click waits for the frame that shows it.
655        self.app.pointer.changed();
656        let gen_before = self.app.screen_generation();
657        match self.app.handle(&AppEvent::Key(key)) {
658            Ok(Some(follow_up)) => self.queue_continuation(follow_up),
659            Ok(None) => {}
660            // Only reachable if the app went busy between the check and the call, which
661            // nothing on this thread does; the key keeps its place either way.
662            Err(deferred) => self.held.push_front(deferred),
663        }
664        if self.app.screen_generation() != gen_before {
665            // This key abandoned the view — home, or a declined download. The keys
666            // queued behind it were typed for the screen that is now gone.
667            self.held.clear();
668            self.app.set_input_dropped(false);
669        } else {
670            // A modal this key opened — an overwrite prompt, an error it surfaced — is
671            // one the queued keys are the answer to, unlike a modal that arrives on its
672            // own from a background result (handled in `drain_from`). Keep them and
673            // re-baseline the stamp so `discard_stale` does not then drop them.
674            self.held_for = Self::screen_of(&self.app);
675        }
676        Ok(())
677    }
678
679    /// Hold a key for later. A held navigation key repeats fast and would replay as a
680    /// burst, each step chaining another collect, so consecutive repeats become one
681    /// press — but only at the plain table view and only while every key already held is
682    /// itself a navigation key. Once `/` or any other key is held the run is text, so
683    /// nothing after it coalesces and a typed `/bookkeeper` keeps both `k`s. A column
684    /// cursor key reads nothing, so each one is kept: `l` `l` `F` counts the column two
685    /// along, as it would idle. So is each `n` and `N`: five typed move five matches.
686    /// At the cap
687    /// the newest key is dropped and the user told; never the oldest, which may be the
688    /// `/` the rest were typed into.
689    fn hold(&mut self, key: KeyEvent) {
690        if self.held.is_empty() {
691            self.held_for = Self::screen_of(&self.app);
692        }
693        if is_navigation(&key)
694            && !replays_each_press(&key)
695            && self.held.back() == Some(&key)
696            && self.app.in_normal_table_view()
697            && self.held.iter().all(is_navigation)
698        {
699            return;
700        }
701        if self.held.len() >= MAX_HELD_KEYS {
702            self.app.set_input_dropped(true);
703            return;
704        }
705        self.held.push_back(key);
706    }
707
708    /// Drop the `n` and `N` held at the front, among the keys that move the cursor.
709    /// Past any other key they are text, or meant for what that key opens.
710    fn drop_held_finds(&mut self) {
711        let run = self.held.iter().take_while(|k| is_navigation(k)).count();
712        let rest = self.held.split_off(run);
713        self.held
714            .retain(|k| !matches!(k.code, KeyCode::Char('n' | 'N')));
715        self.held.extend(rest);
716        if self.held.is_empty() {
717            self.app.set_input_dropped(false);
718        }
719    }
720
721    /// Drop the held keys if the screen they were typed at has gone: the view was
722    /// abandoned (a bumped generation), or a modal appeared that they were not answers
723    /// to. A modal that a held key opens itself is re-baselined in `dispatch`, so this
724    /// only fires for a change the keys did not cause — a background result, above all.
725    fn discard_stale(&mut self) {
726        if self.held.is_empty() {
727            return;
728        }
729        let now = Self::screen_of(&self.app);
730        let abandoned = now.generation != self.held_for.generation;
731        let new_modal = now.modal && !self.held_for.modal;
732        let failed_inline = now.inline_failures != self.held_for.inline_failures;
733        if abandoned || new_modal || failed_inline {
734            self.held.clear();
735            self.app.set_input_dropped(false);
736        }
737    }
738
739    fn screen_of(app: &App) -> Screen {
740        Screen {
741            generation: app.screen_generation(),
742            modal: app.modal_showing(),
743            inline_failures: app.inline_failures(),
744        }
745    }
746}
747
748/// How the run loop ended.
749#[derive(Debug, PartialEq, Eq)]
750pub enum Ended {
751    Quit,
752    Crash(String),
753    /// A path named at startup is not there.
754    NotFound(std::path::PathBuf),
755}
756
757/// What one turn of the run loop handled.
758#[derive(Default)]
759struct Pass {
760    updated: bool,
761    progress_only: bool,
762}
763
764impl Pass {
765    /// Fold one channel drain in; or say how the loop ends.
766    fn add(&mut self, drained: Drained) -> Option<Ended> {
767        match drained {
768            Drained::Continue {
769                updated,
770                progress_only,
771            } => {
772                if updated {
773                    self.progress_only = progress_only && (self.progress_only || !self.updated);
774                    self.updated = true;
775                }
776                None
777            }
778            Drained::Exit => Some(Ended::Quit),
779            Drained::Crash(msg) => Some(Ended::Crash(msg)),
780            Drained::NotFound(path) => Some(Ended::NotFound(path)),
781        }
782    }
783}
784
785/// How often a spinner turns: about 30 frames a second, plenty for a throbber.
786pub const SPINNER_FRAME: Duration = Duration::from_millis(33);
787
788/// The least time between two frames drawn for progress reports alone. A worker
789/// reporting a thousand times a second is drawn thirty times; a key or a result is
790/// drawn at once.
791pub const PROGRESS_FRAME: Duration = Duration::from_millis(33);
792
793/// When the run loop draws, and how long it may sleep.
794///
795/// The loop sleeps until something arrives or a deadline passes, never on a fixed
796/// tick: idle, it does not wake at all. The deadlines are the spinner's next frame
797/// while one is on screen, a progress frame owed, and whatever the app says will
798/// change on its own (a flash expiring).
799#[derive(Debug, Default)]
800pub struct Pacer {
801    last_draw: Option<Instant>,
802    /// A frame for progress reports, held back until [`PROGRESS_FRAME`] has passed.
803    owed: bool,
804    /// The spinner's next frame, while one is on screen.
805    spin_due: Option<Instant>,
806}
807
808impl Pacer {
809    /// Say whether a spinner is on screen. One that starts turns a frame later.
810    pub fn spinning(&mut self, on: bool, now: Instant) {
811        if on {
812            self.spin_due.get_or_insert(now + SPINNER_FRAME);
813        } else {
814            self.spin_due = None;
815        }
816    }
817
818    /// Whether the spinner's next frame is due; moves the deadline on when it is.
819    pub fn turn_spinner(&mut self, now: Instant) -> bool {
820        match self.spin_due {
821            Some(due) if now >= due => {
822                self.spin_due = Some(now + SPINNER_FRAME);
823                true
824            }
825            _ => false,
826        }
827    }
828
829    /// What a pass handled, and whether to draw for it now. A pass of progress
830    /// reports alone, close behind the last frame, is owed a frame instead.
831    pub fn handled(&mut self, updated: bool, progress_only: bool, now: Instant) -> bool {
832        let progress_due = self
833            .last_draw
834            .is_none_or(|last| now >= last + PROGRESS_FRAME);
835        if updated && !progress_only {
836            return true;
837        }
838        if updated {
839            self.owed = true;
840        }
841        self.owed && progress_due
842    }
843
844    /// A frame was drawn.
845    pub fn drew(&mut self, now: Instant) {
846        self.last_draw = Some(now);
847        self.owed = false;
848    }
849
850    /// How long the loop may sleep: until the earliest deadline, or for as long as
851    /// it takes when there is none.
852    pub fn timeout(&self, app_deadline: Option<Instant>, now: Instant) -> Duration {
853        let owed = self
854            .owed
855            .then(|| self.last_draw.map(|last| last + PROGRESS_FRAME))
856            .flatten();
857        [self.spin_due, owed, app_deadline]
858            .into_iter()
859            .flatten()
860            .min()
861            .map_or(Duration::MAX, |at| at.saturating_duration_since(now))
862    }
863}
864
865/// Navigation keys a held run keeps every press of: the column cursor's (`h` `l`
866/// `{` `}` and the arrows across, Shift for a page), which read nothing, and a find's
867/// next and previous, each its own match.
868fn replays_each_press(key: &KeyEvent) -> bool {
869    matches!(
870        key.code,
871        KeyCode::Left
872            | KeyCode::Right
873            | KeyCode::Char('h')
874            | KeyCode::Char('l')
875            | KeyCode::Char('{')
876            | KeyCode::Char('}')
877            | KeyCode::Char('n')
878            | KeyCode::Char('N')
879    )
880}
881
882fn is_drag(mouse: &MouseEvent) -> bool {
883    matches!(mouse.kind, crossterm::event::MouseEventKind::Drag(_))
884}
885
886/// Keys that move the view and are commonly held down. The column cursor's keys
887/// (Left/Right/h/l, `{ }`) are included, so Enter behind them still inspects.
888fn is_navigation(key: &KeyEvent) -> bool {
889    let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
890    match key.code {
891        KeyCode::Up
892        | KeyCode::Down
893        | KeyCode::PageUp
894        | KeyCode::PageDown
895        | KeyCode::Home
896        | KeyCode::End
897        | KeyCode::Left
898        | KeyCode::Right
899        | KeyCode::Char('j')
900        | KeyCode::Char('k')
901        | KeyCode::Char('h')
902        | KeyCode::Char('l')
903        | KeyCode::Char('{')
904        | KeyCode::Char('}')
905        | KeyCode::Char('G')
906        // A find's next and previous move the cursor too.
907        | KeyCode::Char('n')
908        | KeyCode::Char('N') => true,
909        KeyCode::Char('f') | KeyCode::Char('b') | KeyCode::Char('d') | KeyCode::Char('u') => ctrl,
910        _ => false,
911    }
912}
913
914#[cfg(test)]
915mod tests {
916    use super::*;
917    use crate::export_modal::{ExportFocus, ExportFormat};
918    use crate::{InputMode, OpenOptions};
919    use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget};
920    use std::io::Write;
921    use std::sync::mpsc;
922
923    fn plain(code: KeyCode) -> KeyEvent {
924        KeyEvent::new(code, KeyModifiers::NONE)
925    }
926
927    fn ctrl(c: char) -> KeyEvent {
928        KeyEvent::new(KeyCode::Char(c), KeyModifiers::CONTROL)
929    }
930
931    fn held(pump: &EventPump) -> Vec<KeyCode> {
932        pump.held_keys().map(|k| k.code).collect()
933    }
934
935    fn pump() -> EventPump {
936        let (tx, rx) = mpsc::channel();
937        let app = App::new(tx.clone(), crate::tests::test_runtime());
938        EventPump::new(app, tx, rx)
939    }
940
941    /// Type each character as the terminal would deliver it.
942    fn type_keys(pump: &mut EventPump, text: &str) {
943        for c in text.chars() {
944            pump.terminal_key(plain(KeyCode::Char(c))).unwrap();
945        }
946    }
947
948    /// Run the loop the way `run()` does, without a terminal, until the app is idle
949    /// with nothing held, nothing on the channel and no background work still to
950    /// report, or it exits.
951    ///
952    /// Waits on the work rather than on a quiet spell: a result slower than the spell
953    /// on a loaded machine is not a result that never comes, and a row count left
954    /// running lands in the middle of whatever the test does next.
955    ///
956    /// Anything holding the generation is work still to report: a job lets go in the
957    /// step that handles its answer, so an idle app holding it is a job still running
958    /// quietly, or a continuation. The download confirmation's hold is not waited on:
959    /// that one waits on the user.
960    fn settle(pump: &mut EventPump) -> Drained {
961        fn owed(app: &App) -> bool {
962            crate::tests::work_pending(app)
963                || (app.work_a_bump_would_strand() && !app.awaiting_open_confirmation())
964        }
965        // Only a hang guard; nothing here is timed.
966        let deadline = std::time::Instant::now() + Duration::from_secs(300);
967        loop {
968            assert!(
969                std::time::Instant::now() < deadline,
970                "the loop did not settle"
971            );
972            let replayed = pump.replay_one().unwrap();
973            // As `run()` paints after every update.
974            pump.app.frame_painted();
975            let drained = if owed(&pump.app) && !replayed {
976                pump.wait_and_drain(Duration::from_millis(50))
977            } else {
978                pump.drain()
979            }
980            .unwrap();
981            let updated = match &drained {
982                Drained::Continue { updated, .. } => *updated,
983                _ => return drained,
984            };
985            if replayed || updated || owed(&pump.app) {
986                continue;
987            }
988            if pump.held_keys().next().is_none() {
989                return drained;
990            }
991        }
992    }
993
994    /// A pump with a three-row CSV loaded, the way `run()` loads one.
995    fn loaded_pump() -> (EventPump, tempfile::TempDir) {
996        let dir = tempfile::tempdir().expect("temp dir");
997        let path = dir.path().join("people.csv");
998        let mut file = std::fs::File::create(&path).expect("create csv");
999        writeln!(file, "name,age\nada,36\ngrace,45\nalan,41").expect("write csv");
1000        drop(file);
1001
1002        let mut pump = pump();
1003        // The queries typed here are q.
1004        pump.app.app_config.query.default_mode = crate::QueryMode::Q;
1005        pump.send(AppEvent::Open(vec![path], OpenOptions::default()))
1006            .unwrap();
1007        settle(&mut pump);
1008        assert!(
1009            pump.app.data_table_state.is_some(),
1010            "the CSV should have loaded"
1011        );
1012        assert_eq!(pump.app.input_mode, InputMode::Normal);
1013        // A frame sets the visible row count, as it has before any key in `run()`.
1014        rendered(&mut pump.app);
1015        (pump, dir)
1016    }
1017
1018    /// While a find reads, an `n` typed meanwhile waits and replays once it lands;
1019    /// Esc jumps the queue, stops the find in flight, and drops the `n` held behind it.
1020    #[test]
1021    fn keys_typed_while_a_find_reads_wait_and_esc_stops_it() {
1022        let (mut p, _dir) = loaded_pump();
1023        p.terminal_key(plain(KeyCode::Char('f'))).unwrap();
1024        type_keys(&mut p, "a");
1025        p.terminal_key(plain(KeyCode::Enter)).unwrap();
1026        assert!(p.app.finding(), "the find reads in the background");
1027        p.terminal_key(plain(KeyCode::Char('n'))).unwrap();
1028        assert_eq!(held(&p), [KeyCode::Char('n')]);
1029        settle(&mut p);
1030        assert!(held(&p).is_empty());
1031        assert_eq!(
1032            p.app.find_hit(),
1033            Some((1, "name".to_string())),
1034            "the held n moved on from the first match"
1035        );
1036
1037        p.terminal_key(plain(KeyCode::Char('n'))).unwrap();
1038        assert!(p.app.finding());
1039        p.terminal_key(plain(KeyCode::Char('n'))).unwrap();
1040        p.terminal_key(plain(KeyCode::Esc)).unwrap();
1041        assert!(!p.app.finding(), "Esc acted at once");
1042        assert!(held(&p).is_empty(), "and dropped the held n");
1043        settle(&mut p);
1044        assert!(!p.app.finding(), "no held find started");
1045        assert_eq!(
1046            p.app.find_hit(),
1047            Some((1, "name".to_string())),
1048            "the cancelled find moved nothing"
1049        );
1050    }
1051
1052    /// Esc stopping a find drops only the `n` and `N` among the cursor keys held at
1053    /// the front; the other keys keep their place, and an `n` typed after `/` is text.
1054    #[test]
1055    fn esc_stopping_a_find_keeps_the_other_held_keys() {
1056        let (mut p, _dir) = loaded_pump();
1057        p.terminal_key(plain(KeyCode::Char('f'))).unwrap();
1058        type_keys(&mut p, "a");
1059        p.terminal_key(plain(KeyCode::Enter)).unwrap();
1060        assert!(p.app.finding());
1061        type_keys(&mut p, "njN/n");
1062        assert_eq!(
1063            held(&p),
1064            [
1065                KeyCode::Char('n'),
1066                KeyCode::Char('j'),
1067                KeyCode::Char('N'),
1068                KeyCode::Char('/'),
1069                KeyCode::Char('n'),
1070            ]
1071        );
1072        p.terminal_key(plain(KeyCode::Esc)).unwrap();
1073        assert!(!p.app.finding());
1074        assert_eq!(
1075            held(&p),
1076            [KeyCode::Char('j'), KeyCode::Char('/'), KeyCode::Char('n')]
1077        );
1078    }
1079
1080    /// Each `n` typed while a find reads is held and replayed, as the column cursor's
1081    /// keys are: five typed move five matches, not one.
1082    #[test]
1083    fn every_n_typed_while_a_find_reads_moves_a_match() {
1084        let dir = tempfile::tempdir().expect("temp dir");
1085        let path = dir.path().join("matches.csv");
1086        let mut file = std::fs::File::create(&path).expect("create csv");
1087        writeln!(file, "id,v").expect("write csv");
1088        for row in 0..10 {
1089            writeln!(file, "{row},x{row}").expect("write csv");
1090        }
1091        drop(file);
1092        let mut p = pump();
1093        p.send(AppEvent::Open(vec![path], OpenOptions::default()))
1094            .unwrap();
1095        settle(&mut p);
1096        rendered(&mut p.app);
1097
1098        p.terminal_key(plain(KeyCode::Char('f'))).unwrap();
1099        type_keys(&mut p, "x");
1100        p.terminal_key(plain(KeyCode::Enter)).unwrap();
1101        assert!(p.app.finding());
1102        for _ in 0..5 {
1103            p.terminal_key(plain(KeyCode::Char('n'))).unwrap();
1104        }
1105        assert_eq!(held(&p), [KeyCode::Char('n'); 5], "none collapsed");
1106        settle(&mut p);
1107        assert!(held(&p).is_empty());
1108        assert_eq!(p.app.find_hit(), Some((5, "v".to_string())));
1109
1110        p.terminal_key(plain(KeyCode::Char('N'))).unwrap();
1111        assert!(p.app.finding());
1112        p.terminal_key(plain(KeyCode::Char('N'))).unwrap();
1113        p.terminal_key(plain(KeyCode::Char('N'))).unwrap();
1114        assert_eq!(held(&p), [KeyCode::Char('N'); 2]);
1115        settle(&mut p);
1116        assert_eq!(p.app.find_hit(), Some((2, "v".to_string())));
1117    }
1118
1119    /// Enter held behind `n` waits as Space, as behind any key that moves the cursor.
1120    #[test]
1121    fn enter_held_behind_n_waits_as_space() {
1122        let (mut p, _dir) = loaded_pump();
1123        p.terminal_key(plain(KeyCode::Char('f'))).unwrap();
1124        type_keys(&mut p, "a");
1125        p.terminal_key(plain(KeyCode::Enter)).unwrap();
1126        assert!(p.app.finding());
1127        p.terminal_key(plain(KeyCode::Char('n'))).unwrap();
1128        p.terminal_key(plain(KeyCode::Enter)).unwrap();
1129        assert_eq!(held(&p), [KeyCode::Char('n'), KeyCode::Char(' ')]);
1130    }
1131
1132    /// Keys typed at the inspector while it reads a row's hidden fields wait,
1133    /// Esc among them, and replay in order once the read lands.
1134    #[test]
1135    fn keys_typed_while_the_inspector_reads_wait_their_turn() {
1136        let (mut p, _dir) = loaded_pump();
1137        let state = p.app.data_table_state.as_mut().unwrap();
1138        state.set_column_order(vec!["name".to_string()]);
1139        rendered(&mut p.app);
1140        p.terminal_key(plain(KeyCode::Char(' '))).unwrap();
1141        assert_eq!(p.app.input_mode, InputMode::Inspect);
1142        p.terminal_key(plain(KeyCode::End)).unwrap();
1143        p.terminal_key(plain(KeyCode::Enter)).unwrap();
1144        assert!(
1145            p.app.is_busy(),
1146            "the hidden field is read in the background"
1147        );
1148        p.terminal_key(plain(KeyCode::Char('k'))).unwrap();
1149        p.terminal_key(plain(KeyCode::Esc)).unwrap();
1150        assert_eq!(held(&p), [KeyCode::Char('k'), KeyCode::Esc]);
1151        assert_eq!(p.app.input_mode, InputMode::Inspect, "nothing acted yet");
1152        settle(&mut p);
1153        assert!(held(&p).is_empty());
1154        assert_eq!(
1155            p.app.input_mode,
1156            InputMode::Normal,
1157            "Esc closed it, in turn"
1158        );
1159        assert_eq!(
1160            p.app.inspector_modal.focused().map(|f| f.name.as_str()),
1161            Some("name"),
1162            "k moved first"
1163        );
1164    }
1165
1166    /// #615: keys typed while the inspector parses long JSON text wait, then
1167    /// replay inside the level it opens.
1168    #[test]
1169    fn keys_typed_while_json_parses_replay_inside_the_level() {
1170        let dir = tempfile::tempdir().expect("temp dir");
1171        let path = dir.path().join("docs.csv");
1172        // Over the 64 KB parsed on the key.
1173        let doc = format!("[{}0]", "0, ".repeat(30_000));
1174        std::fs::write(&path, format!("doc\n\"{doc}\"\n")).expect("write csv");
1175        let mut p = pump();
1176        p.send(AppEvent::Open(vec![path], OpenOptions::default()))
1177            .unwrap();
1178        settle(&mut p);
1179        rendered(&mut p.app);
1180        p.terminal_key(plain(KeyCode::Char(' '))).unwrap();
1181        rendered(&mut p.app);
1182        p.terminal_key(plain(KeyCode::Enter)).unwrap();
1183        assert!(p.app.is_busy(), "the text is parsed in the background");
1184        p.terminal_key(plain(KeyCode::Char('j'))).unwrap();
1185        p.terminal_key(plain(KeyCode::Char('j'))).unwrap();
1186        assert_eq!(held(&p), [KeyCode::Char('j'), KeyCode::Char('j')]);
1187        settle(&mut p);
1188        let drill = p
1189            .app
1190            .inspector_modal
1191            .drill
1192            .as_ref()
1193            .expect("the array opened");
1194        assert_eq!(drill.level().node.len(), 30_001);
1195        assert_eq!(drill.level().selected, 2, "both j moved inside it");
1196    }
1197
1198    fn rendered(app: &mut App) -> String {
1199        let area = Rect::new(0, 0, 100, 20);
1200        let mut buf = Buffer::empty(area);
1201        app.render(area, &mut buf);
1202        buf.content().iter().map(|c| c.symbol()).collect()
1203    }
1204
1205    fn mouse(kind: crossterm::event::MouseEventKind, (x, y): (u16, u16)) -> MouseEvent {
1206        MouseEvent {
1207            kind,
1208            column: x,
1209            row: y,
1210            modifiers: KeyModifiers::NONE,
1211        }
1212    }
1213
1214    fn click(at: (u16, u16)) -> MouseEvent {
1215        use crossterm::event::{MouseButton, MouseEventKind};
1216        mouse(MouseEventKind::Down(MouseButton::Left), at)
1217    }
1218
1219    /// Where `text` is drawn in a 100×20 frame, in cells.
1220    fn on_screen(app: &mut App, text: &str) -> (u16, u16) {
1221        let area = Rect::new(0, 0, 100, 20);
1222        let mut buf = Buffer::empty(area);
1223        app.render(area, &mut buf);
1224        for y in 0..area.height {
1225            let line: String = (0..area.width).map(|x| buf[(x, y)].symbol()).collect();
1226            if let Some(at) = line.find(text) {
1227                return (line[..at].chars().count() as u16, y);
1228            }
1229        }
1230        panic!("{text:?} is not on screen");
1231    }
1232
1233    /// The table's cursor: the row on screen and the column.
1234    fn cell(pump: &EventPump) -> (Option<usize>, Option<String>) {
1235        let state = pump.app.data_table_state.as_ref().expect("a dataset");
1236        (
1237            state.table_state.selected(),
1238            state.current_column().map(str::to_string),
1239        )
1240    }
1241
1242    /// A click on a cell puts the cursor on its row and column; a second click there
1243    /// is a double click, which presses Enter and inspects the row.
1244    #[test]
1245    fn a_click_moves_the_cursor_and_a_double_click_inspects() {
1246        let (mut p, _dir) = loaded_pump();
1247        assert_eq!(cell(&p), (Some(0), Some("name".to_string())));
1248        let at = on_screen(&mut p.app, "45");
1249        assert!(p.terminal_mouse(click(at)).unwrap());
1250        assert_eq!(cell(&p), (Some(1), Some("age".to_string())));
1251        assert_eq!(p.app.input_mode, InputMode::Normal, "one click only moves");
1252
1253        // A click on the other row, then twice on it.
1254        let at = on_screen(&mut p.app, "alan");
1255        p.terminal_mouse(click(at)).unwrap();
1256        assert_eq!(cell(&p), (Some(2), Some("name".to_string())));
1257        assert_eq!(p.app.input_mode, InputMode::Normal);
1258        p.terminal_mouse(click(at)).unwrap();
1259        assert_eq!(p.app.input_mode, InputMode::Inspect, "Enter inspects");
1260
1261        // In the inspector the table is not under the pointer: nothing to click.
1262        let before = cell(&p);
1263        p.terminal_mouse(click((3, 3))).unwrap();
1264        assert_eq!(cell(&p), before);
1265    }
1266
1267    /// The wheel presses the arrows: down three rows at a time, stopping at the last,
1268    /// and across, or with Shift, the column cursor.
1269    #[test]
1270    fn the_wheel_scrolls_rows_and_across_moves_the_column() {
1271        use crossterm::event::MouseEventKind;
1272        let (mut p, _dir) = numbered_pump(50);
1273        let (mut p2, _dir2) = loaded_pump();
1274        let rows = |p: &EventPump| cell(p).0;
1275        assert!(
1276            p.terminal_mouse(mouse(MouseEventKind::ScrollDown, (5, 5)))
1277                .unwrap()
1278        );
1279        assert_eq!(rows(&p), Some(3));
1280        p.terminal_mouse(mouse(MouseEventKind::ScrollUp, (5, 5)))
1281            .unwrap();
1282        assert_eq!(rows(&p), Some(0));
1283
1284        let mut shift = mouse(MouseEventKind::ScrollDown, (5, 5));
1285        shift.modifiers = KeyModifiers::SHIFT;
1286        p2.terminal_mouse(shift).unwrap();
1287        assert_eq!(cell(&p2), (Some(0), Some("age".to_string())));
1288        p2.terminal_mouse(mouse(MouseEventKind::ScrollLeft, (5, 5)))
1289            .unwrap();
1290        assert_eq!(cell(&p2), (Some(0), Some("name".to_string())));
1291        p2.terminal_mouse(mouse(MouseEventKind::ScrollRight, (5, 5)))
1292            .unwrap();
1293        assert_eq!(cell(&p2).1.as_deref(), Some("age"));
1294        // Three rows down from the first, of three: the last, as ↓ stops there.
1295        p2.terminal_mouse(mouse(MouseEventKind::ScrollDown, (5, 5)))
1296            .unwrap();
1297        rendered(&mut p2.app);
1298        assert_eq!(rows(&p2), Some(2));
1299    }
1300
1301    /// Mouse input is never held. At a busy table the wheel across moves the column
1302    /// cursor at once, as ←→ do; the wheel down, which waits as a typed ↓ would, and a
1303    /// click, aimed at a screen that may be gone by the time it could act, are dropped.
1304    /// Behind held keys a click is dropped too, so it cannot overtake them.
1305    #[test]
1306    fn mouse_input_is_never_held() {
1307        use crossterm::event::MouseEventKind;
1308        let (mut p, _dir) = loaded_pump();
1309        let alan = on_screen(&mut p.app, "alan");
1310        p.app.busy = true;
1311        assert!(
1312            !p.terminal_mouse(mouse(MouseEventKind::ScrollDown, (5, 5)))
1313                .unwrap()
1314        );
1315        assert!(!p.terminal_mouse(click(alan)).unwrap());
1316        assert!(held(&p).is_empty(), "nothing held");
1317        assert_eq!(cell(&p), (Some(0), Some("name".to_string())));
1318        assert!(
1319            p.terminal_mouse(mouse(MouseEventKind::ScrollRight, (5, 5)))
1320                .unwrap()
1321        );
1322        assert_eq!(cell(&p), (Some(0), Some("age".to_string())), "at once");
1323
1324        // Idle again with a key held: the key goes first, the click is dropped.
1325        p.terminal_key(plain(KeyCode::Char('j'))).unwrap();
1326        assert_eq!(held(&p), [KeyCode::Char('j')]);
1327        p.app.busy = false;
1328        assert!(!p.terminal_mouse(click(alan)).unwrap());
1329        settle(&mut p);
1330        assert_eq!(cell(&p).0, Some(1), "the held j, and no click");
1331        assert!(p.terminal_mouse(click(alan)).unwrap());
1332        assert_eq!(cell(&p).0, Some(2));
1333    }
1334
1335    /// A click dropped while busy is not the first of a double click: the next click
1336    /// on the same cell only moves the cursor, and does not inspect.
1337    #[test]
1338    fn a_dropped_click_does_not_make_the_next_a_double_click() {
1339        let (mut p, _dir) = loaded_pump();
1340        let alan = on_screen(&mut p.app, "alan");
1341        p.app.busy = true;
1342        assert!(!p.terminal_mouse(click(alan)).unwrap());
1343        p.app.busy = false;
1344        assert!(p.terminal_mouse(click(alan)).unwrap());
1345        assert_eq!(cell(&p).0, Some(2));
1346        assert_eq!(p.app.input_mode, InputMode::Normal, "one click, no Enter");
1347    }
1348
1349    /// A click read behind an event that may have changed the screen waits for the
1350    /// frame that shows the change, so it lands on what the user sees.
1351    #[test]
1352    fn a_click_waits_for_the_frame_after_a_change() {
1353        let (mut p, _dir) = loaded_pump();
1354        let alan = on_screen(&mut p.app, "alan");
1355        p.app.frame_painted();
1356        p.send(AppEvent::Terminal(Event::Resize(100, 20))).unwrap();
1357        p.send(AppEvent::Terminal(Event::Mouse(click(alan))))
1358            .unwrap();
1359        let drained = p.drain().unwrap();
1360        assert!(
1361            matches!(
1362                drained,
1363                Drained::Continue {
1364                    updated: true,
1365                    progress_only: false
1366                }
1367            ),
1368            "a frame is asked for: {drained:?}"
1369        );
1370        assert_eq!(cell(&p).0, Some(0), "not yet");
1371        rendered(&mut p.app);
1372        p.app.frame_painted();
1373        p.drain().unwrap();
1374        assert_eq!(cell(&p).0, Some(2), "on the frame that shows the resize");
1375    }
1376
1377    /// A key the footer shows presses it, as typed: `? keys` opens help, and
1378    /// while the help is up the wheel moves its selection.
1379    #[test]
1380    fn a_chip_presses_its_key() {
1381        use crossterm::event::MouseEventKind;
1382        let (mut p, _dir) = loaded_pump();
1383        let help = on_screen(&mut p.app, "? keys");
1384        assert!(p.terminal_mouse(click(help)).unwrap());
1385        assert!(p.app.help_visible(), "the Help chip opened help");
1386        rendered(&mut p.app);
1387        p.terminal_mouse(mouse(MouseEventKind::ScrollDown, (5, 5)))
1388            .unwrap();
1389        assert_eq!(p.app.help.selected, 3);
1390        // No table under the help: a click there moves nothing.
1391        p.terminal_mouse(click((5, 5))).unwrap();
1392        assert_eq!(cell(&p), (Some(0), Some("name".to_string())));
1393    }
1394
1395    /// A dialog's footer chips press their keys as the status footer's do: Esc
1396    /// Cancel closes the export form, and over a question only its own chips take
1397    /// clicks.
1398    #[test]
1399    fn a_dialog_s_footer_chips_press_their_keys() {
1400        let (mut p, _dir) = loaded_pump();
1401        p.send(AppEvent::Terminal(Event::Key(plain(KeyCode::Char('e')))))
1402            .unwrap();
1403        settle(&mut p);
1404        assert!(p.app.export_modal.active);
1405        let cancel = on_screen(&mut p.app, "Cancel");
1406        assert!(p.terminal_mouse(click(cancel)).unwrap());
1407        settle(&mut p);
1408        assert!(!p.app.export_modal.active, "Esc Cancel closed the form");
1409
1410        // A question: its Cancel chip answers it; a click beside it does nothing.
1411        p.app.confirmation_modal.show("Delete it?".to_string());
1412        p.terminal_mouse(click((0, 2))).unwrap();
1413        settle(&mut p);
1414        assert!(p.app.confirmation_modal.active, "outside the question");
1415        let cancel = on_screen(&mut p.app, "Cancel");
1416        p.terminal_mouse(click(cancel)).unwrap();
1417        settle(&mut p);
1418        assert!(!p.app.confirmation_modal.active, "its Esc chip answered it");
1419
1420        // Help's own footer: Esc Close.
1421        p.send(AppEvent::Terminal(Event::Key(plain(KeyCode::Char('?')))))
1422            .unwrap();
1423        settle(&mut p);
1424        assert!(p.app.help_visible());
1425        let close = on_screen(&mut p.app, "Close");
1426        p.terminal_mouse(click(close)).unwrap();
1427        settle(&mut p);
1428        assert!(!p.app.help_visible(), "help's Close chip closed it");
1429    }
1430
1431    /// Mouse events reach the app through the channel, in order with the keys typed
1432    /// around them, as `run()` reads them.
1433    #[test]
1434    fn mouse_events_keep_their_place_among_the_keys() {
1435        let (mut p, _dir) = loaded_pump();
1436        let alan = on_screen(&mut p.app, "alan");
1437        p.send(AppEvent::Terminal(Event::Key(plain(KeyCode::Char('l')))))
1438            .unwrap();
1439        p.send(AppEvent::Terminal(Event::Mouse(click(alan))))
1440            .unwrap();
1441        settle(&mut p);
1442        assert_eq!(
1443            cell(&p),
1444            (Some(2), Some("name".to_string())),
1445            "the l moved to age, then the click came back to name"
1446        );
1447    }
1448
1449    /// A continuation never goes to the back of the channel.
1450    ///
1451    /// The gap #221 was left short by is created by exactly one thing: putting a
1452    /// handler's follow-up behind whatever arrived while that handler ran.
1453    /// `queue_continuation` is the only way a follow-up should travel, holding the
1454    /// generation, and `EventPump::send`, for callers pushing an event of their own, is
1455    /// the only `tx.send` that belongs in this file.
1456    #[test]
1457    fn a_continuation_never_goes_to_the_back_of_the_channel() {
1458        let source = include_str!("event_pump.rs");
1459        // Split so this test's own needle is not one of the things it finds.
1460        let needle = concat!("self.tx", ".send(");
1461        assert_eq!(
1462            source.matches(needle).count(),
1463            1,
1464            "the one send left should be `EventPump::send`. A follow-up sent to the \
1465             channel holds nothing while it waits, and the generation reads free in the \
1466             middle of an errand — see `jobs::Hold`."
1467        );
1468    }
1469
1470    /// A continuation holds the generation until it has been dispatched.
1471    ///
1472    /// This is the gap #221 was left short by. An errand of several phases hands off
1473    /// through a returned event, and the pump breaks there so a frame can be drawn —
1474    /// so for one iteration of the loop the phase that finished has let go of the
1475    /// generation and the phase that follows has not taken it. A collect starting in
1476    /// that window bumps `task_generation` out from under the errand, whose answer is
1477    /// then thrown away and never asked for again.
1478    ///
1479    /// An export is the errand used here: `Export` draws its progress and returns
1480    /// `DoExport` before anything has been spawned at all.
1481    #[test]
1482    fn a_continuation_holds_the_generation_until_it_is_dispatched() {
1483        let (mut p, dir) = loaded_pump();
1484        let out = dir.path().join("out.csv");
1485        assert!(!p.app.work_a_bump_would_strand(), "nothing is running yet");
1486        // The export's worker waits until it has been seen running.
1487        let (waits, release) =
1488            crate::tests::worker_waits_once(|job| matches!(job, crate::Job::Export));
1489        p.app.jobs.worker_waits = waits;
1490
1491        p.send(AppEvent::Export(csv_export(&out))).unwrap();
1492        assert!(matches!(p.drain().unwrap(), Drained::Continue { .. }));
1493
1494        // `Export` has returned `DoExport` and nothing has been spawned: this is the
1495        // moment the loop draws a frame and reads the terminal.
1496        assert!(
1497            !p.next_up.is_empty(),
1498            "the continuation is waiting to be dispatched"
1499        );
1500        assert!(
1501            p.app.work_a_bump_would_strand(),
1502            "and the generation is held while it waits"
1503        );
1504
1505        // Dispatching it hands the generation to the phase it starts, rather than
1506        // letting go before the other takes it.
1507        assert!(matches!(p.drain().unwrap(), Drained::Continue { .. }));
1508        assert!(
1509            p.app.work_a_bump_would_strand(),
1510            "the export it started is running now, and holds it in turn"
1511        );
1512
1513        release.send(()).unwrap();
1514        settle(&mut p);
1515        assert!(out.exists(), "and the export finishes, which is the point");
1516        assert!(
1517            !p.app.work_a_bump_would_strand(),
1518            "with the generation free again afterwards"
1519        );
1520    }
1521
1522    /// A key handled in that window does not find the generation free either.
1523    ///
1524    /// The pump breaks so a frame can be drawn and the terminal polled, so exactly one
1525    /// key can be handled between a continuation being queued and being dispatched.
1526    /// Ctrl-C and the other hard escapes act even while busy, and `App::handle` runs the
1527    /// deferred errands at its tail whatever the key was.
1528    #[test]
1529    fn a_key_in_the_handoff_window_does_not_find_the_generation_free() {
1530        let (mut p, dir) = loaded_pump();
1531        let out = dir.path().join("out.csv");
1532
1533        // An errand waiting for the generation to come free. Without one the tail of
1534        // `App::handle` has nothing to run, and the key below would prove nothing.
1535        p.app.owe_rows_for_tests("Loading buffer...");
1536
1537        // And the user exports, which after one drain is mid-handoff.
1538        p.send(AppEvent::Export(csv_export(&out))).unwrap();
1539        assert!(matches!(p.drain().unwrap(), Drained::Continue { .. }));
1540        assert!(!p.next_up.is_empty(), "mid-handoff");
1541        let held_at = p.app.task_generation();
1542
1543        p.terminal_key(plain(KeyCode::Char('?'))).unwrap();
1544
1545        assert_eq!(
1546            p.app.task_generation(),
1547            held_at,
1548            "the owed collect did not go in on the back of a key handled in the window"
1549        );
1550        assert!(
1551            p.app.rows_owed(),
1552            "it is still owed, waiting for the export in front of it"
1553        );
1554    }
1555
1556    /// An errand of several phases never lets go of the generation until it is done.
1557    ///
1558    /// The invariant #221 actually wants, asserted at the boundary rather than through a
1559    /// proxy: every time the pump breaks — which is every time a frame is drawn and a key
1560    /// could be handled — an errand still in progress holds the generation. What used to
1561    /// cover this was a list of three flags in the predicate; what covers it now is the
1562    /// continuation's hold, and that has to be true at each phase change rather than only
1563    /// at the first.
1564    ///
1565    /// An export hands off once, from drawing its progress to the job, which then
1566    /// runs from plan to committed file holding the generation itself, and lets go in
1567    /// the step that handles its answer.
1568    #[test]
1569    fn an_export_holds_the_generation_at_every_phase_change() {
1570        let (mut p, dir) = loaded_pump();
1571        let out = dir.path().join("out.csv");
1572
1573        p.send(AppEvent::Export(csv_export(&out))).unwrap();
1574
1575        let mut breaks = 0;
1576        for _ in 0..10_000 {
1577            let drained = if p.app.is_busy() {
1578                p.wait_and_drain(Duration::from_secs(10)).unwrap()
1579            } else {
1580                p.drain().unwrap()
1581            };
1582            match drained {
1583                Drained::Continue { updated, .. } => {
1584                    // Mid-errand, at the moment the loop would draw and poll.
1585                    if !p.next_up.is_empty() {
1586                        breaks += 1;
1587                    }
1588                    if !p.next_up.is_empty() || p.app.is_busy() {
1589                        assert!(
1590                            p.app.work_a_bump_would_strand(),
1591                            "the export left the generation free after {breaks} hand-offs"
1592                        );
1593                    }
1594                    if !updated && p.next_up.is_empty() && !p.app.is_busy() {
1595                        break;
1596                    }
1597                }
1598                other => panic!("the export should not end the loop: {other:?}"),
1599            }
1600        }
1601
1602        assert!(
1603            breaks >= 1,
1604            "the export handed off to its job, and that was checked; saw {breaks}"
1605        );
1606        assert!(out.exists(), "and the file was written, which is the point");
1607        // The answer that cleared `busy` released the generation in the same step: no
1608        // release is left to arrive behind it (#490).
1609        assert!(
1610            !p.app.work_a_bump_would_strand(),
1611            "the generation is free the moment the export is done"
1612        );
1613    }
1614
1615    fn csv_export(path: &std::path::Path) -> crate::ExportRequest {
1616        crate::ExportRequest {
1617            path: path.to_path_buf(),
1618            format: ExportFormat::Csv,
1619            options: crate::ExportOptions {
1620                csv_delimiter: b',',
1621                csv_include_header: true,
1622                source_file: false,
1623                csv_compression: None,
1624                json_compression: None,
1625                ndjson_compression: None,
1626            },
1627            overwrite: crate::output_file::Overwrite::Forbid,
1628        }
1629    }
1630
1631    /// #455: an export whose worker dies ends: the reason on the export dialog's
1632    /// status line, the keyboard back, nothing of its own left set, no file and
1633    /// the generation free. The next export writes its file.
1634    #[test]
1635    fn an_export_whose_worker_dies_ends_and_the_next_one_writes() {
1636        let (mut p, dir) = loaded_pump();
1637        let out = dir.path().join("out.csv");
1638        let mut exports = 0;
1639        p.app.jobs.worker_dies = Some(Box::new(move |job| {
1640            exports += usize::from(matches!(job, crate::Job::Export));
1641            exports == 1
1642        }));
1643        p.send(AppEvent::Export(csv_export(&out))).unwrap();
1644
1645        let deadline = std::time::Instant::now() + Duration::from_secs(300);
1646        while p.app.export_modal.path_error.is_none() {
1647            assert!(
1648                std::time::Instant::now() < deadline,
1649                "the export never ended"
1650            );
1651            p.wait_and_drain(Duration::from_millis(50)).unwrap();
1652        }
1653        let reason = p.app.export_modal.path_error.clone().unwrap_or_default();
1654        assert!(reason.contains("worker died"), "{reason}");
1655        assert!(p.app.export_modal.active);
1656        assert!(!p.app.error_modal.active);
1657        assert!(!p.app.is_busy());
1658        assert!(p.app.status_message.is_none());
1659        assert!(p.app.nothing_loading());
1660        assert!(!out.exists());
1661        assert!(
1662            !p.app.work_a_bump_would_strand(),
1663            "the generation is free as the failure is shown"
1664        );
1665
1666        p.terminal_key(plain(KeyCode::Esc)).unwrap();
1667        assert!(!p.app.export_modal.active);
1668        p.send(AppEvent::Export(csv_export(&out))).unwrap();
1669        settle(&mut p);
1670        assert!(!p.app.error_modal.active, "{}", p.app.error_modal.message);
1671        assert_eq!(p.app.export_modal.path_error, None);
1672        assert!(out.exists(), "the next export writes its file");
1673        assert!(p.app.nothing_loading());
1674    }
1675
1676    /// #455: a drill whose row read dies says so in one line, pointing at the log
1677    /// rather than spilling the internal error into the flash, and the next Enter
1678    /// drills.
1679    #[test]
1680    fn a_drill_whose_read_dies_flashes_one_line_and_the_next_one_drills() {
1681        let (mut p, _dir) = loaded_pump();
1682        p.send(AppEvent::QQuery("select n: count age by name".to_string()))
1683            .unwrap();
1684        settle(&mut p);
1685        // With the key hidden the buffer cannot say which group a row is, so Enter
1686        // reads it on a worker.
1687        p.app
1688            .data_table_state
1689            .as_mut()
1690            .unwrap()
1691            .set_column_order(vec!["n".to_string()]);
1692        settle(&mut p);
1693        rendered(&mut p.app);
1694
1695        p.app.jobs.worker_dies =
1696            crate::tests::worker_dies_once(|job| matches!(job, crate::Job::DrillRow));
1697        p.terminal_key(plain(KeyCode::Enter)).unwrap();
1698        settle(&mut p);
1699        assert_eq!(
1700            p.app.flash_message(),
1701            Some("Could not drill in; see the log")
1702        );
1703        assert!(!p.app.is_busy());
1704        assert!(p.app.status_message.is_none());
1705        assert!(!p.app.error_modal.active);
1706        assert!(!p.app.data_table_state.as_ref().unwrap().is_drilled_down());
1707
1708        rendered(&mut p.app);
1709        p.terminal_key(plain(KeyCode::Enter)).unwrap();
1710        settle(&mut p);
1711        assert!(
1712            p.app.data_table_state.as_ref().unwrap().is_drilled_down(),
1713            "the next one drills"
1714        );
1715    }
1716
1717    /// #455: a pivot whose worker dies says so and leaves nothing waiting on it — the
1718    /// form is not stuck computing — and the next pivot is installed.
1719    #[test]
1720    fn a_pivot_whose_worker_dies_is_shown_and_the_next_one_installs() {
1721        use crate::pivot_melt_modal::{PivotAggregation, PivotSpec};
1722        let dir = tempfile::tempdir().expect("temp dir");
1723        let path = dir.path().join("long.csv");
1724        std::fs::write(&path, "day,key,val\n1,a,10\n1,b,20\n2,a,30\n2,b,40\n").unwrap();
1725        let mut p = pump();
1726        p.send(AppEvent::Open(vec![path], OpenOptions::default()))
1727            .unwrap();
1728        settle(&mut p);
1729        rendered(&mut p.app);
1730        let spec = PivotSpec {
1731            index: vec!["day".to_string()],
1732            pivot_column: "key".to_string(),
1733            value_column: "val".to_string(),
1734            aggregation: PivotAggregation::Last,
1735            sort_columns: None,
1736        };
1737
1738        p.app.input_mode = InputMode::PivotMelt;
1739        p.app.jobs.worker_dies =
1740            crate::tests::worker_dies_once(|job| matches!(job, crate::Job::Pivot));
1741        p.send(AppEvent::Pivot(spec.clone())).unwrap();
1742        settle(&mut p);
1743        assert!(p.app.error_modal.active, "the user is told");
1744        assert!(!p.app.is_busy());
1745        assert!(!p.app.pivot_computing(), "the form is not left computing");
1746        assert_eq!(p.app.input_mode, InputMode::PivotMelt, "and keeps the spec");
1747        let state = p.app.data_table_state.as_ref().unwrap();
1748        assert!(state.last_pivot_spec().is_none(), "the table is as it was");
1749
1750        p.app.error_modal.hide();
1751        p.send(AppEvent::Pivot(spec)).unwrap();
1752        settle(&mut p);
1753        assert!(!p.app.error_modal.active, "{}", p.app.error_modal.message);
1754        let state = p.app.data_table_state.as_ref().unwrap();
1755        assert!(
1756            state.last_pivot_spec().is_some(),
1757            "the next pivot is installed"
1758        );
1759        assert_eq!(p.app.input_mode, InputMode::Normal);
1760    }
1761
1762    /// #455: failures from work an open has passed are dropped: the open goes on to its
1763    /// rows, with no error over them, whatever kind of job the failure names.
1764    #[test]
1765    fn stale_failures_leave_a_newer_open_alone() {
1766        let (mut p, dir) = loaded_pump();
1767        let passed = p.app.task_generation();
1768        let shown = p.app.dataset_generation;
1769        // An open long since replaced.
1770        let gone = crate::loading::LoadId::for_tests(u64::MAX);
1771        let jobs = [
1772            crate::Job::Load(gone),
1773            crate::Job::OpenNamed(gone),
1774            crate::Job::Rows(crate::InflightCollect::for_tests(0, 3)),
1775            crate::Job::Analysis(crate::jobs::AnalysisRun::default()),
1776            crate::Job::SampleRows,
1777            crate::Job::Pivot,
1778            crate::Job::DrillRow,
1779            crate::Job::InspectRow { frame: 0, row: 0 },
1780            crate::Job::InspectJson { token: 0 },
1781            crate::Job::Export,
1782            crate::Job::Copy,
1783            crate::Job::QualityReport,
1784        ];
1785        let running: Vec<_> = jobs
1786            .into_iter()
1787            .map(|job| p.app.job_for_tests(job, None))
1788            .collect();
1789        // The open's first phase waits until the failures are in, so it is under way
1790        // when they land.
1791        let (waits, release) = crate::tests::worker_waits_once(|job| {
1792            matches!(job, crate::Job::OpenNamed(_) | crate::Job::Load(_))
1793        });
1794        p.app.jobs.worker_waits = waits;
1795        p.send(AppEvent::Open(
1796            vec![dir.path().join("people.csv")],
1797            OpenOptions::default(),
1798        ))
1799        .unwrap();
1800        assert!(matches!(p.drain().unwrap(), Drained::Continue { .. }));
1801        assert!(p.app.awaiting_dataset(), "the open is under way");
1802        assert_ne!(p.app.task_generation(), passed);
1803        for job in running {
1804            job.end(crate::Outcome::Failed {
1805                message: "from work long gone".to_string(),
1806                panicked: true,
1807            });
1808        }
1809        release.send(()).unwrap();
1810        settle(&mut p);
1811        assert!(!p.app.error_modal.active, "{}", p.app.error_modal.message);
1812        assert_ne!(p.app.dataset_generation, shown, "the open finished");
1813        assert!(!p.app.awaiting_dataset());
1814        assert!(p.app.last_load_error.is_none());
1815        assert!(p.app.nothing_loading());
1816    }
1817
1818    /// #455: an open whose schema read dies — its second worker, after the scan —
1819    /// holds the keys and the generation in every frame drawn on the way, then ends
1820    /// where a failed open ends: the reason shown, the dataset before it still up.
1821    /// Opening again works.
1822    #[test]
1823    fn an_open_whose_schema_read_dies_ends_and_the_next_one_opens() {
1824        let (mut p, dir) = loaded_pump();
1825        let shown = p.app.dataset_generation;
1826        let path = dir.path().join("people.csv");
1827        // The scan is the first load job; the schema read is the second.
1828        let mut loads = 0;
1829        p.app.jobs.worker_dies = Some(Box::new(move |job| {
1830            loads += usize::from(matches!(job, crate::Job::Load(_)));
1831            loads == 2
1832        }));
1833        // The first phase waits for the first frame, so the open is seen under way.
1834        let (waits, release) = crate::tests::worker_waits_once(|job| {
1835            matches!(job, crate::Job::OpenNamed(_) | crate::Job::Load(_))
1836        });
1837        p.app.jobs.worker_waits = waits;
1838        let mut release = Some(release);
1839        p.send(AppEvent::Open(vec![path.clone()], OpenOptions::default()))
1840            .unwrap();
1841        let mut frames = 0;
1842        let deadline = std::time::Instant::now() + Duration::from_secs(300);
1843        while !p.app.error_modal.active {
1844            assert!(std::time::Instant::now() < deadline, "the open never ended");
1845            p.wait_and_drain(Duration::from_millis(50)).unwrap();
1846            if let Some(release) = release.take() {
1847                release.send(()).unwrap();
1848            }
1849            if p.app.awaiting_dataset() {
1850                frames += 1;
1851                assert!(p.app.is_busy(), "busy at frame {frames}");
1852                assert!(p.app.work_a_bump_would_strand(), "held at frame {frames}");
1853            }
1854        }
1855        assert!(frames >= 1, "the open was under way between frames");
1856        assert!(
1857            p.app.error_modal.message.contains("worker died"),
1858            "{}",
1859            p.app.error_modal.message
1860        );
1861        settle(&mut p);
1862        assert!(!p.app.is_busy());
1863        assert!(!p.app.awaiting_dataset(), "nothing is waited on");
1864        assert!(p.app.nothing_loading());
1865        assert_eq!(
1866            p.app.dataset_generation, shown,
1867            "the dataset before it stays"
1868        );
1869
1870        p.terminal_key(plain(KeyCode::Esc)).unwrap();
1871        p.send(AppEvent::Open(vec![path], OpenOptions::default()))
1872            .unwrap();
1873        settle(&mut p);
1874        assert!(!p.app.error_modal.active, "{}", p.app.error_modal.message);
1875        assert_ne!(p.app.dataset_generation, shown, "the next open opens");
1876    }
1877
1878    /// A deferred collect that turns out to have nothing to do still ends an open's
1879    /// wait for its first rows.
1880    ///
1881    /// An open's first rows are owed rather than read when other work holds the
1882    /// generation as the dataset installs. The retry that runs then finds the buffer
1883    /// already serves the view; left to the open alone, the bar read "Loading
1884    /// buffer... 70%" with the app idle, for the rest of the session.
1885    #[test]
1886    fn a_deferred_collect_with_nothing_to_do_takes_the_loading_screen_down() {
1887        let (mut p, _dir) = loaded_pump();
1888        // The buffer already holds every row, so the collect will find nothing to do.
1889        p.app.owe_rows_for_tests("Loading buffer...");
1890        p.app.loading.first_rows_for_tests();
1891        p.app.busy = true;
1892
1893        p.send(AppEvent::Update).unwrap();
1894        assert!(matches!(p.drain().unwrap(), Drained::Continue { .. }));
1895
1896        assert!(!p.app.rows_owed(), "the errand is done either way");
1897        assert!(
1898            p.app.nothing_loading(),
1899            "and the loading screen is down rather than stuck at 70%"
1900        );
1901        assert!(!p.app.is_busy(), "with the keyboard back");
1902    }
1903
1904    /// While rows are being read, keys that only redraw the table act at once (#646):
1905    /// `#` toggles row numbers and `j` moves inside the rows held. Busy with anything
1906    /// else, `j` waits in the order typed.
1907    #[test]
1908    fn view_keys_act_while_rows_load() {
1909        let (mut p, _dir) = loaded_pump();
1910        p.app.owe_rows_for_tests("Loading buffer...");
1911        assert!(p.app.is_busy());
1912        let numbered = p.app.data_table_state.as_ref().unwrap().row_numbers();
1913
1914        p.terminal_key(plain(KeyCode::Char('#'))).unwrap();
1915        assert_ne!(
1916            p.app.data_table_state.as_ref().unwrap().row_numbers(),
1917            numbered,
1918            "# toggles at once"
1919        );
1920        let row = p.app.data_table_state.as_ref().unwrap().cursor_row();
1921        p.terminal_key(plain(KeyCode::Char('j'))).unwrap();
1922        assert_eq!(
1923            p.app.data_table_state.as_ref().unwrap().cursor_row(),
1924            row + 1,
1925            "j inside the rows held moves at once"
1926        );
1927        assert!(p.held.is_empty(), "nothing waits");
1928
1929        // Busy with more than rows, a key waits, and the j typed after it waits
1930        // behind it.
1931        p.app.busy = true;
1932        p.terminal_key(plain(KeyCode::Char('s'))).unwrap();
1933        p.terminal_key(plain(KeyCode::Char('j'))).unwrap();
1934        assert_eq!(p.held.len(), 2);
1935        assert_eq!(
1936            p.app.data_table_state.as_ref().unwrap().cursor_row(),
1937            row + 1
1938        );
1939    }
1940
1941    /// The app hands a key it cannot act on back to the caller rather than dropping
1942    /// it; `event()`, for callers with nowhere to hold one, drops it as before.
1943    #[test]
1944    fn a_key_while_busy_comes_back_deferred() {
1945        let mut p = pump();
1946        p.app.busy = true;
1947        assert!(matches!(
1948            p.app.handle(&AppEvent::Key(plain(KeyCode::Char('j')))),
1949            Err(k) if k.code == KeyCode::Char('j')
1950        ));
1951        assert!(
1952            p.app
1953                .event(&AppEvent::Key(plain(KeyCode::Char('j'))))
1954                .is_none()
1955        );
1956        p.app.busy = false;
1957        assert!(matches!(
1958            p.app.handle(&AppEvent::Key(plain(KeyCode::Char('j')))),
1959            Ok(None)
1960        ));
1961    }
1962
1963    /// Typed at a spinner, `/hello` opens the query bar with "hello" in it once the
1964    /// work is done. Nothing in it acts early: the `h` and `l` do not scroll a column
1965    /// and the `q` in `/query` does not quit.
1966    #[test]
1967    fn a_query_typed_while_busy_lands_in_the_query_bar() {
1968        let (mut p, _dir) = loaded_pump();
1969        p.app.busy = true;
1970        type_keys(&mut p, ":hello");
1971        assert_eq!(
1972            p.app.input_mode,
1973            InputMode::Normal,
1974            "nothing acts while busy"
1975        );
1976        assert_eq!(held(&p).len(), 6);
1977        assert!(matches!(p.drain().unwrap(), Drained::Continue { .. }));
1978
1979        p.app.busy = false;
1980        assert!(matches!(settle(&mut p), Drained::Continue { .. }));
1981        assert_eq!(p.app.input_mode, InputMode::Editing);
1982        assert_eq!(p.app.query_input.value(), "hello");
1983
1984        p.app.busy = true;
1985        type_keys(&mut p, "query");
1986        p.app.busy = false;
1987        assert!(
1988            matches!(settle(&mut p), Drained::Continue { .. }),
1989            "q did not quit"
1990        );
1991        assert_eq!(p.app.query_input.value(), "helloquery");
1992    }
1993
1994    /// Ctrl+T typed at a spinner waits its turn like the letters around it: the
1995    /// prompt opens, switches mode, and the text lands in the mode switched to. A
1996    /// build without SQL has one language, so the chord leaves the line on q.
1997    #[test]
1998    fn a_mode_chord_typed_while_busy_switches_before_the_text() {
1999        let (mut p, _dir) = loaded_pump();
2000        p.app.busy = true;
2001        type_keys(&mut p, ":");
2002        p.terminal_key(ctrl('t')).unwrap();
2003        type_keys(&mut p, "ada");
2004        assert_eq!(held(&p).len(), 5, "the chord is held with the text");
2005        p.app.busy = false;
2006        settle(&mut p);
2007
2008        let mode = crate::QueryMode::Q.next();
2009        assert_eq!(p.app.query_prompt_mode(), Some(mode));
2010        let typed = match mode {
2011            crate::QueryMode::Sql => &p.app.sql_input,
2012            crate::QueryMode::Q => &p.app.query_input,
2013        };
2014        assert_eq!(typed.value(), "ada");
2015        if mode != crate::QueryMode::Q {
2016            assert_eq!(p.app.query_input.value(), "");
2017        }
2018    }
2019
2020    /// A key that arrives behind the event that ended the busy state is handled after
2021    /// the keys typed before it, not before them.
2022    #[test]
2023    fn a_fresh_key_waits_behind_the_held_ones() {
2024        let (mut p, _dir) = loaded_pump();
2025        p.app.busy = true;
2026        type_keys(&mut p, ":abc");
2027        // The busy-clearing event was handled; the next key read is still behind.
2028        p.app.busy = false;
2029        type_keys(&mut p, "d");
2030        assert_eq!(p.app.input_mode, InputMode::Normal, "d waited its turn");
2031
2032        settle(&mut p);
2033        assert_eq!(p.app.query_input.value(), "abcd");
2034    }
2035
2036    /// A held Enter runs its search before the key typed after it is offered: the G
2037    /// stays held while the search collects, then lands on the last row of the result.
2038    #[test]
2039    fn a_held_enter_finishes_its_search_before_the_next_key() {
2040        let (mut p, _dir) = loaded_pump();
2041        p.app.busy = true;
2042        type_keys(&mut p, ":select name where age > 40");
2043        p.terminal_key(plain(KeyCode::Enter)).unwrap();
2044        p.terminal_key(plain(KeyCode::Char('G'))).unwrap();
2045        p.app.busy = false;
2046
2047        // Replay up to and including the Enter; the Search it asks for is on the channel.
2048        while held(&p).len() > 1 {
2049            assert!(p.replay_one().unwrap());
2050            if held(&p).len() > 1 {
2051                assert!(matches!(p.drain().unwrap(), Drained::Continue { .. }));
2052            }
2053        }
2054        assert_eq!(
2055            p.app.input_mode,
2056            InputMode::Editing,
2057            "the search has not run"
2058        );
2059        assert_eq!(held(&p), vec![KeyCode::Char('G')], "G waits for it");
2060
2061        // The drain runs the Search (and whatever it spawns) with G still held. The
2062        // prompt stays up until the result's rows are in.
2063        assert!(matches!(p.drain().unwrap(), Drained::Continue { .. }));
2064        assert_eq!(
2065            p.app.data_table_state.as_ref().unwrap().get_active_query(),
2066            "select name where age > 40"
2067        );
2068        assert_eq!(
2069            held(&p),
2070            vec![KeyCode::Char('G')],
2071            "G is offered only after"
2072        );
2073
2074        settle(&mut p);
2075        assert_eq!(p.app.input_mode, InputMode::Normal);
2076        let state = p.app.data_table_state.as_ref().unwrap();
2077        assert_eq!(state.get_active_query(), "select name where age > 40");
2078        assert_eq!(state.num_rows(), 2);
2079        assert_eq!(
2080            state.table_state.selected(),
2081            Some(1),
2082            "G went to the last row of the result"
2083        );
2084    }
2085
2086    /// Past the cap the newest key is dropped and the user told; the oldest, which may
2087    /// be the `/` the rest were typed into, is never evicted.
2088    #[test]
2089    fn the_cap_drops_the_newest_key_and_says_so() {
2090        let mut p = pump();
2091        p.app.busy = true;
2092        p.app.status_message = Some("Loading buffer...".to_string());
2093        type_keys(&mut p, "/");
2094        type_keys(&mut p, &"a".repeat(39));
2095        assert_eq!(held(&p).len(), MAX_HELD_KEYS);
2096        assert_eq!(held(&p)[0], KeyCode::Char('/'));
2097        assert!(p.app.input_dropped);
2098        assert!(rendered(&mut p.app).contains("input dropped while busy"));
2099
2100        p.app.busy = false;
2101        settle(&mut p);
2102        assert!(held(&p).is_empty());
2103        assert!(
2104            !p.app.input_dropped,
2105            "the notice goes with the last held key"
2106        );
2107    }
2108
2109    /// A held navigation key repeats fast; the repeats become one press, so releasing
2110    /// PageDown after a collect moves one page rather than thirty-two.
2111    #[test]
2112    fn a_held_navigation_key_is_one_press() {
2113        let mut p = pump();
2114        p.app.busy = true;
2115        for _ in 0..50 {
2116            p.terminal_key(plain(KeyCode::PageDown)).unwrap();
2117        }
2118        assert_eq!(held(&p), vec![KeyCode::PageDown]);
2119        for _ in 0..10 {
2120            p.terminal_key(plain(KeyCode::Char('j'))).unwrap();
2121        }
2122        assert_eq!(held(&p), vec![KeyCode::PageDown, KeyCode::Char('j')]);
2123        // Repeated text is not navigation and is kept.
2124        type_keys(&mut p, "aa");
2125        assert_eq!(held(&p).len(), 4);
2126    }
2127
2128    /// Work that ends by opening a modal from a background result drops the keys typed
2129    /// before it: a held key was not an answer to a message the user has not seen.
2130    #[test]
2131    fn keys_held_before_an_error_modal_appears_are_dropped() {
2132        let mut p = pump();
2133        p.app.busy = true;
2134        // A typed sequence (starts with `/`, so it is held, not dropped).
2135        type_keys(&mut p, "/x");
2136        assert_eq!(held(&p).len(), 2);
2137
2138        let analysis = p.app.job_for_tests(
2139            crate::Job::Analysis(crate::jobs::AnalysisRun::default()),
2140            Some("Running analysis..."),
2141        );
2142        analysis.end(crate::Outcome::Failed {
2143            message: "disk on fire".to_string(),
2144            panicked: false,
2145        });
2146        assert!(matches!(p.drain().unwrap(), Drained::Continue { .. }));
2147        assert!(!p.app.is_busy());
2148        assert!(held(&p).is_empty());
2149        settle(&mut p);
2150        assert!(p.app.error_modal.active, "the error is still on screen");
2151    }
2152
2153    /// A statement that fails while running puts its reason under it in the prompt.
2154    /// Keys typed while it ran were not answers to that, and are not typed into it.
2155    #[cfg(feature = "sql")]
2156    #[test]
2157    fn keys_held_while_a_statement_runs_are_dropped_when_it_fails() {
2158        let (mut p, _dir) = loaded_pump();
2159        p.app.app_config.query.default_mode = crate::QueryMode::Sql;
2160        p.terminal_key(plain(KeyCode::Char(':'))).unwrap();
2161        let sql = "SELECT CAST(name AS INT) AS n FROM df";
2162        type_keys(&mut p, sql);
2163        // Its worker waits until the keys are typed, so it is still running then.
2164        let (waits, release) =
2165            crate::tests::worker_waits_once(|job| matches!(job, crate::Job::Rows(_)));
2166        p.app.jobs.worker_waits = waits;
2167        p.terminal_key(plain(KeyCode::Enter)).unwrap();
2168        // The statement starts running on the next pass over the channel.
2169        p.drain().unwrap();
2170        assert!(p.app.is_busy());
2171        type_keys(&mut p, "jj");
2172        assert_eq!(held(&p).len(), 2);
2173
2174        release.send(()).unwrap();
2175        settle(&mut p);
2176        assert!(held(&p).is_empty());
2177        assert!(!p.app.error_modal.active);
2178        assert_eq!(p.app.query_prompt_mode(), Some(crate::QueryMode::Sql));
2179        assert_eq!(p.app.query_prompt_text(), Some(sql));
2180        assert!(p.app.query_prompt_error().is_some());
2181    }
2182
2183    // --- Fixes from the high-effort review of #162 --------------------------------
2184
2185    /// Ctrl-C quits from a focused text field too (#649), as Ctrl-Q always has; the
2186    /// field copies with Alt+W instead.
2187    #[test]
2188    fn ctrl_c_in_the_query_bar_quits() {
2189        let (mut p, _dir) = loaded_pump();
2190        p.terminal_key(plain(KeyCode::Char(':'))).unwrap();
2191        assert_eq!(p.app.input_mode, InputMode::Editing);
2192        assert!(matches!(
2193            p.app.handle(&AppEvent::Key(ctrl('c'))),
2194            Ok(Some(AppEvent::Exit))
2195        ));
2196        let alt_w = KeyEvent::new(KeyCode::Char('w'), KeyModifiers::ALT);
2197        assert!(!matches!(
2198            p.app.handle(&AppEvent::Key(alt_w)),
2199            Ok(Some(AppEvent::Exit))
2200        ));
2201        assert_eq!(
2202            p.app.input_mode,
2203            InputMode::Editing,
2204            "Alt+W stays in the field"
2205        );
2206    }
2207
2208    /// Ctrl-C quits from every search box and modal (#649): the Sort tab's column
2209    /// search, the chart view's open column Picker, and the plain table.
2210    #[test]
2211    fn ctrl_c_quits_from_the_sort_and_chart_search_boxes() {
2212        use crate::chart_modal::ChartFocus;
2213        use crate::sort_filter_modal::{SortFilterField, SortFilterTab};
2214
2215        let (mut p, _dir) = loaded_pump();
2216        p.app.input_mode = InputMode::SortFilter;
2217        p.app.sort_filter_modal.active = true;
2218        p.app.sort_filter_modal.active_tab = SortFilterTab::Columns;
2219        p.app.sort_filter_modal.focus = SortFilterField::Find;
2220        assert!(
2221            p.app.text_field_focused(),
2222            "the sort search is a text field"
2223        );
2224        assert!(matches!(
2225            p.app.handle(&AppEvent::Key(ctrl('c'))),
2226            Ok(Some(AppEvent::Exit))
2227        ));
2228
2229        let (mut p2, _d) = loaded_pump();
2230        p2.app.input_mode = InputMode::Chart;
2231        p2.app.chart_modal.active = true;
2232        p2.app.chart_modal.open(
2233            crate::chart_modal::ChartColumns {
2234                numeric: &["a".to_string(), "b".to_string()],
2235                ..Default::default()
2236            },
2237            None,
2238            None,
2239            false,
2240            0,
2241        );
2242        p2.app.chart_modal.focus = ChartFocus::X;
2243        p2.app.chart_modal.open_picker();
2244        assert!(
2245            p2.app.text_field_focused(),
2246            "the open Picker narrows by typing"
2247        );
2248        assert!(matches!(
2249            p2.app.handle(&AppEvent::Key(ctrl('c'))),
2250            Ok(Some(AppEvent::Exit))
2251        ));
2252
2253        let (mut p3, _d3) = loaded_pump();
2254        assert!(matches!(
2255            p3.app.handle(&AppEvent::Key(ctrl('c'))),
2256            Ok(Some(AppEvent::Exit))
2257        ));
2258    }
2259
2260    /// Item 2: a `q` at the startup spinner (busy, plain table view, nothing held) quits
2261    /// at once; a typed `/query` is still held and typed.
2262    #[test]
2263    fn q_at_the_startup_spinner_quits_but_slash_query_types() {
2264        let mut p = pump();
2265        p.app.busy = true;
2266        assert!(p.app.in_normal_table_view());
2267        p.terminal_key(plain(KeyCode::Char('q'))).unwrap();
2268        assert!(
2269            matches!(p.drain().unwrap(), Drained::Exit),
2270            "q quits at once"
2271        );
2272
2273        let mut p2 = pump();
2274        p2.app.busy = true;
2275        type_keys(&mut p2, "/query");
2276        assert_eq!(held(&p2).len(), 6, "the q in /query is held, not a quit");
2277    }
2278
2279    /// Item 3: coalescing never eats a doubled letter in typed text, but does collapse a
2280    /// held navigation key in the plain table view.
2281    #[test]
2282    fn doubled_letters_survive_but_navigation_coalesces() {
2283        let mut p = pump();
2284        p.app.busy = true;
2285        type_keys(&mut p, "/bookkeeper");
2286        let typed: String = held(&p)
2287            .iter()
2288            .filter_map(|c| match c {
2289                KeyCode::Char(ch) => Some(*ch),
2290                _ => None,
2291            })
2292            .collect();
2293        assert_eq!(typed, "/bookkeeper", "both k's, o's and e's survive");
2294
2295        let mut p2 = pump();
2296        p2.app.busy = true;
2297        type_keys(&mut p2, "jj");
2298        assert_eq!(
2299            held(&p2),
2300            vec![KeyCode::Char('j')],
2301            "a doubled navigation key collapses"
2302        );
2303    }
2304
2305    /// Item 4: a hard escape typed during the replay window (idle, keys still held) acts
2306    /// at once rather than queueing behind the held keys; a non-escape waits.
2307    #[test]
2308    fn escapes_act_during_the_replay_window() {
2309        let (mut p, _dir) = loaded_pump();
2310        p.app.busy = true;
2311        type_keys(&mut p, "/foo");
2312        p.app.busy = false; // replay window: idle with keys still held
2313
2314        assert!(p.terminal_key(ctrl('o')).unwrap(), "Ctrl-O acts now");
2315        assert_eq!(p.app.input_mode, InputMode::Home);
2316        assert!(held(&p).is_empty(), "going home cleared the held keys");
2317
2318        let (mut p2, _d) = loaded_pump();
2319        p2.app.busy = true;
2320        type_keys(&mut p2, "/foo");
2321        p2.app.busy = false;
2322        assert!(
2323            !p2.terminal_key(plain(KeyCode::Char('x'))).unwrap(),
2324            "a fresh non-escape key waits behind the held ones"
2325        );
2326        assert_eq!(held(&p2).last().copied(), Some(KeyCode::Char('x')));
2327    }
2328
2329    /// Item 5: a replayed Enter's Search runs before a key typed in the same moment. The
2330    /// run loop drains the channel after a replay, so the fresh key finds the app busy
2331    /// and waits; it then acts on the search's result.
2332    ///
2333    /// The search's rows are held on their worker until G is typed (#568): a result
2334    /// back within the drain would leave nothing running for G to wait on.
2335    #[test]
2336    fn a_replayed_search_runs_before_a_fresh_key() {
2337        let (mut p, _dir) = loaded_pump();
2338        p.app.busy = true;
2339        type_keys(&mut p, ":select name where age > 40");
2340        p.terminal_key(plain(KeyCode::Enter)).unwrap();
2341        p.app.busy = false;
2342        let (waits, release) =
2343            crate::tests::worker_waits_once(|job| matches!(job, crate::Job::Rows(_)));
2344        p.app.jobs.worker_waits = waits;
2345
2346        // Replay one key, then drain, exactly as the run loop does before polling.
2347        loop {
2348            let replayed = p.replay_one().unwrap();
2349            assert!(matches!(p.drain().unwrap(), Drained::Continue { .. }));
2350            if p.app.is_busy() {
2351                break;
2352            }
2353            assert!(replayed, "should still be replaying the query");
2354        }
2355        assert!(held(&p).is_empty(), "the whole query replayed");
2356        // The Search is running: a key typed now must wait for it.
2357        assert!(
2358            !p.terminal_key(plain(KeyCode::Char('G'))).unwrap(),
2359            "G waits behind the running search"
2360        );
2361        assert_eq!(held(&p), [KeyCode::Char('G')]);
2362        assert!(matches!(p.drain().unwrap(), Drained::Continue { .. }));
2363        assert!(!p.replay_one().unwrap(), "G is not replayed while it runs");
2364
2365        release.send(()).unwrap();
2366        settle(&mut p);
2367        assert_eq!(p.app.input_mode, InputMode::Normal);
2368        let state = p.app.data_table_state.as_ref().unwrap();
2369        assert_eq!(state.get_active_query(), "select name where age > 40");
2370        assert_eq!(state.num_rows(), 2);
2371        assert_eq!(
2372            state.table_state.selected(),
2373            Some(1),
2374            "G ran after the search, on its result"
2375        );
2376    }
2377
2378    /// Item 6: a bare Enter or Esc at a busy table confirms nothing and is dropped; the
2379    /// Enter that submits a typed query is held because `/` is queued ahead of it.
2380    #[test]
2381    fn a_bare_enter_or_esc_at_a_busy_table_is_dropped() {
2382        let mut p = pump();
2383        p.app.busy = true;
2384        assert!(!p.terminal_key(plain(KeyCode::Enter)).unwrap());
2385        assert!(!p.terminal_key(plain(KeyCode::Esc)).unwrap());
2386        assert!(held(&p).is_empty(), "neither is queued");
2387
2388        let (mut p2, _d) = loaded_pump();
2389        p2.app.busy = true;
2390        type_keys(&mut p2, "/x");
2391        p2.terminal_key(plain(KeyCode::Enter)).unwrap();
2392        assert_eq!(
2393            held(&p2).last().copied(),
2394            Some(KeyCode::Enter),
2395            "the query's Enter is held"
2396        );
2397    }
2398
2399    /// `H` / `L` move a column and `+` / `-` filter on the cell: both change the
2400    /// view, so at a busy table they are held, and replay in order once it is idle.
2401    #[test]
2402    fn column_moves_and_quick_filters_wait_while_busy() {
2403        let (mut p, _dir) = loaded_pump();
2404        p.app.busy = true;
2405        for c in ['L', '+', 'H', '-'] {
2406            assert!(!p.app.key_acts_while_busy(&plain(KeyCode::Char(c))), "{c}");
2407        }
2408        type_keys(&mut p, "L+");
2409        assert_eq!(held(&p), [KeyCode::Char('L'), KeyCode::Char('+')]);
2410        p.app.busy = false;
2411        settle(&mut p);
2412        rendered(&mut p.app);
2413        assert!(held(&p).is_empty());
2414        let state = p.app.data_table_state.as_ref().unwrap();
2415        assert_eq!(state.headers(), ["age", "name"], "L moved name right");
2416        assert_eq!(
2417            state.current_column(),
2418            Some("name"),
2419            "the cursor went with it"
2420        );
2421        let filters = state.view_filters();
2422        assert_eq!(filters.len(), 1);
2423        assert_eq!(
2424            (filters[0].column.as_str(), filters[0].value.as_str()),
2425            ("name", "ada"),
2426            "+ filtered on the cell the cursor reached"
2427        );
2428    }
2429
2430    /// Enter with nothing to drill into is Space at a busy table too: held, as Space,
2431    /// and replayed into the inspector. Where it would drill, a bare Enter is dropped.
2432    #[test]
2433    fn enter_with_nothing_to_drill_into_waits_as_space() {
2434        let (mut p, _dir) = loaded_pump();
2435        p.app.busy = true;
2436        p.terminal_key(plain(KeyCode::Char('j'))).unwrap();
2437        p.terminal_key(plain(KeyCode::Enter)).unwrap();
2438        assert_eq!(held(&p), [KeyCode::Char('j'), KeyCode::Char(' ')]);
2439        p.app.busy = false;
2440        settle(&mut p);
2441        assert!(held(&p).is_empty());
2442        assert_eq!(p.app.input_mode, InputMode::Inspect);
2443        let state = p.app.data_table_state.as_ref().unwrap();
2444        assert_eq!(state.table_state.selected(), Some(1), "j moved first");
2445        p.terminal_key(plain(KeyCode::Esc)).unwrap();
2446        assert_eq!(p.app.input_mode, InputMode::Normal);
2447
2448        p.send(AppEvent::QQuery("select n: count age by name".to_string()))
2449            .unwrap();
2450        settle(&mut p);
2451        rendered(&mut p.app);
2452        assert!(p.app.data_table_state.as_ref().unwrap().can_drill_down());
2453        p.app.busy = true;
2454        assert!(!p.terminal_key(plain(KeyCode::Enter)).unwrap());
2455        assert!(held(&p).is_empty(), "the drilling Enter is dropped");
2456    }
2457
2458    /// A table loaded with a List column is not a group: Enter typed ahead while busy
2459    /// is held as Space, and replays into the inspector rather than a drill.
2460    #[test]
2461    fn enter_over_a_loaded_list_column_waits_as_space() {
2462        use polars::prelude::{IntoLazy, ParquetWriter, col, df};
2463        let dir = tempfile::tempdir().expect("temp dir");
2464        let path = dir.path().join("lists.parquet");
2465        let mut frame = df!("k" => &[1i64, 1, 2], "v" => &[10i64, 11, 12])
2466            .unwrap()
2467            .lazy()
2468            .group_by([col("k")])
2469            .agg([col("v")])
2470            .collect()
2471            .unwrap();
2472        ParquetWriter::new(std::fs::File::create(&path).unwrap())
2473            .finish(&mut frame)
2474            .unwrap();
2475
2476        let mut p = pump();
2477        p.send(AppEvent::Open(vec![path], OpenOptions::default()))
2478            .unwrap();
2479        settle(&mut p);
2480        rendered(&mut p.app);
2481        assert!(!p.app.data_table_state.as_ref().unwrap().can_drill_down());
2482        p.app.busy = true;
2483        p.terminal_key(plain(KeyCode::Char('j'))).unwrap();
2484        p.terminal_key(plain(KeyCode::Enter)).unwrap();
2485        assert_eq!(held(&p), [KeyCode::Char('j'), KeyCode::Char(' ')]);
2486        p.app.busy = false;
2487        settle(&mut p);
2488        assert_eq!(p.app.input_mode, InputMode::Inspect);
2489        assert!(!p.app.data_table_state.as_ref().unwrap().is_drilled_down());
2490    }
2491
2492    /// Item 7: the column cursor acts live at a busy table rather than queueing, and a
2493    /// held column cursor key is kept, each one, as it would act idle: it reads
2494    /// nothing, so a burst costs nothing, and a per-column key held behind it acts on
2495    /// the column the cursor reached.
2496    #[test]
2497    fn column_cursor_acts_live_and_held_keys_all_replay() {
2498        let cursor = |p: &EventPump| {
2499            p.app
2500                .data_table_state
2501                .as_ref()
2502                .unwrap()
2503                .current_column_index()
2504                .unwrap()
2505        };
2506        let (mut p, _dir) = loaded_pump();
2507        p.app.busy = true;
2508        let before = cursor(&p);
2509        assert!(
2510            p.terminal_key(plain(KeyCode::Right)).unwrap(),
2511            "Right moves the cursor live"
2512        );
2513        assert!(held(&p).is_empty(), "it did not queue");
2514        assert_eq!(cursor(&p), before + 1);
2515
2516        let (mut p2, _d) = wide_pump();
2517        p2.app.busy = true;
2518        // A non-actor navigation key is held first, so the keys behind it queue.
2519        p2.terminal_key(plain(KeyCode::Char('k'))).unwrap();
2520        p2.terminal_key(plain(KeyCode::Char('k'))).unwrap();
2521        for _ in 0..2 {
2522            p2.terminal_key(plain(KeyCode::Char('l'))).unwrap();
2523        }
2524        p2.terminal_key(plain(KeyCode::Char('h'))).unwrap();
2525        p2.terminal_key(plain(KeyCode::Char('l'))).unwrap();
2526        assert_eq!(
2527            held(&p2),
2528            vec![
2529                KeyCode::Char('k'),
2530                KeyCode::Char('l'),
2531                KeyCode::Char('l'),
2532                KeyCode::Char('h'),
2533                KeyCode::Char('l')
2534            ],
2535            "the held k's collapse; every column key stays"
2536        );
2537        let before = cursor(&p2);
2538        p2.app.busy = false;
2539        while p2.replay_one().unwrap() {}
2540        assert_eq!(cursor(&p2), before + 2, "replayed in order, as typed");
2541    }
2542
2543    /// A pump with a CSV of sixty narrow columns loaded and drawn at 100 wide: three
2544    /// pages sideways.
2545    fn wide_pump() -> (EventPump, tempfile::TempDir) {
2546        let dir = tempfile::tempdir().expect("temp dir");
2547        let path = dir.path().join("wide.csv");
2548        let mut file = std::fs::File::create(&path).expect("create csv");
2549        let names: Vec<String> = (0..60).map(|i| format!("c{i:02}")).collect();
2550        writeln!(file, "{}", names.join(",")).expect("write csv");
2551        for row in 0..5 {
2552            let values: Vec<String> = (0..60).map(|i| format!("{}", row * 100 + i)).collect();
2553            writeln!(file, "{}", values.join(",")).expect("write csv");
2554        }
2555        drop(file);
2556        let mut pump = pump();
2557        pump.send(AppEvent::Open(vec![path], OpenOptions::default()))
2558            .unwrap();
2559        settle(&mut pump);
2560        rendered(&mut pump.app);
2561        (pump, dir)
2562    }
2563
2564    fn first_scrolled(pump: &EventPump) -> usize {
2565        pump.app.data_table_state.as_ref().unwrap().termcol_index
2566    }
2567
2568    /// Column paging acts at a busy table at once, as the column cursor does; behind a
2569    /// held key it waits, and it replays in order.
2570    #[test]
2571    fn column_paging_acts_live_and_replays_in_order() {
2572        let (mut p, _dir) = wide_pump();
2573        let shift_right = KeyEvent::new(KeyCode::Right, KeyModifiers::SHIFT);
2574        let shift_left = KeyEvent::new(KeyCode::Left, KeyModifiers::SHIFT);
2575        p.app.busy = true;
2576        assert!(p.terminal_key(shift_right).unwrap());
2577        assert!(held(&p).is_empty(), "Shift+→ did not queue");
2578        rendered(&mut p.app);
2579        let page = first_scrolled(&p);
2580        assert!(page > 1, "a page moves more than a column: {page}");
2581        assert!(p.terminal_key(plain(KeyCode::Char('}'))).unwrap());
2582        rendered(&mut p.app);
2583        let last_page = first_scrolled(&p);
2584        assert!(last_page > page && last_page < 59, "{page} {last_page}");
2585        assert!(p.terminal_key(plain(KeyCode::Char('{'))).unwrap());
2586        assert_eq!(first_scrolled(&p), 0);
2587        assert!(p.terminal_key(shift_right).unwrap());
2588        assert_eq!(first_scrolled(&p), page);
2589        assert!(p.terminal_key(shift_left).unwrap());
2590        assert_eq!(first_scrolled(&p), 0);
2591
2592        // Behind a held key they wait, each one: they read nothing.
2593        p.terminal_key(plain(KeyCode::Char('k'))).unwrap();
2594        for _ in 0..2 {
2595            p.terminal_key(shift_right).unwrap();
2596        }
2597        p.terminal_key(plain(KeyCode::Char('}'))).unwrap();
2598        p.terminal_key(shift_left).unwrap();
2599        assert_eq!(
2600            held(&p),
2601            vec![
2602                KeyCode::Char('k'),
2603                KeyCode::Right,
2604                KeyCode::Right,
2605                KeyCode::Char('}'),
2606                KeyCode::Left,
2607            ]
2608        );
2609        assert_eq!(first_scrolled(&p), 0, "nothing acted yet");
2610        p.app.busy = false;
2611        settle(&mut p);
2612        assert!(held(&p).is_empty());
2613        rendered(&mut p.app);
2614        // Shift+→ then } reach the last page; Shift+← is the page before it.
2615        let before_last = first_scrolled(&p);
2616        assert!(
2617            before_last > 0 && before_last < last_page,
2618            "{before_last} before {last_page}"
2619        );
2620        // And the column picker, typed while busy, waits with what was typed into it.
2621        p.app.busy = true;
2622        for key in [
2623            KeyCode::Char('g'),
2624            KeyCode::Char('c'),
2625            KeyCode::Char('0'),
2626            KeyCode::Char('5'),
2627            KeyCode::Enter,
2628        ] {
2629            p.terminal_key(plain(key)).unwrap();
2630        }
2631        assert_eq!(held(&p).len(), 5, "g is not a view key: everything waits");
2632        p.app.busy = false;
2633        settle(&mut p);
2634        rendered(&mut p.app);
2635        assert_eq!(first_scrolled(&p), 5, "g c05 Enter shows c05 first");
2636        assert_eq!(p.app.input_mode, InputMode::Normal);
2637    }
2638
2639    /// Item 8: F1 and `?` open help during a long load, at once, with nothing held.
2640    #[test]
2641    fn help_opens_during_a_load() {
2642        let (mut p, _dir) = loaded_pump();
2643        p.app.busy = true;
2644        assert!(p.terminal_key(plain(KeyCode::F(1))).unwrap());
2645        assert!(p.app.help_visible(), "F1 opened help immediately");
2646        assert!(held(&p).is_empty());
2647
2648        let (mut p2, _d) = loaded_pump();
2649        p2.app.busy = true;
2650        assert!(p2.terminal_key(plain(KeyCode::Char('?'))).unwrap());
2651        assert!(p2.app.help_visible(), "? opened help immediately");
2652        assert!(held(&p2).is_empty());
2653    }
2654
2655    /// Item 9: a modal a replayed key opens itself (an overwrite prompt) does not discard
2656    /// the answer keys queued behind it, so held Enter, Left, Enter completes the export.
2657    #[test]
2658    fn a_prompt_a_replayed_key_opens_keeps_its_answer_keys() {
2659        let (mut p, dir) = loaded_pump();
2660        let path = dir.path().join("out.csv");
2661        std::fs::write(&path, "old").expect("seed an existing file");
2662
2663        // Stage the export modal on an existing path, focused on the path field.
2664        p.app.export_modal.active = true;
2665        p.app.export_modal.selected_format = ExportFormat::Csv;
2666        p.app.export_modal.focus = ExportFocus::PathInput;
2667        p.app
2668            .export_modal
2669            .path_input
2670            .set_value(path.display().to_string());
2671        p.app.input_mode = InputMode::Export;
2672
2673        // Keys typed while busy in Export mode are all held (not a plain table view).
2674        p.app.busy = true;
2675        p.terminal_key(plain(KeyCode::Enter)).unwrap();
2676        p.terminal_key(plain(KeyCode::Left)).unwrap();
2677        p.terminal_key(plain(KeyCode::Enter)).unwrap();
2678        assert_eq!(held(&p).len(), 3);
2679        p.app.busy = false;
2680
2681        // The first replayed Enter opens the overwrite confirmation.
2682        assert!(p.replay_one().unwrap());
2683        assert!(
2684            p.app.confirmation_modal.active,
2685            "the overwrite prompt is up"
2686        );
2687        assert_eq!(
2688            held(&p),
2689            vec![KeyCode::Left, KeyCode::Enter],
2690            "the answer keys were not discarded by the prompt"
2691        );
2692
2693        settle(&mut p);
2694        assert!(
2695            p.app
2696                .flash
2697                .as_ref()
2698                .is_some_and(|f| f.message.starts_with("Exported to ")),
2699            "the held Left+Enter answered the prompt and the export ran"
2700        );
2701        assert!(
2702            std::fs::read(&path).unwrap().len() > 3,
2703            "the file was overwritten with exported data"
2704        );
2705
2706        // The next key clears the flash: the bar's line about the last action
2707        // is stale the moment another key does something.
2708        p.terminal_key(plain(KeyCode::Down)).unwrap();
2709        assert!(p.app.flash.is_none(), "a keypress clears the flash");
2710    }
2711
2712    /// Going home mid-load drops the keys typed at the load: replayed into the home
2713    /// screen they could open a dataset nobody asked for.
2714    #[test]
2715    fn ctrl_o_during_a_load_drops_the_held_keys() {
2716        let mut p = pump();
2717        p.app.busy = true;
2718        p.app.loading.first_rows_for_tests();
2719        p.terminal_key(plain(KeyCode::Char('j'))).unwrap();
2720        p.terminal_key(plain(KeyCode::Enter)).unwrap();
2721        assert_eq!(held(&p).len(), 2);
2722
2723        p.terminal_key(ctrl('o')).unwrap();
2724        assert_eq!(p.app.input_mode, InputMode::Home);
2725        assert!(!p.app.is_busy());
2726        assert!(held(&p).is_empty());
2727    }
2728
2729    /// A Ctrl+O typed while the settings were read is offered once the startup open
2730    /// has gone out and before anything it sends back, so that open is put down however
2731    /// fast it would have been: nothing opens behind the home screen.
2732    #[test]
2733    fn ctrl_o_typed_before_the_app_existed_puts_the_startup_open_down() {
2734        let dir = tempfile::tempdir().expect("temp dir");
2735        let path = dir.path().join("people.csv");
2736        std::fs::write(&path, "name,age\nada,36\n").expect("write csv");
2737
2738        // As `run_impl` sets it up: the open announced, then the keys from the settings
2739        // read handed over with the open behind them.
2740        let mut p = pump();
2741        p.app.set_loading_phase("Scanning input", 10);
2742        p.handle_first([
2743            AppEvent::Terminal(Event::Key(ctrl('o'))),
2744            AppEvent::OpenNamed(vec![path], OpenOptions::default()),
2745        ]);
2746        settle(&mut p);
2747        // Anything that went out anyway is for nobody: let its answer land.
2748        let deadline = std::time::Instant::now() + Duration::from_secs(300);
2749        while p.app.background_work_in_flight() {
2750            assert!(std::time::Instant::now() < deadline, "the look never ended");
2751            p.wait_and_drain(Duration::from_millis(50)).unwrap();
2752        }
2753        settle(&mut p);
2754
2755        assert_eq!(p.app.input_mode, InputMode::Home);
2756        assert!(
2757            p.app.data_table_state.is_none(),
2758            "nothing opened behind home"
2759        );
2760        assert!(!p.app.error_modal.active, "{}", p.app.error_modal.message);
2761    }
2762
2763    /// The same for a frame handed over from Python: it was on the channel too, and
2764    /// installed behind the home screen once Ctrl+O was offered first.
2765    #[test]
2766    fn ctrl_o_typed_before_the_app_existed_puts_a_startup_frame_down() {
2767        use polars::prelude::IntoLazy;
2768        let lf = polars::df!("a" => [1i64, 2, 3]).expect("frame").lazy();
2769        let mut p = pump();
2770        p.app.set_loading_phase("Scanning input", 10);
2771        p.handle_first([
2772            AppEvent::Terminal(Event::Key(ctrl('o'))),
2773            AppEvent::OpenLazyFrame(Box::new(lf), OpenOptions::default()),
2774        ]);
2775        settle(&mut p);
2776        let deadline = std::time::Instant::now() + Duration::from_secs(300);
2777        while p.app.background_work_in_flight() {
2778            assert!(std::time::Instant::now() < deadline, "the read never ended");
2779            p.wait_and_drain(Duration::from_millis(50)).unwrap();
2780        }
2781        settle(&mut p);
2782
2783        assert_eq!(p.app.input_mode, InputMode::Home);
2784        assert!(
2785            p.app.data_table_state.is_none(),
2786            "nothing opened behind home"
2787        );
2788    }
2789
2790    /// Keys typed before the app existed meet the startup open as the loading screen:
2791    /// a stray `G` or Esc is dropped, not replayed onto the table once it is up, and
2792    /// `q` still quits.
2793    #[test]
2794    fn keys_typed_before_the_app_existed_meet_the_loading_screen() {
2795        let dir = tempfile::tempdir().expect("temp dir");
2796        let path = dir.path().join("people.csv");
2797        std::fs::write(&path, "name,age\nada,36\nbob,41\n").expect("write csv");
2798
2799        let mut p = pump();
2800        p.app.set_loading_phase("Scanning input", 10);
2801        p.handle_first([
2802            AppEvent::Terminal(Event::Key(plain(KeyCode::Char('G')))),
2803            AppEvent::Terminal(Event::Key(plain(KeyCode::Esc))),
2804            AppEvent::OpenNamed(vec![path.clone()], OpenOptions::default()),
2805        ]);
2806        assert!(matches!(settle(&mut p), Drained::Continue { .. }));
2807        assert!(held(&p).is_empty());
2808        assert_eq!(p.app.input_mode, InputMode::Normal);
2809        let state = p.app.data_table_state.as_ref().expect("the table opened");
2810        assert_eq!(state.table_state.selected(), Some(0), "G was not replayed");
2811
2812        let mut p = pump();
2813        p.app.set_loading_phase("Scanning input", 10);
2814        p.handle_first([
2815            AppEvent::Terminal(Event::Key(plain(KeyCode::Char('q')))),
2816            AppEvent::OpenNamed(vec![path], OpenOptions::default()),
2817        ]);
2818        assert!(matches!(settle(&mut p), Drained::Exit), "q quits");
2819    }
2820
2821    /// The loading screen has nothing to type ahead into, so nothing is held
2822    /// there. Held once, a stray key queued `q` behind it for the whole load.
2823    #[test]
2824    fn the_loading_screen_never_holds_keys() {
2825        let mut p = pump();
2826        p.app.opened_from_home = true;
2827        p.app.set_loading_phase("Scanning input", 10);
2828        assert!(p.app.awaiting_dataset() && p.app.is_busy());
2829
2830        // A stray key is dropped, not held.
2831        p.terminal_key(plain(KeyCode::Char('j'))).unwrap();
2832        p.terminal_key(plain(KeyCode::Enter)).unwrap();
2833        assert!(held(&p).is_empty(), "the loading screen holds nothing");
2834
2835        // And q still acts at once, stray keys or not.
2836        p.terminal_key(plain(KeyCode::Char('q'))).unwrap();
2837        assert_eq!(
2838            p.app.input_mode,
2839            InputMode::Home,
2840            "q pops home from the loading screen"
2841        );
2842    }
2843
2844    /// Ctrl-C and Ctrl-Q quit from any mode while busy, including the chart view,
2845    /// which has no CONTROL arm of its own.
2846    #[test]
2847    fn ctrl_c_quits_from_chart_mode_while_busy() {
2848        for c in ['c', 'q'] {
2849            let mut p = pump();
2850            p.app.input_mode = InputMode::Chart;
2851            p.app.chart_modal.active = true;
2852            p.app.busy = true;
2853            assert!(matches!(
2854                p.app.handle(&AppEvent::Key(ctrl(c))),
2855                Ok(Some(AppEvent::Exit))
2856            ));
2857            p.terminal_key(ctrl(c)).unwrap();
2858            assert!(matches!(p.drain().unwrap(), Drained::Exit));
2859        }
2860    }
2861
2862    /// The home screen is never busy on its own account, so it keeps its keys while
2863    /// an export left running behind it finishes.
2864    #[test]
2865    fn home_keys_act_while_background_work_runs() {
2866        let mut p = pump();
2867        p.app.enter_home();
2868        p.app.busy = true;
2869        p.terminal_key(plain(KeyCode::Char('x'))).unwrap();
2870        assert!(held(&p).is_empty());
2871        assert_eq!(p.app.home.filter, "x");
2872    }
2873
2874    /// A pump with `rows` rows loaded, named `r0000` on, so a row on screen can be
2875    /// told from every other.
2876    fn numbered_pump(rows: usize) -> (EventPump, tempfile::TempDir) {
2877        let dir = tempfile::tempdir().expect("temp dir");
2878        let path = dir.path().join("numbered.csv");
2879        let mut file = std::fs::File::create(&path).expect("create csv");
2880        writeln!(file, "name").unwrap();
2881        for i in 0..rows {
2882            writeln!(file, "r{i:04}").unwrap();
2883        }
2884        drop(file);
2885
2886        let mut pump = pump();
2887        pump.send(AppEvent::Open(vec![path], OpenOptions::default()))
2888            .unwrap();
2889        settle(&mut pump);
2890        // The frame sizes the table, and the collect it owes runs, as in `run()`.
2891        rendered(&mut pump.app);
2892        if let Some(state) = pump.app.data_table_state.as_mut()
2893            && std::mem::take(&mut state.needs_recollect)
2894        {
2895            pump.app.spawn_async_collect(App::LOADING_BUFFER);
2896        }
2897        settle(&mut pump);
2898        rendered(&mut pump.app);
2899        (pump, dir)
2900    }
2901
2902    fn table(pump: &EventPump) -> &crate::widgets::datatable::DataTableState {
2903        pump.app.data_table_state.as_ref().expect("a dataset")
2904    }
2905
2906    /// Page inside the buffer until the view is near enough its end to load ahead.
2907    fn page_to_the_edge(pump: &mut EventPump) {
2908        for _ in 0..50 {
2909            if table(pump).wants_to_load_ahead() {
2910                return;
2911            }
2912            pump.terminal_key(plain(KeyCode::PageDown)).unwrap();
2913            assert!(
2914                !pump.app.is_busy(),
2915                "paging inside the buffer fetches nothing"
2916            );
2917            rendered(&mut pump.app);
2918        }
2919        panic!("the view never came near the end of the buffer");
2920    }
2921
2922    /// Paging towards the end of the buffer grows it before the view gets there, and
2923    /// nothing waits on that: no `busy`, no message, keys still live.
2924    #[test]
2925    fn paging_near_the_end_of_the_buffer_loads_ahead_quietly() {
2926        let (mut p, _dir) = numbered_pump(2000);
2927        page_to_the_edge(&mut p);
2928        let end = table(&p).buffered_end();
2929
2930        p.app.request_what_the_frame_needs();
2931        assert!(
2932            p.app.rows_in_flight().is_some() && !p.app.rows_waited_on(),
2933            "a load-ahead went out"
2934        );
2935        assert!(!p.app.is_busy(), "and nothing waits on it");
2936        assert_eq!(p.app.status_message, None);
2937
2938        settle(&mut p);
2939        assert!(
2940            table(&p).buffered_end() > end,
2941            "the buffer grew ahead of the view"
2942        );
2943        assert!(!p.app.is_busy());
2944
2945        // Asked once for a position: the next frame does not ask again.
2946        let generation = p.app.task_generation();
2947        p.app.request_what_the_frame_needs();
2948        p.app.request_what_the_frame_needs();
2949        assert!(p.app.task_generation() - generation <= 1);
2950    }
2951
2952    /// A frame drawn while the generation is held with the app idle does not spend the
2953    /// position: the load-ahead it turns away goes out once the hold is let go (#490).
2954    /// A job no longer holds the generation past its answer, but a download waiting
2955    /// on the user still does.
2956    #[test]
2957    fn a_load_ahead_turned_away_by_a_hold_goes_out_once_it_is_let_go() {
2958        let (mut p, _dir) = numbered_pump(2000);
2959        page_to_the_edge(&mut p);
2960        let end = table(&p).buffered_end();
2961
2962        // Something idle that holds the generation.
2963        let hold = p.app.hold_the_generation();
2964        let generation = p.app.task_generation();
2965        assert!(!p.app.is_busy());
2966        p.app.request_what_the_frame_needs();
2967        assert_eq!(
2968            p.app.task_generation(),
2969            generation,
2970            "nothing went out past it"
2971        );
2972
2973        drop(hold);
2974        p.drain().unwrap();
2975        assert!(!p.app.work_a_bump_would_strand());
2976        p.app.request_what_the_frame_needs();
2977        assert!(
2978            p.app.rows_in_flight().is_some() && !p.app.rows_waited_on(),
2979            "the load-ahead went out once the generation was free"
2980        );
2981        settle(&mut p);
2982        assert!(table(&p).buffered_end() > end, "and the buffer grew");
2983    }
2984
2985    /// PageDown held while a load-ahead is out waits on it rather than fetching the same
2986    /// rows again, and the repeats become one press: one fetch, nothing piled up.
2987    #[test]
2988    fn holding_pagedown_during_a_load_ahead_waits_on_it() {
2989        let (mut p, _dir) = numbered_pump(2000);
2990        page_to_the_edge(&mut p);
2991        let state = table(&p);
2992        let (start, end, page) = (
2993            state.buffered_start(),
2994            state.buffered_end(),
2995            state.visible_rows,
2996        );
2997        let dataset = state.len_generation();
2998        let columns = crate::InflightCollect::columns_of(state);
2999        // Out, and bringing the next few pages: no thread, so it stays out.
3000        let generation = p.app.task_generation();
3001        let _ahead = p.app.job_for_tests(
3002            crate::Job::Rows(crate::InflightCollect {
3003                began: std::time::Instant::now(),
3004                files: None,
3005                dataset,
3006                columns,
3007                start,
3008                end: end + 10 * page,
3009            }),
3010            None,
3011        );
3012
3013        // As `run()` takes them: a key, then whatever it set going.
3014        for _ in 0..30 {
3015            p.terminal_key(plain(KeyCode::PageDown)).unwrap();
3016            p.drain().unwrap();
3017        }
3018        assert_eq!(
3019            p.app.task_generation(),
3020            generation,
3021            "no second fetch went out"
3022        );
3023        assert!(
3024            p.app.rows_waited_on(),
3025            "the page that left the buffer waits on the load-ahead"
3026        );
3027        assert!(p.app.is_busy());
3028        assert!(held(&p).len() <= 1, "held repeats coalesce: {:?}", held(&p));
3029    }
3030
3031    /// A page whose rows are still coming draws the last page that was whole, not a
3032    /// page of blanks under the new position.
3033    #[test]
3034    fn a_page_still_loading_draws_the_last_whole_one() {
3035        let (mut p, _dir) = numbered_pump(2000);
3036        assert!(rendered(&mut p.app).contains("r0000"));
3037        let state = p.app.data_table_state.as_mut().unwrap();
3038        assert!(
3039            state.slide_table(1500),
3040            "far past the buffer, so a fetch is owed"
3041        );
3042
3043        let screen = rendered(&mut p.app);
3044        assert!(screen.contains("r0000"), "the page before stays up");
3045    }
3046
3047    /// A fetch goes unmentioned while it is young, so paging does not blink a sentence
3048    /// over the key chips; one that takes a while says what it is doing.
3049    #[test]
3050    fn the_bar_says_loading_only_once_a_fetch_takes_a_while() {
3051        let (mut p, _dir) = numbered_pump(200);
3052        let dataset = table(&p).len_generation();
3053        let columns = crate::InflightCollect::columns_of(table(&p));
3054        let _read = p.app.job_for_tests(
3055            crate::Job::Rows(crate::InflightCollect {
3056                began: std::time::Instant::now(),
3057                files: None,
3058                dataset,
3059                columns,
3060                start: 0,
3061                end: 200,
3062            }),
3063            Some(App::LOADING_BUFFER),
3064        );
3065        assert!(!rendered(&mut p.app).contains("Loading buffer"));
3066
3067        if let Some(crate::Job::Rows(inflight)) = p
3068            .app
3069            .jobs
3070            .current_mut(|job| matches!(job, crate::Job::Rows(_)))
3071        {
3072            inflight.began -= Duration::from_secs(1);
3073        }
3074        assert!(rendered(&mut p.app).contains("Loading buffer"));
3075    }
3076
3077    fn terminal(key: KeyEvent) -> AppEvent {
3078        AppEvent::Terminal(Event::Key(key))
3079    }
3080
3081    /// A flash that outlives any test: the loop's sleep is bounded by it, so a loop that
3082    /// only woke at deadlines would end the test with the flash gone instead of hanging.
3083    fn long_flash(app: &mut App) {
3084        app.flash = Some(crate::Flash {
3085            message: "guard".to_string(),
3086            path_from: None,
3087            expires: Instant::now() + Duration::from_secs(120),
3088        });
3089    }
3090
3091    /// Idle, the loop sleeps without a deadline; a spinner wakes it each frame.
3092    #[test]
3093    fn the_pacer_sleeps_until_a_deadline_and_has_none_when_idle() {
3094        let start = Instant::now();
3095        let mut pacer = Pacer::default();
3096        pacer.drew(start);
3097        assert_eq!(pacer.timeout(None, start), Duration::MAX, "idle: no tick");
3098
3099        pacer.spinning(true, start);
3100        assert_eq!(pacer.timeout(None, start), SPINNER_FRAME);
3101        assert!(!pacer.turn_spinner(start + SPINNER_FRAME / 2), "not yet");
3102        assert!(pacer.turn_spinner(start + SPINNER_FRAME), "a frame on time");
3103        assert!(!pacer.turn_spinner(start + SPINNER_FRAME), "once per frame");
3104
3105        pacer.spinning(false, start);
3106        let flash = start + Duration::from_secs(2);
3107        assert_eq!(pacer.timeout(Some(flash), start), Duration::from_secs(2));
3108    }
3109
3110    /// Progress reports close behind a frame wait for the next one; anything else, and
3111    /// progress once a frame's time has passed, is drawn at once.
3112    #[test]
3113    fn progress_reports_share_a_frame_and_nothing_else_waits() {
3114        let start = Instant::now();
3115        let mut pacer = Pacer::default();
3116        pacer.drew(start);
3117        let soon = start + PROGRESS_FRAME / 3;
3118        assert!(
3119            pacer.handled(true, false, soon),
3120            "a result is drawn at once"
3121        );
3122        assert!(!pacer.handled(true, true, soon), "progress waits");
3123        assert_eq!(
3124            pacer.timeout(None, soon),
3125            PROGRESS_FRAME - PROGRESS_FRAME / 3,
3126            "until the owed frame"
3127        );
3128        assert!(
3129            !pacer.handled(false, false, soon),
3130            "nothing new, still owed"
3131        );
3132        assert!(
3133            pacer.handled(false, false, start + PROGRESS_FRAME),
3134            "then drawn"
3135        );
3136        pacer.drew(start + PROGRESS_FRAME);
3137        assert!(!pacer.handled(false, false, start + 2 * PROGRESS_FRAME));
3138        assert!(
3139            pacer.handled(true, true, start + 3 * PROGRESS_FRAME),
3140            "progress long after a frame is drawn at once"
3141        );
3142    }
3143
3144    /// Keys come through the channel now. They are classified on the way in exactly as
3145    /// before: typed at a busy table they are held in order, and Ctrl-Q still jumps the
3146    /// queue and quits.
3147    #[test]
3148    fn terminal_keys_on_the_channel_are_held_in_order_and_ctrl_q_jumps_them() {
3149        let mut p = pump();
3150        p.app.busy = true;
3151        for c in ['/', 'a', 'b'] {
3152            p.send(terminal(plain(KeyCode::Char(c)))).unwrap();
3153        }
3154        assert!(matches!(p.drain().unwrap(), Drained::Continue { .. }));
3155        assert_eq!(
3156            held(&p),
3157            vec![KeyCode::Char('/'), KeyCode::Char('a'), KeyCode::Char('b')]
3158        );
3159        p.send(terminal(ctrl('q'))).unwrap();
3160        // The key acts at once; the exit it asks for is its continuation, the very next
3161        // thing handled.
3162        assert!(matches!(
3163            p.drain().unwrap(),
3164            Drained::Continue { updated: true, .. }
3165        ));
3166        assert!(matches!(p.drain().unwrap(), Drained::Exit));
3167    }
3168
3169    /// A worker's answer that lands behind keys typed ahead is handled before the next
3170    /// of them, as when the loop read the terminal itself. Taken in channel order, a
3171    /// held-down key put the load-ahead's answer behind every repeat.
3172    #[test]
3173    fn an_answer_is_not_held_behind_keys_typed_ahead() {
3174        let mut p = pump();
3175        p.app.home.listing_in_flight = true;
3176        for _ in 0..3 {
3177            p.send(terminal(plain(KeyCode::Down))).unwrap();
3178        }
3179        p.send(AppEvent::HomeListingFailed).unwrap();
3180        let keys = p.app.debug.num_key_events;
3181        assert!(matches!(
3182            p.drain().unwrap(),
3183            Drained::Continue { updated: true, .. }
3184        ));
3185        assert!(!p.app.home.listing_in_flight, "the answer is in");
3186        assert_eq!(
3187            p.app.debug.num_key_events,
3188            keys + 1,
3189            "and one key, one frame"
3190        );
3191        p.drain().unwrap();
3192        p.drain().unwrap();
3193        assert_eq!(p.app.debug.num_key_events, keys + 3, "the rest, in turn");
3194    }
3195
3196    /// A worker that reports faster than the loop handles it still lets a typed key
3197    /// through.
3198    #[test]
3199    fn a_stream_of_reports_does_not_starve_a_typed_key() {
3200        let mut p = pump();
3201        p.send(terminal(plain(KeyCode::Down))).unwrap();
3202        for _ in 0..RESULTS_PER_KEY * 4 {
3203            p.send(AppEvent::Wake).unwrap();
3204        }
3205        let keys = p.app.debug.num_key_events;
3206        p.drain().unwrap();
3207        assert_eq!(p.app.debug.num_key_events, keys + 1);
3208    }
3209
3210    /// A resize read from the terminal reaches the app as the resize event it always
3211    /// was.
3212    #[test]
3213    fn a_resize_from_the_terminal_reaches_the_app() {
3214        let mut p = pump();
3215        p.send(AppEvent::Terminal(Event::Resize(90, 30))).unwrap();
3216        let events = p.app.debug.num_events;
3217        assert!(matches!(
3218            p.drain().unwrap(),
3219            Drained::Continue { updated: true, .. }
3220        ));
3221        assert_eq!(p.app.debug.num_events, events + 1, "handled once");
3222    }
3223
3224    /// The loop sleeps until something arrives: a worker's answer wakes it at once,
3225    /// with no deadline to wait out. The guard flash would end the sleep after two
3226    /// minutes and clear itself; it is still there, so the event did it.
3227    #[test]
3228    fn a_worker_answer_wakes_the_sleeping_loop() {
3229        let mut p = pump();
3230        long_flash(&mut p.app);
3231        let tx = p.tx.clone();
3232        let mut frames = 0;
3233        let end = p
3234            .run(|_app| {
3235                frames += 1;
3236                if frames == 1 {
3237                    // After the first frame the loop goes to sleep; the answer lands
3238                    // while it does.
3239                    let tx = tx.clone();
3240                    std::thread::spawn(move || {
3241                        std::thread::sleep(Duration::from_millis(20));
3242                        let _ = tx.send(AppEvent::Exit);
3243                    });
3244                }
3245                Ok(())
3246            })
3247            .unwrap();
3248        assert_eq!(end, Ended::Quit);
3249        assert!(p.app.flash.is_some(), "woken by the event, not a deadline");
3250    }
3251
3252    /// A frame that draws rows nothing has measured asks for them before the loop
3253    /// sleeps. Asked on the next pass instead, they would wait for a key: with the
3254    /// listing in and nothing turning, there is no next pass.
3255    #[test]
3256    fn rows_a_frame_draws_unmeasured_are_asked_for_before_the_loop_sleeps() {
3257        let dir = tempfile::tempdir().expect("temp dir");
3258        let path = dir.path().join("people.csv");
3259        std::fs::write(&path, "name,age\nada,36\n").unwrap();
3260        let mut p = pump();
3261        long_flash(&mut p.app);
3262        p.app.enter_home();
3263        p.app.home_jump_into(dir.path().to_path_buf());
3264        let tx = p.tx.clone();
3265        let mut asked = false;
3266        let end = p
3267            .run(|app| {
3268                rendered(app);
3269                if !asked && app.home.enriched.contains_key(&path) {
3270                    asked = true;
3271                    let _ = tx.send(AppEvent::Exit);
3272                }
3273                Ok(())
3274            })
3275            .unwrap();
3276        assert_eq!(end, Ended::Quit);
3277        assert!(p.app.home.enriched.contains_key(&path), "measured");
3278        assert!(p.app.flash.is_some(), "without waiting for a deadline");
3279    }
3280
3281    /// Each loading phase gets its frame, then the next phase runs: the open goes from
3282    /// the spinner to rows through the loop itself, every phase drawn in order.
3283    #[test]
3284    fn an_open_draws_each_phase_and_then_the_rows() {
3285        let dir = tempfile::tempdir().expect("temp dir");
3286        let path = dir.path().join("people.csv");
3287        std::fs::write(&path, "name,age\nada,36\ngrace,45\n").unwrap();
3288        let mut p = pump();
3289        p.app.set_loading_phase("Scanning input", 10);
3290        p.app.busy = true;
3291        p.send(AppEvent::Open(vec![path], OpenOptions::default()))
3292            .unwrap();
3293        let tx = p.tx.clone();
3294        let mut screens: Vec<String> = Vec::new();
3295        let end = p
3296            .run(|app| {
3297                let screen = rendered(app);
3298                if screen.contains("grace") && !screens.last().is_some_and(|s| s == &screen) {
3299                    let _ = tx.send(AppEvent::Exit);
3300                }
3301                screens.push(screen);
3302                Ok(())
3303            })
3304            .unwrap();
3305        assert_eq!(end, Ended::Quit);
3306        let first_with = |needle: &str| screens.iter().position(|s| s.contains(needle));
3307        let scanning = first_with("Scanning input").expect("the first phase is drawn");
3308        let rows = first_with("grace").expect("the rows are drawn");
3309        assert!(scanning < rows, "the phase is drawn before the rows");
3310    }
3311
3312    /// A pump with a wide, long CSV loaded: columns past the screen and rows past a
3313    /// page, so the keys that move have somewhere to go.
3314    /// A pump with a wide, long CSV loaded: columns past the screen, rows past a page
3315    /// and numbers that group, so the keys that move and format have somewhere to go.
3316    fn long_wide_pump() -> (EventPump, tempfile::TempDir) {
3317        let dir = tempfile::tempdir().expect("temp dir");
3318        let path = dir.path().join("wide.csv");
3319        let mut file = std::fs::File::create(&path).expect("create csv");
3320        let names: Vec<String> = (0..12).map(|c| format!("column_{c}")).collect();
3321        writeln!(file, "name,{}", names.join(",")).unwrap();
3322        for row in 0..300 {
3323            let values: Vec<String> = (0..12)
3324                .map(|c| (((row * 7 + c) % 23) * 1234).to_string())
3325                .collect();
3326            writeln!(file, "n{},{}", row % 17, values.join(",")).unwrap();
3327        }
3328        drop(file);
3329        let mut pump = pump();
3330        pump.app.app_config.query.default_mode = crate::QueryMode::Q;
3331        pump.send(AppEvent::Open(vec![path], OpenOptions::default()))
3332            .unwrap();
3333        settle(&mut pump);
3334        paint(&mut pump);
3335        (pump, dir)
3336    }
3337
3338    /// The whole frame, styles included: the column cursor is a tint.
3339    fn frame(app: &mut App) -> Buffer {
3340        let area = Rect::new(0, 0, 100, 20);
3341        let mut buf = Buffer::empty(area);
3342        app.render(area, &mut buf);
3343        buf
3344    }
3345
3346    /// Draw a frame and read what it asks for, as `run()` does after each update, until
3347    /// nothing more is asked: the rows a page down needs.
3348    fn paint(pump: &mut EventPump) {
3349        for _ in 0..200 {
3350            frame(&mut pump.app);
3351            pump.app.frame_painted();
3352            pump.app.request_what_the_frame_needs();
3353            let collect = pump
3354                .app
3355                .data_table_state
3356                .as_mut()
3357                .is_some_and(|s| std::mem::take(&mut s.needs_recollect));
3358            if collect {
3359                pump.app.spawn_async_collect(App::LOADING_BUFFER);
3360            }
3361            settle(pump);
3362            if !collect {
3363                return;
3364            }
3365        }
3366    }
3367
3368    /// Every key the registry lists for a screen reached from the table, typed at that
3369    /// screen, is taken: the frame changes, the screen changes, or the app ends. A key
3370    /// listed where nothing handles it fails here. The groups checked are the ones the
3371    /// screen opens on; a group of a state within it (a picker, a dialog) and the
3372    /// listed exceptions need a state this test does not set up.
3373    #[test]
3374    fn every_registry_key_is_taken_where_it_is_listed() {
3375        use datui_cli::keys::{self, Context};
3376        let ch = |c: char| plain(KeyCode::Char(c));
3377        let shift = |c: char| KeyEvent::new(KeyCode::Char(c), KeyModifiers::SHIFT);
3378        let (up, down, right) = (
3379            plain(KeyCode::Up),
3380            plain(KeyCode::Down),
3381            plain(KeyCode::Right),
3382        );
3383        // How each screen is reached from the table, and the groups it opens on.
3384        let screens: Vec<(Context, Vec<KeyEvent>, &[&str])> = vec![
3385            (
3386                Context::Table,
3387                vec![],
3388                &["Explore", "Shape", "Analyze", "Output", "Display", "Go"],
3389            ),
3390            // Something typed, for the editing keys to edit.
3391            (Context::Query, vec![ch(':'), ch('a')], &["Run", "Edit"]),
3392            (Context::Find, vec![ch('/'), ch('7')], &["Find"]),
3393            (Context::GoToColumn, vec![ch('g'), ch('c')], &["Go"]),
3394            (Context::Inspector, vec![ch(' ')], &["Fields", "Output"]),
3395            (Context::Info, vec![ch('i')], &["Panel"]),
3396            (
3397                Context::ValueCounts,
3398                vec![shift('F')],
3399                &["Explore", "Output"],
3400            ),
3401            // What is in effect, where Sort & Filter opens; then the Columns tab's
3402            // list: up to the tab bar, across, and down past find.
3403            (
3404                Context::SortFilter,
3405                vec![ch('s')],
3406                &["Sidebar", "In effect"],
3407            ),
3408            (
3409                Context::SortFilter,
3410                vec![ch('s'), up, right, down, down],
3411                &["Columns"],
3412            ),
3413            // On the tab bar, a choice, for ← / → to step.
3414            (Context::PivotMelt, vec![ch('p')], &["Form"]),
3415            // From a number column: its histogram, which has a grid and bins.
3416            (Context::Chart, vec![ch('l'), ch('c')], &["Shelves", "Plot"]),
3417            // Back from the path to the format, a choice, for ← / → to step.
3418            (
3419                Context::Export,
3420                vec![ch('e'), plain(KeyCode::BackTab)],
3421                &["Form"],
3422            ),
3423            (Context::Copy, vec![ch('y')], &["Form"]),
3424            (Context::Views, vec![ch('v')], &["List"]),
3425            // Not the home screen: what it lists, and so what a key there changes, is
3426            // the machine's (the working directory, the desktop's recent places).
3427            // home_test covers its keys.
3428        ];
3429        // What needs a state the fixture does not have, or would leave the test.
3430        let mut exempt: Vec<(Context, &str)> = vec![
3431            // Only where the row count is an estimate.
3432            (Context::Info, "c"),
3433            // Only on a file read through a format spec.
3434            (Context::Table, "b"),
3435            // Nothing to leave at the plain table.
3436            (Context::Table, "Esc"),
3437            // Every column fits already.
3438            (Context::Table, "= / w"),
3439            // Nothing to reset, or to take out of a sort.
3440            (Context::Table, "R"),
3441            (Context::SortFilter, "Del"),
3442            (Context::SortFilter, "[ / ]"),
3443            // The width is automatic already.
3444            (Context::SortFilter, "w"),
3445            // History: none in a fresh session.
3446            (Context::Query, "↑ / ↓"),
3447            (Context::Find, "↑ / ↓"),
3448            // On a group's row, a struct or JSON, or a field the rows lack.
3449            (Context::Inspector, "Enter"),
3450            (Context::Inspector, "r"),
3451            // Long text only; another program.
3452            (Context::Inspector, "w"),
3453            (Context::Inspector, "o"),
3454            // The scrolling tabs: a CSV has none; Notes: none.
3455            (Context::Info, "PgUp / PgDn"),
3456            (Context::Info, "Home / End"),
3457            (Context::Info, "Enter"),
3458            // The Documentation tab: a dataset no catalog lists has none.
3459            (Context::Info, "y"),
3460            (Context::Info, "o"),
3461            // A sample, a followed file, a number column.
3462            (Context::ValueCounts, "a"),
3463            (Context::ValueCounts, "c"),
3464            (Context::ValueCounts, "t"),
3465            // Nothing in effect: the sidebar opens on "add sort", which has no value
3466            // to step, and there is nothing to remove or clear.
3467            (Context::SortFilter, "← / →"),
3468            (Context::SortFilter, "d / Del"),
3469            (Context::SortFilter, "C"),
3470            // History: none in a fresh session.
3471            (Context::Export, "Ctrl+P / Ctrl+N"),
3472            // A range: Enter in the help presses its first.
3473            (Context::SortFilter, "1-9"),
3474            (Context::Chart, "1-7"),
3475            // The number rows; columns picked; a followed file.
3476            (Context::Chart, "+ / -"),
3477            (Context::Chart, "x"),
3478            (Context::Chart, "e"),
3479            (Context::Chart, "t"),
3480            // A saved view.
3481            (Context::Views, "↑ / ↓ (j/k)"),
3482            (Context::Views, "Enter"),
3483            (Context::Views, "e"),
3484            (Context::Views, "d"),
3485            (Context::Views, "i"),
3486        ];
3487        // One language to switch between without SQL.
3488        if crate::QueryMode::available().len() < 2 {
3489            exempt.push((Context::Query, "Ctrl+T"));
3490        }
3491        let mut ignored = Vec::new();
3492        for (context, open, groups) in &screens {
3493            let screen = keys::screen(*context);
3494            for group in screen.groups.iter().filter(|g| groups.contains(&g.name)) {
3495                for key in group.keys {
3496                    let Some(chord) = key.action() else {
3497                        continue;
3498                    };
3499                    if exempt.contains(&(*context, key.keys)) {
3500                        continue;
3501                    }
3502                    let (mut p, dir) = long_wide_pump();
3503                    for k in open {
3504                        p.terminal_key(*k).unwrap();
3505                        settle(&mut p);
3506                        paint(&mut p);
3507                    }
3508                    assert_eq!(p.app.keys_context(), *context, "opened {}", screen.title);
3509                    // The suggested name is relative: an Enter would write it into the
3510                    // crate directory.
3511                    if *context == Context::Export {
3512                        let out = dir.path().join("out.csv");
3513                        p.app
3514                            .export_modal
3515                            .path_input
3516                            .set_value(out.display().to_string());
3517                    }
3518                    // The entry's keys in turn, the first at least: `← / →` at the
3519                    // first column is taken by its →.
3520                    let mut presses = keys::chords(key.keys);
3521                    if !presses.contains(&chord) {
3522                        presses.insert(0, chord);
3523                    }
3524                    let mut taken = false;
3525                    for press in presses {
3526                        let before = frame(&mut p.app);
3527                        p.terminal_key(crate::help::key_event(press)).unwrap();
3528                        if matches!(settle(&mut p), Drained::Exit) {
3529                            taken = true;
3530                            break;
3531                        }
3532                        paint(&mut p);
3533                        if before != frame(&mut p.app) || p.app.keys_context() != *context {
3534                            taken = true;
3535                            break;
3536                        }
3537                    }
3538                    if !taken {
3539                        ignored.push(format!("{} · {} · {}", screen.title, group.name, key.keys));
3540                    }
3541                }
3542            }
3543        }
3544        assert!(
3545            ignored.is_empty(),
3546            "keys nothing took:\n{}",
3547            ignored.join("\n")
3548        );
3549    }
3550
3551    /// Enter on a help line closes the help and presses the line's key at the screen
3552    /// under it, through the loop as a typed key goes.
3553    #[test]
3554    fn enter_in_the_help_runs_the_key() {
3555        let (mut p, _dir) = long_wide_pump();
3556        p.terminal_key(plain(KeyCode::Char('?'))).unwrap();
3557        settle(&mut p);
3558        assert!(p.app.help_visible());
3559        for c in "/value counts".chars() {
3560            p.terminal_key(plain(KeyCode::Char(c))).unwrap();
3561        }
3562        p.terminal_key(plain(KeyCode::Enter)).unwrap();
3563        settle(&mut p);
3564        assert!(!p.app.help_visible(), "Enter closed the help");
3565        assert_eq!(
3566            p.app.input_mode,
3567            InputMode::ValueCounts,
3568            "F ran at the table"
3569        );
3570    }
3571
3572    /// While the app is busy, the key Enter presses waits its turn like a typed one.
3573    #[test]
3574    fn the_key_enter_presses_waits_while_busy() {
3575        let (mut p, _dir) = long_wide_pump();
3576        p.terminal_key(plain(KeyCode::Char('?'))).unwrap();
3577        for c in "/sort & filter sidebar".chars() {
3578            p.terminal_key(plain(KeyCode::Char(c))).unwrap();
3579        }
3580        p.terminal_key(plain(KeyCode::Enter)).unwrap();
3581        p.app.busy = true;
3582        p.drain().unwrap();
3583        assert!(!p.app.help_visible());
3584        assert_eq!(p.app.input_mode, InputMode::Normal, "held while busy");
3585        assert_eq!(held(&p), [KeyCode::Char('s')]);
3586        p.app.busy = false;
3587        settle(&mut p);
3588        assert_eq!(
3589            p.app.input_mode,
3590            InputMode::SortFilter,
3591            "replayed once idle"
3592        );
3593    }
3594
3595    /// Type `text` into the open help's filter, then press Enter.
3596    fn run_from_help(p: &mut EventPump, filter: &str) {
3597        p.terminal_key(plain(KeyCode::Char('/'))).unwrap();
3598        for c in filter.chars() {
3599            p.terminal_key(plain(KeyCode::Char(c))).unwrap();
3600        }
3601        p.terminal_key(plain(KeyCode::Enter)).unwrap();
3602    }
3603
3604    /// Over a text field, a help line whose key is a plain character does not run:
3605    /// pressed, it would type. The help's own key presses F1, never `?`.
3606    #[test]
3607    fn enter_in_the_help_never_types_into_a_field() {
3608        let (mut p, _dir) = long_wide_pump();
3609        p.terminal_key(plain(KeyCode::Char('f'))).unwrap();
3610        p.terminal_key(plain(KeyCode::F(1))).unwrap();
3611        run_from_help(&mut p, "next or previous");
3612        settle(&mut p);
3613        assert!(
3614            p.app.help_visible(),
3615            "n / N does not run from the find prompt"
3616        );
3617        p.terminal_key(plain(KeyCode::Esc)).unwrap();
3618        p.terminal_key(plain(KeyCode::Esc)).unwrap();
3619        assert!(!p.app.help_visible());
3620        assert_eq!(p.app.find.input.value(), "", "nothing was typed");
3621
3622        p.terminal_key(plain(KeyCode::Esc)).unwrap();
3623        p.terminal_key(plain(KeyCode::Char(':'))).unwrap();
3624        p.terminal_key(plain(KeyCode::F(1))).unwrap();
3625        run_from_help(&mut p, "screen's keys");
3626        settle(&mut p);
3627        assert_eq!(p.app.query_input.value(), "", "? was not typed");
3628        assert!(p.app.help_visible(), "F1 opened the help again");
3629    }
3630
3631    /// A question that arrives under the help takes the keys, so the help goes: Enter
3632    /// never answers it unseen. Nor does F1 open the help over one.
3633    #[test]
3634    fn a_question_under_the_help_closes_it() {
3635        let (mut p, _dir) = long_wide_pump();
3636        p.terminal_key(plain(KeyCode::Char('?'))).unwrap();
3637        assert!(p.app.help_visible());
3638        p.app.confirmation_modal.active = true;
3639        rendered(&mut p.app);
3640        assert!(!p.app.help_visible());
3641        p.terminal_key(plain(KeyCode::F(1))).unwrap();
3642        assert!(!p.app.help_visible(), "no help over the question");
3643    }
3644
3645    /// While busy, the help still closes at once, at a load's screen too.
3646    #[test]
3647    fn the_help_closes_while_busy() {
3648        for key in [KeyCode::Esc, KeyCode::F(1), KeyCode::Char('?')] {
3649            let (mut p, _dir) = long_wide_pump();
3650            p.terminal_key(plain(KeyCode::Char('?'))).unwrap();
3651            p.app.busy = true;
3652            assert!(p.terminal_key(plain(key)).unwrap(), "{key:?} acted");
3653            assert!(!p.app.help_visible(), "{key:?} closed the help");
3654            assert!(held(&p).is_empty());
3655        }
3656    }
3657
3658    /// The key Enter presses goes through `classify` as a typed one: a busy Enter
3659    /// that would inspect waits as Space.
3660    #[test]
3661    fn the_pressed_key_is_classified_as_typed() {
3662        let (mut p, _dir) = long_wide_pump();
3663        p.terminal_key(plain(KeyCode::Char('?'))).unwrap();
3664        run_from_help(&mut p, "or inspect");
3665        p.app.busy = true;
3666        p.drain().unwrap();
3667        assert!(!p.app.help_visible());
3668        assert_eq!(held(&p), [KeyCode::Char(' ')]);
3669    }
3670
3671    /// A screen that changes under the help on its own takes the help with it: its
3672    /// keys were for the screen that is gone.
3673    #[test]
3674    fn help_closes_when_its_screen_goes() {
3675        let (mut p, _dir) = long_wide_pump();
3676        p.terminal_key(plain(KeyCode::Char('?'))).unwrap();
3677        assert!(p.app.help_visible());
3678        // As a finished query or a failed load moves the app on.
3679        p.app.input_mode = InputMode::Home;
3680        rendered(&mut p.app);
3681        assert!(!p.app.help_visible());
3682    }
3683
3684    /// Help opened over the analysis view or the views list shows that screen's keys.
3685    #[test]
3686    fn help_shows_the_keys_of_the_screen_under_it() {
3687        let (mut p, _dir) = long_wide_pump();
3688        p.terminal_key(plain(KeyCode::Char('v'))).unwrap();
3689        settle(&mut p);
3690        p.terminal_key(plain(KeyCode::Char('?'))).unwrap();
3691        assert_eq!(p.app.help_context(), Some(datui_cli::keys::Context::Views));
3692    }
3693
3694    // ----- The mouse on forms, tabs, the footer, the header and the menu -----
3695
3696    fn right_click(at: (u16, u16)) -> MouseEvent {
3697        use crossterm::event::{MouseButton, MouseEventKind};
3698        mouse(MouseEventKind::Down(MouseButton::Right), at)
3699    }
3700
3701    fn drag(at: (u16, u16)) -> MouseEvent {
3702        use crossterm::event::{MouseButton, MouseEventKind};
3703        mouse(MouseEventKind::Drag(MouseButton::Left), at)
3704    }
3705
3706    fn release(at: (u16, u16)) -> MouseEvent {
3707        use crossterm::event::{MouseButton, MouseEventKind};
3708        mouse(MouseEventKind::Up(MouseButton::Left), at)
3709    }
3710
3711    /// Where the table's header drew `column`: its cells across, `[from, to)`, and the
3712    /// header's first row.
3713    fn header_of(p: &mut EventPump, column: &str) -> (u16, u16, u16) {
3714        rendered(&mut p.app);
3715        let state = p.app.data_table_state.as_ref().unwrap();
3716        let (area, columns) = state.drawn_header().expect("the table was drawn");
3717        let (from, to, _) = columns
3718            .into_iter()
3719            .find(|(_, _, name)| name == column)
3720            .expect("the column is drawn");
3721        (from, to, area.y)
3722    }
3723
3724    fn order(p: &EventPump) -> Vec<String> {
3725        p.app.data_table_state.as_ref().unwrap().headers()
3726    }
3727
3728    /// In the export dialog a click on a row focuses it and acts as Space: a
3729    /// checkbox flips, a choice steps, a text field only takes the cursor. A click on
3730    /// a format on its row chooses it.
3731    #[test]
3732    fn a_click_focuses_a_form_row_and_acts_on_it() {
3733        use crate::export_modal::{ExportFocus, ExportFormat};
3734        let (mut p, _dir) = loaded_pump();
3735        p.terminal_key(plain(KeyCode::Char('e'))).unwrap();
3736        assert_eq!(p.app.input_mode, InputMode::Export);
3737        assert_eq!(p.app.export_modal.selected_format, ExportFormat::Csv);
3738        let header = p.app.export_modal.csv_include_header;
3739
3740        let at = on_screen(&mut p.app, "Header:");
3741        assert!(p.terminal_mouse(click(at)).unwrap());
3742        assert_eq!(p.app.export_modal.focus, ExportFocus::CsvIncludeHeader);
3743        assert_eq!(p.app.export_modal.csv_include_header, !header, "toggled");
3744
3745        let compression = p.app.export_modal.csv_compression;
3746        let at = on_screen(&mut p.app, "Compression:");
3747        p.terminal_mouse(click(at)).unwrap();
3748        assert_eq!(p.app.export_modal.focus, ExportFocus::Compression);
3749        assert_ne!(p.app.export_modal.csv_compression, compression, "stepped");
3750        // A right click steps it back.
3751        p.terminal_mouse(right_click(at)).unwrap();
3752        assert_eq!(p.app.export_modal.csv_compression, compression);
3753
3754        let path = p.app.export_modal.path_input.value().to_string();
3755        let at = on_screen(&mut p.app, "Path:");
3756        p.terminal_mouse(click(at)).unwrap();
3757        assert_eq!(p.app.export_modal.focus, ExportFocus::PathInput);
3758        assert_eq!(p.app.export_modal.path_input.value(), path, "nothing typed");
3759
3760        let at = on_screen(&mut p.app, "Parquet");
3761        p.terminal_mouse(click(at)).unwrap();
3762        assert_eq!(p.app.export_modal.selected_format, ExportFormat::Parquet);
3763        assert_eq!(p.app.input_mode, InputMode::Export, "still open");
3764    }
3765
3766    /// A click on a tab switches to it: the Sort & Filter sidebar's, through its tab
3767    /// bar field, and the Info panel's, which switch from anywhere.
3768    #[test]
3769    fn a_click_on_a_tab_switches_to_it() {
3770        use crate::sort_filter_modal::{SortFilterField, SortFilterTab};
3771        let (mut p, _dir) = loaded_pump();
3772        p.terminal_key(plain(KeyCode::Char('s'))).unwrap();
3773        assert_eq!(p.app.input_mode, InputMode::SortFilter);
3774        let at = on_screen(&mut p.app, "Columns");
3775        assert!(p.terminal_mouse(click(at)).unwrap());
3776        assert_eq!(p.app.sort_filter_modal.active_tab, SortFilterTab::Columns);
3777        assert_eq!(p.app.sort_filter_modal.focus, SortFilterField::TabBar);
3778        // The tab, not the sidebar's title.
3779        let tab = format!("Sort & Filter {}", crate::glyphs::get().rule);
3780        let at = on_screen(&mut p.app, &tab);
3781        p.terminal_mouse(click(at)).unwrap();
3782        assert_eq!(p.app.sort_filter_modal.active_tab, SortFilterTab::InEffect);
3783
3784        p.terminal_key(plain(KeyCode::Esc)).unwrap();
3785        p.terminal_key(plain(KeyCode::Char('i'))).unwrap();
3786        assert_eq!(p.app.input_mode, InputMode::Info);
3787        let at = on_screen(&mut p.app, "Resources");
3788        p.terminal_mouse(click(at)).unwrap();
3789        assert_eq!(
3790            p.app.info_modal.active_tab,
3791            crate::widgets::info::InfoTab::Resources
3792        );
3793    }
3794
3795    /// The footer's filters open the sidebar that lists them, and its `query`
3796    /// opens the command line on the query's text.
3797    #[test]
3798    fn the_footer_filters_and_query_are_clickable() {
3799        let (mut p, _dir) = loaded_pump();
3800        p.terminal_key(plain(KeyCode::Char('+'))).unwrap();
3801        settle(&mut p);
3802        let at = on_screen(&mut p.app, "name = ");
3803        assert!(p.terminal_mouse(click(at)).unwrap());
3804        assert_eq!(
3805            p.app.input_mode,
3806            InputMode::SortFilter,
3807            "the sidebar opened"
3808        );
3809        p.terminal_key(plain(KeyCode::Esc)).unwrap();
3810        settle(&mut p);
3811
3812        p.terminal_key(plain(KeyCode::Char(':'))).unwrap();
3813        type_keys(&mut p, "select age");
3814        p.terminal_key(plain(KeyCode::Enter)).unwrap();
3815        settle(&mut p);
3816        assert_eq!(p.app.input_mode, InputMode::Normal);
3817        let at = on_screen(&mut p.app, " query ");
3818        assert!(p.terminal_mouse(click((at.0 + 1, at.1))).unwrap());
3819        assert_eq!(p.app.input_mode, InputMode::Editing);
3820        assert_eq!(p.app.query_input.value(), "select age");
3821    }
3822
3823    /// A click outside a dialog does nothing: the table under the sidebar keeps its
3824    /// cursor and the sidebar stays.
3825    #[test]
3826    fn a_click_outside_a_dialog_does_nothing() {
3827        let (mut p, _dir) = loaded_pump();
3828        let alan = on_screen(&mut p.app, "alan");
3829        p.terminal_key(plain(KeyCode::Char('s'))).unwrap();
3830        rendered(&mut p.app);
3831        assert!(!p.terminal_mouse(click(alan)).unwrap());
3832        assert!(!p.terminal_mouse(right_click(alan)).unwrap());
3833        assert_eq!(cell(&p), (Some(0), Some("name".to_string())));
3834        assert_eq!(p.app.input_mode, InputMode::SortFilter);
3835        assert!(p.app.context_menu.is_none());
3836
3837        // Over a dialog the table is covered, and so is anything else drawn under it.
3838        p.terminal_key(plain(KeyCode::Esc)).unwrap();
3839        p.terminal_key(plain(KeyCode::Char('e'))).unwrap();
3840        rendered(&mut p.app);
3841        let focus = p.app.export_modal.focus;
3842        let header = p.app.export_modal.csv_include_header;
3843        assert!(!p.terminal_mouse(click((0, 0))).unwrap());
3844        assert!(!p.terminal_mouse(click(alan)).unwrap());
3845        assert_eq!(p.app.export_modal.focus, focus);
3846        assert_eq!(p.app.export_modal.csv_include_header, header);
3847        assert_eq!(p.app.input_mode, InputMode::Export);
3848    }
3849
3850    /// A header dragged over another column moves there on release, as `L` would,
3851    /// with a rule on the header where it lands while it is carried.
3852    #[test]
3853    fn dragging_a_header_moves_its_column() {
3854        let (mut p, _dir) = loaded_pump();
3855        let (from, _, y) = header_of(&mut p, "name");
3856        let (age, age_to, _) = header_of(&mut p, "age");
3857        assert!(p.terminal_mouse(click((from + 1, y))).unwrap());
3858        assert_eq!(cell(&p).1.as_deref(), Some("name"));
3859        assert!(
3860            p.terminal_mouse(drag((age + 1, y))).unwrap(),
3861            "a frame is due for the drop mark"
3862        );
3863        assert!(
3864            !p.terminal_mouse(drag((age + 2, y))).unwrap(),
3865            "still over age: nothing new to draw"
3866        );
3867        let area = Rect::new(0, 0, 100, 20);
3868        let mut buf = Buffer::empty(area);
3869        p.app.render(area, &mut buf);
3870        assert_eq!(
3871            buf[(age_to, y)].symbol(),
3872            crate::glyphs::get().rule,
3873            "the drop mark, after age"
3874        );
3875        p.terminal_mouse(release((age + 1, y))).unwrap();
3876        settle(&mut p);
3877        assert_eq!(order(&p), ["age", "name"]);
3878        assert_eq!(cell(&p).1.as_deref(), Some("name"), "the cursor came along");
3879
3880        // Let go where it was picked up: nothing moves.
3881        let (from, _, y) = header_of(&mut p, "name");
3882        p.terminal_mouse(click((from + 1, y))).unwrap();
3883        p.terminal_mouse(release((from + 1, y))).unwrap();
3884        settle(&mut p);
3885        assert_eq!(order(&p), ["age", "name"]);
3886    }
3887
3888    /// The gap after a header dragged sideways sets its width by hand, within the
3889    /// bounds `<` and `>` keep.
3890    #[test]
3891    fn dragging_a_header_edge_resizes_its_column() {
3892        use crate::widgets::column_widths::{MAX_WIDTH, MIN_WIDTH, WidthChoice};
3893        let (mut p, _dir) = loaded_pump();
3894        let (from, to, y) = header_of(&mut p, "name");
3895        let width = to - from;
3896        assert!(!p.terminal_mouse(click((to, y))).unwrap(), "a press only");
3897        assert!(p.terminal_mouse(drag((to + 5, y))).unwrap());
3898        let choice = |p: &EventPump| {
3899            p.app
3900                .data_table_state
3901                .as_ref()
3902                .unwrap()
3903                .width_choice("name")
3904        };
3905        assert_eq!(choice(&p), WidthChoice::Manual(width + 5));
3906        p.terminal_mouse(drag((0, y))).unwrap();
3907        assert_eq!(choice(&p), WidthChoice::Manual(MIN_WIDTH));
3908        p.terminal_mouse(drag((99, y))).unwrap();
3909        assert!(matches!(choice(&p), WidthChoice::Manual(w) if w <= MAX_WIDTH));
3910        p.terminal_mouse(release((99, y))).unwrap();
3911        assert_eq!(order(&p), ["name", "age"], "a resize moves no column");
3912        // Without a press first, a drag does nothing.
3913        let before = choice(&p);
3914        p.terminal_mouse(drag((to + 9, y))).unwrap();
3915        assert_eq!(choice(&p), before);
3916    }
3917
3918    /// Press and let go at `at`, after a frame painted, as a click on what is drawn.
3919    fn click_and_release(p: &mut EventPump, at: (u16, u16)) {
3920        rendered(&mut p.app);
3921        p.app.frame_painted();
3922        p.terminal_mouse(click(at)).unwrap();
3923        p.terminal_mouse(release(at)).unwrap();
3924        settle(p);
3925    }
3926
3927    /// The sort in effect: its columns and directions.
3928    fn sorted(p: &EventPump) -> (Vec<String>, Vec<bool>) {
3929        let state = p.app.data_table_state.as_ref().unwrap();
3930        let columns = state.view_sort_columns().to_vec();
3931        let descending = state.view_sort_descending().to_vec();
3932        (
3933            columns.clone(),
3934            descending[..columns.len().min(descending.len())].to_vec(),
3935        )
3936    }
3937
3938    /// A double click on a header sorts by its column as `[` and `]` do: ascending,
3939    /// descending, then off. One click only moves the column cursor.
3940    #[test]
3941    fn a_double_click_on_a_header_cycles_its_sort() {
3942        let (mut p, _dir) = loaded_pump();
3943        let (age, _, y) = header_of(&mut p, "age");
3944        let at = (age + 1, y);
3945        click_and_release(&mut p, at);
3946        assert_eq!(cell(&p).1.as_deref(), Some("age"));
3947        assert_eq!(sorted(&p), (vec![], vec![]), "one click only moves");
3948
3949        click_and_release(&mut p, at);
3950        assert_eq!(sorted(&p), (vec!["age".to_string()], vec![false]));
3951        assert_eq!(order(&p), ["name", "age"], "a sort moves no column");
3952
3953        click_and_release(&mut p, at);
3954        click_and_release(&mut p, at);
3955        assert_eq!(sorted(&p), (vec!["age".to_string()], vec![true]));
3956
3957        click_and_release(&mut p, at);
3958        click_and_release(&mut p, at);
3959        assert_eq!(sorted(&p), (vec![], vec![]), "the third takes it away");
3960    }
3961
3962    /// A double click on the gap after a header fits the column, as `=` does, and
3963    /// sorts nothing.
3964    #[test]
3965    fn a_double_click_on_a_header_edge_fits_the_column() {
3966        use crate::widgets::column_widths::WidthChoice;
3967        let (mut p, _dir) = loaded_pump();
3968        let (_, to, y) = header_of(&mut p, "name");
3969        // The cursor elsewhere, to see it come to the column fitted.
3970        let (age, _, _) = header_of(&mut p, "age");
3971        click_and_release(&mut p, (age + 1, y));
3972        click_and_release(&mut p, (to, y));
3973        let state = p.app.data_table_state.as_ref().unwrap();
3974        assert_ne!(
3975            state.width_choice("name"),
3976            WidthChoice::Fit,
3977            "one press only"
3978        );
3979        click_and_release(&mut p, (to, y));
3980        let state = p.app.data_table_state.as_ref().unwrap();
3981        assert_eq!(state.width_choice("name"), WidthChoice::Fit);
3982        assert_eq!(sorted(&p), (vec![], vec![]), "never a sort");
3983        assert_eq!(cell(&p).1.as_deref(), Some("name"));
3984        assert_eq!(order(&p), ["name", "age"]);
3985    }
3986
3987    /// A key handled marks the frame on screen out of date, so a click read after it
3988    /// waits for the frame that shows what the key did, a replayed key included.
3989    #[test]
3990    fn a_key_handled_puts_the_next_click_behind_a_frame() {
3991        let (mut p, _dir) = loaded_pump();
3992        rendered(&mut p.app);
3993        p.app.frame_painted();
3994        assert!(p.app.pointer.on_screen());
3995        p.terminal_key(plain(KeyCode::Char('j'))).unwrap();
3996        assert!(!p.app.pointer.on_screen());
3997    }
3998
3999    /// The chart's panel takes clicks on its rows: a click focuses one and acts as
4000    /// Space, so the type steps and a toggle flips.
4001    #[test]
4002    fn a_click_on_the_chart_panel_focuses_and_acts() {
4003        use crate::chart_modal::ChartFocus;
4004        let (mut p, _dir) = loaded_pump();
4005        p.terminal_key(plain(KeyCode::Char('c'))).unwrap();
4006        settle(&mut p);
4007        assert_eq!(p.app.input_mode, InputMode::Chart);
4008        let mark = p.app.chart_modal.spec.mark;
4009        let at = on_screen(&mut p.app, "Type");
4010        assert!(p.terminal_mouse(click(at)).unwrap());
4011        settle(&mut p);
4012        assert_eq!(p.app.chart_modal.focus, ChartFocus::Type);
4013        assert_ne!(p.app.chart_modal.spec.mark, mark, "the type stepped");
4014        let grid = p.app.chart_modal.grid;
4015        let at = on_screen(&mut p.app, "Grid");
4016        p.terminal_mouse(click(at)).unwrap();
4017        settle(&mut p);
4018        assert_eq!(p.app.chart_modal.focus, ChartFocus::Grid);
4019        assert_ne!(p.app.chart_modal.grid, grid, "the grid flipped");
4020    }
4021
4022    /// A header carried off the columns is over itself again: let go there, nothing
4023    /// moves. A key while carrying puts it back too.
4024    #[test]
4025    fn a_header_drag_is_cancelled_off_the_columns_or_by_a_key() {
4026        let (mut p, _dir) = loaded_pump();
4027        let (from, _, y) = header_of(&mut p, "name");
4028        let (age, _, _) = header_of(&mut p, "age");
4029        p.terminal_mouse(click((from + 1, y))).unwrap();
4030        p.terminal_mouse(drag((age + 1, y))).unwrap();
4031        assert!(
4032            p.terminal_mouse(drag((age + 1, 19))).unwrap(),
4033            "the mark goes"
4034        );
4035        p.terminal_mouse(release((age + 1, 19))).unwrap();
4036        settle(&mut p);
4037        assert_eq!(order(&p), ["name", "age"]);
4038
4039        p.terminal_mouse(click((from + 1, y))).unwrap();
4040        p.terminal_mouse(drag((age + 1, y))).unwrap();
4041        p.terminal_key(plain(KeyCode::Char('#'))).unwrap();
4042        assert!(p.app.pointer.drag().is_none());
4043        p.terminal_mouse(release((age + 1, y))).unwrap();
4044        settle(&mut p);
4045        assert_eq!(order(&p), ["name", "age"]);
4046    }
4047
4048    /// The last column reaches the right side, so no gap follows it: its last
4049    /// header cell is its edge.
4050    #[test]
4051    fn the_last_column_resizes_from_the_right_side() {
4052        use crate::widgets::column_widths::WidthChoice;
4053        let (mut p, _dir) = loaded_pump();
4054        // Wide enough to be cut at the right side.
4055        p.terminal_key(plain(KeyCode::Char('l'))).unwrap();
4056        for _ in 0..30 {
4057            p.terminal_key(plain(KeyCode::Char('>'))).unwrap();
4058        }
4059        let (from, to, y) = header_of(&mut p, "age");
4060        assert_eq!(to, 100, "age reaches the right side");
4061        let choice = |p: &EventPump| p.app.data_table_state.as_ref().unwrap().width_choice("age");
4062        let WidthChoice::Manual(set) = choice(&p) else {
4063            panic!("set by hand: {:?}", choice(&p));
4064        };
4065        assert!(set > to - from, "cut at the side");
4066        assert!(
4067            !p.terminal_mouse(click((to - 1, y))).unwrap(),
4068            "a press on the edge"
4069        );
4070        p.terminal_mouse(drag((to - 31, y))).unwrap();
4071        assert_eq!(choice(&p), WidthChoice::Manual(set - 30));
4072    }
4073
4074    /// A right click on a choice steps it back; on a checkbox it only focuses.
4075    #[test]
4076    fn a_right_click_on_a_checkbox_only_focuses_it() {
4077        use crate::export_modal::ExportFocus;
4078        let (mut p, _dir) = loaded_pump();
4079        p.terminal_key(plain(KeyCode::Char('e'))).unwrap();
4080        let header = p.app.export_modal.csv_include_header;
4081        let at = on_screen(&mut p.app, "Header:");
4082        p.terminal_mouse(right_click(at)).unwrap();
4083        assert_eq!(p.app.export_modal.focus, ExportFocus::CsvIncludeHeader);
4084        assert_eq!(p.app.export_modal.csv_include_header, header, "not toggled");
4085    }
4086
4087    /// A row of a list (a sort, a column) takes focus on the first click and acts on
4088    /// the second, so it can be picked out without changing it.
4089    #[test]
4090    fn a_list_row_focuses_first_and_acts_on_a_second_click() {
4091        use crate::sort_filter_modal::{SortFilterField, SortFilterTab};
4092        let (mut p, _dir) = loaded_pump();
4093        p.terminal_key(plain(KeyCode::Char('['))).unwrap();
4094        settle(&mut p);
4095        p.terminal_key(plain(KeyCode::Char('s'))).unwrap();
4096        assert_eq!(p.app.sort_filter_modal.active_tab, SortFilterTab::InEffect);
4097        p.app.sort_filter_modal.focus = SortFilterField::TabBar;
4098        let descending = |p: &EventPump| {
4099            let m = &p.app.sort_filter_modal.sort;
4100            m.columns[m.sort_entries()[0]].sort_descending
4101        };
4102        let before = descending(&p);
4103        let row = format!(" 1 {} name", crate::glyphs::get().sort_asc);
4104        let at = on_screen(&mut p.app, &row);
4105        p.terminal_mouse(click(at)).unwrap();
4106        assert_eq!(p.app.sort_filter_modal.focus, SortFilterField::Sort(0));
4107        assert_eq!(descending(&p), before, "the first click only focuses");
4108        p.terminal_mouse(click(at)).unwrap();
4109        assert_ne!(descending(&p), before, "the second flips it");
4110    }
4111
4112    /// The menu opens only on the cell the cursor landed on: rows drawn before the
4113    /// view scrolled are not the ones clicked.
4114    #[test]
4115    fn the_menu_opens_only_where_the_cursor_lands() {
4116        let (mut p, _dir) = numbered_pump(50);
4117        let at = on_screen(&mut p.app, "r0002");
4118        // The view scrolled since the frame on screen was drawn.
4119        p.terminal_key(plain(KeyCode::End)).unwrap();
4120        settle(&mut p);
4121        let row = cell(&p).0;
4122        p.terminal_mouse(right_click(at)).unwrap();
4123        assert_eq!(cell(&p).0, row, "the cursor stays");
4124        assert!(p.app.context_menu.is_none());
4125    }
4126
4127    /// While a job runs, the open menu's own keys still act: they read nothing.
4128    #[test]
4129    fn the_menus_keys_act_while_busy() {
4130        let (mut p, _dir) = loaded_pump();
4131        let alan = on_screen(&mut p.app, "alan");
4132        p.terminal_mouse(right_click(alan)).unwrap();
4133        p.app.busy = true;
4134        assert!(p.terminal_key(plain(KeyCode::Down)).unwrap());
4135        assert_eq!(p.app.context_menu.as_ref().map(|m| m.selected), Some(1));
4136        assert!(p.terminal_key(plain(KeyCode::Esc)).unwrap());
4137        assert!(p.app.context_menu.is_none());
4138        assert!(held(&p).is_empty());
4139    }
4140
4141    /// Drags read together are one: only the last place counts.
4142    #[test]
4143    fn drags_waiting_together_are_one() {
4144        let (mut p, _dir) = loaded_pump();
4145        let (from, _, y) = header_of(&mut p, "name");
4146        let (age, _, _) = header_of(&mut p, "age");
4147        p.app.frame_painted();
4148        for event in [
4149            click((from + 1, y)),
4150            drag((age + 1, y)),
4151            drag((from + 1, y)),
4152            drag((age + 1, y)),
4153            release((age + 1, y)),
4154        ] {
4155            p.send(AppEvent::Terminal(Event::Mouse(event))).unwrap();
4156        }
4157        for _ in 0..6 {
4158            p.drain().unwrap();
4159            rendered(&mut p.app);
4160            p.app.frame_painted();
4161        }
4162        settle(&mut p);
4163        assert_eq!(order(&p), ["age", "name"]);
4164    }
4165
4166    /// A right click on a cell puts the cursor there and opens the menu; Enter runs
4167    /// its line by pressing the line's key, and the menu closes.
4168    #[test]
4169    fn a_right_click_opens_the_menu_and_enter_runs_a_line() {
4170        let (mut p, _dir) = loaded_pump();
4171        let alan = on_screen(&mut p.app, "alan");
4172        assert!(p.terminal_mouse(right_click(alan)).unwrap());
4173        assert_eq!(cell(&p), (Some(2), Some("name".to_string())));
4174        assert!(p.app.context_menu.is_some());
4175        let text = rendered(&mut p.app);
4176        assert!(text.contains("Filter to this value"), "the menu is drawn");
4177        // The menu takes the arrows; the table's cursor stays.
4178        p.terminal_key(plain(KeyCode::Down)).unwrap();
4179        p.terminal_key(plain(KeyCode::Up)).unwrap();
4180        assert_eq!(cell(&p).0, Some(2));
4181        p.terminal_key(plain(KeyCode::Enter)).unwrap();
4182        settle(&mut p);
4183        assert!(p.app.context_menu.is_none());
4184        let state = p.app.data_table_state.as_ref().unwrap();
4185        assert_eq!(state.view_filters().len(), 1, "+ filtered to the value");
4186        assert_eq!(state.num_rows(), 1);
4187    }
4188
4189    /// A click on a line runs it; a click outside closes the menu and does nothing
4190    /// more; Esc closes it.
4191    #[test]
4192    fn a_click_chooses_from_the_menu_and_outside_closes_it() {
4193        let (mut p, _dir) = loaded_pump();
4194        let grace = on_screen(&mut p.app, "grace");
4195        p.terminal_mouse(right_click(grace)).unwrap();
4196        let counts = on_screen(&mut p.app, "Value counts");
4197        assert!(p.terminal_mouse(click(counts)).unwrap());
4198        assert!(p.app.context_menu.is_none());
4199        assert_eq!(p.app.input_mode, InputMode::ValueCounts, "F ran");
4200        p.terminal_key(plain(KeyCode::Esc)).unwrap();
4201        settle(&mut p);
4202
4203        let alan = on_screen(&mut p.app, "alan");
4204        p.terminal_mouse(right_click(alan)).unwrap();
4205        rendered(&mut p.app);
4206        assert!(p.terminal_mouse(click((0, 0))).unwrap());
4207        assert!(p.app.context_menu.is_none(), "closed");
4208        assert_eq!(cell(&p).0, Some(2), "and nothing else moved");
4209        p.terminal_mouse(right_click(alan)).unwrap();
4210        p.terminal_key(plain(KeyCode::Esc)).unwrap();
4211        assert!(p.app.context_menu.is_none());
4212        assert_eq!(p.app.input_mode, InputMode::Normal);
4213    }
4214
4215    /// Busy, the mouse acts only where a typed key would act at once: no menu opens,
4216    /// a dropped header moves nothing and a menu line presses nothing, while a width
4217    /// drag acts, as `>` does at a busy table.
4218    #[test]
4219    fn mouse_actions_while_busy_obey_the_key_rules() {
4220        use crate::widgets::column_widths::WidthChoice;
4221        let (mut p, _dir) = loaded_pump();
4222        let alan = on_screen(&mut p.app, "alan");
4223        let (from, to, y) = header_of(&mut p, "name");
4224        let (age, _, _) = header_of(&mut p, "age");
4225        p.app.busy = true;
4226        assert!(!p.terminal_mouse(right_click(alan)).unwrap());
4227        assert!(p.app.context_menu.is_none());
4228
4229        p.app.busy = false;
4230        p.terminal_mouse(click((from + 1, y))).unwrap();
4231        p.terminal_mouse(drag((age + 1, y))).unwrap();
4232        p.app.busy = true;
4233        p.terminal_mouse(release((age + 1, y))).unwrap();
4234        assert!(held(&p).is_empty(), "the mouse is never held");
4235        p.app.busy = false;
4236        settle(&mut p);
4237        assert_eq!(order(&p), ["name", "age"], "dropped");
4238
4239        p.app.busy = true;
4240        p.terminal_mouse(click((to, y))).unwrap();
4241        p.terminal_mouse(drag((to + 3, y))).unwrap();
4242        let state = p.app.data_table_state.as_ref().unwrap();
4243        assert_eq!(
4244            state.width_choice("name"),
4245            WidthChoice::Manual(to - from + 3)
4246        );
4247
4248        p.app.busy = false;
4249        p.terminal_mouse(right_click(alan)).unwrap();
4250        rendered(&mut p.app);
4251        let line = on_screen(&mut p.app, "Filter to this value");
4252        p.app.busy = true;
4253        p.terminal_mouse(click(line)).unwrap();
4254        assert!(p.app.context_menu.is_none(), "the menu closes");
4255        p.app.busy = false;
4256        settle(&mut p);
4257        let state = p.app.data_table_state.as_ref().unwrap();
4258        assert!(state.view_filters().is_empty(), "and its key was dropped");
4259    }
4260}