using System.Runtime.InteropServices; namespace Lumi.Companion.Overlay; public sealed class GlobalHotkeyManager : IDisposable { private const uint WmHotkey = 0x0312; private const uint WmQuit = 0x0012; private readonly Dictionary _actions = []; private readonly ManualResetEventSlim _ready = new(); private readonly Thread _thread; private IntPtr _window; private uint _threadId; private int _nextId = 0x4C00; public GlobalHotkeyManager() { _thread = new Thread(MessageLoop) { IsBackground = true, Name = "Lumi Overlay hotkeys" }; _thread.Start(); _ready.Wait(TimeSpan.FromSeconds(3)); } public bool Register(string chord, Action action) { if (!TryParse(chord, out var modifiers, out var key) || _window == IntPtr.Zero) return false; var id = Interlocked.Increment(ref _nextId); if (!RegisterHotKey(_window, id, modifiers | 0x4000, key)) return false; lock (_actions) _actions[id] = action; return true; } public void Clear() { lock (_actions) { foreach (var id in _actions.Keys) UnregisterHotKey(_window, id); _actions.Clear(); } } private void MessageLoop() { _threadId = GetCurrentThreadId(); _window = CreateWindowEx(0, "STATIC", "LumiOverlayHotkeys", 0, 0, 0, 0, 0, new IntPtr(-3), IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); _ready.Set(); while (GetMessage(out var message, IntPtr.Zero, 0, 0) > 0) { if (message.message == WmHotkey) { Action? action; lock (_actions) _actions.TryGetValue(message.wParam.ToInt32(), out action); try { action?.Invoke(); } catch { } } TranslateMessage(ref message); DispatchMessage(ref message); } } private static bool TryParse(string chord, out uint modifiers, out uint key) { modifiers = 0; key = 0; var parts = (chord ?? string.Empty).Split('+', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); foreach (var part in parts) { if (part.Equals("Ctrl", StringComparison.OrdinalIgnoreCase)) modifiers |= 0x0002; else if (part.Equals("Alt", StringComparison.OrdinalIgnoreCase)) modifiers |= 0x0001; else if (part.Equals("Shift", StringComparison.OrdinalIgnoreCase)) modifiers |= 0x0004; else if (part.Equals("Win", StringComparison.OrdinalIgnoreCase)) modifiers |= 0x0008; else if (part.Length == 1 && char.IsAsciiLetterOrDigit(part[0])) key = char.ToUpperInvariant(part[0]); else if (part.StartsWith('F') && int.TryParse(part[1..], out var function) && function is >= 1 and <= 24) key = (uint)(0x70 + function - 1); else return false; } return key != 0; } public void Dispose() { Clear(); if (_threadId != 0) PostThreadMessage(_threadId, WmQuit, IntPtr.Zero, IntPtr.Zero); _thread.Join(TimeSpan.FromSeconds(1)); _ready.Dispose(); } [StructLayout(LayoutKind.Sequential)] private struct Msg { public IntPtr hwnd; public uint message; public IntPtr wParam; public IntPtr lParam; public uint time; public int x; public int y; } [DllImport("user32.dll", SetLastError = true)] private static extern bool RegisterHotKey(IntPtr hwnd, int id, uint modifiers, uint key); [DllImport("user32.dll", SetLastError = true)] private static extern bool UnregisterHotKey(IntPtr hwnd, int id); [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] private static extern IntPtr CreateWindowEx(uint exStyle, string className, string windowName, uint style, int x, int y, int width, int height, IntPtr parent, IntPtr menu, IntPtr instance, IntPtr parameter); [DllImport("user32.dll")] private static extern int GetMessage(out Msg message, IntPtr hwnd, uint min, uint max); [DllImport("user32.dll")] private static extern bool TranslateMessage(ref Msg message); [DllImport("user32.dll")] private static extern IntPtr DispatchMessage(ref Msg message); [DllImport("kernel32.dll")] private static extern uint GetCurrentThreadId(); [DllImport("user32.dll")] private static extern bool PostThreadMessage(uint threadId, uint message, IntPtr wParam, IntPtr lParam); }