fm/event/
event_action.rs

1use std::borrow::Borrow;
2use std::path;
3
4use anyhow::{Context, Result};
5use clap::Parser;
6use indicatif::InMemoryTerm;
7
8use crate::app::{Direction, Focus, Status, Tab};
9use crate::common::{
10    content_to_clipboard, filename_to_clipboard, filepath_to_clipboard, get_clipboard,
11    open_in_current_neovim, set_clipboard, set_current_dir, tilde, CONFIG_PATH,
12};
13use crate::config::{Bindings, START_FOLDER};
14use crate::io::{read_log, Args, External};
15use crate::log_info;
16use crate::log_line;
17use crate::modes::{
18    help_string, lsblk_and_udisksctl_installed, nvim_inform_ipc, Content, ContentWindow,
19    Direction as FuzzyDirection, Display, FuzzyKind, Go, InputCompleted, InputSimple, LeaveMenu,
20    MarkAction, Menu, Navigate, NeedConfirmation, NvimIPCAction, Preview, PreviewBuilder,
21    ReEnterMenu, Search, Selectable, To,
22};
23
24/// Links events from ratatui to custom actions.
25/// It mutates `Status` or its children `Tab`.
26pub struct EventAction {}
27
28impl EventAction {
29    /// Once a quit event is received, we change a flag and break the main loop.
30    /// It's useful to be able to reset the cursor before leaving the application.
31    /// If a menu is opened, closes it.
32    pub fn quit(status: &mut Status) -> Result<()> {
33        if status.focus.is_file() {
34            status.internal_settings.quit();
35        } else {
36            status.reset_menu_mode()?;
37        }
38        Ok(())
39    }
40
41    /// Refresh the current view, reloading the files. Move the selection to top.
42    pub fn refresh_view(status: &mut Status) -> Result<()> {
43        status.refresh_view()
44    }
45
46    /// Refresh the views if files were modified in current directory.
47    pub fn refresh_if_needed(status: &mut Status) -> Result<()> {
48        status.menu.flagged.remove_non_existant();
49        status.tabs[0].refresh_if_needed()?;
50        status.tabs[1].refresh_if_needed()
51    }
52
53    /// Handle pasting. Paste a file or an input.
54    /// - Paste an absolute, existing path to current directory in Directory or Tree display mode.
55    ///   Does nothing if the file already exists in current directory.
56    /// - Paste a text in an input box.
57    /// - Does nothing otherwiser.
58    pub fn paste(status: &mut Status, pasted: String) -> Result<()> {
59        log_info!("pasted: ###'{pasted}'###");
60        let pasted = pasted.trim();
61        let display_mode = status.current_tab().display_mode;
62        if status.focus.is_file() && (display_mode.is_tree() || display_mode.is_directory()) {
63            status.paste_pathes(pasted)
64        } else {
65            status.paste_input(pasted)
66        }
67    }
68
69    pub fn resize(status: &mut Status, width: u16, height: u16) -> Result<()> {
70        status.resize(width, height)
71    }
72
73    /// Leave current mode to normal mode.
74    /// Reset the inputs and completion, reset the window, exit the preview.
75    pub fn reset_mode(status: &mut Status) -> Result<()> {
76        if status.focus.is_file() && status.current_tab().display_mode.is_preview() {
77            status.leave_preview()?;
78        }
79        if matches!(status.current_tab().menu_mode, Menu::Nothing) {
80            status.current_tab_mut().reset_visual();
81            return Ok(());
82        };
83        status.leave_menu_mode()?;
84        status.menu.input.reset();
85        status.menu.completion.reset();
86        Ok(())
87    }
88
89    /// Toggle between a full display (aka ls -lah) or a simple mode (only the
90    /// filenames).
91    pub fn toggle_display_full(status: &mut Status) -> Result<()> {
92        status.session.toggle_metadata();
93        Ok(())
94    }
95
96    /// Toggle between dualpane and single pane. Does nothing if the width
97    /// is too low to display both panes.
98    pub fn toggle_dualpane(status: &mut Status) -> Result<()> {
99        status.clear_preview_right();
100        status.session.toggle_dual();
101        status.select_left();
102        Ok(())
103    }
104
105    /// Toggle the second pane between preview & normal mode (files).
106    pub fn toggle_preview_second(status: &mut Status) -> Result<()> {
107        if !status.session.dual() {
108            Self::toggle_dualpane(status)?;
109            status.session.set_preview();
110        } else {
111            status.session.toggle_preview();
112        }
113        if status.session.preview() {
114            status.update_second_pane_for_preview()
115        } else {
116            status.set_menu_mode(1, Menu::Nothing)?;
117            status.tabs[1].display_mode = Display::Directory;
118            status.tabs[1].refresh_view()
119        }
120    }
121
122    /// Creates a tree in every mode but "Tree".
123    /// In display_mode tree it will exit this view.
124    pub fn tree(status: &mut Status) -> Result<()> {
125        if !status.focus.is_file() {
126            return Ok(());
127        }
128        status.current_tab_mut().toggle_tree_mode()?;
129        status.refresh_view()
130    }
131
132    /// Decrease the depth of tree in tree mode
133    pub fn tree_depth_decr(status: &mut Status) -> Result<()> {
134        if !status.focus.is_file() && status.current_tab().display_mode.is_tree() {
135            return Ok(());
136        }
137        let current_depth = status.current_tab().settings.tree_max_depth;
138        if current_depth > 1 {
139            status.current_tab_mut().settings.tree_max_depth -= 1;
140            let new_depth = status.current_tab().settings.tree_max_depth;
141            log_info!("Decrease tree depth current: {new_depth}");
142            log_line!("Decrease tree depth current: {new_depth}");
143        }
144        status.current_tab_mut().toggle_tree_mode()?;
145        status.current_tab_mut().toggle_tree_mode()?;
146        Ok(())
147    }
148
149    /// Increase the depth of tree in tree mode
150    pub fn tree_depth_incr(status: &mut Status) -> Result<()> {
151        if !status.focus.is_file() && status.current_tab().display_mode.is_tree() {
152            return Ok(());
153        }
154        status.current_tab_mut().settings.tree_max_depth += 1;
155        let new_depth = status.current_tab().settings.tree_max_depth;
156        log_info!("Increase tree depth current: {new_depth}");
157        log_line!("Increase tree depth current: {new_depth}");
158
159        status.current_tab_mut().toggle_tree_mode()?;
160        status.current_tab_mut().toggle_tree_mode()?;
161        Ok(())
162    }
163
164    /// Fold the current node of the tree.
165    /// Has no effect on "file" nodes.
166    pub fn tree_fold(status: &mut Status) -> Result<()> {
167        if !status.focus.is_file() {
168            return Ok(());
169        }
170        let tab = status.current_tab_mut();
171        tab.tree.toggle_fold();
172        Ok(())
173    }
174
175    /// Unfold every child node in the tree.
176    /// Recursively explore the tree and unfold every node.
177    /// Reset the display.
178    pub fn tree_unfold_all(status: &mut Status) -> Result<()> {
179        if !status.focus.is_file() {
180            return Ok(());
181        }
182        let tab = status.current_tab_mut();
183        tab.tree.unfold_all();
184        Ok(())
185    }
186
187    /// Fold every child node in the tree.
188    /// Recursively explore the tree and fold every node.
189    /// Reset the display.
190    pub fn tree_fold_all(status: &mut Status) -> Result<()> {
191        if !status.focus.is_file() {
192            return Ok(());
193        }
194        let tab = status.current_tab_mut();
195        tab.tree.fold_all();
196        Ok(())
197    }
198
199    /// Toggle the display of flagged files.
200    /// Does nothing if a menu is opened.
201    pub fn display_flagged(status: &mut Status) -> Result<()> {
202        if !status.focus.is_file() {
203            return Ok(());
204        }
205        let menu_mode = &status.current_tab().menu_mode;
206        if matches!(menu_mode, Menu::Navigate(Navigate::Flagged)) {
207            status.leave_menu_mode()?;
208        } else if matches!(menu_mode, Menu::Nothing) {
209            status.set_menu_mode(status.index, Menu::Navigate(Navigate::Flagged))?;
210        }
211        Ok(())
212    }
213
214    /// Preview the selected file.
215    /// Every file can be previewed. See the `crate::enum::Preview` for
216    /// more details on previewinga file.
217    /// Does nothing if the directory is empty.
218    pub fn preview(status: &mut Status) -> Result<()> {
219        if !status.focus.is_file() {
220            return Ok(());
221        }
222        status.current_tab_mut().make_preview()
223    }
224
225    /// Toggle the display of hidden files.
226    pub fn toggle_hidden(status: &mut Status) -> Result<()> {
227        if !status.focus.is_file() || status.current_tab().display_mode.is_preview() {
228            return Ok(());
229        }
230        status.current_tab_mut().toggle_hidden()
231    }
232
233    /// Remove every flag on files in this directory and others.
234    pub fn clear_flags(status: &mut Status) -> Result<()> {
235        if status.focus.is_file() {
236            status.menu.flagged.clear();
237        }
238        Ok(())
239    }
240
241    /// Flag all files in the current directory.
242    pub fn flag_all(status: &mut Status) -> Result<()> {
243        if status.focus.is_file() {
244            status.flag_all();
245        }
246        Ok(())
247    }
248
249    /// Push every flagged path to the clipboard.
250    pub fn flagged_to_clipboard(status: &mut Status) -> Result<()> {
251        set_clipboard(status.menu.flagged.content_to_string());
252        Ok(())
253    }
254
255    /// Replace the currently flagged files by those in clipboard.
256    /// Does nothing if the clipboard is empty or can't be read.
257    pub fn flagged_from_clipboard(status: &mut Status) -> Result<()> {
258        let Some(files) = get_clipboard() else {
259            return Ok(());
260        };
261        if files.is_empty() {
262            return Ok(());
263        }
264        log_info!("clipboard read: {files}");
265        status.menu.flagged.replace_by_string(files);
266        Ok(())
267    }
268
269    /// Reverse every flag in _current_ directory. Flagged files in other
270    /// directory aren't affected.
271    pub fn reverse_flags(status: &mut Status) -> Result<()> {
272        if status.focus.is_file() {
273            status.reverse_flags();
274        }
275        Ok(())
276    }
277
278    /// Toggle a single flag and move down one row.
279    pub fn toggle_flag(status: &mut Status) -> Result<()> {
280        if status.focus.is_file() {
281            status.toggle_flag_for_selected();
282        }
283        Ok(())
284    }
285
286    /// Flag all _file_ (everything but directories) which are children of a directory.
287    pub fn toggle_flag_children(status: &mut Status) -> Result<()> {
288        if status.focus.is_file() {
289            status.toggle_flag_for_children();
290        }
291        Ok(())
292    }
293
294    pub fn reenter_menu_from_picker(status: &mut Status, menu: Menu) -> Result<()> {
295        menu.reenter(status)?;
296        if !menu.is_input() {
297            return Ok(());
298        }
299        let Some(picked) = &status.menu.picker.selected() else {
300            return Ok(());
301        };
302        status.menu.input.replace(picked);
303        let Menu::InputCompleted(input_completed) = menu else {
304            return Ok(());
305        };
306        status.complete(input_completed)
307    }
308
309    /// Enter the rename mode.
310    /// Keep a track of the current mode to ensure we rename the correct file.
311    /// When we enter rename from a "tree" mode, we'll need to rename the selected file in the tree,
312    /// not the selected file in the pathcontent.
313    pub fn rename(status: &mut Status) -> Result<()> {
314        if matches!(
315            status.current_tab().menu_mode,
316            Menu::InputSimple(InputSimple::Rename)
317        ) {
318            status.reset_menu_mode()?;
319            return Ok(());
320        };
321        let selected_path = status
322            .current_tab()
323            .selected_path()
324            .context("No selected file")?;
325        if selected_path == status.current_tab().directory.path {
326            return Ok(());
327        }
328        if let Some(parent) = status.current_tab().directory.path.parent() {
329            if selected_path.as_ref() == parent {
330                return Ok(());
331            }
332        }
333        let old_name = &selected_path.to_string_lossy();
334        status.set_menu_mode(status.index, Menu::InputSimple(InputSimple::Rename))?;
335        status.menu.input.replace(old_name);
336        Ok(())
337    }
338
339    /// Enter a copy paste mode.
340    /// A confirmation is asked before copying all flagged files to
341    /// the current directory.
342    /// Does nothing if no file is flagged.
343    pub fn copy_paste(status: &mut Status) -> Result<()> {
344        if matches!(
345            status.current_tab().menu_mode,
346            Menu::NeedConfirmation(NeedConfirmation::Copy)
347        ) {
348            status.reset_menu_mode()?;
349        } else {
350            Self::set_copy_paste(status, NeedConfirmation::Copy)?;
351        }
352        Ok(())
353    }
354
355    /// Enter the 'move' mode.
356    /// A confirmation is asked before moving all flagged files to
357    /// the current directory.
358    /// Does nothing if no file is flagged.
359    pub fn cut_paste(status: &mut Status) -> Result<()> {
360        if matches!(
361            status.current_tab().menu_mode,
362            Menu::NeedConfirmation(NeedConfirmation::Move)
363        ) {
364            status.reset_menu_mode()?;
365        } else {
366            Self::set_copy_paste(status, NeedConfirmation::Move)?;
367        }
368        Ok(())
369    }
370
371    fn set_copy_paste(status: &mut Status, copy_or_move: NeedConfirmation) -> Result<()> {
372        if status.menu.flagged.is_empty() {
373            return Ok(());
374        }
375        status.set_menu_mode(status.index, Menu::NeedConfirmation(copy_or_move))
376    }
377
378    /// Creates a symlink of every flagged file to the current directory.
379    pub fn symlink(status: &mut Status) -> Result<()> {
380        if !status.focus.is_file() {
381            return Ok(());
382        }
383        for original_file in status.menu.flagged.content.iter() {
384            let filename = original_file
385                .as_path()
386                .file_name()
387                .context("event symlink: File not found")?;
388            let link = status.current_tab().directory_of_selected()?.join(filename);
389            std::os::unix::fs::symlink(original_file, &link)?;
390            log_line!(
391                "Symlink {link} links to {original_file}",
392                original_file = original_file.display(),
393                link = link.display()
394            );
395        }
396        status.clear_flags_and_reset_view()
397    }
398
399    /// Enter the delete mode.
400    /// A confirmation is then asked before deleting all the flagged files.
401    /// If no file is flagged, flag the selected one before entering the mode.
402    pub fn delete_file(status: &mut Status) -> Result<()> {
403        if status.menu.flagged.is_empty() {
404            Self::toggle_flag(status)?;
405        }
406        status.set_menu_mode(
407            status.index,
408            Menu::NeedConfirmation(NeedConfirmation::Delete),
409        )
410    }
411
412    /// Change to CHMOD mode allowing to edit permissions of a file.
413    pub fn chmod(status: &mut Status) -> Result<()> {
414        if matches!(
415            status.current_tab().menu_mode,
416            Menu::InputSimple(InputSimple::Chmod)
417        ) {
418            status.reset_menu_mode()?;
419        } else {
420            status.set_mode_chmod()?;
421        }
422        Ok(())
423    }
424
425    /// Enter a new node mode.
426    fn new_node(status: &mut Status, input_kind: InputSimple) -> Result<()> {
427        if !matches!(input_kind, InputSimple::Newdir | InputSimple::Newfile) {
428            return Ok(());
429        }
430        if matches!(
431            status.current_tab().menu_mode,
432            Menu::InputSimple(InputSimple::Newdir | InputSimple::Newfile)
433        ) {
434            status.reset_menu_mode()?;
435            return Ok(());
436        }
437        if matches!(
438            status.current_tab().display_mode,
439            Display::Directory | Display::Tree
440        ) {
441            status.set_menu_mode(status.index, Menu::InputSimple(input_kind))?;
442        }
443        Ok(())
444    }
445
446    /// Enter the new dir mode.
447    pub fn new_dir(status: &mut Status) -> Result<()> {
448        Self::new_node(status, InputSimple::Newdir)
449    }
450
451    /// Enter the new file mode.
452    pub fn new_file(status: &mut Status) -> Result<()> {
453        Self::new_node(status, InputSimple::Newfile)
454    }
455
456    fn enter_file(status: &mut Status) -> Result<()> {
457        match status.current_tab_mut().display_mode {
458            Display::Directory => Self::normal_enter_file(status),
459            Display::Tree => Self::tree_enter_file(status),
460            Display::Fuzzy => status.fuzzy_select(),
461            Display::Preview => status.enter_from_preview(),
462        }
463    }
464
465    /// Open the file with configured opener or enter the directory.
466    fn normal_enter_file(status: &mut Status) -> Result<()> {
467        let tab = status.current_tab_mut();
468        if tab.display_mode.is_tree() {
469            return EventAction::open_file(status);
470        };
471        if tab.directory.is_empty() {
472            return Ok(());
473        }
474        if tab.directory.is_selected_dir()? {
475            tab.go_to_selected_dir()?;
476            status.thumbnail_directory_video();
477            Ok(())
478        } else {
479            EventAction::open_file(status)
480        }
481    }
482
483    /// Execute the selected node if it's a file else enter the directory.
484    fn tree_enter_file(status: &mut Status) -> Result<()> {
485        let path = status
486            .current_tab()
487            .selected_path()
488            .context("No selected file")?;
489        if path.is_dir() {
490            status.current_tab_mut().tree_enter_dir(path)
491        } else {
492            EventAction::open_file(status)
493        }
494    }
495
496    /// Open files with custom opener.
497    /// If there's no flagged file, the selected is chosen.
498    /// Otherwise, it will open the flagged files (not the flagged directories) with
499    /// their respective opener.
500    /// Directories aren't opened since it will lead nowhere, it would only replace the
501    /// current tab multiple times. It may change in the future.
502    /// Only files which use an external opener are supported.
503    pub fn open_file(status: &mut Status) -> Result<()> {
504        if !status.focus.is_file() {
505            return Ok(());
506        }
507        if status.menu.flagged.is_empty() {
508            status.open_selected_file()
509        } else {
510            status.open_flagged_files()
511        }
512    }
513
514    pub fn open_all(status: &mut Status) -> Result<()> {
515        status.open_flagged_files()
516    }
517
518    /// Enter the execute mode.
519    pub fn exec(status: &mut Status) -> Result<()> {
520        if matches!(
521            status.current_tab().menu_mode,
522            Menu::InputCompleted(InputCompleted::Exec)
523        ) {
524            status.reset_menu_mode()?;
525            return Ok(());
526        }
527        if status.menu.flagged.is_empty() {
528            status.menu.flagged.push(
529                status
530                    .current_tab()
531                    .selected_path()
532                    .context("No selected file")?
533                    .to_path_buf(),
534            );
535        }
536        status.set_menu_mode(status.index, Menu::InputCompleted(InputCompleted::Exec))
537    }
538
539    /// Enter the sort mode, allowing the user to select a sort method.
540    pub fn sort(status: &mut Status) -> Result<()> {
541        if matches!(
542            status.current_tab().menu_mode,
543            Menu::InputSimple(InputSimple::Sort)
544        ) {
545            status.reset_menu_mode()?;
546        }
547        status.set_height_for_menu_mode(status.index, Menu::Nothing)?;
548        status.tabs[status.index].menu_mode = Menu::Nothing;
549        let len = status.menu.len(Menu::Nothing);
550        let height = status.second_window_height()?;
551        status.menu.window = ContentWindow::new(len, height);
552        status.tabs[status.index].menu_mode = Menu::InputSimple(InputSimple::Sort);
553        status.set_focus_from_mode();
554        Ok(())
555    }
556
557    /// Enter the filter mode, where you can filter.
558    /// See `crate::modes::Filter` for more details.
559    pub fn filter(status: &mut Status) -> Result<()> {
560        if matches!(
561            status.current_tab().menu_mode,
562            Menu::InputSimple(InputSimple::Filter)
563        ) {
564            status.reset_menu_mode()?;
565        } else if matches!(
566            status.current_tab().display_mode,
567            Display::Tree | Display::Directory
568        ) {
569            status.set_menu_mode(status.index, Menu::InputSimple(InputSimple::Filter))?;
570        }
571        Ok(())
572    }
573
574    /// Enter bulkrename mode, opening a random temp file where the user
575    /// can edit the selected filenames.
576    /// Once the temp file is saved, those file names are changed.
577    pub fn bulk(status: &mut Status) -> Result<()> {
578        if !status.focus.is_file() {
579            return Ok(());
580        }
581        status.bulk_ask_filenames()?;
582        Ok(())
583    }
584
585    /// Enter the search mode.
586    /// Matching items are displayed as you type them.
587    pub fn search(status: &mut Status) -> Result<()> {
588        if matches!(
589            status.current_tab().menu_mode,
590            Menu::InputCompleted(InputCompleted::Search)
591        ) {
592            status.reset_menu_mode()?;
593        }
594        let tab = status.current_tab_mut();
595        tab.search = Search::empty();
596        status.set_menu_mode(status.index, Menu::InputCompleted(InputCompleted::Search))
597    }
598
599    /// Enter the regex mode.
600    /// Every file matching the typed regex will be flagged.
601    pub fn regex_match(status: &mut Status) -> Result<()> {
602        if matches!(
603            status.current_tab().menu_mode,
604            Menu::InputSimple(InputSimple::RegexMatch)
605        ) {
606            status.reset_menu_mode()?;
607        }
608        if matches!(
609            status.current_tab().display_mode,
610            Display::Tree | Display::Directory
611        ) {
612            status.set_menu_mode(status.index, Menu::InputSimple(InputSimple::RegexMatch))
613        } else {
614            Ok(())
615        }
616    }
617
618    /// Display the help which can be navigated and displays the configrable
619    /// binds.
620    pub fn help(status: &mut Status, binds: &Bindings) -> Result<()> {
621        if !status.focus.is_file() {
622            return Ok(());
623        }
624        let help = help_string(binds, &status.internal_settings.opener);
625        status.current_tab_mut().set_display_mode(Display::Preview);
626        status.current_tab_mut().preview = PreviewBuilder::help(&help);
627        let len = status.current_tab().preview.len();
628        status.current_tab_mut().window.reset(len);
629        Ok(())
630    }
631
632    /// Display the last actions impacting the file tree
633    pub fn log(status: &mut Status) -> Result<()> {
634        if !status.focus.is_file() {
635            return Ok(());
636        }
637        let Ok(log) = read_log() else {
638            return Ok(());
639        };
640        let tab = status.current_tab_mut();
641        tab.set_display_mode(Display::Preview);
642        tab.preview = PreviewBuilder::log(log);
643        tab.window.reset(tab.preview.len());
644        tab.preview_go_bottom();
645        Ok(())
646    }
647
648    /// Enter the cd mode where an user can type a path to jump to.
649    pub fn cd(status: &mut Status) -> Result<()> {
650        if matches!(
651            status.current_tab().menu_mode,
652            Menu::InputCompleted(InputCompleted::Cd)
653        ) {
654            status.reset_menu_mode()?;
655        } else {
656            status.set_menu_mode(status.index, Menu::InputCompleted(InputCompleted::Cd))?;
657
658            status.tabs[status.index].save_origin_path();
659            status.menu.completion.reset();
660        }
661        Ok(())
662    }
663
664    /// Open a new terminal in current directory and current window.
665    /// The shell is a fork of current process and will exit if the application
666    /// is terminated first.
667    pub fn shell(status: &mut Status) -> Result<()> {
668        if !status.focus.is_file() {
669            return Ok(());
670        }
671        set_current_dir(status.current_tab().current_directory_path())?;
672        status.internal_settings.disable_display();
673        External::open_shell_in_window()?;
674        status.internal_settings.enable_display();
675        Ok(())
676    }
677
678    /// Enter the shell input command mode. The user can type a command which
679    /// will be parsed and run.
680    pub fn shell_command(status: &mut Status) -> Result<()> {
681        if matches!(
682            status.current_tab().menu_mode,
683            Menu::InputSimple(InputSimple::ShellCommand)
684        ) {
685            status.reset_menu_mode()?;
686        }
687        status.set_menu_mode(status.index, Menu::InputSimple(InputSimple::ShellCommand))
688    }
689
690    /// Enter the shell menu mode. You can pick a TUI application to be run
691    pub fn tui_menu(status: &mut Status) -> Result<()> {
692        if matches!(
693            status.current_tab().menu_mode,
694            Menu::Navigate(Navigate::TuiApplication)
695        ) {
696            status.reset_menu_mode()?;
697        } else {
698            if status.menu.tui_applications.is_not_set() {
699                status.menu.tui_applications.setup();
700            }
701            status.set_menu_mode(status.index, Menu::Navigate(Navigate::TuiApplication))?;
702        }
703        Ok(())
704    }
705
706    /// Enter the cli info mode. You can pick a Text application to be
707    /// displayed/
708    pub fn cli_menu(status: &mut Status) -> Result<()> {
709        if matches!(
710            status.current_tab().menu_mode,
711            Menu::Navigate(Navigate::CliApplication)
712        ) {
713            status.reset_menu_mode()?;
714        } else {
715            if status.menu.cli_applications.is_empty() {
716                status.menu.cli_applications.setup();
717            }
718            status.set_menu_mode(status.index, Menu::Navigate(Navigate::CliApplication))?;
719        }
720        Ok(())
721    }
722
723    /// Enter the history mode, allowing to navigate to previously visited
724    /// directory.
725    pub fn history(status: &mut Status) -> Result<()> {
726        if matches!(
727            status.current_tab().menu_mode,
728            Menu::Navigate(Navigate::History)
729        ) {
730            status.reset_menu_mode()?;
731        } else if matches!(
732            status.current_tab().display_mode,
733            Display::Directory | Display::Tree
734        ) {
735            status.set_menu_mode(status.index, Menu::Navigate(Navigate::History))?;
736        }
737        Ok(())
738    }
739
740    /// Enter Marks new mode, allowing to bind a char to a path.
741    pub fn marks_new(status: &mut Status) -> Result<()> {
742        if matches!(
743            status.current_tab().menu_mode,
744            Menu::Navigate(Navigate::Marks(MarkAction::New))
745        ) {
746            status.reset_menu_mode()?;
747        } else {
748            if status.menu.marks.is_empty() {
749                status.menu.marks.setup();
750            }
751            status.set_menu_mode(
752                status.index,
753                Menu::Navigate(Navigate::Marks(MarkAction::New)),
754            )?;
755        }
756        Ok(())
757    }
758
759    /// Enter Marks jump mode, allowing to jump to a marked file.
760    pub fn marks_jump(status: &mut Status) -> Result<()> {
761        if matches!(
762            status.current_tab().menu_mode,
763            Menu::Navigate(Navigate::Marks(MarkAction::Jump))
764        ) {
765            status.reset_menu_mode()?;
766        } else {
767            if status.menu.marks.is_empty() {
768                status.menu.marks.setup();
769            }
770            if status.menu.marks.is_empty() {
771                return Ok(());
772            }
773            status.set_menu_mode(
774                status.index,
775                Menu::Navigate(Navigate::Marks(MarkAction::Jump)),
776            )?;
777        }
778        Ok(())
779    }
780
781    /// Enter TempMarks jump mode, allowing to jump to a marked file.
782    pub fn temp_marks_jump(status: &mut Status) -> Result<()> {
783        if matches!(
784            status.current_tab().menu_mode,
785            Menu::Navigate(Navigate::TempMarks(MarkAction::Jump))
786        ) {
787            status.reset_menu_mode()?;
788        } else {
789            status.set_menu_mode(
790                status.index,
791                Menu::Navigate(Navigate::TempMarks(MarkAction::Jump)),
792            )?;
793        }
794        Ok(())
795    }
796
797    /// Enter TempMarks new mode, allowing to bind a char to a path.
798    pub fn temp_marks_new(status: &mut Status) -> Result<()> {
799        if matches!(
800            status.current_tab().menu_mode,
801            Menu::Navigate(Navigate::TempMarks(MarkAction::New))
802        ) {
803            status.reset_menu_mode()?;
804        } else {
805            status.set_menu_mode(
806                status.index,
807                Menu::Navigate(Navigate::TempMarks(MarkAction::New)),
808            )?;
809        }
810        Ok(())
811    }
812
813    /// Enter the shortcut mode, allowing to visit predefined shortcuts.
814    /// Basic folders (/, /dev... $HOME) and mount points (even impossible to
815    /// visit ones) are proposed.
816    pub fn shortcut(status: &mut Status) -> Result<()> {
817        if matches!(
818            status.current_tab().menu_mode,
819            Menu::Navigate(Navigate::Shortcut)
820        ) {
821            status.reset_menu_mode()?;
822        } else {
823            status.refresh_shortcuts();
824            set_current_dir(status.current_tab().directory_of_selected()?)?;
825            status.set_menu_mode(status.index, Menu::Navigate(Navigate::Shortcut))?;
826        }
827        Ok(())
828    }
829
830    /// Send a signal to parent NVIM process, picking files.
831    /// If there's no flagged file, it picks the selected one.
832    /// otherwise, flagged files are picked.
833    /// If no RPC server were provided at launch time - which may happen for
834    /// reasons unknow to me - it does nothing.
835    pub fn nvim_filepicker(status: &mut Status) -> Result<()> {
836        if !status.focus.is_file() {
837            return Ok(());
838        }
839        status.update_nvim_listen_address();
840        if status.internal_settings.nvim_server.is_empty() {
841            return Ok(());
842        };
843        let nvim_server = &status.internal_settings.nvim_server;
844        if status.menu.flagged.is_empty() {
845            let Some(path) = status.current_tab().selected_path() else {
846                return Ok(());
847            };
848            open_in_current_neovim(&path, nvim_server);
849        } else {
850            for file_path in status.menu.flagged.content.iter() {
851                open_in_current_neovim(file_path, nvim_server)
852            }
853        }
854
855        Ok(())
856    }
857
858    /// Enter the set neovim RPC address mode where the user can type
859    /// the RPC address himself
860    pub fn set_nvim_server(status: &mut Status) -> Result<()> {
861        if matches!(
862            status.current_tab().menu_mode,
863            Menu::InputSimple(InputSimple::SetNvimAddr)
864        ) {
865            status.reset_menu_mode()?;
866            return Ok(());
867        };
868        status.set_menu_mode(status.index, Menu::InputSimple(InputSimple::SetNvimAddr))
869    }
870
871    /// Move back in history to the last visited directory.
872    pub fn back(status: &mut Status) -> Result<()> {
873        if !status.focus.is_file() {
874            return Ok(());
875        }
876        status.current_tab_mut().back()?;
877        status.update_second_pane_for_preview()
878    }
879
880    /// Move to $HOME aka ~.
881    pub fn home(status: &mut Status) -> Result<()> {
882        if !status.focus.is_file() {
883            return Ok(());
884        }
885        let home_cow = tilde("~");
886        let home: &str = home_cow.borrow();
887        let home_path = path::Path::new(home);
888        status.current_tab_mut().cd(home_path)?;
889        status.update_second_pane_for_preview()
890    }
891
892    pub fn go_root(status: &mut Status) -> Result<()> {
893        if !status.focus.is_file() {
894            return Ok(());
895        }
896        let root_path = std::path::PathBuf::from("/");
897        status.current_tab_mut().cd(&root_path)?;
898        status.update_second_pane_for_preview()
899    }
900
901    pub fn go_start(status: &mut Status) -> Result<()> {
902        if !status.focus.is_file() {
903            return Ok(());
904        }
905        status
906            .current_tab_mut()
907            .cd(START_FOLDER.get().context("Start folder should be set")?)?;
908        status.update_second_pane_for_preview()
909    }
910
911    pub fn search_next(status: &mut Status) -> Result<()> {
912        if !status.focus.is_file() {
913            return Ok(());
914        }
915        match status.current_tab().display_mode {
916            Display::Tree => status.tabs[status.index]
917                .search
918                .tree(&mut status.tabs[status.index].tree),
919            Display::Directory => status.current_tab_mut().directory_search_next(),
920            Display::Preview | Display::Fuzzy => {
921                return Ok(());
922            }
923        }
924        status.refresh_status()?;
925        status.update_second_pane_for_preview()?;
926        Ok(())
927    }
928
929    /// Move up one row in modes allowing movement.
930    /// Does nothing if the selected item is already the first in list.
931    pub fn move_up(status: &mut Status) -> Result<()> {
932        if status.focus.is_file() {
933            Self::move_display_up(status)?;
934        } else {
935            let tab = status.current_tab_mut();
936            match tab.menu_mode {
937                Menu::Nothing => Self::move_display_up(status)?,
938                Menu::Navigate(Navigate::History) => tab.history.prev(),
939                Menu::Navigate(navigate) => status.menu.prev(navigate),
940                Menu::InputCompleted(input_completed) => {
941                    status.menu.completion_prev(input_completed);
942                    if matches!(input_completed, InputCompleted::Search) {
943                        status.follow_search()?;
944                    }
945                }
946                Menu::NeedConfirmation(need_confirmation)
947                    if need_confirmation.use_flagged_files() =>
948                {
949                    status.menu.prev(Navigate::Flagged)
950                }
951                Menu::NeedConfirmation(NeedConfirmation::EmptyTrash) => {
952                    status.menu.prev(Navigate::Trash)
953                }
954                Menu::NeedConfirmation(NeedConfirmation::BulkAction) => {
955                    status.menu.bulk.prev();
956                    status.menu.window.scroll_to(status.menu.bulk.index())
957                }
958                _ => (),
959            };
960        }
961        status.update_second_pane_for_preview()
962    }
963
964    /// Special move to the next "thing".
965    /// if we're in tree mode, focusing a file, it's the next sibling (= node of the same level sharing parent)
966    /// if we're inputing something, it's the next history result
967    pub fn next_thing(status: &mut Status) -> Result<()> {
968        if status.current_tab().display_mode.is_tree() && status.focus.is_file() {
969            status.current_tab_mut().tree_next_sibling();
970        } else {
971            status.input_history_prev()?;
972        }
973        Ok(())
974    }
975
976    /// Special move to the previous "thing".
977    /// if we're in tree mode, focusing a file, it's the previous sibling (= node of the same level sharing parent)
978    /// if we're inputing something, it's the previous history result
979    pub fn previous_thing(status: &mut Status) -> Result<()> {
980        if status.current_tab().display_mode.is_tree() && status.focus.is_file() {
981            status.current_tab_mut().tree_prev_sibling();
982        } else {
983            status.input_history_next()?;
984        }
985        Ok(())
986    }
987
988    pub fn next_word(status: &mut Status) -> Result<()> {
989        if status.current_tab().menu_mode.is_input() && !status.focus.is_file() {
990            status.menu.input.next_word();
991        }
992        Ok(())
993    }
994
995    pub fn previous_word(status: &mut Status) -> Result<()> {
996        if status.current_tab().menu_mode.is_input() && !status.focus.is_file() {
997            status.menu.input.previous_word();
998        }
999        Ok(())
1000    }
1001
1002    fn move_display_up(status: &mut Status) -> Result<()> {
1003        let tab = status.current_tab_mut();
1004        match tab.display_mode {
1005            Display::Directory => {
1006                tab.normal_up_one_row();
1007                status.toggle_flag_visual();
1008            }
1009            Display::Preview if matches!(&tab.preview, Preview::Tree(_)) => {
1010                if let Preview::Tree(tree) = &mut tab.preview {
1011                    tree.go(To::Prev);
1012                    tab.window.scroll_up_one(tree.displayable().index());
1013                }
1014            }
1015            Display::Preview => tab.preview_page_up(),
1016            Display::Tree => {
1017                tab.tree_select_prev();
1018                status.toggle_flag_visual();
1019            }
1020            Display::Fuzzy => status.fuzzy_navigate(FuzzyDirection::Up)?,
1021        }
1022        Ok(())
1023    }
1024
1025    fn move_display_down(status: &mut Status) -> Result<()> {
1026        let tab = status.current_tab_mut();
1027        match tab.display_mode {
1028            Display::Directory => {
1029                tab.normal_down_one_row();
1030                status.toggle_flag_visual();
1031            }
1032            Display::Preview if matches!(&tab.preview, Preview::Tree(_)) => {
1033                if let Preview::Tree(tree) = &mut tab.preview {
1034                    tree.go(To::Next);
1035                    tab.window.scroll_down_one(tree.displayable().index());
1036                }
1037            }
1038            Display::Preview => tab.preview_page_down(),
1039            Display::Tree => {
1040                tab.tree_select_next();
1041                status.toggle_flag_visual()
1042            }
1043            Display::Fuzzy => status.fuzzy_navigate(FuzzyDirection::Down)?,
1044        }
1045        Ok(())
1046    }
1047    /// Move down one row in modes allowing movements.
1048    /// Does nothing if the user is already at the bottom.
1049    pub fn move_down(status: &mut Status) -> Result<()> {
1050        if status.focus.is_file() {
1051            Self::move_display_down(status)?
1052        } else {
1053            match status.current_tab_mut().menu_mode {
1054                Menu::Nothing => Self::move_display_down(status)?,
1055                Menu::Navigate(Navigate::History) => status.current_tab_mut().history.next(),
1056                Menu::Navigate(navigate) => status.menu.next(navigate),
1057                Menu::InputCompleted(input_completed) => {
1058                    status.menu.completion_next(input_completed);
1059                    if matches!(input_completed, InputCompleted::Search) {
1060                        status.follow_search()?;
1061                    }
1062                }
1063                Menu::NeedConfirmation(need_confirmation)
1064                    if need_confirmation.use_flagged_files() =>
1065                {
1066                    status.menu.next(Navigate::Flagged)
1067                }
1068                Menu::NeedConfirmation(NeedConfirmation::EmptyTrash) => {
1069                    status.menu.next(Navigate::Trash)
1070                }
1071                Menu::NeedConfirmation(NeedConfirmation::BulkAction) => {
1072                    status.menu.bulk.next();
1073                    status.menu.window.scroll_to(status.menu.bulk.index())
1074                }
1075                _ => (),
1076            };
1077        }
1078        status.update_second_pane_for_preview()
1079    }
1080
1081    /// Move to parent in normal mode,
1082    /// move left one char in mode requiring text input.
1083    pub fn move_left(status: &mut Status) -> Result<()> {
1084        if status.focus.is_file() {
1085            Self::file_move_left(status.current_tab_mut())?;
1086        } else {
1087            let tab = status.current_tab_mut();
1088            match tab.menu_mode {
1089                Menu::InputSimple(_) | Menu::InputCompleted(_) => {
1090                    status.menu.input.cursor_left();
1091                }
1092                Menu::Nothing => Self::file_move_left(tab)?,
1093                Menu::Navigate(Navigate::Cloud) => status.cloud_move_to_parent()?,
1094                _ => (),
1095            }
1096        }
1097        status.update_second_pane_for_preview()
1098    }
1099
1100    fn file_move_left(tab: &mut Tab) -> Result<()> {
1101        match tab.display_mode {
1102            Display::Directory => tab.move_to_parent()?,
1103            Display::Tree => tab.tree_select_parent()?,
1104            _ => (),
1105        };
1106        Ok(())
1107    }
1108
1109    /// Move to child if any or open a regular file in normal mode.
1110    /// Move the cursor one char to right in mode requiring text input.
1111    pub fn move_right(status: &mut Status) -> Result<()> {
1112        if status.focus.is_file() {
1113            Self::enter_file(status)
1114        } else {
1115            let tab: &mut Tab = status.current_tab_mut();
1116            match tab.menu_mode {
1117                Menu::InputSimple(_) | Menu::InputCompleted(_) => {
1118                    status.menu.input.cursor_right();
1119                    Ok(())
1120                }
1121                Menu::Navigate(Navigate::Cloud) => status.cloud_enter_file_or_dir(),
1122                Menu::Nothing => Self::enter_file(status),
1123                _ => Ok(()),
1124            }
1125        }
1126    }
1127
1128    pub fn focus_follow_mouse(status: &mut Status, row: u16, col: u16) -> Result<()> {
1129        status.set_focus_from_pos(row, col)?;
1130        Ok(())
1131    }
1132
1133    /// Click a file at `row`, `col`. Gives the focus to the window container.
1134    fn click(status: &mut Status, binds: &Bindings, row: u16, col: u16) -> Result<()> {
1135        status.click(binds, row, col)
1136    }
1137
1138    /// Left Click a file at `row`, `col`. Gives the focus to the window container.
1139    pub fn left_click(status: &mut Status, binds: &Bindings, row: u16, col: u16) -> Result<()> {
1140        Self::click(status, binds, row, col)
1141    }
1142
1143    /// Right click gives focus to the window and open the context menu
1144    pub fn right_click(status: &mut Status, binds: &Bindings, row: u16, col: u16) -> Result<()> {
1145        Self::click(status, binds, row, col)?;
1146        Self::context(status)
1147    }
1148
1149    /// Wheel up moves the display up
1150    pub fn wheel_up(status: &mut Status, row: u16, col: u16) -> Result<()> {
1151        status.set_focus_from_pos(row, col)?;
1152        Self::move_up(status)
1153    }
1154
1155    /// Wheel down moves the display down
1156    pub fn wheel_down(status: &mut Status, row: u16, col: u16) -> Result<()> {
1157        status.set_focus_from_pos(row, col)?;
1158        Self::move_down(status)
1159    }
1160
1161    /// A middle_click click opens a file or execute a menu item
1162    pub fn middle_click(status: &mut Status, binds: &Bindings, row: u16, col: u16) -> Result<()> {
1163        if Self::click(status, binds, row, col).is_ok() {
1164            if status.focus.is_file() {
1165                Self::enter_file(status)?;
1166            } else {
1167                LeaveMenu::leave_menu(status, binds)?;
1168            }
1169        };
1170        Ok(())
1171    }
1172
1173    /// Delete a char to the left in modes allowing edition.
1174    pub fn backspace(status: &mut Status) -> Result<()> {
1175        if status.focus.is_file() {
1176            return Ok(());
1177        }
1178        match status.current_tab().menu_mode {
1179            Menu::Navigate(Navigate::Marks(_)) => {
1180                status.menu.marks.remove_selected()?;
1181            }
1182            Menu::Navigate(Navigate::TempMarks(_)) => {
1183                status.menu.temp_marks.erase_current_mark();
1184            }
1185            Menu::InputSimple(_) => {
1186                status.menu.input.delete_char_left();
1187            }
1188            Menu::InputCompleted(input_completed) => {
1189                status.menu.input.delete_char_left();
1190                status.complete(input_completed)?;
1191                if matches!(input_completed, InputCompleted::Search) {
1192                    status.follow_search()?;
1193                }
1194            }
1195            _ => (),
1196        }
1197        Ok(())
1198    }
1199
1200    /// When files are focused, try to delete the flagged files (or selected if no file is flagged)
1201    /// Inform through the output socket if any was provided in console line arguments.
1202    /// When edit window is focused, delete all chars to the right in mode allowing edition.
1203    pub fn delete(status: &mut Status) -> Result<()> {
1204        if status.focus.is_file() {
1205            Self::delete_file(status)
1206        } else {
1207            match status.current_tab_mut().menu_mode {
1208                Menu::InputSimple(_) => {
1209                    status.menu.input.delete_chars_right();
1210                    Ok(())
1211                }
1212                Menu::InputCompleted(input_completed) => {
1213                    status.menu.input.delete_chars_right();
1214                    status.complete(input_completed)?;
1215                    if matches!(input_completed, InputCompleted::Search) {
1216                        status.follow_search()?;
1217                    }
1218                    Ok(())
1219                }
1220                _ => Ok(()),
1221            }
1222        }
1223    }
1224
1225    pub fn delete_line(status: &mut Status) -> Result<()> {
1226        if status.focus.is_file() {
1227            status.sync_tabs(Direction::RightToLeft)?;
1228        }
1229        match status.current_tab_mut().menu_mode {
1230            Menu::InputSimple(_) => {
1231                status.menu.input.delete_line();
1232            }
1233            Menu::InputCompleted(_) => {
1234                status.menu.input.delete_line();
1235                status.menu.completion_reset();
1236            }
1237            _ => (),
1238        }
1239        Ok(())
1240    }
1241
1242    /// Delete one word to the left in menus with input
1243    pub fn delete_left(status: &mut Status) -> Result<()> {
1244        if status.focus.is_file() {
1245            return Ok(());
1246        }
1247        match status.current_tab_mut().menu_mode {
1248            Menu::InputSimple(_) => {
1249                status.menu.input.delete_left();
1250            }
1251            Menu::InputCompleted(_) => {
1252                status.menu.input.delete_left();
1253                status.menu.completion_reset();
1254            }
1255            _ => (),
1256        }
1257        Ok(())
1258    }
1259
1260    /// Move to leftmost char in mode allowing edition.
1261    pub fn key_home(status: &mut Status) -> Result<()> {
1262        if status.focus.is_file() {
1263            let tab = status.current_tab_mut();
1264            match tab.display_mode {
1265                Display::Directory => tab.normal_go_top(),
1266                Display::Preview => tab.preview_go_top(),
1267                Display::Tree => tab.tree_go_to_root()?,
1268                Display::Fuzzy => status.fuzzy_start()?,
1269            };
1270            status.update_second_pane_for_preview()
1271        } else {
1272            match status.current_tab().menu_mode {
1273                Menu::InputSimple(_) | Menu::InputCompleted(_) => status.menu.input.cursor_start(),
1274                Menu::Navigate(navigate) => status.menu.set_index(0, navigate),
1275                _ => (),
1276            }
1277            Ok(())
1278        }
1279    }
1280
1281    /// Move to the bottom in any mode.
1282    pub fn end(status: &mut Status) -> Result<()> {
1283        if status.focus.is_file() {
1284            let tab = status.current_tab_mut();
1285            match tab.display_mode {
1286                Display::Directory => tab.normal_go_bottom(),
1287                Display::Preview => tab.preview_go_bottom(),
1288                Display::Tree => tab.tree_go_to_bottom_leaf(),
1289                Display::Fuzzy => status.fuzzy_end()?,
1290            };
1291            status.update_second_pane_for_preview()?;
1292        } else {
1293            match status.current_tab().menu_mode {
1294                Menu::InputSimple(_) | Menu::InputCompleted(_) => status.menu.input.cursor_end(),
1295                Menu::Navigate(navigate) => status.menu.select_last(navigate),
1296                _ => (),
1297            }
1298        }
1299        Ok(())
1300    }
1301
1302    /// Move up 10 lines in normal mode and preview.
1303    pub fn page_up(status: &mut Status) -> Result<()> {
1304        if status.focus.is_file() {
1305            Self::file_page_up(status)?;
1306        } else {
1307            let tab = status.current_tab_mut();
1308            match tab.menu_mode {
1309                Menu::Nothing => Self::file_page_up(status)?,
1310                Menu::Navigate(navigate) => status.menu.page_up(navigate),
1311                Menu::InputCompleted(input_completed) => {
1312                    for _ in 0..10 {
1313                        status.menu.completion_prev(input_completed)
1314                    }
1315                    if matches!(input_completed, InputCompleted::Search) {
1316                        status.follow_search()?;
1317                    }
1318                }
1319                Menu::NeedConfirmation(need_confirmation)
1320                    if need_confirmation.use_flagged_files() =>
1321                {
1322                    for _ in 0..10 {
1323                        status.menu.prev(Navigate::Flagged)
1324                    }
1325                }
1326                Menu::NeedConfirmation(NeedConfirmation::EmptyTrash) => {
1327                    for _ in 0..10 {
1328                        status.menu.prev(Navigate::Trash)
1329                    }
1330                }
1331                Menu::NeedConfirmation(NeedConfirmation::BulkAction) => {
1332                    for _ in 0..10 {
1333                        status.menu.bulk.prev()
1334                    }
1335                    status.menu.window.scroll_to(status.menu.bulk.index())
1336                }
1337                _ => (),
1338            };
1339        }
1340        Ok(())
1341    }
1342
1343    fn file_page_up(status: &mut Status) -> Result<()> {
1344        let tab = status.current_tab_mut();
1345        match tab.display_mode {
1346            Display::Directory => {
1347                tab.normal_page_up();
1348                status.update_second_pane_for_preview()?;
1349            }
1350            Display::Preview => tab.preview_page_up(),
1351            Display::Tree => {
1352                tab.tree_page_up();
1353                status.update_second_pane_for_preview()?;
1354            }
1355            Display::Fuzzy => status.fuzzy_navigate(FuzzyDirection::PageUp)?,
1356        };
1357        Ok(())
1358    }
1359
1360    /// Move down 10 lines in normal & preview mode.
1361    pub fn page_down(status: &mut Status) -> Result<()> {
1362        if status.focus.is_file() {
1363            Self::file_page_down(status)?;
1364        } else {
1365            let tab = status.current_tab_mut();
1366            match tab.menu_mode {
1367                Menu::Nothing => Self::file_page_down(status)?,
1368                Menu::Navigate(navigate) => status.menu.page_down(navigate),
1369                Menu::InputCompleted(input_completed) => {
1370                    for _ in 0..10 {
1371                        status.menu.completion_next(input_completed)
1372                    }
1373                    if matches!(input_completed, InputCompleted::Search) {
1374                        status.follow_search()?;
1375                    }
1376                }
1377                Menu::NeedConfirmation(need_confirmation)
1378                    if need_confirmation.use_flagged_files() =>
1379                {
1380                    for _ in 0..10 {
1381                        status.menu.next(Navigate::Flagged)
1382                    }
1383                }
1384                Menu::NeedConfirmation(NeedConfirmation::EmptyTrash) => {
1385                    for _ in 0..10 {
1386                        status.menu.next(Navigate::Trash)
1387                    }
1388                }
1389                Menu::NeedConfirmation(NeedConfirmation::BulkAction) => {
1390                    for _ in 0..10 {
1391                        status.menu.bulk.next()
1392                    }
1393                    status.menu.window.scroll_to(status.menu.bulk.index())
1394                }
1395                _ => (),
1396            };
1397        }
1398        Ok(())
1399    }
1400
1401    fn file_page_down(status: &mut Status) -> Result<()> {
1402        let tab = status.current_tab_mut();
1403        match tab.display_mode {
1404            Display::Directory => {
1405                tab.normal_page_down();
1406                status.update_second_pane_for_preview()?;
1407            }
1408            Display::Preview => tab.preview_page_down(),
1409            Display::Tree => {
1410                tab.tree_page_down();
1411                status.update_second_pane_for_preview()?;
1412            }
1413            Display::Fuzzy => status.fuzzy_navigate(FuzzyDirection::PageDown)?,
1414        };
1415        Ok(())
1416    }
1417
1418    /// Execute the mode.
1419    /// In modes requiring confirmation or text input, it will execute the
1420    /// related action.
1421    /// In normal mode, it will open the file.
1422    /// Reset to normal mode afterwards.
1423    pub fn enter(status: &mut Status, binds: &Bindings) -> Result<()> {
1424        if status.focus.is_file() {
1425            Self::enter_file(status)
1426        } else {
1427            LeaveMenu::leave_menu(status, binds)
1428        }
1429    }
1430
1431    /// Change tab in normal mode with dual pane displayed,
1432    /// insert a completion in modes allowing completion.
1433    pub fn tab(status: &mut Status) -> Result<()> {
1434        if status.focus.is_file() {
1435            status.next()
1436        } else if let Menu::InputCompleted(input_completed) = status.current_tab_mut().menu_mode {
1437            status.complete_tab(input_completed)?;
1438        }
1439        Ok(())
1440    }
1441
1442    /// Start a fuzzy find with skim.
1443    pub fn fuzzyfind(status: &mut Status) -> Result<()> {
1444        status.force_clear();
1445        status.fuzzy_init(FuzzyKind::File);
1446        status.current_tab_mut().set_display_mode(Display::Fuzzy);
1447        status.fuzzy_find_files()?;
1448        status.update_second_pane_for_preview()
1449    }
1450
1451    /// Start a fuzzy find for a specific line with skim.
1452    pub fn fuzzyfind_line(status: &mut Status) -> Result<()> {
1453        status.force_clear();
1454        status.fuzzy_init(FuzzyKind::Line);
1455        status.current_tab_mut().set_display_mode(Display::Fuzzy);
1456        status.fuzzy_find_lines()
1457    }
1458
1459    /// Start a fuzzy find for a keybinding with skim.
1460    pub fn fuzzyfind_help(status: &mut Status, binds: &Bindings) -> Result<()> {
1461        status.fuzzy_init(FuzzyKind::Action);
1462        status.current_tab_mut().set_display_mode(Display::Fuzzy);
1463        let help = help_string(binds, &status.internal_settings.opener);
1464        status.fuzzy_help(help)?;
1465        Ok(())
1466    }
1467
1468    /// Copy the content of the selected text file in normal mode.
1469    pub fn copy_content(status: &Status) -> Result<()> {
1470        if !status.focus.is_file() {
1471            return Ok(());
1472        }
1473        match status.current_tab().display_mode {
1474            Display::Tree | Display::Directory => {
1475                let Some(path) = status.current_tab().selected_path() else {
1476                    return Ok(());
1477                };
1478                content_to_clipboard(&path);
1479            }
1480            _ => return Ok(()),
1481        }
1482        Ok(())
1483    }
1484    /// Copy the filename of the selected file in normal mode.
1485    pub fn copy_filename(status: &Status) -> Result<()> {
1486        if !status.focus.is_file() {
1487            return Ok(());
1488        }
1489        match status.current_tab().display_mode {
1490            Display::Tree | Display::Directory => {
1491                let Some(path) = status.current_tab().selected_path() else {
1492                    return Ok(());
1493                };
1494                filename_to_clipboard(&path);
1495            }
1496            _ => return Ok(()),
1497        }
1498        Ok(())
1499    }
1500
1501    /// Copy the filepath of the selected file in normal mode.
1502    pub fn copy_filepath(status: &Status) -> Result<()> {
1503        if !status.focus.is_file() {
1504            return Ok(());
1505        }
1506        match status.current_tab().display_mode {
1507            Display::Tree | Display::Directory => {
1508                let Some(path) = status.current_tab().selected_path() else {
1509                    return Ok(());
1510                };
1511                filepath_to_clipboard(&path);
1512            }
1513            _ => return Ok(()),
1514        }
1515        Ok(())
1516    }
1517
1518    /// Move flagged files to the trash directory.
1519    /// If no file is flagged, flag the selected file and move it to trash.
1520    /// Inform the listener through the output_socket if any was provided in console line arguments.
1521    /// More information in the trash mod itself.
1522    /// We can't trash file which aren't mounted in the same partition.
1523    /// If the file is mounted on the $topdir of the trash (aka the $HOME mount point),
1524    /// it is moved there.
1525    /// Else, nothing is done.
1526    pub fn trash_move_file(status: &mut Status) -> Result<()> {
1527        if !status.focus.is_file() {
1528            return Ok(());
1529        }
1530        if status.menu.flagged.is_empty() {
1531            Self::toggle_flag(status)?;
1532        }
1533
1534        status.menu.trash.update()?;
1535        let output_socket = Args::parse().output_socket;
1536        for flagged in status.menu.flagged.content.iter() {
1537            if status.menu.trash.trash(flagged).is_ok() {
1538                if let Some(output_socket) = &output_socket {
1539                    nvim_inform_ipc(output_socket, NvimIPCAction::DELETE(flagged))?;
1540                }
1541            }
1542        }
1543        status.menu.flagged.clear();
1544        status.current_tab_mut().refresh_view()?;
1545        Ok(())
1546    }
1547
1548    /// Ask the user if he wants to empty the trash.
1549    /// It requires a confimation before doing anything
1550    pub fn trash_empty(status: &mut Status) -> Result<()> {
1551        if matches!(
1552            status.current_tab().menu_mode,
1553            Menu::NeedConfirmation(NeedConfirmation::EmptyTrash)
1554        ) {
1555            status.reset_menu_mode()?;
1556        } else {
1557            status.menu.trash.update()?;
1558            status.set_menu_mode(
1559                status.index,
1560                Menu::NeedConfirmation(NeedConfirmation::EmptyTrash),
1561            )?;
1562        }
1563        Ok(())
1564    }
1565
1566    /// Open the trash.
1567    /// Displays a navigable content of the trash.
1568    /// Each item can be restored or deleted.
1569    /// Each opening refresh the trash content.
1570    pub fn trash_open(status: &mut Status) -> Result<()> {
1571        if matches!(
1572            status.current_tab().menu_mode,
1573            Menu::Navigate(Navigate::Trash)
1574        ) {
1575            status.reset_menu_mode()?;
1576        } else {
1577            status.menu.trash.update()?;
1578            status.set_menu_mode(status.index, Menu::Navigate(Navigate::Trash))?;
1579        }
1580        Ok(())
1581    }
1582
1583    pub fn trash_restore(status: &mut Status) -> Result<()> {
1584        LeaveMenu::trash(status)
1585    }
1586
1587    /// Open the config file.
1588    pub fn open_config(status: &mut Status) -> Result<()> {
1589        if !status.focus.is_file() {
1590            return Ok(());
1591        }
1592        match status.open_single_file(&path::PathBuf::from(tilde(CONFIG_PATH).to_string())) {
1593            Ok(_) => log_line!("Opened the config file {CONFIG_PATH}"),
1594            Err(e) => log_info!("Error opening {:?}: the config file {}", CONFIG_PATH, e),
1595        }
1596        Ok(())
1597    }
1598
1599    /// Enter compression mode
1600    pub fn compress(status: &mut Status) -> Result<()> {
1601        if matches!(
1602            status.current_tab().menu_mode,
1603            Menu::Navigate(Navigate::Compress)
1604        ) {
1605            status.reset_menu_mode()?;
1606        } else {
1607            if status.menu.compression.is_empty() {
1608                status.menu.compression.setup();
1609            }
1610            status.set_menu_mode(status.index, Menu::Navigate(Navigate::Compress))?;
1611        }
1612        Ok(())
1613    }
1614
1615    /// Enter the context menu mode where the user can choose a basic file action.
1616    pub fn context(status: &mut Status) -> Result<()> {
1617        if matches!(
1618            status.current_tab().display_mode,
1619            Display::Fuzzy | Display::Preview
1620        ) {
1621            return Ok(());
1622        }
1623        if matches!(
1624            status.current_tab().menu_mode,
1625            Menu::Navigate(Navigate::Context)
1626        ) {
1627            status.reset_menu_mode()?;
1628        } else {
1629            if status.menu.context.is_empty() {
1630                status.menu.context.setup();
1631            } else {
1632                status.menu.context.reset();
1633            }
1634            status.set_menu_mode(status.index, Menu::Navigate(Navigate::Context))?;
1635        }
1636        Ok(())
1637    }
1638
1639    /// Enter action mode in which you can type any valid action.
1640    /// Some action does nothing as they require to be executed from a specific context.
1641    pub fn action(status: &mut Status) -> Result<()> {
1642        if matches!(
1643            status.current_tab().menu_mode,
1644            Menu::InputCompleted(InputCompleted::Action)
1645        ) {
1646            status.reset_menu_mode()?;
1647        } else {
1648            status.set_menu_mode(status.index, Menu::InputCompleted(InputCompleted::Action))?;
1649            status.menu.completion.reset();
1650        }
1651        Ok(())
1652    }
1653
1654    /// Enter the remote mount mode where the user can provide an username, an adress and
1655    /// a mount point to mount a remote device through SSHFS.
1656    pub fn remote_mount(status: &mut Status) -> Result<()> {
1657        if matches!(
1658            status.current_tab().menu_mode,
1659            Menu::InputSimple(InputSimple::Remote)
1660        ) {
1661            status.reset_menu_mode()?;
1662        }
1663        status.set_menu_mode(status.index, Menu::InputSimple(InputSimple::Remote))
1664    }
1665
1666    pub fn cloud_drive(status: &mut Status) -> Result<()> {
1667        status.cloud_open()
1668    }
1669
1670    pub fn cloud_enter_newdir_mode(status: &mut Status) -> Result<()> {
1671        status.cloud_enter_newdir_mode()
1672    }
1673
1674    pub fn cloud_enter_delete_mode(status: &mut Status) -> Result<()> {
1675        status.cloud_enter_delete_mode()
1676    }
1677    /// Select the left or right tab depending on `col`
1678    pub fn select_pane(status: &mut Status, col: u16) -> Result<()> {
1679        status.select_tab_from_col(col)
1680    }
1681
1682    pub fn focus_go_left(status: &mut Status) -> Result<()> {
1683        match status.focus {
1684            Focus::LeftMenu | Focus::LeftFile => (),
1685            Focus::RightFile => {
1686                status.index = 0;
1687                status.focus = Focus::LeftFile;
1688            }
1689            Focus::RightMenu => {
1690                status.index = 0;
1691                if matches!(status.tabs[0].menu_mode, Menu::Nothing) {
1692                    status.focus = Focus::LeftFile;
1693                } else {
1694                    status.focus = Focus::LeftMenu;
1695                }
1696            }
1697        }
1698        Ok(())
1699    }
1700
1701    pub fn focus_go_right(status: &mut Status) -> Result<()> {
1702        match status.focus {
1703            Focus::RightMenu | Focus::RightFile => (),
1704            Focus::LeftFile => {
1705                status.index = 1;
1706                status.focus = Focus::RightFile;
1707            }
1708            Focus::LeftMenu => {
1709                status.index = 1;
1710                if matches!(status.tabs[1].menu_mode, Menu::Nothing) {
1711                    status.focus = Focus::RightFile;
1712                } else {
1713                    status.focus = Focus::RightMenu;
1714                }
1715            }
1716        }
1717        Ok(())
1718    }
1719
1720    pub fn focus_go_down(status: &mut Status) -> Result<()> {
1721        match status.focus {
1722            Focus::RightMenu | Focus::LeftMenu => (),
1723            Focus::LeftFile => {
1724                if !matches!(status.tabs[0].menu_mode, Menu::Nothing) {
1725                    status.focus = Focus::LeftMenu;
1726                }
1727            }
1728            Focus::RightFile => {
1729                if !matches!(status.tabs[1].menu_mode, Menu::Nothing) {
1730                    status.focus = Focus::RightMenu;
1731                }
1732            }
1733        }
1734        Ok(())
1735    }
1736
1737    pub fn focus_go_up(status: &mut Status) -> Result<()> {
1738        match status.focus {
1739            Focus::LeftFile | Focus::RightFile => (),
1740            Focus::LeftMenu => status.focus = Focus::LeftFile,
1741            Focus::RightMenu => status.focus = Focus::RightFile,
1742        }
1743        Ok(())
1744    }
1745
1746    pub fn sync_ltr(status: &mut Status) -> Result<()> {
1747        if status.focus.is_file() {
1748            status.sync_tabs(Direction::LeftToRight)?;
1749        }
1750        Ok(())
1751    }
1752
1753    pub fn bulk_confirm(status: &mut Status) -> Result<()> {
1754        status.bulk_execute()
1755    }
1756
1757    pub fn file_copied(status: &mut Status) -> Result<()> {
1758        log_info!(
1759            "file copied - pool: {pool:?}",
1760            pool = status.internal_settings.copy_file_queue
1761        );
1762        status.internal_settings.copy_file_remove_head()?;
1763        if status.internal_settings.copy_file_queue.is_empty() {
1764            status.internal_settings.unset_copy_progress()
1765        } else {
1766            status.copy_next_file_in_queue()?;
1767        }
1768        Ok(())
1769    }
1770
1771    pub fn display_copy_progress(status: &mut Status, content: InMemoryTerm) -> Result<()> {
1772        status.internal_settings.store_copy_progress(content);
1773        Ok(())
1774    }
1775
1776    pub fn check_preview_fuzzy_tick(status: &mut Status) -> Result<()> {
1777        status.fuzzy_tick();
1778        status.check_preview()
1779    }
1780
1781    pub fn visual(status: &mut Status) -> Result<()> {
1782        status.current_tab_mut().toggle_visual();
1783        status.toggle_flag_visual();
1784
1785        Ok(())
1786    }
1787
1788    /// Open the mount menu
1789    pub fn mount(status: &mut Status) -> Result<()> {
1790        if matches!(
1791            status.current_tab().menu_mode,
1792            Menu::Navigate(Navigate::Mount)
1793        ) {
1794            status.reset_menu_mode()?;
1795        } else if lsblk_and_udisksctl_installed() {
1796            status.menu.mount.update(status.internal_settings.disks())?;
1797            status.set_menu_mode(status.index, Menu::Navigate(Navigate::Mount))?;
1798        }
1799        Ok(())
1800    }
1801
1802    /// Execute a custom event on the selected file
1803    pub fn custom(status: &mut Status, input_string: &str) -> Result<()> {
1804        status.run_custom_command(input_string)
1805    }
1806
1807    /// Parse and execute the received IPC message.
1808    pub fn parse_rpc(status: &mut Status, ipc_msg: String) -> Result<()> {
1809        status.parse_ipc(ipc_msg)
1810    }
1811}