fm/app/
status.rs

1use std::path::{Path, PathBuf};
2use std::str::FromStr;
3use std::sync::{
4    mpsc::{self, Sender, TryRecvError},
5    Arc,
6};
7
8use anyhow::{bail, Context, Result};
9use clap::Parser;
10use crossterm::event::{Event, KeyEvent};
11use opendal::EntryMode;
12use parking_lot::lock_api::Mutex;
13use parking_lot::RawMutex;
14use ratatui::layout::Size;
15use sysinfo::Disks;
16use walkdir::WalkDir;
17
18use crate::app::{
19    ClickableLine, Footer, Header, InternalSettings, PreviewResponse, Previewer, Session, Tab,
20    ThumbnailManager,
21};
22use crate::common::{
23    build_dest_path, current_username, disk_space, filename_from_path, is_in_path, is_sudo_command,
24    path_to_string, row_to_window_index, set_current_dir, tilde, MountPoint,
25};
26use crate::config::{from_keyname, Bindings, START_FOLDER};
27use crate::event::{ActionMap, FmEvents};
28use crate::io::{
29    build_tokio_greper, execute_and_capture_output, execute_sudo_command_with_password,
30    execute_without_output, get_cloud_token_names, google_drive, reset_sudo_faillock, Args,
31    Internal, Kind, Opener, MIN_WIDTH_FOR_DUAL_PANE,
32};
33use crate::modes::{
34    append_files_to_shell_command, copy_move, parse_line_output, regex_flagger,
35    shell_command_parser, Content, ContentWindow, CopyMove, CursorOffset,
36    Direction as FuzzyDirection, Display, FileInfo, FileKind, FilterKind, FuzzyFinder, FuzzyKind,
37    InputCompleted, InputSimple, IsoDevice, Menu, MenuHolder, MountAction, MountCommands,
38    Mountable, Navigate, NeedConfirmation, PasswordKind, PasswordUsage, Permissions, PickerCaller,
39    Preview, PreviewBuilder, ReEnterMenu, Search, Selectable, Users, SAME_WINDOW_TOKEN,
40};
41use crate::{log_info, log_line};
42
43/// Kind of "windows" : Header, Files, Menu, Footer.
44pub enum Window {
45    Header,
46    Files,
47    Menu,
48    Footer,
49}
50
51/// Responsible for switching the focus from one window to another.
52#[derive(Default, Clone, Copy, Debug)]
53pub enum Focus {
54    #[default]
55    LeftFile,
56    LeftMenu,
57    RightFile,
58    RightMenu,
59}
60
61impl Focus {
62    /// True if the focus is on left tab.
63    /// Always true if only one tab is shown
64    pub fn is_left(&self) -> bool {
65        matches!(self, Self::LeftMenu | Self::LeftFile)
66    }
67
68    /// True if the focus is on a top window.
69    /// Always true if no menu is shown.
70    pub fn is_file(&self) -> bool {
71        matches!(self, Self::LeftFile | Self::RightFile)
72    }
73
74    /// Switch from left to right and to file.
75    /// `LeftMenu` -> `RightFile`
76    /// `LeftFile` -> `RightFile`
77    /// And vice versa.
78    pub fn switch(&self) -> Self {
79        match self {
80            Self::LeftFile => Self::RightFile,
81            Self::LeftMenu => Self::RightFile,
82            Self::RightFile => Self::LeftFile,
83            Self::RightMenu => Self::LeftFile,
84        }
85    }
86
87    /// Returns the "parent" of current focus.
88    /// File parent it itself (weird, I know), Menu parent is its associated file.
89    /// Couldn't figure a better name.
90    pub fn to_parent(&self) -> Self {
91        match self {
92            Self::LeftFile | Self::LeftMenu => Self::LeftFile,
93            Self::RightFile | Self::RightMenu => Self::RightFile,
94        }
95    }
96
97    /// In that order : `LeftFile, LeftMenu, RightFile, RightMenu`
98    pub fn index(&self) -> usize {
99        *self as usize
100    }
101
102    /// Index of the tab Left is 0 and Right is one.
103    pub fn tab_index(&self) -> usize {
104        self.index() >> 1
105    }
106
107    /// Is the window a left menu ?
108    pub fn is_left_menu(&self) -> bool {
109        matches!(self, Self::LeftMenu)
110    }
111}
112
113/// What direction is used to sync tabs ? Left to right or right to left ?
114pub enum Direction {
115    RightToLeft,
116    LeftToRight,
117}
118
119impl Direction {
120    /// Returns the indexes of source and destination when tabs are synced
121    const fn source_dest(self) -> (usize, usize) {
122        match self {
123            Self::RightToLeft => (1, 0),
124            Self::LeftToRight => (0, 1),
125        }
126    }
127}
128
129/// Holds every mutable parameter of the application itself, except for
130/// the "display" information.
131/// It holds 2 tabs (left & right), even if only one can be displayed sometimes.
132/// It knows which tab is selected, which files are flagged,
133/// which jump target is selected, a cache of normal file colors,
134/// if we have to display one or two tabs and if all details are shown or only
135/// the filename.
136/// Mutation of this struct are mostly done externally, by the event crate :
137/// `crate::event_exec`.
138pub struct Status {
139    /// Vector of `Tab`, each of them are displayed in a separate tab.
140    pub tabs: [Tab; 2],
141    /// Index of the current selected tab
142    pub index: usize,
143    /// Fuzzy finder of files by name
144    pub fuzzy: Option<FuzzyFinder<String>>,
145    /// Navigable menu
146    pub menu: MenuHolder,
147    /// Display settings
148    pub session: Session,
149    /// Internal settings
150    pub internal_settings: InternalSettings,
151    /// Window being focused currently
152    pub focus: Focus,
153    /// Sender of events
154    pub fm_sender: Arc<Sender<FmEvents>>,
155    /// Receiver of previews, used to build & display previews without bloking
156    preview_receiver: mpsc::Receiver<PreviewResponse>,
157    /// Non bloking preview builder
158    pub previewer: Previewer,
159    /// Preview manager
160    pub thumbnail_manager: Option<ThumbnailManager>,
161}
162
163impl Status {
164    /// Creates a new status for the application.
165    /// It requires most of the information (arguments, configuration, height
166    /// of the terminal, the formated help string).
167    /// Status is wraped around by an `Arc`, `Mutex` from parking_lot.
168    pub fn arc_mutex_new(
169        size: Size,
170        opener: Opener,
171        binds: &Bindings,
172        fm_sender: Arc<Sender<FmEvents>>,
173    ) -> Result<Arc<Mutex<RawMutex, Self>>> {
174        Ok(Arc::new(Mutex::new(Self::new(
175            size, opener, binds, fm_sender,
176        )?)))
177    }
178
179    fn new(
180        size: Size,
181        opener: Opener,
182        binds: &Bindings,
183        fm_sender: Arc<Sender<FmEvents>>,
184    ) -> Result<Self> {
185        let fuzzy = None;
186        let index = 0;
187
188        let args = Args::parse();
189        let path = &START_FOLDER.get().context("Start folder should be set")?;
190        let start_dir = if path.is_dir() {
191            path
192        } else {
193            path.parent().context("")?
194        };
195        let disks = Disks::new_with_refreshed_list();
196        let session = Session::new(size.width);
197        let internal_settings = InternalSettings::new(opener, size, disks);
198        let menu = MenuHolder::new(start_dir, binds)?;
199        let focus = Focus::default();
200
201        let users_left = Users::default();
202        let users_right = users_left.clone();
203
204        let height = size.height as usize;
205        let tabs = [
206            Tab::new(&args, height, users_left)?,
207            Tab::new(&args, height, users_right)?,
208        ];
209        let (previewer_sender, preview_receiver) = mpsc::channel();
210        let previewer = Previewer::new(previewer_sender);
211        let thumbnail_manager = None;
212        Ok(Self {
213            tabs,
214            index,
215            fuzzy,
216            menu,
217            session,
218            internal_settings,
219            focus,
220            fm_sender,
221            preview_receiver,
222            previewer,
223            thumbnail_manager,
224        })
225    }
226
227    /// Returns a non mutable reference to the selected tab.
228    pub fn current_tab(&self) -> &Tab {
229        &self.tabs[self.index]
230    }
231
232    /// Returns a mutable reference to the selected tab.
233    pub fn current_tab_mut(&mut self) -> &mut Tab {
234        &mut self.tabs[self.index]
235    }
236
237    /// Returns a string representing the current path in the selected tab.
238    pub fn current_tab_path_str(&self) -> String {
239        self.current_tab().directory_str()
240    }
241
242    /// True if a quit event was registered in the selected tab.
243    pub fn must_quit(&self) -> bool {
244        self.internal_settings.must_quit
245    }
246
247    /// Give the focus to the selected tab.
248    pub fn focus_follow_index(&mut self) {
249        if (self.index == 0 && !self.focus.is_left()) || (self.index == 1 && self.focus.is_left()) {
250            self.focus = self.focus.switch();
251        }
252    }
253
254    /// Give to focus to the left / right file / menu depending of which mode is selected.
255    pub fn set_focus_from_mode(&mut self) {
256        if self.index == 0 {
257            if self.tabs[0].menu_mode.is_nothing() {
258                self.focus = Focus::LeftFile;
259            } else {
260                self.focus = Focus::LeftMenu;
261            }
262        } else if self.tabs[1].menu_mode.is_nothing() {
263            self.focus = Focus::RightFile;
264        } else {
265            self.focus = Focus::RightMenu;
266        }
267    }
268
269    /// Select the other tab if two are displayed. Does nothing otherwise.
270    pub fn next(&mut self) {
271        if !self.session.dual() {
272            return;
273        }
274        self.index = 1 - self.index;
275        self.focus_follow_index();
276    }
277
278    /// Select the left or right tab depending on where the user clicked.
279    pub fn select_tab_from_col(&mut self, col: u16) -> Result<()> {
280        if self.session.dual() {
281            if col < self.term_width() / 2 {
282                self.select_left();
283            } else {
284                self.select_right();
285            };
286        } else {
287            self.select_left();
288        }
289        Ok(())
290    }
291
292    fn window_from_row(&self, row: u16, height: u16) -> Window {
293        let win_height = if self.current_tab().menu_mode.is_nothing() {
294            height
295        } else {
296            height / 2
297        };
298        let w_index = row / win_height;
299        if w_index == 1 {
300            Window::Menu
301        } else if row == 1 {
302            Window::Header
303        } else if row == win_height - 2 {
304            Window::Footer
305        } else {
306            Window::Files
307        }
308    }
309
310    /// Set focus from a mouse coordinates.
311    /// When a mouse event occurs, focus is given to the window where it happened.
312    pub fn set_focus_from_pos(&mut self, row: u16, col: u16) -> Result<Window> {
313        self.select_tab_from_col(col)?;
314        let window = self.window_from_row(row, self.term_size().height);
315        self.set_focus_from_window_and_index(&window);
316        Ok(window)
317    }
318
319    /// Execute a click at `row`, `col`. Action depends on which window was clicked.
320    pub fn click(&mut self, binds: &Bindings, row: u16, col: u16) -> Result<()> {
321        let window = self.set_focus_from_pos(row, col)?;
322        self.click_action_from_window(&window, row, col, binds)?;
323        Ok(())
324    }
325
326    /// True iff user has clicked on a preview in second pane.
327    fn has_clicked_on_second_pane_preview(&self) -> bool {
328        self.session.dual() && self.session.preview() && self.index == 1
329    }
330
331    fn click_action_from_window(
332        &mut self,
333        window: &Window,
334        row: u16,
335        col: u16,
336        binds: &Bindings,
337    ) -> Result<()> {
338        match window {
339            Window::Header => self.header_action(col, binds),
340            Window::Files => {
341                if self.has_clicked_on_second_pane_preview() {
342                    if let Preview::Tree(tree) = &self.tabs[1].preview {
343                        let index = row_to_window_index(row) + self.tabs[1].window.top;
344                        let path = &tree.path_from_index(index)?;
345                        self.select_file_from_right_tree(path)?;
346                    }
347                } else {
348                    self.tab_select_row(row)?;
349                    self.update_second_pane_for_preview()?;
350                }
351                Ok(())
352            }
353            Window::Footer => self.footer_action(col, binds),
354            Window::Menu => self.menu_action(row, col),
355        }
356    }
357
358    /// Action when user press enter on a preview.
359    /// If the preview is a tree in right tab, the left file will be selected in the left tab.
360    /// Otherwise, we open the file if it exists in the file tree.
361    pub fn enter_from_preview(&mut self) -> Result<()> {
362        if let Preview::Tree(tree) = &self.tabs[1].preview {
363            let index = tree.displayable().index();
364            let path = &tree.path_from_index(index)?;
365            self.select_file_from_right_tree(path)?;
366        } else {
367            let filepath = self.tabs[self.focus.tab_index()].preview.filepath();
368            if filepath.exists() {
369                self.open_single_file(&filepath)?;
370            }
371        }
372        Ok(())
373    }
374
375    fn select_file_from_right_tree(&mut self, path: &Path) -> Result<()> {
376        self.tabs[0].cd_to_file(path)?;
377        self.index = 0;
378        self.focus = Focus::LeftFile;
379        self.update_second_pane_for_preview()
380    }
381
382    /// Select a given row, if there's something in it.
383    /// Returns an error if the clicked row is above the headers margin.
384    pub fn tab_select_row(&mut self, row: u16) -> Result<()> {
385        match self.current_tab().display_mode {
386            Display::Directory => self.current_tab_mut().normal_select_row(row),
387            Display::Tree => self.current_tab_mut().tree_select_row(row)?,
388            Display::Fuzzy => self.fuzzy_navigate(FuzzyDirection::Index(row))?,
389            _ => (),
390        }
391        Ok(())
392    }
393
394    #[rustfmt::skip]
395    fn set_focus_from_window_and_index(&mut self, window: &Window) {
396        self.focus = match (self.index == 0, window) {
397            (true,  Window::Menu)   => Focus::LeftMenu,
398            (true,  _)              => Focus::LeftFile,
399            (false, Window::Menu)   => Focus::RightMenu,
400            (false, _)              => Focus::RightFile
401        };
402    }
403
404    /// Sync right tab from left tab path or vice versa.
405    pub fn sync_tabs(&mut self, direction: Direction) -> Result<()> {
406        let (source, dest) = direction.source_dest();
407        self.tabs[dest].cd(&self.tabs[source]
408            .selected_path()
409            .context("No selected path")?)
410    }
411
412    /// Height of the second window (menu).
413    /// Watchout : it's always ~height / 2 - 2, even if menu is closed.
414    pub fn second_window_height(&self) -> Result<usize> {
415        let height = self.term_size().height;
416        Ok((height / 2).saturating_sub(2) as usize)
417    }
418
419    /// Execute a click on a menu item. Action depends on which menu was opened.
420    fn menu_action(&mut self, row: u16, col: u16) -> Result<()> {
421        let second_window_height = self.second_window_height()?;
422        let row_offset = (row as usize).saturating_sub(second_window_height);
423        const OFFSET: usize =
424            ContentWindow::WINDOW_PADDING + ContentWindow::WINDOW_MARGIN_TOP_U16 as usize;
425        if row_offset >= OFFSET {
426            let index = row_offset - OFFSET + self.menu.window.top;
427            match self.current_tab().menu_mode {
428                Menu::Navigate(navigate) => match navigate {
429                    Navigate::History => self.current_tab_mut().history.set_index(index),
430                    navigate => self.menu.set_index(index, navigate),
431                },
432                Menu::InputCompleted(input_completed) => {
433                    self.menu.completion.set_index(index);
434                    if matches!(input_completed, InputCompleted::Search) {
435                        self.follow_search()?;
436                    }
437                }
438                Menu::NeedConfirmation(need_confirmation)
439                    if need_confirmation.use_flagged_files() =>
440                {
441                    self.menu.flagged.set_index(index)
442                }
443                Menu::NeedConfirmation(NeedConfirmation::EmptyTrash) => {
444                    self.menu.trash.set_index(index)
445                }
446                Menu::NeedConfirmation(NeedConfirmation::BulkAction) => {
447                    self.menu.bulk.set_index(index)
448                }
449                _ => (),
450            }
451            self.menu.window.scroll_to(index);
452        } else if row_offset == 3 && self.current_tab().menu_mode.is_input() {
453            let index =
454                col.saturating_sub(self.current_tab().menu_mode.cursor_offset() + 1) as usize;
455            self.menu.input.cursor_move(index);
456        }
457
458        Ok(())
459    }
460
461    /// Select the left tab
462    pub fn select_left(&mut self) {
463        self.index = 0;
464        self.focus_follow_index();
465    }
466
467    /// Select the right tab
468    pub fn select_right(&mut self) {
469        self.index = 1;
470        self.focus_follow_index();
471    }
472
473    /// Refresh every disk information.
474    /// It also refreshes the disk list, which is usefull to detect removable medias.
475    /// It may be very slow...
476    /// There's surelly a better way, like doing it only once in a while or on
477    /// demand.
478    pub fn refresh_shortcuts(&mut self) {
479        self.menu.refresh_shortcuts(
480            &self.internal_settings.mount_points_vec(),
481            self.tabs[0].current_directory_path(),
482            self.tabs[1].current_directory_path(),
483        );
484    }
485
486    /// Returns the disk spaces for the selected tab..
487    pub fn disk_spaces_of_selected(&self) -> String {
488        disk_space(
489            &self.internal_settings.disks,
490            self.current_tab().current_directory_path(),
491        )
492    }
493
494    /// Returns the size of the terminal (width, height)
495    pub fn term_size(&self) -> Size {
496        self.internal_settings.term_size()
497    }
498
499    /// Returns the width of the terminal window.
500    pub fn term_width(&self) -> u16 {
501        self.term_size().width
502    }
503
504    /// Clears the right preview
505    pub fn clear_preview_right(&mut self) {
506        if self.session.dual() && self.session.preview() && !self.tabs[1].preview.is_empty() {
507            self.tabs[1].preview = PreviewBuilder::empty()
508        }
509    }
510
511    /// Refresh the current view, reloading the files. Move the selection to top.
512    pub fn refresh_view(&mut self) -> Result<()> {
513        self.refresh_status()?;
514        self.update_second_pane_for_preview()
515    }
516
517    /// Reset the view of every tab.
518    pub fn reset_tabs_view(&mut self) -> Result<()> {
519        for tab in self.tabs.iter_mut() {
520            match tab.refresh_and_reselect_file() {
521                Ok(()) => (),
522                Err(error) => log_info!("reset_tabs_view error: {error}"),
523            }
524        }
525        Ok(())
526    }
527
528    /// Leave an edit mode and refresh the menu.
529    /// It should only be called when edit mode isn't nothing.
530    pub fn leave_menu_mode(&mut self) -> Result<()> {
531        match self.current_tab().menu_mode {
532            Menu::InputSimple(InputSimple::Filter) => {
533                self.current_tab_mut().settings.reset_filter()
534            }
535            Menu::InputCompleted(InputCompleted::Cd) => self.current_tab_mut().cd_origin_path()?,
536            _ => (),
537        }
538        if self.reset_menu_mode()? {
539            self.current_tab_mut().refresh_view()?;
540        } else {
541            self.current_tab_mut().refresh_params();
542        }
543        self.current_tab_mut().reset_visual();
544        Ok(())
545    }
546
547    /// Leave the preview or flagged display mode.
548    /// Should only be called when :
549    /// 1. No menu is opened
550    ///
551    /// AND
552    ///
553    /// 2. Display mode is preview or flagged.
554    pub fn leave_preview(&mut self) -> Result<()> {
555        self.current_tab_mut().set_display_mode(Display::Directory);
556        self.current_tab_mut().refresh_and_reselect_file()
557    }
558
559    /// Reset the edit mode to "Nothing" (closing any menu) and returns
560    /// true if the display should be refreshed.
561    /// If the menu is a picker for input history, it will reenter the caller menu.
562    /// Example: Menu CD -> History of CDs `<Esc>` -> Menu CD
563    pub fn reset_menu_mode(&mut self) -> Result<bool> {
564        if self.current_tab().menu_mode.is_picker() {
565            if let Some(PickerCaller::Menu(menu)) = self.menu.picker.caller {
566                menu.reenter(self)?;
567                return Ok(false);
568            }
569        }
570        self.menu.reset();
571        let must_refresh = matches!(self.current_tab().display_mode, Display::Preview);
572        self.set_menu_mode(self.index, Menu::Nothing)?;
573        self.set_height_of_unfocused_menu()?;
574        Ok(must_refresh)
575    }
576
577    fn set_height_of_unfocused_menu(&mut self) -> Result<()> {
578        let unfocused_tab = &self.tabs[1 - self.index];
579        match unfocused_tab.menu_mode {
580            Menu::Nothing => (),
581            unfocused_mode => {
582                let len = self.menu.len(unfocused_mode);
583                let height = self.second_window_height()?;
584                self.menu.window = ContentWindow::new(len, height);
585            }
586        }
587        Ok(())
588    }
589
590    /// Reset the selected tab view to the default.
591    pub fn refresh_status(&mut self) -> Result<()> {
592        self.force_clear();
593        self.refresh_users()?;
594        self.refresh_tabs()?;
595        self.refresh_shortcuts();
596        Ok(())
597    }
598
599    /// Set a "force clear" flag to true, which will reset the display.
600    /// It's used when some command or whatever may pollute the terminal.
601    /// We ensure to clear it before displaying again.
602    pub fn force_clear(&mut self) {
603        self.internal_settings.force_clear();
604    }
605
606    pub fn should_be_cleared(&self) -> bool {
607        self.internal_settings.should_be_cleared()
608    }
609
610    /// Refresh the users for every tab
611    pub fn refresh_users(&mut self) -> Result<()> {
612        self.tabs[0].users.update();
613        self.tabs[1].users = self.tabs[0].users.clone();
614        Ok(())
615    }
616
617    /// Refresh the input, completion and every tab.
618    pub fn refresh_tabs(&mut self) -> Result<()> {
619        self.menu.input.reset();
620        self.menu.completion.reset();
621        self.tabs[0].refresh_and_reselect_file()?;
622        self.tabs[1].refresh_and_reselect_file()
623    }
624
625    /// When a rezise event occurs, we may hide the second panel if the width
626    /// isn't sufficiant to display enough information.
627    /// We also need to know the new height of the terminal to start scrolling
628    /// up or down.
629    pub fn resize(&mut self, width: u16, height: u16) -> Result<()> {
630        let couldnt_dual_but_want = self.couldnt_dual_but_want();
631        self.internal_settings.update_size(width, height);
632        if couldnt_dual_but_want {
633            self.set_dual_pane_if_wide_enough()?;
634        }
635        if !self.wide_enough_for_dual() {
636            self.select_left();
637        }
638        self.resize_all_windows(height)?;
639        self.refresh_status()
640    }
641
642    fn wide_enough_for_dual(&self) -> bool {
643        self.term_width() >= MIN_WIDTH_FOR_DUAL_PANE
644    }
645
646    fn couldnt_dual_but_want(&self) -> bool {
647        !self.wide_enough_for_dual() && self.session.dual()
648    }
649
650    fn use_dual(&self) -> bool {
651        self.wide_enough_for_dual() && self.session.dual()
652    }
653
654    fn left_window_width(&self) -> u16 {
655        if self.use_dual() {
656            self.term_width() / 2
657        } else {
658            self.term_width()
659        }
660    }
661
662    fn resize_all_windows(&mut self, height: u16) -> Result<()> {
663        let height_usize = height as usize;
664        self.tabs[0].set_height(height_usize);
665        self.tabs[1].set_height(height_usize);
666        self.fuzzy_resize(height_usize);
667        self.menu.resize(
668            self.tabs[self.index].menu_mode,
669            self.second_window_height()?,
670        );
671        Ok(())
672    }
673
674    /// Check if the second pane should display a preview and force it.
675    pub fn update_second_pane_for_preview(&mut self) -> Result<()> {
676        if self.are_settings_requiring_dualpane_preview() {
677            if self.can_display_dualpane_preview() {
678                self.set_second_pane_for_preview()?;
679            } else {
680                self.tabs[1].preview = PreviewBuilder::empty();
681            }
682        }
683        Ok(())
684    }
685
686    fn are_settings_requiring_dualpane_preview(&self) -> bool {
687        self.index == 0 && self.session.dual() && self.session.preview()
688    }
689
690    fn can_display_dualpane_preview(&self) -> bool {
691        Session::display_wide_enough(self.term_width())
692    }
693
694    /// Force preview the selected file of the first pane in the second pane.
695    /// Doesn't check if it has do.
696    fn set_second_pane_for_preview(&mut self) -> Result<()> {
697        self.tabs[1].set_display_mode(Display::Preview);
698        self.tabs[1].menu_mode = Menu::Nothing;
699        let Ok((Some(fileinfo), line_index)) = self.get_correct_fileinfo_for_preview() else {
700            return Ok(());
701        };
702        log_info!("sending preview request");
703
704        self.previewer
705            .build(fileinfo.path.to_path_buf(), 1, line_index)?;
706
707        Ok(())
708    }
709
710    /// Build all the video thumbnails of a directory
711    /// Build the the thumbnail manager if it hasn't been initialised yet. If there's still files in the queue, they're cleared first.
712    pub fn thumbnail_directory_video(&mut self) {
713        if !self.are_settings_requiring_dualpane_preview() || !self.can_display_dualpane_preview() {
714            return;
715        }
716        self.thumbnail_init_or_clear();
717        let videos = self.current_tab().directory.videos();
718        if videos.is_empty() {
719            return;
720        }
721        if let Some(thumbnail_manager) = &self.thumbnail_manager {
722            thumbnail_manager.enqueue(videos);
723        }
724    }
725
726    /// Clear the thumbnail queue or init the manager.
727    fn thumbnail_init_or_clear(&mut self) {
728        if self.thumbnail_manager.is_none() {
729            self.thumbnail_manager_init();
730        } else {
731            self.thumbnail_queue_clear();
732        }
733    }
734
735    fn thumbnail_manager_init(&mut self) {
736        self.thumbnail_manager = Some(ThumbnailManager::default());
737    }
738
739    /// Clear the thumbnail queue
740    pub fn thumbnail_queue_clear(&self) {
741        if let Some(thumbnail_manager) = &self.thumbnail_manager {
742            thumbnail_manager.clear()
743        }
744    }
745
746    /// Check if the previewer has sent a preview.
747    ///
748    /// If the previewer has sent a preview, it's attached to the correct tab.
749    /// Returns an error if the previewer disconnected.
750    /// Does nothing otherwise.
751    pub fn check_preview(&mut self) -> Result<()> {
752        match self.preview_receiver.try_recv() {
753            Ok(preview_response) => self.attach_preview(preview_response)?,
754            Err(TryRecvError::Disconnected) => bail!("Previewer Disconnected"),
755            Err(TryRecvError::Empty) => (),
756        }
757        Ok(())
758    }
759
760    /// Attach a preview to the correct tab.
761    /// Nothing is done if the preview doesn't match the file.
762    /// It may happen if the user navigates quickly with "heavy" previews (movies, large pdf, office documents etc.).
763    fn attach_preview(&mut self, preview_response: PreviewResponse) -> Result<()> {
764        let PreviewResponse {
765            path,
766            tab_index,
767            line_nr,
768            preview,
769        } = preview_response;
770        let compared_index = self.pick_correct_tab_from(tab_index)?;
771        if !self.preview_has_correct_path(compared_index, path.as_path())? {
772            return Ok(());
773        }
774        let tab = &mut self.tabs[tab_index];
775        tab.preview = preview;
776        tab.window.reset(tab.preview.len());
777        if let Some(line_nr) = line_nr {
778            tab.window.scroll_to(line_nr);
779        }
780        Ok(())
781    }
782
783    fn pick_correct_tab_from(&self, index: usize) -> Result<usize> {
784        if index == 1 && self.can_display_dualpane_preview() && self.session.preview() {
785            Ok(0)
786        } else {
787            Ok(index)
788        }
789    }
790
791    /// Ok(true) if the preview should be used.
792    /// We only check if one of 3 conditions are true :
793    /// - are we in fuzzy mode in the compared_index tab ?
794    /// - are we in navigate menu in the compared_index ?
795    /// - is the path the current path ?
796    fn preview_has_correct_path(&self, compared_index: usize, path: &Path) -> Result<bool> {
797        let tab = &self.tabs[compared_index];
798        Ok(tab.display_mode.is_fuzzy()
799            || tab.menu_mode.is_navigate()
800            || tab.selected_path().context("No selected path")?.as_ref() == path)
801    }
802
803    /// Look for the correct file_info to preview.
804    /// It depends on what the left tab is doing :
805    /// fuzzy mode ? its selection,
806    /// navigation (shortcut, marks or history) ? the selection,
807    /// otherwise, it's the current selection.
808    fn get_correct_fileinfo_for_preview(&self) -> Result<(Option<FileInfo>, Option<usize>)> {
809        let left_tab = &self.tabs[0];
810        let users = &left_tab.users;
811        if left_tab.display_mode.is_fuzzy() {
812            let (opt_path, line_nr) =
813                parse_line_output(&self.fuzzy_current_selection().context("No selection")?)?;
814            Ok((FileInfo::new(&opt_path, users).ok(), line_nr))
815        } else if self.focus.is_left_menu() {
816            Ok((self.fileinfo_from_navigate(left_tab, users), None))
817        } else {
818            Ok((left_tab.current_file().ok(), None))
819        }
820    }
821
822    /// FileInfo to be previewed depending of what is done in this tab.
823    /// If this tab is navigating in history, shortcut or marks, we return the selection.
824    /// Otherwise, we return the current file.
825    fn fileinfo_from_navigate(&self, tab: &Tab, users: &Users) -> Option<FileInfo> {
826        match tab.menu_mode {
827            Menu::Navigate(Navigate::History) => {
828                FileInfo::new(tab.history.content().get(tab.history.index())?, users).ok()
829            }
830            Menu::Navigate(Navigate::Shortcut) => {
831                let short = &self.menu.shortcut;
832                FileInfo::new(short.content().get(short.index())?, users).ok()
833            }
834            Menu::Navigate(Navigate::Marks(_)) => {
835                let (_, mark_path) = &self.menu.marks.content().get(self.menu.marks.index())?;
836                FileInfo::new(mark_path, users).ok()
837            }
838            Menu::Navigate(Navigate::Flagged) => {
839                FileInfo::new(self.menu.flagged.selected()?, users).ok()
840            }
841            _ => tab.current_file().ok(),
842        }
843    }
844
845    /// Does any tab set its flag for image to be cleared on next display ?
846    pub fn should_tabs_images_be_cleared(&self) -> bool {
847        self.tabs[0].settings.should_clear_image || self.tabs[1].settings.should_clear_image
848    }
849
850    /// Reset the tab flags to false. Called when their image have be cleared.
851    pub fn set_tabs_images_cleared(&mut self) {
852        self.tabs[0].settings.should_clear_image = false;
853        self.tabs[1].settings.should_clear_image = false;
854    }
855
856    /// Set an edit mode for the tab at `index`. Refresh the view.
857    pub fn set_menu_mode(&mut self, index: usize, menu_mode: Menu) -> Result<()> {
858        self.set_menu_mode_no_refresh(index, menu_mode)?;
859        self.current_tab_mut().reset_visual();
860        self.refresh_status()
861    }
862
863    /// Set the menu and without querying a refresh.
864    pub fn set_menu_mode_no_refresh(&mut self, index: usize, menu_mode: Menu) -> Result<()> {
865        if index > 1 {
866            return Ok(());
867        }
868        self.set_height_for_menu_mode(index, menu_mode)?;
869        self.tabs[index].menu_mode = menu_mode;
870        let len = self.menu.len(menu_mode);
871        let height = self.second_window_height()?;
872        self.menu.window = ContentWindow::new(len, height);
873        self.menu.window.scroll_to(self.menu.index(menu_mode));
874        self.set_focus_from_mode();
875        self.menu.input_history.filter_by_mode(menu_mode);
876        Ok(())
877    }
878
879    /// Set the height of the menu and scroll to the selected item.
880    pub fn set_height_for_menu_mode(&mut self, index: usize, menu_mode: Menu) -> Result<()> {
881        let height = self.term_size().height;
882        let prim_window_height = if menu_mode.is_nothing() {
883            height
884        } else {
885            height / 2
886        };
887        self.tabs[index]
888            .window
889            .set_height(prim_window_height as usize);
890        self.tabs[index]
891            .window
892            .scroll_to(self.tabs[index].window.top);
893        Ok(())
894    }
895
896    /// Set dual pane if the term is big enough
897    pub fn set_dual_pane_if_wide_enough(&mut self) -> Result<()> {
898        if self.wide_enough_for_dual() {
899            self.session.set_dual(true);
900        } else {
901            self.select_left();
902            self.session.set_dual(false);
903        }
904        Ok(())
905    }
906
907    /// Empty the flagged files, reset the view of every tab.
908    pub fn clear_flags_and_reset_view(&mut self) -> Result<()> {
909        self.menu.flagged.clear();
910        self.reset_tabs_view()
911    }
912
913    /// Returns the pathes of flagged file or the selected file if nothing is flagged
914    fn flagged_or_selected(&self) -> Vec<PathBuf> {
915        if self.menu.flagged.is_empty() {
916            let Some(path) = self.current_tab().selected_path() else {
917                return vec![];
918            };
919            vec![path.to_path_buf()]
920        } else {
921            self.menu.flagged.content().to_owned()
922        }
923    }
924
925    fn flagged_or_selected_relative_to(&self, here: &Path) -> Vec<PathBuf> {
926        self.flagged_or_selected()
927            .iter()
928            .filter_map(|abs_path| pathdiff::diff_paths(abs_path, here))
929            .filter(|f| !f.starts_with(".."))
930            .collect()
931    }
932
933    /// Returns a vector of path of files which are both flagged and in current
934    /// directory.
935    /// It's necessary since the user may have flagged files OUTSIDE of current
936    /// directory before calling Bulkrename.
937    /// It may be confusing since the same filename can be used in
938    /// different places.
939    pub fn flagged_in_current_dir(&self) -> Vec<PathBuf> {
940        self.menu.flagged.in_dir(&self.current_tab().directory.path)
941    }
942
943    /// Flag all files in the current directory or current tree.
944    pub fn flag_all(&mut self) {
945        match self.current_tab().display_mode {
946            Display::Directory => {
947                self.tabs[self.index]
948                    .directory
949                    .content
950                    .iter()
951                    .filter(|file| file.filename.as_ref() != "." && file.filename.as_ref() != "..")
952                    .for_each(|file| {
953                        self.menu.flagged.push(file.path.to_path_buf());
954                    });
955            }
956            Display::Tree => self.tabs[self.index].tree.flag_all(&mut self.menu.flagged),
957            _ => (),
958        }
959    }
960
961    /// Reverse every flag in _current_ directory. Flagged files in other
962    /// directory aren't affected.
963    pub fn reverse_flags(&mut self) {
964        log_info!("Reverse flags");
965        if !self.current_tab().display_mode.is_preview() {
966            self.tabs[self.index]
967                .directory
968                .content
969                .iter()
970                .for_each(|file| self.menu.flagged.toggle(&file.path));
971        }
972    }
973
974    /// Flag all _file_ (everything but directories) which are children of a directory.
975    pub fn toggle_flag_for_children(&mut self) {
976        let Some(path) = self.current_tab().selected_path() else {
977            return;
978        };
979        match self.current_tab().display_mode {
980            Display::Directory => self.toggle_flag_for_selected(),
981            Display::Tree => {
982                if !path.is_dir() {
983                    self.menu.flagged.toggle(&path);
984                } else {
985                    for entry in WalkDir::new(&path).into_iter().filter_map(|e| e.ok()) {
986                        let p = entry.path();
987                        if !p.is_dir() {
988                            self.menu.flagged.toggle(p);
989                        }
990                    }
991                }
992                let _ = self.update_second_pane_for_preview();
993            }
994            Display::Preview => (),
995            Display::Fuzzy => (),
996        }
997    }
998    /// Flag the selected file if any
999    pub fn toggle_flag_for_selected(&mut self) {
1000        let Some(path) = self.current_tab().selected_path() else {
1001            return;
1002        };
1003        match self.current_tab().display_mode {
1004            Display::Directory => {
1005                self.menu.flagged.toggle(&path);
1006                if !self.current_tab().directory.selected_is_last() {
1007                    self.tabs[self.index].normal_down_one_row();
1008                }
1009                let _ = self.update_second_pane_for_preview();
1010            }
1011            Display::Tree => {
1012                self.menu.flagged.toggle(&path);
1013                if !self.current_tab().tree.selected_is_last() {
1014                    self.current_tab_mut().tree_select_next();
1015                }
1016                let _ = self.update_second_pane_for_preview();
1017            }
1018            Display::Preview => self.menu.flagged.toggle(&path),
1019            Display::Fuzzy => (),
1020        }
1021        if matches!(
1022            self.current_tab().menu_mode,
1023            Menu::Navigate(Navigate::Flagged)
1024        ) {
1025            self.menu.window.set_len(self.menu.flagged.len());
1026        }
1027    }
1028
1029    /// Jumps to the selected flagged file.
1030    pub fn jump_flagged(&mut self) -> Result<()> {
1031        let Some(path) = self.menu.flagged.selected() else {
1032            return Ok(());
1033        };
1034        let path = path.to_owned();
1035        let tab = self.current_tab_mut();
1036        tab.set_display_mode(Display::Directory);
1037        tab.refresh_view()?;
1038        tab.jump(path)?;
1039        self.update_second_pane_for_preview()
1040    }
1041
1042    /// Execute a move or a copy of the flagged files to current directory.
1043    /// A progress bar is displayed (invisible for small files) and a notification
1044    /// is sent every time, even for 0 bytes files...
1045    pub fn cut_or_copy_flagged_files(&mut self, cut_or_copy: CopyMove) -> Result<()> {
1046        let sources = self.menu.flagged.content.clone();
1047        let dest = &self.current_tab().directory_of_selected()?.to_owned();
1048        self.cut_or_copy_files(cut_or_copy, sources, dest)
1049    }
1050
1051    fn cut_or_copy_files(
1052        &mut self,
1053        cut_or_copy: CopyMove,
1054        mut sources: Vec<PathBuf>,
1055        dest: &Path,
1056    ) -> Result<()> {
1057        if matches!(cut_or_copy, CopyMove::Move) {
1058            sources = Self::remove_subdir_of_dest(sources, dest);
1059            if sources.is_empty() {
1060                return Ok(());
1061            }
1062        }
1063
1064        if self.is_simple_move(&cut_or_copy, &sources, dest) {
1065            self.simple_move(&sources, dest)
1066        } else {
1067            self.complex_move(cut_or_copy, sources, dest)
1068        }
1069    }
1070
1071    /// Prevent the user from moving to a subdirectory of itself.
1072    /// Only used before _moving_, for copying it doesn't matter.
1073    fn remove_subdir_of_dest(mut sources: Vec<PathBuf>, dest: &Path) -> Vec<PathBuf> {
1074        for index in (0..sources.len()).rev() {
1075            if sources[index].starts_with(dest) {
1076                log_info!("Cannot move to a subdirectory of itself");
1077                log_line!("Cannot move to a subdirectory of itself");
1078                sources.remove(index);
1079            }
1080        }
1081        sources
1082    }
1083
1084    /// True iff it's a _move_ (not a copy) where all sources share the same mountpoint as destination.
1085    fn is_simple_move(&self, cut_or_copy: &CopyMove, sources: &[PathBuf], dest: &Path) -> bool {
1086        if cut_or_copy.is_copy() || sources.is_empty() {
1087            return false;
1088        }
1089        let mount_points = self.internal_settings.mount_points_set();
1090        let Some(source_mount_point) = sources[0].mount_point(&mount_points) else {
1091            return false;
1092        };
1093        for source in sources {
1094            if source.mount_point(&mount_points) != Some(source_mount_point) {
1095                return false;
1096            }
1097        }
1098        Some(source_mount_point) == dest.mount_point(&mount_points)
1099    }
1100
1101    fn simple_move(&mut self, sources: &[PathBuf], dest: &Path) -> Result<()> {
1102        for source in sources {
1103            let filename = filename_from_path(source)?;
1104            let dest = dest.to_path_buf().join(filename);
1105            log_info!("simple_move {source:?} -> {dest:?}");
1106            match std::fs::rename(source, &dest) {
1107                Ok(()) => {
1108                    log_line!(
1109                        "Moved {source} to {dest}",
1110                        source = source.display(),
1111                        dest = dest.display()
1112                    )
1113                }
1114                Err(e) => {
1115                    log_info!("Error: {e:?}");
1116                    log_line!("Error: {e:?}")
1117                }
1118            }
1119        }
1120        self.clear_flags_and_reset_view()
1121    }
1122
1123    fn complex_move(
1124        &mut self,
1125        cut_or_copy: CopyMove,
1126        sources: Vec<PathBuf>,
1127        dest: &Path,
1128    ) -> Result<()> {
1129        let mut must_act_now = true;
1130        if matches!(cut_or_copy, CopyMove::Copy) {
1131            if !self.internal_settings.copy_file_queue.is_empty() {
1132                log_info!("cut_or_copy_flagged_files: act later");
1133                must_act_now = false;
1134            }
1135            self.internal_settings
1136                .copy_file_queue
1137                .push((sources.to_owned(), dest.to_path_buf()));
1138        }
1139
1140        if must_act_now {
1141            log_info!("cut_or_copy_flagged_files: act now");
1142            let in_mem = copy_move(
1143                cut_or_copy,
1144                sources,
1145                dest,
1146                self.left_window_width(),
1147                self.term_size().height,
1148                Arc::clone(&self.fm_sender),
1149            )?;
1150            self.internal_settings.store_copy_progress(in_mem);
1151        }
1152        self.clear_flags_and_reset_view()
1153    }
1154
1155    /// Copy the next file in copy queue
1156    pub fn copy_next_file_in_queue(&mut self) -> Result<()> {
1157        self.internal_settings
1158            .copy_next_file_in_queue(self.fm_sender.clone(), self.left_window_width())
1159    }
1160
1161    /// Paste a text into an input box.
1162    pub fn paste_input(&mut self, pasted: &str) -> Result<()> {
1163        if self.focus.is_file() && self.current_tab().display_mode.is_fuzzy() {
1164            let Some(fuzzy) = &mut self.fuzzy else {
1165                return Ok(());
1166            };
1167            fuzzy.input.insert_string(pasted);
1168        } else if !self.focus.is_file() && self.current_tab().menu_mode.is_input() {
1169            self.menu.input.insert_string(pasted);
1170        }
1171        Ok(())
1172    }
1173
1174    /// Paste a path a move or copy file in current directory.
1175    pub fn paste_pathes(&mut self, pasted: &str) -> Result<()> {
1176        for pasted in pasted.split_whitespace() {
1177            self.paste_path(pasted)?;
1178        }
1179        Ok(())
1180    }
1181
1182    fn paste_path(&mut self, pasted: &str) -> Result<()> {
1183        // recognize pathes
1184        let pasted = Path::new(&pasted);
1185        if !pasted.is_absolute() {
1186            log_info!("pasted {pasted} isn't absolute.", pasted = pasted.display());
1187            return Ok(());
1188        }
1189        if !pasted.exists() {
1190            log_info!("pasted {pasted} doesn't exist.", pasted = pasted.display());
1191            return Ok(());
1192        }
1193        // build dest path
1194        let dest = self.current_tab().current_directory_path().to_path_buf();
1195        let Some(dest_filename) = build_dest_path(pasted, &dest) else {
1196            return Ok(());
1197        };
1198        if dest_filename == pasted {
1199            log_info!("pasted is same directory.");
1200            return Ok(());
1201        }
1202        if dest_filename.exists() {
1203            log_info!(
1204                "pasted {dest_filename} already exists",
1205                dest_filename = dest_filename.display()
1206            );
1207            return Ok(());
1208        }
1209        let sources = vec![pasted.to_path_buf()];
1210
1211        log_info!("pasted copy {sources:?} to {dest:?}");
1212        if self.is_simple_move(&CopyMove::Move, &sources, &dest) {
1213            self.simple_move(&sources, &dest)
1214        } else {
1215            self.complex_move(CopyMove::Copy, sources, &dest)
1216        }
1217    }
1218
1219    /// Init the fuzzy finder
1220    pub fn fuzzy_init(&mut self, kind: FuzzyKind) {
1221        self.fuzzy = Some(FuzzyFinder::new(kind).set_height(self.current_tab().window.height));
1222    }
1223
1224    fn fuzzy_drop(&mut self) {
1225        self.fuzzy = None;
1226    }
1227
1228    /// Sets the fuzzy finder to find files
1229    pub fn fuzzy_find_files(&mut self) -> Result<()> {
1230        let Some(fuzzy) = &self.fuzzy else {
1231            bail!("Fuzzy should be set");
1232        };
1233        let current_path = self.current_tab().current_directory_path().to_path_buf();
1234        fuzzy.find_files(current_path);
1235        Ok(())
1236    }
1237
1238    /// Sets the fuzzy finder to match against help lines
1239    pub fn fuzzy_help(&mut self, help: String) -> Result<()> {
1240        let Some(fuzzy) = &self.fuzzy else {
1241            bail!("Fuzzy should be set");
1242        };
1243        fuzzy.find_action(help);
1244        Ok(())
1245    }
1246
1247    /// Sets the fuzzy finder to match against text file content
1248    pub fn fuzzy_find_lines(&mut self) -> Result<()> {
1249        let Some(fuzzy) = &self.fuzzy else {
1250            bail!("Fuzzy should be set");
1251        };
1252        let Some(tokio_greper) = build_tokio_greper() else {
1253            log_info!("ripgrep & grep aren't in $PATH");
1254            return Ok(());
1255        };
1256        fuzzy.find_line(tokio_greper);
1257        Ok(())
1258    }
1259
1260    fn fuzzy_current_selection(&self) -> Option<std::string::String> {
1261        if let Some(fuzzy) = &self.fuzzy {
1262            fuzzy.pick()
1263        } else {
1264            None
1265        }
1266    }
1267
1268    /// Action when a fuzzy item is selected.
1269    /// It depends of the kind of fuzzy:
1270    /// files / line: go to this file,
1271    /// help: execute the selected action.
1272    pub fn fuzzy_select(&mut self) -> Result<()> {
1273        let Some(fuzzy) = &self.fuzzy else {
1274            bail!("Fuzzy should be set");
1275        };
1276        if let Some(pick) = fuzzy.pick() {
1277            match fuzzy.kind {
1278                FuzzyKind::File => self.tabs[self.index].cd_to_file(Path::new(&pick))?,
1279                FuzzyKind::Line => {
1280                    self.tabs[self.index].cd_to_file(&parse_line_output(&pick)?.0)?
1281                }
1282                FuzzyKind::Action => self.fuzzy_send_event(&pick)?,
1283            }
1284        } else {
1285            log_info!("Fuzzy had nothing to pick from");
1286        };
1287        self.fuzzy_leave()
1288    }
1289
1290    pub fn fuzzy_toggle_flag_selected(&mut self) -> Result<()> {
1291        let Some(fuzzy) = &self.fuzzy else {
1292            bail!("Fuzzy should be set");
1293        };
1294        if let Some(pick) = fuzzy.pick() {
1295            if let FuzzyKind::File = fuzzy.kind {
1296                self.menu.flagged.toggle(Path::new(&pick));
1297                self.fuzzy_navigate(FuzzyDirection::Down)?;
1298            }
1299        } else {
1300            log_info!("Fuzzy had nothing to select from");
1301        };
1302        Ok(())
1303    }
1304
1305    /// Run a command directly from help.
1306    /// Search a command with fuzzy finder, if it's a keybinding, run it directly.
1307    /// If the result can't be parsed, nothing is done.
1308    fn fuzzy_send_event(&self, pick: &str) -> Result<()> {
1309        if let Ok(key) = find_keybind_from_fuzzy(pick) {
1310            self.fm_sender.send(FmEvents::Term(Event::Key(key)))?;
1311        };
1312        Ok(())
1313    }
1314
1315    /// Exits the fuzzy finder and drops its instance
1316    pub fn fuzzy_leave(&mut self) -> Result<()> {
1317        self.fuzzy_drop();
1318        self.current_tab_mut().set_display_mode(Display::Directory);
1319        self.refresh_view()
1320    }
1321
1322    /// Deletes a char to the left in fuzzy finder.
1323    pub fn fuzzy_backspace(&mut self) -> Result<()> {
1324        let Some(fuzzy) = &mut self.fuzzy else {
1325            bail!("Fuzzy should be set");
1326        };
1327        fuzzy.input.delete_char_left();
1328        fuzzy.update_input(false);
1329        Ok(())
1330    }
1331
1332    /// Delete all chars to the right in fuzzy finder.
1333    pub fn fuzzy_delete(&mut self) -> Result<()> {
1334        let Some(fuzzy) = &mut self.fuzzy else {
1335            bail!("Fuzzy should be set");
1336        };
1337        fuzzy.input.delete_chars_right();
1338        fuzzy.update_input(false);
1339        Ok(())
1340    }
1341
1342    /// Move cursor to the left in fuzzy finder
1343    pub fn fuzzy_left(&mut self) -> Result<()> {
1344        let Some(fuzzy) = &mut self.fuzzy else {
1345            bail!("Fuzzy should be set");
1346        };
1347        fuzzy.input.cursor_left();
1348        Ok(())
1349    }
1350
1351    /// Move cursor to the right in fuzzy finder
1352    pub fn fuzzy_right(&mut self) -> Result<()> {
1353        let Some(fuzzy) = &mut self.fuzzy else {
1354            bail!("Fuzzy should be set");
1355        };
1356        fuzzy.input.cursor_right();
1357        Ok(())
1358    }
1359
1360    /// Move selection to the start (top)
1361    pub fn fuzzy_start(&mut self) -> Result<()> {
1362        self.fuzzy_navigate(FuzzyDirection::Start)
1363    }
1364
1365    /// Move selection to the end (bottom)
1366    pub fn fuzzy_end(&mut self) -> Result<()> {
1367        self.fuzzy_navigate(FuzzyDirection::End)
1368    }
1369
1370    /// Navigate to a [`FuzzyDirection`].
1371    pub fn fuzzy_navigate(&mut self, direction: FuzzyDirection) -> Result<()> {
1372        let Some(fuzzy) = &mut self.fuzzy else {
1373            bail!("Fuzzy should be set");
1374        };
1375        fuzzy.navigate(direction);
1376        if fuzzy.should_preview() {
1377            self.update_second_pane_for_preview()?;
1378        }
1379        Ok(())
1380    }
1381
1382    /// Issue a tick to the fuzzy finder
1383    pub fn fuzzy_tick(&mut self) {
1384        if let Some(fuzzy) = &mut self.fuzzy {
1385            fuzzy.tick(false);
1386        }
1387    }
1388
1389    /// Resize the fuzzy finder according to given height
1390    pub fn fuzzy_resize(&mut self, height: usize) {
1391        if let Some(fuzzy) = &mut self.fuzzy {
1392            fuzzy.resize(height)
1393        }
1394    }
1395
1396    /// Replace the current input by the next result from history
1397    pub fn input_history_next(&mut self) -> Result<()> {
1398        if self.focus.is_file() {
1399            return Ok(());
1400        }
1401        self.menu.input_history_next(&mut self.tabs[self.index])?;
1402        if let Menu::InputCompleted(input_completed) = self.current_tab().menu_mode {
1403            self.complete(input_completed)?;
1404        }
1405        Ok(())
1406    }
1407
1408    /// Replace the current input by the previous result from history
1409    pub fn input_history_prev(&mut self) -> Result<()> {
1410        if self.focus.is_file() {
1411            return Ok(());
1412        }
1413        self.menu.input_history_prev(&mut self.tabs[self.index])?;
1414        if let Menu::InputCompleted(input_completed) = self.current_tab().menu_mode {
1415            self.complete(input_completed)?;
1416        }
1417        Ok(())
1418    }
1419
1420    /// Push the typed char `c` into the input string and fill the completion menu with results.
1421    pub fn input_and_complete(&mut self, input_completed: InputCompleted, c: char) -> Result<()> {
1422        self.menu.input.insert(c);
1423        self.complete(input_completed)
1424    }
1425
1426    pub fn complete(&mut self, input_completed: InputCompleted) -> Result<()> {
1427        match input_completed {
1428            InputCompleted::Search => self.complete_search(),
1429            _ => self.complete_non_search(),
1430        }
1431    }
1432
1433    /// Update the input string with the current selection and fill the completion menu with results.
1434    pub fn complete_tab(&mut self, input_completed: InputCompleted) -> Result<()> {
1435        self.menu.completion_tab();
1436        self.complete_cd_move()?;
1437        if matches!(input_completed, InputCompleted::Search) {
1438            self.update_search()?;
1439            self.search()?;
1440        } else {
1441            self.menu.input_complete(&mut self.tabs[self.index])?
1442        }
1443        Ok(())
1444    }
1445
1446    fn complete_search(&mut self) -> Result<()> {
1447        self.update_search()?;
1448        self.search()?;
1449        self.menu.input_complete(&mut self.tabs[self.index])
1450    }
1451
1452    fn update_search(&mut self) -> Result<()> {
1453        if let Ok(search) = Search::new(&self.menu.input.string()) {
1454            self.current_tab_mut().search = search;
1455        };
1456        Ok(())
1457    }
1458
1459    /// Select the currently proposed item in search mode.
1460    /// This is usefull to allow the user to select an item when moving with up or down.
1461    pub fn follow_search(&mut self) -> Result<()> {
1462        let Some(proposition) = self.menu.completion.selected() else {
1463            return Ok(());
1464        };
1465        let current_path = match self.current_tab().display_mode {
1466            Display::Directory => self.current_tab().current_directory_path(),
1467            Display::Tree => self.current_tab().tree.root_path(),
1468            _ => {
1469                return Ok(());
1470            }
1471        };
1472        let mut full_path = current_path.to_path_buf();
1473        full_path.push(proposition);
1474        self.current_tab_mut().select_by_path(Arc::from(full_path));
1475        Ok(())
1476    }
1477
1478    fn complete_non_search(&mut self) -> Result<()> {
1479        self.complete_cd_move()?;
1480        self.menu.input_complete(&mut self.tabs[self.index])
1481    }
1482
1483    /// Move to the input path if possible.
1484    pub fn complete_cd_move(&mut self) -> Result<()> {
1485        if let Menu::InputCompleted(InputCompleted::Cd) = self.current_tab().menu_mode {
1486            let input = self.menu.input.string();
1487            if self.tabs[self.index].try_cd_to_file(input)? {
1488                self.update_second_pane_for_preview()?;
1489            }
1490        }
1491        Ok(())
1492    }
1493
1494    /// Update the flagged files depending of the input regex.
1495    pub fn input_regex(&mut self, char: char) -> Result<()> {
1496        self.menu.input.insert(char);
1497        self.flag_from_regex()?;
1498        Ok(())
1499    }
1500
1501    /// Flag every file matching a typed regex.
1502    /// Move to the "first" found match
1503    pub fn flag_from_regex(&mut self) -> Result<()> {
1504        let input = self.menu.input.string();
1505        if input.is_empty() {
1506            return Ok(());
1507        }
1508        let paths = match self.current_tab().display_mode {
1509            Display::Directory => self.tabs[self.index].directory.paths(),
1510            Display::Tree => self.tabs[self.index].tree.paths(),
1511            _ => return Ok(()),
1512        };
1513        regex_flagger(&input, &paths, &mut self.menu.flagged)?;
1514        if !self.menu.flagged.is_empty() {
1515            self.tabs[self.index]
1516                .go_to_file(self.menu.flagged.selected().context("no selected file")?);
1517        }
1518        Ok(())
1519    }
1520
1521    /// Open a the selected file with its opener
1522    pub fn open_selected_file(&mut self) -> Result<()> {
1523        let path = self
1524            .current_tab()
1525            .selected_path()
1526            .context("No selected path")?;
1527        self.open_single_file(&path)
1528    }
1529
1530    /// Opens the selected file (single)
1531    pub fn open_single_file(&mut self, path: &Path) -> Result<()> {
1532        match self.internal_settings.opener.kind(path) {
1533            Some(Kind::Internal(Internal::NotSupported)) => self.mount_iso_drive(),
1534            Some(_) => self.internal_settings.open_single_file(path),
1535            None => Ok(()),
1536        }
1537    }
1538
1539    /// Open every flagged file with their respective opener.
1540    pub fn open_flagged_files(&mut self) -> Result<()> {
1541        self.internal_settings
1542            .open_flagged_files(&self.menu.flagged)
1543    }
1544
1545    fn ensure_iso_device_is_some(&mut self) -> Result<()> {
1546        if self.menu.iso_device.is_none() {
1547            let path = path_to_string(
1548                &self
1549                    .current_tab()
1550                    .selected_path()
1551                    .context("No selected path")?,
1552            );
1553            self.menu.iso_device = Some(IsoDevice::from_path(path));
1554        }
1555        Ok(())
1556    }
1557
1558    /// Mount the currently selected file (which should be an .iso file) to
1559    /// `/run/media/$CURRENT_USER/fm_iso`
1560    /// Ask a sudo password first if needed. It should always be the case.
1561    fn mount_iso_drive(&mut self) -> Result<()> {
1562        if !self.menu.password_holder.has_sudo() {
1563            self.ask_password(Some(MountAction::MOUNT), PasswordUsage::ISO)?;
1564        } else {
1565            self.ensure_iso_device_is_some()?;
1566            let Some(ref mut iso_device) = self.menu.iso_device else {
1567                return Ok(());
1568            };
1569            if iso_device.mount(&current_username()?, &mut self.menu.password_holder)? {
1570                log_info!("iso mounter mounted {iso_device:?}");
1571                log_line!("iso : {iso_device}");
1572                let path = iso_device
1573                    .mountpoints
1574                    .clone()
1575                    .expect("mountpoint should be set");
1576                self.current_tab_mut().cd(Path::new(&path))?;
1577            };
1578            self.menu.iso_device = None;
1579        };
1580
1581        Ok(())
1582    }
1583
1584    /// Currently unused.
1585    /// Umount an iso device.
1586    pub fn umount_iso_drive(&mut self) -> Result<()> {
1587        if let Some(ref mut iso_device) = self.menu.iso_device {
1588            if !self.menu.password_holder.has_sudo() {
1589                self.ask_password(Some(MountAction::UMOUNT), PasswordUsage::ISO)?;
1590            } else {
1591                iso_device.umount(&current_username()?, &mut self.menu.password_holder)?;
1592            };
1593        }
1594        self.menu.iso_device = None;
1595        Ok(())
1596    }
1597
1598    /// Mount an encrypted device.
1599    /// If sudo password isn't set, asks for it and returns,
1600    /// Else if device keypass isn't set, asks for it and returns,
1601    /// Else, mount the device.
1602    pub fn mount_encrypted_drive(&mut self) -> Result<()> {
1603        if !self.menu.password_holder.has_sudo() {
1604            self.ask_password(
1605                Some(MountAction::MOUNT),
1606                PasswordUsage::CRYPTSETUP(PasswordKind::SUDO),
1607            )
1608        } else if !self.menu.password_holder.has_cryptsetup() {
1609            self.ask_password(
1610                Some(MountAction::MOUNT),
1611                PasswordUsage::CRYPTSETUP(PasswordKind::CRYPTSETUP),
1612            )
1613        } else {
1614            let Some(Mountable::Encrypted(device)) = &self.menu.mount.selected() else {
1615                return Ok(());
1616            };
1617            if let Ok(true) = device.mount(&current_username()?, &mut self.menu.password_holder) {
1618                self.go_to_encrypted_drive(device.uuid.clone())?;
1619            }
1620            Ok(())
1621        }
1622    }
1623
1624    /// Unmount the selected device.
1625    /// Will ask first for a sudo password which is immediatly forgotten.
1626    pub fn umount_encrypted_drive(&mut self) -> Result<()> {
1627        if !self.menu.password_holder.has_sudo() {
1628            self.ask_password(
1629                Some(MountAction::UMOUNT),
1630                PasswordUsage::CRYPTSETUP(PasswordKind::SUDO),
1631            )
1632        } else {
1633            let Some(Mountable::Encrypted(device)) = &self.menu.mount.selected() else {
1634                log_info!("Cannot find Encrypted device");
1635                return Ok(());
1636            };
1637            let success =
1638                device.umount_close_crypto(&current_username()?, &mut self.menu.password_holder)?;
1639            log_info!("umount_encrypted_drive: {success}");
1640            Ok(())
1641        }
1642    }
1643    /// Mount the selected encrypted device. Will ask first for sudo password and
1644    /// passphrase.
1645    /// Those passwords are always dropped immediatly after the commands are run.
1646    pub fn mount_normal_device(&mut self) -> Result<()> {
1647        let Some(device) = self.menu.mount.selected() else {
1648            return Ok(());
1649        };
1650        if device.is_mounted() {
1651            return Ok(());
1652        }
1653        if device.is_crypto() {
1654            return self.mount_encrypted_drive();
1655        }
1656        let Ok(success) = self.menu.mount.mount_selected_no_password() else {
1657            return Ok(());
1658        };
1659        if success {
1660            self.menu.mount.update(self.internal_settings.disks())?;
1661            self.go_to_normal_drive()?;
1662            return Ok(());
1663        }
1664        if !self.menu.password_holder.has_sudo() {
1665            self.ask_password(Some(MountAction::MOUNT), PasswordUsage::DEVICE)
1666        } else {
1667            if let Ok(true) = self
1668                .menu
1669                .mount
1670                .mount_selected(&mut self.menu.password_holder)
1671            {
1672                self.go_to_normal_drive()?;
1673            }
1674            Ok(())
1675        }
1676    }
1677
1678    /// Move to the selected device mount point.
1679    pub fn go_to_normal_drive(&mut self) -> Result<()> {
1680        let Some(path) = self.menu.mount.selected_mount_point() else {
1681            return Ok(());
1682        };
1683        let tab = self.current_tab_mut();
1684        tab.cd(&path)?;
1685        tab.refresh_view()
1686    }
1687
1688    /// Move to the mount point based on its onscreen index
1689    pub fn go_to_mount_per_index(&mut self, c: char) -> Result<()> {
1690        let Some(index) = c.to_digit(10) else {
1691            return Ok(());
1692        };
1693        self.menu.mount.set_index(index.saturating_sub(1) as _);
1694        self.go_to_normal_drive()
1695    }
1696
1697    fn go_to_encrypted_drive(&mut self, uuid: Option<String>) -> Result<()> {
1698        self.menu.mount.update(&self.internal_settings.disks)?;
1699        let Some(mountpoint) = self.menu.mount.find_encrypted_by_uuid(uuid) else {
1700            return Ok(());
1701        };
1702        log_info!("mountpoint {mountpoint}");
1703        let tab = self.current_tab_mut();
1704        tab.cd(Path::new(&mountpoint))?;
1705        tab.refresh_view()
1706    }
1707
1708    /// Unmount the selected device.
1709    /// Will ask first for a sudo password which is immediatly forgotten.
1710    pub fn umount_normal_device(&mut self) -> Result<()> {
1711        let Some(device) = self.menu.mount.selected() else {
1712            return Ok(());
1713        };
1714        if !device.is_mounted() {
1715            return Ok(());
1716        }
1717        if device.is_crypto() {
1718            return self.umount_encrypted_drive();
1719        }
1720        let Ok(success) = self.menu.mount.umount_selected_no_password() else {
1721            return Ok(());
1722        };
1723        if success {
1724            self.menu.mount.update(self.internal_settings.disks())?;
1725            return Ok(());
1726        }
1727        if !self.menu.password_holder.has_sudo() {
1728            self.ask_password(Some(MountAction::UMOUNT), PasswordUsage::DEVICE)
1729        } else {
1730            self.menu
1731                .mount
1732                .umount_selected(&mut self.menu.password_holder)
1733        }
1734    }
1735
1736    /// Ejects a removable device (usb key, mtp etc.).
1737    pub fn eject_removable_device(&mut self) -> Result<()> {
1738        if self.menu.mount.is_empty() {
1739            return Ok(());
1740        }
1741        let success = self.menu.mount.eject_removable_device()?;
1742        if success {
1743            self.menu.mount.update(self.internal_settings.disks())?;
1744        }
1745        Ok(())
1746    }
1747
1748    /// Reads and parse a shell command. Some arguments may be expanded.
1749    /// See [`crate::modes::shell_command_parser`] for more information.
1750    pub fn execute_shell_command_from_input(&mut self) -> Result<bool> {
1751        let shell_command = self.menu.input.string();
1752        self.execute_shell_command(shell_command, None, true)
1753    }
1754
1755    /// Parse and execute a shell command and expand tokens like %s, %t etc.
1756    pub fn execute_shell_command(
1757        &mut self,
1758        shell_command: String,
1759        files: Option<Vec<String>>,
1760        capture_output: bool,
1761    ) -> Result<bool> {
1762        let command = append_files_to_shell_command(shell_command, files);
1763        let Ok(args) = shell_command_parser(&command, self) else {
1764            self.set_menu_mode(self.index, Menu::Nothing)?;
1765            return Ok(true);
1766        };
1767        self.execute_parsed_command(args, command, capture_output)
1768    }
1769
1770    fn execute_parsed_command(
1771        &mut self,
1772        mut args: Vec<String>,
1773        shell_command: String,
1774        capture_output: bool,
1775    ) -> Result<bool> {
1776        let executable = args.remove(0);
1777        if is_sudo_command(&executable) {
1778            self.enter_sudo_mode(shell_command)?;
1779            return Ok(false);
1780        }
1781        let params: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
1782        if executable == *SAME_WINDOW_TOKEN {
1783            self.internal_settings.open_in_window(&params)?;
1784            return Ok(true);
1785        }
1786        if !is_in_path(&executable) {
1787            log_line!("{executable} isn't in path.");
1788            return Ok(true);
1789        }
1790        if capture_output {
1791            match execute_and_capture_output(executable, &params) {
1792                Ok(output) => self.preview_command_output(output, shell_command),
1793                Err(e) => {
1794                    log_info!("Error {e:?}");
1795                    log_line!("Command {shell_command} disn't finish properly");
1796                }
1797            }
1798            Ok(true)
1799        } else {
1800            let _ = execute_without_output(executable, &params);
1801            Ok(true)
1802        }
1803    }
1804
1805    fn enter_sudo_mode(&mut self, shell_command: String) -> Result<()> {
1806        self.menu.sudo_command = Some(shell_command);
1807        self.ask_password(None, PasswordUsage::SUDOCOMMAND)?;
1808        Ok(())
1809    }
1810
1811    /// Ask for a password of some kind (sudo or device passphrase).
1812    fn ask_password(
1813        &mut self,
1814        encrypted_action: Option<MountAction>,
1815        password_dest: PasswordUsage,
1816    ) -> Result<()> {
1817        log_info!("ask_password");
1818        self.set_menu_mode(
1819            self.index,
1820            Menu::InputSimple(InputSimple::Password(encrypted_action, password_dest)),
1821        )
1822    }
1823
1824    /// Attach the typed password to the correct receiver and
1825    /// execute the command requiring a password.
1826    pub fn execute_password_command(
1827        &mut self,
1828        action: Option<MountAction>,
1829        dest: PasswordUsage,
1830    ) -> Result<()> {
1831        let password = self.menu.input.string();
1832        self.menu.input.reset();
1833        if matches!(dest, PasswordUsage::CRYPTSETUP(PasswordKind::CRYPTSETUP)) {
1834            self.menu.password_holder.set_cryptsetup(password)
1835        } else {
1836            self.menu.password_holder.set_sudo(password)
1837        };
1838        let sudo_command = self.menu.sudo_command.to_owned();
1839        self.reset_menu_mode()?;
1840        self.dispatch_password(action, dest, sudo_command)?;
1841        Ok(())
1842    }
1843
1844    /// Execute a new mark, saving it to a config file for futher use.
1845    pub fn marks_new(&mut self, c: char) -> Result<()> {
1846        let path = self.current_tab_mut().directory.path.clone();
1847        self.menu.marks.new_mark(c, &path)?;
1848        self.current_tab_mut().refresh_view()?;
1849        self.reset_menu_mode()?;
1850        self.refresh_status()
1851    }
1852
1853    /// Execute a jump to a mark, moving to a valid path.
1854    /// If the saved path is invalid, it does nothing but reset the view.
1855    pub fn marks_jump_char(&mut self, c: char) -> Result<()> {
1856        if let Some(path) = self.menu.marks.get(c) {
1857            self.current_tab_mut().cd(&path)?;
1858        }
1859        self.current_tab_mut().refresh_view()?;
1860        self.reset_menu_mode()?;
1861        self.refresh_status()
1862    }
1863
1864    /// Execute a new temp mark, saving it temporary for futher use.
1865    pub fn temp_marks_new(&mut self, c: char) -> Result<()> {
1866        let Some(index) = c.to_digit(10) else {
1867            return Ok(());
1868        };
1869        let path = self.current_tab_mut().directory.path.to_path_buf();
1870        self.menu.temp_marks.set_mark(index as _, path);
1871        self.current_tab_mut().refresh_view()?;
1872        self.reset_menu_mode()?;
1873        self.refresh_status()
1874    }
1875
1876    /// Erase the current mark
1877    pub fn temp_marks_erase(&mut self) -> Result<()> {
1878        self.menu.temp_marks.erase_current_mark();
1879        Ok(())
1880    }
1881
1882    /// Execute a jump to a temporay mark, moving to a valid path.
1883    /// If the saved path is invalid, it does nothing but reset the view.
1884    pub fn temp_marks_jump_char(&mut self, c: char) -> Result<()> {
1885        let Some(index) = c.to_digit(10) else {
1886            return Ok(());
1887        };
1888        if let Some(path) = self.menu.temp_marks.get_mark(index as _) {
1889            self.tabs[self.index].cd(path)?;
1890        }
1891        self.current_tab_mut().refresh_view()?;
1892        self.reset_menu_mode()?;
1893        self.refresh_status()
1894    }
1895
1896    /// Recursively delete all flagged files.
1897    /// If we try to delete the current root of the tab, nothing is deleted but a warning is displayed.
1898    pub fn confirm_delete_files(&mut self) -> Result<()> {
1899        if self.menu.flagged.contains(self.current_tab().root_path()) {
1900            log_info!("Can't delete current root path");
1901            log_line!("Can't delete current root path");
1902            return Ok(());
1903        }
1904        self.menu.delete_flagged_files()?;
1905        self.reset_menu_mode()?;
1906        self.clear_flags_and_reset_view()?;
1907        self.refresh_status()
1908    }
1909
1910    /// Empty the trash folder permanently.
1911    pub fn confirm_trash_empty(&mut self) -> Result<()> {
1912        self.menu.trash.empty_trash()?;
1913        self.reset_menu_mode()?;
1914        self.clear_flags_and_reset_view()?;
1915        Ok(())
1916    }
1917
1918    /// Ask the new filenames and set the confirmation mode.
1919    pub fn bulk_ask_filenames(&mut self) -> Result<()> {
1920        let flagged = self.flagged_in_current_dir();
1921        let current_path = self.current_tab_path_str();
1922        self.menu.bulk.ask_filenames(flagged, &current_path)?;
1923        if let Some(temp_file) = self.menu.bulk.temp_file() {
1924            self.open_single_file(&temp_file)?;
1925            if self.internal_settings.opener.extension_use_term("txt") {
1926                self.fm_sender.send(FmEvents::BulkExecute)?;
1927            } else {
1928                self.menu.bulk.watch_in_thread(self.fm_sender.clone())?;
1929            }
1930        }
1931        Ok(())
1932    }
1933
1934    /// Execute a bulk create / rename
1935    pub fn bulk_execute(&mut self) -> Result<()> {
1936        self.menu.bulk.get_new_names()?;
1937        self.set_menu_mode(
1938            self.index,
1939            Menu::NeedConfirmation(NeedConfirmation::BulkAction),
1940        )?;
1941        Ok(())
1942    }
1943
1944    /// Execute the bulk action.
1945    pub fn confirm_bulk_action(&mut self) -> Result<()> {
1946        if let (Some(paths), Some(create)) = self.menu.bulk.execute()? {
1947            self.menu.flagged.update(paths);
1948            self.menu.flagged.extend(create);
1949        } else {
1950            self.menu.flagged.clear();
1951        };
1952        self.reset_menu_mode()?;
1953        self.reset_tabs_view()?;
1954        Ok(())
1955    }
1956
1957    fn run_sudo_command(&mut self, sudo_command: Option<String>) -> Result<()> {
1958        let Some(sudo_command) = sudo_command else {
1959            log_info!("No sudo_command received from args.");
1960            return self.menu.clear_sudo_attributes();
1961        };
1962        self.set_menu_mode(self.index, Menu::Nothing)?;
1963        reset_sudo_faillock()?;
1964        let Some(command) = sudo_command.strip_prefix("sudo ") else {
1965            log_info!("run_sudo_command cannot run {sudo_command}. It doesn't start with 'sudo '");
1966            return self.menu.clear_sudo_attributes();
1967        };
1968        let args = shell_command_parser(command, self)?;
1969        if args.is_empty() {
1970            return self.menu.clear_sudo_attributes();
1971        }
1972        let Some(password) = self.menu.password_holder.sudo() else {
1973            log_info!("run_sudo_command password isn't set");
1974            return self.menu.clear_sudo_attributes();
1975        };
1976        let directory_of_selected = self.current_tab().directory_of_selected()?;
1977        let (success, stdout, stderr) =
1978            execute_sudo_command_with_password(&args, &password, directory_of_selected)?;
1979        log_info!("sudo command execution. success: {success}");
1980        self.menu.clear_sudo_attributes()?;
1981        if !success {
1982            log_line!("sudo command failed: {stderr}");
1983        }
1984        self.preview_command_output(stdout, sudo_command.to_owned());
1985        Ok(())
1986    }
1987
1988    /// Dispatch the known password depending of which component set
1989    /// the `PasswordUsage`.
1990    #[rustfmt::skip]
1991    pub fn dispatch_password(
1992        &mut self,
1993        action: Option<MountAction>,
1994        dest: PasswordUsage,
1995        sudo_command: Option<String>,
1996    ) -> Result<()> {
1997        match (dest, action) {
1998            (PasswordUsage::ISO,            Some(MountAction::MOUNT))  => self.mount_iso_drive(),
1999            (PasswordUsage::ISO,            Some(MountAction::UMOUNT)) => self.umount_iso_drive(),
2000            (PasswordUsage::CRYPTSETUP(_),  Some(MountAction::MOUNT))  => self.mount_encrypted_drive(),
2001            (PasswordUsage::CRYPTSETUP(_),  Some(MountAction::UMOUNT)) => self.umount_encrypted_drive(),
2002            (PasswordUsage::DEVICE,         Some(MountAction::MOUNT))  => self.mount_normal_device(),
2003            (PasswordUsage::DEVICE,         Some(MountAction::UMOUNT)) => self.umount_normal_device(),
2004            (PasswordUsage::SUDOCOMMAND,    _)                         => self.run_sudo_command(sudo_command),
2005            (_,                             _)                         => Ok(()),
2006        }
2007    }
2008
2009    /// Set the display to preview a command output
2010    pub fn preview_command_output(&mut self, output: String, command: String) {
2011        log_info!("preview_command_output for {command}:\n{output}");
2012        if output.is_empty() {
2013            return;
2014        }
2015        let _ = self.reset_menu_mode();
2016        self.current_tab_mut().set_display_mode(Display::Preview);
2017        let preview = PreviewBuilder::cli_info(&output, command);
2018        if let Preview::Text(text) = &preview {
2019            log_info!("preview is Text with: {text:?}");
2020        } else {
2021            log_info!("preview is empty ? {empty}", empty = preview.is_empty());
2022        }
2023        self.current_tab_mut().window.reset(preview.len());
2024        self.current_tab_mut().preview = preview;
2025    }
2026
2027    /// Set the nvim listen address from what the user typed.
2028    pub fn update_nvim_listen_address(&mut self) {
2029        self.internal_settings.update_nvim_listen_address()
2030    }
2031
2032    /// Execute a command requiring a confirmation (Delete, Move or Copy).
2033    /// The action is only executed if the user typed the char `y`
2034    pub fn confirm(&mut self, c: char, confirmed_action: NeedConfirmation) -> Result<()> {
2035        if c == 'y' {
2036            if let Ok(must_leave) = self.match_confirmed_mode(confirmed_action) {
2037                if must_leave {
2038                    return Ok(());
2039                }
2040            }
2041        }
2042        self.reset_menu_mode()?;
2043        self.current_tab_mut().refresh_view()?;
2044
2045        Ok(())
2046    }
2047
2048    /// Execute a `NeedConfirmation` action (delete, move, copy, empty trash)
2049    fn match_confirmed_mode(&mut self, confirmed_action: NeedConfirmation) -> Result<bool> {
2050        match confirmed_action {
2051            NeedConfirmation::Delete => self.confirm_delete_files(),
2052            NeedConfirmation::Move => self.cut_or_copy_flagged_files(CopyMove::Move),
2053            NeedConfirmation::Copy => self.cut_or_copy_flagged_files(CopyMove::Copy),
2054            NeedConfirmation::EmptyTrash => self.confirm_trash_empty(),
2055            NeedConfirmation::BulkAction => self.confirm_bulk_action(),
2056            NeedConfirmation::DeleteCloud => {
2057                self.cloud_confirm_delete()?;
2058                return Ok(true);
2059            }
2060        }?;
2061        Ok(false)
2062    }
2063
2064    /// Execute an action when the header line was clicked.
2065    pub fn header_action(&mut self, col: u16, binds: &Bindings) -> Result<()> {
2066        if self.current_tab().display_mode.is_preview() {
2067            return Ok(());
2068        }
2069        Header::new(self, self.current_tab())?
2070            .action(col, !self.focus.is_left())
2071            .matcher(self, binds)
2072    }
2073
2074    /// Execute an action when the footer line was clicked.
2075    pub fn footer_action(&mut self, col: u16, binds: &Bindings) -> Result<()> {
2076        log_info!("footer clicked col {col}");
2077        let is_right = self.index == 1;
2078        let action = match self.current_tab().display_mode {
2079            Display::Preview => return Ok(()),
2080            Display::Tree | Display::Directory => {
2081                let footer = Footer::new(self, self.current_tab())?;
2082                footer.action(col, is_right).to_owned()
2083            }
2084            Display::Fuzzy => return Ok(()),
2085        };
2086        log_info!("action: {action}");
2087        action.matcher(self, binds)
2088    }
2089
2090    /// Change permission of the flagged files.
2091    /// Once the user has typed an octal permission like 754, it's applied to
2092    /// the file.
2093    /// Nothing is done if the user typed nothing or an invalid permission like
2094    /// 955.
2095    pub fn chmod(&mut self) -> Result<()> {
2096        if self.menu.input.is_empty() || self.menu.flagged.is_empty() {
2097            return Ok(());
2098        }
2099        let input_permission = &self.menu.input.string();
2100        Permissions::set_permissions_of_flagged(input_permission, &self.menu.flagged)?;
2101        self.reset_tabs_view()
2102    }
2103
2104    /// Enter the chmod mode where user can chmod a file.
2105    pub fn set_mode_chmod(&mut self) -> Result<()> {
2106        if self.current_tab_mut().directory.is_empty() {
2107            return Ok(());
2108        }
2109        if self.menu.flagged.is_empty() {
2110            self.toggle_flag_for_selected();
2111        }
2112        self.set_menu_mode(self.index, Menu::InputSimple(InputSimple::Chmod))?;
2113        self.menu.replace_input_by_permissions();
2114        Ok(())
2115    }
2116
2117    /// Execute a custom event on the selected file
2118    pub fn run_custom_command(&mut self, string: &str) -> Result<()> {
2119        self.execute_shell_command(string.to_owned(), None, true)?;
2120        Ok(())
2121    }
2122
2123    /// Compress the flagged files into an archive.
2124    /// Compression method is chosen by the user.
2125    /// The archive is created in the current directory and is named "archive.tar.??" or "archive.zip".
2126    /// Files which are above the CWD are filtered out since they can't be added to an archive.
2127    /// Archive creation depends on CWD so we ensure it's set to the selected tab.
2128    pub fn compress(&mut self) -> Result<()> {
2129        let here = &self.current_tab().directory.path;
2130        set_current_dir(here)?;
2131        let files_with_relative_paths = self.flagged_or_selected_relative_to(here);
2132        if files_with_relative_paths.is_empty() {
2133            return Ok(());
2134        }
2135        match self
2136            .menu
2137            .compression
2138            .compress(files_with_relative_paths, here)
2139        {
2140            Ok(()) => (),
2141            Err(error) => log_info!("Error compressing files. Error: {error}"),
2142        }
2143        Ok(())
2144    }
2145
2146    /// Sort all file in a tab with a sort key which is selected according to which char was pressed.
2147    pub fn sort_by_char(&mut self, c: char) -> Result<()> {
2148        self.current_tab_mut().sort(c)?;
2149        self.menu.reset();
2150        self.set_height_for_menu_mode(self.index, Menu::Nothing)?;
2151        self.tabs[self.index].menu_mode = Menu::Nothing;
2152        let len = self.menu.len(Menu::Nothing);
2153        let height = self.second_window_height()?;
2154        self.menu.window = ContentWindow::new(len, height);
2155        self.focus = self.focus.to_parent();
2156        Ok(())
2157    }
2158
2159    /// The width of a displayed canvas.
2160    pub fn canvas_width(&self) -> Result<u16> {
2161        let full_width = self.term_width();
2162        if self.session.dual() && full_width >= MIN_WIDTH_FOR_DUAL_PANE {
2163            Ok(full_width / 2)
2164        } else {
2165            Ok(full_width)
2166        }
2167    }
2168
2169    /// Executes a search in current folder, selecting the first file matching
2170    /// the current completion proposition.
2171    /// ie. If you typed `"jpg"` before, it will move to the first file
2172    /// whose filename contains `"jpg"`.
2173    /// The current order of files is used.
2174    fn search(&mut self) -> Result<()> {
2175        let Some(search) = self.build_search_from_input() else {
2176            self.current_tab_mut().search = Search::empty();
2177            return Ok(());
2178        };
2179        self.search_and_update(search)
2180    }
2181
2182    fn build_search_from_input(&self) -> Option<Search> {
2183        let searched = &self.menu.input.string();
2184        if searched.is_empty() {
2185            return None;
2186        }
2187        Search::new(searched).ok()
2188    }
2189
2190    fn search_and_update(&mut self, mut search: Search) -> Result<()> {
2191        search.execute_search(self.current_tab_mut())?;
2192        self.current_tab_mut().search = search;
2193        self.update_second_pane_for_preview()
2194    }
2195
2196    fn search_again(&mut self) -> Result<()> {
2197        let search = self.current_tab().search.clone_with_regex();
2198        self.search_and_update(search)
2199    }
2200
2201    /// Set a new filter.
2202    /// Doesn't reset the input.
2203    pub fn filter(&mut self) -> Result<()> {
2204        let filter = FilterKind::from_input(&self.menu.input.string());
2205        self.current_tab_mut().set_filter(filter)?;
2206        self.search_again()
2207    }
2208
2209    /// input the typed char and update the filterkind.
2210    pub fn input_filter(&mut self, c: char) -> Result<()> {
2211        self.menu.input_insert(c)?;
2212        self.filter()
2213    }
2214
2215    /// Open the picker menu. Does nothing if already in a picker.
2216    pub fn open_picker(&mut self) -> Result<()> {
2217        if self.current_tab().menu_mode.is_picker() || self.menu.input_history.filtered_is_empty() {
2218            return Ok(());
2219        }
2220        let menu = self.current_tab().menu_mode;
2221        let content = self.menu.input_history.filtered_as_list();
2222        self.menu.picker.set(
2223            Some(PickerCaller::Menu(menu)),
2224            menu.name_for_picker(),
2225            content,
2226        );
2227
2228        self.set_menu_mode(self.index, Menu::Navigate(Navigate::Picker))
2229    }
2230
2231    /// Load the selected cloud configuration file from the config folder and open navigation the menu.
2232    pub fn cloud_load_config(&mut self) -> Result<()> {
2233        let Some(picked) = self.menu.picker.selected() else {
2234            log_info!("nothing selected");
2235            return Ok(());
2236        };
2237        let Ok(cloud) = google_drive(picked) else {
2238            log_line!("Invalid config file {picked}");
2239            return Ok(());
2240        };
2241        self.menu.cloud = cloud;
2242        self.set_menu_mode(self.index, Menu::Navigate(Navigate::Cloud))
2243    }
2244
2245    /// Open the cloud menu.
2246    /// If no cloud has been selected yet, all cloud config file will be displayed.
2247    /// if a cloud has been selected, it will open it.
2248    pub fn cloud_open(&mut self) -> Result<()> {
2249        if self.menu.cloud.is_set() {
2250            self.set_menu_mode(self.index, Menu::Navigate(Navigate::Cloud))
2251        } else {
2252            self.cloud_picker()
2253        }
2254    }
2255
2256    fn cloud_picker(&mut self) -> Result<()> {
2257        let content = get_cloud_token_names()?;
2258        self.menu.picker.set(
2259            Some(PickerCaller::Cloud),
2260            Some("Pick a cloud provider".to_owned()),
2261            content,
2262        );
2263        self.set_menu_mode(self.index, Menu::Navigate(Navigate::Picker))
2264    }
2265
2266    /// Disconnect from the current cloud and open the picker
2267    pub fn cloud_disconnect(&mut self) -> Result<()> {
2268        self.menu.cloud.disconnect();
2269        self.cloud_open()
2270    }
2271
2272    /// Enter the delete mode and ask confirmation.
2273    /// Only the currently selected file can be deleted.
2274    pub fn cloud_enter_delete_mode(&mut self) -> Result<()> {
2275        self.set_menu_mode(
2276            self.index,
2277            Menu::NeedConfirmation(NeedConfirmation::DeleteCloud),
2278        )
2279    }
2280
2281    /// Delete the selected file once a confirmation has been received from the user.
2282    pub fn cloud_confirm_delete(&mut self) -> Result<()> {
2283        self.menu.cloud.delete()?;
2284        self.set_menu_mode(self.index, Menu::Navigate(Navigate::Cloud))?;
2285        self.menu.cloud.refresh_current()?;
2286        self.menu.window.scroll_to(self.menu.cloud.index);
2287        Ok(())
2288    }
2289
2290    /// Update the metadata of the current file.
2291    pub fn cloud_update_metadata(&mut self) -> Result<()> {
2292        self.menu.cloud.update_metadata()
2293    }
2294
2295    /// Ask the user to enter a name for the new directory.
2296    pub fn cloud_enter_newdir_mode(&mut self) -> Result<()> {
2297        self.set_menu_mode(self.index, Menu::InputSimple(InputSimple::CloudNewdir))?;
2298        self.refresh_view()
2299    }
2300
2301    /// Create the new directory in current path with the name the user entered.
2302    pub fn cloud_create_newdir(&mut self, dirname: String) -> Result<()> {
2303        self.menu.cloud.create_newdir(dirname)?;
2304        self.menu.cloud.refresh_current()
2305    }
2306
2307    fn get_normal_selected_file(&self) -> Option<FileInfo> {
2308        let local_file = self.tabs[self.index].current_file().ok()?;
2309        match local_file.file_kind {
2310            FileKind::NormalFile => Some(local_file),
2311            _ => None,
2312        }
2313    }
2314
2315    /// Upload the current file (tree or directory mode) the the current remote path.
2316    pub fn cloud_upload_selected_file(&mut self) -> Result<()> {
2317        let Some(local_file) = self.get_normal_selected_file() else {
2318            log_line!("Can only upload normal files.");
2319            return Ok(());
2320        };
2321        self.menu.cloud.upload(&local_file)?;
2322        self.menu.cloud.refresh_current()
2323    }
2324
2325    /// Enter a file (download it) or the directory (explore it).
2326    pub fn cloud_enter_file_or_dir(&mut self) -> Result<()> {
2327        if let Some(entry) = self.menu.cloud.selected() {
2328            match entry.metadata().mode() {
2329                EntryMode::Unknown => (),
2330                EntryMode::FILE => self
2331                    .menu
2332                    .cloud
2333                    .download(self.current_tab().directory_of_selected()?)?,
2334                EntryMode::DIR => {
2335                    self.menu.cloud.enter_selected()?;
2336                    self.cloud_set_content_window_len()?;
2337                }
2338            };
2339        };
2340        Ok(())
2341    }
2342
2343    fn cloud_set_content_window_len(&mut self) -> Result<()> {
2344        let len = self.menu.cloud.content.len();
2345        let height = self.second_window_height()?;
2346        self.menu.window = ContentWindow::new(len, height);
2347        Ok(())
2348    }
2349
2350    /// Move to the parent folder if possible.
2351    /// Nothing is done in the root folder.
2352    pub fn cloud_move_to_parent(&mut self) -> Result<()> {
2353        self.menu.cloud.move_to_parent()?;
2354        self.cloud_set_content_window_len()?;
2355        Ok(())
2356    }
2357
2358    pub fn toggle_flag_visual(&mut self) {
2359        if self.current_tab().visual {
2360            let Some(path) = self.current_tab().selected_path() else {
2361                return;
2362            };
2363            self.menu.flagged.toggle(&path)
2364        }
2365    }
2366
2367    /// Parse and execute the received IPC message.
2368    /// IPC message can currently be of 3 forms:
2369    /// - `GO <path>` -> cd to this path and select the file.
2370    /// - `KEY <key>` -> act as if the key was pressed. `<key>` should be formated like in the config file.
2371    /// - `ACTION <action>` -> execute the action. Similar to `:<action><Enter>`. `<action>` should be formated like in the config file.
2372    ///
2373    /// Other messages are ignored.
2374    /// Failed messages (path don't exists, wrongly formated key or action) are ignored silently.
2375    pub fn parse_ipc(&mut self, msg: String) -> Result<()> {
2376        let mut split = msg.split_whitespace();
2377        match split.next() {
2378            Some("GO") => {
2379                log_info!("Received IPC command GO");
2380                if let Some(dest) = split.next() {
2381                    log_info!("Received IPC command GO to {dest}");
2382                    let dest = tilde(dest);
2383                    self.current_tab_mut().cd_to_file(dest.as_ref())?
2384                }
2385            }
2386            Some("KEY") => {
2387                log_info!("Received IPC command KEY");
2388                if let Some(keyname) = split.next() {
2389                    if let Some(key) = from_keyname(keyname) {
2390                        self.fm_sender.send(FmEvents::Term(Event::Key(key)))?;
2391                        log_info!("Sent key event: {key:?}");
2392                    }
2393                }
2394            }
2395            Some("ACTION") => {
2396                log_info!("Received IPC command ACTION");
2397                if let Some(action_str) = split.next() {
2398                    if let Ok(action) = ActionMap::from_str(action_str) {
2399                        log_info!("Sent action event: {action:?}");
2400                        self.fm_sender.send(FmEvents::Action(action))?;
2401                    }
2402                }
2403            }
2404            Some(_unknown) => log_info!("Received unknown IPC command: {msg}"),
2405            None => (),
2406        };
2407        Ok(())
2408    }
2409}
2410
2411fn find_keybind_from_fuzzy(line: &str) -> Result<KeyEvent> {
2412    let Some(keybind) = line.split(':').next() else {
2413        bail!("No keybind found");
2414    };
2415    let Some(key) = from_keyname(keybind.trim()) else {
2416        bail!("{keybind} isn't a valid Key name.");
2417    };
2418    Ok(key)
2419}