1use 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
24pub const MAX_HELD_KEYS: usize = 32;
28
29enum Act {
31 Now,
33 Hold,
35 HoldAs(KeyEvent),
37 Drop,
39 StopFind,
42}
43
44#[derive(Debug, Clone, Copy)]
47enum Input {
48 Key(KeyEvent),
49 Mouse(MouseEvent),
50}
51
52#[derive(Debug)]
54pub enum Drained {
55 Continue {
59 updated: bool,
60 progress_only: bool,
61 },
62 Exit,
63 Crash(String),
64 NotFound(std::path::PathBuf),
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73struct Screen {
74 generation: u64,
75 modal: bool,
76 inline_failures: u64,
77}
78
79pub struct EventPump {
81 pub app: App,
82 tx: Sender<AppEvent>,
83 rx: Receiver<AppEvent>,
84 held: VecDeque<KeyEvent>,
85 held_for: Screen,
86 next_up: VecDeque<(AppEvent, Hold)>,
95 backlog: VecDeque<AppEvent>,
99 typed: VecDeque<Input>,
105 since_key: usize,
108 early: usize,
113}
114
115const 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 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 pub fn held_keys(&self) -> impl Iterator<Item = &KeyEvent> {
161 self.held.iter()
162 }
163
164 pub fn replaying(&self) -> bool {
167 !self.held.is_empty() && !self.app.is_busy()
168 }
169
170 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 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 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 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 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 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 if self.app.menu_takes(key) {
334 return Act::Now;
335 }
336 let queued = !self.held.is_empty();
337 if !self.app.is_busy() && !queued {
339 return Act::Now;
340 }
341 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 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 if self.app.is_busy() && !queued && self.app.key_acts_while_sampling(key) {
364 return Act::Now;
365 }
366 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 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 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 pub fn drain(&mut self) -> Result<Drained> {
416 let first = self.take_next();
417 self.drain_from(first)
418 }
419
420 pub fn wait_and_drain(&mut self, timeout: Duration) -> Result<Drained> {
425 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 Ok((AppEvent::NamedPathMissing(path), _)) => {
456 return Ok(Drained::NotFound(path));
457 }
458 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 self.queue_continuation(follow_up);
515 drop(continuation);
516 break;
517 }
518 if let Some(hold) = continuation.take() {
522 drop(hold);
523 self.app.let_waiting_errands_in();
524 }
525 }
526 Err(TryRecvError::Empty) => {
527 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 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 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 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 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 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 app.frame_painted();
629 first_rows.painted(app);
630 pacer.drew(now);
631 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 app.request_what_the_frame_needs();
641 }
642 }
643 }
644
645 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 fn dispatch(&mut self, key: KeyEvent) -> Result<()> {
654 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 Err(deferred) => self.held.push_front(deferred),
663 }
664 if self.app.screen_generation() != gen_before {
665 self.held.clear();
668 self.app.set_input_dropped(false);
669 } else {
670 self.held_for = Self::screen_of(&self.app);
675 }
676 Ok(())
677 }
678
679 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 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 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#[derive(Debug, PartialEq, Eq)]
750pub enum Ended {
751 Quit,
752 Crash(String),
753 NotFound(std::path::PathBuf),
755}
756
757#[derive(Default)]
759struct Pass {
760 updated: bool,
761 progress_only: bool,
762}
763
764impl Pass {
765 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
785pub const SPINNER_FRAME: Duration = Duration::from_millis(33);
787
788pub const PROGRESS_FRAME: Duration = Duration::from_millis(33);
792
793#[derive(Debug, Default)]
800pub struct Pacer {
801 last_draw: Option<Instant>,
802 owed: bool,
804 spin_due: Option<Instant>,
806}
807
808impl Pacer {
809 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 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 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 pub fn drew(&mut self, now: Instant) {
846 self.last_draw = Some(now);
847 self.owed = false;
848 }
849
850 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
865fn 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
886fn 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 | 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 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 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 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 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 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 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 rendered(&mut pump.app);
1015 (pump, dir)
1016 }
1017
1018 #[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 #[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 #[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 #[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 #[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 #[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 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 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 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 #[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 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 let before = cell(&p);
1263 p.terminal_mouse(click((3, 3))).unwrap();
1264 assert_eq!(cell(&p), before);
1265 }
1266
1267 #[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 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 #[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 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 #[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 #[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 #[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 p.terminal_mouse(click((5, 5))).unwrap();
1392 assert_eq!(cell(&p), (Some(0), Some("name".to_string())));
1393 }
1394
1395 #[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 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 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 #[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 #[test]
1457 fn a_continuation_never_goes_to_the_back_of_the_channel() {
1458 let source = include_str!("event_pump.rs");
1459 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 #[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 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 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 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 #[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 p.app.owe_rows_for_tests("Loading buffer...");
1536
1537 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 #[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 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 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 #[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 #[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 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 #[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 #[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 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 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 #[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 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 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 #[test]
1886 fn a_deferred_collect_with_nothing_to_do_takes_the_loading_screen_down() {
1887 let (mut p, _dir) = loaded_pump();
1888 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 #[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 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 #[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 #[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 #[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 #[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 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 #[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 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 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 #[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 #[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 type_keys(&mut p, "aa");
2125 assert_eq!(held(&p).len(), 4);
2126 }
2127
2128 #[test]
2131 fn keys_held_before_an_error_modal_appears_are_dropped() {
2132 let mut p = pump();
2133 p.app.busy = true;
2134 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 #[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 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 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 #[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 #[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 #[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 #[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 #[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; 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 #[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 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 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 #[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 #[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 #[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 #[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 #[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 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 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 #[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 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 let before_last = first_scrolled(&p);
2616 assert!(
2617 before_last > 0 && before_last < last_page,
2618 "{before_last} before {last_page}"
2619 );
2620 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 #[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 #[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 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 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 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 p.terminal_key(plain(KeyCode::Down)).unwrap();
2709 assert!(p.app.flash.is_none(), "a keypress clears the flash");
2710 }
2711
2712 #[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 #[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 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 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 #[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 #[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 #[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 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 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 #[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 #[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 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 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 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 #[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 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 #[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 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 #[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 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 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 #[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 #[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 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 #[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 #[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 #[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 assert!(matches!(
3163 p.drain().unwrap(),
3164 Drained::Continue { updated: true, .. }
3165 ));
3166 assert!(matches!(p.drain().unwrap(), Drained::Exit));
3167 }
3168
3169 #[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 #[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 #[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 #[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 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 #[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 #[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 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 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 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 #[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 let screens: Vec<(Context, Vec<KeyEvent>, &[&str])> = vec![
3385 (
3386 Context::Table,
3387 vec![],
3388 &["Explore", "Shape", "Analyze", "Output", "Display", "Go"],
3389 ),
3390 (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 (
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 (Context::PivotMelt, vec![ch('p')], &["Form"]),
3415 (Context::Chart, vec![ch('l'), ch('c')], &["Shelves", "Plot"]),
3417 (
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 ];
3429 let mut exempt: Vec<(Context, &str)> = vec![
3431 (Context::Info, "c"),
3433 (Context::Table, "b"),
3435 (Context::Table, "Esc"),
3437 (Context::Table, "= / w"),
3439 (Context::Table, "R"),
3441 (Context::SortFilter, "Del"),
3442 (Context::SortFilter, "[ / ]"),
3443 (Context::SortFilter, "w"),
3445 (Context::Query, "↑ / ↓"),
3447 (Context::Find, "↑ / ↓"),
3448 (Context::Inspector, "Enter"),
3450 (Context::Inspector, "r"),
3451 (Context::Inspector, "w"),
3453 (Context::Inspector, "o"),
3454 (Context::Info, "PgUp / PgDn"),
3456 (Context::Info, "Home / End"),
3457 (Context::Info, "Enter"),
3458 (Context::Info, "y"),
3460 (Context::Info, "o"),
3461 (Context::ValueCounts, "a"),
3463 (Context::ValueCounts, "c"),
3464 (Context::ValueCounts, "t"),
3465 (Context::SortFilter, "← / →"),
3468 (Context::SortFilter, "d / Del"),
3469 (Context::SortFilter, "C"),
3470 (Context::Export, "Ctrl+P / Ctrl+N"),
3472 (Context::SortFilter, "1-9"),
3474 (Context::Chart, "1-7"),
3475 (Context::Chart, "+ / -"),
3477 (Context::Chart, "x"),
3478 (Context::Chart, "e"),
3479 (Context::Chart, "t"),
3480 (Context::Views, "↑ / ↓ (j/k)"),
3482 (Context::Views, "Enter"),
3483 (Context::Views, "e"),
3484 (Context::Views, "d"),
3485 (Context::Views, "i"),
3486 ];
3487 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 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 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 #[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 #[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 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 #[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 #[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 #[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 #[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 #[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 p.app.input_mode = InputMode::Home;
3680 rendered(&mut p.app);
3681 assert!(!p.app.help_visible());
3682 }
3683
3684 #[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 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 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 #[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 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 #[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 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 #[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 #[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 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 #[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 (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 #[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 let before = choice(&p);
3914 p.terminal_mouse(drag((to + 9, y))).unwrap();
3915 assert_eq!(choice(&p), before);
3916 }
3917
3918 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 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 #[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 #[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 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 #[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 #[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 #[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 #[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 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 #[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 #[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 #[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 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 #[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 #[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 #[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 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 #[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 #[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}