use serde::Deserialize;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum Action {
Quit,
Yank,
Cut,
Paste,
Trash,
Create,
Rename,
CopyPath,
SearchFolders,
Zoxide,
Open,
OpenWith,
Sort,
ToggleView,
ToggleHidden,
ScrollPreviewLeft,
ScrollPreviewRight,
ScrollPreviewUp,
ScrollPreviewDown,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct KeyBindings {
pub quit: char,
pub yank: char,
pub cut: char,
pub paste: char,
pub trash: char,
pub create: char,
pub rename: char,
pub copy_path: char,
pub search_folders: char,
pub zoxide: char,
pub open: char,
pub open_with: char,
pub sort: char,
pub toggle_view: char,
pub toggle_hidden: char,
pub scroll_preview_left: char,
pub scroll_preview_right: char,
pub scroll_preview_up: char,
pub scroll_preview_down: char,
}
const RESERVED_CHARS: &[char] = &[
'h', 'j', 'k', 'l', 'g', 'G', '?', '[', ']', '+', '=', '-', '_', ' ', ];
impl Default for KeyBindings {
fn default() -> Self {
Self {
quit: 'q',
yank: 'y',
cut: 'x',
paste: 'p',
trash: 'd',
create: 'a',
rename: 'r',
copy_path: 'c',
search_folders: 'f',
zoxide: 'z',
open: 'o',
open_with: 'O',
sort: 's',
toggle_view: 'v',
toggle_hidden: '.',
scroll_preview_left: 'H',
scroll_preview_right: 'L',
scroll_preview_up: 'K',
scroll_preview_down: 'J',
}
}
}
#[derive(Deserialize, Default)]
pub(super) struct KeysConfigOverride {
quit: Option<String>,
yank: Option<String>,
cut: Option<String>,
paste: Option<String>,
trash: Option<String>,
create: Option<String>,
rename: Option<String>,
copy_path: Option<String>,
search_folders: Option<String>,
zoxide: Option<String>,
open: Option<String>,
open_with: Option<String>,
sort: Option<String>,
toggle_view: Option<String>,
toggle_hidden: Option<String>,
scroll_preview_left: Option<String>,
scroll_preview_right: Option<String>,
scroll_preview_up: Option<String>,
scroll_preview_down: Option<String>,
}
impl KeyBindings {
pub(crate) fn action_for(&self, c: char) -> Option<Action> {
match c {
_ if c == self.quit => Some(Action::Quit),
_ if c == self.yank => Some(Action::Yank),
_ if c == self.cut => Some(Action::Cut),
_ if c == self.paste => Some(Action::Paste),
_ if c == self.trash => Some(Action::Trash),
_ if c == self.create => Some(Action::Create),
_ if c == self.rename => Some(Action::Rename),
_ if c == self.copy_path => Some(Action::CopyPath),
_ if c == self.search_folders => Some(Action::SearchFolders),
_ if c == self.zoxide => Some(Action::Zoxide),
_ if c == self.open => Some(Action::Open),
_ if c == self.open_with => Some(Action::OpenWith),
_ if c == self.sort => Some(Action::Sort),
_ if c == self.toggle_view => Some(Action::ToggleView),
_ if c == self.toggle_hidden => Some(Action::ToggleHidden),
_ if c == self.scroll_preview_left => Some(Action::ScrollPreviewLeft),
_ if c == self.scroll_preview_right => Some(Action::ScrollPreviewRight),
_ if c == self.scroll_preview_up => Some(Action::ScrollPreviewUp),
_ if c == self.scroll_preview_down => Some(Action::ScrollPreviewDown),
_ => None,
}
}
#[cfg(test)]
pub(crate) fn from_toml_str(s: &str) -> Self {
super::Config::from_str(s)
.map(|config| config.keys)
.unwrap_or_else(|_| Self::default())
}
pub(super) fn from_override(overrides: KeysConfigOverride, defaults: &Self) -> Self {
let raw: [(&str, Option<String>, char); 19] = [
("quit", overrides.quit, defaults.quit),
("yank", overrides.yank, defaults.yank),
("cut", overrides.cut, defaults.cut),
("paste", overrides.paste, defaults.paste),
("trash", overrides.trash, defaults.trash),
("create", overrides.create, defaults.create),
("rename", overrides.rename, defaults.rename),
("copy_path", overrides.copy_path, defaults.copy_path),
(
"search_folders",
overrides.search_folders,
defaults.search_folders,
),
("zoxide", overrides.zoxide, defaults.zoxide),
("open", overrides.open, defaults.open),
("open_with", overrides.open_with, defaults.open_with),
("sort", overrides.sort, defaults.sort),
("toggle_view", overrides.toggle_view, defaults.toggle_view),
(
"toggle_hidden",
overrides.toggle_hidden,
defaults.toggle_hidden,
),
(
"scroll_preview_left",
overrides.scroll_preview_left,
defaults.scroll_preview_left,
),
(
"scroll_preview_right",
overrides.scroll_preview_right,
defaults.scroll_preview_right,
),
(
"scroll_preview_up",
overrides.scroll_preview_up,
defaults.scroll_preview_up,
),
(
"scroll_preview_down",
overrides.scroll_preview_down,
defaults.scroll_preview_down,
),
];
let mut candidates: [(char, bool); 19] = [(' ', false); 19];
for (index, (name, override_str, default)) in raw.iter().enumerate() {
candidates[index] = match override_str {
None => (*default, false),
Some(value) => {
let mut chars = value.chars();
match (chars.next(), chars.next()) {
(Some(c), None) if RESERVED_CHARS.contains(&c) => {
eprintln!(
"elio: keys.{name}: '{c}' is reserved and cannot be rebound; \
using default '{default}'"
);
(*default, false)
}
(Some(c), None) if c.is_control() => {
eprintln!(
"elio: keys.{name}: control characters cannot be used as key \
bindings; using default '{default}'"
);
(*default, false)
}
(Some(c), None) => (c, true),
_ => {
eprintln!(
"elio: keys.{name}: {value:?} is not a single character; \
using default '{default}'"
);
(*default, false)
}
}
}
};
}
loop {
let mut changed = false;
for index in 0..19 {
if !candidates[index].1 {
continue;
}
let candidate = candidates[index].0;
let collision = (0..19)
.filter(|&other_index| other_index != index)
.any(|other_index| candidates[other_index].0 == candidate);
if collision {
let (name, _, default) = &raw[index];
let other = raw
.iter()
.enumerate()
.filter(|&(other_index, _)| {
other_index != index && candidates[other_index].0 == candidate
})
.map(|(_, (name, _, _))| *name)
.next()
.unwrap_or("another key");
eprintln!(
"elio: keys.{name}: '{candidate}' is already bound to {other}; \
using default '{default}'"
);
candidates[index] = (*default, false);
changed = true;
}
}
if !changed {
break;
}
}
let resolved = |index: usize| candidates[index].0;
Self {
quit: resolved(0),
yank: resolved(1),
cut: resolved(2),
paste: resolved(3),
trash: resolved(4),
create: resolved(5),
rename: resolved(6),
copy_path: resolved(7),
search_folders: resolved(8),
zoxide: resolved(9),
open: resolved(10),
open_with: resolved(11),
sort: resolved(12),
toggle_view: resolved(13),
toggle_hidden: resolved(14),
scroll_preview_left: resolved(15),
scroll_preview_right: resolved(16),
scroll_preview_up: resolved(17),
scroll_preview_down: resolved(18),
}
}
}