Skip to content
ahatemPublic

About

Standalone cross-platform native input injection/hotkey library (Rust + Java ABI)

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

QInput

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.

What it provides

  • 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 matrix

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.

Repository layout

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

Requirements

  • 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.

Build the native library

From the repository root:

cargo build --release -p qinput-native

Expected 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

Build the Java binding

./gradlew -p java test build

The 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.

Stage a native binary into the Java JAR

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.dll

This 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.

Use as a Maven dependency

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 run

You can also bypass resource extraction at runtime:

-Dqinput.library.path=/absolute/path/to/qinput_native.dll

Java example

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.

Accelerator syntax

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.

Lifecycle

QInput.close() uses two native calls in a fixed order:

  1. qip_stop() stops and joins native producers but leaves the handle valid.
  2. The Java dispatcher exits its polling loop.
  3. 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.

Validation status

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 Robot input, 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.

Releasing

Maven Central publication is tag-triggered; see docs/RELEASING.md.

License

MPL-2.0. See LICENSE.

About

Standalone cross-platform native input injection/hotkey library (Rust + Java ABI)

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages