fm/modes/
mode.rs

1use std::fmt;
2
3use anyhow::Result;
4
5use crate::app::Status;
6use crate::common::{
7    UtfWidth, CHMOD_LINES, CLOUD_NEWDIR_LINES, FILTER_LINES, NEWDIR_LINES, NEWFILE_LINES,
8    NVIM_ADDRESS_LINES, PASSWORD_LINES_DEVICE, PASSWORD_LINES_SUDO, REGEX_LINES, REMOTE_LINES,
9    RENAME_LINES, SHELL_LINES, SORT_LINES,
10};
11use crate::event::EventAction;
12use crate::modes::InputCompleted;
13use crate::modes::MountAction;
14use crate::modes::{PasswordKind, PasswordUsage};
15
16/// Different kind of mark actions.
17/// Either we jump to an existing mark or we save current path to a mark.
18/// In both case, we'll have to listen to the next char typed.
19#[derive(Clone, Copy, Debug, Eq, PartialEq)]
20pub enum MarkAction {
21    /// Jump to a selected mark (ie a path associated to a char)
22    Jump,
23    /// Creates a new mark (a path associated to a char)
24    New,
25}
26
27/// Different kind of last edition command received requiring a confirmation.
28/// Copy, move and delete require a confirmation to prevent big mistakes.
29#[derive(Clone, Copy, Debug, Eq, PartialEq)]
30pub enum NeedConfirmation {
31    /// Copy flagged files
32    Copy,
33    /// Delete flagged files
34    Delete,
35    /// Move flagged files
36    Move,
37    /// Empty Trash
38    EmptyTrash,
39    /// Bulk
40    BulkAction,
41    /// Delete cloud files
42    DeleteCloud,
43}
44
45impl NeedConfirmation {
46    /// A confirmation message to be displayed before executing the mode.
47    /// When files are moved or copied the destination is displayed.
48    #[must_use]
49    pub fn confirmation_string(&self, destination: &str) -> String {
50        match *self {
51            Self::Copy => {
52                format!("Files will be copied to {destination}")
53            }
54            Self::Delete | Self::EmptyTrash => "Files will be deleted permanently".to_owned(),
55            Self::Move => {
56                format!("Files will be moved to {destination}")
57            }
58            Self::BulkAction => "Those files will be renamed or created :".to_owned(),
59            Self::DeleteCloud => "Remote Files will be deleted permanently".to_owned(),
60        }
61    }
62
63    pub fn use_flagged_files(&self) -> bool {
64        matches!(self, Self::Copy | Self::Move | Self::Delete)
65    }
66}
67
68impl CursorOffset for NeedConfirmation {
69    /// Offset before the cursor.
70    /// Since we ask the user confirmation, we need to know how much space
71    /// is needed.
72    fn cursor_offset(&self) -> u16 {
73        self.to_string().utf_width_u16() + 9
74    }
75}
76
77impl Leave for NeedConfirmation {
78    fn must_refresh(&self) -> bool {
79        true
80    }
81
82    fn must_reset_mode(&self) -> bool {
83        true
84    }
85}
86
87impl std::fmt::Display for NeedConfirmation {
88    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
89        match *self {
90            Self::Delete => write!(f, "Delete files :"),
91            Self::DeleteCloud => write!(f, "Delete files :"),
92            Self::Move => write!(f, "Move files here :"),
93            Self::Copy => write!(f, "Copy files here :"),
94            Self::EmptyTrash => write!(f, "Empty the trash ?"),
95            Self::BulkAction => write!(f, "Bulk :"),
96        }
97    }
98}
99
100/// Different modes in which the user is expeted to type something.
101/// It may be a new filename, a mode (aka an octal permission),
102/// the name of a new file, of a new directory,
103/// A regex to match all files in current directory,
104/// a kind of sort, a mark name, a new mark or a filter.
105#[derive(Clone, Copy, PartialEq, Eq)]
106pub enum InputSimple {
107    /// Rename the selected file
108    Rename,
109    /// Change permissions of the selected file
110    Chmod,
111    /// Touch a new file
112    Newfile,
113    /// Mkdir a new directory
114    Newdir,
115    /// Flag files matching a regex
116    RegexMatch,
117    /// Change the type of sort
118    Sort,
119    /// Filter by extension, name, directory or no filter
120    Filter,
121    /// Set a new neovim RPC address
122    SetNvimAddr,
123    /// Input a password (chars a replaced by *)
124    Password(Option<MountAction>, PasswordUsage),
125    /// Shell command execute as is
126    ShellCommand,
127    /// Mount a remote directory with sshfs
128    Remote,
129    /// Create a new file in the current cloud
130    CloudNewdir,
131}
132
133impl fmt::Display for InputSimple {
134    fn fmt(&self, f: &mut std::fmt::Formatter) -> fmt::Result {
135        match *self {
136            Self::Rename => write!(f, "Rename:  "),
137            Self::Chmod => write!(f, "Chmod:   "),
138            Self::Newfile => write!(f, "Newfile: "),
139            Self::Newdir => write!(f, "Newdir:  "),
140            Self::RegexMatch => write!(f, "Regex:   "),
141            Self::SetNvimAddr => write!(f, "Neovim:  "),
142            Self::CloudNewdir => write!(f, "Newdir:  "),
143            Self::ShellCommand => write!(f, "Shell:   "),
144            Self::Sort => {
145                write!(f, "Sort: ")
146            }
147            Self::Filter => write!(f, "Filter:  "),
148            Self::Password(_, PasswordUsage::CRYPTSETUP(password_kind)) => {
149                write!(f, "{password_kind}")
150            }
151            Self::Password(_, _) => write!(f, " sudo: "),
152            Self::Remote => write!(f, "Remote:  "),
153        }
154    }
155}
156
157impl InputSimple {
158    const EDIT_BOX_OFFSET: u16 = 11;
159    const SORT_CURSOR_OFFSET: u16 = 8;
160    const PASSWORD_CURSOR_OFFSET: u16 = 9;
161
162    /// Returns a vector of static &str describing what
163    /// the mode does.
164    #[must_use]
165    pub const fn lines(&self) -> &'static [&'static str] {
166        match *self {
167            Self::Chmod => &CHMOD_LINES,
168            Self::Filter => &FILTER_LINES,
169            Self::Newdir => &NEWDIR_LINES,
170            Self::Newfile => &NEWFILE_LINES,
171            Self::Password(_, PasswordUsage::CRYPTSETUP(PasswordKind::SUDO)) => {
172                &PASSWORD_LINES_SUDO
173            }
174            Self::Password(_, PasswordUsage::CRYPTSETUP(PasswordKind::CRYPTSETUP)) => {
175                &PASSWORD_LINES_DEVICE
176            }
177            Self::Password(_, _) => &PASSWORD_LINES_SUDO,
178            Self::RegexMatch => &REGEX_LINES,
179            Self::Rename => &RENAME_LINES,
180            Self::SetNvimAddr => &NVIM_ADDRESS_LINES,
181            Self::ShellCommand => &SHELL_LINES,
182            Self::Sort => &SORT_LINES,
183            Self::Remote => &REMOTE_LINES,
184            Self::CloudNewdir => &CLOUD_NEWDIR_LINES,
185        }
186    }
187}
188
189/// Used to know how many cells are used before the cursor is drawned it the input line.
190pub trait CursorOffset {
191    fn cursor_offset(&self) -> u16;
192}
193
194impl CursorOffset for InputSimple {
195    fn cursor_offset(&self) -> u16 {
196        match *self {
197            Self::Sort => Self::SORT_CURSOR_OFFSET,
198            Self::Password(_, _) => Self::PASSWORD_CURSOR_OFFSET,
199            _ => Self::EDIT_BOX_OFFSET,
200        }
201    }
202}
203
204impl Leave for InputSimple {
205    fn must_refresh(&self) -> bool {
206        !matches!(
207            self,
208            Self::ShellCommand | Self::Filter | Self::Password(_, _) | Self::Sort
209        )
210    }
211
212    fn must_reset_mode(&self) -> bool {
213        !matches!(self, Self::ShellCommand | Self::Password(_, _))
214    }
215}
216
217/// Different modes in which we display a bunch of possible actions.
218/// In all those mode we can select an action and execute it.
219/// For some of them, it's just moving there, for some it acts on some file.
220#[derive(Clone, Copy, Debug, Eq, PartialEq)]
221pub enum Navigate {
222    /// Navigate back to a visited path
223    History,
224    /// Navigate to a predefined shortcut
225    Shortcut,
226    /// Manipulate trash files
227    Trash,
228    /// Edit a mark or cd to it
229    Marks(MarkAction),
230    /// Edit a temporary mark or cd to it
231    TempMarks(MarkAction),
232    /// See mount points, mount, unmount partions
233    Mount,
234    /// Pick a compression method
235    Compress,
236    /// Shell menu applications. Start a new shell with this application.
237    TuiApplication,
238    /// Cli info
239    CliApplication,
240    /// Context menu
241    Context,
242    /// Cloud menu
243    Cloud,
244    /// Picker menu
245    Picker,
246    /// Flagged files
247    Flagged,
248}
249
250impl fmt::Display for Navigate {
251    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
252        match *self {
253            Self::Marks(_) => write!(f, "Marks jump:"),
254            Self::TempMarks(_) => write!(f, "Temp marks jump:"),
255            Self::History => write!(f, "History :"),
256            Self::Shortcut => write!(f, "Shortcut :"),
257            Self::Trash => write!(f, "Trash :"),
258            Self::TuiApplication => {
259                write!(f, "Start a new shell running a command:")
260            }
261            Self::Compress => write!(f, "Compress :"),
262            Self::Mount => write!(f, "Mount :"),
263            Self::CliApplication => write!(f, "Display infos :"),
264            Self::Context => write!(f, "Context"),
265            Self::Cloud => write!(f, "Cloud"),
266            Self::Picker => write!(f, "Picker"),
267            Self::Flagged => write!(f, "Flagged"),
268        }
269    }
270}
271
272impl CursorOffset for Navigate {
273    #[inline]
274    fn cursor_offset(&self) -> u16 {
275        0
276    }
277}
278
279impl Leave for Navigate {
280    fn must_refresh(&self) -> bool {
281        !matches!(self, Self::CliApplication | Self::Context)
282    }
283
284    fn must_reset_mode(&self) -> bool {
285        !matches!(self, Self::CliApplication | Self::Context)
286    }
287}
288
289impl Navigate {
290    /// True if the draw_menu trait can be called directly to display this mode
291    pub fn simple_draw_menu(&self) -> bool {
292        matches!(
293            self,
294            Self::Compress
295                | Self::Shortcut
296                | Self::TuiApplication
297                | Self::CliApplication
298                | Self::Marks(_)
299                | Self::Mount
300        )
301    }
302}
303
304/// Different "menu" mode in which the application can be.
305/// It dictates the reaction to event and is displayed in the bottom window.
306#[derive(Clone, Copy, Eq, PartialEq)]
307pub enum Menu {
308    /// Do something that may be completed
309    /// Completion may come from :
310    /// - executable in $PATH,
311    /// - current directory or tree,
312    /// - directory in your file system,
313    /// - known actions. See [`crate::event::EventAction`],
314    InputCompleted(InputCompleted),
315    /// Do something that need typing :
316    /// - renaming a file or directory,
317    /// - creating a file or directory,
318    /// - typing a password (won't be displayed, will be dropped ASAP)
319    InputSimple(InputSimple),
320    /// Select something in a list and act on it
321    Navigate(Navigate),
322    /// Confirmation is required before modification is made to existing files :
323    /// delete, move, copy
324    NeedConfirmation(NeedConfirmation),
325    /// No action is currently performed
326    Nothing,
327}
328
329impl fmt::Display for Menu {
330    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
331        match *self {
332            Self::InputCompleted(input_completed) => input_completed.fmt(f),
333            Self::InputSimple(input_simple) => input_simple.fmt(f),
334            Self::Navigate(navigate) => navigate.fmt(f),
335            Self::NeedConfirmation(need_confirmation) => need_confirmation.fmt(f),
336            Self::Nothing => write!(f, ""),
337        }
338    }
339}
340
341impl Menu {
342    /// Does this mode requires a cursor ?
343    pub fn show_cursor(&self) -> bool {
344        self.cursor_offset() != 0
345    }
346
347    pub fn binds_per_mode(&self) -> &'static str {
348        match self {
349            Self::InputCompleted(_) => "Tab: completion. shift+⬆️, shift+⬇️: previous entries, shift+⬅️: erase line. Enter: validate",
350            Self::InputSimple(InputSimple::Filter) => "Enter reset the filters",
351            Self::InputSimple(InputSimple::Sort ) => "Enter reset the sort",
352            Self::InputSimple(_) => "shift+⬆️, shift+⬇️: previous entries, shift+⬅️: erase line. Enter: validate",
353            Self::Navigate(Navigate::Marks(MarkAction::Jump)) => "Type the mark letter to jump there. up, down to navigate, ENTER to select an element",
354            Self::Navigate(Navigate::Marks(MarkAction::New)) => "Type the mark set a mark here. up, down to navigate, ENTER to select an element",
355            Self::Navigate(Navigate::TempMarks(MarkAction::New)) => "Type the mark set a mark here. up, down to navigate, ENTER to select an element",
356            Self::Navigate(Navigate::Cloud) => "l: leave drive, arrows: navigation, Enter: enter dir / download file, d: new dir, x: delete selected, u: upload local file",
357            Self::Navigate(Navigate::Flagged) => "Up, Down: navigate, Enter / j: jump to this file, x: remove from flagged, u: clear",
358            Self::Navigate(Navigate::Trash) => "Up, Down: navigate.",
359            Self::Navigate(Navigate::Mount) => "m: mount, u: umount, e: eject (removable), g/ENTER: go to",
360            Self::Navigate(_) => "up, down to navigate, Enter to select an element",
361            Self::NeedConfirmation(_) => "",
362            _ => "",
363        }
364    }
365
366    /// True if the edit mode is "Nothing" aka no menu is opened in this tab.
367    pub fn is_nothing(&self) -> bool {
368        matches!(self, Self::Nothing)
369    }
370
371    pub fn is_navigate(&self) -> bool {
372        matches!(self, Self::Navigate(_))
373    }
374
375    /// True iff the menu requires an input aka InputSimple or InputCompleted.
376    pub fn is_input(&self) -> bool {
377        matches!(self, Self::InputCompleted(_) | Self::InputSimple(_))
378    }
379
380    pub fn is_complete(&self) -> bool {
381        matches!(self, Self::InputCompleted(_))
382    }
383
384    pub fn is_picker(&self) -> bool {
385        matches!(self, Self::Navigate(Navigate::Picker))
386    }
387
388    /// Nice name for the picker menu.
389    pub fn name_for_picker(&self) -> Option<String> {
390        self.to_string().split(':').next().map(|s| s.to_string())
391    }
392}
393
394impl CursorOffset for Menu {
395    /// Constant offset for the cursor.
396    /// In any mode, we display the mode used and then the cursor if needed.
397    #[inline]
398    fn cursor_offset(&self) -> u16 {
399        match self {
400            Self::InputCompleted(input_completed) => input_completed.cursor_offset(),
401            Self::InputSimple(input_simple) => input_simple.cursor_offset(),
402            Self::Navigate(navigate) => navigate.cursor_offset(),
403            Self::NeedConfirmation(confirmed_action) => confirmed_action.cursor_offset(),
404            Self::Nothing => 0,
405        }
406    }
407}
408
409impl Leave for Menu {
410    fn must_refresh(&self) -> bool {
411        match self {
412            Self::InputCompleted(input_completed) => input_completed.must_refresh(),
413            Self::InputSimple(input_simple) => input_simple.must_refresh(),
414            Self::Navigate(navigate) => navigate.must_refresh(),
415            Self::NeedConfirmation(need_confirmation) => need_confirmation.must_refresh(),
416            Self::Nothing => true,
417        }
418    }
419
420    fn must_reset_mode(&self) -> bool {
421        match self {
422            Self::InputCompleted(input_completed) => input_completed.must_reset_mode(),
423            Self::InputSimple(input_simple) => input_simple.must_reset_mode(),
424            Self::Navigate(navigate) => navigate.must_reset_mode(),
425            Self::NeedConfirmation(need_confirmation) => need_confirmation.must_reset_mode(),
426            Self::Nothing => true,
427        }
428    }
429}
430
431/// Trait which should be implemented for every edit mode.
432/// It says if leaving this mode should be followed with a reset of the display & file content,
433/// and if we have to reset the edit mode.
434pub trait Leave {
435    /// Should the file content & window be refreshed when leaving this mode?
436    fn must_refresh(&self) -> bool;
437    /// Should the edit mode be reset to Nothing when leaving this mode ?
438    fn must_reset_mode(&self) -> bool;
439}
440
441pub trait ReEnterMenu {
442    fn reenter(&self, status: &mut Status) -> Result<()>;
443}
444
445impl ReEnterMenu for Menu {
446    #[rustfmt::skip]
447    fn reenter(&self, status: &mut Status) -> Result<()> {
448        match self {
449            Self::InputCompleted(InputCompleted::Cd)                => EventAction::cd(status),
450            Self::InputCompleted(InputCompleted::Search)            => EventAction::search(status),
451            Self::InputCompleted(InputCompleted::Exec)              => EventAction::exec(status),
452            Self::InputCompleted(InputCompleted::Action)            => EventAction::action(status),
453            Self::InputSimple(InputSimple::Rename)                  => EventAction::rename(status),
454            Self::InputSimple(InputSimple::Chmod)                   => EventAction::chmod(status),
455            Self::InputSimple(InputSimple::Newfile)                 => EventAction::new_file(status),
456            Self::InputSimple(InputSimple::Newdir)                  => EventAction::new_dir(status),
457            Self::InputSimple(InputSimple::RegexMatch)              => EventAction::regex_match(status),
458            Self::InputSimple(InputSimple::Sort)                    => EventAction::sort(status),
459            Self::InputSimple(InputSimple::Filter)                  => EventAction::filter(status),
460            Self::InputSimple(InputSimple::SetNvimAddr)             => EventAction::set_nvim_server(status),
461            Self::InputSimple(InputSimple::Password(_mount_action, _usage)) => unreachable!("Can't pick a password, those aren't saved."),
462            Self::InputSimple(InputSimple::ShellCommand)            => EventAction::shell_command(status),
463            Self::InputSimple(InputSimple::Remote)                  => EventAction::remote_mount(status),
464            Self::InputSimple(InputSimple::CloudNewdir)             => EventAction::cloud_enter_newdir_mode(status),
465            Self::Navigate(Navigate::History)                       => EventAction::history(status),
466            Self::Navigate(Navigate::Shortcut)                      => EventAction::shortcut(status),
467            Self::Navigate(Navigate::Trash)                         => EventAction::trash_open(status),
468            Self::Navigate(Navigate::Marks(markaction)) => match markaction {
469                MarkAction::Jump => EventAction::marks_jump(status),
470                MarkAction::New => EventAction::marks_new(status),
471            },
472            Self::Navigate(Navigate::TempMarks(markaction)) => match markaction {
473                MarkAction::Jump => EventAction::temp_marks_jump(status),
474                MarkAction::New => EventAction::temp_marks_new(status),
475            },
476            Self::Navigate(Navigate::Mount)                         => EventAction::mount(status),
477            Self::Navigate(Navigate::Picker)                        => unreachable!("Can't reenter picker from itself"),
478            Self::Navigate(Navigate::Compress)                      => EventAction::compress(status),
479            Self::Navigate(Navigate::TuiApplication)                => EventAction::tui_menu(status),
480            Self::Navigate(Navigate::CliApplication)                => EventAction::cli_menu(status),
481            Self::Navigate(Navigate::Context)                       => EventAction::context(status),
482            Self::Navigate(Navigate::Cloud)                         => EventAction::cloud_drive(status),
483            Self::Navigate(Navigate::Flagged)                       => EventAction::display_flagged(status),
484            Self::NeedConfirmation(NeedConfirmation::Copy)          => EventAction::copy_paste(status),
485            Self::NeedConfirmation(NeedConfirmation::Delete)        => EventAction::delete_file(status),
486            Self::NeedConfirmation(NeedConfirmation::DeleteCloud)   => EventAction::cloud_enter_delete_mode(status),
487            Self::NeedConfirmation(NeedConfirmation::Move)          => EventAction::cut_paste(status),
488            Self::NeedConfirmation(NeedConfirmation::BulkAction)    => EventAction::bulk(status),
489            Self::NeedConfirmation(NeedConfirmation::EmptyTrash)    => EventAction::trash_empty(status),
490            Self::Nothing                                           => Ok(()),
491        }
492    }
493}
494
495/// What kind of content is displayed in the main window of this tab.
496/// Directory (all files of a directory), Tree (all files and children up to a certain depth),
497/// preview of a content (file, command output...) or fuzzy finder of file.
498#[derive(Default, PartialEq, Clone, Copy)]
499pub enum Display {
500    #[default]
501    /// Display the files like `ls -lh` does
502    Directory,
503    /// Display files like `tree` does
504    Tree,
505    /// Preview a file or directory
506    Preview,
507    /// Fuzzy finder of something
508    Fuzzy,
509}
510
511impl Display {
512    fn is(&self, other: Self) -> bool {
513        self == &other
514    }
515
516    pub fn is_tree(&self) -> bool {
517        self.is(Self::Tree)
518    }
519
520    pub fn is_preview(&self) -> bool {
521        self.is(Self::Preview)
522    }
523
524    pub fn is_fuzzy(&self) -> bool {
525        self.is(Self::Fuzzy)
526    }
527
528    pub fn is_directory(&self) -> bool {
529        self.is(Self::Directory)
530    }
531}