QInput is a small native input runtime for JVM desktop applications. It gives a JVM application one coherent API for registered global hotkeys, raw global keyboard/mouse observation, physical key-state queries and chord injection, instead of splitting those roles across separate libraries.
The public boundary is intentionally small: a Rust cdylib exposes a stable C ABI (currently ABI v1, see QIP_ABI_VERSION in include/qinput.h), and the JVM side uses a thin Java 11/JNA binding. Platform details stay behind the native runtime.
- Registered global hotkeys with stable application IDs.
- Raw global keyboard events when the platform permits them.
- Raw global mouse button events and drag motion when the platform permits them.
- A capability model instead of pretending every desktop supports the same input surface.
- Windows injected-input tagging for low-level raw events.
- XDG Desktop Portal GlobalShortcuts on Wayland.
- Safe two-phase shutdown so the native handle is not freed while the JVM poll thread is using it.
- Batch
apply()semantics for hotkeys, matching Wayland's session model. - A Java callback executor so Swing callers can dispatch directly to the EDT.
| Platform | Registered hotkeys | Raw keyboard | Raw mouse buttons | Drag motion | Notes |
|---|---|---|---|---|---|
| Windows 10/11 | Yes | Yes | Yes | Yes | Registered shortcuts via global-hotkey; raw events via Win32 low-level hooks. Injected events are tagged. |
| macOS | Yes | Yes* | Yes* | Yes* | Carbon for registered hotkeys; CoreGraphics event tap for raw input. * Raw input requires the relevant privacy permission. |
| Linux/X11 | Yes | Yes** | Yes** | Yes** | Registered shortcuts via global-hotkey; raw events via X11 RECORD. ** Requires the RECORD extension. |
| Linux/Wayland | Yes*** | No | No | No | *** Uses org.freedesktop.portal.GlobalShortcuts; compositor/portal support and user confirmation may be required. |
Wayland intentionally does not claim passive raw input. Callers should query capabilities and disable or adapt raw-input-dependent features when raw input is unavailable.
qinput/
include/qinput.h Stable C ABI
native/ Rust runtime and OS backends
java/ Java 11/JNA binding
consumer-smoke/ Standalone project proving the published Maven artifact resolves and loads
examples/ Small usage examples
docs/ Architecture, platform notes, and consumer integration guides
scripts/ Build/staging helpers
.github/workflows/ Cross-platform CI and release packaging
- Rust stable, currently Rust 1.87 or newer (the Wayland portal dependency sets the floor).
- Java 11 or newer (Gradle's toolchain support will provision a matching JDK if needed, but the JVM running Gradle itself must be one Gradle 8.14 supports, e.g. Java 17 or 21).
- No local Gradle install is required; use the checked-in wrapper (
./gradlew/gradlew.bat). - Linux/X11 native build packages: X11 and Xtst/RECORD development headers.
- Linux/Wayland runtime: a working XDG Desktop Portal backend implementing GlobalShortcuts.
From the repository root:
cargo build --release -p qinput-nativeExpected outputs:
- Windows:
target/release/qinput_native.dll - macOS:
target/release/libqinput_native.dylib - Linux:
target/release/libqinput_native.so
The source tree is a Cargo workspace, so native tests can be run with:
cargo test -p qinput-native./gradlew -p java test buildThe Java module depends on JNA 5.19.1 and targets Java 11 bytecode. AwtAccelerator.format(keyCode, modifiers) converts AWT/Swing key bindings directly to the portable accelerator syntax, including F13 through F24 on backends that support those keys.
The Java loader looks for resources under:
/native/windows-x86_64/qinput_native.dll
/native/windows-aarch64/qinput_native.dll
/native/macos-x86_64/libqinput_native.dylib
/native/macos-aarch64/libqinput_native.dylib
/native/linux-x86_64/libqinput_native.so
/native/linux-aarch64/libqinput_native.so
The supplied packaging workflow builds all six common 64-bit combinations above using current GitHub-hosted runner labels. If a native resource is absent, the loader fails explicitly rather than silently loading an unrelated system library. -Dqinput.library.path=... remains available for development and custom packaging.
Use:
python scripts/stage_native.py --platform windows --arch x86_64 --binary target/release/qinput_native.dllThis stages into java/build/generated/nativeResources, a Gradle build-output directory rather than a source directory, so gradle clean wipes it; re-run the script after cleaning. Then rebuild the Java module.
dependencies {
implementation("io.github.ahatem:qinput:0.2.0")
}Published artifacts bundle the Java/JNA classes together with all six native binaries above; consumers never clone or build QInput. consumer-smoke/ is a small standalone Gradle project that proves this end to end against whatever is in your local Maven repository:
./gradlew -p java publishToMavenLocal
./gradlew -p consumer-smoke runYou can also bypass resource extraction at runtime:
-Dqinput.library.path=/absolute/path/to/qinput_native.dll
Executor swing = command -> javax.swing.SwingUtilities.invokeLater(command);
try (QInput input = new QInput(new QInput.Listener() {
@Override public void onEvent(QInputEvent event) {
if (event.kind() == QInputEvent.Kind.HOTKEY && event.isPressed()) {
System.out.println("Hotkey " + event.id());
}
}
}, swing)) {
input.apply(List.of(
new QInput.Shortcut(1, "control+KeyQ", "Quick action"),
new QInput.Shortcut(2, "control+shift+KeyT", "Toggle window")
));
}For raw events, create with raw input disabled first, inspect capabilities(), then enable only features the backend advertises. This matters on Wayland. Do not post high-rate raw mouse motion directly to the Swing EDT; keep raw state tracking on a lightweight executor and only dispatch UI work when an action is ready.
The common input syntax follows keyboard-types / global-hotkey naming for the normal JVM-facing surface:
control+KeyQ
control+shift+KeyT
alt+F4
super+KeyK
macOS Carbon exposes function keys through F20; F21-F24 therefore fail explicitly there. Windows and X11 use global-hotkey 0.8, whose parser includes F13-F24. Unsupported keys fail explicitly instead of being silently remapped.
Wayland preferred triggers are emitted using the freedesktop Shortcuts Specification form such as CTRL+SHIFT+t.
QInput.close() uses two native calls in a fixed order:
qip_stop()stops and joins native producers but leaves the handle valid.- The Java dispatcher exits its polling loop.
qip_destroy()frees the handle.
This ordering prevents the use-after-free race that occurs when a native handle is destroyed while another JVM thread is still inside a polling call.
The C header and Java sources are intended to be locally syntax/ABI checked by scripts/verify_local.sh. The GitHub Actions matrix is the authoritative compile gate for all Rust platform backends.
A real desktop integration pass is still required before a consuming application replaces its existing production input stack with QInput. In particular, validate:
- Windows hotkeys, F13-F24, double-tap modifier detection, injected
Robotinput, and selection drag. - macOS hotkeys with and without raw-input permission.
- GNOME/KDE Wayland portal confirmation, persistence and rebind behavior.
- X11 with and without the RECORD extension.
- shutdown/restart loops and hotkey reconfiguration under load.
See docs/VALIDATION.md. docs/QTRANSLATE_MIGRATION.md is an integration guide for QTranslate specifically, one example consumer.
Maven Central publication is tag-triggered; see docs/RELEASING.md.
MPL-2.0. See LICENSE.