diff --git a/Cargo.lock b/Cargo.lock index ada0618..4cd06bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -208,7 +208,7 @@ dependencies = [ [[package]] name = "lefthk-core" -version = "0.1.9" +version = "0.2.0" dependencies = [ "inventory", "mio", diff --git a/README.md b/README.md index 74e6583..24f9726 100644 --- a/README.md +++ b/README.md @@ -6,16 +6,16 @@ LeftHK - A hotkey daemon written in Rust The configuration file should be created in ~/.config/lefthk/ and called config.ron. If the configuration file is not created the program will exit. Example config: ```ron +#![enable(implicit_some)] Config( + default_modifier: ["Mod4", "Shift"], keybinds: [ Keybind( command: Execute("st -e htop"), - modifier: ["Mod4", "Shift"], key: Key("x"), ), Keybind( command: Executes(["st -e htop", "st -e bpytop"]), - modifier: ["Mod4", "Shift"], key: Keys(["x", "m"]), ), Keybind( diff --git a/lefthk/src/config/command.rs b/lefthk/src/config/command.rs index 265ba0b..0f5e1cd 100644 --- a/lefthk/src/config/command.rs +++ b/lefthk/src/config/command.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use super::keybind::Keybind; +use crate::config::keybind::Keybind; #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] pub enum Command { diff --git a/lefthk/src/config/keybind.rs b/lefthk/src/config/keybind.rs index 31cbc01..1edc6fc 100644 --- a/lefthk/src/config/keybind.rs +++ b/lefthk/src/config/keybind.rs @@ -1,13 +1,10 @@ use crate::errors::{LeftError, Result}; -use lefthk_core::config::command as command_mod; -use lefthk_core::config::Command as core_command; -use lefthk_core::config::Keybind as core_keybind; -use serde::Deserialize; -use serde::Serialize; +use lefthk_core::config::{ + command as command_mod, Command as core_command, Keybind as core_keybind, +}; +use serde::{Deserialize, Serialize}; -use super::{command::Command, key::Key}; - -use std::convert::TryFrom; +use crate::config::{command::Command, key::Key}; macro_rules! get_key { ($expr:expr $(,)?) => { @@ -32,75 +29,74 @@ pub type Keybinds = Vec; #[derive(Debug, PartialEq, Clone, Eq, Serialize, Deserialize)] pub struct Keybind { pub command: Command, - pub modifier: Vec, + pub modifier: Option>, pub key: Key, } -impl TryFrom for Vec { - type Error = LeftError; - - fn try_from(kb: Keybind) -> Result { - let command_key_pairs: Vec<(Box, String)> = match kb.command { - Command::Chord(children) if !children.is_empty() => { - let key = get_key!(kb.key); - let children = children - .iter() - .filter_map(|kb| match TryFrom::try_from(kb.clone()) { - Ok(keybinds) => Some::>(keybinds), - Err(err) => { - tracing::error!("Invalid key binding: {}\n{:?}", err, kb); - None - } - }) - .flatten() - .collect(); +pub(crate) fn try_from(kb: Keybind, default_modifier: &[String]) -> Result> { + let command_key_pairs: Vec<(Box, String)> = match kb.command { + Command::Chord(children) if !children.is_empty() => { + let key = get_key!(kb.key); + let children = children + .iter() + .filter_map(|kb| match try_from(kb.clone(), default_modifier) { + Ok(keybinds) => Some::>(keybinds), + Err(err) => { + tracing::error!("Invalid key binding: {}\n{:?}", err, kb); + None + } + }) + .flatten() + .collect(); - vec![(Box::new(command_mod::Chord::new(children)), key)] - } - Command::Chord(_) => return Err(LeftError::ChildrenNotFound), - Command::Execute(value) if !value.is_empty() => { - let keys = get_key!(kb.key); - vec![(Box::new(command_mod::Execute::new(&value)), keys)] - } - Command::Execute(_) => return Err(LeftError::ValueNotFound), - Command::Executes(values) if !values.is_empty() => { - let keys = get_keys!(kb.key); - if keys.len() != values.len() { - return Err(LeftError::NumberOfKeysDiffersFromValues); - } - values - .iter() - .enumerate() - .map(|(i, v)| { - ( - Box::new(command_mod::Execute::new(&v)) as Box, - keys[i].clone(), - ) - }) - .collect() - } - Command::Executes(_) => return Err(LeftError::ValuesNotFound), - Command::ExitChord => { - let keys = get_key!(kb.key); - vec![(Box::new(command_mod::ExitChord::new()), keys)] - } - Command::Reload => { - let keys = get_key!(kb.key); - vec![(Box::new(command_mod::Reload::new()), keys)] - } - Command::Kill => { - let keys = get_key!(kb.key); - vec![(Box::new(command_mod::Kill::new()), keys)] + vec![(Box::new(command_mod::Chord::new(children)), key)] + } + Command::Chord(_) => return Err(LeftError::ChildrenNotFound), + Command::Execute(value) if !value.is_empty() => { + let keys = get_key!(kb.key); + vec![((Box::new(command_mod::Execute::new(&value))), keys)] + } + Command::Execute(_) => return Err(LeftError::ValueNotFound), + Command::Executes(values) if !values.is_empty() => { + let keys = get_keys!(kb.key); + if keys.len() != values.len() { + return Err(LeftError::NumberOfKeysDiffersFromValues); } - }; - let keybinds = command_key_pairs - .iter() - .map(|(c, k)| core_keybind { - command: c.normalize(), - modifier: kb.modifier.clone(), - key: k.clone(), - }) - .collect(); - Ok(keybinds) - } + values + .iter() + .enumerate() + .map(|(i, v)| { + ( + Box::new(command_mod::Execute::new(&v)) as Box, + keys[i].clone(), + ) + }) + .collect() + } + Command::Executes(_) => return Err(LeftError::ValuesNotFound), + Command::ExitChord => { + let keys = get_key!(kb.key); + vec![((Box::new(command_mod::ExitChord::new())), keys)] + } + Command::Reload => { + let keys = get_key!(kb.key); + vec![((Box::new(command_mod::Reload::new())), keys)] + } + Command::Kill => { + let keys = get_key!(kb.key); + vec![((Box::new(command_mod::Kill::new())), keys)] + } + }; + let keybinds = command_key_pairs + .iter() + .map(|(c, k)| core_keybind { + command: c.normalize(), + modifier: kb + .modifier + .clone() + .unwrap_or_else(|| default_modifier.to_vec()), + key: k.clone(), + }) + .collect(); + Ok(keybinds) } diff --git a/lefthk/src/config/mod.rs b/lefthk/src/config/mod.rs index 3a2362f..3ae27a2 100644 --- a/lefthk/src/config/mod.rs +++ b/lefthk/src/config/mod.rs @@ -5,7 +5,7 @@ pub mod keybind; use crate::errors::{LeftError, Result}; use serde::{Deserialize, Serialize}; -use std::{convert::TryFrom, fs, path::Path}; +use std::{fs, path::Path}; use xdg::BaseDirectories; use self::{ @@ -15,6 +15,7 @@ use self::{ #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] pub struct Config { + pub(crate) default_modifier: Vec, keybinds: Keybinds, } @@ -22,28 +23,27 @@ impl lefthk_core::config::Config for Config { fn mapped_bindings(&self) -> Vec { self.keybinds .iter() - .filter_map(|kb| match TryFrom::try_from(kb.clone()) { - Ok(keybinds) => Some::>(keybinds), - Err(err) => { - tracing::error!("Invalid key binding: {}\n{:?}", err, kb); - None - } - }) + .filter_map( + |kb| match keybind::try_from(kb.clone(), &self.default_modifier.clone()) { + Ok(keybinds) => Some::>(keybinds), + Err(err) => { + tracing::error!("Invalid key binding: {}\n{:?}", err, kb); + None + } + }, + ) .flatten() .collect() } } -/// # Errors -/// -/// Thes will error when no config file is found, most propably as system or -/// user error for provideng a wrong path -pub fn load() -> Result { - let path = BaseDirectories::with_prefix(lefthk_core::LEFTHK_DIR_NAME)?; - fs::create_dir_all(path.get_config_home())?; - let file_name = path.place_config_file("config.ron")?; - if Path::new(&file_name).exists() { - let contents = fs::read_to_string(file_name)?; +impl TryFrom for Config { + type Error = LeftError; + /// # Errors + /// + /// Thes will error when no config file is found, most propably as system or + /// user error for provideng a wrong path + fn try_from(contents: String) -> Result { let mut config: Config = ron::from_str(&contents)?; let global_exit_chord = config .keybinds @@ -57,7 +57,20 @@ pub fn load() -> Result { .collect(); propagate_exit_chord(chords, &global_exit_chord); - return Ok(config); + Ok(config) + } +} + +/// # Errors +/// +/// This errors, when no Config is found at the path +pub fn load() -> Result { + let path = BaseDirectories::with_prefix(lefthk_core::LEFTHK_DIR_NAME)?; + fs::create_dir_all(path.get_config_home())?; + let file_name = path.place_config_file("config.ron")?; + if Path::new(&file_name).exists() { + let contents = fs::read_to_string(file_name)?; + Config::try_from(contents)?; } Err(LeftError::NoConfigFound) } diff --git a/lefthk/src/main.rs b/lefthk/src/main.rs index cd8c8f1..edee0dd 100644 --- a/lefthk/src/main.rs +++ b/lefthk/src/main.rs @@ -1,12 +1,15 @@ use crate::errors::LeftError; use clap::{App, Arg}; -use lefthk_core::config::{command, Command}; -use lefthk_core::ipc::Pipe; -use lefthk_core::worker::Status; -use lefthk_core::{config::Config, worker::Worker}; -use std::fs; -use std::io::Write; -use std::sync::atomic::{AtomicBool, Ordering}; +use lefthk_core::{ + config::{command, Command, Config}, + ipc::Pipe, + worker::{Status, Worker}, +}; +use std::{ + fs, + io::Write, + sync::atomic::{AtomicBool, Ordering}, +}; use xdg::BaseDirectories; use tracing_subscriber::{filter::EnvFilter, filter::LevelFilter, fmt, layer::SubscriberExt}; diff --git a/lefthk/src/tests.rs b/lefthk/src/tests.rs index 9ae3c01..49cb540 100644 --- a/lefthk/src/tests.rs +++ b/lefthk/src/tests.rs @@ -1,50 +1,153 @@ /// Config Testing #[cfg(test)] mod config { - // use crate::{ - // config::{Command, Keybind}, - // errors::Result, - // }; - // use std::convert::TryFrom; - - // #[test] - // fn parse_kdl_nodes() { - // let execute_kb: Keybind = Keybind { - // command: Command::Execute, - // value: Some("st".to_owned()), - // modifier: vec!["Mod4".to_owned()], - // key: "x".to_owned(), - // children: None, - // }; - // let reload_kb: Keybind = Keybind { - // command: Command::Reload, - // value: None, - // modifier: vec!["Mod4".to_owned()], - // key: "x".to_owned(), - // children: None, - // }; - // let kill_kb: Keybind = Keybind { - // command: Command::Kill, - // value: None, - // modifier: vec!["Mod4".to_owned()], - // key: "x".to_owned(), - // children: None, - // }; - // let exit_chord_kb: Keybind = Keybind { - // command: Command::ExitChord, - // value: None, - // modifier: vec!["Mod4".to_owned()], - // key: "x".to_owned(), - // children: None, - // }; - // let chord_kb: Keybind = Keybind { - // command: Command::Chord, - // value: None, - // modifier: vec!["Mod4".to_owned()], - // key: "x".to_owned(), - // children: Some(vec![execute_kb.clone(), reload_kb.clone(), kill_kb.clone()]), - // }; - // let keybinds: Vec = vec![chord_kb, execute_kb, exit_chord_kb, reload_kb, kill_kb]; - // assert_eq!(parsed_keybands, keybinds); - // } + use lefthk_core::config::command::utils::normalized_command::NormalizedCommand; + use lefthk_core::config::Config; + + use crate::config::Config as Cfg; + + #[test] + fn parse_config() { + let config = r#"#![enable(implicit_some)] +Config( + default_modifier: ["Mod4", "Shift"], + keybinds: [ + Keybind( + command: Execute("st -e htop"), + key: Key("x"), + ), + Keybind( + command: Execute("st -e btm"), + modifier: ["Mod4"], + key: Key("c"), + ), + ] +)"#; + let conf = Cfg::try_from(config.to_string()); + assert!(conf.is_ok()); + let conf = conf.unwrap(); + assert_eq!(conf.default_modifier.len(), 2); + assert_eq!( + conf.default_modifier, + vec!["Mod4".to_string(), "Shift".to_string()] + ); + let conf_mapped = conf.mapped_bindings(); + + // Verify default modifier implementation + let default_keybind = conf_mapped.first().unwrap(); + assert_eq!(default_keybind.modifier.len(), 2); + assert_eq!(default_keybind.modifier, conf.default_modifier); + + // Verify own implementation + let custom_keybind = conf_mapped.last().unwrap(); + assert_eq!(custom_keybind.modifier.len(), 1); + assert_eq!(custom_keybind.modifier, vec!["Mod4".to_string()]); + } + + #[test] + fn parse_empty_config() { + let config = r#"Config( + default_modifier: ["Mod4", "Shift"], + keybinds: [] +)"#; + let conf = Cfg::try_from(config.to_string()); + assert!(conf.is_ok()); + let conf = conf.unwrap(); + assert_eq!(conf.default_modifier.len(), 2); + assert_eq!( + conf.default_modifier, + vec!["Mod4".to_string(), "Shift".to_string()] + ); + let conf_mapped = conf.mapped_bindings(); + + // Verify implementation + assert_eq!(conf_mapped.len(), 0); + } + + #[test] + fn parse_none_config() { + // Define empty string + let conf = Cfg::try_from(String::new()); + assert!(conf.is_err()); + } + + #[test] + fn parse_sub_keybind_config() { + let config = r#"#![enable(implicit_some)] +Config( + default_modifier: ["Mod4", "Shift"], + keybinds: [ + Keybind( + command: Chord([ + Keybind( + command: Execute("st -e htop"), + modifier: ["Mod4"], + key: Key("c"), + ), + ]), + modifier: ["Mod4"], + key: Key("c"), + ), + Keybind( + command: Chord([ + Keybind( + command: Execute("st -e htop"), + key: Key("c"), + ), + ]), + key: Key("c"), + ), + ] +)"#; + let conf = Cfg::try_from(config.to_string()); + assert!(conf.is_ok()); + let conf = conf.unwrap(); + assert_eq!(conf.default_modifier.len(), 2); + assert_eq!( + conf.default_modifier, + vec!["Mod4".to_string(), "Shift".to_string()] + ); + let conf_mapped = conf.mapped_bindings(); + + // Verify default modifier implementation + let default_keybind = conf_mapped.last().unwrap(); + assert_eq!(default_keybind.modifier.len(), 2); + assert_eq!(default_keybind.modifier, conf.default_modifier); + assert_eq!( + default_keybind.command, + NormalizedCommand( + r#"Chord([ + Keybind( + command: NormalizedCommand("Execute(\"st -e htop\")"), + modifier: [ + "Mod4", + "Shift", + ], + key: "c", + ), +])"# + .to_string() + ) + ); + + // Verify custom modifier implementation + let custom_keybind = conf_mapped.first().unwrap(); + assert_eq!(custom_keybind.modifier.len(), 1); + assert_eq!(custom_keybind.modifier, vec!["Mod4".to_string()]); + assert_eq!( + custom_keybind.command, + NormalizedCommand( + r#"Chord([ + Keybind( + command: NormalizedCommand("Execute(\"st -e htop\")"), + modifier: [ + "Mod4", + ], + key: "c", + ), +])"# + .to_string() + ) + ); + } }