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