Skip to main content

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.use_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                    self.rename_marks(source, dest)?;
1114                }
1115                Err(e) => {
1116                    log_info!("Error: {e:?}");
1117                    log_line!("Error: {e:?}")
1118                }
1119            }
1120        }
1121        self.clear_flags_and_reset_view()
1122    }
1123
1124    fn complex_move(
1125        &mut self,
1126        cut_or_copy: CopyMove,
1127        sources: Vec<PathBuf>,
1128        dest: &Path,
1129    ) -> Result<()> {
1130        let mut must_act_now = true;
1131        if matches!(cut_or_copy, CopyMove::Copy) {
1132            if !self.internal_settings.copy_file_queue.is_empty() {
1133                log_info!("cut_or_copy_flagged_files: act later");
1134                must_act_now = false;
1135            }
1136            self.internal_settings
1137                .copy_file_queue
1138                .push((sources.to_owned(), dest.to_path_buf()));
1139        }
1140
1141        if must_act_now {
1142            log_info!("cut_or_copy_flagged_files: act now");
1143            let in_mem = copy_move(
1144                cut_or_copy,
1145                sources,
1146                dest,
1147                self.left_window_width(),
1148                self.term_size().height,
1149                Arc::clone(&self.fm_sender),
1150            )?;
1151            self.internal_settings.store_copy_progress(in_mem);
1152        }
1153        self.clear_flags_and_reset_view()
1154    }
1155
1156    /// Copy the next file in copy queue
1157    pub fn copy_next_file_in_queue(&mut self) -> Result<()> {
1158        self.internal_settings
1159            .copy_next_file_in_queue(self.fm_sender.clone(), self.left_window_width())
1160    }
1161
1162    /// Paste a text into an input box.
1163    pub fn paste_input(&mut self, pasted: &str) -> Result<()> {
1164        if self.focus.is_file() && self.current_tab().display_mode.is_fuzzy() {
1165            let Some(fuzzy) = &mut self.fuzzy else {
1166                return Ok(());
1167            };
1168            fuzzy.input.insert_string(pasted);
1169        } else if !self.focus.is_file() && self.current_tab().menu_mode.is_input() {
1170            self.menu.input.insert_string(pasted);
1171        }
1172        Ok(())
1173    }
1174
1175    /// Paste a path a move or copy file in current directory.
1176    pub fn paste_pathes(&mut self, pasted: &str) -> Result<()> {
1177        for pasted in pasted.split_whitespace() {
1178            self.paste_path(pasted)?;
1179        }
1180        Ok(())
1181    }
1182
1183    fn paste_path(&mut self, pasted: &str) -> Result<()> {
1184        // recognize pathes
1185        let pasted = Path::new(&pasted);
1186        if !pasted.is_absolute() {
1187            log_info!("pasted {pasted} isn't absolute.", pasted = pasted.display());
1188            return Ok(());
1189        }
1190        if !pasted.exists() {
1191            log_info!("pasted {pasted} doesn't exist.", pasted = pasted.display());
1192            return Ok(());
1193        }
1194        // build dest path
1195        let dest = self.current_tab().root_path().to_path_buf();
1196        let Some(dest_filename) = build_dest_path(pasted, &dest) else {
1197            return Ok(());
1198        };
1199        if dest_filename == pasted {
1200            log_info!("pasted is same directory.");
1201            return Ok(());
1202        }
1203        if dest_filename.exists() {
1204            log_info!(
1205                "pasted {dest_filename} already exists",
1206                dest_filename = dest_filename.display()
1207            );
1208            return Ok(());
1209        }
1210        let sources = vec![pasted.to_path_buf()];
1211
1212        log_info!("pasted copy {sources:?} to {dest:?}");
1213        if self.is_simple_move(&CopyMove::Move, &sources, &dest) {
1214            self.simple_move(&sources, &dest)
1215        } else {
1216            self.complex_move(CopyMove::Copy, sources, &dest)
1217        }
1218    }
1219
1220    /// Init the fuzzy finder
1221    pub fn fuzzy_init(&mut self, kind: FuzzyKind) {
1222        self.fuzzy = Some(FuzzyFinder::new(kind).set_height(self.current_tab().window.height));
1223    }
1224
1225    fn fuzzy_drop(&mut self) {
1226        self.fuzzy = None;
1227    }
1228
1229    /// Sets the fuzzy finder to find files
1230    pub fn fuzzy_find_files(&mut self) -> Result<()> {
1231        let Some(fuzzy) = &self.fuzzy else {
1232            bail!("Fuzzy should be set");
1233        };
1234        let current_path = self.current_tab().current_directory_path().to_path_buf();
1235        fuzzy.find_files(current_path);
1236        Ok(())
1237    }
1238
1239    /// Sets the fuzzy finder to match against help lines
1240    pub fn fuzzy_help(&mut self, help: String) -> Result<()> {
1241        let Some(fuzzy) = &self.fuzzy else {
1242            bail!("Fuzzy should be set");
1243        };
1244        fuzzy.find_action(help);
1245        Ok(())
1246    }
1247
1248    /// Sets the fuzzy finder to match against text file content
1249    pub fn fuzzy_find_lines(&mut self) -> Result<()> {
1250        let Some(fuzzy) = &self.fuzzy else {
1251            bail!("Fuzzy should be set");
1252        };
1253        let Some(tokio_greper) = build_tokio_greper() else {
1254            log_info!("ripgrep & grep aren't in $PATH");
1255            return Ok(());
1256        };
1257        fuzzy.find_line(tokio_greper);
1258        Ok(())
1259    }
1260
1261    fn fuzzy_current_selection(&self) -> Option<std::string::String> {
1262        if let Some(fuzzy) = &self.fuzzy {
1263            fuzzy.pick()
1264        } else {
1265            None
1266        }
1267    }
1268
1269    /// Action when a fuzzy item is selected.
1270    /// It depends of the kind of fuzzy:
1271    /// files / line: go to this file,
1272    /// help: execute the selected action.
1273    pub fn fuzzy_select(&mut self) -> Result<()> {
1274        let Some(fuzzy) = &self.fuzzy else {
1275            bail!("Fuzzy should be set");
1276        };
1277        if let Some(pick) = fuzzy.pick() {
1278            match fuzzy.kind {
1279                FuzzyKind::File => self.tabs[self.index].cd_to_file(Path::new(&pick))?,
1280                FuzzyKind::Line => {
1281                    self.tabs[self.index].cd_to_file(&parse_line_output(&pick)?.0)?
1282                }
1283                FuzzyKind::Action => self.fuzzy_send_event(&pick)?,
1284            }
1285        } else {
1286            log_info!("Fuzzy had nothing to pick from");
1287        };
1288        self.fuzzy_leave()
1289    }
1290
1291    pub fn fuzzy_toggle_flag_selected(&mut self) -> Result<()> {
1292        let Some(fuzzy) = &self.fuzzy else {
1293            bail!("Fuzzy should be set");
1294        };
1295        if let Some(pick) = fuzzy.pick() {
1296            if let FuzzyKind::File = fuzzy.kind {
1297                self.menu.flagged.toggle(Path::new(&pick));
1298                self.fuzzy_navigate(FuzzyDirection::Down)?;
1299            }
1300        } else {
1301            log_info!("Fuzzy had nothing to select from");
1302        };
1303        Ok(())
1304    }
1305
1306    /// Open a file directly from the fuzzy finder.
1307    /// Only works for fuzzy finder of file & fuzzy finder of lines.
1308    pub fn fuzzy_open_file(&mut self) -> Result<()> {
1309        let Some(fuzzy) = &self.fuzzy else {
1310            bail!("Fuzzy should be set");
1311        };
1312        if let Some(pick) = fuzzy.pick() {
1313            match fuzzy.kind {
1314                FuzzyKind::File => {
1315                    self.open_single_file(Path::new(&pick))?;
1316                }
1317                FuzzyKind::Line => {
1318                    let (path, _) = parse_line_output(&pick)?;
1319                    self.open_single_file(&path)?;
1320                }
1321                _ => (),
1322            }
1323        } else {
1324            log_info!("Fuzzy had nothing to select from");
1325        };
1326        Ok(())
1327    }
1328
1329    /// Run a command directly from help.
1330    /// Search a command with fuzzy finder, if it's a keybinding, run it directly.
1331    /// If the result can't be parsed, nothing is done.
1332    fn fuzzy_send_event(&self, pick: &str) -> Result<()> {
1333        if let Ok(key) = find_keybind_from_fuzzy(pick) {
1334            self.fm_sender.send(FmEvents::Term(Event::Key(key)))?;
1335        };
1336        Ok(())
1337    }
1338
1339    /// Exits the fuzzy finder and drops its instance
1340    pub fn fuzzy_leave(&mut self) -> Result<()> {
1341        self.fuzzy_drop();
1342        self.current_tab_mut().set_display_mode(Display::Directory);
1343        self.refresh_view()
1344    }
1345
1346    /// Deletes a char to the left in fuzzy finder.
1347    pub fn fuzzy_backspace(&mut self) -> Result<()> {
1348        let Some(fuzzy) = &mut self.fuzzy else {
1349            bail!("Fuzzy should be set");
1350        };
1351        fuzzy.input.delete_char_left();
1352        fuzzy.update_input(false);
1353        Ok(())
1354    }
1355
1356    /// Delete all chars to the right in fuzzy finder.
1357    pub fn fuzzy_delete(&mut self) -> Result<()> {
1358        let Some(fuzzy) = &mut self.fuzzy else {
1359            bail!("Fuzzy should be set");
1360        };
1361        fuzzy.input.delete_chars_right();
1362        fuzzy.update_input(false);
1363        Ok(())
1364    }
1365
1366    /// Move cursor to the left in fuzzy finder
1367    pub fn fuzzy_left(&mut self) -> Result<()> {
1368        let Some(fuzzy) = &mut self.fuzzy else {
1369            bail!("Fuzzy should be set");
1370        };
1371        fuzzy.input.cursor_left();
1372        Ok(())
1373    }
1374
1375    /// Move cursor to the right in fuzzy finder
1376    pub fn fuzzy_right(&mut self) -> Result<()> {
1377        let Some(fuzzy) = &mut self.fuzzy else {
1378            bail!("Fuzzy should be set");
1379        };
1380        fuzzy.input.cursor_right();
1381        Ok(())
1382    }
1383
1384    /// Move selection to the start (top)
1385    pub fn fuzzy_start(&mut self) -> Result<()> {
1386        self.fuzzy_navigate(FuzzyDirection::Start)
1387    }
1388
1389    /// Move selection to the end (bottom)
1390    pub fn fuzzy_end(&mut self) -> Result<()> {
1391        self.fuzzy_navigate(FuzzyDirection::End)
1392    }
1393
1394    /// Navigate to a [`FuzzyDirection`].
1395    pub fn fuzzy_navigate(&mut self, direction: FuzzyDirection) -> Result<()> {
1396        let Some(fuzzy) = &mut self.fuzzy else {
1397            bail!("Fuzzy should be set");
1398        };
1399        fuzzy.navigate(direction);
1400        if fuzzy.should_preview() {
1401            self.update_second_pane_for_preview()?;
1402        }
1403        Ok(())
1404    }
1405
1406    /// Issue a tick to the fuzzy finder
1407    pub fn fuzzy_tick(&mut self) {
1408        if let Some(fuzzy) = &mut self.fuzzy {
1409            fuzzy.tick(false);
1410        }
1411    }
1412
1413    /// Resize the fuzzy finder according to given height
1414    pub fn fuzzy_resize(&mut self, height: usize) {
1415        if let Some(fuzzy) = &mut self.fuzzy {
1416            fuzzy.resize(height)
1417        }
1418    }
1419
1420    /// Replace the current input by the next result from history
1421    pub fn input_history_next(&mut self) -> Result<()> {
1422        if self.focus.is_file() {
1423            return Ok(());
1424        }
1425        self.menu.input_history_next(&mut self.tabs[self.index])?;
1426        if let Menu::InputCompleted(input_completed) = self.current_tab().menu_mode {
1427            self.complete(input_completed)?;
1428        }
1429        Ok(())
1430    }
1431
1432    /// Replace the current input by the previous result from history
1433    pub fn input_history_prev(&mut self) -> Result<()> {
1434        if self.focus.is_file() {
1435            return Ok(());
1436        }
1437        self.menu.input_history_prev(&mut self.tabs[self.index])?;
1438        if let Menu::InputCompleted(input_completed) = self.current_tab().menu_mode {
1439            self.complete(input_completed)?;
1440        }
1441        Ok(())
1442    }
1443
1444    /// Push the typed char `c` into the input string and fill the completion menu with results.
1445    pub fn input_and_complete(&mut self, input_completed: InputCompleted, c: char) -> Result<()> {
1446        self.menu.input.insert(c);
1447        self.complete(input_completed)
1448    }
1449
1450    pub fn complete(&mut self, input_completed: InputCompleted) -> Result<()> {
1451        match input_completed {
1452            InputCompleted::Search => self.complete_search(),
1453            _ => self.complete_non_search(),
1454        }
1455    }
1456
1457    /// Update the input string with the current selection and fill the completion menu with results.
1458    pub fn complete_tab(&mut self, input_completed: InputCompleted) -> Result<()> {
1459        self.menu.completion_tab();
1460        self.complete_cd_move()?;
1461        if matches!(input_completed, InputCompleted::Search) {
1462            self.update_search()?;
1463            self.search()?;
1464        } else {
1465            self.menu.input_complete(&mut self.tabs[self.index])?
1466        }
1467        Ok(())
1468    }
1469
1470    fn complete_search(&mut self) -> Result<()> {
1471        self.update_search()?;
1472        self.search()?;
1473        self.menu.input_complete(&mut self.tabs[self.index])
1474    }
1475
1476    fn update_search(&mut self) -> Result<()> {
1477        if let Ok(search) = Search::new(&self.menu.input.string()) {
1478            self.current_tab_mut().search = search;
1479        };
1480        Ok(())
1481    }
1482
1483    /// Select the currently proposed item in search mode.
1484    /// This is usefull to allow the user to select an item when moving with up or down.
1485    pub fn follow_search(&mut self) -> Result<()> {
1486        let Some(proposition) = self.menu.completion.selected() else {
1487            return Ok(());
1488        };
1489        let current_path = match self.current_tab().display_mode {
1490            Display::Directory => self.current_tab().current_directory_path(),
1491            Display::Tree => self.current_tab().tree.root_path(),
1492            _ => {
1493                return Ok(());
1494            }
1495        };
1496        let mut full_path = current_path.to_path_buf();
1497        full_path.push(proposition);
1498        self.current_tab_mut().select_by_path(Arc::from(full_path));
1499        Ok(())
1500    }
1501
1502    fn complete_non_search(&mut self) -> Result<()> {
1503        self.complete_cd_move()?;
1504        self.menu.input_complete(&mut self.tabs[self.index])
1505    }
1506
1507    /// Move to the input path if possible.
1508    pub fn complete_cd_move(&mut self) -> Result<()> {
1509        if let Menu::InputCompleted(InputCompleted::Cd) = self.current_tab().menu_mode {
1510            let input = self.menu.input.string();
1511            if self.tabs[self.index].try_cd_to_file(input)? {
1512                self.update_second_pane_for_preview()?;
1513            }
1514        }
1515        Ok(())
1516    }
1517
1518    /// Update the flagged files depending of the input regex.
1519    pub fn input_regex(&mut self, char: char) -> Result<()> {
1520        self.menu.input.insert(char);
1521        self.flag_from_regex()?;
1522        Ok(())
1523    }
1524
1525    /// Flag every file matching a typed regex.
1526    /// Move to the "first" found match
1527    pub fn flag_from_regex(&mut self) -> Result<()> {
1528        let input = self.menu.input.string();
1529        if input.is_empty() {
1530            return Ok(());
1531        }
1532        let paths = match self.current_tab().display_mode {
1533            Display::Directory => self.tabs[self.index].directory.paths(),
1534            Display::Tree => self.tabs[self.index].tree.paths(),
1535            _ => return Ok(()),
1536        };
1537        regex_flagger(&input, &paths, &mut self.menu.flagged)?;
1538        if !self.menu.flagged.is_empty() {
1539            self.tabs[self.index]
1540                .go_to_file(self.menu.flagged.selected().context("no selected file")?);
1541        }
1542        Ok(())
1543    }
1544
1545    /// Open a the selected file with its opener
1546    pub fn open_selected_file(&mut self) -> Result<()> {
1547        let path = self
1548            .current_tab()
1549            .selected_path()
1550            .context("No selected path")?;
1551        self.open_single_file(&path)
1552    }
1553
1554    /// Opens the selected file (single)
1555    pub fn open_single_file(&mut self, path: &Path) -> Result<()> {
1556        match self.internal_settings.opener.kind(path) {
1557            Some(Kind::Internal(Internal::NotSupported)) => self.mount_iso_drive(),
1558            Some(_) => self.internal_settings.open_single_file(path),
1559            None => Ok(()),
1560        }
1561    }
1562
1563    /// Open every flagged file with their respective opener.
1564    pub fn open_flagged_files(&mut self) -> Result<()> {
1565        self.internal_settings
1566            .open_flagged_files(&self.menu.flagged)
1567    }
1568
1569    fn ensure_iso_device_is_some(&mut self) -> Result<()> {
1570        if self.menu.iso_device.is_none() {
1571            let path = path_to_string(
1572                &self
1573                    .current_tab()
1574                    .selected_path()
1575                    .context("No selected path")?,
1576            );
1577            self.menu.iso_device = Some(IsoDevice::from_path(path));
1578        }
1579        Ok(())
1580    }
1581
1582    /// Mount the currently selected file (which should be an .iso file) to
1583    /// `/run/media/$CURRENT_USER/fm_iso`
1584    /// Ask a sudo password first if needed. It should always be the case.
1585    fn mount_iso_drive(&mut self) -> Result<()> {
1586        if !self.menu.password_holder.has_sudo() {
1587            self.ask_password(Some(MountAction::MOUNT), PasswordUsage::ISO)?;
1588        } else {
1589            self.ensure_iso_device_is_some()?;
1590            let Some(ref mut iso_device) = self.menu.iso_device else {
1591                return Ok(());
1592            };
1593            if iso_device.mount(&current_username()?, &mut self.menu.password_holder)? {
1594                log_info!("iso mounter mounted {iso_device:?}");
1595                log_line!("iso : {iso_device}");
1596                let path = iso_device
1597                    .mountpoints
1598                    .clone()
1599                    .expect("mountpoint should be set");
1600                self.current_tab_mut().cd(Path::new(&path))?;
1601            };
1602            self.menu.iso_device = None;
1603        };
1604
1605        Ok(())
1606    }
1607
1608    /// Currently unused.
1609    /// Umount an iso device.
1610    pub fn umount_iso_drive(&mut self) -> Result<()> {
1611        if let Some(ref mut iso_device) = self.menu.iso_device {
1612            if !self.menu.password_holder.has_sudo() {
1613                self.ask_password(Some(MountAction::UMOUNT), PasswordUsage::ISO)?;
1614            } else {
1615                iso_device.umount(&current_username()?, &mut self.menu.password_holder)?;
1616            };
1617        }
1618        self.menu.iso_device = None;
1619        Ok(())
1620    }
1621
1622    /// Mount an encrypted device.
1623    /// If sudo password isn't set, asks for it and returns,
1624    /// Else if device keypass isn't set, asks for it and returns,
1625    /// Else, mount the device.
1626    pub fn mount_encrypted_drive(&mut self) -> Result<()> {
1627        if !self.menu.password_holder.has_sudo() {
1628            self.ask_password(
1629                Some(MountAction::MOUNT),
1630                PasswordUsage::CRYPTSETUP(PasswordKind::SUDO),
1631            )
1632        } else if !self.menu.password_holder.has_cryptsetup() {
1633            self.ask_password(
1634                Some(MountAction::MOUNT),
1635                PasswordUsage::CRYPTSETUP(PasswordKind::CRYPTSETUP),
1636            )
1637        } else {
1638            let Some(Mountable::Encrypted(device)) = &self.menu.mount.selected() else {
1639                return Ok(());
1640            };
1641            if let Ok(true) = device.mount(&current_username()?, &mut self.menu.password_holder) {
1642                self.go_to_encrypted_drive(device.uuid.clone())?;
1643            }
1644            Ok(())
1645        }
1646    }
1647
1648    /// Unmount the selected device.
1649    /// Will ask first for a sudo password which is immediatly forgotten.
1650    pub fn umount_encrypted_drive(&mut self) -> Result<()> {
1651        if !self.menu.password_holder.has_sudo() {
1652            self.ask_password(
1653                Some(MountAction::UMOUNT),
1654                PasswordUsage::CRYPTSETUP(PasswordKind::SUDO),
1655            )
1656        } else {
1657            let Some(Mountable::Encrypted(device)) = &self.menu.mount.selected() else {
1658                log_info!("Cannot find Encrypted device");
1659                return Ok(());
1660            };
1661            let success =
1662                device.umount_close_crypto(&current_username()?, &mut self.menu.password_holder)?;
1663            log_info!("umount_encrypted_drive: {success}");
1664            Ok(())
1665        }
1666    }
1667    /// Mount the selected encrypted device. Will ask first for sudo password and
1668    /// passphrase.
1669    /// Those passwords are always dropped immediatly after the commands are run.
1670    pub fn mount_normal_device(&mut self) -> Result<()> {
1671        let Some(device) = self.menu.mount.selected() else {
1672            return Ok(());
1673        };
1674        if device.is_mounted() {
1675            return Ok(());
1676        }
1677        if device.is_crypto() {
1678            return self.mount_encrypted_drive();
1679        }
1680        let Ok(success) = self.menu.mount.mount_selected_no_password() else {
1681            return Ok(());
1682        };
1683        if success {
1684            self.menu.mount.update(self.internal_settings.disks())?;
1685            self.go_to_normal_drive()?;
1686            return Ok(());
1687        }
1688        if !self.menu.password_holder.has_sudo() {
1689            self.ask_password(Some(MountAction::MOUNT), PasswordUsage::DEVICE)
1690        } else {
1691            if let Ok(true) = self
1692                .menu
1693                .mount
1694                .mount_selected(&mut self.menu.password_holder)
1695            {
1696                self.go_to_normal_drive()?;
1697            }
1698            Ok(())
1699        }
1700    }
1701
1702    /// Move to the selected device mount point.
1703    pub fn go_to_normal_drive(&mut self) -> Result<()> {
1704        let Some(path) = self.menu.mount.selected_mount_point() else {
1705            return Ok(());
1706        };
1707        let tab = self.current_tab_mut();
1708        tab.cd(&path)?;
1709        tab.refresh_view()
1710    }
1711
1712    /// Move to the mount point based on its onscreen index
1713    pub fn go_to_mount_per_index(&mut self, c: char) -> Result<()> {
1714        let Some(index) = c.to_digit(10) else {
1715            return Ok(());
1716        };
1717        self.menu.mount.set_index(index.saturating_sub(1) as _);
1718        self.go_to_normal_drive()
1719    }
1720
1721    fn go_to_encrypted_drive(&mut self, uuid: Option<String>) -> Result<()> {
1722        self.menu.mount.update(&self.internal_settings.disks)?;
1723        let Some(mountpoint) = self.menu.mount.find_encrypted_by_uuid(uuid) else {
1724            return Ok(());
1725        };
1726        log_info!("mountpoint {mountpoint}");
1727        let tab = self.current_tab_mut();
1728        tab.cd(Path::new(&mountpoint))?;
1729        tab.refresh_view()
1730    }
1731
1732    /// Unmount the selected device.
1733    /// Will ask first for a sudo password which is immediatly forgotten.
1734    pub fn umount_normal_device(&mut self) -> Result<()> {
1735        let Some(device) = self.menu.mount.selected() else {
1736            return Ok(());
1737        };
1738        if !device.is_mounted() {
1739            return Ok(());
1740        }
1741        if device.is_crypto() {
1742            return self.umount_encrypted_drive();
1743        }
1744        let Ok(success) = self.menu.mount.umount_selected_no_password() else {
1745            return Ok(());
1746        };
1747        if success {
1748            self.menu.mount.update(self.internal_settings.disks())?;
1749            return Ok(());
1750        }
1751        if !self.menu.password_holder.has_sudo() {
1752            self.ask_password(Some(MountAction::UMOUNT), PasswordUsage::DEVICE)
1753        } else {
1754            self.menu
1755                .mount
1756                .umount_selected(&mut self.menu.password_holder)
1757        }
1758    }
1759
1760    /// Ejects a removable device (usb key, mtp etc.).
1761    pub fn eject_removable_device(&mut self) -> Result<()> {
1762        if self.menu.mount.is_empty() {
1763            return Ok(());
1764        }
1765        let success = self.menu.mount.eject_removable_device()?;
1766        if success {
1767            self.menu.mount.update(self.internal_settings.disks())?;
1768        }
1769        Ok(())
1770    }
1771
1772    /// Reads and parse a shell command. Some arguments may be expanded.
1773    /// See [`crate::modes::shell_command_parser`] for more information.
1774    pub fn execute_shell_command_from_input(&mut self) -> Result<bool> {
1775        let shell_command = self.menu.input.string();
1776        self.execute_shell_command(shell_command, None, true)
1777    }
1778
1779    /// Parse and execute a shell command and expand tokens like %s, %t etc.
1780    pub fn execute_shell_command(
1781        &mut self,
1782        shell_command: String,
1783        files: Option<Vec<String>>,
1784        capture_output: bool,
1785    ) -> Result<bool> {
1786        let command = append_files_to_shell_command(shell_command, files);
1787        let Ok(args) = shell_command_parser(&command, self) else {
1788            self.set_menu_mode(self.index, Menu::Nothing)?;
1789            return Ok(true);
1790        };
1791        self.execute_parsed_command(args, command, capture_output)
1792    }
1793
1794    fn execute_parsed_command(
1795        &mut self,
1796        mut args: Vec<String>,
1797        shell_command: String,
1798        capture_output: bool,
1799    ) -> Result<bool> {
1800        let executable = args.remove(0);
1801        if is_sudo_command(&executable) {
1802            self.enter_sudo_mode(shell_command)?;
1803            return Ok(false);
1804        }
1805        let params: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
1806        if executable == *SAME_WINDOW_TOKEN {
1807            self.internal_settings.open_in_window(&params)?;
1808            return Ok(true);
1809        }
1810        if !is_in_path(&executable) {
1811            log_line!("{executable} isn't in path.");
1812            return Ok(true);
1813        }
1814        if capture_output {
1815            match execute_and_capture_output(executable, &params) {
1816                Ok(output) => self.preview_command_output(output, shell_command),
1817                Err(e) => {
1818                    log_info!("Error {e:?}");
1819                    log_line!("Command {shell_command} disn't finish properly");
1820                }
1821            }
1822            Ok(true)
1823        } else {
1824            let _ = execute_without_output(executable, &params);
1825            Ok(true)
1826        }
1827    }
1828
1829    fn enter_sudo_mode(&mut self, shell_command: String) -> Result<()> {
1830        self.menu.sudo_command = Some(shell_command);
1831        self.ask_password(None, PasswordUsage::SUDOCOMMAND)?;
1832        Ok(())
1833    }
1834
1835    /// Ask for a password of some kind (sudo or device passphrase).
1836    fn ask_password(
1837        &mut self,
1838        encrypted_action: Option<MountAction>,
1839        password_dest: PasswordUsage,
1840    ) -> Result<()> {
1841        log_info!("ask_password");
1842        self.set_menu_mode(
1843            self.index,
1844            Menu::InputSimple(InputSimple::Password(encrypted_action, password_dest)),
1845        )
1846    }
1847
1848    /// Attach the typed password to the correct receiver and
1849    /// execute the command requiring a password.
1850    pub fn execute_password_command(
1851        &mut self,
1852        action: Option<MountAction>,
1853        dest: PasswordUsage,
1854    ) -> Result<()> {
1855        let password = self.menu.input.string();
1856        self.menu.input.reset();
1857        if matches!(dest, PasswordUsage::CRYPTSETUP(PasswordKind::CRYPTSETUP)) {
1858            self.menu.password_holder.set_cryptsetup(password)
1859        } else {
1860            self.menu.password_holder.set_sudo(password)
1861        };
1862        let sudo_command = self.menu.sudo_command.to_owned();
1863        self.reset_menu_mode()?;
1864        self.dispatch_password(action, dest, sudo_command)?;
1865        Ok(())
1866    }
1867
1868    /// Execute a new mark, saving it to a config file for futher use.
1869    pub fn marks_new(&mut self, c: char) -> Result<()> {
1870        let path = self.current_tab_mut().directory.path.clone();
1871        self.menu.marks.new_mark(c, &path)?;
1872        self.current_tab_mut().refresh_view()?;
1873        self.reset_menu_mode()?;
1874        self.refresh_status()
1875    }
1876
1877    /// Execute a jump to a mark, moving to a valid path.
1878    /// If the saved path is invalid, it does nothing but reset the view.
1879    pub fn marks_jump_char(&mut self, c: char) -> Result<()> {
1880        if let Some(path) = self.menu.marks.get(c) {
1881            self.current_tab_mut().cd(&path)?;
1882        }
1883        self.current_tab_mut().refresh_view()?;
1884        self.reset_menu_mode()?;
1885        self.refresh_status()
1886    }
1887
1888    /// Execute a new temp mark, saving it temporary for futher use.
1889    pub fn temp_marks_new(&mut self, c: char) -> Result<()> {
1890        let Some(index) = c.to_digit(10) else {
1891            return Ok(());
1892        };
1893        let path = self.current_tab_mut().directory.path.to_path_buf();
1894        self.menu.temp_marks.set_mark(index as _, path);
1895        self.current_tab_mut().refresh_view()?;
1896        self.reset_menu_mode()?;
1897        self.refresh_status()
1898    }
1899
1900    /// Erase the current mark
1901    pub fn temp_marks_erase(&mut self) -> Result<()> {
1902        self.menu.temp_marks.erase_current_mark();
1903        Ok(())
1904    }
1905
1906    /// Execute a jump to a temporay mark, moving to a valid path.
1907    /// If the saved path is invalid, it does nothing but reset the view.
1908    pub fn temp_marks_jump_char(&mut self, c: char) -> Result<()> {
1909        let Some(index) = c.to_digit(10) else {
1910            return Ok(());
1911        };
1912        if let Some(path) = self.menu.temp_marks.get_mark(index as _) {
1913            self.tabs[self.index].cd(path)?;
1914        }
1915        self.current_tab_mut().refresh_view()?;
1916        self.reset_menu_mode()?;
1917        self.refresh_status()
1918    }
1919
1920    /// Recursively delete all flagged files.
1921    /// If we try to delete the current root of the tab, nothing is deleted but a warning is displayed.
1922    pub fn confirm_delete_files(&mut self) -> Result<()> {
1923        if self.menu.flagged.contains(self.current_tab().root_path()) {
1924            log_info!("Can't delete current root path");
1925            log_line!("Can't delete current root path");
1926            return Ok(());
1927        }
1928        self.menu.delete_flagged_files()?;
1929        self.reset_menu_mode()?;
1930        self.clear_flags_and_reset_view()?;
1931        self.refresh_status()
1932    }
1933
1934    /// Empty the trash folder permanently.
1935    pub fn confirm_trash_empty(&mut self) -> Result<()> {
1936        self.menu.trash.empty_trash()?;
1937        self.reset_menu_mode()?;
1938        self.clear_flags_and_reset_view()?;
1939        Ok(())
1940    }
1941
1942    /// Ask the new filenames and set the confirmation mode.
1943    pub fn bulk_ask_filenames(&mut self) -> Result<()> {
1944        let flagged = self.flagged_in_current_dir();
1945        let current_path = self.current_tab_path_str();
1946        self.menu.bulk.ask_filenames(flagged, &current_path)?;
1947        if let Some(temp_file) = self.menu.bulk.temp_file() {
1948            self.open_single_file(&temp_file)?;
1949            if self.internal_settings.opener.extension_use_term("txt") {
1950                self.fm_sender.send(FmEvents::BulkExecute)?;
1951            } else {
1952                self.menu.bulk.watch_in_thread(self.fm_sender.clone())?;
1953            }
1954        }
1955        Ok(())
1956    }
1957
1958    /// Execute a bulk create / rename
1959    pub fn bulk_execute(&mut self) -> Result<()> {
1960        self.menu.bulk.get_new_names()?;
1961        self.set_menu_mode(
1962            self.index,
1963            Menu::NeedConfirmation(NeedConfirmation::BulkAction),
1964        )?;
1965        Ok(())
1966    }
1967
1968    /// Execute the bulk action.
1969    pub fn confirm_bulk_action(&mut self) -> Result<()> {
1970        if let (Some(renamed), Some(created)) = self.menu.bulk.execute()? {
1971            for (old_path, new_path) in &renamed {
1972                log_info!("old_path {old_path:?} -> new_path {new_path:?}");
1973                self.rename_marks(old_path, new_path)?;
1974            }
1975            self.menu
1976                .flagged
1977                .update(renamed.into_iter().map(|(_, p)| p).collect());
1978            self.menu.flagged.extend(created);
1979        } else {
1980            self.menu.flagged.clear();
1981        };
1982        self.reset_menu_mode()?;
1983        self.reset_tabs_view()?;
1984        Ok(())
1985    }
1986
1987    fn run_sudo_command(&mut self, sudo_command: Option<String>) -> Result<()> {
1988        let Some(sudo_command) = sudo_command else {
1989            log_info!("No sudo_command received from args.");
1990            return self.menu.clear_sudo_attributes();
1991        };
1992        self.set_menu_mode(self.index, Menu::Nothing)?;
1993        reset_sudo_faillock()?;
1994        let Some(command) = sudo_command.strip_prefix("sudo ") else {
1995            log_info!("run_sudo_command cannot run {sudo_command}. It doesn't start with 'sudo '");
1996            return self.menu.clear_sudo_attributes();
1997        };
1998        let args = shell_command_parser(command, self)?;
1999        if args.is_empty() {
2000            return self.menu.clear_sudo_attributes();
2001        }
2002        let Some(password) = self.menu.password_holder.sudo() else {
2003            log_info!("run_sudo_command password isn't set");
2004            return self.menu.clear_sudo_attributes();
2005        };
2006        let directory_of_selected = self.current_tab().directory_of_selected()?;
2007        let (success, stdout, stderr) =
2008            execute_sudo_command_with_password(&args, &password, directory_of_selected)?;
2009        log_info!("sudo command execution. success: {success}");
2010        self.menu.clear_sudo_attributes()?;
2011        if !success {
2012            log_line!("sudo command failed: {stderr}");
2013        }
2014        self.preview_command_output(stdout, sudo_command.to_owned());
2015        Ok(())
2016    }
2017
2018    /// Dispatch the known password depending of which component set
2019    /// the `PasswordUsage`.
2020    #[rustfmt::skip]
2021    pub fn dispatch_password(
2022        &mut self,
2023        action: Option<MountAction>,
2024        dest: PasswordUsage,
2025        sudo_command: Option<String>,
2026    ) -> Result<()> {
2027        match (dest, action) {
2028            (PasswordUsage::ISO,            Some(MountAction::MOUNT))  => self.mount_iso_drive(),
2029            (PasswordUsage::ISO,            Some(MountAction::UMOUNT)) => self.umount_iso_drive(),
2030            (PasswordUsage::CRYPTSETUP(_),  Some(MountAction::MOUNT))  => self.mount_encrypted_drive(),
2031            (PasswordUsage::CRYPTSETUP(_),  Some(MountAction::UMOUNT)) => self.umount_encrypted_drive(),
2032            (PasswordUsage::DEVICE,         Some(MountAction::MOUNT))  => self.mount_normal_device(),
2033            (PasswordUsage::DEVICE,         Some(MountAction::UMOUNT)) => self.umount_normal_device(),
2034            (PasswordUsage::SUDOCOMMAND,    _)                         => self.run_sudo_command(sudo_command),
2035            (_,                             _)                         => Ok(()),
2036        }
2037    }
2038
2039    /// Set the display to preview a command output
2040    pub fn preview_command_output(&mut self, output: String, command: String) {
2041        log_info!("preview_command_output for {command}:\n{output}");
2042        if output.is_empty() {
2043            return;
2044        }
2045        let _ = self.reset_menu_mode();
2046        self.current_tab_mut().set_display_mode(Display::Preview);
2047        let preview = PreviewBuilder::cli_info(&output, command);
2048        if let Preview::Text(text) = &preview {
2049            log_info!("preview is Text with: {text:?}");
2050        } else {
2051            log_info!("preview is empty ? {empty}", empty = preview.is_empty());
2052        }
2053        self.current_tab_mut().window.reset(preview.len());
2054        self.current_tab_mut().preview = preview;
2055    }
2056
2057    /// Set the nvim listen address from what the user typed.
2058    pub fn update_nvim_listen_address(&mut self) {
2059        self.internal_settings.update_nvim_listen_address()
2060    }
2061
2062    /// Execute a command requiring a confirmation (Delete, Move or Copy).
2063    /// The action is only executed if the user typed the char `y`
2064    pub fn confirm(&mut self, c: char, confirmed_action: NeedConfirmation) -> Result<()> {
2065        if c == 'y' {
2066            if let Ok(must_leave) = self.match_confirmed_mode(confirmed_action) {
2067                if must_leave {
2068                    return Ok(());
2069                }
2070            }
2071        }
2072        self.reset_menu_mode()?;
2073        self.current_tab_mut().refresh_view()?;
2074
2075        Ok(())
2076    }
2077
2078    /// Execute a `NeedConfirmation` action (delete, move, copy, empty trash)
2079    fn match_confirmed_mode(&mut self, confirmed_action: NeedConfirmation) -> Result<bool> {
2080        match confirmed_action {
2081            NeedConfirmation::Delete => self.confirm_delete_files(),
2082            NeedConfirmation::Move => self.cut_or_copy_flagged_files(CopyMove::Move),
2083            NeedConfirmation::Copy => self.cut_or_copy_flagged_files(CopyMove::Copy),
2084            NeedConfirmation::EmptyTrash => self.confirm_trash_empty(),
2085            NeedConfirmation::BulkAction => self.confirm_bulk_action(),
2086            NeedConfirmation::DeleteCloud => {
2087                self.cloud_confirm_delete()?;
2088                return Ok(true);
2089            }
2090        }?;
2091        Ok(false)
2092    }
2093
2094    /// Execute an action when the header line was clicked.
2095    pub fn header_action(&mut self, col: u16, binds: &Bindings) -> Result<()> {
2096        if self.current_tab().display_mode.is_preview() {
2097            return Ok(());
2098        }
2099        Header::new(self, self.current_tab())?
2100            .action(col, !self.focus.is_left())
2101            .matcher(self, binds)
2102    }
2103
2104    /// Execute an action when the footer line was clicked.
2105    pub fn footer_action(&mut self, col: u16, binds: &Bindings) -> Result<()> {
2106        log_info!("footer clicked col {col}");
2107        let is_right = self.index == 1;
2108        let action = match self.current_tab().display_mode {
2109            Display::Preview => return Ok(()),
2110            Display::Tree | Display::Directory => {
2111                let footer = Footer::new(self, self.current_tab())?;
2112                footer.action(col, is_right).to_owned()
2113            }
2114            Display::Fuzzy => return Ok(()),
2115        };
2116        log_info!("action: {action}");
2117        action.matcher(self, binds)
2118    }
2119
2120    /// Change permission of the flagged files.
2121    /// Once the user has typed an octal permission like 754, it's applied to
2122    /// the file.
2123    /// Nothing is done if the user typed nothing or an invalid permission like
2124    /// 955.
2125    pub fn chmod(&mut self) -> Result<()> {
2126        if self.menu.input.is_empty() || self.menu.flagged.is_empty() {
2127            return Ok(());
2128        }
2129        let input_permission = &self.menu.input.string();
2130        Permissions::set_permissions_of_flagged(input_permission, &self.menu.flagged)?;
2131        self.reset_tabs_view()
2132    }
2133
2134    /// Enter the chmod mode where user can chmod a file.
2135    pub fn set_mode_chmod(&mut self) -> Result<()> {
2136        if self.current_tab_mut().directory.is_empty() {
2137            return Ok(());
2138        }
2139        if self.menu.flagged.is_empty() {
2140            self.toggle_flag_for_selected();
2141        }
2142        self.set_menu_mode(self.index, Menu::InputSimple(InputSimple::Chmod))?;
2143        self.menu.replace_input_by_permissions();
2144        Ok(())
2145    }
2146
2147    /// Execute a custom event on the selected file
2148    pub fn run_custom_command(&mut self, string: &str) -> Result<()> {
2149        self.execute_shell_command(string.to_owned(), None, true)?;
2150        Ok(())
2151    }
2152
2153    /// Compress the flagged files into an archive.
2154    /// Compression method is chosen by the user.
2155    /// The archive is created in the current directory and is named "archive.tar.??" or "archive.zip".
2156    /// Files which are above the CWD are filtered out since they can't be added to an archive.
2157    /// Archive creation depends on CWD so we ensure it's set to the selected tab.
2158    pub fn compress(&mut self) -> Result<()> {
2159        let here = &self.current_tab().directory.path;
2160        set_current_dir(here)?;
2161        let files_with_relative_paths = self.flagged_or_selected_relative_to(here);
2162        if files_with_relative_paths.is_empty() {
2163            return Ok(());
2164        }
2165        match self
2166            .menu
2167            .compression
2168            .compress(files_with_relative_paths, here)
2169        {
2170            Ok(()) => (),
2171            Err(error) => log_info!("Error compressing files. Error: {error}"),
2172        }
2173        Ok(())
2174    }
2175
2176    /// Sort all file in a tab with a sort key which is selected according to which char was pressed.
2177    pub fn sort_by_char(&mut self, c: char) -> Result<()> {
2178        self.current_tab_mut().sort(c)?;
2179        self.menu.reset();
2180        self.set_height_for_menu_mode(self.index, Menu::Nothing)?;
2181        self.tabs[self.index].menu_mode = Menu::Nothing;
2182        let len = self.menu.len(Menu::Nothing);
2183        let height = self.second_window_height()?;
2184        self.menu.window = ContentWindow::new(len, height);
2185        self.focus = self.focus.to_parent();
2186        Ok(())
2187    }
2188
2189    /// The width of a displayed canvas.
2190    pub fn canvas_width(&self) -> Result<u16> {
2191        let full_width = self.term_width();
2192        if self.session.dual() && full_width >= MIN_WIDTH_FOR_DUAL_PANE {
2193            Ok(full_width / 2)
2194        } else {
2195            Ok(full_width)
2196        }
2197    }
2198
2199    /// Executes a search in current folder, selecting the first file matching
2200    /// the current completion proposition.
2201    /// ie. If you typed `"jpg"` before, it will move to the first file
2202    /// whose filename contains `"jpg"`.
2203    /// The current order of files is used.
2204    fn search(&mut self) -> Result<()> {
2205        let Some(search) = self.build_search_from_input() else {
2206            self.current_tab_mut().search = Search::empty();
2207            return Ok(());
2208        };
2209        self.search_and_update(search)
2210    }
2211
2212    fn build_search_from_input(&self) -> Option<Search> {
2213        let searched = &self.menu.input.string();
2214        if searched.is_empty() {
2215            return None;
2216        }
2217        Search::new(searched).ok()
2218    }
2219
2220    fn search_and_update(&mut self, mut search: Search) -> Result<()> {
2221        search.execute_search(self.current_tab_mut())?;
2222        self.current_tab_mut().search = search;
2223        self.update_second_pane_for_preview()
2224    }
2225
2226    fn search_again(&mut self) -> Result<()> {
2227        let search = self.current_tab().search.clone_with_regex();
2228        self.search_and_update(search)
2229    }
2230
2231    /// Set a new filter.
2232    /// Doesn't reset the input.
2233    pub fn filter(&mut self) -> Result<()> {
2234        let filter = FilterKind::from_input(&self.menu.input.string());
2235        self.current_tab_mut().set_filter(filter)?;
2236        self.search_again()
2237    }
2238
2239    /// input the typed char and update the filterkind.
2240    pub fn input_filter(&mut self, c: char) -> Result<()> {
2241        self.menu.input_insert(c)?;
2242        self.filter()
2243    }
2244
2245    /// Open the picker menu. Does nothing if already in a picker.
2246    pub fn open_picker(&mut self) -> Result<()> {
2247        if self.current_tab().menu_mode.is_picker() || self.menu.input_history.filtered_is_empty() {
2248            return Ok(());
2249        }
2250        let menu = self.current_tab().menu_mode;
2251        let content = self.menu.input_history.filtered_as_list();
2252        self.menu.picker.set(
2253            Some(PickerCaller::Menu(menu)),
2254            menu.name_for_picker(),
2255            content,
2256        );
2257
2258        self.set_menu_mode(self.index, Menu::Navigate(Navigate::Picker))
2259    }
2260
2261    /// Load the selected cloud configuration file from the config folder and open navigation the menu.
2262    pub fn cloud_load_config(&mut self) -> Result<()> {
2263        let Some(picked) = self.menu.picker.selected() else {
2264            log_info!("nothing selected");
2265            return Ok(());
2266        };
2267        let Ok(cloud) = google_drive(picked) else {
2268            log_line!("Invalid config file {picked}");
2269            return Ok(());
2270        };
2271        self.menu.cloud = cloud;
2272        self.set_menu_mode(self.index, Menu::Navigate(Navigate::Cloud))
2273    }
2274
2275    /// Open the cloud menu.
2276    /// If no cloud has been selected yet, all cloud config file will be displayed.
2277    /// if a cloud has been selected, it will open it.
2278    pub fn cloud_open(&mut self) -> Result<()> {
2279        if self.menu.cloud.is_set() {
2280            self.set_menu_mode(self.index, Menu::Navigate(Navigate::Cloud))
2281        } else {
2282            self.cloud_picker()
2283        }
2284    }
2285
2286    fn cloud_picker(&mut self) -> Result<()> {
2287        let content = get_cloud_token_names()?;
2288        self.menu.picker.set(
2289            Some(PickerCaller::Cloud),
2290            Some("Pick a cloud provider".to_owned()),
2291            content,
2292        );
2293        self.set_menu_mode(self.index, Menu::Navigate(Navigate::Picker))
2294    }
2295
2296    /// Disconnect from the current cloud and open the picker
2297    pub fn cloud_disconnect(&mut self) -> Result<()> {
2298        self.menu.cloud.disconnect();
2299        self.cloud_open()
2300    }
2301
2302    /// Enter the delete mode and ask confirmation.
2303    /// Only the currently selected file can be deleted.
2304    pub fn cloud_enter_delete_mode(&mut self) -> Result<()> {
2305        self.set_menu_mode(
2306            self.index,
2307            Menu::NeedConfirmation(NeedConfirmation::DeleteCloud),
2308        )
2309    }
2310
2311    /// Delete the selected file once a confirmation has been received from the user.
2312    pub fn cloud_confirm_delete(&mut self) -> Result<()> {
2313        self.menu.cloud.delete()?;
2314        self.set_menu_mode(self.index, Menu::Navigate(Navigate::Cloud))?;
2315        self.menu.cloud.refresh_current()?;
2316        self.menu.window.scroll_to(self.menu.cloud.index);
2317        Ok(())
2318    }
2319
2320    /// Update the metadata of the current file.
2321    pub fn cloud_update_metadata(&mut self) -> Result<()> {
2322        self.menu.cloud.update_metadata()
2323    }
2324
2325    /// Ask the user to enter a name for the new directory.
2326    pub fn cloud_enter_newdir_mode(&mut self) -> Result<()> {
2327        self.set_menu_mode(self.index, Menu::InputSimple(InputSimple::CloudNewdir))?;
2328        self.refresh_view()
2329    }
2330
2331    /// Create the new directory in current path with the name the user entered.
2332    pub fn cloud_create_newdir(&mut self, dirname: String) -> Result<()> {
2333        self.menu.cloud.create_newdir(dirname)?;
2334        self.menu.cloud.refresh_current()
2335    }
2336
2337    fn get_normal_selected_file(&self) -> Option<FileInfo> {
2338        let local_file = self.tabs[self.index].current_file().ok()?;
2339        match local_file.file_kind {
2340            FileKind::NormalFile => Some(local_file),
2341            _ => None,
2342        }
2343    }
2344
2345    /// Upload the current file (tree or directory mode) the the current remote path.
2346    pub fn cloud_upload_selected_file(&mut self) -> Result<()> {
2347        let Some(local_file) = self.get_normal_selected_file() else {
2348            log_line!("Can only upload normal files.");
2349            return Ok(());
2350        };
2351        self.menu.cloud.upload(&local_file)?;
2352        self.menu.cloud.refresh_current()
2353    }
2354
2355    /// Enter a file (download it) or the directory (explore it).
2356    pub fn cloud_enter_file_or_dir(&mut self) -> Result<()> {
2357        if let Some(entry) = self.menu.cloud.selected() {
2358            match entry.metadata().mode() {
2359                EntryMode::Unknown => (),
2360                EntryMode::FILE => self
2361                    .menu
2362                    .cloud
2363                    .download(self.current_tab().directory_of_selected()?)?,
2364                EntryMode::DIR => {
2365                    self.menu.cloud.enter_selected()?;
2366                    self.cloud_set_content_window_len()?;
2367                }
2368            };
2369        };
2370        Ok(())
2371    }
2372
2373    fn cloud_set_content_window_len(&mut self) -> Result<()> {
2374        let len = self.menu.cloud.content.len();
2375        let height = self.second_window_height()?;
2376        self.menu.window = ContentWindow::new(len, height);
2377        Ok(())
2378    }
2379
2380    /// Move to the parent folder if possible.
2381    /// Nothing is done in the root folder.
2382    pub fn cloud_move_to_parent(&mut self) -> Result<()> {
2383        self.menu.cloud.move_to_parent()?;
2384        self.cloud_set_content_window_len()?;
2385        Ok(())
2386    }
2387
2388    pub fn toggle_flag_visual(&mut self) {
2389        if self.current_tab().visual {
2390            let Some(path) = self.current_tab().selected_path() else {
2391                return;
2392            };
2393            self.menu.flagged.toggle(&path)
2394        }
2395    }
2396
2397    /// Parse and execute the received IPC message.
2398    /// IPC message can currently be of 3 forms:
2399    /// - `GO <path>` -> cd to this path and select the file.
2400    /// - `KEY <key>` -> act as if the key was pressed. `<key>` should be formated like in the config file.
2401    /// - `ACTION <action>` -> execute the action. Similar to `:<action><Enter>`. `<action>` should be formated like in the config file.
2402    ///
2403    /// Other messages are ignored.
2404    /// Failed messages (path don't exists, wrongly formated key or action) are ignored silently.
2405    pub fn parse_ipc(&mut self, msg: String) -> Result<()> {
2406        let mut split = msg.split_whitespace();
2407        match split.next() {
2408            Some("GO") => {
2409                log_info!("Received IPC command GO");
2410                if let Some(dest) = split.next() {
2411                    log_info!("Received IPC command GO to {dest}");
2412                    let dest = tilde(dest);
2413                    self.current_tab_mut().cd_to_file(dest.as_ref())?
2414                }
2415            }
2416            Some("KEY") => {
2417                log_info!("Received IPC command KEY");
2418                if let Some(keyname) = split.next() {
2419                    if let Some(key) = from_keyname(keyname) {
2420                        self.fm_sender.send(FmEvents::Term(Event::Key(key)))?;
2421                        log_info!("Sent key event: {key:?}");
2422                    }
2423                }
2424            }
2425            Some("ACTION") => {
2426                log_info!("Received IPC command ACTION");
2427                if let Some(action_str) = split.next() {
2428                    if let Ok(action) = ActionMap::from_str(action_str) {
2429                        log_info!("Sent action event: {action:?}");
2430                        self.fm_sender.send(FmEvents::Action(action))?;
2431                    }
2432                }
2433            }
2434            Some(_unknown) => log_info!("Received unknown IPC command: {msg}"),
2435            None => (),
2436        };
2437        Ok(())
2438    }
2439
2440    /// Update marks & temps marks from old path to new path.
2441    /// Does nothing if the oldpath wasn't marked.
2442    pub fn rename_marks<P>(&mut self, old_path: &Path, new_path: P) -> Result<()>
2443    where
2444        P: AsRef<Path>,
2445    {
2446        log_info!("{old_path:?} -> {new_path:?}", new_path = new_path.as_ref());
2447        self.menu.temp_marks.move_path(old_path, new_path.as_ref());
2448        self.menu.marks.move_path(old_path, new_path.as_ref())
2449    }
2450}
2451
2452fn find_keybind_from_fuzzy(line: &str) -> Result<KeyEvent> {
2453    let Some(keybind) = line.split(':').next() else {
2454        bail!("No keybind found");
2455    };
2456    let Some(key) = from_keyname(keybind.trim()) else {
2457        bail!("{keybind} isn't a valid Key name.");
2458    };
2459    Ok(key)
2460}