use bitflags::bitflags;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;
use crate::error::{Error, Result};
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Modifiers: u32 {
const CMD_LEFT = 1 << 0;
const SHIFT_LEFT = 1 << 1;
const CTRL_LEFT = 1 << 2;
const OPT_LEFT = 1 << 3;
const FN = 1 << 4;
const CMD_RIGHT = 1 << 5;
const SHIFT_RIGHT = 1 << 6;
const CTRL_RIGHT = 1 << 7;
const OPT_RIGHT = 1 << 8;
const CMD = Self::CMD_LEFT.bits() | Self::CMD_RIGHT.bits();
const SHIFT = Self::SHIFT_LEFT.bits() | Self::SHIFT_RIGHT.bits();
const CTRL = Self::CTRL_LEFT.bits() | Self::CTRL_RIGHT.bits();
const OPT = Self::OPT_LEFT.bits() | Self::OPT_RIGHT.bits();
}
}
const GROUPS: [(Modifiers, Modifiers, Modifiers); 4] = [
(Modifiers::CMD_LEFT, Modifiers::CMD_RIGHT, Modifiers::CMD),
(
Modifiers::SHIFT_LEFT,
Modifiers::SHIFT_RIGHT,
Modifiers::SHIFT,
),
(Modifiers::CTRL_LEFT, Modifiers::CTRL_RIGHT, Modifiers::CTRL),
(Modifiers::OPT_LEFT, Modifiers::OPT_RIGHT, Modifiers::OPT),
];
impl fmt::Display for Modifiers {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut parts = Vec::new();
if self.contains(Modifiers::CTRL) {
parts.push("Ctrl");
} else if self.contains(Modifiers::CTRL_LEFT) {
parts.push("CtrlLeft");
} else if self.contains(Modifiers::CTRL_RIGHT) {
parts.push("CtrlRight");
}
if self.contains(Modifiers::OPT) {
parts.push("Opt");
} else if self.contains(Modifiers::OPT_LEFT) {
parts.push("OptLeft");
} else if self.contains(Modifiers::OPT_RIGHT) {
parts.push("OptRight");
}
if self.contains(Modifiers::SHIFT) {
parts.push("Shift");
} else if self.contains(Modifiers::SHIFT_LEFT) {
parts.push("ShiftLeft");
} else if self.contains(Modifiers::SHIFT_RIGHT) {
parts.push("ShiftRight");
}
if self.contains(Modifiers::CMD) {
parts.push("Cmd");
} else if self.contains(Modifiers::CMD_LEFT) {
parts.push("CmdLeft");
} else if self.contains(Modifiers::CMD_RIGHT) {
parts.push("CmdRight");
}
if self.contains(Modifiers::FN) {
parts.push("Fn");
}
write!(f, "{}", parts.join("+"))
}
}
impl Modifiers {
pub(crate) fn parse_single(s: &str) -> Option<Modifiers> {
match s.to_lowercase().as_str() {
"cmd" | "command" | "meta" | "super" | "win" | "windows" => Some(Modifiers::CMD),
"shift" => Some(Modifiers::SHIFT),
"ctrl" | "control" => Some(Modifiers::CTRL),
"opt" | "option" | "alt" => Some(Modifiers::OPT),
"fn" | "function" => Some(Modifiers::FN),
"cmdleft" | "cmd_left" | "lcmd" | "commandleft" | "command_left" | "lcommand"
| "superleft" | "super_left" | "winleft" | "win_left" | "windowsleft"
| "windows_left" | "metaleft" | "meta_left" => Some(Modifiers::CMD_LEFT),
"shiftleft" | "shift_left" | "lshift" => Some(Modifiers::SHIFT_LEFT),
"ctrlleft" | "ctrl_left" | "lctrl" | "controlleft" | "control_left" | "lcontrol" => {
Some(Modifiers::CTRL_LEFT)
}
"optleft" | "opt_left" | "lopt" | "optionleft" | "option_left" | "loption"
| "altleft" | "alt_left" | "lalt" => Some(Modifiers::OPT_LEFT),
"cmdright" | "cmd_right" | "rcmd" | "commandright" | "command_right" | "rcommand"
| "superright" | "super_right" | "winright" | "win_right" | "windowsright"
| "windows_right" | "metaright" | "meta_right" => Some(Modifiers::CMD_RIGHT),
"shiftright" | "shift_right" | "rshift" => Some(Modifiers::SHIFT_RIGHT),
"ctrlright" | "ctrl_right" | "rctrl" | "controlright" | "control_right"
| "rcontrol" => Some(Modifiers::CTRL_RIGHT),
"optright" | "opt_right" | "ropt" | "optionright" | "option_right" | "roption"
| "altright" | "alt_right" | "ralt" | "altgr" => Some(Modifiers::OPT_RIGHT),
_ => None,
}
}
pub fn matches(self, event: Modifiers) -> bool {
for &(left, right, _compound) in &GROUPS {
let hotkey_has_left = self.contains(left);
let hotkey_has_right = self.contains(right);
let event_has_left = event.contains(left);
let event_has_right = event.contains(right);
let event_has_any = event_has_left || event_has_right;
if hotkey_has_left && hotkey_has_right {
if !event_has_any {
return false;
}
} else if hotkey_has_left {
if !event_has_left {
return false;
}
} else if hotkey_has_right {
if !event_has_right {
return false;
}
} else {
if event_has_any {
return false;
}
}
}
self.contains(Modifiers::FN) == event.contains(Modifiers::FN)
}
}
impl FromStr for Modifiers {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
let s = s.trim();
if s.is_empty() {
return Ok(Modifiers::empty());
}
let mut modifiers = Modifiers::empty();
for part in s.split('+') {
let part = part.trim();
if part.is_empty() {
continue;
}
match Modifiers::parse_single(part) {
Some(m) => modifiers |= m,
None => return Err(Error::UnknownModifier(part.to_string())),
}
}
Ok(modifiers)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_single_modifiers() {
assert_eq!("Cmd".parse::<Modifiers>().unwrap(), Modifiers::CMD);
assert_eq!("command".parse::<Modifiers>().unwrap(), Modifiers::CMD);
assert_eq!("meta".parse::<Modifiers>().unwrap(), Modifiers::CMD);
assert_eq!("super".parse::<Modifiers>().unwrap(), Modifiers::CMD);
assert_eq!("win".parse::<Modifiers>().unwrap(), Modifiers::CMD);
assert_eq!("Shift".parse::<Modifiers>().unwrap(), Modifiers::SHIFT);
assert_eq!("SHIFT".parse::<Modifiers>().unwrap(), Modifiers::SHIFT);
assert_eq!("Ctrl".parse::<Modifiers>().unwrap(), Modifiers::CTRL);
assert_eq!("control".parse::<Modifiers>().unwrap(), Modifiers::CTRL);
assert_eq!("Opt".parse::<Modifiers>().unwrap(), Modifiers::OPT);
assert_eq!("option".parse::<Modifiers>().unwrap(), Modifiers::OPT);
assert_eq!("alt".parse::<Modifiers>().unwrap(), Modifiers::OPT);
assert_eq!("Fn".parse::<Modifiers>().unwrap(), Modifiers::FN);
assert_eq!("function".parse::<Modifiers>().unwrap(), Modifiers::FN);
}
#[test]
fn parse_side_specific_modifiers() {
assert_eq!("CmdLeft".parse::<Modifiers>().unwrap(), Modifiers::CMD_LEFT);
assert_eq!("LCmd".parse::<Modifiers>().unwrap(), Modifiers::CMD_LEFT);
assert_eq!(
"CmdRight".parse::<Modifiers>().unwrap(),
Modifiers::CMD_RIGHT
);
assert_eq!("RCmd".parse::<Modifiers>().unwrap(), Modifiers::CMD_RIGHT);
assert_eq!(
"ShiftLeft".parse::<Modifiers>().unwrap(),
Modifiers::SHIFT_LEFT
);
assert_eq!(
"ShiftRight".parse::<Modifiers>().unwrap(),
Modifiers::SHIFT_RIGHT
);
assert_eq!(
"CtrlLeft".parse::<Modifiers>().unwrap(),
Modifiers::CTRL_LEFT
);
assert_eq!(
"CtrlRight".parse::<Modifiers>().unwrap(),
Modifiers::CTRL_RIGHT
);
assert_eq!("OptLeft".parse::<Modifiers>().unwrap(), Modifiers::OPT_LEFT);
assert_eq!(
"AltRight".parse::<Modifiers>().unwrap(),
Modifiers::OPT_RIGHT
);
assert_eq!("AltGr".parse::<Modifiers>().unwrap(), Modifiers::OPT_RIGHT);
}
#[test]
fn parse_combined_modifiers() {
assert_eq!(
"Cmd+Shift".parse::<Modifiers>().unwrap(),
Modifiers::CMD | Modifiers::SHIFT
);
assert_eq!(
"Ctrl+Alt+Shift".parse::<Modifiers>().unwrap(),
Modifiers::CTRL | Modifiers::OPT | Modifiers::SHIFT
);
}
#[test]
fn parse_empty_modifiers() {
assert_eq!("".parse::<Modifiers>().unwrap(), Modifiers::empty());
assert_eq!(" ".parse::<Modifiers>().unwrap(), Modifiers::empty());
}
#[test]
fn parse_unknown_modifier_fails() {
assert!("Unknown".parse::<Modifiers>().is_err());
assert!("Cmd+Unknown".parse::<Modifiers>().is_err());
}
#[test]
fn modifiers_display() {
assert_eq!(format!("{}", Modifiers::CMD), "Cmd");
assert_eq!(format!("{}", Modifiers::SHIFT), "Shift");
assert_eq!(
format!("{}", Modifiers::CMD | Modifiers::SHIFT),
"Shift+Cmd"
);
}
#[test]
fn modifiers_display_side_specific() {
assert_eq!(format!("{}", Modifiers::CMD_LEFT), "CmdLeft");
assert_eq!(format!("{}", Modifiers::CMD_RIGHT), "CmdRight");
assert_eq!(format!("{}", Modifiers::SHIFT_LEFT), "ShiftLeft");
assert_eq!(format!("{}", Modifiers::CTRL_RIGHT), "CtrlRight");
assert_eq!(format!("{}", Modifiers::OPT_LEFT), "OptLeft");
}
#[test]
fn matches_compound_hotkey() {
let hotkey = Modifiers::CMD;
assert!(hotkey.matches(Modifiers::CMD_LEFT));
assert!(hotkey.matches(Modifiers::CMD_RIGHT));
assert!(hotkey.matches(Modifiers::CMD_LEFT | Modifiers::CMD_RIGHT));
assert!(!hotkey.matches(Modifiers::empty()));
assert!(!hotkey.matches(Modifiers::SHIFT_LEFT));
}
#[test]
fn matches_side_specific_hotkey() {
let hotkey = Modifiers::CMD_LEFT;
assert!(hotkey.matches(Modifiers::CMD_LEFT));
assert!(!hotkey.matches(Modifiers::CMD_RIGHT));
assert!(hotkey.matches(Modifiers::CMD_LEFT | Modifiers::CMD_RIGHT));
assert!(!hotkey.matches(Modifiers::empty()));
}
#[test]
fn matches_rejects_extra_groups() {
let hotkey = Modifiers::CMD;
assert!(!hotkey.matches(Modifiers::CMD_LEFT | Modifiers::SHIFT_LEFT));
let hotkey = Modifiers::CMD_LEFT | Modifiers::SHIFT_LEFT;
assert!(hotkey.matches(Modifiers::CMD_LEFT | Modifiers::SHIFT_LEFT));
}
#[test]
fn matches_fn_exact() {
let hotkey = Modifiers::CMD | Modifiers::FN;
assert!(hotkey.matches(Modifiers::CMD_LEFT | Modifiers::FN));
assert!(!hotkey.matches(Modifiers::CMD_LEFT));
let hotkey = Modifiers::CMD;
assert!(!hotkey.matches(Modifiers::CMD_LEFT | Modifiers::FN)); }
#[test]
fn matches_empty() {
let hotkey = Modifiers::empty();
assert!(hotkey.matches(Modifiers::empty()));
assert!(!hotkey.matches(Modifiers::CMD_LEFT));
}
#[test]
fn compound_equals_both_sides() {
assert_eq!(Modifiers::CMD, Modifiers::CMD_LEFT | Modifiers::CMD_RIGHT);
assert_eq!(
Modifiers::SHIFT,
Modifiers::SHIFT_LEFT | Modifiers::SHIFT_RIGHT
);
assert_eq!(
Modifiers::CTRL,
Modifiers::CTRL_LEFT | Modifiers::CTRL_RIGHT
);
assert_eq!(Modifiers::OPT, Modifiers::OPT_LEFT | Modifiers::OPT_RIGHT);
}
}