• Stars
    star
    160
  • Rank 233,689 (Top 5 %)
  • Language
    C#
  • License
    MIT License
  • Created about 3 years ago
  • Updated about 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

SharpHook provides a cross-platform global keyboard and mouse hook for .NET, and the ability to simulate input events

SharpHook

NuGet NuGet

SharpHook provides a cross-platform global keyboard and mouse hook for .NET, and the ability to simulate input events. It is a thin wrapper of libuiohook and provides direct access to its features as well as higher-level types to work with it.

Installation

dotnet add package SharpHook
dotnet add package SharpHook.Reactive

Upgrading

A migration guide is available for upgrading between major versions.

Docs

You can find more information (including the API reference) in the docs at https://sharphook.tolik.io. Or if you need a specific version:

Supported Platforms

SharpHook targets .NET 6+, .NET Framework 4.6.1+, and .NET Standard 2.0. The following table describes the availability of SharpHook on various platforms:

Windows macOS Linux
x86 Yes N/A No1
x64 Yes Yes Yes
Arm32 Yes2 N/A Yes
Arm64 Yes Yes Yes

[1] - Linux on x86 is not supported by .NET itself.

[2] - Windows Arm32 support was dropped in .NET 5 so it will most probably be dropped by this library in a future version as well.

libuiohook only supports X11 on Linux. Wayland support may be coming, but it's not yet here.

Usage

Native Functions of libuiohook

SharpHook exposes the functions of libuiohook in the SharpHook.Native.UioHook class. The SharpHook.Native namespace also contains structs and enums which represent the data returned by libuiohook.

Note: In general, you don't need to use the native methods directly. Instead, use the higher-level types provided by SharpHook.

UioHook contains the following methods for working with the global hook:

  • SetDispatchProc - sets the function which will be called when an event is raised by libuiohook.

  • Run - creates a global hook and runs it on the current thread, blocking it until Stop is called.

  • Stop - destroys the global hook.

Additionally, UioHook contains the PostEvent method for simulating input events, and the SetLoggerProc method for setting the log callback.

libuiohook also provides functions to get various system properties. The corresponding methods are also present in UioHook.

Important: An application manifest is required on Windows to enable DPI awareness for your app. If it's not enabled then mouse coordinates will be wrong on high-DPI screens. You can look at the sample app in this repository to see the manifest example.

Default Global Hooks

SharpHook provides the IGlobalHook interface along with two default implementations which you can use to control the hook and subscribe to its events. Here's a basic usage example:

using SharpHook;

// ...

var hook = new TaskPoolGlobalHook();

hook.HookEnabled += OnHookEnabled;
hook.HookDisabled += OnHookDisabled;

hook.KeyTyped += OnKeyTyped;
hook.KeyPressed += OnKeyPressed;
hook.KeyReleased += OnKeyReleased;

hook.MouseClicked += OnMouseClicked;
hook.MousePressed += OnMousePressed;
hook.MouseReleased += OnMouseReleased;
hook.MouseMoved += OnMouseMoved;
hook.MouseDragged += OnMouseDragged;

hook.MouseWheel += OnMouseWheel;

hook.Run();
// or
await hook.RunAsync();

First, you create the hook, then subscribe to its events, and then run it. The Run method runs the hook on the current thread, blocking it. The RunAsync() method runs the hook on a separate thread and returns a Task which is finished when the hook is destroyed. You can subscribe to events after the hook is started.

IGlobalHook extends IDisposable. When you call the Dispose method on a hook, it's destroyed. The contract of the interface is that once a hook has been destroyed, it cannot be started again - you'll have to create a new instance. Calling Dispose when the hook is not running is safe - it just won't do anything (other than marking the instance as disposed).

Important: Always use one instance of IGlobalHook at a time in the entire application since they all must use the same static method to set the hook callback for libuiohook, so there may only be one callback at a time.

SharpHook provides two implementations of IGlobalHook:

  • SimpleGlobalHook runs all of its event handlers on the same thread where the hook itself runs. This means that the handlers should generally be fast since they will block the hook from handling the events that follow if they run for too long.

  • TaskPoolGlobalHook runs all of its event handlers on other threads inside the default thread pool for tasks. The parallelism level of the handlers can be configured. On backpressure it will queue the remaining handlers. This means that the hook will be able to process all events. This implementation should be preferred to SimpleGlobalHook except for very simple use-cases. But it has a downside - suppressing event propagation will be ignored since event handlers are run on other threads.

The library also provides the GlobalHookBase class which you can extend to create your own implementation of the global hook. It calls appropriate event handlers, and you only need to implement a strategy for dispatching the events.

Reactive Global Hooks

If you're using Rx.NET, you can use the SharpHook.Reactive package to integrate SharpHook with Rx.NET.

SharpHook.Reactive provides the IReactiveGlobalHook interface along with a default implementation and an adapter which you can use to use to control the hook and subscribe to its observables. Here's a basic example:

using SharpHook.Reactive;

// ...

var hook = new SimpleReactiveGlobalHook();

hook.HookEnabled.Subscribe(OnHookEnabled);
hook.HookDisabled.Subscribe(OnHookDisabled);

hook.KeyTyped.Subscribe(OnKeyTyped);
hook.KeyPressed.Subscribe(OnKeyPressed);
hook.KeyReleased.Subscribe(OnKeyReleased);

hook.MouseClicked.Subscribe(OnMouseClicked);
hook.MousePressed.Subscribe(OnMousePressed);
hook.MouseReleased.Subscribe(OnMouseReleased);

hook.MouseMoved
    .Throttle(TimeSpan.FromSeconds(0.5))
    .Subscribe(OnMouseMoved);

hook.MouseDragged
    .Throttle(TimeSpan.FromSeconds(0.5))
    .Subscribe(OnMouseDragged);

hook.MouseWheel.Subscribe(OnMouseWheel);

hook.Run();
// or
hook.RunAsync().Subscribe();

Reactive global hooks are basically the same as the default global hooks and the same rules apply to them.

SharpHook.Reactive provides two implementations of IReactiveGlobalHook:

  • SimpleReactiveGlobalHook. Since we are dealing with observables, it's up to you to decide when and where to handle the events through schedulers.

  • ReactiveGlobalHookAdapter adapts an IGlobalHook to IReactiveGlobalHook. All subscriptions and changes are propagated to the adapted hook.

Event Simulation

SharpHook provides the ability to simulate keyboard and mouse events in a cross-platform way as well. Here's a quick example:

using SharpHook;
using SharpHook.Native;

// ...

var simulator = new EventSimulator();

// Press Ctrl+C
simulator.SimulateKeyPress(KeyCode.VcLeftControl);
simulator.SimulateKeyPress(KeyCode.VcC);

// Release Ctrl+C
simulator.SimulateKeyRelease(KeyCode.VcC);
simulator.SimulateKeyRelease(KeyCode.VcLeftControl);

// Press the left mouse button
simulator.SimulateMousePress(MouseButton.Button1);

// Release the left mouse button
simulator.SimulateMouseRelease(MouseButton.Button1);

// Move the mouse pointer to (0, 0)
simulator.SimulateMouseMovement(0, 0);

// Move the mouse pointer 50 pixels to the right and 100 pixels down
simulator.SimulateMouseMovementRelative(50, 100);

// Scroll the mouse wheel
simulator.SimulateMouseWheel(2, -120);

SharpHook provides the IEventSimulator interface, and the default implementation, EventSimulator, which calls UioHook.PostEvent to simulate the events.

Logging

libuiohook can log messages throughout its execution. By default the messages are not logged anywhere, but you can get these logs by using the ILogSource interface and its default implementation, LogSource:

using SharpHook.Logging;

// ...

var logSource = LogSource.Register();
logSource.MessageLogged += this.OnMessageLogged;

private void OnMessageLogged(object? sender, LogEventArgs e) =>
    this.logger.Log(this.AdaptLogLevel(e.LogEntry.Level), e.LogEntry.FullText);

As with global hooks, you should use only one LogSource object at a time. ILogSource extends IDisposable - you can dispose of a log source to stop receiving libuiohook messages. You should keep a reference to an instance of LogSource when you use it since it will stop receiving messages when garbage collector deletes it, to avoid memory leaks.

An EmptyLogSource class is also available - this class doesn't listen to the libuiohook logs and can be used instead of LogSource in release builds.

SharpHook.Reactive contains the IReactiveLogSource and ReactiveLogSourceAdapter so you can use them in a more reactive way:

using SharpHook.Logging;
using SharpHook.Reactive.Logging;

// ...

var logSource = LogSource.Register();
var reactiveLogSource = new ReactiveLogSourceAdapter(logSource);
reactiveLogSource.MessageLogged.Subscribe(this.OnMessageLogged);

Building from Source

In order to build this library, you'll first need to get libuiohook binaries. You you can get a nightly build from this repository, or you can build them yourself as instructued in the libuiohook fork that SharpHook uses (not recommended as it's non-trivial, and you should most probably use the same options that the build in this repository uses anyway).

Place the binaries into the appropriate directories in the SharpHook project, as described in the following table:

OS Files Source directory Target directory
Windows uiohook.dll windows/<platform>/bin lib/win-<platform>
macOS libuiohook.dylib darwin/<platform>/lib lib/osx-<platform>
Linux libuiohook.so linux/<platform>/lib lib/linux-<platform>

With libuiohook in place you can build SharpHook using your usual methods, e.g. with Visual Studio or the dotnet CLI. You need .NET 7 to build SharpHook.

The SharpHook project defines multiple platforms. If you want to run SharpHook.Sample, make sure you don't use AnyCPU since the libuiohook version for it is not defined.

Library Status

I will maintain the library to keep up with the releases of libuiohook which uses a rolling release model - every commit to its 1.3 branch is considered stable. If you've noticed that this library hasn't gotten new commits in some time, rest assured that it's not abandoned! I'm not giving up on this library any time soon.

Icon

Icon made by Freepik from www.flaticon.com.