// 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; } } }