1use std::collections::HashMap;
11use std::ffi::OsString;
12
13use tui_treelistview::{TreeListViewState, TreeQuery};
14
15use crate::cli::ExpandSpec;
16use crate::config::{AppCommand, Binding, BindingAction, Config};
17use crate::jump::{Jump, JumpOutcome};
18use crate::keybindings::{
19 CLOSE_KEY, KeybindingEntry, KeybindingPanelState, TOGGLE_KEY, build_entries,
20};
21use crate::keys::Key;
22use crate::tree::{NodeId, Tree};
23
24#[derive(Clone, PartialEq, Debug)]
26pub enum Effect {
27 None,
28 Quit,
30 PrintAndExit(OsString),
32 Open(OsString),
34 RunShell {
36 cmd: String,
37 path: OsString,
38 relpath: OsString,
39 bg: bool,
40 exit: bool,
41 },
42}
43
44pub enum Mode {
48 Normal,
49 Jump(Jump),
50 Indexing,
53}
54
55const WORK_QUANTUM: usize = 500;
57
58pub struct App {
59 pub tree: Tree,
60 pub state: TreeListViewState<NodeId>,
61 pub query: TreeQuery,
62 pub mode: Mode,
64 keymap: HashMap<Key, Binding>,
65 pub(crate) panel_entries: Vec<KeybindingEntry>,
67 pub(crate) panel_user_entry_count: usize,
69 pub keybinding_panel: KeybindingPanelState,
70 root_history: Vec<Option<NodeId>>,
72 pub page_height: usize,
74 pub palette: Option<crate::ui::Palette>,
76}
77
78impl App {
79 pub fn new(tree: Tree, config: &Config, expand: Option<ExpandSpec>) -> Self {
80 let mut builtin_keymap = Self::default_keymap();
81 let user_keymap: HashMap<_, _> = config
84 .bindings
85 .iter()
86 .filter(|&(&key, _)| key != TOGGLE_KEY)
87 .map(|(&key, binding)| (key, binding.clone()))
88 .collect();
89 for key in user_keymap.keys() {
90 builtin_keymap.remove(key);
91 }
92
93 let mut panel_entries = build_entries(&user_keymap);
94 let panel_user_entry_count = panel_entries.len();
95 panel_entries.extend(build_entries(&builtin_keymap));
96
97 let mut keymap = builtin_keymap;
98 keymap.extend(user_keymap);
99 let mut app = Self {
100 tree,
101 state: TreeListViewState::with_capacity(0),
102 query: TreeQuery::new(),
103 mode: Mode::Normal,
104 keymap,
105 panel_entries,
106 panel_user_entry_count,
107 keybinding_panel: KeybindingPanelState::default(),
108 root_history: Vec::new(),
109 page_height: 20,
110 palette: None,
111 };
112 match expand {
113 None => {}
114 Some(spec) => {
115 app.tree.index_all();
117 let branches: Vec<_> = app.tree.branches().collect();
118 for (id, parent) in branches {
119 let expand = match spec {
120 ExpandSpec::All => true,
121 ExpandSpec::Depth(n) => app.tree.depth(id) < n,
122 };
123 if expand {
124 app.state.set_expanded(id, parent, true);
125 }
126 }
127 }
128 }
129 if app.tree.root_ids().len() == 1 {
131 let root = app.tree.root_ids()[0];
132 app.tree.ensure_children(root);
133 if !app.tree.is_leaf(root) {
134 app.state
135 .set_expanded(root, app.tree.view_parent(root), true);
136 }
137 }
138 app.state.ensure_projection(&app.tree, &app.query);
139 app.state.select_first();
140 app
141 }
142
143 pub fn default_keymap() -> HashMap<Key, Binding> {
145 let cmd = |action: AppCommand| Binding {
146 action: BindingAction::Cmd(action),
147 help: None,
148 exit: false,
149 bg: false,
150 };
151 let mut map = HashMap::new();
152 for (keys, action) in [
153 (&["j", "down"][..], AppCommand::Down),
154 (&["k", "up"], AppCommand::Up),
155 (&["l", "right"], AppCommand::Expand),
156 (&["h", "left"], AppCommand::Collapse),
157 (&["L", "shift+right"], AppCommand::ExpandRecursively),
158 (&["H", "shift+left"], AppCommand::CollapseRecursively),
159 (&["space"], AppCommand::Toggle),
160 (&["ctrl+space"], AppCommand::ToggleRecursively),
161 (&["enter"], AppCommand::Select),
162 (&["ctrl+enter"], AppCommand::Accept),
163 (&["alt+enter"], AppCommand::AcceptAlternate),
164 (&["tab"], AppCommand::Root),
165 (&["shift+tab"], AppCommand::PopRoot),
166 (&["J"], AppCommand::NextSibling),
167 (&["K"], AppCommand::PrevSibling),
168 (&["ctrl+f"], AppCommand::PageDown),
169 (&["ctrl+b"], AppCommand::PageUp),
170 (&["ctrl+d"], AppCommand::HalfPageDown),
171 (&["ctrl+u"], AppCommand::HalfPageUp),
172 (&["z"], AppCommand::Center),
173 (&["g"], AppCommand::First),
174 (&["G"], AppCommand::Last),
175 (&["/"], AppCommand::Jump),
176 (&["o"], AppCommand::Open),
177 (&["?"], AppCommand::ToggleKeybindingPanel),
178 (&["esc"], AppCommand::Back),
179 (&["q", "ctrl+c"], AppCommand::Quit),
180 ] {
181 for key in keys {
182 map.insert(Key::parse(key).expect("valid default key"), cmd(action));
183 }
184 }
185 map
186 }
187
188 pub fn focused_id(&mut self) -> Option<NodeId> {
189 self.state.ensure_projection(&self.tree, &self.query);
190 self.state.selected_id()
191 }
192
193 pub fn visible_names(&mut self) -> Vec<String> {
195 self.state.ensure_projection(&self.tree, &self.query);
196 self.state
197 .visible_ids()
198 .map(|id| self.tree.name(id))
199 .collect()
200 }
201
202 pub fn handle_key(&mut self, key: Key) -> Effect {
204 let _span = crate::profile::span("app::handle_key");
205 if let Mode::Indexing = self.mode {
207 if key == Key::parse("esc").unwrap() || key == Key::parse("ctrl+c").unwrap() {
208 self.mode = Mode::Normal;
209 }
210 return Effect::None;
211 }
212 if let Mode::Jump(jump) = &mut self.mode {
216 return match jump.handle_key(key) {
217 JumpOutcome::Stay => Effect::None,
218 JumpOutcome::Cancel => {
219 self.mode = Mode::Normal;
220 Effect::None
221 }
222 JumpOutcome::Accept(id) => {
223 self.mode = Mode::Normal;
224 self.state.select_by_id(&self.tree, &self.query, id);
225 Effect::None
226 }
227 };
228 }
229
230 if self.keybinding_panel.is_open() && key == CLOSE_KEY {
231 self.keybinding_panel.close();
232 return Effect::None;
233 }
234
235 match self.keymap.get(&key).cloned() {
236 None => Effect::None,
237 Some(binding) => match binding.action {
238 BindingAction::Cmd(cmd) => self.run_command(cmd),
239 BindingAction::Sh(cmd) => match self.focused_id() {
240 None => Effect::None,
241 Some(id) => Effect::RunShell {
242 cmd,
243 path: self.tree.path(id),
244 relpath: self.tree.relpath(id),
245 bg: binding.bg,
246 exit: binding.exit,
247 },
248 },
249 },
250 }
251 }
252
253 pub fn run_command(&mut self, cmd: AppCommand) -> Effect {
255 self.state.ensure_projection(&self.tree, &self.query);
256 match cmd {
257 AppCommand::Down => {
258 self.state.select_next();
259 }
260 AppCommand::Up => {
261 self.state.select_prev();
262 }
263 AppCommand::Expand => {
264 if let Some(id) = self.focused_id() {
265 self.tree.ensure_children(id);
266 let parent = self.tree.view_parent(id);
267 if self.tree.is_leaf(id) {
268 self.move_sibling(1);
270 } else if self.state.node_is_expanded(id, parent) {
271 self.state.select_id(Some(self.tree.children_of(id)[0]));
272 } else {
273 self.expand_and_reveal_children(id);
274 }
275 }
276 }
277 AppCommand::Collapse => {
278 if let Some(id) = self.focused_id() {
279 let parent = self.tree.view_parent(id);
280 if !self.tree.is_leaf(id) && self.state.node_is_expanded(id, parent) {
281 self.state.set_expanded(id, parent, false);
282 } else if let Some(parent) = parent {
283 self.state.select_id(Some(parent));
284 }
285 }
286 }
287 AppCommand::ExpandRecursively => {
288 if let Some(id) = self.focused_id() {
289 self.tree.ensure_children(id);
290 if self.tree.is_leaf(id) {
291 self.move_sibling(1);
293 } else {
294 self.set_expanded_recursively(id, true);
295 }
296 }
297 }
298 AppCommand::CollapseRecursively => {
299 if let Some(id) = self.focused_id() {
300 let parent = self.tree.view_parent(id);
301 if !self.tree.is_leaf(id) && self.state.node_is_expanded(id, parent) {
302 self.set_expanded_recursively(id, false);
303 } else if let Some(parent) = parent {
304 self.set_expanded_recursively(parent, false);
307 self.state.ensure_projection(&self.tree, &self.query);
308 self.state.select_id(Some(parent));
309 }
310 }
311 }
312 AppCommand::Toggle => self.toggle_focused(false),
313 AppCommand::ToggleRecursively => self.toggle_focused(true),
314 AppCommand::Select => {
315 if let Some(id) = self.focused_id() {
316 self.tree.ensure_children(id);
317 if self.tree.is_leaf(id) {
318 return Effect::PrintAndExit(self.tree.output(id));
319 }
320 self.expand_and_reveal_children(id);
321 }
322 }
323 AppCommand::Accept => {
324 if let Some(id) = self.focused_id() {
325 return Effect::PrintAndExit(self.tree.output(id));
326 }
327 }
328 AppCommand::AcceptAlternate => {
329 if let Some(id) = self.focused_id() {
330 return Effect::PrintAndExit(self.tree.alternate_output(id));
331 }
332 }
333 AppCommand::Descend => {
334 if let Some(id) = self.focused_branch() {
335 self.expand_and_reveal_children(id);
336 let first_child = self.tree.children_of(id)[0];
337 self.state.select_id(Some(first_child));
338 }
339 }
340 AppCommand::Root => self.push_root(),
341 AppCommand::PopRoot => {
342 self.pop_root();
343 }
344 AppCommand::Back => {
345 if !self.pop_root() {
346 return Effect::Quit;
347 }
348 }
349 AppCommand::NextSibling => self.move_sibling(1),
350 AppCommand::PrevSibling => self.move_sibling(-1),
351 AppCommand::PageDown => self.move_focus_by(self.page_height as isize),
352 AppCommand::PageUp => self.move_focus_by(-(self.page_height as isize)),
353 AppCommand::HalfPageDown => self.move_focus_by((self.page_height / 2) as isize),
354 AppCommand::HalfPageUp => self.move_focus_by(-((self.page_height / 2) as isize)),
355 AppCommand::Center => self.center_focused(),
356 AppCommand::First => {
357 self.state.select_first();
358 }
359 AppCommand::Last => {
360 self.state.select_last();
361 }
362 AppCommand::Jump => {
363 if self.tree.fully_indexed() {
366 self.mode = Mode::Jump(Jump::open(&self.tree));
367 } else {
368 self.mode = Mode::Indexing;
369 }
370 }
371 AppCommand::Open => {
372 if let Some(id) = self.focused_id()
375 && self.tree.is_leaf(id)
376 && let Some(path) = self.tree.filesystem_path(id)
377 {
378 return Effect::Open(path);
379 }
380 }
381 AppCommand::ToggleKeybindingPanel => self.keybinding_panel.toggle(),
382 AppCommand::Quit => return Effect::Quit,
383 }
384 Effect::None
385 }
386
387 fn focused_branch(&mut self) -> Option<NodeId> {
389 let id = self.focused_id()?;
390 self.tree.ensure_children(id);
391 (!self.tree.is_leaf(id)).then_some(id)
392 }
393
394 pub fn do_work(&mut self) -> bool {
401 let _span = crate::profile::span("app::do_work");
402 if !self.tree.fully_indexed() {
403 self.state.ensure_projection(&self.tree, &self.query);
404 let visible: Vec<NodeId> = self.state.visible_ids().collect();
405 let mut budget = WORK_QUANTUM;
406 for id in visible {
407 if budget == 0 {
408 break;
409 }
410 if self.tree.ensure_children(id) {
411 budget -= 1;
412 }
413 }
414 self.tree.index_some(budget);
415 }
416 if matches!(self.mode, Mode::Indexing) && self.tree.fully_indexed() {
417 self.mode = Mode::Jump(Jump::open(&self.tree));
418 }
419 !self.tree.fully_indexed()
420 }
421
422 pub fn report_error(&mut self, message: String) {
426 self.tree.record_error(message);
427 }
428
429 pub fn has_work(&self) -> bool {
431 !self.tree.fully_indexed()
432 }
433
434 fn toggle_focused(&mut self, recursive: bool) {
439 let Some(id) = self.focused_id() else { return };
440 self.tree.ensure_children(id);
441 if self.tree.is_leaf(id) {
442 return;
443 }
444 let parent = self.tree.view_parent(id);
445 let expand = !self.state.node_is_expanded(id, parent);
446 if recursive {
447 self.set_expanded_recursively(id, expand);
448 } else if expand {
449 self.expand_and_reveal_children(id);
450 } else {
451 self.state.set_expanded(id, parent, false);
452 }
453 }
454
455 fn set_expanded_recursively(&mut self, root: NodeId, expanded: bool) {
456 let mut stack = vec![root];
457 while let Some(id) = stack.pop() {
458 if expanded {
459 self.tree.ensure_children(id);
460 }
461 if !self.tree.is_leaf(id) {
462 self.state
463 .set_expanded(id, self.tree.view_parent(id), expanded);
464 stack.extend_from_slice(self.tree.children_of(id));
465 }
466 }
467 if expanded {
468 self.reveal_expanded_children(root);
469 }
470 }
471
472 fn expand_and_reveal_children(&mut self, id: NodeId) {
473 self.state.set_expanded(id, self.tree.view_parent(id), true);
474 self.reveal_expanded_children(id);
475 }
476
477 fn reveal_expanded_children(&mut self, id: NodeId) {
481 let height = self.page_height;
482 if height == 0 {
483 return;
484 }
485 self.state.ensure_projection(&self.tree, &self.query);
486 let Some(container_index) = self.state.visible_index_of(id) else {
487 return;
488 };
489 let Some(last_child_index) = self
490 .tree
491 .children_of(id)
492 .last()
493 .and_then(|&child| self.state.visible_index_of(child))
494 else {
495 return;
496 };
497 if last_child_index < self.state.offset().saturating_add(height) {
498 return;
499 }
500
501 let branch_rows = last_child_index.saturating_sub(container_index) + 1;
502 let offset = if branch_rows <= height {
503 last_child_index + 1 - height
504 } else {
505 container_index
506 };
507 self.state.set_offset(offset);
508 }
509
510 fn push_root(&mut self) {
511 let Some(id) = self.focused_id() else {
512 return;
513 };
514 if self.tree.view_root() == Some(id) {
515 return;
516 }
517 self.tree.ensure_children(id);
518
519 self.root_history.push(self.tree.view_root());
520 self.tree.set_view_root(Some(id));
521 if !self.tree.is_leaf(id) {
522 self.state.set_expanded(id, None, true);
523 }
524 self.state.ensure_projection(&self.tree, &self.query);
525 self.state.select_id(Some(id));
526 }
527
528 fn pop_root(&mut self) -> bool {
529 let Some(previous_root) = self.root_history.pop() else {
530 return false;
531 };
532 let selected = self.state.selected_id();
533
534 if let Some(current_root) = self.tree.view_root() {
535 let expanded = self.state.node_is_expanded(current_root, None);
536 self.state
537 .set_expanded(current_root, self.tree.parent(current_root), expanded);
538 }
539
540 self.tree.set_view_root(previous_root);
541 self.state.ensure_projection(&self.tree, &self.query);
542 if !selected.is_some_and(|id| self.state.select_by_id(&self.tree, &self.query, id)) {
543 self.state.select_first();
544 }
545 true
546 }
547
548 fn move_sibling(&mut self, delta: isize) {
549 let Some(id) = self.focused_id() else { return };
550 if self.tree.view_root() == Some(id) {
551 return;
552 }
553 let siblings = match self.tree.parent(id) {
554 Some(parent) => self.tree.children_of(parent),
555 None => self.tree.root_ids(),
556 };
557 let pos = siblings.iter().position(|&s| s == id).unwrap_or(0) as isize;
558 let target = pos + delta;
559 if (0..siblings.len() as isize).contains(&target) {
560 let target = siblings[target as usize];
561 self.state.select_id(Some(target));
562 }
563 }
564
565 fn move_focus_by(&mut self, delta: isize) {
566 let len = self.state.visible_len();
567 if len == 0 {
568 return;
569 }
570 let current = self.state.selected_index().unwrap_or(0) as isize;
571 let target = (current + delta).clamp(0, len as isize - 1);
572 self.state.select_index(Some(target as usize));
573 }
574
575 fn center_focused(&mut self) {
576 if self.page_height == 0 {
577 return;
578 }
579 let Some(selected) = self.state.selected_index() else {
580 return;
581 };
582 let maximum = self.state.visible_len().saturating_sub(self.page_height);
583 let centered = selected.saturating_sub(self.page_height / 2);
584 self.state.set_offset(centered.min(maximum));
585 }
586}
587
588#[cfg(test)]
589mod tests {
590 use super::*;
591 use crate::fstree;
592
593 fn fixture() -> (tempfile::TempDir, Tree) {
603 let dir = tempfile::tempdir().unwrap();
604 let p = dir.path();
605 std::fs::create_dir_all(p.join("a/aa")).unwrap();
606 std::fs::write(p.join("a/aa/aaa.txt"), "").unwrap();
607 std::fs::write(p.join("a/ab.txt"), "").unwrap();
608 std::fs::create_dir(p.join("b")).unwrap();
609 std::fs::write(p.join("b/ba.txt"), "").unwrap();
610 std::fs::write(p.join("c.txt"), "").unwrap();
611 let tree = fstree::scan(p, false).unwrap();
612 (dir, tree)
613 }
614
615 fn app() -> (tempfile::TempDir, App) {
616 let (dir, tree) = fixture();
617 (dir, App::new(tree, &Config::default(), None))
618 }
619
620 fn app_with_leaf_siblings() -> (tempfile::TempDir, App) {
626 let dir = tempfile::tempdir().unwrap();
627 let p = dir.path();
628 std::fs::create_dir(p.join("d")).unwrap();
629 std::fs::write(p.join("d/d1.txt"), "").unwrap();
630 std::fs::write(p.join("d/d2.txt"), "").unwrap();
631 let tree = fstree::scan(p, false).unwrap();
632 (dir, App::new(tree, &Config::default(), None))
633 }
634
635 fn app_with_branch_at_bottom() -> (tempfile::TempDir, App) {
642 let dir = tempfile::tempdir().unwrap();
643 let p = dir.path();
644 std::fs::create_dir(p.join("a")).unwrap();
645 std::fs::create_dir(p.join("b")).unwrap();
646 std::fs::create_dir(p.join("d")).unwrap();
647 std::fs::write(p.join("d/d1.txt"), "").unwrap();
648 std::fs::write(p.join("d/d2.txt"), "").unwrap();
649 let tree = fstree::scan(p, false).unwrap();
650 (dir, App::new(tree, &Config::default(), None))
651 }
652
653 fn focused_name(app: &mut App) -> String {
654 let id = app.focused_id().expect("something focused");
655 app.tree.name(id)
656 }
657
658 #[test]
659 fn starts_focused_on_first_row_all_collapsed() {
660 let (_d, mut app) = app();
661 assert_eq!(app.visible_names(), ["a", "b", "c.txt"]);
662 assert_eq!(focused_name(&mut app), "a");
663 }
664
665 #[test]
666 fn down_and_up_move_focus_clamped() {
667 let (_d, mut app) = app();
668 app.run_command(AppCommand::Down);
669 assert_eq!(focused_name(&mut app), "b");
670 app.run_command(AppCommand::Down);
671 assert_eq!(focused_name(&mut app), "c.txt");
672 app.run_command(AppCommand::Down);
673 assert_eq!(focused_name(&mut app), "c.txt");
674 app.run_command(AppCommand::Up);
675 assert_eq!(focused_name(&mut app), "b");
676 }
677
678 #[test]
679 fn expand_reveals_children_and_down_enters_them() {
680 let (_d, mut app) = app();
681 app.run_command(AppCommand::Expand);
682 assert_eq!(app.visible_names(), ["a", "aa", "ab.txt", "b", "c.txt"]);
683 app.run_command(AppCommand::Down);
684 assert_eq!(focused_name(&mut app), "aa");
685 }
686
687 #[test]
688 fn expanding_scrolls_just_enough_to_reveal_last_child_when_branch_fits() {
689 let (_d, mut app) = app_with_branch_at_bottom();
690 app.page_height = 4;
691 app.run_command(AppCommand::Down);
692 app.run_command(AppCommand::Down); app.run_command(AppCommand::Expand);
695
696 assert_eq!(app.state.offset(), 1);
697 }
698
699 #[test]
700 fn expanding_an_oversized_branch_places_container_at_viewport_top() {
701 let (_d, mut app) = app_with_branch_at_bottom();
702 app.page_height = 2;
703 app.run_command(AppCommand::Down);
704 app.run_command(AppCommand::Down); app.run_command(AppCommand::Expand);
707
708 assert_eq!(app.state.offset(), 2);
709 }
710
711 #[test]
712 fn l_on_expanded_branch_descends_to_first_child() {
713 let (_d, mut app) = app();
714 app.handle_key(Key::parse("l").unwrap());
715
716 app.handle_key(Key::parse("l").unwrap());
717
718 assert_eq!(focused_name(&mut app), "aa");
719 }
720
721 #[test]
722 fn l_on_leaf_focuses_next_sibling() {
723 let (_d, mut app) = app_with_leaf_siblings();
724 app.run_command(AppCommand::Expand); app.run_command(AppCommand::Down); app.handle_key(Key::parse("l").unwrap());
728
729 assert_eq!(focused_name(&mut app), "d2.txt");
730 assert_eq!(app.visible_names(), ["d", "d1.txt", "d2.txt"]);
731 }
732
733 #[test]
734 fn l_on_last_leaf_of_a_container_stays_inside_it() {
735 let (_d, mut app) = app();
736 app.run_command(AppCommand::Expand); app.run_command(AppCommand::Down); app.run_command(AppCommand::Down); app.handle_key(Key::parse("l").unwrap());
741
742 assert_eq!(focused_name(&mut app), "ab.txt");
744 assert_eq!(app.visible_names(), ["a", "aa", "ab.txt", "b", "c.txt"]);
745 }
746
747 #[test]
748 fn expand_on_the_last_top_level_leaf_is_a_noop() {
749 let (_d, mut app) = app();
750 app.run_command(AppCommand::Last);
751 assert_eq!(focused_name(&mut app), "c.txt");
752 assert_eq!(app.run_command(AppCommand::Expand), Effect::None);
753 assert_eq!(focused_name(&mut app), "c.txt");
754 assert_eq!(app.visible_names(), ["a", "b", "c.txt"]);
755 }
756
757 #[test]
758 fn collapse_hides_children() {
759 let (_d, mut app) = app();
760 app.run_command(AppCommand::Expand);
761 app.run_command(AppCommand::Collapse);
762 assert_eq!(app.visible_names(), ["a", "b", "c.txt"]);
763 }
764
765 #[test]
766 fn h_on_leaf_focuses_parent_without_collapsing_it() {
767 let (_d, mut app) = app();
768 app.run_command(AppCommand::Expand);
769 app.run_command(AppCommand::Down); app.run_command(AppCommand::Expand);
771 app.run_command(AppCommand::Down); app.handle_key(Key::parse("h").unwrap());
774
775 assert_eq!(focused_name(&mut app), "aa");
776 assert_eq!(
777 app.visible_names(),
778 ["a", "aa", "aaa.txt", "ab.txt", "b", "c.txt"]
779 );
780 }
781
782 #[test]
783 fn h_on_collapsed_branch_focuses_parent_without_collapsing_it() {
784 let (_d, mut app) = app();
785 app.run_command(AppCommand::Expand);
786 app.run_command(AppCommand::Down); app.handle_key(Key::parse("h").unwrap());
789
790 assert_eq!(focused_name(&mut app), "a");
791 assert_eq!(app.visible_names(), ["a", "aa", "ab.txt", "b", "c.txt"]);
792 }
793
794 #[test]
795 fn expand_recursively_expands_whole_subtree() {
796 let (_d, mut app) = app();
797 app.run_command(AppCommand::ExpandRecursively);
798 assert_eq!(
799 app.visible_names(),
800 ["a", "aa", "aaa.txt", "ab.txt", "b", "c.txt"]
801 );
802 }
803
804 #[test]
805 fn space_toggles_a_container_open_and_shut() {
806 let (_d, mut app) = app();
807
808 app.handle_key(Key::parse("space").unwrap());
809 assert_eq!(app.visible_names(), ["a", "aa", "ab.txt", "b", "c.txt"]);
810 assert_eq!(focused_name(&mut app), "a");
811
812 app.handle_key(Key::parse("space").unwrap());
813 assert_eq!(app.visible_names(), ["a", "b", "c.txt"]);
814 assert_eq!(focused_name(&mut app), "a");
815 }
816
817 #[test]
818 fn space_on_a_leaf_does_nothing() {
819 let (_d, mut app) = app();
820 app.run_command(AppCommand::Last); app.handle_key(Key::parse("space").unwrap());
823
824 assert_eq!(focused_name(&mut app), "c.txt");
825 assert_eq!(app.visible_names(), ["a", "b", "c.txt"]);
826 }
827
828 #[test]
829 fn ctrl_space_toggles_a_container_recursively() {
830 let (_d, mut app) = app();
831
832 app.handle_key(Key::parse("ctrl+space").unwrap());
833 assert_eq!(
834 app.visible_names(),
835 ["a", "aa", "aaa.txt", "ab.txt", "b", "c.txt"]
836 );
837 assert_eq!(focused_name(&mut app), "a");
838
839 app.handle_key(Key::parse("ctrl+space").unwrap());
840 assert_eq!(app.visible_names(), ["a", "b", "c.txt"]);
841 app.run_command(AppCommand::Expand);
843 assert_eq!(app.visible_names(), ["a", "aa", "ab.txt", "b", "c.txt"]);
844 }
845
846 #[test]
847 fn ctrl_space_direction_follows_the_focused_container() {
848 let (_d, mut app) = app();
849 app.run_command(AppCommand::Expand); app.handle_key(Key::parse("ctrl+space").unwrap());
853
854 assert_eq!(app.visible_names(), ["a", "b", "c.txt"]);
855 }
856
857 #[test]
858 fn shift_l_on_a_container_leaves_focus_on_it() {
859 let (_d, mut app) = app();
860
861 app.handle_key(Key::parse("L").unwrap());
862
863 assert_eq!(focused_name(&mut app), "a");
864 }
865
866 #[test]
867 fn shift_l_on_leaf_focuses_next_sibling() {
868 let (_d, mut app) = app_with_leaf_siblings();
869 app.run_command(AppCommand::Expand); app.run_command(AppCommand::Down); app.handle_key(Key::parse("L").unwrap());
873
874 assert_eq!(focused_name(&mut app), "d2.txt");
875 assert_eq!(app.visible_names(), ["d", "d1.txt", "d2.txt"]);
876 }
877
878 #[test]
879 fn shift_l_on_the_last_top_level_leaf_is_a_noop() {
880 let (_d, mut app) = app();
881 app.run_command(AppCommand::Last); app.handle_key(Key::parse("L").unwrap());
884
885 assert_eq!(focused_name(&mut app), "c.txt");
886 assert_eq!(app.visible_names(), ["a", "b", "c.txt"]);
887 }
888
889 #[test]
890 fn collapse_recursively_collapses_whole_subtree() {
891 let (_d, mut app) = app();
892 app.run_command(AppCommand::ExpandRecursively);
893 app.run_command(AppCommand::CollapseRecursively);
894 assert_eq!(app.visible_names(), ["a", "b", "c.txt"]);
895 app.run_command(AppCommand::Expand);
897 assert_eq!(app.visible_names(), ["a", "aa", "ab.txt", "b", "c.txt"]);
898 }
899
900 #[test]
901 fn shift_h_on_leaf_collapses_parent_and_focuses_it() {
902 let (_d, mut app) = app();
903 app.run_command(AppCommand::ExpandRecursively);
904 app.run_command(AppCommand::Down); app.run_command(AppCommand::Down); app.handle_key(Key::parse("H").unwrap());
908
909 assert_eq!(focused_name(&mut app), "aa");
910 assert_eq!(app.visible_names(), ["a", "aa", "ab.txt", "b", "c.txt"]);
911 }
912
913 #[test]
914 fn shift_h_on_collapsed_branch_collapses_parent_and_focuses_it() {
915 let (_d, mut app) = app();
916 app.run_command(AppCommand::Expand);
917 app.run_command(AppCommand::Down); app.handle_key(Key::parse("H").unwrap());
920
921 assert_eq!(focused_name(&mut app), "a");
922 assert_eq!(app.visible_names(), ["a", "b", "c.txt"]);
923 }
924
925 #[test]
926 fn shift_h_collapses_the_parent_recursively() {
927 let (_d, mut app) = app();
928 app.run_command(AppCommand::ExpandRecursively);
929 app.run_command(AppCommand::Down); app.run_command(AppCommand::Down); app.run_command(AppCommand::Down); app.handle_key(Key::parse("H").unwrap());
934
935 assert_eq!(focused_name(&mut app), "a");
936 assert_eq!(app.visible_names(), ["a", "b", "c.txt"]);
937 app.run_command(AppCommand::Expand);
939 assert_eq!(app.visible_names(), ["a", "aa", "ab.txt", "b", "c.txt"]);
940 }
941
942 #[test]
943 fn shift_h_on_a_top_level_leaf_leaves_focus_alone() {
944 let (_d, mut app) = app();
945 app.run_command(AppCommand::Last); app.handle_key(Key::parse("H").unwrap());
948
949 assert_eq!(focused_name(&mut app), "c.txt");
950 assert_eq!(app.visible_names(), ["a", "b", "c.txt"]);
951 }
952
953 #[test]
954 fn select_expands_collapsed_dir_and_prints_leaf() {
955 let (_d, mut app) = app();
956 assert_eq!(app.run_command(AppCommand::Select), Effect::None);
957 assert_eq!(app.visible_names(), ["a", "aa", "ab.txt", "b", "c.txt"]);
958 app.run_command(AppCommand::Last);
959 let effect = app.run_command(AppCommand::Select);
960 let Effect::PrintAndExit(path) = effect else {
961 panic!("expected PrintAndExit, got {effect:?}");
962 };
963 assert!(std::path::Path::new(&path).is_absolute());
964 assert!(std::path::Path::new(&path).ends_with("c.txt"));
965 }
966
967 #[test]
968 fn select_does_not_descend_into_an_expanded_branch() {
969 let (_d, mut app) = app();
970 app.run_command(AppCommand::Select);
971
972 app.run_command(AppCommand::Select);
973
974 assert_eq!(focused_name(&mut app), "a");
975 }
976
977 #[test]
978 fn accept_prints_even_on_dir() {
979 let (_d, mut app) = app();
980 let effect = app.run_command(AppCommand::Accept);
981 let Effect::PrintAndExit(path) = effect else {
982 panic!("expected PrintAndExit, got {effect:?}");
983 };
984 assert!(std::path::Path::new(&path).ends_with("a"));
985 }
986
987 #[test]
988 fn alt_enter_prints_the_filesystem_basename() {
989 let (_d, mut app) = app();
990
991 assert_eq!(
992 app.handle_key(Key::parse("alt+enter").unwrap()),
993 Effect::PrintAndExit(OsString::from("a"))
994 );
995 }
996
997 #[test]
998 fn o_opens_the_focused_leaf_with_its_absolute_path() {
999 let (_d, mut app) = app();
1000 app.run_command(AppCommand::Last); let effect = app.handle_key(Key::parse("o").unwrap());
1003
1004 let Effect::Open(path) = effect else {
1005 panic!("expected Open, got {effect:?}");
1006 };
1007 assert!(std::path::Path::new(&path).is_absolute());
1008 assert!(std::path::Path::new(&path).ends_with("c.txt"));
1009 }
1010
1011 #[test]
1012 fn o_on_a_container_does_nothing() {
1013 let (_d, mut app) = app();
1014 assert_eq!(focused_name(&mut app), "a"); assert_eq!(app.handle_key(Key::parse("o").unwrap()), Effect::None);
1017 }
1018
1019 #[test]
1020 fn o_on_a_json_leaf_does_nothing() {
1021 let tree = crate::json_tree::from_reader(r#"{"a": 1}"#.as_bytes()).unwrap();
1023 let mut app = App::new(tree, &Config::default(), None);
1024 app.run_command(AppCommand::Last);
1025
1026 assert_eq!(app.handle_key(Key::parse("o").unwrap()), Effect::None);
1027 }
1028
1029 #[test]
1030 fn descend_expands_and_focuses_first_child() {
1031 let (_d, mut app) = app();
1032 app.run_command(AppCommand::Descend);
1033 assert_eq!(focused_name(&mut app), "aa");
1034 }
1035
1036 #[test]
1037 fn tab_pushes_focused_nodes_as_view_roots_and_shift_tab_pops_them() {
1038 let (_d, mut app) = app();
1039
1040 app.handle_key(Key::parse("tab").unwrap());
1041 assert_eq!(focused_name(&mut app), "a");
1042 assert_eq!(app.visible_names(), ["a", "aa", "ab.txt"]);
1043
1044 app.handle_key(Key::parse("l").unwrap()); app.handle_key(Key::parse("tab").unwrap());
1046 assert_eq!(focused_name(&mut app), "aa");
1047 assert_eq!(app.visible_names(), ["aa", "aaa.txt"]);
1048
1049 app.handle_key(Key::parse("h").unwrap()); assert_eq!(app.visible_names(), ["aa"]);
1052 app.handle_key(Key::parse("h").unwrap()); assert_eq!(focused_name(&mut app), "aa");
1054
1055 app.handle_key(Key::parse("l").unwrap()); assert_eq!(app.visible_names(), ["aa", "aaa.txt"]);
1057
1058 app.handle_key(Key::parse("shift+tab").unwrap());
1059 assert_eq!(focused_name(&mut app), "aa");
1060 assert_eq!(app.visible_names(), ["a", "aa", "aaa.txt", "ab.txt"]);
1061
1062 app.handle_key(Key::parse("shift+tab").unwrap());
1063 assert_eq!(focused_name(&mut app), "aa");
1064 assert_eq!(
1065 app.visible_names(),
1066 ["a", "aa", "aaa.txt", "ab.txt", "b", "c.txt"]
1067 );
1068
1069 app.handle_key(Key::parse("shift+tab").unwrap());
1071 assert_eq!(focused_name(&mut app), "aa");
1072 assert_eq!(
1073 app.visible_names(),
1074 ["a", "aa", "aaa.txt", "ab.txt", "b", "c.txt"]
1075 );
1076 }
1077
1078 #[test]
1079 fn tab_can_make_a_leaf_the_view_root() {
1080 let (_d, mut app) = app();
1081 app.run_command(AppCommand::Last);
1082
1083 app.handle_key(Key::parse("tab").unwrap());
1084
1085 assert_eq!(focused_name(&mut app), "c.txt");
1086 assert_eq!(app.visible_names(), ["c.txt"]);
1087 }
1088
1089 #[test]
1090 fn escape_pops_one_root_at_a_time_then_quits() {
1091 let (_d, mut app) = app();
1092 app.handle_key(Key::parse("tab").unwrap()); app.handle_key(Key::parse("l").unwrap()); app.handle_key(Key::parse("tab").unwrap()); assert_eq!(app.handle_key(Key::parse("esc").unwrap()), Effect::None);
1097 assert_eq!(focused_name(&mut app), "aa");
1098 assert_eq!(app.visible_names(), ["a", "aa", "aaa.txt", "ab.txt"]);
1099
1100 assert_eq!(app.handle_key(Key::parse("esc").unwrap()), Effect::None);
1101 assert_eq!(focused_name(&mut app), "aa");
1102 assert_eq!(
1103 app.visible_names(),
1104 ["a", "aa", "aaa.txt", "ab.txt", "b", "c.txt"]
1105 );
1106
1107 assert_eq!(app.handle_key(Key::parse("esc").unwrap()), Effect::Quit);
1108 }
1109
1110 #[test]
1111 fn sibling_navigation_skips_expanded_children() {
1112 let (_d, mut app) = app();
1113 app.run_command(AppCommand::Expand); app.run_command(AppCommand::NextSibling);
1115 assert_eq!(focused_name(&mut app), "b");
1116 app.run_command(AppCommand::PrevSibling);
1117 assert_eq!(focused_name(&mut app), "a");
1118 app.run_command(AppCommand::PrevSibling);
1120 assert_eq!(focused_name(&mut app), "a");
1121 }
1122
1123 #[test]
1124 fn first_and_last() {
1125 let (_d, mut app) = app();
1126 app.run_command(AppCommand::Last);
1127 assert_eq!(focused_name(&mut app), "c.txt");
1128 app.run_command(AppCommand::First);
1129 assert_eq!(focused_name(&mut app), "a");
1130 }
1131
1132 #[test]
1133 fn paging_moves_focus_by_page_amounts() {
1134 let (_d, mut app) = app();
1135 app.run_command(AppCommand::ExpandRecursively); app.page_height = 4;
1137 app.run_command(AppCommand::HalfPageDown);
1138 assert_eq!(focused_name(&mut app), "aaa.txt"); app.run_command(AppCommand::PageDown);
1140 assert_eq!(focused_name(&mut app), "c.txt"); app.run_command(AppCommand::HalfPageUp);
1142 assert_eq!(focused_name(&mut app), "ab.txt");
1143 app.run_command(AppCommand::PageUp);
1144 assert_eq!(focused_name(&mut app), "a");
1145 }
1146
1147 #[test]
1148 fn z_centers_the_focused_node_in_the_viewport() {
1149 let (_d, mut app) = app();
1150 app.run_command(AppCommand::ExpandRecursively); app.page_height = 3;
1152 for _ in 0..3 {
1153 app.run_command(AppCommand::Down); }
1155 app.state.set_offset(0);
1156
1157 app.handle_key(Key::parse("z").unwrap());
1158
1159 assert_eq!(focused_name(&mut app), "ab.txt");
1160 assert_eq!(app.state.offset(), 2);
1161 }
1162
1163 #[test]
1164 fn default_keys_drive_commands() {
1165 let (_d, mut app) = app();
1166 app.handle_key(Key::parse("j").unwrap());
1167 assert_eq!(focused_name(&mut app), "b");
1168 app.handle_key(Key::parse("k").unwrap());
1169 assert_eq!(focused_name(&mut app), "a");
1170 app.handle_key(Key::parse("l").unwrap());
1171 assert_eq!(app.visible_names().len(), 5);
1172 app.handle_key(Key::parse("h").unwrap());
1173 assert_eq!(app.visible_names().len(), 3);
1174 assert_eq!(app.handle_key(Key::parse("q").unwrap()), Effect::Quit);
1175 assert_eq!(app.handle_key(Key::parse("esc").unwrap()), Effect::Quit);
1176 assert_eq!(app.handle_key(Key::parse("ctrl+c").unwrap()), Effect::Quit);
1177 }
1178
1179 #[test]
1180 fn g_goes_to_first_line_without_a_chord() {
1181 let (_d, mut app) = app();
1182 app.run_command(AppCommand::Last);
1183 assert_eq!(app.handle_key(Key::parse("g").unwrap()), Effect::None);
1184 assert_eq!(focused_name(&mut app), "a");
1185 }
1186
1187 #[test]
1188 fn shift_g_goes_to_last_visible_line() {
1189 let (_d, mut app) = app();
1190 app.handle_key(Key::parse("G").unwrap());
1191 assert_eq!(focused_name(&mut app), "c.txt");
1192 }
1193
1194 #[test]
1195 fn user_binding_produces_shell_effect_with_paths() {
1196 let (_d, tree) = fixture();
1197 let config = Config::parse("[ctrl+e]\nsh = \"vim $path\"\nexit = true\n").unwrap();
1198 let mut app = App::new(tree, &config, None);
1199 app.run_command(AppCommand::Down); let effect = app.handle_key(Key::parse("ctrl+e").unwrap());
1201 let Effect::RunShell {
1202 cmd,
1203 path,
1204 relpath,
1205 bg,
1206 exit,
1207 } = effect
1208 else {
1209 panic!("expected RunShell, got {effect:?}");
1210 };
1211 assert_eq!(cmd, "vim $path");
1212 assert!(std::path::Path::new(&path).is_absolute());
1213 assert!(std::path::Path::new(&path).ends_with("b"));
1214 assert_eq!(relpath, OsString::from("b"));
1215 assert!(!bg);
1216 assert!(exit);
1217 }
1218
1219 #[test]
1220 fn user_binding_overrides_default() {
1221 let (_d, tree) = fixture();
1222 let config = Config::parse("[j]\ncmd = \"quit\"\n").unwrap();
1223 let mut app = App::new(tree, &config, None);
1224 assert_eq!(app.handle_key(Key::parse("j").unwrap()), Effect::Quit);
1225 }
1226
1227 #[test]
1228 fn user_can_override_the_new_g_binding() {
1229 let (_d, tree) = fixture();
1230 let config = Config::parse("[g]\ncmd = \"quit\"\n").unwrap();
1231 let mut app = App::new(tree, &config, None);
1232 assert_eq!(app.handle_key(Key::parse("g").unwrap()), Effect::Quit);
1233 }
1234
1235 #[test]
1236 fn question_mark_is_reserved_and_toggles_the_panel() {
1237 let (_d, tree) = fixture();
1238 let config = Config::parse("[?]\ncmd = \"quit\"\nhelp = \"Wrong\"\n").unwrap();
1239 let mut app = App::new(tree, &config, None);
1240
1241 let question: Vec<_> = app
1244 .panel_entries
1245 .iter()
1246 .filter(|entry| entry.key == Key::parse("?").unwrap())
1247 .collect();
1248 assert_eq!(question.len(), 1);
1249 assert_eq!(question[0].description, "Shortcuts");
1250
1251 assert!(!app.keybinding_panel.is_open());
1252 assert_eq!(app.handle_key(Key::parse("?").unwrap()), Effect::None);
1253 assert!(app.keybinding_panel.is_open());
1254 assert_eq!(app.handle_key(Key::parse("?").unwrap()), Effect::None);
1255 assert!(!app.keybinding_panel.is_open());
1256 }
1257
1258 #[test]
1259 fn open_panel_stays_open_while_bindings_run_and_escape_only_closes_it() {
1260 let (_d, mut app) = app();
1261 app.handle_key(Key::parse("?").unwrap());
1262
1263 app.handle_key(Key::parse("j").unwrap());
1264 assert_eq!(focused_name(&mut app), "b");
1265 assert!(app.keybinding_panel.is_open());
1266
1267 assert_eq!(app.handle_key(Key::parse("esc").unwrap()), Effect::None);
1268 assert!(!app.keybinding_panel.is_open());
1269 assert_eq!(focused_name(&mut app), "b");
1270 }
1271
1272 #[test]
1273 fn jump_owns_question_mark_and_escape_without_dismissing_the_panel() {
1274 let (_d, mut app) = app();
1275 while app.do_work() {}
1276 app.handle_key(Key::parse("?").unwrap());
1277 app.handle_key(Key::parse("/").unwrap());
1278 app.handle_key(Key::parse("?").unwrap());
1279
1280 let Mode::Jump(jump) = &app.mode else {
1281 panic!("expected jump mode");
1282 };
1283 assert_eq!(jump.query(), "?");
1284 assert!(app.keybinding_panel.is_open());
1285
1286 app.handle_key(Key::parse("esc").unwrap());
1287 assert!(matches!(app.mode, Mode::Normal));
1288 assert!(app.keybinding_panel.is_open());
1289 }
1290
1291 #[test]
1292 fn unbound_key_is_noop() {
1293 let (_d, mut app) = app();
1294 assert_eq!(app.handle_key(Key::parse("x").unwrap()), Effect::None);
1295 }
1296
1297 #[test]
1298 fn initial_expand_depth_one_expands_top_level_only() {
1299 let (_d, tree) = fixture();
1300 let mut app = App::new(tree, &Config::default(), Some(ExpandSpec::Depth(1)));
1301 assert_eq!(
1302 app.visible_names(),
1303 ["a", "aa", "ab.txt", "b", "ba.txt", "c.txt"]
1304 );
1305 }
1306
1307 #[test]
1308 fn initial_expand_all_expands_everything() {
1309 let (_d, tree) = fixture();
1310 let mut app = App::new(tree, &Config::default(), Some(ExpandSpec::All));
1311 assert_eq!(
1312 app.visible_names(),
1313 ["a", "aa", "aaa.txt", "ab.txt", "b", "ba.txt", "c.txt"]
1314 );
1315 }
1316
1317 fn in_jump(app: &App) -> bool {
1318 matches!(app.mode, Mode::Jump(_))
1319 }
1320
1321 #[test]
1322 fn slash_opens_the_jump_picker() {
1323 let (_d, mut app) = app();
1324 assert!(!in_jump(&app));
1325 while app.do_work() {} app.handle_key(Key::parse("/").unwrap());
1327 assert!(in_jump(&app));
1328 }
1329
1330 #[test]
1331 fn cancelling_the_picker_leaves_focus_untouched() {
1332 let (_d, mut app) = app();
1333 app.run_command(AppCommand::Down); assert_eq!(focused_name(&mut app), "b");
1335 app.handle_key(Key::parse("/").unwrap());
1336 app.handle_key(Key::parse("a").unwrap()); app.handle_key(Key::parse("esc").unwrap());
1338 assert!(!in_jump(&app));
1339 assert_eq!(focused_name(&mut app), "b");
1340 }
1341
1342 #[test]
1343 fn accepting_jumps_focus_and_expands_ancestors() {
1344 let (_d, mut app) = app();
1345 assert_eq!(app.visible_names(), ["a", "b", "c.txt"]);
1347 while app.do_work() {} app.handle_key(Key::parse("/").unwrap());
1349 for k in ["a", "a", "a"] {
1350 app.handle_key(Key::parse(k).unwrap()); }
1352 app.handle_key(Key::parse("enter").unwrap());
1353 assert!(!in_jump(&app));
1354 assert_eq!(focused_name(&mut app), "aaa.txt");
1355 assert!(app.visible_names().contains(&"aaa.txt".to_string()));
1357 }
1358
1359 #[test]
1360 fn a_user_can_rebind_jump_off_slash() {
1361 let (_d, tree) = fixture();
1362 let config = Config::parse("[ctrl+p]\ncmd = \"jump\"\n").unwrap();
1363 let mut app = App::new(tree, &config, None);
1364 while app.do_work() {} app.handle_key(Key::parse("ctrl+p").unwrap());
1366 assert!(in_jump(&app));
1367 }
1368
1369 #[test]
1370 fn a_single_rooted_tree_opens_with_its_first_level_expanded() {
1371 use crate::tree::ActionValues;
1372 let mut tree = Tree::new();
1373 let root = tree.push(None, "root", true, ActionValues::new("", "", ""));
1374 tree.push(Some(root), "child", false, ActionValues::new("", "", ""));
1375 let mut app = App::new(tree, &Config::default(), None);
1376
1377 assert_eq!(app.visible_names(), ["root", "child"]);
1378 assert_eq!(focused_name(&mut app), "root");
1379 }
1380
1381 #[test]
1382 fn expanding_an_unindexed_container_materializes_its_children() {
1383 let tree =
1384 crate::json_tree::from_reader(r#"{"users": [1, 2], "n": 3}"#.as_bytes()).unwrap();
1385 let mut app = App::new(tree, &Config::default(), None);
1386 assert_eq!(app.visible_names(), ["users [2]", "n: 3"]);
1387
1388 app.handle_key(Key::parse("l").unwrap());
1389
1390 assert_eq!(
1391 app.visible_names(),
1392 ["users [2]", "[0]: 1", "[1]: 2", "n: 3"]
1393 );
1394 }
1395
1396 #[test]
1397 fn jump_blocks_on_an_incomplete_index_and_opens_when_done() {
1398 let tree =
1399 crate::json_tree::from_reader(r#"{"a": {"b": 1}, "c": {"d": 2}}"#.as_bytes()).unwrap();
1400 let mut app = App::new(tree, &Config::default(), None);
1401 assert!(app.has_work());
1402
1403 app.handle_key(Key::parse("/").unwrap());
1404 assert!(matches!(app.mode, Mode::Indexing));
1405 assert_eq!(app.handle_key(Key::parse("j").unwrap()), Effect::None);
1407 assert!(matches!(app.mode, Mode::Indexing));
1408
1409 while app.do_work() {}
1410
1411 assert!(matches!(app.mode, Mode::Jump(_)));
1412 assert!(!app.has_work());
1413 }
1414
1415 #[test]
1416 fn escape_cancels_the_indexing_wait() {
1417 let tree =
1418 crate::json_tree::from_reader(r#"{"a": {"b": 1}, "c": {"d": 2}}"#.as_bytes()).unwrap();
1419 let mut app = App::new(tree, &Config::default(), None);
1420 app.handle_key(Key::parse("/").unwrap());
1421 assert!(matches!(app.mode, Mode::Indexing));
1422
1423 app.handle_key(Key::parse("esc").unwrap());
1424
1425 assert!(matches!(app.mode, Mode::Normal));
1426 }
1427}