// Package hotkey converts between human-readable hotkey strings // ("Ctrl+Alt+G") and the win32.Hotkey struct the registration syscall // expects. The mapping is intentionally limited to the keys WinAuth // users actually configured: letters, digits, F1-F12. package hotkey import ( "errors" "fmt" "strings" "git.wxccs.org/iceking2nd/winauth-go/internal/win32" ) // ErrEmpty is returned by Parse when the input is empty or whitespace. // It is *not* a syntax error — callers typically treat it as "no // hotkey configured" rather than a validation failure. var ErrEmpty = errors.New("hotkey: empty") // Parse turns "Ctrl+Alt+G" / "ctrl + shift + f5" into a win32.Hotkey. // Whitespace and case are ignored. Modifier order is irrelevant. func Parse(s string) (win32.Hotkey, error) { s = strings.TrimSpace(s) if s == "" { return win32.Hotkey{}, ErrEmpty } parts := strings.Split(s, "+") if len(parts) == 0 { return win32.Hotkey{}, fmt.Errorf("hotkey: malformed %q", s) } var mods uint32 var key string for _, p := range parts { token := strings.ToLower(strings.TrimSpace(p)) switch token { case "ctrl", "control": mods |= win32.ModCtrl case "alt": mods |= win32.ModAlt case "shift": mods |= win32.ModShift case "win", "super": mods |= win32.ModWin case "": // tolerate trailing "+" default: if key != "" { return win32.Hotkey{}, fmt.Errorf("hotkey: more than one base key in %q", s) } key = token } } if key == "" { return win32.Hotkey{}, fmt.Errorf("hotkey: no base key in %q", s) } if mods == 0 { return win32.Hotkey{}, fmt.Errorf("hotkey: %q has no modifier (would conflict with normal typing)", s) } vk, ok := vkFromName(key) if !ok { return win32.Hotkey{}, fmt.Errorf("hotkey: unsupported key %q", key) } return win32.Hotkey{Mods: mods | win32.ModNoRepeat, VK: vk}, nil } // Format canonicalises h back into a "Ctrl+Alt+G" style string. The // modifier order is fixed (Ctrl, Alt, Shift, Win) so two equivalent // hotkeys render identically. func Format(h win32.Hotkey) string { if h.VK == 0 { return "" } parts := make([]string, 0, 4) if h.Mods&win32.ModCtrl != 0 { parts = append(parts, "Ctrl") } if h.Mods&win32.ModAlt != 0 { parts = append(parts, "Alt") } if h.Mods&win32.ModShift != 0 { parts = append(parts, "Shift") } if h.Mods&win32.ModWin != 0 { parts = append(parts, "Win") } parts = append(parts, nameFromVK(h.VK)) return strings.Join(parts, "+") } // vkFromName maps the lowercase key name to a Win32 virtual-key code. // Returns false for anything it doesn't know. func vkFromName(name string) (uint32, bool) { if len(name) == 1 { c := name[0] switch { case c >= 'a' && c <= 'z': return uint32(c - 'a' + 'A'), true case c >= '0' && c <= '9': return uint32(c), true } } if strings.HasPrefix(name, "f") { // F1=0x70, F12=0x7B var n int if _, err := fmt.Sscanf(name, "f%d", &n); err == nil && n >= 1 && n <= 12 { return uint32(0x70 + n - 1), true } } switch name { case "space": return 0x20, true case "enter", "return": return 0x0D, true case "tab": return 0x09, true } return 0, false } // nameFromVK is the inverse of vkFromName for the cases Parse accepts. // Unknown codes render as their hex value so the UI still shows // something. func nameFromVK(vk uint32) string { switch { case vk >= 'A' && vk <= 'Z': return string(rune(vk)) case vk >= '0' && vk <= '9': return string(rune(vk)) case vk >= 0x70 && vk <= 0x7B: return fmt.Sprintf("F%d", vk-0x70+1) } switch vk { case 0x20: return "Space" case 0x0D: return "Enter" case 0x09: return "Tab" } return fmt.Sprintf("0x%X", vk) }