use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use std::collections::HashMap;
use std::time::{Duration, Instant};
#[derive(Copy, Clone, Debug, PartialEq)]
pub(crate) enum Action {
Nav(NavAction),
File(FileAction),
System(SystemAction),
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub(crate) enum NavAction {
GoParent,
GoIntoDir,
GoUp,
GoDown,
GoToTop,
GoToPath,
GoToHome,
ToggleMarker,
ClearMarker,
ClearFilter,
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub(crate) enum FileAction {
Delete,
Copy,
Open,
Paste,
Rename,
Create,
CreateDirectory,
Filter,
ShowInfo,
Find,
MoveFile,
AlternateDelete,
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub(crate) enum SystemAction {
Quit,
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub(crate) enum PrefixCommand {
Nav(NavAction),
}
#[derive(Hash, Eq, PartialEq, Copy, Clone, Debug)]
pub(crate) struct Key {
pub(crate) code: KeyCode,
pub(crate) modifiers: KeyModifiers,
}
pub(crate) struct Keymap {
map: HashMap<Key, Action>,
gmap: HashMap<KeyCode, PrefixCommand>,
}
impl Keymap {
pub(crate) fn from_config(config: &crate::config::Config) -> Self {
let mut map = HashMap::new();
let mut gmap = HashMap::new();
let keys = config.keys();
macro_rules! bind {
($keys:expr, $action:expr) => {
bind($keys, $action, &mut map);
};
}
macro_rules! bind_prefix {
($keys:expr, $action:expr, $prefix:expr) => {
bind_prefix($keys, $prefix, &mut gmap);
};
}
bind!(keys.go_parent(), Action::Nav(NavAction::GoParent));
bind!(keys.go_into_dir(), Action::Nav(NavAction::GoIntoDir));
bind!(keys.go_up(), Action::Nav(NavAction::GoUp));
bind!(keys.go_down(), Action::Nav(NavAction::GoDown));
bind!(keys.toggle_marker(), Action::Nav(NavAction::ToggleMarker));
bind!(keys.open_file(), Action::File(FileAction::Open));
bind!(keys.delete(), Action::File(FileAction::Delete));
bind!(keys.copy(), Action::File(FileAction::Copy));
bind!(keys.paste(), Action::File(FileAction::Paste));
bind!(keys.move_file(), Action::File(FileAction::MoveFile));
bind!(keys.rename(), Action::File(FileAction::Rename));
bind!(keys.create(), Action::File(FileAction::Create));
bind!(
keys.create_directory(),
Action::File(FileAction::CreateDirectory)
);
bind!(keys.filter(), Action::File(FileAction::Filter));
bind!(keys.quit(), Action::System(SystemAction::Quit));
bind!(keys.show_info(), Action::File(FileAction::ShowInfo));
bind!(keys.find(), Action::File(FileAction::Find));
bind!(keys.clear_markers(), Action::Nav(NavAction::ClearMarker));
bind!(keys.clear_filter(), Action::Nav(NavAction::ClearFilter));
bind!(
keys.alternate_delete(),
Action::File(FileAction::AlternateDelete)
);
bind_prefix!(
keys.go_to_top(),
Action::Nav(NavAction::GoToTop),
PrefixCommand::Nav(NavAction::GoToTop)
);
bind_prefix!(
keys.go_to_home(),
Action::Nav(NavAction::GoToHome),
PrefixCommand::Nav(NavAction::GoToHome)
);
bind_prefix!(
keys.go_to_path(),
Action::Nav(NavAction::GoToPath),
PrefixCommand::Nav(NavAction::GoToPath)
);
Keymap { map, gmap }
}
pub(crate) fn lookup(&self, key: KeyEvent) -> Option<Action> {
let k = Key {
code: key.code,
modifiers: key.modifiers,
};
self.map.get(&k).copied()
}
pub(crate) fn gmap(&self) -> &HashMap<KeyCode, PrefixCommand> {
&self.gmap
}
}
pub(crate) struct KeyPrefix {
state: PrefixState,
last_time: Option<Instant>,
timeout: Duration,
started: bool,
exited: bool,
}
#[derive(Copy, Clone, Debug, PartialEq)]
enum PrefixState {
None,
G,
}
impl KeyPrefix {
pub(crate) fn new(timeout: Duration) -> Self {
Self {
state: PrefixState::None,
last_time: None,
timeout,
started: false,
exited: false,
}
}
pub(crate) fn feed(
&mut self,
key: &KeyEvent,
gmap: &HashMap<KeyCode, PrefixCommand>,
) -> Option<PrefixCommand> {
self.started = false;
self.exited = false;
let now = Instant::now();
match self.state {
PrefixState::None => {
if key.code == KeyCode::Char('g') && key.modifiers.is_empty() {
self.state = PrefixState::G;
self.last_time = Some(now);
self.started = true;
None
} else {
None
}
}
PrefixState::G => {
let elapsed = self
.last_time
.map_or(Duration::MAX, |t| now.duration_since(t));
self.state = PrefixState::None;
self.last_time = None;
self.exited = true;
if elapsed <= self.timeout {
gmap.get(&key.code).copied()
} else {
None
}
}
}
}
#[inline]
pub(crate) fn started_prefix(&self) -> bool {
self.started
}
#[inline]
pub(crate) fn exited_prefix(&self) -> bool {
self.exited
}
pub(crate) fn is_g_state(&self) -> bool {
self.state == PrefixState::G
}
pub(crate) fn expired(&self) -> bool {
self.state == PrefixState::G
&& self
.last_time
.is_some_and(|time| time.elapsed() >= self.timeout)
}
pub(crate) fn cancel(&mut self) {
self.state = PrefixState::None;
self.last_time = None;
self.exited = true;
}
}
fn parse_key(s: &str) -> Option<Key> {
let mut modifiers = KeyModifiers::NONE;
let mut code: Option<KeyCode> = None;
for part in s.split('+') {
match part {
"Ctrl" | "Control" => modifiers |= KeyModifiers::CONTROL,
"Shift" => modifiers |= KeyModifiers::SHIFT,
"Alt" => modifiers |= KeyModifiers::ALT,
"Up" => code = Some(KeyCode::Up),
"Down" => code = Some(KeyCode::Down),
"Left" => code = Some(KeyCode::Left),
"Right" => code = Some(KeyCode::Right),
"Enter" => code = Some(KeyCode::Enter),
"Esc" => code = Some(KeyCode::Esc),
"Backspace" => code = Some(KeyCode::Backspace),
"Tab" => code = Some(KeyCode::Tab),
p if p.starts_with('F') => {
let n = p[1..].parse().ok()?;
code = Some(KeyCode::F(n));
}
p if p.len() == 1 => {
let mut char = p.chars().next()?;
if modifiers.contains(KeyModifiers::SHIFT) {
char = char.to_ascii_uppercase();
}
code = Some(KeyCode::Char(char));
}
_ => return None,
}
}
Some(Key {
code: code?,
modifiers,
})
}
fn bind(key_list: &[String], action: Action, map: &mut HashMap<Key, Action>) {
for k in key_list {
if let Some(key) = parse_key(k) {
map.insert(key, action);
}
}
}
fn bind_prefix(
key_list: &[String],
prefix: PrefixCommand,
gmap: &mut HashMap<KeyCode, PrefixCommand>,
) {
for k in key_list {
if let Some(key) = parse_key(k)
&& key.modifiers.is_empty()
&& let KeyCode::Char(c) = key.code
{
gmap.insert(KeyCode::Char(c), prefix);
}
}
}