Skip to main content

fm/modes/utils/
leave_menu.rs

1use std::str::FromStr;
2
3use anyhow::{bail, Context, Result};
4
5use crate::app::Status;
6use crate::common::{path_to_string, rename_fullpath, string_to_path};
7use crate::config::Bindings;
8use crate::event::{ActionMap, EventAction, FmEvents};
9use crate::modes::{
10    Content, InputCompleted, InputSimple, Leave, MarkAction, Menu, MountAction, Navigate,
11    NodeCreation, PasswordUsage, PickerCaller, TerminalApplications,
12};
13use crate::{log_info, log_line};
14
15/// Methods called when executing something with Enter key.
16pub struct LeaveMenu;
17
18impl LeaveMenu {
19    pub fn leave_menu(status: &mut Status, binds: &Bindings) -> Result<()> {
20        status
21            .menu
22            .input_history
23            .update(status.current_tab().menu_mode, &status.menu.input.string())?;
24        let must_refresh = status.current_tab().menu_mode.must_refresh();
25        let must_reset_mode = status.current_tab().menu_mode.must_reset_mode();
26
27        match status.current_tab().menu_mode {
28            Menu::Nothing => Ok(()),
29            Menu::InputSimple(InputSimple::Rename) => LeaveMenu::rename(status),
30            Menu::InputSimple(InputSimple::Newfile) => LeaveMenu::new_file(status),
31            Menu::InputSimple(InputSimple::Newdir) => LeaveMenu::new_dir(status),
32            Menu::InputSimple(InputSimple::Chmod) => LeaveMenu::chmod(status),
33            Menu::InputSimple(InputSimple::RegexMatch) => LeaveMenu::regex_match(status),
34            Menu::InputSimple(InputSimple::SetNvimAddr) => LeaveMenu::set_nvim_addr(status),
35            Menu::InputSimple(InputSimple::ShellCommand) => LeaveMenu::shell_command(status),
36            Menu::InputSimple(InputSimple::Sort) => LeaveMenu::sort(status),
37            Menu::InputSimple(InputSimple::Filter) => LeaveMenu::filter(status),
38            Menu::InputSimple(InputSimple::Password(action, usage)) => {
39                LeaveMenu::password(status, action, usage)
40            }
41            Menu::InputSimple(InputSimple::CloudNewdir) => {
42                LeaveMenu::cloud_newdir(status)?;
43                return Ok(());
44            }
45            Menu::InputSimple(InputSimple::Remote) => LeaveMenu::remote(status),
46            Menu::Navigate(Navigate::History) => LeaveMenu::history(status),
47            Menu::Navigate(Navigate::Shortcut) => LeaveMenu::shortcut(status),
48            Menu::Navigate(Navigate::Trash) => LeaveMenu::trash(status),
49            Menu::Navigate(Navigate::TuiApplication) => LeaveMenu::tui_application(status),
50            Menu::Navigate(Navigate::CliApplication) => LeaveMenu::cli_info(status),
51            Menu::Navigate(Navigate::Cloud) => {
52                LeaveMenu::cloud_enter(status)?;
53                return Ok(());
54            }
55            Menu::Navigate(Navigate::Marks(MarkAction::New)) => LeaveMenu::marks_update(status),
56            Menu::Navigate(Navigate::Marks(MarkAction::Jump)) => LeaveMenu::marks_jump(status),
57            Menu::Navigate(Navigate::TempMarks(MarkAction::New)) => {
58                LeaveMenu::tempmark_update(status)
59            }
60            Menu::Navigate(Navigate::TempMarks(MarkAction::Jump)) => LeaveMenu::tempmark_jp(status),
61            Menu::Navigate(Navigate::Compress) => LeaveMenu::compress(status),
62            Menu::Navigate(Navigate::Mount) => LeaveMenu::go_to_mount(status),
63            Menu::Navigate(Navigate::Context) => LeaveMenu::context(status, binds),
64            Menu::Navigate(Navigate::Picker) => {
65                LeaveMenu::picker(status)?;
66                return Ok(());
67            }
68            Menu::Navigate(Navigate::Flagged) => LeaveMenu::flagged(status),
69            Menu::InputCompleted(InputCompleted::Exec) => {
70                LeaveMenu::exec(status)?;
71                return Ok(());
72            }
73            Menu::InputCompleted(InputCompleted::Search) => Ok(()),
74            Menu::InputCompleted(InputCompleted::Cd) => LeaveMenu::cd(status),
75            Menu::InputCompleted(InputCompleted::Action) => LeaveMenu::action(status),
76            // To avoid mistakes, the default answer is No. We do nothing here.
77            Menu::NeedConfirmation(_) => Ok(()),
78        }?;
79
80        status.menu.input.reset();
81        if must_reset_mode {
82            status.reset_menu_mode()?;
83        }
84        if must_refresh {
85            status.refresh_status()?;
86        }
87        Ok(())
88    }
89
90    /// Restore a file from the trash if possible.
91    /// Parent folders are created if needed.
92    pub fn trash(status: &mut Status) -> Result<()> {
93        if status.focus.is_file() {
94            return Ok(());
95        }
96        let _ = status.menu.trash.restore();
97        status.reset_menu_mode()?;
98        status.current_tab_mut().refresh_view()?;
99        status.update_second_pane_for_preview()
100    }
101
102    /// Jump to the current mark.
103    fn marks_jump(status: &mut Status) -> Result<()> {
104        if let Some((_, path)) = &status.menu.marks.selected() {
105            let len = status.current_tab().directory.content.len();
106            status.tabs[status.index].cd(path)?;
107            status.current_tab_mut().window.reset(len);
108            status.menu.input.reset();
109        }
110        status.update_second_pane_for_preview()
111    }
112
113    /// Update the selected mark with the current path.
114    /// Doesn't change its char.
115    /// If it doesn't fail, a new pair will be set with (oldchar, new path).
116    fn marks_update(status: &mut Status) -> Result<()> {
117        if let Some((ch, _)) = status.menu.marks.selected() {
118            let len = status.current_tab().directory.content.len();
119            let new_path = &status.tabs[status.index].directory.path;
120            log_line!("Saved mark {ch} -> {p}", p = new_path.display());
121            status.menu.marks.new_mark(*ch, new_path)?;
122            status.current_tab_mut().window.reset(len);
123            status.menu.input.reset();
124        }
125        Ok(())
126    }
127
128    /// Update the selected mark with the current path.
129    /// Doesn't change its char.
130    /// If it doesn't fail, a new pair will be set with (oldchar, new path).
131    fn tempmark_update(status: &mut Status) -> Result<()> {
132        let index = status.menu.temp_marks.index;
133        let len = status.current_tab().directory.content.len();
134        let new_path = &status.tabs[status.index].directory_of_selected()?;
135        status
136            .menu
137            .temp_marks
138            .set_mark(index as _, new_path.to_path_buf());
139        log_line!("Saved temp mark {index} -> {p}", p = new_path.display());
140        status.current_tab_mut().window.reset(len);
141        status.menu.input.reset();
142        Ok(())
143    }
144
145    /// Jump to the current mark.
146    fn tempmark_jp(status: &mut Status) -> Result<()> {
147        let Some(opt_path) = &status.menu.temp_marks.selected() else {
148            log_info!("no selected temp mark");
149            return Ok(());
150        };
151        let Some(path) = opt_path else {
152            return Ok(());
153        };
154        let len = status.current_tab().directory.content.len();
155        status.tabs[status.index].cd(path)?;
156        status.current_tab_mut().window.reset(len);
157        status.menu.input.reset();
158
159        status.update_second_pane_for_preview()
160    }
161
162    /// Execute a shell command picked from the tui_applications menu.
163    /// It will be run an a spawned terminal
164    fn tui_application(status: &mut Status) -> Result<()> {
165        status.internal_settings.disable_display();
166        status.menu.tui_applications.execute(status)?;
167        status.internal_settings.enable_display();
168        Ok(())
169    }
170
171    fn cli_info(status: &mut Status) -> Result<()> {
172        let (output, command) = status.menu.cli_applications.execute(status)?;
173        log_info!("cli info: command {command}, output\n{output}");
174        status.preview_command_output(output, command);
175        Ok(())
176    }
177
178    fn cloud_enter(status: &mut Status) -> Result<()> {
179        status.cloud_enter_file_or_dir()
180    }
181
182    fn cloud_newdir(status: &mut Status) -> Result<()> {
183        status.cloud_create_newdir(status.menu.input.string())?;
184        status.reset_menu_mode()?;
185        status.cloud_open()
186    }
187
188    /// Change permission of the flagged files.
189    /// Once the user has typed an octal permission like 754, it's applied to
190    /// the file.
191    /// Nothing is done if the user typed nothing or an invalid permission like
192    /// 955.
193    fn chmod(status: &mut Status) -> Result<()> {
194        status.chmod()
195    }
196
197    fn set_nvim_addr(status: &mut Status) -> Result<()> {
198        status.internal_settings.nvim_server = status.menu.input.string();
199        status.reset_menu_mode()?;
200        Ok(())
201    }
202
203    /// Select the first file matching the typed regex in current dir.
204    fn regex_match(status: &mut Status) -> Result<()> {
205        status.flag_from_regex()?;
206        status.menu.input.reset();
207        Ok(())
208    }
209
210    /// Execute a shell command typed by the user.
211    /// but expansions are supported
212    /// It won't return an `Err` if the command fail but log a message.
213    fn shell_command(status: &mut Status) -> Result<()> {
214        status.execute_shell_command_from_input()?;
215        Ok(())
216    }
217
218    /// Execute a rename of the selected file.
219    /// It uses the `fs::rename` function and has the same limitations.
220    /// Intermediates directory are created if needed.
221    /// It acts like a move (without any confirmation...)
222    /// The new file is selected.
223    fn rename(status: &mut Status) -> Result<()> {
224        if status.menu.input.is_empty() {
225            log_line!("Can't rename: new name is empty");
226            log_info!("Can't rename: new name is empty");
227            return Ok(());
228        }
229        let new_path = status.menu.input.string();
230        let old_path = status.current_tab().current_file()?.path;
231        match rename_fullpath(&old_path, &new_path) {
232            Ok(()) => {
233                status.rename_marks(&old_path, &new_path)?;
234                status.current_tab_mut().refresh_view()?;
235                status.current_tab_mut().cd_to_file(&new_path)?;
236            }
237            Err(error) => {
238                log_info!(
239                    "Error renaming {old_path} to {new_path}. Error: {error}",
240                    old_path = old_path.display()
241                );
242                log_line!(
243                    "Error renaming {old_path} to {new_path}. Error: {error}",
244                    old_path = old_path.display()
245                );
246            }
247        }
248        Ok(())
249    }
250
251    /// Creates a new file with input string as name.
252    /// Nothing is done if the file already exists.
253    fn new_file(status: &mut Status) -> Result<()> {
254        match NodeCreation::Newfile.create(status) {
255            Ok(path) => {
256                status.current_tab_mut().go_to_file(&path);
257                status.menu.flagged.push(path);
258                status.refresh_tabs()?;
259            }
260            Err(error) => log_info!("Error creating file. Error: {error}",),
261        }
262        Ok(())
263    }
264
265    /// Creates a new directory with input string as name.
266    /// Nothing is done if the directory already exists.
267    /// We use `fs::create_dir` internally so it will fail if the input string
268    /// ie. the user can create `newdir` or `newdir/newfolder`.
269    fn new_dir(status: &mut Status) -> Result<()> {
270        match NodeCreation::Newdir.create(status) {
271            Ok(path) => {
272                status.refresh_tabs()?;
273                status.current_tab_mut().go_to_file(&path);
274                status.menu.flagged.push(path);
275            }
276            Err(error) => log_info!("Error creating directory. Error: {error}",),
277        }
278        Ok(())
279    }
280
281    /// Tries to execute the selected file with an executable which is read
282    /// from the input string. It will fail silently if the executable can't
283    /// be found.
284    /// Optional parameters can be passed normally. ie. `"ls -lah"`
285    fn exec(status: &mut Status) -> Result<()> {
286        if status.current_tab().directory.content.is_empty() {
287            bail!("exec: empty directory")
288        }
289        let exec_command = status.menu.input.string();
290        if status.execute_shell_command(
291            exec_command,
292            Some(status.menu.flagged.as_strings()),
293            false,
294        )? {
295            status.menu.completion.reset();
296            status.menu.input.reset();
297        }
298        Ok(())
299    }
300
301    /// Move to the folder typed by the user.
302    /// The first completion proposition is used, `~` expansion is done.
303    /// If no result were found, no cd is done and we go back to normal mode
304    /// silently.
305    fn cd(status: &mut Status) -> Result<()> {
306        if status.menu.completion.is_empty() {
307            return Ok(());
308        }
309        let completed = status.menu.completion.current_proposition();
310        let path = string_to_path(completed)?;
311        status.thumbnail_queue_clear();
312        status.menu.input.reset();
313        status.current_tab_mut().cd_to_file(&path)?;
314        let len = status.current_tab().directory.content.len();
315        status.current_tab_mut().window.reset(len);
316        status.update_second_pane_for_preview()
317    }
318
319    /// Move to the selected shortcut.
320    /// It may fail if the user has no permission to visit the path.
321    fn shortcut(status: &mut Status) -> Result<()> {
322        let path = status
323            .menu
324            .shortcut
325            .selected()
326            .context("exec shortcut: empty shortcuts")?;
327        status.tabs[status.index].cd(path)?;
328        status.current_tab_mut().refresh_view()?;
329        status.update_second_pane_for_preview()
330    }
331
332    fn sort(status: &mut Status) -> Result<()> {
333        status.current_tab_mut().set_sortkind_per_mode();
334        status.update_second_pane_for_preview()?;
335        status.focus = status.focus.to_parent();
336        Ok(())
337    }
338
339    /// Move back to a previously visited path.
340    /// It may fail if the user has no permission to visit the path
341    fn history(status: &mut Status) -> Result<()> {
342        status.current_tab_mut().history_cd_to_last()?;
343        status.update_second_pane_for_preview()
344    }
345
346    /// Execute a password command (sudo or device passphrase).
347    fn password(
348        status: &mut Status,
349        action: Option<MountAction>,
350        usage: PasswordUsage,
351    ) -> Result<()> {
352        status.execute_password_command(action, usage)
353    }
354
355    /// Compress the flagged files into an archive.
356    /// Compression method is chosen by the user.
357    /// The archive is created in the current directory and is named "archive.tar.??" or "archive.zip".
358    /// Files which are above the CWD are filtered out since they can't be added to an archive.
359    /// Archive creation depends on CWD so we ensure it's set to the selected tab.
360    fn compress(status: &mut Status) -> Result<()> {
361        status.compress()
362    }
363
364    /// Open a menu with most common actions
365    fn context(status: &mut Status, binds: &Bindings) -> Result<()> {
366        let command = status.menu.context.matcher().to_owned();
367        EventAction::reset_mode(status)?;
368        command.matcher(status, binds)
369    }
370
371    /// Execute the selected action.
372    /// Some commands does nothing as they require to be executed from a specific
373    /// context.
374    fn action(status: &mut Status) -> Result<()> {
375        let action_str = status.menu.completion.current_proposition();
376        let Ok(action) = ActionMap::from_str(action_str) else {
377            return Ok(());
378        };
379
380        status.reset_menu_mode()?;
381        status.focus = status.focus.to_parent();
382        status.fm_sender.send(FmEvents::Action(action))?;
383        Ok(())
384    }
385
386    /// Apply a filter to the displayed files.
387    /// See `crate::filter` for more details.
388    fn filter(status: &mut Status) -> Result<()> {
389        status.filter()?;
390        status.menu.input.reset();
391        Ok(())
392    }
393
394    /// Run sshfs with typed parameters to mount a remote directory in current directory.
395    /// sshfs should be reachable in path.
396    /// The user must type 3 arguments like this : `username hostname remote_path`.
397    /// If the user doesn't provide 3 arguments,
398    fn remote(status: &mut Status) -> Result<()> {
399        let current_path = &path_to_string(&status.current_tab().directory_of_selected()?);
400        status.menu.mount_remote(current_path);
401        Ok(())
402    }
403
404    /// Go to the _mounted_ device. Does nothing if the device isn't mounted.
405    fn go_to_mount(status: &mut Status) -> Result<()> {
406        match status.current_tab().menu_mode {
407            Menu::Navigate(Navigate::Mount) => status.go_to_normal_drive(),
408            _ => Ok(()),
409        }
410    }
411
412    fn picker(status: &mut Status) -> Result<()> {
413        let Some(caller) = &status.menu.picker.caller else {
414            return Ok(());
415        };
416        match caller {
417            PickerCaller::Cloud => status.cloud_load_config(),
418            PickerCaller::Menu(menu) => EventAction::reenter_menu_from_picker(status, *menu),
419            PickerCaller::Unknown => Ok(()),
420        }
421    }
422
423    fn flagged(status: &mut Status) -> Result<()> {
424        status.jump_flagged()
425    }
426}