use super::{HotkeyError, MODIFIER_COUNT, Mods, keys::key_name, modifier_bit, trigger_name};
const SIDE_MODIFIERS: u32 = MODIFIER_COUNT / 2;
const SIDE_MODIFIER_MASK: u8 = (1 << SIDE_MODIFIERS) - 1;
pub fn recorded_trigger(usages: &[u32]) -> Result<String, HotkeyError> {
let [usage] = usages else {
return Err(HotkeyError("press exactly one trigger key".into()));
};
trigger_name(*usage).map(str::to_owned).ok_or_else(|| {
HotkeyError(
"that key types text; pick a modifier, F-key, Scroll Lock, Pause or Menu".into(),
)
})
}
pub fn recorded_chord(usages: &[u32]) -> Result<String, HotkeyError> {
let mut keys = usages
.iter()
.copied()
.filter(|usage| modifier_bit(*usage).is_none());
let key = keys
.next()
.ok_or_else(|| HotkeyError("a chord needs one non-modifier key".into()))?;
if keys.next().is_some() {
return Err(HotkeyError(
"a chord needs exactly one non-modifier key".into(),
));
}
let name =
key_name(key).ok_or_else(|| HotkeyError("that key cannot be used in a chord".into()))?;
let mut parts = modifier_names(usages);
parts.push(&name);
Ok(parts.join("+"))
}
pub fn recorded_modifiers(usages: &[u32]) -> Result<String, HotkeyError> {
if usages.is_empty() || usages.iter().any(|usage| modifier_bit(*usage).is_none()) {
return Err(HotkeyError(
"press only modifiers: Ctrl, Alt, Shift or Meta".into(),
));
}
Ok(modifier_names(usages).join("+"))
}
fn modifier_names(usages: &[u32]) -> Vec<&'static str> {
let held = usages
.iter()
.filter_map(|usage| modifier_bit(*usage))
.fold(0, |held, bit| held | bit);
let generic = (held & SIDE_MODIFIER_MASK) | (held >> SIDE_MODIFIERS);
["ctrl", "alt", "shift", "meta"]
.into_iter()
.filter(|name| Mods::token(name).is_some_and(|mods| generic & mods.0 != 0))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hotkey::{Chord, DoubleTap, Mods, keys::key_usage};
#[test]
fn recorded_chord_mixed_sides_round_trips() {
let usages =
["left_ctrl", "right_alt", "left_shift", "home"].map(|name| key_usage(name).unwrap());
let value = recorded_chord(&usages).unwrap();
assert_eq!(value, "ctrl+alt+shift+home");
assert_eq!(
Chord::parse(&value).unwrap(),
Chord::parse("ctrl+alt+shift+home").unwrap()
);
}
#[test]
fn recorded_chord_supported_keys_round_trip() {
let names = ["a", "z", "0", "1", "9", "f24", "scroll_lock", "page_up"];
for name in names {
let value = recorded_chord(&[key_usage(name).unwrap()]).unwrap();
assert_eq!(Chord::parse(&value).unwrap().key, key_usage(name).unwrap());
}
}
#[test]
fn recorded_modifiers_both_sides_round_trip_without_duplicates() {
let usages = [
"left_ctrl",
"right_ctrl",
"right_alt",
"left_shift",
"right_meta",
]
.map(|name| key_usage(name).unwrap());
let value = recorded_modifiers(&usages).unwrap();
assert_eq!(value, "ctrl+alt+shift+meta");
assert_eq!(
Mods::parse(&value).unwrap(),
Mods::parse("ctrl+alt+shift+meta").unwrap()
);
}
#[test]
fn recorded_trigger_allowed_key_round_trips() {
let usage = key_usage("right_alt").unwrap();
let value = recorded_trigger(&[usage]).unwrap();
assert!(DoubleTap::parse(&value).unwrap().matches(usage));
}
#[test]
fn recorded_value_invalid_combinations_are_rejected() {
let a = key_usage("a").unwrap();
let ctrl = key_usage("left_ctrl").unwrap();
let cases = [
(recorded_trigger as fn(&[u32]) -> _, vec![]),
(recorded_trigger, vec![a]),
(recorded_trigger, vec![ctrl, a]),
(recorded_chord, vec![]),
(recorded_chord, vec![ctrl]),
(recorded_chord, vec![a, key_usage("b").unwrap()]),
(recorded_chord, vec![0]),
(recorded_modifiers, vec![]),
(recorded_modifiers, vec![ctrl, a]),
];
for (record, usages) in cases {
let result = record(&usages);
assert!(result.is_err(), "{usages:?}");
}
}
}