You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
800 lines
26 KiB
800 lines
26 KiB
use itertools::Itertools;
|
|
use std::collections::HashMap;
|
|
use std::fs::File;
|
|
use std::io::Read;
|
|
use std::{
|
|
fmt,
|
|
path::{Path, PathBuf},
|
|
};
|
|
|
|
#[derive(Debug)]
|
|
pub enum Error {
|
|
ConfigNotFound,
|
|
Io(std::io::Error),
|
|
InvalidConfig(ParseError),
|
|
}
|
|
|
|
#[derive(Debug, PartialEq)]
|
|
pub enum ParseError {
|
|
// u32 is the line number where an error occured
|
|
UnknownSymbol(PathBuf, u32),
|
|
InvalidModifier(PathBuf, u32),
|
|
InvalidKeysym(PathBuf, u32),
|
|
}
|
|
|
|
impl From<std::io::Error> for Error {
|
|
fn from(val: std::io::Error) -> Self {
|
|
if val.kind() == std::io::ErrorKind::NotFound {
|
|
Error::ConfigNotFound
|
|
} else {
|
|
Error::Io(val)
|
|
}
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for Error {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match &*self {
|
|
Error::ConfigNotFound => "Config file not found.".fmt(f),
|
|
|
|
Error::Io(io_err) => format!("I/O Error while parsing config file: {}", io_err).fmt(f),
|
|
Error::InvalidConfig(parse_err) => match parse_err {
|
|
ParseError::UnknownSymbol(path, line_nr) => format!(
|
|
"Error parsing config file {:?}. Unknown symbol at line {}.",
|
|
path, line_nr
|
|
)
|
|
.fmt(f),
|
|
ParseError::InvalidKeysym(path, line_nr) => format!(
|
|
"Error parsing config file {:?}. Invalid keysym at line {}.",
|
|
path, line_nr
|
|
)
|
|
.fmt(f),
|
|
ParseError::InvalidModifier(path, line_nr) => format!(
|
|
"Error parsing config file {:?}. Invalid modifier at line {}.",
|
|
path, line_nr
|
|
)
|
|
.fmt(f),
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
pub const IMPORT_STATEMENT: &str = "include";
|
|
pub const UNBIND_STATEMENT: &str = "ignore";
|
|
pub const MODE_STATEMENT: &str = "mode";
|
|
pub const MODE_END_STATEMENT: &str = "endmode";
|
|
pub const MODE_ENTER_STATEMENT: &str = "@enter";
|
|
pub const MODE_ESCAPE_STATEMENT: &str = "@escape";
|
|
pub const ODILIA_SEND_STATEMENT: &str = "@odilia";
|
|
|
|
#[derive(Debug, PartialEq, Clone)]
|
|
pub struct Config {
|
|
pub path: PathBuf,
|
|
pub contents: String,
|
|
pub imports: Vec<PathBuf>,
|
|
}
|
|
|
|
pub fn load_file_contents(path: &Path) -> Result<String, Error> {
|
|
let mut file = File::open(path)?;
|
|
let mut contents = String::new();
|
|
file.read_to_string(&mut contents)?;
|
|
Ok(contents)
|
|
}
|
|
|
|
impl Config {
|
|
pub fn get_imports(contents: &str) -> Result<Vec<PathBuf>, Error> {
|
|
let mut imports = Vec::new();
|
|
for line in contents.lines() {
|
|
if line.split(' ').next().unwrap() == IMPORT_STATEMENT {
|
|
if let Some(import_path) = line.split(' ').nth(1) {
|
|
imports.push(Path::new(import_path).to_path_buf());
|
|
}
|
|
}
|
|
}
|
|
Ok(imports)
|
|
}
|
|
|
|
pub fn new(path: &Path) -> Result<Self, Error> {
|
|
let contents = load_file_contents(path)?;
|
|
let imports = Self::get_imports(&contents)?;
|
|
Ok(Config { path: path.to_path_buf(), contents, imports })
|
|
}
|
|
|
|
pub fn load_to_configs(&self) -> Result<Vec<Self>, Error> {
|
|
let mut configs = Vec::new();
|
|
for import in &self.imports {
|
|
configs.push(Self::new(import)?)
|
|
}
|
|
Ok(configs)
|
|
}
|
|
|
|
pub fn load_and_merge(config: Self) -> Result<Vec<Self>, Error> {
|
|
let mut configs = vec![config];
|
|
let mut prev_count = 0;
|
|
let mut current_count = configs.len();
|
|
while prev_count != current_count {
|
|
prev_count = configs.len();
|
|
for config in configs.clone() {
|
|
for import in Self::load_to_configs(&config)? {
|
|
if !configs.contains(&import) {
|
|
configs.push(import);
|
|
}
|
|
}
|
|
}
|
|
current_count = configs.len();
|
|
}
|
|
Ok(configs)
|
|
}
|
|
}
|
|
|
|
pub fn load(path: &Path) -> Result<Vec<Mode>, Error> {
|
|
let config_self = Config::new(path)?;
|
|
let mut configs: Vec<Config> = Config::load_and_merge(config_self.clone())?;
|
|
configs.remove(0);
|
|
configs.push(config_self);
|
|
let mut modes: Vec<Mode> = vec![Mode::default()];
|
|
for config in configs {
|
|
let mut output = parse_contents(path.to_path_buf(), config.contents)?;
|
|
for hotkey in output[0].hotkeys.drain(..) {
|
|
modes[0].hotkeys.retain(|hk| hk.keybinding != hotkey.keybinding);
|
|
modes[0].hotkeys.push(hotkey);
|
|
}
|
|
for unbind in output[0].unbinds.drain(..) {
|
|
modes[0].hotkeys.retain(|hk| hk.keybinding != unbind);
|
|
}
|
|
output.remove(0);
|
|
for mut mode in output {
|
|
mode.hotkeys.retain(|x| !mode.unbinds.contains(&x.keybinding));
|
|
modes.push(mode);
|
|
}
|
|
}
|
|
Ok(modes)
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct KeyBinding {
|
|
pub keysym: Option<evdev::Key>,
|
|
pub modifiers: Vec<Modifier>,
|
|
pub send: bool,
|
|
pub on_release: bool,
|
|
}
|
|
|
|
impl PartialEq for KeyBinding {
|
|
fn eq(&self, other: &Self) -> bool {
|
|
self.keysym == other.keysym
|
|
&& self.modifiers.iter().all(|modifier| other.modifiers.contains(modifier))
|
|
&& self.modifiers.len() == other.modifiers.len()
|
|
&& self.send == other.send
|
|
&& self.on_release == other.on_release
|
|
}
|
|
}
|
|
|
|
pub trait Prefix {
|
|
fn send(self) -> Self;
|
|
fn on_release(self) -> Self;
|
|
}
|
|
|
|
pub trait Value {
|
|
fn keysym(&self) -> Option<evdev::Key>;
|
|
fn modifiers(&self) -> Vec<Modifier>;
|
|
fn is_send(&self) -> bool;
|
|
fn is_on_release(&self) -> bool;
|
|
}
|
|
|
|
impl KeyBinding {
|
|
pub fn new(keysym: Option<evdev::Key>, modifiers: Vec<Modifier>) -> Self {
|
|
KeyBinding { keysym, modifiers, send: false, on_release: false }
|
|
}
|
|
pub fn on_release(mut self) -> Self {
|
|
self.on_release = true;
|
|
self
|
|
}
|
|
}
|
|
|
|
impl Prefix for KeyBinding {
|
|
fn send(mut self) -> Self {
|
|
self.send = true;
|
|
self
|
|
}
|
|
fn on_release(mut self) -> Self {
|
|
self.on_release = true;
|
|
self
|
|
}
|
|
}
|
|
|
|
impl Value for KeyBinding {
|
|
fn keysym(&self) -> Option<evdev::Key> {
|
|
self.keysym
|
|
}
|
|
fn modifiers(&self) -> Vec<Modifier> {
|
|
self.clone().modifiers
|
|
}
|
|
fn is_send(&self) -> bool {
|
|
self.send
|
|
}
|
|
fn is_on_release(&self) -> bool {
|
|
self.on_release
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub struct Hotkey {
|
|
pub keybinding: KeyBinding,
|
|
pub command: String,
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Eq, Copy, Clone, Hash)]
|
|
pub enum Modifier {
|
|
Super,
|
|
Alt,
|
|
Control,
|
|
Shift,
|
|
CapsLock,
|
|
Any,
|
|
}
|
|
|
|
impl Hotkey {
|
|
pub fn from_keybinding(keybinding: KeyBinding, command: String) -> Self {
|
|
Hotkey { keybinding, command }
|
|
}
|
|
#[cfg(test)]
|
|
pub fn new(keysym: Option<evdev::Key>, modifiers: Vec<Modifier>, command: String) -> Self {
|
|
Hotkey { keybinding: KeyBinding::new(keysym, modifiers), command }
|
|
}
|
|
}
|
|
|
|
impl Prefix for Hotkey {
|
|
fn send(mut self) -> Self {
|
|
self.keybinding.send = true;
|
|
self
|
|
}
|
|
fn on_release(mut self) -> Self {
|
|
self.keybinding.on_release = true;
|
|
self
|
|
}
|
|
}
|
|
|
|
impl Value for &Hotkey {
|
|
fn keysym(&self) -> Option<evdev::Key> {
|
|
self.keybinding.keysym
|
|
}
|
|
fn modifiers(&self) -> Vec<Modifier> {
|
|
self.keybinding.clone().modifiers
|
|
}
|
|
fn is_send(&self) -> bool {
|
|
self.keybinding.send
|
|
}
|
|
fn is_on_release(&self) -> bool {
|
|
self.keybinding.on_release
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub struct Mode {
|
|
pub name: String,
|
|
pub hotkeys: Vec<Hotkey>,
|
|
pub unbinds: Vec<KeyBinding>,
|
|
}
|
|
|
|
impl Mode {
|
|
pub fn new(name: String) -> Self {
|
|
Mode { name, hotkeys: Vec::new(), unbinds: Vec::new() }
|
|
}
|
|
|
|
pub fn default() -> Self {
|
|
Self::new("normal".to_string())
|
|
}
|
|
}
|
|
|
|
pub fn parse_contents(path: PathBuf, contents: String) -> Result<Vec<Mode>, Error> {
|
|
// Don't forget to update valid key list on the man page if you do change this list.
|
|
let key_to_evdev_key: HashMap<&str, evdev::Key> = HashMap::from([
|
|
("q", evdev::Key::KEY_Q),
|
|
("w", evdev::Key::KEY_W),
|
|
("e", evdev::Key::KEY_E),
|
|
("r", evdev::Key::KEY_R),
|
|
("t", evdev::Key::KEY_T),
|
|
("y", evdev::Key::KEY_Y),
|
|
("u", evdev::Key::KEY_U),
|
|
("i", evdev::Key::KEY_I),
|
|
("o", evdev::Key::KEY_O),
|
|
("p", evdev::Key::KEY_P),
|
|
("a", evdev::Key::KEY_A),
|
|
("s", evdev::Key::KEY_S),
|
|
("d", evdev::Key::KEY_D),
|
|
("f", evdev::Key::KEY_F),
|
|
("g", evdev::Key::KEY_G),
|
|
("h", evdev::Key::KEY_H),
|
|
("j", evdev::Key::KEY_J),
|
|
("k", evdev::Key::KEY_K),
|
|
("l", evdev::Key::KEY_L),
|
|
("z", evdev::Key::KEY_Z),
|
|
("x", evdev::Key::KEY_X),
|
|
("c", evdev::Key::KEY_C),
|
|
("v", evdev::Key::KEY_V),
|
|
("b", evdev::Key::KEY_B),
|
|
("n", evdev::Key::KEY_N),
|
|
("m", evdev::Key::KEY_M),
|
|
("1", evdev::Key::KEY_1),
|
|
("2", evdev::Key::KEY_2),
|
|
("3", evdev::Key::KEY_3),
|
|
("4", evdev::Key::KEY_4),
|
|
("5", evdev::Key::KEY_5),
|
|
("6", evdev::Key::KEY_6),
|
|
("7", evdev::Key::KEY_7),
|
|
("8", evdev::Key::KEY_8),
|
|
("9", evdev::Key::KEY_9),
|
|
("0", evdev::Key::KEY_0),
|
|
("escape", evdev::Key::KEY_ESC),
|
|
("backspace", evdev::Key::KEY_BACKSPACE),
|
|
//("capslock", evdev::Key::KEY_CAPSLOCK),
|
|
("return", evdev::Key::KEY_ENTER),
|
|
("enter", evdev::Key::KEY_ENTER),
|
|
("tab", evdev::Key::KEY_TAB),
|
|
("space", evdev::Key::KEY_SPACE),
|
|
("plus", evdev::Key::KEY_KPPLUS), // Shouldn't this be kpplus?
|
|
("kp0", evdev::Key::KEY_KP0),
|
|
("kp1", evdev::Key::KEY_KP1),
|
|
("kp2", evdev::Key::KEY_KP2),
|
|
("kp3", evdev::Key::KEY_KP3),
|
|
("kp4", evdev::Key::KEY_KP4),
|
|
("kp5", evdev::Key::KEY_KP5),
|
|
("kp6", evdev::Key::KEY_KP6),
|
|
("kp7", evdev::Key::KEY_KP7),
|
|
("kp8", evdev::Key::KEY_KP8),
|
|
("kp9", evdev::Key::KEY_KP9),
|
|
("kpasterisk", evdev::Key::KEY_KPASTERISK),
|
|
("kpcomma", evdev::Key::KEY_KPCOMMA),
|
|
("kpdot", evdev::Key::KEY_KPDOT),
|
|
("kpenter", evdev::Key::KEY_KPENTER),
|
|
("kpequal", evdev::Key::KEY_KPEQUAL),
|
|
("kpjpcomma", evdev::Key::KEY_KPJPCOMMA),
|
|
("kpleftparen", evdev::Key::KEY_KPLEFTPAREN),
|
|
("kpminus", evdev::Key::KEY_KPMINUS),
|
|
("kpplusminus", evdev::Key::KEY_KPPLUSMINUS),
|
|
("kprightparen", evdev::Key::KEY_KPRIGHTPAREN),
|
|
("minus", evdev::Key::KEY_MINUS),
|
|
("-", evdev::Key::KEY_MINUS),
|
|
("equal", evdev::Key::KEY_EQUAL),
|
|
("=", evdev::Key::KEY_EQUAL),
|
|
("grave", evdev::Key::KEY_GRAVE),
|
|
("`", evdev::Key::KEY_GRAVE),
|
|
("print", evdev::Key::KEY_SYSRQ),
|
|
("volumeup", evdev::Key::KEY_VOLUMEUP),
|
|
("xf86audioraisevolume", evdev::Key::KEY_VOLUMEUP),
|
|
("volumedown", evdev::Key::KEY_VOLUMEDOWN),
|
|
("xf86audiolowervolume", evdev::Key::KEY_VOLUMEDOWN),
|
|
("mute", evdev::Key::KEY_MUTE),
|
|
("xf86audiomute", evdev::Key::KEY_MUTE),
|
|
("brightnessup", evdev::Key::KEY_BRIGHTNESSUP),
|
|
("xf86monbrightnessup", evdev::Key::KEY_BRIGHTNESSUP),
|
|
("brightnessdown", evdev::Key::KEY_BRIGHTNESSDOWN),
|
|
("xf86audiomedia", evdev::Key::KEY_MEDIA),
|
|
("xf86audiomicmute", evdev::Key::KEY_MICMUTE),
|
|
("micmute", evdev::Key::KEY_MICMUTE),
|
|
("xf86audionext", evdev::Key::KEY_NEXTSONG),
|
|
("xf86audioplay", evdev::Key::KEY_PLAYPAUSE),
|
|
("xf86audioprev", evdev::Key::KEY_PREVIOUSSONG),
|
|
("xf86audiostop", evdev::Key::KEY_STOP),
|
|
("xf86monbrightnessdown", evdev::Key::KEY_BRIGHTNESSDOWN),
|
|
(",", evdev::Key::KEY_COMMA),
|
|
("comma", evdev::Key::KEY_COMMA),
|
|
(".", evdev::Key::KEY_DOT),
|
|
("dot", evdev::Key::KEY_DOT),
|
|
("period", evdev::Key::KEY_DOT),
|
|
("/", evdev::Key::KEY_SLASH),
|
|
("question", evdev::Key::KEY_QUESTION),
|
|
("slash", evdev::Key::KEY_SLASH),
|
|
("backslash", evdev::Key::KEY_BACKSLASH),
|
|
("leftbrace", evdev::Key::KEY_LEFTBRACE),
|
|
("[", evdev::Key::KEY_LEFTBRACE),
|
|
("bracketleft", evdev::Key::KEY_LEFTBRACE),
|
|
("rightbrace", evdev::Key::KEY_RIGHTBRACE),
|
|
("]", evdev::Key::KEY_RIGHTBRACE),
|
|
("bracketright", evdev::Key::KEY_RIGHTBRACE),
|
|
(";", evdev::Key::KEY_SEMICOLON),
|
|
("scroll_lock", evdev::Key::KEY_SCROLLLOCK),
|
|
("semicolon", evdev::Key::KEY_SEMICOLON),
|
|
("'", evdev::Key::KEY_APOSTROPHE),
|
|
("apostrophe", evdev::Key::KEY_APOSTROPHE),
|
|
("left", evdev::Key::KEY_LEFT),
|
|
("right", evdev::Key::KEY_RIGHT),
|
|
("up", evdev::Key::KEY_UP),
|
|
("down", evdev::Key::KEY_DOWN),
|
|
("pause", evdev::Key::KEY_PAUSE),
|
|
("home", evdev::Key::KEY_HOME),
|
|
("delete", evdev::Key::KEY_DELETE),
|
|
("insert", evdev::Key::KEY_INSERT),
|
|
("end", evdev::Key::KEY_END),
|
|
("pause", evdev::Key::KEY_PAUSE),
|
|
("prior", evdev::Key::KEY_PAGEDOWN),
|
|
("next", evdev::Key::KEY_PAGEUP),
|
|
("pagedown", evdev::Key::KEY_PAGEDOWN),
|
|
("pageup", evdev::Key::KEY_PAGEUP),
|
|
("f1", evdev::Key::KEY_F1),
|
|
("f2", evdev::Key::KEY_F2),
|
|
("f3", evdev::Key::KEY_F3),
|
|
("f4", evdev::Key::KEY_F4),
|
|
("f5", evdev::Key::KEY_F5),
|
|
("f6", evdev::Key::KEY_F6),
|
|
("f7", evdev::Key::KEY_F7),
|
|
("f8", evdev::Key::KEY_F8),
|
|
("f9", evdev::Key::KEY_F9),
|
|
("f10", evdev::Key::KEY_F10),
|
|
("f11", evdev::Key::KEY_F11),
|
|
("f12", evdev::Key::KEY_F12),
|
|
("f13", evdev::Key::KEY_F13),
|
|
("f14", evdev::Key::KEY_F14),
|
|
("f15", evdev::Key::KEY_F15),
|
|
("f16", evdev::Key::KEY_F16),
|
|
("f17", evdev::Key::KEY_F17),
|
|
("f18", evdev::Key::KEY_F18),
|
|
("f19", evdev::Key::KEY_F19),
|
|
("f20", evdev::Key::KEY_F20),
|
|
("f21", evdev::Key::KEY_F21),
|
|
("f22", evdev::Key::KEY_F22),
|
|
("f23", evdev::Key::KEY_F23),
|
|
("f24", evdev::Key::KEY_F24),
|
|
]);
|
|
|
|
// Don't forget to update modifier list on the man page if you do change this list.
|
|
let mod_to_mod_enum: HashMap<&str, Modifier> = HashMap::from([
|
|
("ctrl", Modifier::Control),
|
|
("control", Modifier::Control),
|
|
("super", Modifier::Super),
|
|
("mod4", Modifier::Super),
|
|
("alt", Modifier::Alt),
|
|
("mod1", Modifier::Alt),
|
|
("shift", Modifier::Shift),
|
|
("capslock", Modifier::CapsLock),
|
|
("any", Modifier::Any),
|
|
]);
|
|
|
|
let lines: Vec<&str> = contents.split('\n').collect();
|
|
let mut modes: Vec<Mode> = vec![Mode::default()];
|
|
let mut current_mode: usize = 0;
|
|
|
|
// Go through each line, ignore comments and empty lines, mark lines starting with whitespace
|
|
// as commands, and mark the other lines as keysyms. Mark means storing a line's type and the
|
|
// line number in a vector.
|
|
let mut lines_with_types: Vec<(&str, u32)> = Vec::new();
|
|
for (line_number, line) in lines.iter().enumerate() {
|
|
if line.trim().starts_with('#')
|
|
|| line.split(' ').next().unwrap() == IMPORT_STATEMENT
|
|
|| line.trim().is_empty()
|
|
{
|
|
continue;
|
|
}
|
|
if line.starts_with(' ') || line.starts_with('\t') {
|
|
lines_with_types.push(("command", line_number as u32));
|
|
} else if line.starts_with(UNBIND_STATEMENT) {
|
|
lines_with_types.push(("unbind", line_number as u32));
|
|
} else if line.starts_with(MODE_STATEMENT) {
|
|
lines_with_types.push(("modestart", line_number as u32));
|
|
} else if line.starts_with(MODE_END_STATEMENT) {
|
|
lines_with_types.push(("modeend", line_number as u32));
|
|
} else {
|
|
lines_with_types.push(("keysym", line_number as u32));
|
|
}
|
|
}
|
|
|
|
// Edge case: return a blank vector if no lines detected
|
|
if lines_with_types.is_empty() {
|
|
return Ok(modes);
|
|
}
|
|
|
|
let mut actual_lines: Vec<(&str, u32, String)> = Vec::new();
|
|
|
|
if contents.contains('\\') {
|
|
// Go through lines_with_types, and add the next line over and over until the current line no
|
|
// longer ends with backslash. (Only if the lines have the same type)
|
|
let mut current_line_type = lines_with_types[0].0;
|
|
let mut current_line_number = lines_with_types[0].1;
|
|
let mut current_line_string = String::new();
|
|
let mut continue_backslash;
|
|
|
|
for (line_type, line_number) in lines_with_types {
|
|
if line_type != current_line_type {
|
|
current_line_type = line_type;
|
|
current_line_number = line_number;
|
|
current_line_string = String::new();
|
|
}
|
|
|
|
let line_to_add = lines[line_number as usize].trim();
|
|
continue_backslash = line_to_add.ends_with('\\');
|
|
|
|
let line_to_add = line_to_add.strip_suffix('\\').unwrap_or(line_to_add);
|
|
|
|
current_line_string.push_str(line_to_add);
|
|
|
|
if !continue_backslash {
|
|
actual_lines.push((current_line_type, current_line_number, current_line_string));
|
|
current_line_type = line_type;
|
|
current_line_number = line_number;
|
|
current_line_string = String::new();
|
|
}
|
|
}
|
|
} else {
|
|
for (line_type, line_number) in lines_with_types {
|
|
actual_lines.push((
|
|
line_type,
|
|
line_number,
|
|
lines[line_number as usize].trim().to_string(),
|
|
));
|
|
}
|
|
}
|
|
|
|
drop(lines);
|
|
|
|
for (i, item) in actual_lines.iter().enumerate() {
|
|
let line_type = item.0;
|
|
let line_number = item.1;
|
|
let line = &item.2;
|
|
|
|
if line_type == "unbind" {
|
|
let to_unbind = line.trim_start_matches(UNBIND_STATEMENT).trim();
|
|
modes[current_mode].unbinds.push(parse_keybind(
|
|
path.clone(),
|
|
to_unbind,
|
|
line_number + 1,
|
|
&key_to_evdev_key,
|
|
&mod_to_mod_enum,
|
|
)?);
|
|
}
|
|
|
|
if line_type == "modestart" {
|
|
let modename = line.split(' ').nth(1).unwrap();
|
|
modes.push(Mode::new(modename.to_string()));
|
|
current_mode = modes.len() - 1;
|
|
}
|
|
|
|
if line_type == "modeend" {
|
|
current_mode = 0;
|
|
}
|
|
|
|
if line_type != "keysym" {
|
|
continue;
|
|
}
|
|
|
|
let next_line = actual_lines.get(i + 1);
|
|
if next_line.is_none() {
|
|
break;
|
|
}
|
|
let next_line = next_line.unwrap();
|
|
|
|
if next_line.0 != "command" {
|
|
continue; // this should ignore keysyms that are not followed by a command
|
|
}
|
|
|
|
let extracted_keys = extract_curly_brace(line);
|
|
let extracted_commands = extract_curly_brace(&next_line.2);
|
|
|
|
for (key, command) in extracted_keys.iter().zip(extracted_commands.iter()) {
|
|
let keybinding = parse_keybind(
|
|
path.clone(),
|
|
key,
|
|
line_number + 1,
|
|
&key_to_evdev_key,
|
|
&mod_to_mod_enum,
|
|
)?;
|
|
let hotkey = Hotkey::from_keybinding(keybinding, command.to_string());
|
|
|
|
// Override latter
|
|
modes[current_mode].hotkeys.retain(|h| h.keybinding != hotkey.keybinding);
|
|
modes[current_mode].hotkeys.push(hotkey);
|
|
}
|
|
}
|
|
|
|
Ok(modes)
|
|
}
|
|
|
|
// We need to get the reference to key_to_evdev_key
|
|
// and mod_to_mod enum instead of recreating them
|
|
// after each function call because it's too expensive
|
|
fn parse_keybind(
|
|
path: PathBuf,
|
|
line: &str,
|
|
line_nr: u32,
|
|
key_to_evdev_key: &HashMap<&str, evdev::Key>,
|
|
mod_to_mod_enum: &HashMap<&str, Modifier>,
|
|
) -> Result<KeyBinding, Error> {
|
|
let line = line.split('#').next().unwrap();
|
|
let tokens: Vec<String> =
|
|
line.split('+').map(|s| s.trim().to_lowercase()).filter(|s| s != "_").collect();
|
|
|
|
let mut tokens_new = Vec::new();
|
|
for mut token in tokens {
|
|
while token.trim().starts_with('_') {
|
|
token = token.trim().strip_prefix('_').unwrap().to_string();
|
|
}
|
|
tokens_new.push(token.trim().to_string());
|
|
}
|
|
|
|
let last_token = tokens_new.last().unwrap().trim();
|
|
|
|
// Check if last_token is prefixed with @ or ~ or even both.
|
|
// If prefixed @, on_release = true; if prefixed ~, send = true
|
|
let send = last_token.starts_with('~') || last_token.starts_with("@~");
|
|
let on_release = last_token.starts_with('@') || last_token.starts_with("~@");
|
|
|
|
// Delete the @ and ~ in the last token
|
|
fn strip_at(token: &str) -> &str {
|
|
if token.starts_with('@') {
|
|
let token = token.strip_prefix('@').unwrap();
|
|
strip_tilde(token)
|
|
} else if token.starts_with('~') {
|
|
strip_tilde(token)
|
|
} else {
|
|
token
|
|
}
|
|
}
|
|
|
|
fn strip_tilde(token: &str) -> &str {
|
|
if token.starts_with('~') {
|
|
let token = token.strip_prefix('~').unwrap();
|
|
strip_at(token)
|
|
} else if token.starts_with('@') {
|
|
strip_at(token)
|
|
} else {
|
|
token
|
|
}
|
|
}
|
|
|
|
let last_token = strip_at(last_token);
|
|
|
|
// Check if each token is valid
|
|
for token in &tokens_new {
|
|
let token = strip_at(token);
|
|
if key_to_evdev_key.contains_key(token) {
|
|
// Can't have a keysym that's like a modifier
|
|
if token != last_token {
|
|
return Err(Error::InvalidConfig(ParseError::InvalidModifier(path, line_nr)));
|
|
}
|
|
} else if mod_to_mod_enum.contains_key(token) {
|
|
// Can't have a modifier that's like a keysym
|
|
/*if token == last_token {
|
|
return Err(Error::InvalidConfig(ParseError::InvalidKeysym(path, line_nr)));
|
|
}*/
|
|
} else {
|
|
return Err(Error::InvalidConfig(ParseError::UnknownSymbol(path, line_nr)));
|
|
}
|
|
}
|
|
|
|
// Translate keypress into evdev key
|
|
let keysym = match key_to_evdev_key.get(last_token) {
|
|
Some(k) => Some(*k),
|
|
_ => None
|
|
};
|
|
|
|
let modifiers: Vec<Modifier> = tokens_new
|
|
.iter()
|
|
.filter_map(|token| match mod_to_mod_enum.get(token.as_str()) {
|
|
Some(m) => Some(*m),
|
|
_ => None,
|
|
})
|
|
.collect();
|
|
|
|
let mut keybinding = KeyBinding::new(keysym, modifiers);
|
|
if send {
|
|
keybinding = keybinding.send();
|
|
}
|
|
if on_release {
|
|
keybinding = keybinding.on_release();
|
|
}
|
|
Ok(keybinding)
|
|
}
|
|
|
|
pub fn extract_curly_brace(line: &str) -> Vec<String> {
|
|
if !line.contains('{') || !line.contains('}') || !line.is_ascii() {
|
|
return vec![line.to_string()];
|
|
}
|
|
|
|
// go through each character in the line and mark the position of each { and }
|
|
// if a { is not followed by a }, return the line as is
|
|
let mut brace_positions: Vec<usize> = Vec::new();
|
|
let mut flag = false;
|
|
for (i, c) in line.chars().enumerate() {
|
|
if c == '{' {
|
|
if flag {
|
|
return vec![line.to_string()];
|
|
}
|
|
brace_positions.push(i);
|
|
flag = true;
|
|
} else if c == '}' {
|
|
if !flag {
|
|
return vec![line.to_string()];
|
|
}
|
|
brace_positions.push(i);
|
|
flag = false;
|
|
}
|
|
}
|
|
|
|
// now we have a list of positions of { and }
|
|
// we should extract the items between each pair of braces and store them in a vector
|
|
let mut items: Vec<String> = Vec::new();
|
|
let mut remaining_line: Vec<String> = Vec::new();
|
|
let mut start_index = 0;
|
|
for i in brace_positions.chunks(2) {
|
|
items.push(line[i[0] + 1..i[1]].to_string());
|
|
remaining_line.push(line[start_index..i[0]].to_string());
|
|
start_index = i[1] + 1;
|
|
}
|
|
|
|
// now we have a list of items between each pair of braces
|
|
// we should extract the items between each comma and store them in a vector
|
|
let mut tokens_vec: Vec<Vec<String>> = Vec::new();
|
|
for item in items {
|
|
// Edge case: escape periods
|
|
// example:
|
|
// ```
|
|
// super + {\,, .}
|
|
// riverctl focus-output {previous, next}
|
|
// ```
|
|
let item = item.replace("\\,", "comma");
|
|
|
|
let items: Vec<String> = item.split(',').map(|s| s.trim().to_string()).collect();
|
|
tokens_vec.push(handle_ranges(items));
|
|
}
|
|
|
|
fn handle_ranges(items: Vec<String>) -> Vec<String> {
|
|
let mut output: Vec<String> = Vec::new();
|
|
for item in items {
|
|
if !item.contains('-') {
|
|
output.push(item);
|
|
continue;
|
|
}
|
|
let mut range = item.split('-').map(|s| s.trim());
|
|
|
|
let begin_char: &str = if let Some(b) = range.next() {
|
|
b
|
|
} else {
|
|
output.push(item);
|
|
continue;
|
|
};
|
|
|
|
let end_char: &str = if let Some(e) = range.next() {
|
|
e
|
|
} else {
|
|
output.push(item);
|
|
continue;
|
|
};
|
|
|
|
// Do not accept range values that are longer than one char
|
|
// Example invalid: {ef-p} {3-56}
|
|
// Beginning of the range cannot be greater than end
|
|
// Example invalid: {9-4} {3-2}
|
|
if begin_char.len() != 1 || end_char.len() != 1 || begin_char > end_char {
|
|
output.push(item);
|
|
continue;
|
|
}
|
|
|
|
// In sohkd we will parse the full range using ASCII values.
|
|
|
|
let begin_ascii_val = begin_char.parse::<char>().unwrap() as u8;
|
|
let end_ascii_val = end_char.parse::<char>().unwrap() as u8;
|
|
|
|
for ascii_number in begin_ascii_val..=end_ascii_val {
|
|
output.push((ascii_number as char).to_string());
|
|
}
|
|
}
|
|
output
|
|
}
|
|
|
|
// now write the tokens back to the line and output a vector
|
|
let mut output: Vec<String> = Vec::new();
|
|
// generate a cartesian product iterator for all the vectors in tokens_vec
|
|
let cartesian_product_iter = tokens_vec.iter().multi_cartesian_product();
|
|
for tokens in cartesian_product_iter.collect_vec() {
|
|
let mut line_to_push = String::new();
|
|
for i in 0..remaining_line.len() {
|
|
line_to_push.push_str(&remaining_line[i]);
|
|
line_to_push.push_str(tokens[i]);
|
|
}
|
|
if brace_positions[brace_positions.len() - 1] < line.len() - 1 {
|
|
line_to_push.push_str(&line[brace_positions[brace_positions.len() - 1] + 1..]);
|
|
}
|
|
output.push(line_to_push);
|
|
}
|
|
output
|
|
}
|