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