Skip to main content

ite_cli/
app.rs

1//! I/O-free interaction coordinator. `App` owns the tree-list state, effective
2//! keymap, expansion/focus behavior, modal jump picker, keybinding-panel state,
3//! and the execution of configured `AppCommand`s.
4//!
5//! Key handling mutates application state or returns an `Effect` for the
6//! executable to perform; this module neither draws nor touches the terminal,
7//! filesystem, stdout, or subprocesses. Modal state machines live in their own
8//! modules and are routed from here.
9
10use 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/// What the event loop must do after a key is handled.
25#[derive(Clone, PartialEq, Debug)]
26pub enum Effect {
27    None,
28    /// Exit without output.
29    Quit,
30    /// The default action: print the node's source-specific value and exit.
31    PrintAndExit(OsString),
32    /// Run a configured shell command on the focused node.
33    RunShell {
34        cmd: String,
35        path: OsString,
36        relpath: OsString,
37        bg: bool,
38        exit: bool,
39    },
40}
41
42/// The app's input mode. `Normal` drives the tree via the keymap; other modes
43/// take over key handling and rendering until they close. Adding a mode is a
44/// new variant plus one dispatch arm in `handle_key` and one in `ui::draw`.
45pub enum Mode {
46    Normal,
47    Jump(Jump),
48    /// Blocking on index completion before the jump picker can open; a
49    /// progress line renders until the sweep finishes (esc cancels).
50    Indexing,
51}
52
53/// Nodes loaded per cooperative work quantum in [`App::do_work`].
54const WORK_QUANTUM: usize = 500;
55
56pub struct App {
57    pub tree: Tree,
58    pub state: TreeListViewState<NodeId>,
59    pub query: TreeQuery,
60    /// The current input mode; see [`Mode`].
61    pub mode: Mode,
62    keymap: HashMap<Key, Binding>,
63    /// User entries followed by non-overridden built-ins, built once at startup.
64    pub(crate) panel_entries: Vec<KeybindingEntry>,
65    /// The leading entries that came from configuration rather than defaults.
66    pub(crate) panel_user_entry_count: usize,
67    pub keybinding_panel: KeybindingPanelState,
68    /// Temporary view roots, from the original forest to the current subtree.
69    root_history: Vec<Option<NodeId>>,
70    /// Rows per screen; the UI updates this every frame.
71    pub page_height: usize,
72    /// Terminal default colors, when the terminal answered the startup query.
73    pub palette: Option<crate::ui::Palette>,
74}
75
76impl App {
77    pub fn new(tree: Tree, config: &Config, expand: Option<ExpandSpec>) -> Self {
78        let mut builtin_keymap = Self::default_keymap();
79        // `?` is reserved for the panel toggle: a configured `[?]` table is
80        // accepted but silently ignored.
81        let user_keymap: HashMap<_, _> = config
82            .bindings
83            .iter()
84            .filter(|&(&key, _)| key != TOGGLE_KEY)
85            .map(|(&key, binding)| (key, binding.clone()))
86            .collect();
87        for key in user_keymap.keys() {
88            builtin_keymap.remove(key);
89        }
90
91        let mut panel_entries = build_entries(&user_keymap);
92        let panel_user_entry_count = panel_entries.len();
93        panel_entries.extend(build_entries(&builtin_keymap));
94
95        let mut keymap = builtin_keymap;
96        keymap.extend(user_keymap);
97        let mut app = Self {
98            tree,
99            state: TreeListViewState::with_capacity(0),
100            query: TreeQuery::new(),
101            mode: Mode::Normal,
102            keymap,
103            panel_entries,
104            panel_user_entry_count,
105            keybinding_panel: KeybindingPanelState::default(),
106            root_history: Vec::new(),
107            page_height: 20,
108            palette: None,
109        };
110        match expand {
111            None => {}
112            Some(spec) => {
113                // Explicit expansion wants the whole tree present up front.
114                app.tree.index_all();
115                let branches: Vec<_> = app.tree.branches().collect();
116                for (id, parent) in branches {
117                    let expand = match spec {
118                        ExpandSpec::All => true,
119                        ExpandSpec::Depth(n) => app.tree.depth(id) < n,
120                    };
121                    if expand {
122                        app.state.set_expanded(id, parent, true);
123                    }
124                }
125            }
126        }
127        // A single-rooted tree opens with its first level showing.
128        if app.tree.root_ids().len() == 1 {
129            let root = app.tree.root_ids()[0];
130            app.tree.ensure_children(root);
131            if !app.tree.is_leaf(root) {
132                app.state
133                    .set_expanded(root, app.tree.view_parent(root), true);
134            }
135        }
136        app.state.ensure_projection(&app.tree, &app.query);
137        app.state.select_first();
138        app
139    }
140
141    /// The default keybindings, before user config is merged.
142    pub fn default_keymap() -> HashMap<Key, Binding> {
143        let cmd = |action: AppCommand| Binding {
144            action: BindingAction::Cmd(action),
145            help: None,
146            exit: false,
147            bg: false,
148        };
149        let mut map = HashMap::new();
150        for (keys, action) in [
151            (&["j", "down"][..], AppCommand::Down),
152            (&["k", "up"], AppCommand::Up),
153            (&["l", "right"], AppCommand::Expand),
154            (&["h", "left"], AppCommand::Collapse),
155            (&["L", "shift+right"], AppCommand::ExpandRecursively),
156            (&["H", "shift+left"], AppCommand::CollapseRecursively),
157            (&["space"], AppCommand::Toggle),
158            (&["ctrl+space"], AppCommand::ToggleRecursively),
159            (&["enter"], AppCommand::Select),
160            (&["ctrl+enter"], AppCommand::Accept),
161            (&["alt+enter"], AppCommand::AcceptAlternate),
162            (&["tab"], AppCommand::Root),
163            (&["shift+tab"], AppCommand::PopRoot),
164            (&["J"], AppCommand::NextSibling),
165            (&["K"], AppCommand::PrevSibling),
166            (&["ctrl+f"], AppCommand::PageDown),
167            (&["ctrl+b"], AppCommand::PageUp),
168            (&["ctrl+d"], AppCommand::HalfPageDown),
169            (&["ctrl+u"], AppCommand::HalfPageUp),
170            (&["g"], AppCommand::First),
171            (&["G"], AppCommand::Last),
172            (&["/"], AppCommand::Jump),
173            (&["?"], AppCommand::ToggleKeybindingPanel),
174            (&["esc"], AppCommand::Back),
175            (&["q", "ctrl+c"], AppCommand::Quit),
176        ] {
177            for key in keys {
178                map.insert(Key::parse(key).expect("valid default key"), cmd(action));
179            }
180        }
181        map
182    }
183
184    pub fn focused_id(&mut self) -> Option<NodeId> {
185        self.state.ensure_projection(&self.tree, &self.query);
186        self.state.selected_id()
187    }
188
189    /// Names of currently visible rows, in on-screen order.
190    pub fn visible_names(&mut self) -> Vec<String> {
191        self.state.ensure_projection(&self.tree, &self.query);
192        self.state
193            .visible_ids()
194            .map(|id| self.tree.name(id))
195            .collect()
196    }
197
198    /// Handle a normalized key through the active mode and effective keymap.
199    pub fn handle_key(&mut self, key: Key) -> Effect {
200        let _span = crate::profile::span("app::handle_key");
201        // Only cancellation means anything while the index builds for `/`.
202        if let Mode::Indexing = self.mode {
203            if key == Key::parse("esc").unwrap() || key == Key::parse("ctrl+c").unwrap() {
204                self.mode = Mode::Normal;
205            }
206            return Effect::None;
207        }
208        // A modal picker takes over key handling until it closes. Accepting
209        // moves focus (expanding ancestors); cancelling leaves focus untouched,
210        // so the user returns to exactly where they opened it.
211        if let Mode::Jump(jump) = &mut self.mode {
212            return match jump.handle_key(key) {
213                JumpOutcome::Stay => Effect::None,
214                JumpOutcome::Cancel => {
215                    self.mode = Mode::Normal;
216                    Effect::None
217                }
218                JumpOutcome::Accept(id) => {
219                    self.mode = Mode::Normal;
220                    self.state.select_by_id(&self.tree, &self.query, id);
221                    Effect::None
222                }
223            };
224        }
225
226        if self.keybinding_panel.is_open() && key == CLOSE_KEY {
227            self.keybinding_panel.close();
228            return Effect::None;
229        }
230
231        match self.keymap.get(&key).cloned() {
232            None => Effect::None,
233            Some(binding) => match binding.action {
234                BindingAction::Cmd(cmd) => self.run_command(cmd),
235                BindingAction::Sh(cmd) => match self.focused_id() {
236                    None => Effect::None,
237                    Some(id) => Effect::RunShell {
238                        cmd,
239                        path: self.tree.path(id),
240                        relpath: self.tree.relpath(id),
241                        bg: binding.bg,
242                        exit: binding.exit,
243                    },
244                },
245            },
246        }
247    }
248
249    /// Execute an app command.
250    pub fn run_command(&mut self, cmd: AppCommand) -> Effect {
251        self.state.ensure_projection(&self.tree, &self.query);
252        match cmd {
253            AppCommand::Down => {
254                self.state.select_next();
255            }
256            AppCommand::Up => {
257                self.state.select_prev();
258            }
259            AppCommand::Expand => {
260                if let Some(id) = self.focused_id() {
261                    self.tree.ensure_children(id);
262                    let parent = self.tree.view_parent(id);
263                    if self.tree.is_leaf(id) {
264                        // Nothing to open: step along to the next sibling.
265                        self.move_sibling(1);
266                    } else if self.state.node_is_expanded(id, parent) {
267                        self.state.select_id(Some(self.tree.children_of(id)[0]));
268                    } else {
269                        self.state.set_expanded(id, parent, true);
270                    }
271                }
272            }
273            AppCommand::Collapse => {
274                if let Some(id) = self.focused_id() {
275                    let parent = self.tree.view_parent(id);
276                    if !self.tree.is_leaf(id) && self.state.node_is_expanded(id, parent) {
277                        self.state.set_expanded(id, parent, false);
278                    } else if let Some(parent) = parent {
279                        self.state.select_id(Some(parent));
280                    }
281                }
282            }
283            AppCommand::ExpandRecursively => {
284                if let Some(id) = self.focused_id() {
285                    self.tree.ensure_children(id);
286                    if self.tree.is_leaf(id) {
287                        // Nothing to open: step along to the next sibling.
288                        self.move_sibling(1);
289                    } else {
290                        self.set_expanded_recursively(id, true);
291                    }
292                }
293            }
294            AppCommand::CollapseRecursively => {
295                if let Some(id) = self.focused_id() {
296                    let parent = self.tree.view_parent(id);
297                    if !self.tree.is_leaf(id) && self.state.node_is_expanded(id, parent) {
298                        self.set_expanded_recursively(id, false);
299                    } else if let Some(parent) = parent {
300                        // Nothing to collapse here: fold up the enclosing
301                        // container and land on it.
302                        self.set_expanded_recursively(parent, false);
303                        self.state.ensure_projection(&self.tree, &self.query);
304                        self.state.select_id(Some(parent));
305                    }
306                }
307            }
308            AppCommand::Toggle => self.toggle_focused(false),
309            AppCommand::ToggleRecursively => self.toggle_focused(true),
310            AppCommand::Select => {
311                if let Some(id) = self.focused_id() {
312                    self.tree.ensure_children(id);
313                    if self.tree.is_leaf(id) {
314                        return Effect::PrintAndExit(self.tree.output(id));
315                    }
316                    self.state.set_expanded(id, self.tree.view_parent(id), true);
317                }
318            }
319            AppCommand::Accept => {
320                if let Some(id) = self.focused_id() {
321                    return Effect::PrintAndExit(self.tree.output(id));
322                }
323            }
324            AppCommand::AcceptAlternate => {
325                if let Some(id) = self.focused_id() {
326                    return Effect::PrintAndExit(self.tree.alternate_output(id));
327                }
328            }
329            AppCommand::Descend => {
330                if let Some(id) = self.focused_branch() {
331                    self.state.set_expanded(id, self.tree.view_parent(id), true);
332                    self.state.ensure_projection(&self.tree, &self.query);
333                    let first_child = self.tree.children_of(id)[0];
334                    self.state.select_id(Some(first_child));
335                }
336            }
337            AppCommand::Root => self.push_root(),
338            AppCommand::PopRoot => {
339                self.pop_root();
340            }
341            AppCommand::Back => {
342                if !self.pop_root() {
343                    return Effect::Quit;
344                }
345            }
346            AppCommand::NextSibling => self.move_sibling(1),
347            AppCommand::PrevSibling => self.move_sibling(-1),
348            AppCommand::PageDown => self.move_focus_by(self.page_height as isize),
349            AppCommand::PageUp => self.move_focus_by(-(self.page_height as isize)),
350            AppCommand::HalfPageDown => self.move_focus_by((self.page_height / 2) as isize),
351            AppCommand::HalfPageUp => self.move_focus_by(-((self.page_height / 2) as isize)),
352            AppCommand::First => {
353                self.state.select_first();
354            }
355            AppCommand::Last => {
356                self.state.select_last();
357            }
358            AppCommand::Jump => {
359                // The picker only ever sees a complete index; block on the
360                // progress screen until the sweep catches up.
361                if self.tree.fully_indexed() {
362                    self.mode = Mode::Jump(Jump::open(&self.tree));
363                } else {
364                    self.mode = Mode::Indexing;
365                }
366            }
367            AppCommand::ToggleKeybindingPanel => self.keybinding_panel.toggle(),
368            AppCommand::Quit => return Effect::Quit,
369        }
370        Effect::None
371    }
372
373    /// The focused node if it is expandable, its children materialized.
374    fn focused_branch(&mut self) -> Option<NodeId> {
375        let id = self.focused_id()?;
376        self.tree.ensure_children(id);
377        (!self.tree.is_leaf(id)).then_some(id)
378    }
379
380    /// One cooperative quantum of index building, run by the event loop
381    /// between input polls: load what is on screen first, then advance the
382    /// arena-order sweep. Returns true while more work remains. This is the
383    /// background indexer — cooperative rather than threaded, so the tree
384    /// stays single-threaded and lock-free; a worker thread could later slot
385    /// in behind this same seam.
386    pub fn do_work(&mut self) -> bool {
387        let _span = crate::profile::span("app::do_work");
388        if !self.tree.fully_indexed() {
389            self.state.ensure_projection(&self.tree, &self.query);
390            let visible: Vec<NodeId> = self.state.visible_ids().collect();
391            let mut budget = WORK_QUANTUM;
392            for id in visible {
393                if budget == 0 {
394                    break;
395                }
396                if self.tree.ensure_children(id) {
397                    budget -= 1;
398                }
399            }
400            self.tree.index_some(budget);
401        }
402        if matches!(self.mode, Mode::Indexing) && self.tree.fully_indexed() {
403            self.mode = Mode::Jump(Jump::open(&self.tree));
404        }
405        !self.tree.fully_indexed()
406    }
407
408    /// Whether the event loop should keep scheduling work quanta.
409    pub fn has_work(&self) -> bool {
410        !self.tree.fully_indexed()
411    }
412
413    /// Flip the focused container's expansion, leaving focus on it. A leaf has
414    /// nothing to toggle. The direction comes from the focused node's own
415    /// state, so a recursive toggle on an open container shuts it rather than
416    /// opening what is still shut underneath.
417    fn toggle_focused(&mut self, recursive: bool) {
418        let Some(id) = self.focused_id() else { return };
419        self.tree.ensure_children(id);
420        if self.tree.is_leaf(id) {
421            return;
422        }
423        let parent = self.tree.view_parent(id);
424        let expand = !self.state.node_is_expanded(id, parent);
425        if recursive {
426            self.set_expanded_recursively(id, expand);
427        } else {
428            self.state.set_expanded(id, parent, expand);
429        }
430    }
431
432    fn set_expanded_recursively(&mut self, root: NodeId, expanded: bool) {
433        let mut stack = vec![root];
434        while let Some(id) = stack.pop() {
435            if expanded {
436                self.tree.ensure_children(id);
437            }
438            if !self.tree.is_leaf(id) {
439                self.state
440                    .set_expanded(id, self.tree.view_parent(id), expanded);
441                stack.extend_from_slice(self.tree.children_of(id));
442            }
443        }
444    }
445
446    fn push_root(&mut self) {
447        let Some(id) = self.focused_id() else {
448            return;
449        };
450        if self.tree.view_root() == Some(id) {
451            return;
452        }
453        self.tree.ensure_children(id);
454
455        self.root_history.push(self.tree.view_root());
456        self.tree.set_view_root(Some(id));
457        if !self.tree.is_leaf(id) {
458            self.state.set_expanded(id, None, true);
459        }
460        self.state.ensure_projection(&self.tree, &self.query);
461        self.state.select_id(Some(id));
462    }
463
464    fn pop_root(&mut self) -> bool {
465        let Some(previous_root) = self.root_history.pop() else {
466            return false;
467        };
468        let selected = self.state.selected_id();
469
470        if let Some(current_root) = self.tree.view_root() {
471            let expanded = self.state.node_is_expanded(current_root, None);
472            self.state
473                .set_expanded(current_root, self.tree.parent(current_root), expanded);
474        }
475
476        self.tree.set_view_root(previous_root);
477        self.state.ensure_projection(&self.tree, &self.query);
478        if !selected.is_some_and(|id| self.state.select_by_id(&self.tree, &self.query, id)) {
479            self.state.select_first();
480        }
481        true
482    }
483
484    fn move_sibling(&mut self, delta: isize) {
485        let Some(id) = self.focused_id() else { return };
486        if self.tree.view_root() == Some(id) {
487            return;
488        }
489        let siblings = match self.tree.parent(id) {
490            Some(parent) => self.tree.children_of(parent),
491            None => self.tree.root_ids(),
492        };
493        let pos = siblings.iter().position(|&s| s == id).unwrap_or(0) as isize;
494        let target = pos + delta;
495        if (0..siblings.len() as isize).contains(&target) {
496            let target = siblings[target as usize];
497            self.state.select_id(Some(target));
498        }
499    }
500
501    fn move_focus_by(&mut self, delta: isize) {
502        let len = self.state.visible_len();
503        if len == 0 {
504            return;
505        }
506        let current = self.state.selected_index().unwrap_or(0) as isize;
507        let target = (current + delta).clamp(0, len as isize - 1);
508        self.state.select_index(Some(target as usize));
509    }
510}
511
512#[cfg(test)]
513mod tests {
514    use super::*;
515    use crate::fstree;
516
517    /// Builds:
518    ///   root/
519    ///     a/
520    ///       aa/
521    ///         aaa.txt
522    ///       ab.txt
523    ///     b/
524    ///       ba.txt
525    ///     c.txt
526    fn fixture() -> (tempfile::TempDir, Tree) {
527        let dir = tempfile::tempdir().unwrap();
528        let p = dir.path();
529        std::fs::create_dir_all(p.join("a/aa")).unwrap();
530        std::fs::write(p.join("a/aa/aaa.txt"), "").unwrap();
531        std::fs::write(p.join("a/ab.txt"), "").unwrap();
532        std::fs::create_dir(p.join("b")).unwrap();
533        std::fs::write(p.join("b/ba.txt"), "").unwrap();
534        std::fs::write(p.join("c.txt"), "").unwrap();
535        let tree = fstree::scan(p, false).unwrap();
536        (dir, tree)
537    }
538
539    fn app() -> (tempfile::TempDir, App) {
540        let (dir, tree) = fixture();
541        (dir, App::new(tree, &Config::default(), None))
542    }
543
544    /// The main fixture has no leaf with a following sibling. Builds:
545    ///   root/
546    ///     d/
547    ///       d1.txt
548    ///       d2.txt
549    fn app_with_leaf_siblings() -> (tempfile::TempDir, App) {
550        let dir = tempfile::tempdir().unwrap();
551        let p = dir.path();
552        std::fs::create_dir(p.join("d")).unwrap();
553        std::fs::write(p.join("d/d1.txt"), "").unwrap();
554        std::fs::write(p.join("d/d2.txt"), "").unwrap();
555        let tree = fstree::scan(p, false).unwrap();
556        (dir, App::new(tree, &Config::default(), None))
557    }
558
559    fn focused_name(app: &mut App) -> String {
560        let id = app.focused_id().expect("something focused");
561        app.tree.name(id)
562    }
563
564    #[test]
565    fn starts_focused_on_first_row_all_collapsed() {
566        let (_d, mut app) = app();
567        assert_eq!(app.visible_names(), ["a", "b", "c.txt"]);
568        assert_eq!(focused_name(&mut app), "a");
569    }
570
571    #[test]
572    fn down_and_up_move_focus_clamped() {
573        let (_d, mut app) = app();
574        app.run_command(AppCommand::Down);
575        assert_eq!(focused_name(&mut app), "b");
576        app.run_command(AppCommand::Down);
577        assert_eq!(focused_name(&mut app), "c.txt");
578        app.run_command(AppCommand::Down);
579        assert_eq!(focused_name(&mut app), "c.txt");
580        app.run_command(AppCommand::Up);
581        assert_eq!(focused_name(&mut app), "b");
582    }
583
584    #[test]
585    fn expand_reveals_children_and_down_enters_them() {
586        let (_d, mut app) = app();
587        app.run_command(AppCommand::Expand);
588        assert_eq!(app.visible_names(), ["a", "aa", "ab.txt", "b", "c.txt"]);
589        app.run_command(AppCommand::Down);
590        assert_eq!(focused_name(&mut app), "aa");
591    }
592
593    #[test]
594    fn l_on_expanded_branch_descends_to_first_child() {
595        let (_d, mut app) = app();
596        app.handle_key(Key::parse("l").unwrap());
597
598        app.handle_key(Key::parse("l").unwrap());
599
600        assert_eq!(focused_name(&mut app), "aa");
601    }
602
603    #[test]
604    fn l_on_leaf_focuses_next_sibling() {
605        let (_d, mut app) = app_with_leaf_siblings();
606        app.run_command(AppCommand::Expand); // expand "d"
607        app.run_command(AppCommand::Down); // focus "d1.txt"
608
609        app.handle_key(Key::parse("l").unwrap());
610
611        assert_eq!(focused_name(&mut app), "d2.txt");
612        assert_eq!(app.visible_names(), ["d", "d1.txt", "d2.txt"]);
613    }
614
615    #[test]
616    fn l_on_last_leaf_of_a_container_stays_inside_it() {
617        let (_d, mut app) = app();
618        app.run_command(AppCommand::Expand); // expand "a"
619        app.run_command(AppCommand::Down); // focus "aa"
620        app.run_command(AppCommand::Down); // focus "ab.txt", last child of "a"
621
622        app.handle_key(Key::parse("l").unwrap());
623
624        // Does not spill over to "b": siblings, not the next visible row.
625        assert_eq!(focused_name(&mut app), "ab.txt");
626        assert_eq!(app.visible_names(), ["a", "aa", "ab.txt", "b", "c.txt"]);
627    }
628
629    #[test]
630    fn expand_on_the_last_top_level_leaf_is_a_noop() {
631        let (_d, mut app) = app();
632        app.run_command(AppCommand::Last);
633        assert_eq!(focused_name(&mut app), "c.txt");
634        assert_eq!(app.run_command(AppCommand::Expand), Effect::None);
635        assert_eq!(focused_name(&mut app), "c.txt");
636        assert_eq!(app.visible_names(), ["a", "b", "c.txt"]);
637    }
638
639    #[test]
640    fn collapse_hides_children() {
641        let (_d, mut app) = app();
642        app.run_command(AppCommand::Expand);
643        app.run_command(AppCommand::Collapse);
644        assert_eq!(app.visible_names(), ["a", "b", "c.txt"]);
645    }
646
647    #[test]
648    fn h_on_leaf_focuses_parent_without_collapsing_it() {
649        let (_d, mut app) = app();
650        app.run_command(AppCommand::Expand);
651        app.run_command(AppCommand::Down); // focus collapsed "aa"
652        app.run_command(AppCommand::Expand);
653        app.run_command(AppCommand::Down); // focus "aaa.txt"
654
655        app.handle_key(Key::parse("h").unwrap());
656
657        assert_eq!(focused_name(&mut app), "aa");
658        assert_eq!(
659            app.visible_names(),
660            ["a", "aa", "aaa.txt", "ab.txt", "b", "c.txt"]
661        );
662    }
663
664    #[test]
665    fn h_on_collapsed_branch_focuses_parent_without_collapsing_it() {
666        let (_d, mut app) = app();
667        app.run_command(AppCommand::Expand);
668        app.run_command(AppCommand::Down); // focus collapsed "aa"
669
670        app.handle_key(Key::parse("h").unwrap());
671
672        assert_eq!(focused_name(&mut app), "a");
673        assert_eq!(app.visible_names(), ["a", "aa", "ab.txt", "b", "c.txt"]);
674    }
675
676    #[test]
677    fn expand_recursively_expands_whole_subtree() {
678        let (_d, mut app) = app();
679        app.run_command(AppCommand::ExpandRecursively);
680        assert_eq!(
681            app.visible_names(),
682            ["a", "aa", "aaa.txt", "ab.txt", "b", "c.txt"]
683        );
684    }
685
686    #[test]
687    fn space_toggles_a_container_open_and_shut() {
688        let (_d, mut app) = app();
689
690        app.handle_key(Key::parse("space").unwrap());
691        assert_eq!(app.visible_names(), ["a", "aa", "ab.txt", "b", "c.txt"]);
692        assert_eq!(focused_name(&mut app), "a");
693
694        app.handle_key(Key::parse("space").unwrap());
695        assert_eq!(app.visible_names(), ["a", "b", "c.txt"]);
696        assert_eq!(focused_name(&mut app), "a");
697    }
698
699    #[test]
700    fn space_on_a_leaf_does_nothing() {
701        let (_d, mut app) = app();
702        app.run_command(AppCommand::Last); // focus "c.txt"
703
704        app.handle_key(Key::parse("space").unwrap());
705
706        assert_eq!(focused_name(&mut app), "c.txt");
707        assert_eq!(app.visible_names(), ["a", "b", "c.txt"]);
708    }
709
710    #[test]
711    fn ctrl_space_toggles_a_container_recursively() {
712        let (_d, mut app) = app();
713
714        app.handle_key(Key::parse("ctrl+space").unwrap());
715        assert_eq!(
716            app.visible_names(),
717            ["a", "aa", "aaa.txt", "ab.txt", "b", "c.txt"]
718        );
719        assert_eq!(focused_name(&mut app), "a");
720
721        app.handle_key(Key::parse("ctrl+space").unwrap());
722        assert_eq!(app.visible_names(), ["a", "b", "c.txt"]);
723        // Descendant expansion was cleared, not just hidden.
724        app.run_command(AppCommand::Expand);
725        assert_eq!(app.visible_names(), ["a", "aa", "ab.txt", "b", "c.txt"]);
726    }
727
728    #[test]
729    fn ctrl_space_direction_follows_the_focused_container() {
730        let (_d, mut app) = app();
731        app.run_command(AppCommand::Expand); // "a" expanded, "aa" still shut
732
733        // "a" is open, so the recursive toggle shuts it rather than reopening.
734        app.handle_key(Key::parse("ctrl+space").unwrap());
735
736        assert_eq!(app.visible_names(), ["a", "b", "c.txt"]);
737    }
738
739    #[test]
740    fn shift_l_on_a_container_leaves_focus_on_it() {
741        let (_d, mut app) = app();
742
743        app.handle_key(Key::parse("L").unwrap());
744
745        assert_eq!(focused_name(&mut app), "a");
746    }
747
748    #[test]
749    fn shift_l_on_leaf_focuses_next_sibling() {
750        let (_d, mut app) = app_with_leaf_siblings();
751        app.run_command(AppCommand::Expand); // expand "d"
752        app.run_command(AppCommand::Down); // focus "d1.txt"
753
754        app.handle_key(Key::parse("L").unwrap());
755
756        assert_eq!(focused_name(&mut app), "d2.txt");
757        assert_eq!(app.visible_names(), ["d", "d1.txt", "d2.txt"]);
758    }
759
760    #[test]
761    fn shift_l_on_the_last_top_level_leaf_is_a_noop() {
762        let (_d, mut app) = app();
763        app.run_command(AppCommand::Last); // focus "c.txt"
764
765        app.handle_key(Key::parse("L").unwrap());
766
767        assert_eq!(focused_name(&mut app), "c.txt");
768        assert_eq!(app.visible_names(), ["a", "b", "c.txt"]);
769    }
770
771    #[test]
772    fn collapse_recursively_collapses_whole_subtree() {
773        let (_d, mut app) = app();
774        app.run_command(AppCommand::ExpandRecursively);
775        app.run_command(AppCommand::CollapseRecursively);
776        assert_eq!(app.visible_names(), ["a", "b", "c.txt"]);
777        // Descendant expansion was cleared, not just hidden.
778        app.run_command(AppCommand::Expand);
779        assert_eq!(app.visible_names(), ["a", "aa", "ab.txt", "b", "c.txt"]);
780    }
781
782    #[test]
783    fn shift_h_on_leaf_collapses_parent_and_focuses_it() {
784        let (_d, mut app) = app();
785        app.run_command(AppCommand::ExpandRecursively);
786        app.run_command(AppCommand::Down); // focus expanded "aa"
787        app.run_command(AppCommand::Down); // focus "aaa.txt"
788
789        app.handle_key(Key::parse("H").unwrap());
790
791        assert_eq!(focused_name(&mut app), "aa");
792        assert_eq!(app.visible_names(), ["a", "aa", "ab.txt", "b", "c.txt"]);
793    }
794
795    #[test]
796    fn shift_h_on_collapsed_branch_collapses_parent_and_focuses_it() {
797        let (_d, mut app) = app();
798        app.run_command(AppCommand::Expand);
799        app.run_command(AppCommand::Down); // focus collapsed "aa"
800
801        app.handle_key(Key::parse("H").unwrap());
802
803        assert_eq!(focused_name(&mut app), "a");
804        assert_eq!(app.visible_names(), ["a", "b", "c.txt"]);
805    }
806
807    #[test]
808    fn shift_h_collapses_the_parent_recursively() {
809        let (_d, mut app) = app();
810        app.run_command(AppCommand::ExpandRecursively);
811        app.run_command(AppCommand::Down); // focus expanded "aa"
812        app.run_command(AppCommand::Down); // focus "aaa.txt"
813        app.run_command(AppCommand::Down); // focus "ab.txt"
814
815        app.handle_key(Key::parse("H").unwrap());
816
817        assert_eq!(focused_name(&mut app), "a");
818        assert_eq!(app.visible_names(), ["a", "b", "c.txt"]);
819        // "aa" was collapsed along with "a", not just hidden by it.
820        app.run_command(AppCommand::Expand);
821        assert_eq!(app.visible_names(), ["a", "aa", "ab.txt", "b", "c.txt"]);
822    }
823
824    #[test]
825    fn shift_h_on_a_top_level_leaf_leaves_focus_alone() {
826        let (_d, mut app) = app();
827        app.run_command(AppCommand::Last); // focus top-level leaf "c.txt"
828
829        app.handle_key(Key::parse("H").unwrap());
830
831        assert_eq!(focused_name(&mut app), "c.txt");
832        assert_eq!(app.visible_names(), ["a", "b", "c.txt"]);
833    }
834
835    #[test]
836    fn select_expands_collapsed_dir_and_prints_leaf() {
837        let (_d, mut app) = app();
838        assert_eq!(app.run_command(AppCommand::Select), Effect::None);
839        assert_eq!(app.visible_names(), ["a", "aa", "ab.txt", "b", "c.txt"]);
840        app.run_command(AppCommand::Last);
841        let effect = app.run_command(AppCommand::Select);
842        let Effect::PrintAndExit(path) = effect else {
843            panic!("expected PrintAndExit, got {effect:?}");
844        };
845        assert!(std::path::Path::new(&path).is_absolute());
846        assert!(std::path::Path::new(&path).ends_with("c.txt"));
847    }
848
849    #[test]
850    fn select_does_not_descend_into_an_expanded_branch() {
851        let (_d, mut app) = app();
852        app.run_command(AppCommand::Select);
853
854        app.run_command(AppCommand::Select);
855
856        assert_eq!(focused_name(&mut app), "a");
857    }
858
859    #[test]
860    fn accept_prints_even_on_dir() {
861        let (_d, mut app) = app();
862        let effect = app.run_command(AppCommand::Accept);
863        let Effect::PrintAndExit(path) = effect else {
864            panic!("expected PrintAndExit, got {effect:?}");
865        };
866        assert!(std::path::Path::new(&path).ends_with("a"));
867    }
868
869    #[test]
870    fn alt_enter_prints_the_filesystem_basename() {
871        let (_d, mut app) = app();
872
873        assert_eq!(
874            app.handle_key(Key::parse("alt+enter").unwrap()),
875            Effect::PrintAndExit(OsString::from("a"))
876        );
877    }
878
879    #[test]
880    fn descend_expands_and_focuses_first_child() {
881        let (_d, mut app) = app();
882        app.run_command(AppCommand::Descend);
883        assert_eq!(focused_name(&mut app), "aa");
884    }
885
886    #[test]
887    fn tab_pushes_focused_nodes_as_view_roots_and_shift_tab_pops_them() {
888        let (_d, mut app) = app();
889
890        app.handle_key(Key::parse("tab").unwrap());
891        assert_eq!(focused_name(&mut app), "a");
892        assert_eq!(app.visible_names(), ["a", "aa", "ab.txt"]);
893
894        app.handle_key(Key::parse("l").unwrap()); // focus "aa"
895        app.handle_key(Key::parse("tab").unwrap());
896        assert_eq!(focused_name(&mut app), "aa");
897        assert_eq!(app.visible_names(), ["aa", "aaa.txt"]);
898
899        // The temporary root is a navigation boundary.
900        app.handle_key(Key::parse("h").unwrap()); // collapse root "aa"
901        assert_eq!(app.visible_names(), ["aa"]);
902        app.handle_key(Key::parse("h").unwrap()); // cannot focus parent "a"
903        assert_eq!(focused_name(&mut app), "aa");
904
905        app.handle_key(Key::parse("l").unwrap()); // expand root "aa"
906        assert_eq!(app.visible_names(), ["aa", "aaa.txt"]);
907
908        app.handle_key(Key::parse("shift+tab").unwrap());
909        assert_eq!(focused_name(&mut app), "aa");
910        assert_eq!(app.visible_names(), ["a", "aa", "aaa.txt", "ab.txt"]);
911
912        app.handle_key(Key::parse("shift+tab").unwrap());
913        assert_eq!(focused_name(&mut app), "aa");
914        assert_eq!(
915            app.visible_names(),
916            ["a", "aa", "aaa.txt", "ab.txt", "b", "c.txt"]
917        );
918
919        // There is no root before the original forest.
920        app.handle_key(Key::parse("shift+tab").unwrap());
921        assert_eq!(focused_name(&mut app), "aa");
922        assert_eq!(
923            app.visible_names(),
924            ["a", "aa", "aaa.txt", "ab.txt", "b", "c.txt"]
925        );
926    }
927
928    #[test]
929    fn tab_can_make_a_leaf_the_view_root() {
930        let (_d, mut app) = app();
931        app.run_command(AppCommand::Last);
932
933        app.handle_key(Key::parse("tab").unwrap());
934
935        assert_eq!(focused_name(&mut app), "c.txt");
936        assert_eq!(app.visible_names(), ["c.txt"]);
937    }
938
939    #[test]
940    fn escape_pops_one_root_at_a_time_then_quits() {
941        let (_d, mut app) = app();
942        app.handle_key(Key::parse("tab").unwrap()); // root "a"
943        app.handle_key(Key::parse("l").unwrap()); // focus "aa"
944        app.handle_key(Key::parse("tab").unwrap()); // root "aa"
945
946        assert_eq!(app.handle_key(Key::parse("esc").unwrap()), Effect::None);
947        assert_eq!(focused_name(&mut app), "aa");
948        assert_eq!(app.visible_names(), ["a", "aa", "aaa.txt", "ab.txt"]);
949
950        assert_eq!(app.handle_key(Key::parse("esc").unwrap()), Effect::None);
951        assert_eq!(focused_name(&mut app), "aa");
952        assert_eq!(
953            app.visible_names(),
954            ["a", "aa", "aaa.txt", "ab.txt", "b", "c.txt"]
955        );
956
957        assert_eq!(app.handle_key(Key::parse("esc").unwrap()), Effect::Quit);
958    }
959
960    #[test]
961    fn sibling_navigation_skips_expanded_children() {
962        let (_d, mut app) = app();
963        app.run_command(AppCommand::Expand); // "a" expanded, children visible
964        app.run_command(AppCommand::NextSibling);
965        assert_eq!(focused_name(&mut app), "b");
966        app.run_command(AppCommand::PrevSibling);
967        assert_eq!(focused_name(&mut app), "a");
968        // No previous sibling: no-op.
969        app.run_command(AppCommand::PrevSibling);
970        assert_eq!(focused_name(&mut app), "a");
971    }
972
973    #[test]
974    fn first_and_last() {
975        let (_d, mut app) = app();
976        app.run_command(AppCommand::Last);
977        assert_eq!(focused_name(&mut app), "c.txt");
978        app.run_command(AppCommand::First);
979        assert_eq!(focused_name(&mut app), "a");
980    }
981
982    #[test]
983    fn paging_moves_focus_by_page_amounts() {
984        let (_d, mut app) = app();
985        app.run_command(AppCommand::ExpandRecursively); // 6 visible rows
986        app.page_height = 4;
987        app.run_command(AppCommand::HalfPageDown);
988        assert_eq!(focused_name(&mut app), "aaa.txt"); // moved 2
989        app.run_command(AppCommand::PageDown);
990        assert_eq!(focused_name(&mut app), "c.txt"); // clamped at end
991        app.run_command(AppCommand::HalfPageUp);
992        assert_eq!(focused_name(&mut app), "ab.txt");
993        app.run_command(AppCommand::PageUp);
994        assert_eq!(focused_name(&mut app), "a");
995    }
996
997    #[test]
998    fn default_keys_drive_commands() {
999        let (_d, mut app) = app();
1000        app.handle_key(Key::parse("j").unwrap());
1001        assert_eq!(focused_name(&mut app), "b");
1002        app.handle_key(Key::parse("k").unwrap());
1003        assert_eq!(focused_name(&mut app), "a");
1004        app.handle_key(Key::parse("l").unwrap());
1005        assert_eq!(app.visible_names().len(), 5);
1006        app.handle_key(Key::parse("h").unwrap());
1007        assert_eq!(app.visible_names().len(), 3);
1008        assert_eq!(app.handle_key(Key::parse("q").unwrap()), Effect::Quit);
1009        assert_eq!(app.handle_key(Key::parse("esc").unwrap()), Effect::Quit);
1010        assert_eq!(app.handle_key(Key::parse("ctrl+c").unwrap()), Effect::Quit);
1011    }
1012
1013    #[test]
1014    fn g_goes_to_first_line_without_a_chord() {
1015        let (_d, mut app) = app();
1016        app.run_command(AppCommand::Last);
1017        assert_eq!(app.handle_key(Key::parse("g").unwrap()), Effect::None);
1018        assert_eq!(focused_name(&mut app), "a");
1019    }
1020
1021    #[test]
1022    fn shift_g_goes_to_last_visible_line() {
1023        let (_d, mut app) = app();
1024        app.handle_key(Key::parse("G").unwrap());
1025        assert_eq!(focused_name(&mut app), "c.txt");
1026    }
1027
1028    #[test]
1029    fn user_binding_produces_shell_effect_with_paths() {
1030        let (_d, tree) = fixture();
1031        let config = Config::parse("[ctrl+e]\nsh = \"vim $path\"\nexit = true\n").unwrap();
1032        let mut app = App::new(tree, &config, None);
1033        app.run_command(AppCommand::Down); // focus "b"
1034        let effect = app.handle_key(Key::parse("ctrl+e").unwrap());
1035        let Effect::RunShell {
1036            cmd,
1037            path,
1038            relpath,
1039            bg,
1040            exit,
1041        } = effect
1042        else {
1043            panic!("expected RunShell, got {effect:?}");
1044        };
1045        assert_eq!(cmd, "vim $path");
1046        assert!(std::path::Path::new(&path).is_absolute());
1047        assert!(std::path::Path::new(&path).ends_with("b"));
1048        assert_eq!(relpath, OsString::from("b"));
1049        assert!(!bg);
1050        assert!(exit);
1051    }
1052
1053    #[test]
1054    fn user_binding_overrides_default() {
1055        let (_d, tree) = fixture();
1056        let config = Config::parse("[j]\ncmd = \"quit\"\n").unwrap();
1057        let mut app = App::new(tree, &config, None);
1058        assert_eq!(app.handle_key(Key::parse("j").unwrap()), Effect::Quit);
1059    }
1060
1061    #[test]
1062    fn user_can_override_the_new_g_binding() {
1063        let (_d, tree) = fixture();
1064        let config = Config::parse("[g]\ncmd = \"quit\"\n").unwrap();
1065        let mut app = App::new(tree, &config, None);
1066        assert_eq!(app.handle_key(Key::parse("g").unwrap()), Effect::Quit);
1067    }
1068
1069    #[test]
1070    fn question_mark_is_reserved_and_toggles_the_panel() {
1071        let (_d, tree) = fixture();
1072        let config = Config::parse("[?]\ncmd = \"quit\"\nhelp = \"Wrong\"\n").unwrap();
1073        let mut app = App::new(tree, &config, None);
1074
1075        // The `[?]` table was dropped: the panel lists the reserved binding,
1076        // not the configured one.
1077        let question: Vec<_> = app
1078            .panel_entries
1079            .iter()
1080            .filter(|entry| entry.key == Key::parse("?").unwrap())
1081            .collect();
1082        assert_eq!(question.len(), 1);
1083        assert_eq!(question[0].description, "Shortcuts");
1084
1085        assert!(!app.keybinding_panel.is_open());
1086        assert_eq!(app.handle_key(Key::parse("?").unwrap()), Effect::None);
1087        assert!(app.keybinding_panel.is_open());
1088        assert_eq!(app.handle_key(Key::parse("?").unwrap()), Effect::None);
1089        assert!(!app.keybinding_panel.is_open());
1090    }
1091
1092    #[test]
1093    fn open_panel_stays_open_while_bindings_run_and_escape_only_closes_it() {
1094        let (_d, mut app) = app();
1095        app.handle_key(Key::parse("?").unwrap());
1096
1097        app.handle_key(Key::parse("j").unwrap());
1098        assert_eq!(focused_name(&mut app), "b");
1099        assert!(app.keybinding_panel.is_open());
1100
1101        assert_eq!(app.handle_key(Key::parse("esc").unwrap()), Effect::None);
1102        assert!(!app.keybinding_panel.is_open());
1103        assert_eq!(focused_name(&mut app), "b");
1104    }
1105
1106    #[test]
1107    fn jump_owns_question_mark_and_escape_without_dismissing_the_panel() {
1108        let (_d, mut app) = app();
1109        while app.do_work() {}
1110        app.handle_key(Key::parse("?").unwrap());
1111        app.handle_key(Key::parse("/").unwrap());
1112        app.handle_key(Key::parse("?").unwrap());
1113
1114        let Mode::Jump(jump) = &app.mode else {
1115            panic!("expected jump mode");
1116        };
1117        assert_eq!(jump.query(), "?");
1118        assert!(app.keybinding_panel.is_open());
1119
1120        app.handle_key(Key::parse("esc").unwrap());
1121        assert!(matches!(app.mode, Mode::Normal));
1122        assert!(app.keybinding_panel.is_open());
1123    }
1124
1125    #[test]
1126    fn unbound_key_is_noop() {
1127        let (_d, mut app) = app();
1128        assert_eq!(app.handle_key(Key::parse("x").unwrap()), Effect::None);
1129    }
1130
1131    #[test]
1132    fn initial_expand_depth_one_expands_top_level_only() {
1133        let (_d, tree) = fixture();
1134        let mut app = App::new(tree, &Config::default(), Some(ExpandSpec::Depth(1)));
1135        assert_eq!(
1136            app.visible_names(),
1137            ["a", "aa", "ab.txt", "b", "ba.txt", "c.txt"]
1138        );
1139    }
1140
1141    #[test]
1142    fn initial_expand_all_expands_everything() {
1143        let (_d, tree) = fixture();
1144        let mut app = App::new(tree, &Config::default(), Some(ExpandSpec::All));
1145        assert_eq!(
1146            app.visible_names(),
1147            ["a", "aa", "aaa.txt", "ab.txt", "b", "ba.txt", "c.txt"]
1148        );
1149    }
1150
1151    fn in_jump(app: &App) -> bool {
1152        matches!(app.mode, Mode::Jump(_))
1153    }
1154
1155    #[test]
1156    fn slash_opens_the_jump_picker() {
1157        let (_d, mut app) = app();
1158        assert!(!in_jump(&app));
1159        while app.do_work() {} // the picker waits for a complete index
1160        app.handle_key(Key::parse("/").unwrap());
1161        assert!(in_jump(&app));
1162    }
1163
1164    #[test]
1165    fn cancelling_the_picker_leaves_focus_untouched() {
1166        let (_d, mut app) = app();
1167        app.run_command(AppCommand::Down); // focus "b"
1168        assert_eq!(focused_name(&mut app), "b");
1169        app.handle_key(Key::parse("/").unwrap());
1170        app.handle_key(Key::parse("a").unwrap()); // type into the query
1171        app.handle_key(Key::parse("esc").unwrap());
1172        assert!(!in_jump(&app));
1173        assert_eq!(focused_name(&mut app), "b");
1174    }
1175
1176    #[test]
1177    fn accepting_jumps_focus_and_expands_ancestors() {
1178        let (_d, mut app) = app();
1179        // Everything starts collapsed: only the top level is visible.
1180        assert_eq!(app.visible_names(), ["a", "b", "c.txt"]);
1181        while app.do_work() {} // the picker waits for a complete index
1182        app.handle_key(Key::parse("/").unwrap());
1183        for k in ["a", "a", "a"] {
1184            app.handle_key(Key::parse(k).unwrap()); // query "aaa" -> a/aa/aaa.txt
1185        }
1186        app.handle_key(Key::parse("enter").unwrap());
1187        assert!(!in_jump(&app));
1188        assert_eq!(focused_name(&mut app), "aaa.txt");
1189        // The path to it was expanded so the focused node is visible.
1190        assert!(app.visible_names().contains(&"aaa.txt".to_string()));
1191    }
1192
1193    #[test]
1194    fn a_user_can_rebind_jump_off_slash() {
1195        let (_d, tree) = fixture();
1196        let config = Config::parse("[ctrl+p]\ncmd = \"jump\"\n").unwrap();
1197        let mut app = App::new(tree, &config, None);
1198        while app.do_work() {} // the picker waits for a complete index
1199        app.handle_key(Key::parse("ctrl+p").unwrap());
1200        assert!(in_jump(&app));
1201    }
1202
1203    #[test]
1204    fn a_single_rooted_tree_opens_with_its_first_level_expanded() {
1205        use crate::tree::ActionValues;
1206        let mut tree = Tree::new();
1207        let root = tree.push(None, "root", true, ActionValues::new("", "", ""));
1208        tree.push(Some(root), "child", false, ActionValues::new("", "", ""));
1209        let mut app = App::new(tree, &Config::default(), None);
1210
1211        assert_eq!(app.visible_names(), ["root", "child"]);
1212        assert_eq!(focused_name(&mut app), "root");
1213    }
1214
1215    #[test]
1216    fn expanding_an_unindexed_container_materializes_its_children() {
1217        let tree =
1218            crate::json_tree::from_reader(r#"{"users": [1, 2], "n": 3}"#.as_bytes()).unwrap();
1219        let mut app = App::new(tree, &Config::default(), None);
1220        assert_eq!(app.visible_names(), ["users [2]", "n: 3"]);
1221
1222        app.handle_key(Key::parse("l").unwrap());
1223
1224        assert_eq!(
1225            app.visible_names(),
1226            ["users [2]", "[0]: 1", "[1]: 2", "n: 3"]
1227        );
1228    }
1229
1230    #[test]
1231    fn jump_blocks_on_an_incomplete_index_and_opens_when_done() {
1232        let tree =
1233            crate::json_tree::from_reader(r#"{"a": {"b": 1}, "c": {"d": 2}}"#.as_bytes()).unwrap();
1234        let mut app = App::new(tree, &Config::default(), None);
1235        assert!(app.has_work());
1236
1237        app.handle_key(Key::parse("/").unwrap());
1238        assert!(matches!(app.mode, Mode::Indexing));
1239        // Keys other than cancel are ignored while the index builds.
1240        assert_eq!(app.handle_key(Key::parse("j").unwrap()), Effect::None);
1241        assert!(matches!(app.mode, Mode::Indexing));
1242
1243        while app.do_work() {}
1244
1245        assert!(matches!(app.mode, Mode::Jump(_)));
1246        assert!(!app.has_work());
1247    }
1248
1249    #[test]
1250    fn escape_cancels_the_indexing_wait() {
1251        let tree =
1252            crate::json_tree::from_reader(r#"{"a": {"b": 1}, "c": {"d": 2}}"#.as_bytes()).unwrap();
1253        let mut app = App::new(tree, &Config::default(), None);
1254        app.handle_key(Key::parse("/").unwrap());
1255        assert!(matches!(app.mode, Mode::Indexing));
1256
1257        app.handle_key(Key::parse("esc").unwrap());
1258
1259        assert!(matches!(app.mode, Mode::Normal));
1260    }
1261}