feat: add curly brace syntax parsing and error display

Curly brace syntax parsing by EdenQwQ
main
Angelo Fallaria 2 years ago committed by GitHub
commit b2230760fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -93,47 +93,6 @@ fn load_file_contents(path: path::PathBuf) -> Result<String, Error> {
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<Modifier>), Error> {
let line = line.split('#').next().unwrap();
let tokens: Vec<String> = 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<Modifier> = 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<Vec<Hotkey>, 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<Vec<Hotkey>, 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<Modifier>), Error> {
let line = line.split('#').next().unwrap();
let tokens: Vec<String> = 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<Modifier> = 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<String> {
if !line.is_ascii() {
return vec![line.to_string()];
}
let mut output: Vec<String> = 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::<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(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)]

Loading…
Cancel
Save