SharpHook

repository·main·Indexed 19 days ago

https://github.com/tolikpylypchuk/sharphook

A cross-platform .NET wrapper for libuiohook that provides global keyboard and mouse hooks, as well as event and text entry simulation. It supports .NET 8+, .NET Framework 4.7.2+, and .NET Standard 2.0 across Windows, macOS, and Linux (X11). The library includes multiple threading models via IGlobalHook (Simple, EventLoop, and TaskPool) and offers reactive extensions through SharpHook.Reactive (Rx.NET) and SharpHook.R3.

Tokens
18.3K
Snippets
30
Records
71
Agent score
66%

What's inside SharpHook

  1. Suppress input events (Windows and macOS only)

    main

    You can block an event from propagating further by setting the EventMask.SuppressEvent flag in the event handler. This must be done synchronously on the same thread that handles the event.

    Note: Suppressing KeyTyped and MouseClicked events has no effect because these are raised by libuiohook itself rather than the OS.

  2. How to pass custom data to callbacks safely

    main

    Both SetDispatchProc and SetLoggerProc allow passing a pointer to user-supplied data (nint).

    Warning: It is generally recommended to pass IntPtr.Zero and avoid using these parameters to point to managed objects. Pinning managed objects for the lifetime of these long-lived callbacks can degrade garbage collector performance and memory layout.

    Recommended Approach: Use closures to capture data instead of passing pointers.

    Exception (Mac Catalyst): For Mac Catalyst applications, callbacks must be static and annotated with [MonoPInvokeCallback], which prevents the use of closures. In this specific case, use the nint parameter as a key (e.g., an integer key for a static dictionary) rather than a direct pointer to a managed object.

  3. Suppress input events on Windows and macOS

    main

    To prevent an input event from reaching the rest of the system, you can set the SuppressEvent property to true within your event handler.

    Requirements for suppression:

    1. The operation must be performed synchronously within the handler.
    2. It is only supported on Windows and macOS.

    Example logic within a handler:

    hook.KeyTyped += (s, e) => {
        if (someCondition) {
            e.SuppressEvent = true;
        }
    };
  4. Implement custom hooks using BasicGlobalHookBase

    main

    If you need to implement a hook that does not follow the IGlobalHook interface (for example, if you are implementing IReactiveGlobalHook or IR3GlobalHook), you should extend BasicGlobalHookBase instead of GlobalHookBase.

    BasicGlobalHookBase is the root implementation for the SharpHook hierarchy. It provides the core logic for IBasicGlobalHook (which defines Run, RunAsync, Stop, IsRunning, and IsDisposed) and handles the non-trivial event-handling machinery.

    By extending BasicGlobalHookBase, you gain access to HandleHookEvent, BeforeRun, AfterStop, and Dispose without having to implement the underlying provider logic yourself.

    public sealed class StraightforwardGlobalHook : BasicGlobalHookBase
    {
        protected override void HandleHookEvent(ref UioHookEvent e) =>
            this.HookEvent?.Invoke(this, e);
    
        public event EventHandler<UioHookEvent>? HookEvent;
    }
  5. Choose the right IGlobalHook implementation

    main

    SharpHook provides three implementations of IGlobalHook, each with different threading models for handling events:

    ImplementationThreading ModelBest Use Case
    SimpleGlobalHookRuns all handlers on the same thread as the hook.Very simple, fast handlers where blocking the hook is acceptable.
    EventLoopGlobalHookRuns handlers on a dedicated separate thread; queues events on backpressure.Preferred default. Use this for most scenarios. Note: Event suppression is ignored because handlers run on a different thread.
    TaskPoolGlobalHookRuns handlers in parallel using the default .NET Task Pool.Use only if you specifically need to process events in parallel. Note: Event suppression is ignored.

    Configuring Hook Types

    You can create a keyboard-only or mouse-only hook by passing a GlobalHookType to the constructor.

    • Windows: This utilizes the OS-specific distinction between keyboard and mouse hooks.
    • macOS/Linux: This simply enables filtering of the respective event types.
  6. Implement IReactiveGlobalHook

    main

    SharpHook.Reactive provides two primary implementations of the IReactiveGlobalHook interface:

    1. SharpHook.Reactive.ReactiveGlobalHook: A standalone implementation. Since it uses observables, you are responsible for managing event handling via schedulers. You can specify a default scheduler for all observables.

    2. SharpHook.Reactive.ReactiveGlobalHookAdapter: An adapter that wraps an existing IGlobalHook and converts its events into IReactiveGlobalHook observables. All subscriptions and state changes on the underlying hook are propagated to the adapter. A default scheduler can also be specified here.

  7. How Global Hooks work in SharpHook

    main

    A Global Hook allows you to monitor and intercept system-wide keyboard and mouse events. SharpHook provides the IGlobalHook interface with three primary implementations, each with different threading models:

    1. SimpleGlobalHook: Runs all event handlers on the same thread as the hook. Handlers must be extremely fast to avoid blocking the hook from processing subsequent events.
    2. EventLoopGlobalHook: Runs event handlers on a dedicated separate thread. It queues events if backpressure occurs, ensuring no events are lost. Note: Because handlers run on a different thread, suppressing event propagation (setting SuppressEvent = true) is not supported.
    3. TaskPoolGlobalHook: Runs event handlers in parallel using the default .NET task pool. Like the event loop implementation, it queues events on backpressure, but suppressing event propagation is not supported.

    Critical Constraints:

    • Single Instance Rule: You must use only one IGlobalHook instance at a time in your entire application. Multiple instances will attempt to use the same underlying callback in libuiohook, leading to corruption of the internal global state.
    • Lifecycle: IGlobalHook implements IDisposable. Calling Dispose stops the hook and prevents it from being restarted. You must create a new instance to start a hook again after disposal.
    // Example of using EventLoopGlobalHook
    var hook = new EventLoopGlobalHook();
    
    hook.KeyPressed += (s, e) => Console.WriteLine($"Key pressed: {e.Key}");
    
    hook.Run(); // Blocks current thread
    // OR
    await hook.RunAsync(); // Runs on separate thread
  8. Implement IR3GlobalHook

    main

    SharpHook.R3 provides two primary implementations of the IR3GlobalHook interface:

    1. SharpHook.R3.R3GlobalHook: A standalone implementation where you manage event handling via observables. You can specify a default time provider for all observables to control time-based operations.
    2. SharpHook.R3.R3GlobalHookAdapter: An adapter that wraps an existing IGlobalHook and converts its events into IR3GlobalHook observables. All subscriptions and state changes on the underlying IGlobalHook are propagated to the adapter. Like the standalone version, a default time provider can be specified.
  9. Lifecycle and Constraints for Reactive Global Hooks

    main

    Both IReactiveGlobalHook (Rx.NET) and IR3GlobalHook (R3) follow these lifecycle rules and constraints:

    Critical Constraint: Singleton Usage

    Always use only one instance of a reactive global hook at a time in your entire application. Because all hooks must use the same static method to set the callback for libuiohook, running multiple hooks simultaneously will corrupt the internal global state of libuiohook.

    Lifecycle Methods

    • Run() / RunAsync(): Starts the hook. Running the hook while it is already running is not allowed. Check IsRunning to verify state.
    • Stop(): Stops the running hook.
    • Dispose(): Disposes of the hook and stops it if it is running. All observables will complete upon disposal. Once disposed, the instance cannot be started again. Check IsDisposed to verify state.

    Observables

    All observables emit EventArgs-derived types. When the hook is disposed, all active observables will complete.

  10. Understand SharpHook SemVer exceptions

    main
    SharpHook follows Semantic Versioning (SemVer) with two specific exceptions. Changes to the IEventSimulator interface and interfaces within the SharpHook.Providers namespace are treated as minor updates rather than breaking major updates. This is because these interfaces are intended as abstractions over internal classes and are not meant to be implemented directly in client code. Changes to them are expected to be safe for consumers.
  11. Understand SharpHook KeyCode mappings

    main

    SharpHook uses the SharpHook.Data.KeyCode enum to provide a cross-platform abstraction for keyboard input. This enum maps virtual key codes to OS-specific definitions for Windows, macOS, X11, and Evdev.

    Important Usage Rules

    • Do not rely on integer values: The underlying numeric values in the KeyCode enum are meaningless and may change between major versions.
    • Use Enum Names: Always use the enum constant names (e.g., KeyCode.VcEscape) when writing logic or persisting key codes. This ensures your code remains compatible across updates.
    • Platform Availability: Not all key codes are available on all operating systems. Some mappings may be undefined for specific platforms.