fm/modes/utils/
menu_holder.rs

1use std::os::unix::fs::MetadataExt;
2
3use anyhow::Result;
4use clap::Parser;
5use ratatui::layout::Rect;
6use ratatui::Frame;
7
8use crate::app::Tab;
9use crate::common::{index_from_a, INPUT_HISTORY_PATH};
10use crate::config::Bindings;
11use crate::io::{drop_sudo_privileges, InputHistory, OpendalContainer};
12use crate::io::{Args, DrawMenu};
13use crate::log_line;
14use crate::modes::{
15    nvim_inform_ipc, Bulk, CliApplications, Completion, Compresser, ContentWindow, ContextMenu,
16    Flagged, History, Input, InputCompleted, IsoDevice, Marks, Menu, Mount, Navigate,
17    NeedConfirmation, NvimIPCAction, PasswordHolder, Picker, Remote, Selectable, Shortcut,
18    TempMarks, Trash, TuiApplications, MAX_FILE_MODE,
19};
20
21macro_rules! impl_navigate_from_char {
22    ($name:ident, $field:ident) => {
23        #[doc = concat!(
24         "Navigates to the index in the `",
25         stringify!($field),
26         "` field based on the given character."
27                                                                                                )]
28        pub fn $name(&mut self, c: char) -> bool {
29            let Some(index) = index_from_a(c) else {
30                return false;
31            };
32            if index < self.$field.len() {
33                self.$field.set_index(index);
34                self.window.scroll_to(index);
35                return true;
36            }
37            false
38        }
39    };
40}
41
42/// Holds almost every menu except for the history, which is tab specific.
43/// Only one instance is created and hold by status.
44/// It acts as an interface for basic methods (navigation, length, completion) etc.
45/// it also keeps track of the content window and user input (in menus only, not for the fuzzy finder).
46///
47/// The poor choices of architecture forced the creation of such a monster.
48/// For instance, even if you never use marks or cloud, their instance is saved here,
49/// waisting ressources.
50///
51/// Building them lazylly is on the todo list.
52pub struct MenuHolder {
53    /// Window for scrollable menus
54    pub window: ContentWindow,
55    /// Bulk rename
56    pub bulk: Bulk,
57    /// CLI applications
58    pub cli_applications: CliApplications,
59    /// cloud
60    pub cloud: OpendalContainer,
61    /// Completion list and index in it.
62    pub completion: Completion,
63    /// Compression methods
64    pub compression: Compresser,
65    /// Cotext menu
66    pub context: ContextMenu,
67    /// The flagged files
68    pub flagged: Flagged,
69    /// The typed input by the user
70    pub input: Input,
71    /// The user input history.
72    pub input_history: InputHistory,
73    /// Iso mounter. Set to None by default, dropped ASAP
74    pub iso_device: Option<IsoDevice>,
75    /// Marks allows you to jump to a save mark
76    pub marks: Marks,
77    /// Temporary marks allows you to jump to a save mark
78    pub temp_marks: TempMarks,
79    /// Hold password between their typing and usage
80    pub password_holder: PasswordHolder,
81    /// basic picker
82    pub picker: Picker,
83    /// Predefined shortcuts
84    pub shortcut: Shortcut,
85    /// TUI application
86    pub tui_applications: TuiApplications,
87    /// The trash
88    pub trash: Trash,
89    /// Last sudo command ran
90    pub sudo_command: Option<String>,
91    /// History - here for compatibility reasons only
92    pub history: History,
93    /// mounts
94    pub mount: Mount,
95}
96
97impl MenuHolder {
98    pub fn new(start_dir: &std::path::Path, binds: &Bindings) -> Result<Self> {
99        Ok(Self {
100            bulk: Bulk::default(),
101            cli_applications: CliApplications::default(),
102            cloud: OpendalContainer::default(),
103            completion: Completion::default(),
104            compression: Compresser::default(),
105            context: ContextMenu::default(),
106            flagged: Flagged::default(),
107            history: History::default(),
108            input: Input::default(),
109            input_history: InputHistory::load(INPUT_HISTORY_PATH)?,
110            iso_device: None,
111            marks: Marks::default(),
112            password_holder: PasswordHolder::default(),
113            picker: Picker::default(),
114            shortcut: Shortcut::empty(start_dir),
115            sudo_command: None,
116            temp_marks: TempMarks::default(),
117            trash: Trash::new(binds)?,
118            tui_applications: TuiApplications::default(),
119            window: ContentWindow::default(),
120            mount: Mount::default(),
121        })
122    }
123
124    pub fn reset(&mut self) {
125        self.input.reset();
126        self.completion.reset();
127        self.bulk.reset();
128        self.sudo_command = None;
129    }
130
131    pub fn resize(&mut self, menu_mode: Menu, height: usize) {
132        self.window.set_height(height);
133        if let Menu::Navigate(_) = menu_mode {
134            self.window.scroll_to(self.index(menu_mode))
135        }
136    }
137
138    /// Replace the current input by the permission of the first flagged file as an octal value.
139    /// If the flagged has permission "rwxrw.r.." or 764 in octal, "764" will be the current input.
140    /// Only the last 3 octal digits are kept.
141    /// Nothing is done if :
142    /// - there's no flagged file,
143    /// - can't read the metadata of the first flagged file.
144    ///
145    /// It should never happen.
146    pub fn replace_input_by_permissions(&mut self) {
147        let Some(flagged) = &self.flagged.content.first() else {
148            return;
149        };
150        let Ok(metadata) = flagged.metadata() else {
151            return;
152        };
153        let mode = metadata.mode() & MAX_FILE_MODE;
154        self.input.replace(&format!("{mode:o}"));
155    }
156
157    /// Fill the input string with the currently selected completion.
158    pub fn input_complete(&mut self, tab: &mut Tab) -> Result<()> {
159        self.fill_completion(tab);
160        self.window.reset(self.completion.len());
161        Ok(())
162    }
163
164    fn fill_completion(&mut self, tab: &mut Tab) {
165        match tab.menu_mode {
166            Menu::InputCompleted(InputCompleted::Cd) => self.completion.cd(
167                tab.current_directory_path()
168                    .as_os_str()
169                    .to_string_lossy()
170                    .as_ref(),
171                &self.input.string(),
172            ),
173            Menu::InputCompleted(InputCompleted::Exec) => {
174                self.completion.exec(&self.input.string())
175            }
176            Menu::InputCompleted(InputCompleted::Search) => {
177                self.completion.search(tab.completion_search_files());
178            }
179            Menu::InputCompleted(InputCompleted::Action) => {
180                self.completion.action(&self.input.string())
181            }
182            _ => (),
183        }
184    }
185
186    /// Run sshfs with typed parameters to mount a remote directory in current directory.
187    /// sshfs should be reachable in path.
188    /// The user must type 3 arguments like this : `username hostname remote_path`.
189    /// If the user doesn't provide 3 arguments,
190    pub fn mount_remote(&mut self, current_path: &str) {
191        let input = self.input.string();
192        if let Some(remote_builder) = Remote::from_input(input, current_path) {
193            remote_builder.mount();
194        }
195        self.input.reset();
196    }
197
198    /// Remove a flag file from Jump mode
199    pub fn remove_selected_flagged(&mut self) -> Result<()> {
200        self.flagged.remove_selected();
201        Ok(())
202    }
203
204    pub fn trash_delete_permanently(&mut self) -> Result<()> {
205        self.trash.delete_permanently()
206    }
207
208    /// Delete all the flagged files & directory recursively.
209    /// If an output socket was provided at launch, it will inform the IPC server about those deletions.
210    /// Clear the flagged files.
211    ///
212    /// # Errors
213    ///
214    /// May fail if any deletion is impossible (permissions, file already deleted etc.)
215    pub fn delete_flagged_files(&mut self) -> Result<()> {
216        let nb = self.flagged.len();
217        let output_socket = Args::parse().output_socket;
218        for pathbuf in self.flagged.content.iter() {
219            if pathbuf.is_dir() {
220                std::fs::remove_dir_all(pathbuf)?;
221            } else {
222                std::fs::remove_file(pathbuf)?;
223            }
224            if let Some(output_socket) = &output_socket {
225                nvim_inform_ipc(output_socket, NvimIPCAction::DELETE(pathbuf))?;
226            }
227        }
228        self.flagged.clear();
229        log_line!("Deleted {nb} flagged files");
230        Ok(())
231    }
232
233    /// Reset the password holder, drop the sudo privileges (sudo -k) and clear the sudo command.
234    pub fn clear_sudo_attributes(&mut self) -> Result<()> {
235        self.password_holder.reset();
236        drop_sudo_privileges()?;
237        self.sudo_command = None;
238        Ok(())
239    }
240
241    /// Insert a char in the input string.
242    pub fn input_insert(&mut self, char: char) -> Result<()> {
243        self.input.insert(char);
244        Ok(())
245    }
246
247    /// Refresh the shortcuts. It drops non "hardcoded" shortcuts and
248    /// extend the vector with the mount points.
249    pub fn refresh_shortcuts(
250        &mut self,
251        mount_points: &[&std::path::Path],
252        left_path: &std::path::Path,
253        right_path: &std::path::Path,
254    ) {
255        self.shortcut.refresh(mount_points, left_path, right_path)
256    }
257
258    pub fn completion_reset(&mut self) {
259        self.completion.reset();
260    }
261
262    pub fn completion_tab(&mut self) {
263        self.input.replace(self.completion.current_proposition())
264    }
265
266    pub fn len(&self, menu_mode: Menu) -> usize {
267        match menu_mode {
268            Menu::Navigate(navigate) => self.apply_method(navigate, |variant| variant.len()),
269            Menu::InputCompleted(_) => self.completion.len(),
270            Menu::NeedConfirmation(need_confirmation) if need_confirmation.use_flagged_files() => {
271                self.flagged.len()
272            }
273            Menu::NeedConfirmation(NeedConfirmation::EmptyTrash) => self.trash.len(),
274            Menu::NeedConfirmation(NeedConfirmation::BulkAction) => self.bulk.len(),
275            _ => 0,
276        }
277    }
278
279    pub fn index(&self, menu_mode: Menu) -> usize {
280        match menu_mode {
281            Menu::Navigate(navigate) => self.apply_method(navigate, |variant| variant.index()),
282            Menu::InputCompleted(_) => self.completion.index,
283            Menu::NeedConfirmation(need_confirmation) if need_confirmation.use_flagged_files() => {
284                self.flagged.index()
285            }
286            Menu::NeedConfirmation(NeedConfirmation::EmptyTrash) => self.trash.index(),
287            Menu::NeedConfirmation(NeedConfirmation::BulkAction) => self.bulk.index(),
288            _ => 0,
289        }
290    }
291
292    pub fn page_down(&mut self, navigate: Navigate) {
293        for _ in 0..10 {
294            self.next(navigate)
295        }
296    }
297
298    pub fn page_up(&mut self, navigate: Navigate) {
299        for _ in 0..10 {
300            self.prev(navigate)
301        }
302    }
303
304    pub fn completion_prev(&mut self, input_completed: InputCompleted) {
305        self.completion.prev();
306        self.window
307            .scroll_to(self.index(Menu::InputCompleted(input_completed)));
308    }
309
310    pub fn completion_next(&mut self, input_completed: InputCompleted) {
311        self.completion.next();
312        self.window
313            .scroll_to(self.index(Menu::InputCompleted(input_completed)));
314    }
315
316    pub fn next(&mut self, navigate: Navigate) {
317        self.apply_method_mut(navigate, |variant| variant.next());
318        self.window.scroll_to(self.index(Menu::Navigate(navigate)));
319    }
320
321    pub fn prev(&mut self, navigate: Navigate) {
322        self.apply_method_mut(navigate, |variant| variant.prev());
323        self.window.scroll_to(self.index(Menu::Navigate(navigate)));
324    }
325
326    pub fn set_index(&mut self, index: usize, navigate: Navigate) {
327        self.apply_method_mut(navigate, |variant| variant.set_index(index));
328        self.window.scroll_to(self.index(Menu::Navigate(navigate)))
329    }
330
331    pub fn select_last(&mut self, navigate: Navigate) {
332        let index = self.len(Menu::Navigate(navigate)).saturating_sub(1);
333        self.set_index(index, navigate);
334    }
335
336    fn apply_method_mut<F, T>(&mut self, navigate: Navigate, func: F) -> T
337    where
338        F: FnOnce(&mut dyn Selectable) -> T,
339    {
340        match navigate {
341            Navigate::CliApplication => func(&mut self.cli_applications),
342            Navigate::Compress => func(&mut self.compression),
343            Navigate::Mount => func(&mut self.mount),
344            Navigate::Context => func(&mut self.context),
345            Navigate::History => func(&mut self.history),
346            Navigate::Marks(_) => func(&mut self.marks),
347            Navigate::TempMarks(_) => func(&mut self.temp_marks),
348            Navigate::Shortcut => func(&mut self.shortcut),
349            Navigate::Trash => func(&mut self.trash),
350            Navigate::TuiApplication => func(&mut self.tui_applications),
351            Navigate::Cloud => func(&mut self.cloud),
352            Navigate::Picker => func(&mut self.picker),
353            Navigate::Flagged => func(&mut self.flagged),
354        }
355    }
356
357    fn apply_method<F, T>(&self, navigate: Navigate, func: F) -> T
358    where
359        F: FnOnce(&dyn Selectable) -> T,
360    {
361        match navigate {
362            Navigate::CliApplication => func(&self.cli_applications),
363            Navigate::Compress => func(&self.compression),
364            Navigate::Mount => func(&self.mount),
365            Navigate::Context => func(&self.context),
366            Navigate::History => func(&self.history),
367            Navigate::Marks(_) => func(&self.marks),
368            Navigate::TempMarks(_) => func(&self.temp_marks),
369            Navigate::Shortcut => func(&self.shortcut),
370            Navigate::Trash => func(&self.trash),
371            Navigate::TuiApplication => func(&self.tui_applications),
372            Navigate::Cloud => func(&self.cloud),
373            Navigate::Picker => func(&self.picker),
374            Navigate::Flagged => func(&self.flagged),
375        }
376    }
377
378    // TODO! ensure it's displayed
379    /// Draw a navigation menu with its simple `draw_menu` method.
380    ///
381    /// # Errors
382    ///
383    /// Some mode can't be displayed directly and this method will raise an error.
384    /// It's the responsability of the caller to check beforehand.
385    pub fn draw_navigate(&self, f: &mut Frame, rect: &Rect, navigate: Navigate) {
386        match navigate {
387            Navigate::Compress => self.compression.draw_menu(f, rect, &self.window),
388            Navigate::Shortcut => self.shortcut.draw_menu(f, rect, &self.window),
389            Navigate::Marks(_) => self.marks.draw_menu(f, rect, &self.window),
390            Navigate::TuiApplication => self.tui_applications.draw_menu(f, rect, &self.window),
391            Navigate::CliApplication => self.cli_applications.draw_menu(f, rect, &self.window),
392            Navigate::Mount => self.mount.draw_menu(f, rect, &self.window),
393            _ => unreachable!("{navigate} requires more information to be displayed."),
394        }
395    }
396
397    /// Replace the current input by the next proposition from history
398    /// for this edit mode.
399    pub fn input_history_next(&mut self, tab: &mut Tab) -> Result<()> {
400        if !self.input_history.is_mode_logged(&tab.menu_mode) {
401            return Ok(());
402        }
403        self.input_history.next();
404        self.input_history_replace(tab)
405    }
406
407    /// Replace the current input by the previous proposition from history
408    /// for this edit mode.
409    pub fn input_history_prev(&mut self, tab: &mut Tab) -> Result<()> {
410        if !self.input_history.is_mode_logged(&tab.menu_mode) {
411            return Ok(());
412        }
413        self.input_history.prev();
414        self.input_history_replace(tab)
415    }
416
417    fn input_history_replace(&mut self, tab: &mut Tab) -> Result<()> {
418        let Some(history_element) = self.input_history.current() else {
419            return Ok(());
420        };
421        self.input.replace(history_element.content());
422        self.input_complete(tab)?;
423        Ok(())
424    }
425
426    impl_navigate_from_char!(shortcut_from_char, shortcut);
427    impl_navigate_from_char!(context_from_char, context);
428    impl_navigate_from_char!(tui_applications_from_char, tui_applications);
429    impl_navigate_from_char!(cli_applications_from_char, cli_applications);
430    impl_navigate_from_char!(compression_method_from_char, compression);
431}