diff --git a/src/config.rs b/src/config.rs index abf1105..27d2c37 100644 --- a/src/config.rs +++ b/src/config.rs @@ -93,47 +93,6 @@ fn load_file_contents(path: path::PathBuf) -> Result { Ok(contents) } -// 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( - line: &str, - line_nr: u32, - key_to_evdev_key: &HashMap<&str, evdev::Key>, - mod_to_mod_enum: &HashMap<&str, Modifier>, -) -> Result<(evdev::Key, Vec), Error> { - let line = line.split('#').next().unwrap(); - let tokens: Vec = line.split('+').map(|s| s.trim().to_lowercase()).collect(); - let last_token = tokens.last().unwrap().trim(); - - // Check if each token is valid - for token in &tokens { - if key_to_evdev_key.contains_key(token.as_str()) { - // Can't have a key that's like a modifier - if token != last_token { - return Err(Error::InvalidConfig(ParseError::InvalidModifier(line_nr))); - } - } else if mod_to_mod_enum.contains_key(token.as_str()) { - // Can't have a modifier that's like a modifier - if token == last_token { - return Err(Error::InvalidConfig(ParseError::InvalidKeysym(line_nr))); - } - } else { - return Err(Error::InvalidConfig(ParseError::UnknownSymbol(line_nr))); - } - } - - // Translate keypress into evdev key - let keysym = key_to_evdev_key.get(last_token).unwrap(); - - let modifiers: Vec = tokens[0..(tokens.len() - 1)] - .iter() - .map(|token| *mod_to_mod_enum.get(token.as_str()).unwrap()) - .collect(); - - Ok((*keysym, modifiers)) -} - fn parse_contents(contents: String) -> Result, Error> { let key_to_evdev_key: HashMap<&str, evdev::Key> = HashMap::from([ ("q", evdev::Key::KEY_Q), @@ -295,37 +254,160 @@ fn parse_contents(contents: String) -> Result, Error> { let line_type = item.0; let line_number = item.1; let line = &item.2; - if line_type == "keysym" { - let mut current_command = String::new(); + + 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); + + 'hotkey_parse: for (key, command) in extracted_keys.iter().zip(extracted_commands.iter()) { + println!("{} {}", key, command); let (keysym, modifiers) = - parse_keybind(line, line_number + 1, &key_to_evdev_key, &mod_to_mod_enum)?; - if let Some(next_line) = actual_lines.get(i + 1) { - if next_line.0 == "command" { - current_command.push_str(&next_line.2.clone()); - } else { - continue; // this should ignore keysyms that are not followed by a command + parse_keybind(key, line_number + 1, &key_to_evdev_key, &mod_to_mod_enum)?; + let hotkey = Hotkey { keysym, modifiers, command: command.to_string() }; + + // Ignore duplicate hotkeys + for i in hotkeys.iter() { + if i.keysym == hotkey.keysym && i.modifiers == hotkey.modifiers { + continue 'hotkey_parse; } - } else { - continue; } - // check if hotkeys already contains a hotkey with the same keysym and modifiers. If - // so, ignore this keysym. - let mut flag = false; - for hotkey in hotkeys.iter() { - if hotkey.keysym == keysym && hotkey.modifiers == modifiers { - flag = true; - break; - } + hotkeys.push(hotkey); + } + } + Ok(hotkeys) +} + +// 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( + line: &str, + line_nr: u32, + key_to_evdev_key: &HashMap<&str, evdev::Key>, + mod_to_mod_enum: &HashMap<&str, Modifier>, +) -> Result<(evdev::Key, Vec), Error> { + let line = line.split('#').next().unwrap(); + let tokens: Vec = line.split('+').map(|s| s.trim().to_lowercase()).collect(); + let last_token = tokens.last().unwrap().trim(); + + // Check if each token is valid + for token in &tokens { + if key_to_evdev_key.contains_key(token.as_str()) { + // Can't have a key that's like a modifier + if token != last_token { + return Err(Error::InvalidConfig(ParseError::InvalidModifier(line_nr))); } - if flag { - continue; - } else { - hotkeys.push(Hotkey::new(keysym, modifiers, current_command)); + } else if mod_to_mod_enum.contains_key(token.as_str()) { + // Can't have a modifier that's like a modifier + if token == last_token { + return Err(Error::InvalidConfig(ParseError::InvalidKeysym(line_nr))); } + } else { + return Err(Error::InvalidConfig(ParseError::UnknownSymbol(line_nr))); } } - Ok(hotkeys) + + // Translate keypress into evdev key + let keysym = key_to_evdev_key.get(last_token).unwrap(); + + let modifiers: Vec = tokens[0..(tokens.len() - 1)] + .iter() + .map(|token| *mod_to_mod_enum.get(token.as_str()).unwrap()) + .collect(); + + Ok((*keysym, modifiers)) +} + +fn extract_curly_brace(line: &str) -> Vec { + if !line.is_ascii() { + return vec![line.to_string()]; + } + let mut output: Vec = Vec::new(); + + let index_open_brace = line.chars().position(|c| c == '{'); + let index_close_brace = line.chars().position(|c| c == '}'); + + if index_open_brace.is_none() || index_close_brace.is_none() { + return vec![line.to_string()]; + } + + let start_index = index_open_brace.unwrap(); + let end_index = index_close_brace.unwrap(); + + // There are no expansions to build if } is earlier than { + if start_index >= end_index { + return vec![line.to_string()]; + } + + let str_before_braces = line[..start_index].to_string(); + let str_after_braces = line[end_index + 1..].to_string(); + + let comma_separated_items = line[start_index + 1..end_index].split(','); + + for item in comma_separated_items { + let mut push_one_item = || { + output.push(format!("{}{}{}", str_before_braces, item.trim(), str_after_braces)); + }; + + if !item.contains('-') { + push_one_item(); + continue; + } + + // Parse dash ranges like {1-5} and {a-f} + + let mut range = item.split('-').map(|s| s.trim()); + let begin_char: &str; + let end_char: &str; + + if let Some(b) = range.next() { + begin_char = b; + } else { + push_one_item(); + continue; + } + + if let Some(e) = range.next() { + end_char = e; + } else { + push_one_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 { + push_one_item(); + continue; + } + + // In swhkd we will parse the full range using ASCII values. + + let begin_ascii_val = begin_char.parse::().unwrap() as u8; + let end_ascii_val = end_char.parse::().unwrap() as u8; + + for ascii_number in begin_ascii_val..=end_ascii_val { + output + .push(format!("{}{}{}", str_before_braces, ascii_number as char, str_after_braces)); + } + } + output } #[cfg(test)] @@ -645,16 +727,6 @@ w ) } - #[test] - fn test_nonsensical_file() -> std::io::Result<()> { - let contents = " -WE WISH YOU A MERRY RUSTMAS - - "; - - eval_invalid_config_test(contents, ParseError::UnknownSymbol(2)) - } - #[test] fn test_real_config_snippet() -> std::io::Result<()> { let contents = " @@ -1051,6 +1123,176 @@ super + shift + b fn test_multiple_brackets_only_one_in_command() -> std::io::Result<()> { Ok(()) } + + #[test] + fn test_extract_curly_brace() -> std::io::Result<()> { + let keybind_with_curly_brace = "super + {a,b,c}"; + assert_eq!( + extract_curly_brace(keybind_with_curly_brace), + vec!["super + a", "super + b", "super + c",] + ); + let command_with_curly_brace = "bspc node -p {west,south,north,west}"; + assert_eq!( + extract_curly_brace(command_with_curly_brace), + vec![ + "bspc node -p west", + "bspc node -p south", + "bspc node -p north", + "bspc node -p west", + ] + ); + let wrong_format = "super + }a, b, c{"; + assert_eq!(extract_curly_brace(wrong_format), vec![wrong_format]); + let single_sym = "super + {a}"; + assert_eq!(extract_curly_brace(single_sym), vec!["super + a"]); + Ok(()) + } + + #[test] + fn test_curly_brace() -> std::io::Result<()> { + let contents = " +super + {a,b,c} + {firefox, brave, chrome}"; + eval_config_test( + contents, + vec![ + Hotkey::new(evdev::Key::KEY_A, vec![Modifier::Super], "firefox".to_string()), + Hotkey::new(evdev::Key::KEY_B, vec![Modifier::Super], "brave".to_string()), + Hotkey::new(evdev::Key::KEY_C, vec![Modifier::Super], "chrome".to_string()), + ], + ) + } + + #[test] + fn test_curly_brace_less_commands() -> std::io::Result<()> { + let contents = " +super + {a,b,c} + {firefox, brave}"; + eval_config_test( + contents, + vec![ + Hotkey::new(evdev::Key::KEY_A, vec![Modifier::Super], "firefox".to_string()), + Hotkey::new(evdev::Key::KEY_B, vec![Modifier::Super], "brave".to_string()), + ], + ) + } + + #[test] + fn test_curly_brace_less_keysyms() -> std::io::Result<()> { + let contents = " +super + {a, b} + {firefox, brave, chrome}"; + eval_config_test( + contents, + vec![ + Hotkey::new(evdev::Key::KEY_A, vec![Modifier::Super], "firefox".to_string()), + Hotkey::new(evdev::Key::KEY_B, vec![Modifier::Super], "brave".to_string()), + ], + ) + } + + #[test] + fn test_range_syntax() -> std::io::Result<()> { + let contents = " +super + {1-9,0} + bspc desktop -f '{1-9,0}'"; + eval_config_test( + contents, + vec![ + Hotkey::new( + evdev::Key::KEY_1, + vec![Modifier::Super], + "bspc desktop -f '1'".to_string(), + ), + Hotkey::new( + evdev::Key::KEY_2, + vec![Modifier::Super], + "bspc desktop -f '2'".to_string(), + ), + Hotkey::new( + evdev::Key::KEY_3, + vec![Modifier::Super], + "bspc desktop -f '3'".to_string(), + ), + Hotkey::new( + evdev::Key::KEY_4, + vec![Modifier::Super], + "bspc desktop -f '4'".to_string(), + ), + Hotkey::new( + evdev::Key::KEY_5, + vec![Modifier::Super], + "bspc desktop -f '5'".to_string(), + ), + Hotkey::new( + evdev::Key::KEY_6, + vec![Modifier::Super], + "bspc desktop -f '6'".to_string(), + ), + Hotkey::new( + evdev::Key::KEY_7, + vec![Modifier::Super], + "bspc desktop -f '7'".to_string(), + ), + Hotkey::new( + evdev::Key::KEY_8, + vec![Modifier::Super], + "bspc desktop -f '8'".to_string(), + ), + Hotkey::new( + evdev::Key::KEY_9, + vec![Modifier::Super], + "bspc desktop -f '9'".to_string(), + ), + Hotkey::new( + evdev::Key::KEY_0, + vec![Modifier::Super], + "bspc desktop -f '0'".to_string(), + ), + ], + ) + } + + #[test] + fn test_range_syntax_ascii_character() -> std::io::Result<()> { + let contents = " +super + {a-c} + {firefox, brave, chrome}"; + eval_config_test( + contents, + vec![ + Hotkey::new(evdev::Key::KEY_A, vec![Modifier::Super], "firefox".to_string()), + Hotkey::new(evdev::Key::KEY_B, vec![Modifier::Super], "brave".to_string()), + Hotkey::new(evdev::Key::KEY_C, vec![Modifier::Super], "chrome".to_string()), + ], + ) + } + + #[test] + fn test_range_syntax_not_ascii() -> std::io::Result<()> { + let contents = " +super + {a-是} + {firefox, brave} + "; + eval_invalid_config_test(contents, ParseError::UnknownSymbol(2)) + } + + #[test] + fn test_range_syntax_invalid_range() -> std::io::Result<()> { + let contents = " +super + {bc-ad} + {firefox, brave} + "; + eval_invalid_config_test(contents, ParseError::UnknownSymbol(2)) + } + + #[test] + fn test_ranger_syntax_not_full_range() -> std::io::Result<()> { + let contents = " +super + {a-} + {firefox, brave}"; + eval_invalid_config_test(contents, ParseError::UnknownSymbol(2)) + } } #[cfg(test)]