Skip to main content

fm/event/
event_action.rs

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