fm/app/
status.rs

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