///|
/// Stores the normalized shortcut data passed into the native backend.
struct ParsedGlobalHotkey {
normalized : String
modifiers : Int
key_name : String
} derive(Debug, Eq)
///|
/// Tracks which modifier families have already been seen while parsing.
priv struct ModifierSet {
mut control : Bool
mut shift : Bool
mut alt : Bool
mut meta : Bool
}
///|
/// Represents the canonical modifier families supported by this package.
priv enum ModifierKind {
Control
Shift
Alt
Meta
}
///|
/// Creates an empty modifier set for one accelerator parse.
fn new_modifier_set() -> ModifierSet {
{ control: false, shift: false, alt: false, meta: false }
}
///|
/// Normalizes a single modifier or key token for case-insensitive matching.
fn normalize_keyword(raw : String) -> String {
raw.trim().to_owned().to_lower()
}
///|
/// Returns the user-facing duplicate-modifier error for one modifier family.
fn ModifierKind::duplicate_error(self : ModifierKind) -> String {
match self {
Control => "global_hotkey accelerator repeats Ctrl"
Shift => "global_hotkey accelerator repeats Shift"
Alt => "global_hotkey accelerator repeats Alt"
Meta => "global_hotkey accelerator repeats Meta"
}
}
///|
/// Parses modifier aliases into their canonical modifier family.
fn parse_modifier_kind(token : String) -> ModifierKind? {
match normalize_keyword(token) {
"ctrl" | "control" | "ctl" => Some(Control)
"shift" => Some(Shift)
"alt" | "option" => Some(Alt)
"meta" | "cmd" | "command" | "super" | "win" | "windows" => Some(Meta)
_ => None
}
}
///|
/// Marks one modifier family as seen, rejecting duplicate aliases.
fn ModifierSet::enable(
self : ModifierSet,
modifier : ModifierKind,
) -> Result[Unit, String] {
match modifier {
Control =>
if self.control {
Err(modifier.duplicate_error())
} else {
self.control = true
Ok(())
}
Shift =>
if self.shift {
Err(modifier.duplicate_error())
} else {
self.shift = true
Ok(())
}
Alt =>
if self.alt {
Err(modifier.duplicate_error())
} else {
self.alt = true
Ok(())
}
Meta =>
if self.meta {
Err(modifier.duplicate_error())
} else {
self.meta = true
Ok(())
}
}
}
///|
/// Converts modifier booleans into the backend's logical modifier bitset.
fn ModifierSet::mask(self : ModifierSet) -> Int {
let mut mask = 0
if self.alt {
mask += 0x0001
}
if self.control {
mask += 0x0002
}
if self.shift {
mask += 0x0004
}
if self.meta {
mask += 0x0008
}
mask
}
///|
/// Renders a normalized accelerator string in a stable modifier order.
fn ModifierSet::normalize_accelerator_name(
self : ModifierSet,
key_name : String,
) -> String {
let builder = StringBuilder::new()
let mut wrote_modifier = false
if self.control {
builder.write_string("Ctrl")
wrote_modifier = true
}
if self.shift {
if wrote_modifier {
builder.write_char('+')
}
builder.write_string("Shift")
wrote_modifier = true
}
if self.alt {
if wrote_modifier {
builder.write_char('+')
}
builder.write_string("Alt")
wrote_modifier = true
}
if self.meta {
if wrote_modifier {
builder.write_char('+')
}
builder.write_string("Meta")
wrote_modifier = true
}
if wrote_modifier {
builder.write_char('+')
}
builder.write_string(key_name)
builder.to_string()
}
///|
/// Builds the unsupported-key error shared by all key decoders.
fn unsupported_key_error(raw : String) -> String {
"unsupported global_hotkey key: " + raw
}
///|
/// Decodes a key token into the canonical key name used by all backends.
fn decode_key_name(raw : String) -> Result[String, String] {
let token = raw.trim().to_owned()
let upper = token.to_upper()
if upper.length() == 1 {
match upper.get_char(0) {
Some(ch) =>
if (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') {
Ok(upper)
} else {
match ch {
'-' => Ok("Minus")
'=' => Ok("Equal")
',' => Ok("Comma")
'.' => Ok("Period")
'/' => Ok("Slash")
'\\' => Ok("Backslash")
';' => Ok("Semicolon")
'\'' => Ok("Quote")
'`' => Ok("Backquote")
'[' => Ok("LeftBracket")
']' => Ok("RightBracket")
_ => Err(unsupported_key_error(raw))
}
}
None => Err(unsupported_key_error(raw))
}
} else {
match normalize_keyword(token) {
"space" => Ok("Space")
"tab" => Ok("Tab")
"enter" => Ok("Enter")
"return" => Ok("Enter")
"escape" => Ok("Escape")
"esc" => Ok("Escape")
"backspace" => Ok("Backspace")
"delete" => Ok("Delete")
"del" => Ok("Delete")
"insert" => Ok("Insert")
"ins" => Ok("Insert")
"home" => Ok("Home")
"end" => Ok("End")
"pageup" => Ok("PageUp")
"page-up" => Ok("PageUp")
"pgup" => Ok("PageUp")
"pagedown" => Ok("PageDown")
"page-down" => Ok("PageDown")
"pgdown" => Ok("PageDown")
"left" => Ok("Left")
"right" => Ok("Right")
"up" => Ok("Up")
"down" => Ok("Down")
"minus" => Ok("Minus")
"hyphen" => Ok("Minus")
"equal" => Ok("Equal")
"equals" => Ok("Equal")
"plus" => Ok("Plus")
"comma" => Ok("Comma")
"period" => Ok("Period")
"dot" => Ok("Period")
"slash" => Ok("Slash")
"backslash" => Ok("Backslash")
"semicolon" => Ok("Semicolon")
"quote" => Ok("Quote")
"apostrophe" => Ok("Quote")
"backquote" => Ok("Backquote")
"grave" => Ok("Backquote")
"tilde" => Ok("Backquote")
"leftbracket" => Ok("LeftBracket")
"lbracket" => Ok("LeftBracket")
"bracketleft" => Ok("LeftBracket")
"rightbracket" => Ok("RightBracket")
"rbracket" => Ok("RightBracket")
"bracketright" => Ok("RightBracket")
"f1" => Ok("F1")
"f2" => Ok("F2")
"f3" => Ok("F3")
"f4" => Ok("F4")
"f5" => Ok("F5")
"f6" => Ok("F6")
"f7" => Ok("F7")
"f8" => Ok("F8")
"f9" => Ok("F9")
"f10" => Ok("F10")
"f11" => Ok("F11")
"f12" => Ok("F12")
"f13" => Ok("F13")
"f14" => Ok("F14")
"f15" => Ok("F15")
"f16" => Ok("F16")
"f17" => Ok("F17")
"f18" => Ok("F18")
"f19" => Ok("F19")
"f20" => Ok("F20")
"f21" => Ok("F21")
"f22" => Ok("F22")
"f23" => Ok("F23")
"f24" => Ok("F24")
_ => Err(unsupported_key_error(raw))
}
}
}
///|
/// Parses a user-facing accelerator string into canonical backend data.
fn parse_accelerator(raw : String) -> Result[ParsedGlobalHotkey, String] {
let parts = raw.split("+").to_array()
let modifiers = new_modifier_set()
let mut key_name : String? = None
for part in parts {
let token = part.to_owned().trim().to_owned()
if token.is_empty() {
return Err("global_hotkey accelerator contains an empty segment")
}
match parse_modifier_kind(token) {
Some(modifier) =>
match modifiers.enable(modifier) {
Ok(_) => ()
Err(error) => return Err(error)
}
None =>
match key_name {
Some(_) =>
return Err("global_hotkey accelerator must contain exactly one key")
None =>
match decode_key_name(token) {
Ok(name) => key_name = Some(name)
Err(error) => return Err(error)
}
}
}
}
match key_name {
Some(key_name) => {
let normalized = modifiers.normalize_accelerator_name(key_name)
Ok({ normalized, modifiers: modifiers.mask(), key_name })
}
None => Err("global_hotkey accelerator must include a key")
}
}