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