From fdcc8c7487212fdae7a931ed0f1c6ca66408ffb7 Mon Sep 17 00:00:00 2001 From: Youssef <151364628+viix0dev@users.noreply.github.com> Date: Sun, 16 Aug 2026 11:10:46 +0300 Subject: [PATCH 1/4] fix(AttackShark): x11 fix the error --- src/drivers/attackshark/hid.test.ts | 48 ++++++++++++++++++++++++++++- src/drivers/attackshark/hid.ts | 39 +++++++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/src/drivers/attackshark/hid.test.ts b/src/drivers/attackshark/hid.test.ts index ebeed68..7789aa5 100644 --- a/src/drivers/attackshark/hid.test.ts +++ b/src/drivers/attackshark/hid.test.ts @@ -1,7 +1,7 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { AttackSharkHidClient } from "./hid.ts"; +import { AttackSharkHidClient, attackSharkNativeOnlyMessage } from "./hid.ts"; function device(vendorId: number, usagePage = 0xffff): HIDDevice { return { @@ -26,6 +26,52 @@ test("Attack Shark OEM devices require the expected feature-report collection", assert.equal(AttackSharkHidClient.isSupported(device(0x25a7, 0x0001)), false); }); +// The four HID entries a real X11 wireless receiver (0x1d57:0xfa60) presents +// to Chrome: two keyboards, a boot mouse, and a system-control/consumer +// composite — none with visible feature reports (Chrome hides reports on +// protected keyboard/system collections; nothing else declares any). +function x11Entry(productId: number, collections: Array<[number, number]>): HIDDevice { + return { + vendorId: 0x1d57, + productId, + productName: "2.4G Wireless Device", + collections: collections.map(([usagePage, usage]) => ({ + usagePage, + usage, + type: 0, + children: [], + inputReports: [], + outputReports: [], + featureReports: [], + })), + } as unknown as HIDDevice; +} + +test("real X11 entries expose no feature reports and are refused", () => { + const entries = [ + x11Entry(0xfa60, [[0x01, 0x06]]), + x11Entry(0xfa60, [[0x01, 0x06]]), + x11Entry(0xfa60, [[0x01, 0x02]]), + x11Entry(0xfa60, [[0x01, 0x80], [0x0c, 0x01], [0x0a, 0x00], [0x0b, 0x00]]), + ]; + for (const entry of entries) { + assert.equal(AttackSharkHidClient.isSupported(entry), false); + } +}); + +test("X11-family grants get a native-only explanation, other refusals do not", () => { + const wireless = attackSharkNativeOnlyMessage([x11Entry(0xfa60, [[0x01, 0x06]])]); + assert.match(wireless ?? "", /X11 \(wireless receiver\)/); + assert.match(wireless ?? "", /native desktop driver/); + + const wired = attackSharkNativeOnlyMessage([x11Entry(0xfa55, [[0x01, 0x02]])]); + assert.match(wired ?? "", /X11 \(wired\)/); + + // Unknown 0x1d57 PIDs and other vendors keep the generic error. + assert.equal(attackSharkNativeOnlyMessage([x11Entry(0x1234, [[0x01, 0x02]])]), null); + assert.equal(attackSharkNativeOnlyMessage([device(0x25a7)]), null); +}); + test("Attack Shark battery reports validate their signature and percentage", () => { assert.equal(AttackSharkHidClient.parseBatteryReport(new Uint8Array([0x03, 0x55, 0x40, 0x01, 73])), 73); assert.equal(AttackSharkHidClient.parseBatteryReport(new Uint8Array([0x03, 0x55, 0x40, 0x00, 73])), null); diff --git a/src/drivers/attackshark/hid.ts b/src/drivers/attackshark/hid.ts index 981d05f..61785ca 100644 --- a/src/drivers/attackshark/hid.ts +++ b/src/drivers/attackshark/hid.ts @@ -11,6 +11,15 @@ import { LAMZU_PRODUCTS } from "@openmouse/protocol/lamzu"; // PIDs change between firmware revisions, so detection is collection-based, // not PID-based. For 0x373e we exclude known Lamzu PIDs. // +// Real X11 hardware (0x1d57, wired 0xfa55 and wireless 0xfa60) exposes NO +// feature reports to the browser on any of its four HID entries: its config +// channel lives on keyboard/system-control collections, which Chromium +// always protects (reports hidden from the page, transfers refused), and +// Windows additionally rejects SET_REPORT for report ids a collection does +// not declare. This is why the reference X11 drivers are native USB apps. +// The collection gate below therefore correctly refuses these units; the +// X11-family helper further down turns that refusal into a real explanation. +// // Protocol source: xb-bx/attack-shark-r1-driver (Odin) // HarukaYamamoto0/attack-shark-x11-driver (TypeScript) // Research credit: viix0dev @@ -55,6 +64,36 @@ function hasVendorControl(collection: HIDCollectionInfo): boolean { return collection.children.some(hasVendorControl); } +// ── X11 family (native-only in practice) ───────────────────────────────── + +// Documented 0x1d57 PIDs: wired X11, wireless X11 receiver, R1. +const X11_FAMILY_PIDS: ReadonlySet = new Set([0xfa55, 0xfa60, 0xfa61]); + +const X11_FAMILY_NAMES: ReadonlyMap = new Map([ + [0xfa55, "Attack Shark X11 (wired)"], + [0xfa60, "Attack Shark X11 (wireless receiver)"], + [0xfa61, "Attack Shark R1"], +]); + +/** + * If the granted devices include an X11-family unit that no driver could + * claim, explain why instead of letting the generic "not a control + * interface" error blame the picker choice: the config channel sits on + * browser-protected collections, so no selectable entry can ever answer. + * Returns null when no X11-family device is present. + */ +export function attackSharkNativeOnlyMessage(devices: HIDDevice[]): string | null { + const unit = devices.find( + (device) => device.vendorId === VID_1D57 && X11_FAMILY_PIDS.has(device.productId), + ); + if (!unit) return null; + const name = X11_FAMILY_NAMES.get(unit.productId) ?? "Attack Shark X11"; + return `The ${name} cannot be configured in the browser: its settings channel ` + + "sits on keyboard/system HID collections that Chrome always protects, so no " + + "entry in the picker can carry its commands. Configuring this mouse needs a " + + "native desktop driver (e.g. the open-source Attack Shark X11 driver)."; +} + // ── Protocol family detection ───────────────────────────────────────────── type ProtocolFamily = "1d57" | "25a7" | "373e" | null; From 6ca70faa2b9ea060dcd8e91cc13c90ca8d1449b4 Mon Sep 17 00:00:00 2001 From: Youssef <151364628+viix0dev@users.noreply.github.com> Date: Sun, 16 Aug 2026 11:43:17 +0300 Subject: [PATCH 2/4] test(AttackShark): trying to read without writing --- src/drivers/attackshark/hid.test.ts | 42 ++++++++++- src/drivers/attackshark/hid.ts | 109 +++++++++++++++++++++++----- 2 files changed, 129 insertions(+), 22 deletions(-) diff --git a/src/drivers/attackshark/hid.test.ts b/src/drivers/attackshark/hid.test.ts index 7789aa5..d6052dc 100644 --- a/src/drivers/attackshark/hid.test.ts +++ b/src/drivers/attackshark/hid.test.ts @@ -47,16 +47,50 @@ function x11Entry(productId: number, collections: Array<[number, number]>): HIDD } as unknown as HIDDevice; } -test("real X11 entries expose no feature reports and are refused", () => { - const entries = [ +test("X11 boot entries are refused; the composite status entry is claimed", () => { + const refused = [ x11Entry(0xfa60, [[0x01, 0x06]]), x11Entry(0xfa60, [[0x01, 0x06]]), x11Entry(0xfa60, [[0x01, 0x02]]), - x11Entry(0xfa60, [[0x01, 0x80], [0x0c, 0x01], [0x0a, 0x00], [0x0b, 0x00]]), ]; - for (const entry of entries) { + for (const entry of refused) { assert.equal(AttackSharkHidClient.isSupported(entry), false); } + const composite = x11Entry(0xfa60, [[0x01, 0x80], [0x0c, 0x01], [0x0a, 0x00], [0x0b, 0x00]]); + assert.equal(AttackSharkHidClient.isSupported(composite), true); + // An unknown 0x1d57 PID with the same shape stays refused: the read-only + // claim is scoped to units whose battery stream is documented. + const unknownPid = x11Entry(0x1234, [[0x01, 0x80], [0x0c, 0x01]]); + assert.equal(AttackSharkHidClient.isSupported(unknownPid), false); +}); + +test("X11 read-only client reports battery from input reports and refuses writes", async () => { + const listeners = new Map void>(); + const composite = { + ...x11Entry(0xfa60, [[0x01, 0x80], [0x0c, 0x01], [0x0a, 0x00], [0x0b, 0x00]]), + opened: false, + open() { (this as { opened: boolean }).opened = true; return Promise.resolve(); }, + close() { (this as { opened: boolean }).opened = false; return Promise.resolve(); }, + addEventListener(type: string, handler: (event: unknown) => void) { listeners.set(type, handler); }, + removeEventListener(type: string) { listeners.delete(type); }, + } as unknown as HIDDevice; + + const client = new AttackSharkHidClient(composite); + const before = await client.readStatus(); + assert.equal(before.name, "Attack Shark X11"); + assert.equal(before.ui?.settingsReady, false); + assert.equal(before.ui?.forceShowBattery, true); + assert.equal(before.batteryPercent, null); + assert.equal(before.connectionType, "Wireless"); + + // Raw packet 03 55 40 01 50: WebHID moves the leading 0x03 into reportId. + const payload = new Uint8Array([0x55, 0x40, 0x01, 0x50]); + listeners.get("inputreport")?.({ reportId: 0x03, data: new DataView(payload.buffer) }); + const after = await client.readStatus(); + assert.equal(after.batteryPercent, 80); + assert.equal(after.batteryState, "Discharging"); + + await assert.rejects(() => client.setPollingRate(1000), /native Attack Shark X11 driver/); }); test("X11-family grants get a native-only explanation, other refusals do not", () => { diff --git a/src/drivers/attackshark/hid.ts b/src/drivers/attackshark/hid.ts index 61785ca..1466eff 100644 --- a/src/drivers/attackshark/hid.ts +++ b/src/drivers/attackshark/hid.ts @@ -12,13 +12,16 @@ import { LAMZU_PRODUCTS } from "@openmouse/protocol/lamzu"; // not PID-based. For 0x373e we exclude known Lamzu PIDs. // // Real X11 hardware (0x1d57, wired 0xfa55 and wireless 0xfa60) exposes NO -// feature reports to the browser on any of its four HID entries: its config -// channel lives on keyboard/system-control collections, which Chromium -// always protects (reports hidden from the page, transfers refused), and -// Windows additionally rejects SET_REPORT for report ids a collection does -// not declare. This is why the reference X11 drivers are native USB apps. -// The collection gate below therefore correctly refuses these units; the -// X11-family helper further down turns that refusal into a real explanation. +// feature reports to the browser on any of its four HID entries. Its config +// channel is USB interface 2 (a system-control/consumer composite; see the +// lsusb dump in dressedinblack5/attack-shark-x11-electron docs/descritors), +// and the reference driver bypasses the HID stack entirely: it claims +// interface 2 with libusb, detaches the kernel HID driver on Linux, and on +// Windows requires Zadig to swap the driver for WinUSB. A browser can do +// none of that — WebHID sees no feature reports, and WebUSB refuses to +// claim HID-class interfaces. The collection gate below therefore correctly +// refuses these units; the X11-family helper further down turns that +// refusal into a real explanation. // // Protocol source: xb-bx/attack-shark-r1-driver (Odin) // HarukaYamamoto0/attack-shark-x11-driver (TypeScript) @@ -64,7 +67,7 @@ function hasVendorControl(collection: HIDCollectionInfo): boolean { return collection.children.some(hasVendorControl); } -// ── X11 family (native-only in practice) ───────────────────────────────── +// ── X11 family (config is native-only; battery is readable) ────────────── // Documented 0x1d57 PIDs: wired X11, wireless X11 receiver, R1. const X11_FAMILY_PIDS: ReadonlySet = new Set([0xfa55, 0xfa60, 0xfa61]); @@ -75,11 +78,24 @@ const X11_FAMILY_NAMES: ReadonlyMap = new Map([ [0xfa61, "Attack Shark R1"], ]); +const X11_FAMILY_MODELS: ReadonlyMap = new Map([ + [0xfa55, "Attack Shark X11"], + [0xfa60, "Attack Shark X11"], + [0xfa61, "Attack Shark R1"], +]); + +// The wireless receiver's interface 2 pushes battery packets on its own — +// no command needed — so a read-only claim of that entry costs nothing and +// risks nothing. The wired PIDs never report battery on this endpoint. +const X11_WIRELESS_PID = 0xfa60; + /** * If the granted devices include an X11-family unit that no driver could * claim, explain why instead of letting the generic "not a control - * interface" error blame the picker choice: the config channel sits on - * browser-protected collections, so no selectable entry can ever answer. + * interface" error blame the picker choice. This fires only when the + * status entry (the composite with a Consumer collection) was not among + * the grants — the rows look identical in the picker, so say how to get + * the right one — and it stays honest about settings being native-only. * Returns null when no X11-family device is present. */ export function attackSharkNativeOnlyMessage(devices: HIDDevice[]): string | null { @@ -88,19 +104,31 @@ export function attackSharkNativeOnlyMessage(devices: HIDDevice[]): string | nul ); if (!unit) return null; const name = X11_FAMILY_NAMES.get(unit.productId) ?? "Attack Shark X11"; - return `The ${name} cannot be configured in the browser: its settings channel ` - + "sits on keyboard/system HID collections that Chrome always protects, so no " - + "entry in the picker can carry its commands. Configuring this mouse needs a " - + "native desktop driver (e.g. the open-source Attack Shark X11 driver)."; + return `This ${name} entry cannot answer: it is one of the mouse's plain ` + + "keyboard/mouse interfaces. Re-open the picker and ctrl-click every row with " + + "this name so OpenMouse can attach to the status entry (battery readout on " + + "wireless). Changing settings is not possible from a browser for this mouse: " + + "its config channel needs a native desktop driver (e.g. the open-source " + + "Attack Shark X11 driver)."; } // ── Protocol family detection ───────────────────────────────────────────── -type ProtocolFamily = "1d57" | "25a7" | "373e" | null; +type ProtocolFamily = "1d57" | "1d57-x11" | "25a7" | "373e" | null; function detectFamily(device: HIDDevice): ProtocolFamily { if (device.vendorId === VID_1D57) { - return device.collections.some(hasFeatureReports) ? "1d57" : null; + if (device.collections.some(hasFeatureReports)) return "1d57"; + // Real X11/R1 units declare no feature reports anywhere (see header + // note), so config writes are impossible here — but their interface-2 + // entry, the composite with a Consumer top-level collection, carries the + // autonomous battery stream. Claim that one read-only; the plain boot + // keyboard/mouse entries stay refused. + if (X11_FAMILY_PIDS.has(device.productId) + && device.collections.some((collection) => collection.usagePage === 0x0c)) { + return "1d57-x11"; + } + return null; } if (device.vendorId === VID_25A7) { return device.collections.some(hasVendorControl) ? "25a7" : null; @@ -120,6 +148,8 @@ export class AttackSharkHidClient { private readonly family: ProtocolFamily; private lastStatus: MouseStatus | null = null; private queue: Promise = Promise.resolve(); + private batteryPercent: number | null = null; + private listening = false; constructor(device: HIDDevice) { this.device = device; @@ -130,12 +160,37 @@ export class AttackSharkHidClient { return detectFamily(device) !== null; } + // Battery packets arrive as inputreport events; the raw packet's leading + // 0x03 is the HID report id, which WebHID strips into event.reportId, so + // rebuild the native shape before matching the signature. + private readonly onInputReport = (event: HIDInputReportEvent): void => { + const data = new Uint8Array(event.data.buffer, event.data.byteOffset, event.data.byteLength); + const packet = new Uint8Array(data.length + 1); + packet[0] = event.reportId; + packet.set(data, 1); + const percent = AttackSharkHidClient.parseBatteryReport(packet); + if (percent !== null) { + this.batteryPercent = percent; + if (this.lastStatus) { + this.lastStatus = { ...this.lastStatus, batteryPercent: percent, batteryState: "Discharging" }; + } + } + }; + async open(): Promise { if (!this.device.opened) await this.device.open(); + if (this.family === "1d57-x11" && !this.listening) { + this.device.addEventListener("inputreport", this.onInputReport); + this.listening = true; + } } async close(): Promise { this.lastStatus = null; + if (this.listening) { + this.device.removeEventListener("inputreport", this.onInputReport); + this.listening = false; + } if (this.device.opened) await this.device.close(); } @@ -144,6 +199,12 @@ export class AttackSharkHidClient { } displayName(): string { + // X11-family product strings are generic OEM labels ("2.4G Wireless + // Device", "USB Gaming Mouse"), so name those models by PID instead. + const model = this.device.vendorId === VID_1D57 + ? X11_FAMILY_MODELS.get(this.device.productId) + : undefined; + if (model) return model; const name = this.device.productName?.trim(); if (!name) return "Attack Shark"; return /^attack\s*shark/i.test(name) ? name : `Attack Shark ${name}`; @@ -154,6 +215,9 @@ export class AttackSharkHidClient { } isWireless(): boolean { + if (this.device.vendorId === VID_1D57 && X11_FAMILY_PIDS.has(this.device.productId)) { + return this.device.productId === X11_WIRELESS_PID; + } return /receiver|dongle|wireless|2\.4g/i.test(this.device.productName || ""); } @@ -182,9 +246,12 @@ export class AttackSharkHidClient { settingsReady: this.family === "1d57", hideUnsupportedPollingRates: true, hideProcessingCard: true, + // Wireless X11-family units push battery on their own; keep the + // column visible while the first packet is still on its way. + forceShowBattery: this.family === "1d57-x11" && this.isWireless(), }, - batteryPercent: null, - batteryState: "Unknown", + batteryPercent: this.batteryPercent, + batteryState: this.batteryPercent !== null ? "Discharging" : "Unknown", dpi: 0, pollingRateHz, supportedPollingRates: this.getSupportedPollingRates(), @@ -196,6 +263,12 @@ export class AttackSharkHidClient { } async setPollingRate(pollingRateHz: number): Promise { + if (this.family === "1d57-x11") { + throw new Error( + "This mouse's settings channel is not reachable from a browser; " + + "changing settings needs the native Attack Shark X11 driver.", + ); + } if (this.family !== "1d57") { throw new Error("Polling rate control is not yet implemented for this Attack Shark model."); } From dbe8248cae56aa015fea373133e7506610508c02 Mon Sep 17 00:00:00 2001 From: Youssef <151364628+viix0dev@users.noreply.github.com> Date: Sun, 16 Aug 2026 13:18:54 +0300 Subject: [PATCH 3/4] feat(AttackShark): link to bridge --- src/drivers/attackshark/hid.test.ts | 24 +++++++++++++++++++++++- src/drivers/attackshark/hid.ts | 21 ++++++++++++++++++--- src/drivers/mouse-types.ts | 5 +++++ 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/drivers/attackshark/hid.test.ts b/src/drivers/attackshark/hid.test.ts index d6052dc..208f8f7 100644 --- a/src/drivers/attackshark/hid.test.ts +++ b/src/drivers/attackshark/hid.test.ts @@ -66,8 +66,11 @@ test("X11 boot entries are refused; the composite status entry is claimed", () = test("X11 read-only client reports battery from input reports and refuses writes", async () => { const listeners = new Map void>(); + const base = x11Entry(0xfa60, [[0x01, 0x80], [0x0c, 0x01]]); + // A unit whose battery report (id 0x03) is visible on the consumer collection. + (base.collections[1] as { inputReports: unknown[] }).inputReports = [{ reportId: 0x03, items: [] }]; const composite = { - ...x11Entry(0xfa60, [[0x01, 0x80], [0x0c, 0x01], [0x0a, 0x00], [0x0b, 0x00]]), + ...base, opened: false, open() { (this as { opened: boolean }).opened = true; return Promise.resolve(); }, close() { (this as { opened: boolean }).opened = false; return Promise.resolve(); }, @@ -80,6 +83,7 @@ test("X11 read-only client reports battery from input reports and refuses writes assert.equal(before.name, "Attack Shark X11"); assert.equal(before.ui?.settingsReady, false); assert.equal(before.ui?.forceShowBattery, true); + assert.match(before.ui?.statusNote ?? "", /needs a native driver/); assert.equal(before.batteryPercent, null); assert.equal(before.connectionType, "Wireless"); @@ -93,6 +97,24 @@ test("X11 read-only client reports battery from input reports and refuses writes await assert.rejects(() => client.setPollingRate(1000), /native Attack Shark X11 driver/); }); +test("X11 units whose battery report is hidden do not advertise a battery column", async () => { + // Real receivers declare the battery report under the protected + // system-control collection, which Chrome hides — collections then show + // no input report 0x03 at all (openmouse-1d57-fa60 diagnostics). + const composite = { + ...x11Entry(0xfa60, [[0x01, 0x80], [0x0c, 0x01]]), + opened: true, + open: () => Promise.resolve(), + addEventListener: () => undefined, + removeEventListener: () => undefined, + } as unknown as HIDDevice; + (composite.collections[1] as { inputReports: unknown[] }).inputReports = [{ reportId: 0x02, items: [] }]; + + const client = new AttackSharkHidClient(composite); + const status = await client.readStatus(); + assert.equal(status.ui?.forceShowBattery, false); +}); + test("X11-family grants get a native-only explanation, other refusals do not", () => { const wireless = attackSharkNativeOnlyMessage([x11Entry(0xfa60, [[0x01, 0x06]])]); assert.match(wireless ?? "", /X11 \(wireless receiver\)/); diff --git a/src/drivers/attackshark/hid.ts b/src/drivers/attackshark/hid.ts index 1466eff..1434885 100644 --- a/src/drivers/attackshark/hid.ts +++ b/src/drivers/attackshark/hid.ts @@ -53,7 +53,9 @@ const POLLING_RATES_1D57: ReadonlyArray = [ const DPI_READ_REPORT_ID = 0xa0; // Battery arrives as input report with this 4-byte signature; byte 4 = %. +// The leading 0x03 is the HID report id of the battery packet. const BATTERY_SIGNATURE = [0x03, 0x55, 0x40, 0x01]; +const BATTERY_REPORT_ID = BATTERY_SIGNATURE[0]; // ── Collection helpers ───────────────────────────────────────────────────── @@ -67,6 +69,11 @@ function hasVendorControl(collection: HIDCollectionInfo): boolean { return collection.children.some(hasVendorControl); } +function declaresInputReport(collection: HIDCollectionInfo, reportId: number): boolean { + if (collection.inputReports.some((report) => report.reportId === reportId)) return true; + return collection.children.some((child) => declaresInputReport(child, reportId)); +} + // ── X11 family (config is native-only; battery is readable) ────────────── // Documented 0x1d57 PIDs: wired X11, wireless X11 receiver, R1. @@ -246,9 +253,17 @@ export class AttackSharkHidClient { settingsReady: this.family === "1d57", hideUnsupportedPollingRates: true, hideProcessingCard: true, - // Wireless X11-family units push battery on their own; keep the - // column visible while the first packet is still on its way. - forceShowBattery: this.family === "1d57-x11" && this.isWireless(), + // Wireless X11-family units push battery on their own — but only + // show the column when the battery report is actually visible to + // the browser. On known units it is declared under the protected + // system-control collection, so Chrome hides it and the packet can + // never arrive; an always-empty battery column would just confuse. + forceShowBattery: this.family === "1d57-x11" + && this.isWireless() + && this.device.collections.some((collection) => declaresInputReport(collection, BATTERY_REPORT_ID)), + statusNote: this.family === "1d57-x11" + ? "Status only: this mouse's settings channel is not reachable from a browser and needs a native driver." + : undefined, }, batteryPercent: this.batteryPercent, batteryState: this.batteryPercent !== null ? "Discharging" : "Unknown", diff --git a/src/drivers/mouse-types.ts b/src/drivers/mouse-types.ts index dd1ffb8..f2029b8 100644 --- a/src/drivers/mouse-types.ts +++ b/src/drivers/mouse-types.ts @@ -42,6 +42,11 @@ export interface MouseUiHints { showAdvancedSection?: boolean; /** Always show battery column (even wired with null %). */ forceShowBattery?: boolean; + /** + * Extra sentence appended to the connected status line, for drivers whose + * connection is deliberately limited (e.g. why settings are unavailable). + */ + statusNote?: string; /** Override the polling-rate footnote. */ pollingNote?: string; /** Sidebar name before first status read. */ From 50164646d48041ec258b65be12737d65c0ad759a Mon Sep 17 00:00:00 2001 From: Youssef <151364628+viix0dev@users.noreply.github.com> Date: Sun, 16 Aug 2026 16:25:26 +0300 Subject: [PATCH 4/4] fix(AttackShark): point X11 browser message to the Bridge native-control flow --- src/drivers/attackshark/hid.test.ts | 3 ++- src/drivers/attackshark/hid.ts | 10 ++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/drivers/attackshark/hid.test.ts b/src/drivers/attackshark/hid.test.ts index 208f8f7..45465fc 100644 --- a/src/drivers/attackshark/hid.test.ts +++ b/src/drivers/attackshark/hid.test.ts @@ -118,7 +118,8 @@ test("X11 units whose battery report is hidden do not advertise a battery column test("X11-family grants get a native-only explanation, other refusals do not", () => { const wireless = attackSharkNativeOnlyMessage([x11Entry(0xfa60, [[0x01, 0x06]])]); assert.match(wireless ?? "", /X11 \(wireless receiver\)/); - assert.match(wireless ?? "", /native desktop driver/); + assert.match(wireless ?? "", /Enable native control/); + assert.match(wireless ?? "", /OpenMouse Bridge/); const wired = attackSharkNativeOnlyMessage([x11Entry(0xfa55, [[0x01, 0x02]])]); assert.match(wired ?? "", /X11 \(wired\)/); diff --git a/src/drivers/attackshark/hid.ts b/src/drivers/attackshark/hid.ts index 1434885..9f793b5 100644 --- a/src/drivers/attackshark/hid.ts +++ b/src/drivers/attackshark/hid.ts @@ -111,12 +111,10 @@ export function attackSharkNativeOnlyMessage(devices: HIDDevice[]): string | nul ); if (!unit) return null; const name = X11_FAMILY_NAMES.get(unit.productId) ?? "Attack Shark X11"; - return `This ${name} entry cannot answer: it is one of the mouse's plain ` - + "keyboard/mouse interfaces. Re-open the picker and ctrl-click every row with " - + "this name so OpenMouse can attach to the status entry (battery readout on " - + "wireless). Changing settings is not possible from a browser for this mouse: " - + "its config channel needs a native desktop driver (e.g. the open-source " - + "Attack Shark X11 driver)."; + return `This ${name} cannot be configured through the browser: its settings channel ` + + "is on an interface the browser is not allowed to reach. To change DPI, polling " + + "rate and lighting, install the OpenMouse Bridge, then open Interface settings " + + "→ Bridge → Native devices and click “Enable native control”."; } // ── Protocol family detection ─────────────────────────────────────────────