using System.Runtime.InteropServices; using LlamaApp.Common; namespace LlamaApp; /// /// Registers a system-wide hotkey while LlamaApp is running, mirroring the /// macOS spotlight/Raycast summon. Uses a tiny message-only Win32 window /// (created on the UI thread, whose message loop dispatches the resulting /// WM_HOTKEY) so we don't need to subclass any WinUI window's WndProc. /// /// "Only works when the app is started" falls out naturally: the hotkey /// is registered on and unregistered on /// , so it is live exactly for the app's lifetime. /// internal sealed class GlobalHotkey : IDisposable { private const uint WM_HOTKEY = 0x0312; private const int HotkeyId = 0x0001; // MOD_ALT + MOD_NOREPEAT, VK_SPACE → Alt+Space (the spotlight convention, // matching PowerToys Run's default). The global registration intercepts it // system-wide, so the Win11 system-menu shortcut is shadowed only while // the app is running. private const uint Modifier = 0x0001 /*MOD_ALT*/ | 0x4000 /*MOD_NOREPEAT*/; private const uint Vk = 0x20; // VK_SPACE private static readonly IntPtr HWND_MESSAGE = new(-3); // Single-slot pinning (one hotkey window per app). A GC handle keeps the // delegate alive across the native boundary; the static WndProc looks it up. private static WndProc? _current; private static GCHandle _currentHandle; private IntPtr _hwnd; private bool _registered; private bool _disposed; /// /// True when the hotkey is live. False after a failed /// (another app already owns Alt+Space) — the /// caller surfaces that instead of the shortcut silently doing nothing. /// public bool IsRegistered => _registered; /// Registers the hotkey and delivers presses to . public void Register(Action onPressed) { if (_registered) return; ObjectDisposedException.ThrowIf(_disposed, this); _current = (hwnd, msg, wParam, lParam) => { if (msg == WM_HOTKEY) { onPressed(); return IntPtr.Zero; } return DefWindowProcW(hwnd, msg, wParam, lParam); }; _currentHandle = GCHandle.Alloc(_current); _hwnd = CreateMessageOnlyWindow(); _registered = RegisterHotKey(_hwnd, HotkeyId, Modifier, Vk); if (!_registered) Log.Warn("global hotkey registration failed (Alt+Space may already be taken by another app)."); else Log.Info("global hotkey registered: Alt+Space"); } public void Dispose() { if (_disposed) return; _disposed = true; if (_registered) { UnregisterHotKey(_hwnd, HotkeyId); _registered = false; } if (_hwnd != IntPtr.Zero) { DestroyWindow(_hwnd); _hwnd = IntPtr.Zero; } if (_currentHandle.IsAllocated) { _currentHandle.Free(); _current = null; } } private static IntPtr CreateMessageOnlyWindow() { var className = "LlamaAppHotkeyWnd_" + Guid.NewGuid().ToString("N"); var fnPtr = Marshal.GetFunctionPointerForDelegate(_current!); var wc = new WNDCLASSEXW { cbSize = (uint)Marshal.SizeOf(), lpfnWndProc = fnPtr, lpszClassName = className, }; RegisterClassExW(ref wc); var hwnd = CreateWindowExW( 0, className, "LlamaAppHotkey", 0, 0, 0, 0, 0, HWND_MESSAGE, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); return hwnd; } // Window procedure MUST return LRESULT (IntPtr); unhandled messages defer // to DefWindowProc so creation messages (WM_NCCREATE etc.) succeed. private delegate IntPtr WndProc(IntPtr hwnd, uint msg, IntPtr wParam, IntPtr lParam); [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] private struct WNDCLASSEXW { public uint cbSize; public uint style; public IntPtr lpfnWndProc; public int cbClsExtra; public int cbWndExtra; public IntPtr hInstance; public IntPtr hIcon; public IntPtr hCursor; public IntPtr hbrBackground; public string? lpszMenuName; public string? lpszClassName; public IntPtr hIconSm; } [DllImport("user32.dll", SetLastError = true, EntryPoint = "RegisterClassExW", CharSet = CharSet.Unicode)] private static extern ushort RegisterClassExW([In] ref WNDCLASSEXW lpwcx); [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] private static extern IntPtr CreateWindowExW( uint dwExStyle, string lpClassName, string lpWindowName, uint dwStyle, int x, int y, int nWidth, int nHeight, IntPtr hWndParent, IntPtr hMenu, IntPtr hInstance, IntPtr lpParam); [DllImport("user32.dll", SetLastError = true)] private static extern bool DestroyWindow(IntPtr hwnd); [DllImport("user32.dll")] private static extern IntPtr DefWindowProcW(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); [DllImport("user32.dll", SetLastError = true)] private static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk); [DllImport("user32.dll", SetLastError = true)] private static extern bool UnregisterHotKey(IntPtr hWnd, int id); }