Skip to main content

ite_cli/
app.rs

1//! Application state: focus/expansion driven by app commands and keybindings.
2
3use std::collections::HashMap;
4use std::ffi::OsString;
5
6use tui_treelistview::{TreeListViewState, TreeQuery};
7
8use crate::cli::ExpandSpec;
9use crate::config::{AppCommand, Binding, BindingAction, Config};
10use crate::jump::{Jump, JumpOutcome};
11use crate::keys::Key;
12use crate::tree::{NodeId, Tree};
13
14/// What the event loop must do after a key is handled.
15#[derive(Clone, PartialEq, Debug)]
16pub enum Effect {
17    None,
18    /// Exit without output.
19    Quit,
20    /// The default action: print the node's source-specific value and exit.
21    PrintAndExit(OsString),
22    /// Run a configured shell command on the focused node.
23    RunShell {
24        cmd: String,
25        path: OsString,
26        relpath: OsString,
27        bg: bool,
28        exit: bool,
29    },
30}
31
32/// The app's input mode. `Normal` drives the tree via the keymap; other modes
33/// take over key handling and rendering until they close. Adding a mode is a
34/// new variant plus one dispatch arm in `handle_key` and one in `ui::draw`.
35pub enum Mode {
36    Normal,
37    Jump(Jump),
38}
39
40pub struct App {
41    pub tree: Tree,
42    pub state: TreeListViewState<NodeId>,
43    pub query: TreeQuery,
44    /// The current input mode; see [`Mode`].
45    pub mode: Mode,
46    keymap: HashMap<Key, Binding>,
47    /// True after a bare `g`, waiting for the second `g` of the chord.
48    pending_g: bool,
49    /// Rows per screen; the UI updates this every frame.
50    pub page_height: usize,
51    /// Terminal default colors, when the terminal answered the startup query.
52    pub palette: Option<crate::ui::Palette>,
53}
54
55impl App {
56    pub fn new(tree: Tree, config: &Config, expand: Option<ExpandSpec>) -> Self {
57        let mut keymap = Self::default_keymap();
58        keymap.extend(config.bindings.clone());
59        let mut app = Self {
60            tree,
61            state: TreeListViewState::with_capacity(0),
62            query: TreeQuery::new(),
63            mode: Mode::Normal,
64            keymap,
65            pending_g: false,
66            page_height: 20,
67            palette: None,
68        };
69        match expand {
70            None => {}
71            Some(ExpandSpec::All) => {
72                let branches: Vec<_> = app.tree.branches().collect();
73                for (id, parent) in branches {
74                    app.state.set_expanded(id, parent, true);
75                }
76            }
77            Some(ExpandSpec::Depth(n)) => {
78                let branches: Vec<_> = app.tree.branches().collect();
79                for (id, parent) in branches {
80                    if app.tree.node(id).depth < n {
81                        app.state.set_expanded(id, parent, true);
82                    }
83                }
84            }
85        }
86        app.state.ensure_projection(&app.tree, &app.query);
87        app.state.select_first();
88        app
89    }
90
91    /// The default keybindings, before user config is merged.
92    pub fn default_keymap() -> HashMap<Key, Binding> {
93        let cmd = |action: AppCommand| Binding {
94            action: BindingAction::Cmd(action),
95            exit: false,
96            bg: false,
97        };
98        let mut map = HashMap::new();
99        for (keys, action) in [
100            (&["j", "down"][..], AppCommand::Down),
101            (&["k", "up"], AppCommand::Up),
102            (&["l", "right"], AppCommand::Expand),
103            (&["h", "left"], AppCommand::Collapse),
104            (&["L", "shift+right"], AppCommand::ExpandRecursively),
105            (&["H", "shift+left"], AppCommand::CollapseRecursively),
106            (&["enter"], AppCommand::Select),
107            (&["ctrl+enter"], AppCommand::Accept),
108            (&["alt+enter"], AppCommand::AcceptAlternate),
109            (&["tab"], AppCommand::Descend),
110            (&["J"], AppCommand::NextSibling),
111            (&["K"], AppCommand::PrevSibling),
112            (&["ctrl+f"], AppCommand::PageDown),
113            (&["ctrl+b"], AppCommand::PageUp),
114            (&["ctrl+d"], AppCommand::HalfPageDown),
115            (&["ctrl+u"], AppCommand::HalfPageUp),
116            (&["G"], AppCommand::Last),
117            (&["/"], AppCommand::Jump),
118            (&["q", "esc", "ctrl+c"], AppCommand::Quit),
119        ] {
120            for key in keys {
121                map.insert(Key::parse(key).expect("valid default key"), cmd(action));
122            }
123        }
124        map
125    }
126
127    pub fn focused_id(&mut self) -> Option<NodeId> {
128        self.state.ensure_projection(&self.tree, &self.query);
129        self.state.selected_id()
130    }
131
132    /// Names of currently visible rows, in on-screen order.
133    pub fn visible_names(&mut self) -> Vec<String> {
134        self.state.ensure_projection(&self.tree, &self.query);
135        self.state
136            .visible_ids()
137            .map(|id| self.tree.node(id).name.clone())
138            .collect()
139    }
140
141    /// Handle a normalized key, resolving chords and the keymap.
142    pub fn handle_key(&mut self, key: Key) -> Effect {
143        let _span = crate::profile::span("app::handle_key");
144        // A modal picker takes over key handling until it closes. Accepting
145        // moves focus (expanding ancestors); cancelling leaves focus untouched,
146        // so the user returns to exactly where they opened it.
147        if let Mode::Jump(jump) = &mut self.mode {
148            return match jump.handle_key(key) {
149                JumpOutcome::Stay => Effect::None,
150                JumpOutcome::Cancel => {
151                    self.mode = Mode::Normal;
152                    Effect::None
153                }
154                JumpOutcome::Accept(id) => {
155                    self.mode = Mode::Normal;
156                    self.state.select_by_id(&self.tree, &self.query, id);
157                    Effect::None
158                }
159            };
160        }
161        let g = Key::parse("g").unwrap();
162        if self.pending_g {
163            self.pending_g = false;
164            if key == g {
165                return self.run_command(AppCommand::First);
166            }
167            // fall through: the second key is handled normally
168        } else if key == g && !self.keymap.contains_key(&g) {
169            self.pending_g = true;
170            return Effect::None;
171        }
172        match self.keymap.get(&key).cloned() {
173            None => Effect::None,
174            Some(binding) => match binding.action {
175                BindingAction::Cmd(cmd) => self.run_command(cmd),
176                BindingAction::Sh(cmd) => match self.focused_id() {
177                    None => Effect::None,
178                    Some(id) => Effect::RunShell {
179                        cmd,
180                        path: self.tree.node(id).action.path.clone(),
181                        relpath: self.tree.node(id).action.relpath.clone(),
182                        bg: binding.bg,
183                        exit: binding.exit,
184                    },
185                },
186            },
187        }
188    }
189
190    /// Execute an app command.
191    pub fn run_command(&mut self, cmd: AppCommand) -> Effect {
192        self.state.ensure_projection(&self.tree, &self.query);
193        match cmd {
194            AppCommand::Down => {
195                self.state.select_next();
196            }
197            AppCommand::Up => {
198                self.state.select_prev();
199            }
200            AppCommand::Expand => {
201                if let Some(id) = self.focused_branch() {
202                    let parent = self.tree.node(id).parent;
203                    if self.state.node_is_expanded(id, parent) {
204                        self.state.select_id(Some(self.tree.node(id).children[0]));
205                    } else {
206                        self.state.set_expanded(id, parent, true);
207                    }
208                }
209            }
210            AppCommand::Collapse => {
211                if let Some(id) = self.focused_id() {
212                    let parent = self.tree.node(id).parent;
213                    if !self.tree.is_leaf(id) && self.state.node_is_expanded(id, parent) {
214                        self.state.set_expanded(id, parent, false);
215                    } else if let Some(parent) = parent {
216                        self.state.select_id(Some(parent));
217                    }
218                }
219            }
220            AppCommand::ExpandRecursively => self.set_expanded_recursively(true),
221            AppCommand::CollapseRecursively => self.set_expanded_recursively(false),
222            AppCommand::Select => {
223                if let Some(id) = self.focused_id() {
224                    if self.tree.is_leaf(id) {
225                        return Effect::PrintAndExit(self.tree.node(id).action.output.clone());
226                    }
227                    self.state.set_expanded(id, self.tree.node(id).parent, true);
228                }
229            }
230            AppCommand::Accept => {
231                if let Some(id) = self.focused_id() {
232                    return Effect::PrintAndExit(self.tree.node(id).action.output.clone());
233                }
234            }
235            AppCommand::AcceptAlternate => {
236                if let Some(id) = self.focused_id() {
237                    return Effect::PrintAndExit(
238                        self.tree.node(id).action.alternate_output.clone(),
239                    );
240                }
241            }
242            AppCommand::Descend => {
243                if let Some(id) = self.focused_branch() {
244                    self.state.set_expanded(id, self.tree.node(id).parent, true);
245                    self.state.ensure_projection(&self.tree, &self.query);
246                    let first_child = self.tree.node(id).children[0];
247                    self.state.select_id(Some(first_child));
248                }
249            }
250            AppCommand::NextSibling => self.move_sibling(1),
251            AppCommand::PrevSibling => self.move_sibling(-1),
252            AppCommand::PageDown => self.move_focus_by(self.page_height as isize),
253            AppCommand::PageUp => self.move_focus_by(-(self.page_height as isize)),
254            AppCommand::HalfPageDown => self.move_focus_by((self.page_height / 2) as isize),
255            AppCommand::HalfPageUp => self.move_focus_by(-((self.page_height / 2) as isize)),
256            AppCommand::First => {
257                self.state.select_first();
258            }
259            AppCommand::Last => {
260                self.state.select_last();
261            }
262            AppCommand::Jump => {
263                self.mode = Mode::Jump(Jump::open(&self.tree));
264            }
265            AppCommand::Quit => return Effect::Quit,
266        }
267        Effect::None
268    }
269
270    /// The focused node if it is expandable.
271    fn focused_branch(&mut self) -> Option<NodeId> {
272        self.focused_id().filter(|&id| !self.tree.is_leaf(id))
273    }
274
275    fn set_expanded_recursively(&mut self, expanded: bool) {
276        let Some(root) = self.focused_id() else {
277            return;
278        };
279        let mut stack = vec![root];
280        while let Some(id) = stack.pop() {
281            if !self.tree.is_leaf(id) {
282                self.state
283                    .set_expanded(id, self.tree.node(id).parent, expanded);
284                stack.extend_from_slice(&self.tree.node(id).children);
285            }
286        }
287    }
288
289    fn move_sibling(&mut self, delta: isize) {
290        let Some(id) = self.focused_id() else { return };
291        let siblings = match self.tree.node(id).parent {
292            Some(parent) => self.tree.node(parent).children.as_slice(),
293            None => self.tree.root_ids(),
294        };
295        let pos = siblings.iter().position(|&s| s == id).unwrap_or(0) as isize;
296        let target = pos + delta;
297        if (0..siblings.len() as isize).contains(&target) {
298            let target = siblings[target as usize];
299            self.state.select_id(Some(target));
300        }
301    }
302
303    fn move_focus_by(&mut self, delta: isize) {
304        let len = self.state.visible_len();
305        if len == 0 {
306            return;
307        }
308        let current = self.state.selected_index().unwrap_or(0) as isize;
309        let target = (current + delta).clamp(0, len as isize - 1);
310        self.state.select_index(Some(target as usize));
311    }
312}
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317    use crate::fstree;
318
319    /// Builds:
320    ///   root/
321    ///     a/
322    ///       aa/
323    ///         aaa.txt
324    ///       ab.txt
325    ///     b/
326    ///       ba.txt
327    ///     c.txt
328    fn fixture() -> (tempfile::TempDir, Tree) {
329        let dir = tempfile::tempdir().unwrap();
330        let p = dir.path();
331        std::fs::create_dir_all(p.join("a/aa")).unwrap();
332        std::fs::write(p.join("a/aa/aaa.txt"), "").unwrap();
333        std::fs::write(p.join("a/ab.txt"), "").unwrap();
334        std::fs::create_dir(p.join("b")).unwrap();
335        std::fs::write(p.join("b/ba.txt"), "").unwrap();
336        std::fs::write(p.join("c.txt"), "").unwrap();
337        let tree = fstree::scan(p, false).unwrap();
338        (dir, tree)
339    }
340
341    fn app() -> (tempfile::TempDir, App) {
342        let (dir, tree) = fixture();
343        (dir, App::new(tree, &Config::default(), None))
344    }
345
346    fn focused_name(app: &mut App) -> String {
347        let id = app.focused_id().expect("something focused");
348        app.tree.node(id).name.clone()
349    }
350
351    #[test]
352    fn starts_focused_on_first_row_all_collapsed() {
353        let (_d, mut app) = app();
354        assert_eq!(app.visible_names(), ["a", "b", "c.txt"]);
355        assert_eq!(focused_name(&mut app), "a");
356    }
357
358    #[test]
359    fn down_and_up_move_focus_clamped() {
360        let (_d, mut app) = app();
361        app.run_command(AppCommand::Down);
362        assert_eq!(focused_name(&mut app), "b");
363        app.run_command(AppCommand::Down);
364        assert_eq!(focused_name(&mut app), "c.txt");
365        app.run_command(AppCommand::Down);
366        assert_eq!(focused_name(&mut app), "c.txt");
367        app.run_command(AppCommand::Up);
368        assert_eq!(focused_name(&mut app), "b");
369    }
370
371    #[test]
372    fn expand_reveals_children_and_down_enters_them() {
373        let (_d, mut app) = app();
374        app.run_command(AppCommand::Expand);
375        assert_eq!(app.visible_names(), ["a", "aa", "ab.txt", "b", "c.txt"]);
376        app.run_command(AppCommand::Down);
377        assert_eq!(focused_name(&mut app), "aa");
378    }
379
380    #[test]
381    fn l_on_expanded_branch_descends_to_first_child() {
382        let (_d, mut app) = app();
383        app.handle_key(Key::parse("l").unwrap());
384
385        app.handle_key(Key::parse("l").unwrap());
386
387        assert_eq!(focused_name(&mut app), "aa");
388    }
389
390    #[test]
391    fn expand_is_noop_on_leaf() {
392        let (_d, mut app) = app();
393        app.run_command(AppCommand::Last);
394        assert_eq!(focused_name(&mut app), "c.txt");
395        assert_eq!(app.run_command(AppCommand::Expand), Effect::None);
396        assert_eq!(app.visible_names(), ["a", "b", "c.txt"]);
397    }
398
399    #[test]
400    fn collapse_hides_children() {
401        let (_d, mut app) = app();
402        app.run_command(AppCommand::Expand);
403        app.run_command(AppCommand::Collapse);
404        assert_eq!(app.visible_names(), ["a", "b", "c.txt"]);
405    }
406
407    #[test]
408    fn h_on_leaf_focuses_parent_without_collapsing_it() {
409        let (_d, mut app) = app();
410        app.run_command(AppCommand::Expand);
411        app.run_command(AppCommand::Down); // focus collapsed "aa"
412        app.run_command(AppCommand::Expand);
413        app.run_command(AppCommand::Down); // focus "aaa.txt"
414
415        app.handle_key(Key::parse("h").unwrap());
416
417        assert_eq!(focused_name(&mut app), "aa");
418        assert_eq!(
419            app.visible_names(),
420            ["a", "aa", "aaa.txt", "ab.txt", "b", "c.txt"]
421        );
422    }
423
424    #[test]
425    fn h_on_collapsed_branch_focuses_parent_without_collapsing_it() {
426        let (_d, mut app) = app();
427        app.run_command(AppCommand::Expand);
428        app.run_command(AppCommand::Down); // focus collapsed "aa"
429
430        app.handle_key(Key::parse("h").unwrap());
431
432        assert_eq!(focused_name(&mut app), "a");
433        assert_eq!(app.visible_names(), ["a", "aa", "ab.txt", "b", "c.txt"]);
434    }
435
436    #[test]
437    fn expand_recursively_expands_whole_subtree() {
438        let (_d, mut app) = app();
439        app.run_command(AppCommand::ExpandRecursively);
440        assert_eq!(
441            app.visible_names(),
442            ["a", "aa", "aaa.txt", "ab.txt", "b", "c.txt"]
443        );
444    }
445
446    #[test]
447    fn collapse_recursively_collapses_whole_subtree() {
448        let (_d, mut app) = app();
449        app.run_command(AppCommand::ExpandRecursively);
450        app.run_command(AppCommand::CollapseRecursively);
451        assert_eq!(app.visible_names(), ["a", "b", "c.txt"]);
452        // Descendant expansion was cleared, not just hidden.
453        app.run_command(AppCommand::Expand);
454        assert_eq!(app.visible_names(), ["a", "aa", "ab.txt", "b", "c.txt"]);
455    }
456
457    #[test]
458    fn select_expands_collapsed_dir_and_prints_leaf() {
459        let (_d, mut app) = app();
460        assert_eq!(app.run_command(AppCommand::Select), Effect::None);
461        assert_eq!(app.visible_names(), ["a", "aa", "ab.txt", "b", "c.txt"]);
462        app.run_command(AppCommand::Last);
463        let effect = app.run_command(AppCommand::Select);
464        let Effect::PrintAndExit(path) = effect else {
465            panic!("expected PrintAndExit, got {effect:?}");
466        };
467        assert!(std::path::Path::new(&path).is_absolute());
468        assert!(std::path::Path::new(&path).ends_with("c.txt"));
469    }
470
471    #[test]
472    fn select_does_not_descend_into_an_expanded_branch() {
473        let (_d, mut app) = app();
474        app.run_command(AppCommand::Select);
475
476        app.run_command(AppCommand::Select);
477
478        assert_eq!(focused_name(&mut app), "a");
479    }
480
481    #[test]
482    fn accept_prints_even_on_dir() {
483        let (_d, mut app) = app();
484        let effect = app.run_command(AppCommand::Accept);
485        let Effect::PrintAndExit(path) = effect else {
486            panic!("expected PrintAndExit, got {effect:?}");
487        };
488        assert!(std::path::Path::new(&path).ends_with("a"));
489    }
490
491    #[test]
492    fn alt_enter_prints_the_filesystem_basename() {
493        let (_d, mut app) = app();
494
495        assert_eq!(
496            app.handle_key(Key::parse("alt+enter").unwrap()),
497            Effect::PrintAndExit(OsString::from("a"))
498        );
499    }
500
501    #[test]
502    fn descend_expands_and_focuses_first_child() {
503        let (_d, mut app) = app();
504        app.run_command(AppCommand::Descend);
505        assert_eq!(focused_name(&mut app), "aa");
506    }
507
508    #[test]
509    fn sibling_navigation_skips_expanded_children() {
510        let (_d, mut app) = app();
511        app.run_command(AppCommand::Expand); // "a" expanded, children visible
512        app.run_command(AppCommand::NextSibling);
513        assert_eq!(focused_name(&mut app), "b");
514        app.run_command(AppCommand::PrevSibling);
515        assert_eq!(focused_name(&mut app), "a");
516        // No previous sibling: no-op.
517        app.run_command(AppCommand::PrevSibling);
518        assert_eq!(focused_name(&mut app), "a");
519    }
520
521    #[test]
522    fn first_and_last() {
523        let (_d, mut app) = app();
524        app.run_command(AppCommand::Last);
525        assert_eq!(focused_name(&mut app), "c.txt");
526        app.run_command(AppCommand::First);
527        assert_eq!(focused_name(&mut app), "a");
528    }
529
530    #[test]
531    fn paging_moves_focus_by_page_amounts() {
532        let (_d, mut app) = app();
533        app.run_command(AppCommand::ExpandRecursively); // 6 visible rows
534        app.page_height = 4;
535        app.run_command(AppCommand::HalfPageDown);
536        assert_eq!(focused_name(&mut app), "aaa.txt"); // moved 2
537        app.run_command(AppCommand::PageDown);
538        assert_eq!(focused_name(&mut app), "c.txt"); // clamped at end
539        app.run_command(AppCommand::HalfPageUp);
540        assert_eq!(focused_name(&mut app), "ab.txt");
541        app.run_command(AppCommand::PageUp);
542        assert_eq!(focused_name(&mut app), "a");
543    }
544
545    #[test]
546    fn default_keys_drive_commands() {
547        let (_d, mut app) = app();
548        app.handle_key(Key::parse("j").unwrap());
549        assert_eq!(focused_name(&mut app), "b");
550        app.handle_key(Key::parse("k").unwrap());
551        assert_eq!(focused_name(&mut app), "a");
552        app.handle_key(Key::parse("l").unwrap());
553        assert_eq!(app.visible_names().len(), 5);
554        app.handle_key(Key::parse("h").unwrap());
555        assert_eq!(app.visible_names().len(), 3);
556        assert_eq!(app.handle_key(Key::parse("q").unwrap()), Effect::Quit);
557        assert_eq!(app.handle_key(Key::parse("esc").unwrap()), Effect::Quit);
558        assert_eq!(app.handle_key(Key::parse("ctrl+c").unwrap()), Effect::Quit);
559    }
560
561    #[test]
562    fn gg_chord_goes_to_first_line() {
563        let (_d, mut app) = app();
564        app.run_command(AppCommand::Last);
565        assert_eq!(app.handle_key(Key::parse("g").unwrap()), Effect::None);
566        app.handle_key(Key::parse("g").unwrap());
567        assert_eq!(focused_name(&mut app), "a");
568        // A non-g key cancels the pending chord.
569        app.run_command(AppCommand::Last);
570        app.handle_key(Key::parse("g").unwrap());
571        app.handle_key(Key::parse("j").unwrap());
572        assert_eq!(focused_name(&mut app), "c.txt");
573    }
574
575    #[test]
576    fn shift_g_goes_to_last_visible_line() {
577        let (_d, mut app) = app();
578        app.handle_key(Key::parse("G").unwrap());
579        assert_eq!(focused_name(&mut app), "c.txt");
580    }
581
582    #[test]
583    fn user_binding_produces_shell_effect_with_paths() {
584        let (_d, tree) = fixture();
585        let config = Config::parse("[ctrl+e]\nsh = \"vim $path\"\nexit = true\n").unwrap();
586        let mut app = App::new(tree, &config, None);
587        app.run_command(AppCommand::Down); // focus "b"
588        let effect = app.handle_key(Key::parse("ctrl+e").unwrap());
589        let Effect::RunShell {
590            cmd,
591            path,
592            relpath,
593            bg,
594            exit,
595        } = effect
596        else {
597            panic!("expected RunShell, got {effect:?}");
598        };
599        assert_eq!(cmd, "vim $path");
600        assert!(std::path::Path::new(&path).is_absolute());
601        assert!(std::path::Path::new(&path).ends_with("b"));
602        assert_eq!(relpath, OsString::from("b"));
603        assert!(!bg);
604        assert!(exit);
605    }
606
607    #[test]
608    fn user_binding_overrides_default() {
609        let (_d, tree) = fixture();
610        let config = Config::parse("[j]\ncmd = \"quit\"\n").unwrap();
611        let mut app = App::new(tree, &config, None);
612        assert_eq!(app.handle_key(Key::parse("j").unwrap()), Effect::Quit);
613    }
614
615    #[test]
616    fn unbound_key_is_noop() {
617        let (_d, mut app) = app();
618        assert_eq!(app.handle_key(Key::parse("x").unwrap()), Effect::None);
619    }
620
621    #[test]
622    fn initial_expand_depth_one_expands_top_level_only() {
623        let (_d, tree) = fixture();
624        let mut app = App::new(tree, &Config::default(), Some(ExpandSpec::Depth(1)));
625        assert_eq!(
626            app.visible_names(),
627            ["a", "aa", "ab.txt", "b", "ba.txt", "c.txt"]
628        );
629    }
630
631    #[test]
632    fn initial_expand_all_expands_everything() {
633        let (_d, tree) = fixture();
634        let mut app = App::new(tree, &Config::default(), Some(ExpandSpec::All));
635        assert_eq!(
636            app.visible_names(),
637            ["a", "aa", "aaa.txt", "ab.txt", "b", "ba.txt", "c.txt"]
638        );
639    }
640
641    fn in_jump(app: &App) -> bool {
642        matches!(app.mode, Mode::Jump(_))
643    }
644
645    #[test]
646    fn slash_opens_the_jump_picker() {
647        let (_d, mut app) = app();
648        assert!(!in_jump(&app));
649        app.handle_key(Key::parse("/").unwrap());
650        assert!(in_jump(&app));
651    }
652
653    #[test]
654    fn cancelling_the_picker_leaves_focus_untouched() {
655        let (_d, mut app) = app();
656        app.run_command(AppCommand::Down); // focus "b"
657        assert_eq!(focused_name(&mut app), "b");
658        app.handle_key(Key::parse("/").unwrap());
659        app.handle_key(Key::parse("a").unwrap()); // type into the query
660        app.handle_key(Key::parse("esc").unwrap());
661        assert!(!in_jump(&app));
662        assert_eq!(focused_name(&mut app), "b");
663    }
664
665    #[test]
666    fn accepting_jumps_focus_and_expands_ancestors() {
667        let (_d, mut app) = app();
668        // Everything starts collapsed: only the top level is visible.
669        assert_eq!(app.visible_names(), ["a", "b", "c.txt"]);
670        app.handle_key(Key::parse("/").unwrap());
671        for k in ["a", "a", "a"] {
672            app.handle_key(Key::parse(k).unwrap()); // query "aaa" -> a/aa/aaa.txt
673        }
674        app.handle_key(Key::parse("enter").unwrap());
675        assert!(!in_jump(&app));
676        assert_eq!(focused_name(&mut app), "aaa.txt");
677        // The path to it was expanded so the focused node is visible.
678        assert!(app.visible_names().contains(&"aaa.txt".to_string()));
679    }
680
681    #[test]
682    fn a_user_can_rebind_jump_off_slash() {
683        let (_d, tree) = fixture();
684        let config = Config::parse("[ctrl+p]\ncmd = \"jump\"\n").unwrap();
685        let mut app = App::new(tree, &config, None);
686        app.handle_key(Key::parse("ctrl+p").unwrap());
687        assert!(in_jump(&app));
688    }
689}