Skip to content

Commit 8752090

Browse files
committed
initial release (v0.1.0)
Cross-platform, observe-only global keyboard taps with left/right modifier fidelity and clean shutdown. macOS, Windows, and Linux (evdev). Backends: - macOS: CGEventTap on a dedicated CFRunLoop thread, IOHIDCheckAccess permission gate, per-instance user_info so multiple Taps coexist, FlagsChanged synthesized into KeyDown/KeyUp via CGEventFlags diff. Never calls TSM / UCKeyTranslate — avoids the Sonoma main-thread crash class entirely. - Windows: SetWindowsHookExW(WH_KEYBOARD_LL) on a message-pump thread, thread_local routing (no globals), PostThreadMessageW(WM_QUIT) drives clean Drop, repeat detection synthesized from a held-set, defensive left/right disambiguation via scanCode / LLKHF_EXTENDED. - Linux: evdev via /dev/input/event*; works on X11 and Wayland. Device scan filtered by KEY_A probe, non-blocking fd polling, /dev/input rescan for USB/Bluetooth hotplug, typed PermissionDenied / NoDevices. Optional chord-matcher (default 'chord' feature) built on top: set-based chord definitions, longest-match resolution with first-registered tie-break, edge-triggered on KeyRepeat, non-overlapping Start/End transitions. 10 unit tests covering the state machine. Scancode tables ported from Narsil/rdev (MIT) for all three platforms. Linux device-scan pattern adapted from martintrojer/hotkey-listener (MIT). Verified: cargo check + clippy clean on aarch64-apple-darwin, aarch64-unknown-linux-gnu, x86_64-pc-windows-gnu. cargo test --lib: 13/13. macOS pipeline live-tested end-to-end.
0 parents  commit 8752090

22 files changed

Lines changed: 3175 additions & 0 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
name: ci
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
workflow_dispatch:
8+
9+
env:
10+
CARGO_TERM_COLOR: always
11+
RUSTFLAGS: -Dwarnings
12+
13+
jobs:
14+
test:
15+
name: test (${{ matrix.os }})
16+
runs-on: ${{ matrix.os }}
17+
strategy:
18+
fail-fast: false
19+
matrix:
20+
os: [ubuntu-latest, macos-latest, windows-latest]
21+
steps:
22+
- uses: actions/checkout@v4
23+
24+
- uses: dtolnay/rust-toolchain@stable
25+
with:
26+
components: clippy, rustfmt
27+
28+
- uses: Swatinem/rust-cache@v2
29+
30+
- name: cargo fmt
31+
run: cargo fmt --all -- --check
32+
33+
- name: cargo clippy
34+
run: cargo clippy --all-targets --all-features -- -D warnings
35+
36+
- name: cargo build
37+
run: cargo build --all-targets --all-features
38+
39+
- name: cargo test
40+
run: cargo test --lib --all-features
41+
42+
cross-check:
43+
name: cross-check
44+
runs-on: ubuntu-latest
45+
strategy:
46+
fail-fast: false
47+
matrix:
48+
target:
49+
- aarch64-apple-darwin
50+
- aarch64-unknown-linux-gnu
51+
- x86_64-pc-windows-gnu
52+
steps:
53+
- uses: actions/checkout@v4
54+
55+
- uses: dtolnay/rust-toolchain@stable
56+
with:
57+
targets: ${{ matrix.target }}
58+
59+
- uses: Swatinem/rust-cache@v2
60+
with:
61+
key: ${{ matrix.target }}
62+
63+
- name: cargo check
64+
run: cargo check --target ${{ matrix.target }} --all-features
65+
66+
msrv:
67+
name: msrv (1.85)
68+
runs-on: ubuntu-latest
69+
steps:
70+
- uses: actions/checkout@v4
71+
72+
- uses: dtolnay/rust-toolchain@1.85.0
73+
74+
- uses: Swatinem/rust-cache@v2
75+
76+
- name: cargo build
77+
run: cargo build --all-features

‎.gitignore‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/target
2+
/Cargo.lock

‎Cargo.toml‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
[package]
2+
name = "keytap"
3+
version = "0.1.0"
4+
edition = "2024"
5+
rust-version = "1.85"
6+
license = "MIT OR Apache-2.0"
7+
description = "Cross-platform, observe-only global keyboard taps with left/right modifier fidelity and clean shutdown"
8+
repository = "https://github.com/jamiepine/keytap"
9+
keywords = ["keyboard", "hotkey", "global", "input", "evdev"]
10+
categories = ["os", "hardware-support", "api-bindings"]
11+
readme = "README.md"
12+
13+
[features]
14+
default = ["chord"]
15+
chord = []
16+
tracing = ["dep:tracing"]
17+
serde = ["dep:serde"]
18+
19+
[dependencies]
20+
crossbeam-channel = "0.5"
21+
thiserror = "2"
22+
tracing = { version = "0.1", optional = true }
23+
serde = { version = "1", optional = true, features = ["derive"] }
24+
25+
[target.'cfg(target_os = "macos")'.dependencies]
26+
objc2 = "0.6"
27+
objc2-foundation = { version = "0.3", default-features = false, features = ["std", "NSAutoreleasePool"] }
28+
objc2-core-foundation = { version = "0.3", default-features = false, features = ["std", "CFRunLoop", "CFMachPort"] }
29+
objc2-core-graphics = { version = "0.3", default-features = false, features = ["std", "CGEvent", "CGEventTypes"] }
30+
libc = "0.2"
31+
32+
[target.'cfg(target_os = "windows")'.dependencies.windows-sys]
33+
version = "0.59"
34+
features = [
35+
"Win32_UI_WindowsAndMessaging",
36+
"Win32_UI_Input_KeyboardAndMouse",
37+
"Win32_Foundation",
38+
"Win32_System_Threading",
39+
"Win32_System_LibraryLoader",
40+
]
41+
42+
[target.'cfg(target_os = "linux")'.dependencies]
43+
evdev = "0.13"
44+
nix = { version = "0.29", features = ["poll", "event"] }
45+
46+
[dev-dependencies]
47+
anyhow = "1"

0 commit comments

Comments
 (0)