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