// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Concurrent; using System.Runtime.InteropServices; namespace Microsoft.Maui.Platform.Linux.Services; /// /// Provides global hotkey registration and handling using X11. /// public class GlobalHotkeyService : IDisposable { private nint _display; private nint _rootWindow; private readonly ConcurrentDictionary _registrations = new(); private int _nextId = 1; private bool _disposed; private Thread? _eventThread; private bool _isListening; /// /// Event raised when a registered hotkey is pressed. /// public event EventHandler? HotkeyPressed; /// /// Initializes the global hotkey service. /// public void Initialize() { _display = XOpenDisplay(IntPtr.Zero); if (_display == IntPtr.Zero) { throw new InvalidOperationException("Failed to open X display"); } _rootWindow = XDefaultRootWindow(_display); // Start listening for hotkeys in background _isListening = true; _eventThread = new Thread(ListenForHotkeys) { IsBackground = true, Name = "GlobalHotkeyListener" }; _eventThread.Start(); } /// /// Registers a global hotkey. /// /// The key code. /// The modifier keys. /// A registration ID that can be used to unregister. public int Register(HotkeyKey key, HotkeyModifiers modifiers) { if (_display == IntPtr.Zero) { throw new InvalidOperationException("Service not initialized"); } int keyCode = XKeysymToKeycode(_display, (nint)key); if (keyCode == 0) { throw new ArgumentException($"Invalid key: {key}"); } uint modifierMask = GetModifierMask(modifiers); // Register for all modifier combinations (with/without NumLock, CapsLock) uint[] masks = GetModifierCombinations(modifierMask); foreach (var mask in masks) { int result = XGrabKey(_display, keyCode, mask, _rootWindow, true, GrabModeAsync, GrabModeAsync); if (result == 0) { Console.WriteLine($"Failed to grab key {key} with modifiers {modifiers}"); } } int id = _nextId++; _registrations[id] = new HotkeyRegistration { Id = id, KeyCode = keyCode, Modifiers = modifierMask, Key = key, ModifierKeys = modifiers }; XFlush(_display); return id; } /// /// Unregisters a global hotkey. /// /// The registration ID. public void Unregister(int id) { if (_registrations.TryRemove(id, out var registration)) { uint[] masks = GetModifierCombinations(registration.Modifiers); foreach (var mask in masks) { XUngrabKey(_display, registration.KeyCode, mask, _rootWindow); } XFlush(_display); } } /// /// Unregisters all global hotkeys. /// public void UnregisterAll() { foreach (var id in _registrations.Keys.ToList()) { Unregister(id); } } private void ListenForHotkeys() { while (_isListening && _display != IntPtr.Zero) { try { if (XPending(_display) > 0) { var xevent = new XEvent(); XNextEvent(_display, ref xevent); if (xevent.type == KeyPress) { var keyEvent = xevent.KeyEvent; ProcessKeyEvent(keyEvent.keycode, keyEvent.state); } } else { Thread.Sleep(10); } } catch (Exception ex) { Console.WriteLine($"GlobalHotkeyService error: {ex.Message}"); } } } private void ProcessKeyEvent(int keyCode, uint state) { // Remove NumLock and CapsLock from state for comparison uint cleanState = state & ~(NumLockMask | CapsLockMask | ScrollLockMask); foreach (var registration in _registrations.Values) { if (registration.KeyCode == keyCode && (registration.Modifiers == cleanState || registration.Modifiers == (cleanState & ~Mod2Mask))) // Mod2 is often NumLock { OnHotkeyPressed(registration); break; } } } private void OnHotkeyPressed(HotkeyRegistration registration) { HotkeyPressed?.Invoke(this, new HotkeyEventArgs( registration.Id, registration.Key, registration.ModifierKeys)); } private uint GetModifierMask(HotkeyModifiers modifiers) { uint mask = 0; if (modifiers.HasFlag(HotkeyModifiers.Shift)) mask |= ShiftMask; if (modifiers.HasFlag(HotkeyModifiers.Control)) mask |= ControlMask; if (modifiers.HasFlag(HotkeyModifiers.Alt)) mask |= Mod1Mask; if (modifiers.HasFlag(HotkeyModifiers.Super)) mask |= Mod4Mask; return mask; } private uint[] GetModifierCombinations(uint baseMask) { // Include combinations with NumLock and CapsLock return new uint[] { baseMask, baseMask | NumLockMask, baseMask | CapsLockMask, baseMask | NumLockMask | CapsLockMask }; } public void Dispose() { if (_disposed) return; _disposed = true; _isListening = false; UnregisterAll(); if (_display != IntPtr.Zero) { XCloseDisplay(_display); _display = IntPtr.Zero; } } #region X11 Interop private const int KeyPress = 2; private const int GrabModeAsync = 1; private const uint ShiftMask = 1 << 0; private const uint LockMask = 1 << 1; // CapsLock private const uint ControlMask = 1 << 2; private const uint Mod1Mask = 1 << 3; // Alt private const uint Mod2Mask = 1 << 4; // NumLock private const uint Mod4Mask = 1 << 6; // Super private const uint NumLockMask = Mod2Mask; private const uint CapsLockMask = LockMask; private const uint ScrollLockMask = 0; // Usually not used [StructLayout(LayoutKind.Explicit)] private struct XEvent { [FieldOffset(0)] public int type; [FieldOffset(0)] public XKeyEvent KeyEvent; } [StructLayout(LayoutKind.Sequential)] private struct XKeyEvent { public int type; public ulong serial; public bool send_event; public nint display; public nint window; public nint root; public nint subwindow; public ulong time; public int x, y; public int x_root, y_root; public uint state; public int keycode; public bool same_screen; } [DllImport("libX11.so.6")] private static extern nint XOpenDisplay(nint display); [DllImport("libX11.so.6")] private static extern void XCloseDisplay(nint display); [DllImport("libX11.so.6")] private static extern nint XDefaultRootWindow(nint display); [DllImport("libX11.so.6")] private static extern int XKeysymToKeycode(nint display, nint keysym); [DllImport("libX11.so.6")] private static extern int XGrabKey(nint display, int keycode, uint modifiers, nint grabWindow, bool ownerEvents, int pointerMode, int keyboardMode); [DllImport("libX11.so.6")] private static extern int XUngrabKey(nint display, int keycode, uint modifiers, nint grabWindow); [DllImport("libX11.so.6")] private static extern int XPending(nint display); [DllImport("libX11.so.6")] private static extern int XNextEvent(nint display, ref XEvent xevent); [DllImport("libX11.so.6")] private static extern void XFlush(nint display); #endregion private class HotkeyRegistration { public int Id { get; set; } public int KeyCode { get; set; } public uint Modifiers { get; set; } public HotkeyKey Key { get; set; } public HotkeyModifiers ModifierKeys { get; set; } } } /// /// Event args for hotkey pressed events. /// public class HotkeyEventArgs : EventArgs { /// /// Gets the registration ID. /// public int Id { get; } /// /// Gets the key. /// public HotkeyKey Key { get; } /// /// Gets the modifier keys. /// public HotkeyModifiers Modifiers { get; } public HotkeyEventArgs(int id, HotkeyKey key, HotkeyModifiers modifiers) { Id = id; Key = key; Modifiers = modifiers; } } /// /// Hotkey modifier keys. /// [Flags] public enum HotkeyModifiers { None = 0, Shift = 1 << 0, Control = 1 << 1, Alt = 1 << 2, Super = 1 << 3 } /// /// Hotkey keys (X11 keysyms). /// public enum HotkeyKey : uint { // Letters A = 0x61, B = 0x62, C = 0x63, D = 0x64, E = 0x65, F = 0x66, G = 0x67, H = 0x68, I = 0x69, J = 0x6A, K = 0x6B, L = 0x6C, M = 0x6D, N = 0x6E, O = 0x6F, P = 0x70, Q = 0x71, R = 0x72, S = 0x73, T = 0x74, U = 0x75, V = 0x76, W = 0x77, X = 0x78, Y = 0x79, Z = 0x7A, // Numbers D0 = 0x30, D1 = 0x31, D2 = 0x32, D3 = 0x33, D4 = 0x34, D5 = 0x35, D6 = 0x36, D7 = 0x37, D8 = 0x38, D9 = 0x39, // Function keys F1 = 0xFFBE, F2 = 0xFFBF, F3 = 0xFFC0, F4 = 0xFFC1, F5 = 0xFFC2, F6 = 0xFFC3, F7 = 0xFFC4, F8 = 0xFFC5, F9 = 0xFFC6, F10 = 0xFFC7, F11 = 0xFFC8, F12 = 0xFFC9, // Special keys Escape = 0xFF1B, Tab = 0xFF09, Return = 0xFF0D, Space = 0x20, BackSpace = 0xFF08, Delete = 0xFFFF, Insert = 0xFF63, Home = 0xFF50, End = 0xFF57, PageUp = 0xFF55, PageDown = 0xFF56, // Arrow keys Left = 0xFF51, Up = 0xFF52, Right = 0xFF53, Down = 0xFF54, // Media keys AudioPlay = 0x1008FF14, AudioStop = 0x1008FF15, AudioPrev = 0x1008FF16, AudioNext = 0x1008FF17, AudioMute = 0x1008FF12, AudioRaiseVolume = 0x1008FF13, AudioLowerVolume = 0x1008FF11, // Print screen Print = 0xFF61 }