A .NET library to subscribe to global user actions across Windows, macOS, and Linux: keyboard, mouse, clipboard, application windows, print jobs, and hotkeys.
develop)dotnet add package EventHook
| Platform | Target framework |
|---|---|
| Windows | net10.0-windows |
| macOS / Linux | net10.0 |
Windows applications must target net10.0-windows. The portable net10.0 package is for macOS and Linux.
using System;
using (var eventHookFactory = new EventHookFactory())
{
var keyboardWatcher = eventHookFactory.GetKeyboardWatcher();
var kb = keyboardWatcher.Start();
if (!kb.Success)
{
Console.WriteLine(kb); // PermissionDenied / PrivilegeRequired / etc.
}
else
{
keyboardWatcher.OnKeyInput += (s, e) =>
Console.WriteLine($"Key {e.KeyData.EventType} of {e.KeyData.Keyname}");
}
var mouseWatcher = eventHookFactory.GetMouseWatcher();
mouseWatcher.IncludeMouseMove = false; // default is false
mouseWatcher.Start().ThrowIfFailed();
mouseWatcher.OnMouseInput += (s, e) =>
Console.WriteLine($"Mouse {e.Message} at {e.Point.x},{e.Point.y}");
var clipboardWatcher = eventHookFactory.GetClipboardWatcher();
clipboardWatcher.Start().ThrowIfFailed();
clipboardWatcher.OnClipboardModified += (s, e) =>
Console.WriteLine($"Clipboard {e.DataFormat}: {e.Data}");
var applicationWatcher = eventHookFactory.GetApplicationWatcher();
applicationWatcher.Start().ThrowIfFailed();
applicationWatcher.OnApplicationWindowChange += (s, e) =>
Console.WriteLine($"{e.ApplicationData.AppName} was {e.Event}");
var printWatcher = eventHookFactory.GetPrintWatcher();
printWatcher.Start().ThrowIfFailed();
printWatcher.OnPrintEvent += (s, e) =>
Console.WriteLine($"Printer {e.EventData.PrinterName} pages={e.EventData.Pages}");
var hotkeyWatcher = eventHookFactory.GetHotkeyWatcher();
hotkeyWatcher.Start().ThrowIfFailed();
hotkeyWatcher.Register("demo", new Hotkey(KeyModifiers.Control | KeyModifiers.Alt, EventKey.H))
.ThrowIfFailed();
hotkeyWatcher.OnHotkeyPressed += (s, e) =>
Console.WriteLine($"Hotkey {e.Id} ({e.Hotkey})");
Console.ReadLine();
}
OS hook callbacks only copy a lightweight snapshot and return immediately. Decoding and user event handlers run on a dedicated offload path so input is never blocked.
Start() / Register() return HookStartResult. Check Success, or call ThrowIfFailed(). IsRunning is true only after a successful install.
| Reason | Typical cause |
|---|---|
PermissionDenied |
macOS Input Monitoring / Accessibility (TCC) |
PrivilegeRequired |
Linux /dev/input not readable (add user to input group) |
DisplayUnavailable |
No DISPLAY / session when required (Linux/macOS). Not used for a missing Win32 pump. |
NotSupportedOnPlatform |
Feature needs X11 on Linux Wayland-only, or wrong TFM on Windows |
AlreadyInUse |
Hotkey already registered by another process |
NativeFailure |
Native API failed after permissions were OK |
If EventHookFactory cannot start its own Windows message pump (or macOS cannot start its CFRunLoop), construction throws TimeoutException. That is not a HookStartResult — there is no HookFailureReason for “pump missing.”
See examples/MAC.md and examples/LINUX.md for host setup.
Hooks need an OS event loop. Callers usually do not create one: the factory (Windows) or platform hosts (macOS / Linux) start it.
| Platform | Who pumps | What you must do |
|---|---|---|
| Windows | EventHookFactory |
Nothing in console/service code. Construct the factory on an STA UI thread or let it create a background STA WinForms loop (EventHook.MessagePump). |
| macOS | Library CFRunLoop (EventHook.Mac.CFRunLoop) |
Nothing. The HWND argument is ignored. |
| Linux X11 | Library XNextEvent thread |
Need DISPLAY (or Xvfb). HWND is ignored. |
| Linux evdev | /dev/input reads (no X loop) |
User must be in the input group. Clipboard / windows / hotkeys still need X11. |
Windows details
WH_KEYBOARD_LL / WH_MOUSE_LL) are installed on the factory pump thread. That thread must keep pumping or Windows stops delivering.WM_CLIPBOARDUPDATE), hotkeys (WM_HOTKEY), and application/shell hooks are HWND messages on the same pump.WindowHookEx does not use the factory pump. Call Start() from a thread that already pumps messages (your UI thread).WndProc must dispatch WM_HOTKEY or the watcher will report IsRunning and still never fire.new EventHookFactory() (library-owned pump) unless you are hosting and will forward messages.EventHook.Helpers.AppWindowFilter.IncludeWindowsWithoutSysMenu = true;
EventHook.Helpers.AppWindowFilter.IncludeDialogs = true;
EventHook.Helpers.AppWindowFilter.CustomFilter = hwnd => true;
Prefer constructing the factory on an STA UI thread, or pass an existing message-pump HWND:
using var factory = new EventHookFactory(hostMainWindowHandle);
dotnet build src/Event.Hook.sln -c Releasedotnet test tests/Event.Hook.Testsdotnet test tests/Event.Hook.IntegrationTestsdotnet test tests/Event.Hook.E2ETests --filter Category=E2Edotnet tool restore then docfx .github/docfx.json| Branch | NuGet | Notes |
|---|---|---|
develop |
(no publish) | CI on Windows + macOS + Linux; SonarCloud + DocFX on develop push only |
beta |
{VersionPrefix}-beta.2 |
Same CI, then publish prerelease (3.0.0-beta already shipped) |
stable / tag v* |
{VersionPrefix} |
Same CI, then stable release + GitHub Pages docs |
Publishing uses NuGet Trusted Publishing (NUGET_USER on the nuget-publish environment).
Breaking: multi-platform (net10.0-windows + net10.0), Start()/Register() return HookStartResult, portable Hotkey replaces WinForms Keys, IncludeMouseMove defaults to false, non-blocking OS hook offload.
Repository renamed from windows-user-action-hook to event-hook. Solution/projects use Event.Hook.*; NuGet package id remains EventHook.