use crossterm::event::{MouseButton, MouseEvent, MouseEventKind};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeyAction {
MoveUp,
MoveDown,
MoveLeft,
MoveRight,
JumpToTop,
JumpToBottom,
PageUp,
PageDown,
ToggleExpand,
DrillDown,
NavigateBack,
ToggleMark,
EnterVisual,
ClearMarks,
Yank,
Cut,
Paste,
Delete,
Rename,
CreateFile,
CreateDirectory,
Take,
Undo,
ToggleDetails,
ToggleHelp,
ToggleTheme,
ToggleLayout,
ToggleSizeMode,
CyclePreviewMode,
PreviewDown,
PreviewUp,
OpenSettings,
Refresh,
Search,
Sort,
ReverseSort,
CommandMode,
GoTo,
Cancel,
NextView,
PrevView,
SetBookmark,
JumpToBookmark,
ShowBookmarkList,
NewDirTab,
CloseDirTab,
NextDirTab,
PrevDirTab,
DirTab(u8),
Quit,
ForceQuit,
None,
}
impl KeyAction {
pub fn changes_files(self) -> bool {
matches!(
self,
Self::Paste
| Self::Delete
| Self::Rename
| Self::CreateFile
| Self::CreateDirectory
| Self::Take
| Self::Undo
| Self::Refresh
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MouseAction {
Click { x: u16, y: u16 },
DoubleClick { x: u16, y: u16 },
RightClick { x: u16, y: u16 },
ScrollUp { x: u16, y: u16 },
ScrollDown { x: u16, y: u16 },
DragTo { x: u16, y: u16 },
DragEnd,
None,
}
impl MouseAction {
pub fn from_mouse_event(event: MouseEvent) -> Self {
let x = event.column;
let y = event.row;
match event.kind {
MouseEventKind::Down(MouseButton::Left) => MouseAction::Click { x, y },
MouseEventKind::Down(MouseButton::Right) => MouseAction::RightClick { x, y },
MouseEventKind::Up(_) => MouseAction::DragEnd,
MouseEventKind::Drag(MouseButton::Left) => MouseAction::DragTo { x, y },
MouseEventKind::ScrollUp => MouseAction::ScrollUp { x, y },
MouseEventKind::ScrollDown => MouseAction::ScrollDown { x, y },
_ => MouseAction::None,
}
}
}
#[derive(Debug, Default)]
pub struct ClickTracker {
last: Option<(std::time::Instant, u16)>,
}
impl ClickTracker {
pub const WINDOW: std::time::Duration = std::time::Duration::from_millis(400);
pub fn classify(&mut self, action: MouseAction, now: std::time::Instant) -> MouseAction {
match action {
MouseAction::Click { x, y } => {
if let Some((at, row)) = self.last.take()
&& row == y
&& now.saturating_duration_since(at) <= Self::WINDOW
{
return MouseAction::DoubleClick { x, y };
}
self.last = Some((now, y));
action
}
MouseAction::DragEnd | MouseAction::None => action,
_ => {
self.last = None;
action
}
}
}
}
pub fn get_command_help() -> Vec<(&'static str, &'static str)> {
vec![
(":q :quit", "Quit application"),
(":cd <path>", "Change directory"),
(":touch <name>", "Create file"),
(":mkdir <name>", "Create directory"),
(":take <name>", "Create dir and cd into it"),
(":yank :y", "Copy to clipboard"),
(":cut :x", "Cut to clipboard"),
(":paste :p", "Paste from clipboard"),
(":delete :rm", "Delete marked items"),
(":rename <name>", "Rename current item"),
(":rename-bulk :brn", "Bulk rename marked items"),
(":clear", "Clear all marks"),
(
":theme [name]",
"Next theme, or gestalt, latte, nord, dracula, ansi",
),
(":layout tree|miller", "Set layout"),
(":sort <mode>", "Set sort (size/name/date/count)"),
(":treemap :map", "Show disk-usage treemap"),
(":analyze", "Run duplicate/age analysis"),
(":bookmark [key]", "Set bookmark (or list all)"),
(":bookmarks", "Show bookmark list"),
(":delmark <key>", "Delete a bookmark"),
(":help", "Show help"),
]
}
#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::KeyModifiers;
#[test]
fn a_quick_second_click_on_the_same_row_is_a_double_click() {
let start = std::time::Instant::now();
let at = |millis| start + std::time::Duration::from_millis(millis);
let click = |y| MouseAction::Click { x: 3, y };
let mut clicks = ClickTracker::default();
assert_eq!(clicks.classify(click(5), at(0)), click(5));
assert_eq!(
clicks.classify(MouseAction::DragEnd, at(50)),
MouseAction::DragEnd
);
assert_eq!(
clicks.classify(click(5), at(300)),
MouseAction::DoubleClick { x: 3, y: 5 }
);
assert_eq!(clicks.classify(click(5), at(350)), click(5));
assert_eq!(clicks.classify(click(5), at(900)), click(5));
assert_eq!(clicks.classify(click(6), at(950)), click(6));
clicks.classify(MouseAction::ScrollDown { x: 3, y: 6 }, at(960));
assert_eq!(clicks.classify(click(6), at(970)), click(6));
}
#[test]
fn mouse_events_map_to_actions() {
use crossterm::event::{MouseButton, MouseEvent, MouseEventKind};
let at = |kind| MouseEvent {
kind,
column: 4,
row: 7,
modifiers: KeyModifiers::NONE,
};
assert_eq!(
MouseAction::from_mouse_event(at(MouseEventKind::Down(MouseButton::Left))),
MouseAction::Click { x: 4, y: 7 }
);
assert_eq!(
MouseAction::from_mouse_event(at(MouseEventKind::Down(MouseButton::Right))),
MouseAction::RightClick { x: 4, y: 7 }
);
assert_eq!(
MouseAction::from_mouse_event(at(MouseEventKind::ScrollUp)),
MouseAction::ScrollUp { x: 4, y: 7 }
);
assert_eq!(
MouseAction::from_mouse_event(at(MouseEventKind::ScrollDown)),
MouseAction::ScrollDown { x: 4, y: 7 }
);
assert_eq!(
MouseAction::from_mouse_event(at(MouseEventKind::Drag(MouseButton::Left))),
MouseAction::DragTo { x: 4, y: 7 }
);
assert_eq!(
MouseAction::from_mouse_event(at(MouseEventKind::Up(MouseButton::Left))),
MouseAction::DragEnd
);
assert_eq!(
MouseAction::from_mouse_event(at(MouseEventKind::Moved)),
MouseAction::None
);
}
#[test]
fn command_help_lists_each_command_once() {
let commands = get_command_help();
assert!(!commands.is_empty());
let mut names: Vec<&str> = commands.iter().map(|(name, _)| *name).collect();
let count = names.len();
names.sort_unstable();
names.dedup();
assert_eq!(names.len(), count, "duplicate entries in the command help");
for (name, description) in &commands {
assert!(name.starts_with(':'), "{name} is not a command");
assert!(!description.is_empty(), "{name} has no description");
}
}
}