fm/modes/
mode.rs

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