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