Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ winresource = "0.1"
anyhow = "1.0"
axum = { version = "0.8", features = ["ws"] }
directories = "6.0"
hidapi = "2.6.6"
# macos-shared-device: macOS otherwise seizes every opened device, which freezes
# the cursor when the opened interface is a mouse's pointer collection.
hidapi = { version = "2.6.6", features = ["macos-shared-device"] }
notify-rust = "4.11"
png = "0.18"
semver = "1.0"
Expand Down
2 changes: 1 addition & 1 deletion native-hid/src/attack-shark-x11.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export function buildX11PollingReport(pollingRateHz) {

export function applyX11PollingRate(
pollingRateHz,
{ infos = devices(), open = (path) => new HID(path) } = {},
{ infos = devices(), open = (path) => new HID(path, { nonExclusive: true }) } = {},
) {
const candidates = x11DeviceInfos(infos);
if (candidates.length === 0) return false;
Expand Down
18 changes: 18 additions & 0 deletions native-hid/src/automation.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -209,3 +209,21 @@ test("manual adapter keeps its existing multi-path behavior without a live ident
await device.close();
assert.deepEqual(calls, [["open", endpoint.path], ["open", "manual-second"], "close", "close"]);
});

test("native opens are non-exclusive so macOS does not seize the device and freeze the cursor", async () => {
const options = [];
class FakeHID {
constructor(path, opts) { options.push(opts); }
getDeviceInfo() { return { ...endpoint }; }
on() {}
removeAllListeners() {}
close() {}
}
const manual = new HidDeviceAdapter([endpoint, { ...endpoint, path: "manual-second" }], null, FakeHID);
await manual.open();
await manual.close();
const verified = new HidDeviceAdapter([endpoint], endpoint, FakeHID);
await verified.open();
await verified.close();
assert.deepEqual(options, [{ nonExclusive: true }, { nonExclusive: true }, { nonExclusive: true }]);
});
10 changes: 8 additions & 2 deletions native-hid/src/hid-device-adapter.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ import { HID, devices } from "node-hid";

const REPLAY_ID_MAX = 255;

// macOS seizes a device by default when it is opened, which detaches it from
// the system: a mouse's own pointer collection stops moving the cursor for as
// long as Bridge holds it. Shared opens leave the OS driver in place. The flag
// is ignored on Windows and Linux.
const OPEN_OPTIONS = { nonExclusive: true };

/**
* @param {import("node-hid").Device[]} infos - every node-hid entry that
* splits off the same underlying USB/BT interface (see module docs above).
Expand Down Expand Up @@ -59,7 +65,7 @@ export class HidDeviceAdapter {
let device;
try {
if (this._infos.length !== 1) throw new Error("native live identity requires one endpoint");
device = new this._Handle(this._infos[0].path);
device = new this._Handle(this._infos[0].path, OPEN_OPTIONS);
const live = device.getDeviceInfo();
const fields = ["vendorId", "productId", "path", "serialNumber", "interface"];
if (!live || fields.some((field) => live[field] === undefined || live[field] !== this._identity[field])) throw new Error("native live handle identity is missing or changed");
Expand All @@ -80,7 +86,7 @@ export class HidDeviceAdapter {
let lastError = null;
for (const info of this._infos) {
try {
const device = new this._Handle(info.path);
const device = new this._Handle(info.path, OPEN_OPTIONS);
device.on("data", this._onData);
device.on("error", this._onError);
openedDevices.push(device);
Expand Down
9 changes: 9 additions & 0 deletions src/hid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1545,6 +1545,15 @@ async fn send_json(socket: &mut impl HidSocket, value: &impl Serialize) -> Resul

#[cfg(test)]
mod tests {
/// macOS seizes devices opened in exclusive mode, which stops a mouse's
/// pointer collection from moving the cursor while Bridge holds it.
#[cfg(target_os = "macos")]
#[test]
fn macos_opens_devices_without_seizing_them() {
let api = HidApi::new().expect("HID subsystem should initialize");
assert!(!api.get_open_exclusive());
}

use super::*;

/// Logitech receivers also split their report paths across several HID
Expand Down
Loading