Skip to main content

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    /// Move all flagged files to the trash.
209    /// It will also send an IPC action "DELETE `flagged`" to nvim
210    /// It will also remove and associated mark.
211    pub fn trash_and_inform(&mut self) -> Result<()> {
212        self.trash.update()?;
213        let output_socket = Args::parse().output_socket;
214        while let Some(flagged) = self.flagged.content.pop() {
215            if self.trash_a_file(&flagged).is_ok() {
216                if let Some(output_socket) = &output_socket {
217                    nvim_inform_ipc(output_socket, NvimIPCAction::DELETE(&flagged))?;
218                }
219            }
220        }
221        Ok(())
222    }
223
224    /// Move the file to trash and remove its associated marks.
225    fn trash_a_file(&mut self, origin: &std::path::Path) -> Result<()> {
226        self.trash.trash(origin)?;
227        self.delete_mark(origin)
228    }
229
230    /// Delete all the flagged files & directory recursively.
231    /// If an output socket was provided at launch, it will inform the IPC server about those deletions.
232    /// Clear the flagged files.
233    ///
234    /// # Errors
235    ///
236    /// May fail if any deletion is impossible (permissions, file already deleted etc.)
237    pub fn delete_flagged_files(&mut self) -> Result<()> {
238        let nb = self.flagged.len();
239        let output_socket = Args::parse().output_socket;
240        while let Some(pathbuf) = self.flagged.content.pop() {
241            if pathbuf.is_dir() {
242                std::fs::remove_dir_all(&pathbuf)?;
243            } else {
244                std::fs::remove_file(&pathbuf)?;
245            }
246            if let Some(output_socket) = &output_socket {
247                nvim_inform_ipc(output_socket, NvimIPCAction::DELETE(&pathbuf))?;
248            }
249            self.delete_mark(&pathbuf)?;
250        }
251        log_line!("Deleted {nb} flagged files");
252        Ok(())
253    }
254
255    /// Reset the password holder, drop the sudo privileges (sudo -k) and clear the sudo command.
256    pub fn clear_sudo_attributes(&mut self) -> Result<()> {
257        self.password_holder.reset();
258        drop_sudo_privileges()?;
259        self.sudo_command = None;
260        Ok(())
261    }
262
263    /// Insert a char in the input string.
264    pub fn input_insert(&mut self, char: char) -> Result<()> {
265        self.input.insert(char);
266        Ok(())
267    }
268
269    /// Refresh the shortcuts. It drops non "hardcoded" shortcuts and
270    /// extend the vector with the mount points.
271    pub fn refresh_shortcuts(
272        &mut self,
273        mount_points: &[&std::path::Path],
274        left_path: &std::path::Path,
275        right_path: &std::path::Path,
276    ) {
277        self.shortcut.refresh(mount_points, left_path, right_path)
278    }
279
280    pub fn completion_reset(&mut self) {
281        self.completion.reset();
282    }
283
284    pub fn completion_tab(&mut self) {
285        self.input.replace(self.completion.current_proposition())
286    }
287
288    pub fn len(&self, menu_mode: Menu) -> usize {
289        match menu_mode {
290            Menu::Navigate(navigate) => self.apply_method(navigate, |variant| variant.len()),
291            Menu::InputCompleted(_) => self.completion.len(),
292            Menu::NeedConfirmation(need_confirmation) if need_confirmation.use_flagged_files() => {
293                self.flagged.len()
294            }
295            Menu::NeedConfirmation(NeedConfirmation::EmptyTrash) => self.trash.len(),
296            Menu::NeedConfirmation(NeedConfirmation::BulkAction) => self.bulk.len(),
297            _ => 0,
298        }
299    }
300
301    pub fn index(&self, menu_mode: Menu) -> usize {
302        match menu_mode {
303            Menu::Navigate(navigate) => self.apply_method(navigate, |variant| variant.index()),
304            Menu::InputCompleted(_) => self.completion.index,
305            Menu::NeedConfirmation(need_confirmation) if need_confirmation.use_flagged_files() => {
306                self.flagged.index()
307            }
308            Menu::NeedConfirmation(NeedConfirmation::EmptyTrash) => self.trash.index(),
309            Menu::NeedConfirmation(NeedConfirmation::BulkAction) => self.bulk.index(),
310            _ => 0,
311        }
312    }
313
314    pub fn page_down(&mut self, navigate: Navigate) {
315        for _ in 0..10 {
316            self.next(navigate)
317        }
318    }
319
320    pub fn page_up(&mut self, navigate: Navigate) {
321        for _ in 0..10 {
322            self.prev(navigate)
323        }
324    }
325
326    pub fn completion_prev(&mut self, input_completed: InputCompleted) {
327        self.completion.prev();
328        self.window
329            .scroll_to(self.index(Menu::InputCompleted(input_completed)));
330    }
331
332    pub fn completion_next(&mut self, input_completed: InputCompleted) {
333        self.completion.next();
334        self.window
335            .scroll_to(self.index(Menu::InputCompleted(input_completed)));
336    }
337
338    pub fn next(&mut self, navigate: Navigate) {
339        self.apply_method_mut(navigate, |variant| variant.next());
340        self.window.scroll_to(self.index(Menu::Navigate(navigate)));
341    }
342
343    pub fn prev(&mut self, navigate: Navigate) {
344        self.apply_method_mut(navigate, |variant| variant.prev());
345        self.window.scroll_to(self.index(Menu::Navigate(navigate)));
346    }
347
348    pub fn set_index(&mut self, index: usize, navigate: Navigate) {
349        self.apply_method_mut(navigate, |variant| variant.set_index(index));
350        self.window.scroll_to(self.index(Menu::Navigate(navigate)))
351    }
352
353    pub fn select_last(&mut self, navigate: Navigate) {
354        let index = self.len(Menu::Navigate(navigate)).saturating_sub(1);
355        self.set_index(index, navigate);
356    }
357
358    fn apply_method_mut<F, T>(&mut self, navigate: Navigate, func: F) -> T
359    where
360        F: FnOnce(&mut dyn Selectable) -> T,
361    {
362        match navigate {
363            Navigate::CliApplication => func(&mut self.cli_applications),
364            Navigate::Compress => func(&mut self.compression),
365            Navigate::Mount => func(&mut self.mount),
366            Navigate::Context => func(&mut self.context),
367            Navigate::History => func(&mut self.history),
368            Navigate::Marks(_) => func(&mut self.marks),
369            Navigate::TempMarks(_) => func(&mut self.temp_marks),
370            Navigate::Shortcut => func(&mut self.shortcut),
371            Navigate::Trash => func(&mut self.trash),
372            Navigate::TuiApplication => func(&mut self.tui_applications),
373            Navigate::Cloud => func(&mut self.cloud),
374            Navigate::Picker => func(&mut self.picker),
375            Navigate::Flagged => func(&mut self.flagged),
376        }
377    }
378
379    fn apply_method<F, T>(&self, navigate: Navigate, func: F) -> T
380    where
381        F: FnOnce(&dyn Selectable) -> T,
382    {
383        match navigate {
384            Navigate::CliApplication => func(&self.cli_applications),
385            Navigate::Compress => func(&self.compression),
386            Navigate::Mount => func(&self.mount),
387            Navigate::Context => func(&self.context),
388            Navigate::History => func(&self.history),
389            Navigate::Marks(_) => func(&self.marks),
390            Navigate::TempMarks(_) => func(&self.temp_marks),
391            Navigate::Shortcut => func(&self.shortcut),
392            Navigate::Trash => func(&self.trash),
393            Navigate::TuiApplication => func(&self.tui_applications),
394            Navigate::Cloud => func(&self.cloud),
395            Navigate::Picker => func(&self.picker),
396            Navigate::Flagged => func(&self.flagged),
397        }
398    }
399
400    // TODO! ensure it's displayed
401    /// Draw a navigation menu with its simple `draw_menu` method.
402    ///
403    /// # Errors
404    ///
405    /// Some mode can't be displayed directly and this method will raise an error.
406    /// It's the responsability of the caller to check beforehand.
407    pub fn draw_navigate(&self, f: &mut Frame, rect: &Rect, navigate: Navigate) {
408        match navigate {
409            Navigate::Compress => self.compression.draw_menu(f, rect, &self.window),
410            Navigate::Shortcut => self.shortcut.draw_menu(f, rect, &self.window),
411            Navigate::Marks(_) => self.marks.draw_menu(f, rect, &self.window),
412            Navigate::TuiApplication => self.tui_applications.draw_menu(f, rect, &self.window),
413            Navigate::CliApplication => self.cli_applications.draw_menu(f, rect, &self.window),
414            Navigate::Mount => self.mount.draw_menu(f, rect, &self.window),
415            _ => unreachable!("{navigate} requires more information to be displayed."),
416        }
417    }
418
419    /// Replace the current input by the next proposition from history
420    /// for this edit mode.
421    pub fn input_history_next(&mut self, tab: &mut Tab) -> Result<()> {
422        if !self.input_history.is_mode_logged(&tab.menu_mode) {
423            return Ok(());
424        }
425        self.input_history.next();
426        self.input_history_replace(tab)
427    }
428
429    /// Replace the current input by the previous proposition from history
430    /// for this edit mode.
431    pub fn input_history_prev(&mut self, tab: &mut Tab) -> Result<()> {
432        if !self.input_history.is_mode_logged(&tab.menu_mode) {
433            return Ok(());
434        }
435        self.input_history.prev();
436        self.input_history_replace(tab)
437    }
438
439    fn input_history_replace(&mut self, tab: &mut Tab) -> Result<()> {
440        let Some(history_element) = self.input_history.current() else {
441            return Ok(());
442        };
443        self.input.replace(history_element.content());
444        self.input_complete(tab)?;
445        Ok(())
446    }
447
448    pub fn delete_mark(&mut self, old_path: &std::path::Path) -> Result<()> {
449        crate::log_info!("Remove mark {old_path:?}");
450        self.temp_marks.remove_path(old_path);
451        self.marks.remove_path(old_path)
452    }
453
454    impl_navigate_from_char!(shortcut_from_char, shortcut);
455    impl_navigate_from_char!(context_from_char, context);
456    impl_navigate_from_char!(tui_applications_from_char, tui_applications);
457    impl_navigate_from_char!(cli_applications_from_char, cli_applications);
458    impl_navigate_from_char!(compression_method_from_char, compression);
459}