From 1ecd2858b5fbadbb17fa591a9224b4d0278523d5 Mon Sep 17 00:00:00 2001 From: Youssef <151364628+viix0dev@users.noreply.github.com> Date: Sun, 16 Aug 2026 11:11:49 +0300 Subject: [PATCH 01/14] feat(AttackShark): Adding error message --- src/device/controller.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/device/controller.ts b/src/device/controller.ts index 11406f0..9fad841 100644 --- a/src/device/controller.ts +++ b/src/device/controller.ts @@ -102,6 +102,7 @@ import { TeevolutionHidClient } from "@openmouse/protocol/drivers/teevolution/hi import { teevolutionProfileForCid } from "@openmouse/protocol/teevolution"; import { VgnF2HidClient } from "@openmouse/protocol/drivers/vgn/hid"; import { KeychronHidClient } from "@openmouse/protocol/drivers/keychron/hid"; +import { attackSharkNativeOnlyMessage } from "@openmouse/protocol/drivers/attackshark/hid"; import { SUPPORTED_HID_FILTERS } from "@openmouse/protocol/drivers/vendors"; import { WLMouseHidClient } from "@openmouse/protocol/drivers/wlmouse/hid"; import { parsePreviewMode, previewsEnabled, type PreviewMode } from "../preview-modes"; @@ -1528,6 +1529,10 @@ async function requestSupportedClient(): Promise { + "Synapse app, so this mouse needs a native client.", ); } + // The Attack Shark X11 family has the same problem: its settings channel is + // on browser-protected collections, so no granted entry can ever answer. + const x11Message = attackSharkNativeOnlyMessage(devices); + if (x11Message) throw new Error(x11Message); throw new Error( `Selected device is not a supported control interface (${details}). ` + "Pick a vendor control interface (not a plain boot mouse). " From e9d2868051b4b6858d287a05b63fbf3121b1ffdf Mon Sep 17 00:00:00 2001 From: Youssef <151364628+viix0dev@users.noreply.github.com> Date: Sun, 16 Aug 2026 11:12:36 +0300 Subject: [PATCH 02/14] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 59d572e..f72954c 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "preview": "vite preview" }, "dependencies": { - "@openmouse/protocol": "https://github.com/OpenMouse-Project/mouse-protocol.git", + "@openmouse/protocol": "https://github.com/viix0dev/mouse-protocol.git", "preact": "^10.29.8" }, "devDependencies": { From 618d4e1194993e35cfd949dee9cbe96b5169eefb Mon Sep 17 00:00:00 2001 From: Youssef <151364628+viix0dev@users.noreply.github.com> Date: Sun, 16 Aug 2026 11:12:46 +0300 Subject: [PATCH 03/14] Update package-lock.json --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index f876889..f844227 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "openmouse", "version": "0.1.0", "dependencies": { - "@openmouse/protocol": "https://github.com/OpenMouse-Project/mouse-protocol.git", + "@openmouse/protocol": "https://github.com/viix0dev/mouse-protocol.git", "preact": "^10.29.8" }, "devDependencies": { From e5307a8442d14119cb26f6ed88ae4feaf76df5ef Mon Sep 17 00:00:00 2001 From: Youssef <151364628+viix0dev@users.noreply.github.com> Date: Sun, 16 Aug 2026 11:49:19 +0300 Subject: [PATCH 04/14] fix(AttackShark): adding to controller.ts --- src/device/controller.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/device/controller.ts b/src/device/controller.ts index 9fad841..634df9f 100644 --- a/src/device/controller.ts +++ b/src/device/controller.ts @@ -102,7 +102,7 @@ import { TeevolutionHidClient } from "@openmouse/protocol/drivers/teevolution/hi import { teevolutionProfileForCid } from "@openmouse/protocol/teevolution"; import { VgnF2HidClient } from "@openmouse/protocol/drivers/vgn/hid"; import { KeychronHidClient } from "@openmouse/protocol/drivers/keychron/hid"; -import { attackSharkNativeOnlyMessage } from "@openmouse/protocol/drivers/attackshark/hid"; +import { AttackSharkHidClient, attackSharkNativeOnlyMessage } from "@openmouse/protocol/drivers/attackshark/hid"; import { SUPPORTED_HID_FILTERS } from "@openmouse/protocol/drivers/vendors"; import { WLMouseHidClient } from "@openmouse/protocol/drivers/wlmouse/hid"; import { parsePreviewMode, previewsEnabled, type PreviewMode } from "../preview-modes"; @@ -158,6 +158,7 @@ const NEEDS_OPEN = [TeevolutionHidClient, VgnF2HidClient, KeychronHidClient, Mod const DEDICATED = [ ...DM_CLASSES, ...RAZER_CLASSES, ...NEEDS_OPEN, EggOp1HidClient, LogitechHidppClient, OrbitalHidClient, RazerViperV4ProHidClient, FinalmouseHidClient, + AttackSharkHidClient, ] as const; const logitechClient = (): LogitechHidppClient | null => activeAs(LogitechHidppClient); From 9c38ad37e5d3e71653b95b262b096bfbd811aaec Mon Sep 17 00:00:00 2001 From: Youssef <151364628+viix0dev@users.noreply.github.com> Date: Sun, 16 Aug 2026 11:57:56 +0300 Subject: [PATCH 05/14] feat(AttackShark): X11 read-only support --- src/device/controller.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/device/controller.ts b/src/device/controller.ts index 634df9f..6ad4bc9 100644 --- a/src/device/controller.ts +++ b/src/device/controller.ts @@ -1214,9 +1214,10 @@ function applyStatusInner(deviceStatus: MouseStatus, statusKey?: string): void { const summary = deviceStatus.batteryPercent === null ? "Connected" : `Battery ${deviceStatus.batteryPercent}%`; - readStatus = status.ui?.valuesVerified + const base = status.ui?.valuesVerified ? [summary, `${deviceStatus.dpi.toLocaleString()} DPI`, `${deviceStatus.pollingRateHz.toLocaleString()} Hz`].join(" · ") : summary; + readStatus = status.ui?.statusNote ? `${base} · ${status.ui.statusNote}` : base; } else if (!hasPendingChanges()) { readStatus = `Current: ${deviceStatus.dpi.toLocaleString()} DPI · ${deviceStatus.pollingRateHz.toLocaleString()} Hz`; } From 996b2b93fd06bc70996cb468c1e568bea963eea5 Mon Sep 17 00:00:00 2001 From: Youssef <151364628+viix0dev@users.noreply.github.com> Date: Sun, 16 Aug 2026 13:19:06 +0300 Subject: [PATCH 06/14] feat(AttackShark): link to bridge --- build/check-bundle-size.ts | 7 +++- src/app/InterfaceSettings.tsx | 79 ++++++++++++++++++++++++++++++++++- src/bridge.ts | 33 +++++++++++++++ src/control.css | 11 +++++ 4 files changed, 128 insertions(+), 2 deletions(-) diff --git a/build/check-bundle-size.ts b/build/check-bundle-size.ts index 58f5494..d951235 100644 --- a/build/check-bundle-size.ts +++ b/build/check-bundle-size.ts @@ -14,7 +14,12 @@ const BUDGET_BYTES: Record = { // with the supported-device page and MX Master remap controls. Preview // fixtures retain their separate allowance below; the measured aggregate // is 573.4 kB with them, plus the ~11 kB Hall of Fame chunk. - ".js": 590_000, + // + // Raised again from 590 kB (+2 kB) for native Bridge device control: the + // /v1/devices client and the "Native devices" panel that lists Bridge- + // reached mice (e.g. the Attack Shark X11, unreachable over WebHID) and + // sets their polling rate. Measured aggregate 609.9 kB. + ".js": 612_000, }; const ASSETS = join("dist", "assets"); diff --git a/src/app/InterfaceSettings.tsx b/src/app/InterfaceSettings.tsx index cb565b0..be8b718 100644 --- a/src/app/InterfaceSettings.tsx +++ b/src/app/InterfaceSettings.tsx @@ -1,11 +1,14 @@ import { useCallback, useEffect, useState, type ReactNode } from "react"; import { + bridgeDevices, bridgeGames, bridgeHandshake, bridgeProfiles, bridgeStatus, saveBridgeProfiles, saveBridgeDefaultProfile, + setBridgeDevicePolling, + type BridgeDevice, type BridgeGame, type BridgeProfile, type BridgeStatus, @@ -109,6 +112,8 @@ export function InterfaceSettings({ snapshot }: { snapshot: ControlSnapshot }): const [bridge, setBridge] = useState(null); const [bridgeProfilesList, setBridgeProfilesList] = useState([]); const [bridgeGamesList, setBridgeGamesList] = useState([]); + const [bridgeDeviceList, setBridgeDeviceList] = useState([]); + const [bridgeDeviceBusy, setBridgeDeviceBusy] = useState(null); const [selectedGameName, setSelectedGameName] = useState(""); const [bridgeConnectionRequested, setBridgeConnectionRequested] = useState(true); const [bridgeChecking, setBridgeChecking] = useState(false); @@ -146,14 +151,21 @@ export function InterfaceSettings({ snapshot }: { snapshot: ControlSnapshot }): setBridgeChecking(true); try { await bridgeHandshake(signal); - const [status, profiles, games] = await Promise.all([ + const [status, profiles, games, devices] = await Promise.all([ bridgeStatus(signal), bridgeProfiles(signal), bridgeGames(signal), + bridgeDevices(signal).catch(() => [] as BridgeDevice[]), ]); setBridge(status); setBridgeProfilesList(profiles); setBridgeGamesList(games); + // Leave the list untouched while a polling write is settling so the + // control does not flicker back to the pre-write value mid-request. + setBridgeDeviceBusy((busy) => { + if (busy === null) setBridgeDeviceList(devices); + return busy; + }); setSelectedGameName((current) => current || status.activeGames[0] || games[0]?.name || ""); setBridgeMessage(""); } catch { @@ -161,6 +173,7 @@ export function InterfaceSettings({ snapshot }: { snapshot: ControlSnapshot }): setBridge(null); setBridgeProfilesList([]); setBridgeGamesList([]); + setBridgeDeviceList([]); } } finally { if (!signal?.aborted) setBridgeChecking(false); @@ -190,6 +203,21 @@ export function InterfaceSettings({ snapshot }: { snapshot: ControlSnapshot }): }; }, [bridgeConnectionRequested, checkBridge]); + const changeDevicePolling = useCallback(async (device: BridgeDevice, hz: number): Promise => { + setBridgeDeviceBusy(device.id); + setBridgeMessage(""); + try { + const confirmed = await setBridgeDevicePolling(device.id, hz); + setBridgeDeviceList((list) => + list.map((entry) => (entry.id === device.id ? { ...entry, pollingRateHz: confirmed } : entry))); + setBridgeMessage(`${device.name} set to ${confirmed.toLocaleString()} Hz.`); + } catch (error) { + setBridgeMessage(error instanceof Error ? error.message : "Could not change the polling rate."); + } finally { + setBridgeDeviceBusy(null); + } + }, []); + const selectedGame = bridgeGamesList.find((game) => game.name === selectedGameName) ?? null; const status = snapshot.status; const deviceId = status ? `${status.brand}:${status.name}` : ""; @@ -365,6 +393,55 @@ export function InterfaceSettings({ snapshot }: { snapshot: ControlSnapshot }): ) : null} + {bridge ? ( +
+
+
+ NATIVE DEVICES +

Mice the Bridge reaches directly, bypassing the browser

+
+
+ {bridgeDeviceList.length === 0 ? ( +

+ No Bridge-controlled mice detected. Plug in an Attack Shark X11 — it needs the Bridge because its + settings channel is not reachable from a browser. +

+ ) : ( +
    + {bridgeDeviceList.map((device) => ( +
  • +
    + {device.name} + + {device.connection === "wireless" ? "Wireless" : "Wired"} + {device.batteryPercent !== null ? ` · ${device.batteryPercent}% battery` : ""} + +
    + {device.supportedPollingRates.length > 0 ? ( + + ) : null} + {device.note} +
  • + ))} +
+ )} +
+ ) : null} {bridge ? (
diff --git a/src/bridge.ts b/src/bridge.ts index f0adb1b..95d355a 100644 --- a/src/bridge.ts +++ b/src/bridge.ts @@ -60,6 +60,39 @@ export async function bridgeGames(signal?: AbortSignal): Promise { return bridgeRequest("/v1/games", undefined, signal); } +/** + * A mouse the Bridge can reach natively — used for devices whose config + * channel the browser cannot touch (e.g. the Attack Shark X11, whose settings + * live on HID collections Chrome protects). `null` fields mean "not readable". + */ +export interface BridgeDevice { + id: string; + name: string; + vendorId: number; + productId: number; + connection: "wired" | "wireless"; + batteryPercent: number | null; + pollingRateHz: number | null; + supportedPollingRates: number[]; + note: string; +} + +export async function bridgeDevices(signal?: AbortSignal): Promise { + return bridgeRequest("/v1/devices", undefined, signal); +} + +export async function setBridgeDevicePolling(id: string, hz: number): Promise { + const result = await bridgeRequest<{ ok: boolean; pollingRateHz: number }>( + `/v1/devices/${encodeURIComponent(id)}/polling`, + { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ hz }), + }, + ); + return result.pollingRateHz; +} + export async function saveBridgeProfiles(profiles: BridgeProfile[]): Promise { await bridgeRequest("/v1/profiles", { method: "PUT", diff --git a/src/control.css b/src/control.css index 29f5720..e7d956a 100644 --- a/src/control.css +++ b/src/control.css @@ -817,6 +817,17 @@ nav { display: grid; gap: .3rem; margin-top: 1.4rem; } .openmouse-bridge-action .openmouse-bridge-connect:not(:disabled) { cursor: pointer; opacity: 1; } .openmouse-bridge-action small { color: var(--text-faint); font-size: .54rem; line-height: 1.4; } .openmouse-bridge-applications { grid-column: 1 / -1; display: grid; gap: .65rem; padding-top: 1rem; border-top: 1px solid var(--border); } +.openmouse-bridge-devices { grid-column: 1 / -1; display: grid; gap: .65rem; padding-top: 1rem; border-top: 1px solid var(--border); } +.openmouse-bridge-device-list { display: grid; gap: .55rem; margin: 0; padding: 0; list-style: none; } +.openmouse-bridge-device { display: grid; gap: .4rem; padding: .65rem .75rem; border: 1px solid var(--border); border-radius: 8px; background: var(--surface-sunken, #0d0e10); } +.openmouse-bridge-device-head { display: flex; align-items: baseline; justify-content: space-between; gap: .75rem; } +.openmouse-bridge-device-head strong { color: var(--text-strong); font-size: .74rem; } +.openmouse-bridge-device-meta { color: var(--faint); font: .55rem var(--font-mono); } +.openmouse-bridge-device-control { display: flex; align-items: center; justify-content: space-between; gap: .75rem; } +.openmouse-bridge-device-control span { color: var(--text-soft); font-size: .62rem; } +.openmouse-bridge-device-control select { padding: .3rem .5rem; border: 1px solid var(--border); border-radius: 6px; background: var(--surface, #16181b); color: var(--text-strong); font-size: .62rem; cursor: pointer; } +.openmouse-bridge-device-control select:disabled { opacity: .55; cursor: default; } +.openmouse-bridge-device-note { color: var(--faint); font-size: .52rem; line-height: 1.4; } .openmouse-arch-udev { grid-column: 1 / -1; display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: .55rem .75rem; align-items: center; padding: .7rem; border: 1px solid #8a6a38; border-radius: 8px; background: #211b12; } .openmouse-arch-udev strong, .openmouse-arch-udev small { display: block; } .openmouse-arch-udev strong { color: #edca8d; font-size: .68rem; } From 0f44c8008f45cace26d1363f5dd83ef7b2504447 Mon Sep 17 00:00:00 2001 From: Youssef <151364628+viix0dev@users.noreply.github.com> Date: Sun, 16 Aug 2026 15:52:19 +0300 Subject: [PATCH 07/14] feat(bridge): native device panel with polling control and driver install --- build/check-bundle-size.ts | 6 +++-- src/app/InterfaceSettings.tsx | 49 ++++++++++++++++++++++++++++++++--- src/bridge.ts | 37 +++++++++++++++++++++++--- src/control.css | 5 ++++ 4 files changed, 89 insertions(+), 8 deletions(-) diff --git a/build/check-bundle-size.ts b/build/check-bundle-size.ts index d951235..72189fd 100644 --- a/build/check-bundle-size.ts +++ b/build/check-bundle-size.ts @@ -7,8 +7,10 @@ const BUDGET_BYTES: Record = { // layer (SVG displacement filters plus their component rules) adds the // largest share. The measured bundle is 153.7 kB; 175 kB adds headroom for // the Developer Hall of Fame page (~15 kB of animated card and hero - // styles that load only on /contributors.html). - ".css": 175_000, + // styles that load only on /contributors.html). Raised again to 177 kB for + // the Bridge "Native devices" panel (per-device rows, polling control, and + // the Enable/Remove-driver buttons); measured 175.5 kB. + ".css": 177_000, // Raised from 510 kB for Bridge discovery, profile editing, automatic // reconnection, and recent device support, which have since grown further // with the supported-device page and MX Master remap controls. Preview diff --git a/src/app/InterfaceSettings.tsx b/src/app/InterfaceSettings.tsx index be8b718..70a8cb1 100644 --- a/src/app/InterfaceSettings.tsx +++ b/src/app/InterfaceSettings.tsx @@ -8,6 +8,7 @@ import { saveBridgeProfiles, saveBridgeDefaultProfile, setBridgeDevicePolling, + setBridgeDriver, type BridgeDevice, type BridgeGame, type BridgeProfile, @@ -218,6 +219,24 @@ export function InterfaceSettings({ snapshot }: { snapshot: ControlSnapshot }): } }, []); + const changeDriver = useCallback(async (action: "install" | "uninstall"): Promise => { + setBridgeDeviceBusy(`driver-${action}`); + setBridgeMessage(action === "install" + ? "Approve the Windows prompt to enable native control…" + : "Approve the Windows prompt to remove the driver…"); + try { + await setBridgeDriver(action); + setBridgeMessage(action === "install" + ? "Driver installed. Reconnect the mouse if it does not appear as controllable." + : "Driver removed. The mouse is back to its normal driver."); + await checkBridge(); + } catch (error) { + setBridgeMessage(error instanceof Error ? error.message : "The driver change did not complete."); + } finally { + setBridgeDeviceBusy(null); + } + }, [checkBridge]); + const selectedGame = bridgeGamesList.find((game) => game.name === selectedGameName) ?? null; const status = snapshot.status; const deviceId = status ? `${status.brand}:${status.name}` : ""; @@ -409,15 +428,16 @@ export function InterfaceSettings({ snapshot }: { snapshot: ControlSnapshot }): ) : (
    {bridgeDeviceList.map((device) => ( -
  • +
  • {device.name} {device.connection === "wireless" ? "Wireless" : "Wired"} {device.batteryPercent !== null ? ` · ${device.batteryPercent}% battery` : ""} + {device.controllable ? "" : " · needs setup"}
    - {device.supportedPollingRates.length > 0 ? ( + {device.controllable && device.supportedPollingRates.length > 0 ? (
  • + ))} +
+ +
+ ); +} + export function InterfaceSettings({ snapshot }: { snapshot: ControlSnapshot }): ReactNode { const preferences = snapshot.preferences; const [bridge, setBridge] = useState(null); @@ -219,6 +290,25 @@ export function InterfaceSettings({ snapshot }: { snapshot: ControlSnapshot }): } }, []); + const changeDeviceDpi = useCallback( + async (device: BridgeDevice, stages: number[], activeStage: number): Promise => { + setBridgeDeviceBusy(device.id); + setBridgeMessage(""); + try { + await setBridgeDeviceDpi(device.id, stages, activeStage); + setBridgeDeviceList((list) => + list.map((entry) => + entry.id === device.id ? { ...entry, dpiStages: stages, activeDpiStage: activeStage } : entry)); + setBridgeMessage(`${device.name} DPI set to ${stages[activeStage - 1]?.toLocaleString()}.`); + } catch (error) { + setBridgeMessage(error instanceof Error ? error.message : "Could not change the DPI."); + } finally { + setBridgeDeviceBusy(null); + } + }, + [], + ); + const changeDriver = useCallback(async (action: "install" | "uninstall"): Promise => { setBridgeDeviceBusy(`driver-${action}`); setBridgeMessage(action === "install" @@ -459,6 +549,13 @@ export function InterfaceSettings({ snapshot }: { snapshot: ControlSnapshot }): ) : null} + {device.controllable && device.dpiStages.length > 0 ? ( + void changeDeviceDpi(device, stages, active)} + /> + ) : null} {device.note} {bridge?.platform === "windows" ? (
diff --git a/src/bridge.ts b/src/bridge.ts index c8a7abc..9414973 100644 --- a/src/bridge.ts +++ b/src/bridge.ts @@ -76,6 +76,13 @@ export interface BridgeDevice { batteryPercent: number | null; pollingRateHz: number | null; supportedPollingRates: number[]; + /** DPI stages as last written through the Bridge (the mouse doesn't report them). */ + dpiStages: number[]; + /** Active DPI stage, 1-based. */ + activeDpiStage: number; + dpiMin: number; + dpiMax: number; + dpiStep: number; note: string; } @@ -95,6 +102,22 @@ export async function setBridgeDevicePolling(id: string, hz: number): Promise { + await bridgeRequest( + `/v1/devices/${encodeURIComponent(id)}/dpi`, + { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ stages, activeStage }), + }, + ); +} + /** * Install or remove the WinUSB driver package (Windows only) that lets the * Bridge reach a mouse whose config interface the HID stack blocks — e.g. the diff --git a/src/control.css b/src/control.css index deca8a5..0609091 100644 --- a/src/control.css +++ b/src/control.css @@ -833,6 +833,14 @@ nav { display: grid; gap: .3rem; margin-top: 1.4rem; } .openmouse-bridge-device-enable:disabled { opacity: .55; cursor: default; } .openmouse-bridge-device-secondary { padding: .34rem .6rem; border: 1px solid var(--border); border-radius: 6px; background: transparent; color: var(--faint); font-size: .56rem; cursor: pointer; } .openmouse-bridge-device-secondary:disabled { opacity: .55; cursor: default; } +.openmouse-bridge-dpi { display: grid; gap: .4rem; } +.openmouse-bridge-dpi > span { color: var(--text-soft); font-size: .62rem; } +.openmouse-bridge-dpi ul { display: grid; grid-template-columns: repeat(3, 1fr); gap: .35rem .5rem; margin: 0; padding: 0; list-style: none; } +.openmouse-bridge-dpi li { display: flex; align-items: center; gap: .3rem; } +.openmouse-bridge-dpi li input[type="number"] { width: 100%; min-width: 0; padding: .25rem .3rem; border: 1px solid var(--border); border-radius: 5px; background: var(--surface, #16181b); color: var(--text-strong); font-size: .6rem; } +.openmouse-bridge-dpi li input[type="radio"] { flex: none; } +.openmouse-bridge-dpi li input:disabled { opacity: .55; } +.openmouse-bridge-dpi > button { justify-self: start; } .openmouse-arch-udev { grid-column: 1 / -1; display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: .55rem .75rem; align-items: center; padding: .7rem; border: 1px solid #8a6a38; border-radius: 8px; background: #211b12; } .openmouse-arch-udev strong, .openmouse-arch-udev small { display: block; } .openmouse-arch-udev strong { color: #edca8d; font-size: .68rem; } From a3cee9e7f98a415bf500aeb59a56512d15b74445 Mon Sep 17 00:00:00 2001 From: Youssef <151364628+viix0dev@users.noreply.github.com> Date: Sun, 16 Aug 2026 18:00:21 +0300 Subject: [PATCH 11/14] feat(AttackShark): X11 DPI/polling/battery in the main control panel --- src/app/InterfaceSettings.tsx | 140 ++-------------------------------- src/control.css | 12 --- src/device/controller.ts | 102 ++++++++++++++++++++++++- 3 files changed, 109 insertions(+), 145 deletions(-) diff --git a/src/app/InterfaceSettings.tsx b/src/app/InterfaceSettings.tsx index 39e0104..4b961f4 100644 --- a/src/app/InterfaceSettings.tsx +++ b/src/app/InterfaceSettings.tsx @@ -7,8 +7,6 @@ import { bridgeStatus, saveBridgeProfiles, saveBridgeDefaultProfile, - setBridgeDeviceDpi, - setBridgeDevicePolling, setBridgeDriver, type BridgeDevice, type BridgeGame, @@ -109,76 +107,6 @@ function SwitchCard({ ); } -/** - * Editor for a Bridge device's six DPI stages. Holds a local draft and only - * writes on "Apply", so editing a value doesn't fire a USB command per - * keystroke. Re-seeds when the device's stored values change. - */ -function DpiEditor({ device, busy, onApply }: { - device: BridgeDevice; - busy: boolean; - onApply: (stages: number[], activeStage: number) => void; -}): ReactNode { - const [stages, setStages] = useState(device.dpiStages); - const [active, setActive] = useState(device.activeDpiStage); - const storedKey = device.dpiStages.join(","); - useEffect(() => { - setStages(device.dpiStages); - setActive(device.activeDpiStage); - }, [storedKey, device.activeDpiStage]); - - const clamp = (value: number): number => { - if (!Number.isFinite(value)) return device.dpiMin; - const stepped = Math.round(value / device.dpiStep) * device.dpiStep; - return Math.min(device.dpiMax, Math.max(device.dpiMin, stepped)); - }; - const dirty = stages.join(",") !== storedKey || active !== device.activeDpiStage; - - return ( -
- DPI stages -
    - {stages.map((dpi, index) => ( -
  • - setActive(index + 1)} - aria-label={`Make stage ${index + 1} active`} - /> - { - const value = Number(event.currentTarget.value); - setStages((current) => current.map((entry, other) => (other === index ? value : entry))); - }} - onBlur={(event) => { - const value = clamp(Number(event.currentTarget.value)); - setStages((current) => current.map((entry, other) => (other === index ? value : entry))); - }} - /> -
  • - ))} -
- -
- ); -} - export function InterfaceSettings({ snapshot }: { snapshot: ControlSnapshot }): ReactNode { const preferences = snapshot.preferences; const [bridge, setBridge] = useState(null); @@ -275,40 +203,6 @@ export function InterfaceSettings({ snapshot }: { snapshot: ControlSnapshot }): }; }, [bridgeConnectionRequested, checkBridge]); - const changeDevicePolling = useCallback(async (device: BridgeDevice, hz: number): Promise => { - setBridgeDeviceBusy(device.id); - setBridgeMessage(""); - try { - const confirmed = await setBridgeDevicePolling(device.id, hz); - setBridgeDeviceList((list) => - list.map((entry) => (entry.id === device.id ? { ...entry, pollingRateHz: confirmed } : entry))); - setBridgeMessage(`${device.name} set to ${confirmed.toLocaleString()} Hz.`); - } catch (error) { - setBridgeMessage(error instanceof Error ? error.message : "Could not change the polling rate."); - } finally { - setBridgeDeviceBusy(null); - } - }, []); - - const changeDeviceDpi = useCallback( - async (device: BridgeDevice, stages: number[], activeStage: number): Promise => { - setBridgeDeviceBusy(device.id); - setBridgeMessage(""); - try { - await setBridgeDeviceDpi(device.id, stages, activeStage); - setBridgeDeviceList((list) => - list.map((entry) => - entry.id === device.id ? { ...entry, dpiStages: stages, activeDpiStage: activeStage } : entry)); - setBridgeMessage(`${device.name} DPI set to ${stages[activeStage - 1]?.toLocaleString()}.`); - } catch (error) { - setBridgeMessage(error instanceof Error ? error.message : "Could not change the DPI."); - } finally { - setBridgeDeviceBusy(null); - } - }, - [], - ); - const changeDriver = useCallback(async (action: "install" | "uninstall"): Promise => { setBridgeDeviceBusy(`driver-${action}`); setBridgeMessage(action === "install" @@ -531,32 +425,14 @@ export function InterfaceSettings({ snapshot }: { snapshot: ControlSnapshot }): {device.controllable ? "" : " · needs setup"}
- {device.controllable && device.supportedPollingRates.length > 0 ? ( - - ) : null} - {device.controllable && device.dpiStages.length > 0 ? ( - void changeDeviceDpi(device, stages, active)} - /> - ) : null} - {device.note} + {device.controllable ? ( + + Ready. Add this mouse from the sidebar to change DPI and polling rate + under Overview and Performance, like any other mouse. + + ) : ( + {device.note} + )} {bridge?.platform === "windows" ? (
{device.controllable ? ( diff --git a/src/control.css b/src/control.css index 0609091..927228c 100644 --- a/src/control.css +++ b/src/control.css @@ -823,24 +823,12 @@ nav { display: grid; gap: .3rem; margin-top: 1.4rem; } .openmouse-bridge-device-head { display: flex; align-items: baseline; justify-content: space-between; gap: .75rem; } .openmouse-bridge-device-head strong { color: var(--text-strong); font-size: .74rem; } .openmouse-bridge-device-meta { color: var(--faint); font: .55rem var(--font-mono); } -.openmouse-bridge-device-control { display: flex; align-items: center; justify-content: space-between; gap: .75rem; } -.openmouse-bridge-device-control span { color: var(--text-soft); font-size: .62rem; } -.openmouse-bridge-device-control select { padding: .3rem .5rem; border: 1px solid var(--border); border-radius: 6px; background: var(--surface, #16181b); color: var(--text-strong); font-size: .62rem; cursor: pointer; } -.openmouse-bridge-device-control select:disabled { opacity: .55; cursor: default; } .openmouse-bridge-device-note { color: var(--faint); font-size: .52rem; line-height: 1.4; } .openmouse-bridge-device-actions { display: flex; gap: .5rem; } .openmouse-bridge-device-enable { padding: .38rem .7rem; border: 1px solid var(--ui-accent); border-radius: 6px; background: color-mix(in srgb, var(--ui-accent) 16%, transparent); color: var(--text-strong); font-size: .6rem; cursor: pointer; } .openmouse-bridge-device-enable:disabled { opacity: .55; cursor: default; } .openmouse-bridge-device-secondary { padding: .34rem .6rem; border: 1px solid var(--border); border-radius: 6px; background: transparent; color: var(--faint); font-size: .56rem; cursor: pointer; } .openmouse-bridge-device-secondary:disabled { opacity: .55; cursor: default; } -.openmouse-bridge-dpi { display: grid; gap: .4rem; } -.openmouse-bridge-dpi > span { color: var(--text-soft); font-size: .62rem; } -.openmouse-bridge-dpi ul { display: grid; grid-template-columns: repeat(3, 1fr); gap: .35rem .5rem; margin: 0; padding: 0; list-style: none; } -.openmouse-bridge-dpi li { display: flex; align-items: center; gap: .3rem; } -.openmouse-bridge-dpi li input[type="number"] { width: 100%; min-width: 0; padding: .25rem .3rem; border: 1px solid var(--border); border-radius: 5px; background: var(--surface, #16181b); color: var(--text-strong); font-size: .6rem; } -.openmouse-bridge-dpi li input[type="radio"] { flex: none; } -.openmouse-bridge-dpi li input:disabled { opacity: .55; } -.openmouse-bridge-dpi > button { justify-self: start; } .openmouse-arch-udev { grid-column: 1 / -1; display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: .55rem .75rem; align-items: center; padding: .7rem; border: 1px solid #8a6a38; border-radius: 8px; background: #211b12; } .openmouse-arch-udev strong, .openmouse-arch-udev small { display: block; } .openmouse-arch-udev strong { color: #edca8d; font-size: .68rem; } diff --git a/src/device/controller.ts b/src/device/controller.ts index f799814..98d2f6d 100644 --- a/src/device/controller.ts +++ b/src/device/controller.ts @@ -8,7 +8,7 @@ import { type PulsarClient, type SupportedClient, } from "../device-clients"; -import { bridgeDevices } from "../bridge"; +import { bridgeDevices, setBridgeDeviceDpi, setBridgeDevicePolling, type BridgeDevice } from "../bridge"; import { closestDpiOption } from "../dpi-presets"; import { formatHex, hidTraffic, isMark, markHidActivity, startHidCapture, type HidTrafficEntry } from "../hid-diagnostics"; import { @@ -453,6 +453,92 @@ function activeSettingsClient(): SupportedClient | null { return active; } +// ── Attack Shark X11: settings via the Bridge ────────────────────────────── +// The X11 connects over WebHID as a read-only shell; its DPI/polling live on a +// USB interface the browser cannot reach, so the OpenMouse Bridge drives them. +// When the active device is a Bridge-controlled X11 we fold the Bridge's state +// into its status (so the normal DPI/polling cards render) and route those +// setters to the Bridge. Every branch is additive: it only fires for this +// device, so no other mouse's code path changes. + +const ATTACK_SHARK_X11_VID = 0x1d57; +const ATTACK_SHARK_X11_PIDS = new Set([0xfa55, 0xfa60, 0xfa61]); + +/** The Bridge device currently backing the active X11, or null. */ +let activeBridgeX11: BridgeDevice | null = null; + +function bridgeDeviceId(device: HIDDevice): string { + const hex = (value: number): string => value.toString(16).padStart(4, "0"); + return `${hex(device.vendorId)}:${hex(device.productId)}`; +} + +/** + * If the active client is a Bridge-controlled X11, merge the Bridge's DPI, + * polling and battery into `status` and populate `dpiOptions`, so the standard + * cards render. Sets `activeBridgeX11` for setter routing. No-op (and leaves the + * device read-only) when it isn't an X11 or the Bridge can't reach it. + */ +async function applyBridgeX11(client: SupportedClient, status: MouseStatus): Promise { + activeBridgeX11 = null; + if (!(client instanceof AttackSharkHidClient)) return; + const { vendorId, productId } = client.device; + if (vendorId !== ATTACK_SHARK_X11_VID || !ATTACK_SHARK_X11_PIDS.has(productId)) return; + + const id = bridgeDeviceId(client.device); + let device: BridgeDevice | undefined; + try { + device = (await bridgeDevices()).find((entry) => entry.id === id && entry.controllable); + } catch { + return; // Bridge not running — keep the read-only shell. + } + if (!device || device.dpiStages.length === 0) return; + activeBridgeX11 = device; + + const activeStage = Math.max(0, Math.min(device.activeDpiStage - 1, device.dpiStages.length - 1)); + status.dpiStages = device.dpiStages.slice(); + status.activeDpiStage = activeStage; + status.dpi = device.dpiStages[activeStage] ?? status.dpi; + status.pollingRateHz = device.pollingRateHz ?? status.pollingRateHz; + status.supportedPollingRates = device.supportedPollingRates; + status.batteryPercent = device.batteryPercent ?? status.batteryPercent; + status.batteryState = device.batteryPercent != null ? "Discharging" : status.batteryState; + // An empty LOD list hides the sensor card (see cardAvailability). + status.supportedLiftOffDistances = []; + status.ui = { + ...status.ui, + settingsReady: true, + valuesVerified: true, + hideProcessingCard: true, + forceShowBattery: device.connection === "wireless", + statusNote: undefined, + dpiStageEditor: { + maxStages: device.dpiStages.length, + countEditable: false, + minDpi: device.dpiMin, + maxDpi: device.dpiMax, + stepDpi: device.dpiStep, + }, + }; + + // The stage editor validates against dpiOptions, so list every supported step. + const options: number[] = []; + for (let value = device.dpiMin; value <= device.dpiMax; value += device.dpiStep) options.push(value); + dpiOptions = options; +} + +/** Bridge id to route settings to when the active device is a controlled X11. */ +function bridgeX11Target(): string | null { + return activeBridgeX11?.id ?? null; +} + +/** Push the current (staged) DPI stages and active stage to the Bridge. */ +async function writeBridgeX11Dpi(id: string): Promise { + const staged = latestDeviceStatus ? withPendingChanges(latestDeviceStatus) : null; + const stages = staged?.dpiStages ?? activeBridgeX11?.dpiStages ?? []; + const activeStage = (staged?.activeDpiStage ?? 0) + 1; + await setBridgeDeviceDpi(id, stages, activeStage); +} + function hasActiveClient(): boolean { return activeSettingsClient() !== null; } @@ -1347,6 +1433,7 @@ async function activateClientNow(client: SupportedClient): Promise { } else { const status = await client.readStatus(); dpiOptions = await client.getDpiOptions(); + await applyBridgeX11(client, status); const dm = dmClient(); const keychron = keychronClient(); if (dm) lastSleepSeconds = status.sleepTimeout ?? dm.getSleepOptions()[0] ?? 60; @@ -1405,6 +1492,7 @@ function showDisconnectedState(): void { } clearActiveClients(); activeDevice = null; + activeBridgeX11 = null; onboardProfiles = null; lastRenderedStatusKey = null; capabilities = null; @@ -1731,6 +1819,8 @@ export function applyDpiValue(dpi: number): boolean { } }, apply: async () => { + const bridgeId = bridgeX11Target(); + if (bridgeId) return writeBridgeX11Dpi(bridgeId); await requireClientMethod("setDpi", "DPI").setDpi(dpi); }, }); @@ -1781,6 +1871,8 @@ export function applyActiveDpiStage(stage: number): void { status.dpi = status.dpiStages?.[stage] ?? status.dpi; }, apply: async () => { + const bridgeId = bridgeX11Target(); + if (bridgeId) return writeBridgeX11Dpi(bridgeId); await requireClientMethod("setActiveDpiStage", "DPI stage").setActiveDpiStage(stage); }, }); @@ -1812,6 +1904,8 @@ export function applyDpiStageValue(stage: number, rawDpi: number): void { if ((status.activeDpiStage ?? 0) === stage) status.dpi = dpi; }, apply: async () => { + const bridgeId = bridgeX11Target(); + if (bridgeId) return writeBridgeX11Dpi(bridgeId); await requireClientMethod("setDpiStageValue", "DPI stage value").setDpiStageValue(stage, dpi); }, }); @@ -2476,6 +2570,11 @@ export function applyPollingRate(rate: number): void { status.pollingRateHz = rate; }, apply: async () => { + const bridgeId = bridgeX11Target(); + if (bridgeId) { + await setBridgeDevicePolling(bridgeId, rate); + return; + } await requireClientMethod("setPollingRate", "the polling rate").setPollingRate(rate); }, }); @@ -3005,6 +3104,7 @@ async function refreshStatus(): Promise { try { const dm = dmClient(); const status = dm && client === dm ? await dm.readStatus(true) : await client.readStatus(); + await applyBridgeX11(client, status); const currentClient = activeSettingsClient(); if (client !== currentClient || client.device !== activeDevice) return; const key = JSON.stringify(status); From 9eba275643f4588cbd0044735987355569d334cc Mon Sep 17 00:00:00 2001 From: Youssef <151364628+viix0dev@users.noreply.github.com> Date: Sun, 16 Aug 2026 18:13:56 +0300 Subject: [PATCH 12/14] feat(AttackShark): X11 as a standalone Bridge device in the sidebar --- src/app/InterfaceSettings.tsx | 4 +- src/app/Sidebar.tsx | 3 +- src/device/controller.ts | 141 +++++++++++++++++++++++++++++++++- src/device/types.ts | 2 + 4 files changed, 144 insertions(+), 6 deletions(-) diff --git a/src/app/InterfaceSettings.tsx b/src/app/InterfaceSettings.tsx index 4b961f4..4879467 100644 --- a/src/app/InterfaceSettings.tsx +++ b/src/app/InterfaceSettings.tsx @@ -427,8 +427,8 @@ export function InterfaceSettings({ snapshot }: { snapshot: ControlSnapshot }):
{device.controllable ? ( - Ready. Add this mouse from the sidebar to change DPI and polling rate - under Overview and Performance, like any other mouse. + Ready. It appears in the sidebar — select it to change DPI and polling + rate under Overview and Performance, like any other mouse. ) : ( {device.note} diff --git a/src/app/Sidebar.tsx b/src/app/Sidebar.tsx index 3eff12a..62c4043 100644 --- a/src/app/Sidebar.tsx +++ b/src/app/Sidebar.tsx @@ -150,7 +150,8 @@ export function Sidebar({ snapshot, onOpenSupportRequests }: { snapshot: Control aria-current={device.selected} onClick={() => { control.closeInterfaceSettings(); - void control.selectAuthorizedDevice(device.index); + if (device.bridgeId) void control.selectBridgeDevice(device.bridgeId); + else void control.selectAuthorizedDevice(device.index); }} > diff --git a/src/device/controller.ts b/src/device/controller.ts index 98d2f6d..cfe3082 100644 --- a/src/device/controller.ts +++ b/src/device/controller.ts @@ -537,10 +537,127 @@ async function writeBridgeX11Dpi(id: string): Promise { const stages = staged?.dpiStages ?? activeBridgeX11?.dpiStages ?? []; const activeStage = (staged?.activeDpiStage ?? 0) + 1; await setBridgeDeviceDpi(id, stages, activeStage); + if (activeBridgeX11?.id === id) { + activeBridgeX11 = { ...activeBridgeX11, dpiStages: [...stages], activeDpiStage: activeStage }; + } +} + +// ── Bridge devices as first-class sidebar entries ────────────────────────── +// A Bridge-controlled X11 has no WebHID entry (its interface is bound to +// WinUSB), so it appears in the sidebar straight from the Bridge and activates +// without a browser step. `active`/`activeDevice` stay null; `activeBridgeX11` +// marks the mode and the setters route to the Bridge. + +let bridgeDeviceCache: BridgeDevice[] = []; +let bridgeCacheKey = ""; +let bridgePollTimer: number | null = null; + +/** Build a full MouseStatus from a Bridge device so the normal cards render. */ +function bridgeStatusFor(device: BridgeDevice): MouseStatus { + const activeStage = Math.max(0, Math.min(device.activeDpiStage - 1, device.dpiStages.length - 1)); + return { + brand: "Attack Shark", + name: device.name, + ui: { + family: "attackshark", + settingsReady: true, + valuesVerified: true, + hideProcessingCard: true, + hideUnsupportedPollingRates: true, + forceShowBattery: device.connection === "wireless", + dpiStageEditor: { + maxStages: device.dpiStages.length, + countEditable: false, + minDpi: device.dpiMin, + maxDpi: device.dpiMax, + stepDpi: device.dpiStep, + }, + }, + batteryPercent: device.batteryPercent, + batteryState: device.batteryPercent != null ? "Discharging" : "Unknown", + dpi: device.dpiStages[activeStage] ?? 0, + dpiStages: device.dpiStages.slice(), + activeDpiStage: activeStage, + pollingRateHz: device.pollingRateHz ?? 0, + supportedPollingRates: device.supportedPollingRates, + supportedLiftOffDistances: [], + activeProfile: null, + connectionType: device.connection === "wireless" ? "Wireless" : "Wired", + liftOffDistance: null, + firmware: [], + }; +} + +/** Activate a Bridge device (no WebHID). Renders it in the main control panel. */ +export async function selectBridgeDevice(id: string): Promise { + if (settingInProgress || refreshInProgress) return; + let device: BridgeDevice | undefined; + try { + device = (await bridgeDevices()).find((entry) => entry.id === id && entry.controllable); + } catch { + toastForError("Connection failed", new Error("The OpenMouse Bridge is not reachable.")); + return; + } + if (!device || activeBridgeX11?.id === id) return; + + await active?.close().catch(() => undefined); + active = null; + activeDevice = null; + activeBridgeX11 = device; + clearPendingChanges(); + onboardProfiles = null; + + const options: number[] = []; + for (let value = device.dpiMin; value <= device.dpiMax; value += device.dpiStep) options.push(value); + dpiOptions = options; + + capabilities = readCapabilities(); + deviceStatusText = "Connected"; + applyStatus(bridgeStatusFor(device)); + await refreshSidebar(); + startAutomaticRefresh(); + setConnectionButtons(false, "Add device"); + pushToast("success", `Connected to ${device.name}`); +} + +/** Poll the Bridge so its devices stay listed in the sidebar and live-updated. */ +function startBridgePolling(): void { + if (bridgePollTimer !== null) return; + const poll = async (): Promise => { + let list: BridgeDevice[] = []; + try { + list = (await bridgeDevices()).filter((entry) => entry.controllable); + } catch { + list = []; + } + const key = JSON.stringify( + list.map((d) => [d.id, d.name, d.connection, d.batteryPercent, d.pollingRateHz, d.dpiStages, d.activeDpiStage]), + ); + if (key === bridgeCacheKey) return; + bridgeCacheKey = key; + bridgeDeviceCache = list; + // Keep the active Bridge device's status live (battery, etc.), or drop it + // if it was unplugged. + if (activeBridgeX11) { + const current = list.find((entry) => entry.id === activeBridgeX11!.id); + if (current) { + activeBridgeX11 = current; + const status = bridgeStatusFor(current); + const statusKey = JSON.stringify(status); + if (statusKey !== lastRenderedStatusKey && !hasPendingChanges()) applyStatus(status, statusKey); + } else { + showDisconnectedState(); + emit(); + } + } + void refreshSidebar(); + }; + void poll(); + bridgePollTimer = window.setInterval(() => void poll(), 5_000); } function hasActiveClient(): boolean { - return activeSettingsClient() !== null; + return activeSettingsClient() !== null || activeBridgeX11 !== null; } function requireSettingsClient(): SupportedClient { @@ -1357,7 +1474,17 @@ function sidebarEntries(devices: HIDDevice[]): SidebarDevice[] { async function refreshSidebar(devices?: HIDDevice[]): Promise { const all = devices ?? await navigator.hid?.getDevices() ?? []; - sidebarDevices = sidebarEntries(all); + const webhid = sidebarEntries(all); + // Bridge-only devices (e.g. the WinUSB-bound X11) have no WebHID entry, so add + // them from the cached Bridge list. Negative indices keep them distinct. + const bridge: SidebarDevice[] = bridgeDeviceCache.map((device, index) => ({ + index: -1 - index, + bridgeId: device.id, + name: device.name, + detail: `Attack Shark · ${device.connection === "wireless" ? "Wireless" : "Wired"}`, + selected: activeBridgeX11?.id === device.id, + })); + sidebarDevices = [...webhid, ...bridge]; emit(); } @@ -1421,6 +1548,7 @@ async function activateClientNow(client: SupportedClient): Promise { if (activeDevice !== client.device) onboardProfiles = null; buttons = null; activeDevice = client.device; + activeBridgeX11 = null; // Switching to a WebHID device leaves Bridge mode. recordDiagnosticCommand("Read device status"); lastRenderedStatusKey = null; active = client; @@ -2572,7 +2700,10 @@ export function applyPollingRate(rate: number): void { apply: async () => { const bridgeId = bridgeX11Target(); if (bridgeId) { - await setBridgeDevicePolling(bridgeId, rate); + const confirmed = await setBridgeDevicePolling(bridgeId, rate); + if (activeBridgeX11?.id === bridgeId) { + activeBridgeX11 = { ...activeBridgeX11, pollingRateHz: confirmed }; + } return; } await requireClientMethod("setPollingRate", "the polling rate").setPollingRate(rate); @@ -3096,6 +3227,8 @@ function startAutomaticRefresh(): void { } async function refreshStatus(): Promise { + // Bridge devices refresh from the Bridge poll, not a WebHID client. + if (activeBridgeX11) return; const client = activeSettingsClient(); if (!client || refreshInProgress || settingInProgress || activationInProgress) return; if ("pollIntervalMs" in client && Number((client as { pollIntervalMs: number }).pollIntervalMs) <= 0) return; @@ -3318,6 +3451,8 @@ export function start(): void { navigator.hid?.addEventListener("connect", handleHidConnect); navigator.hid?.addEventListener("disconnect", handleHidDisconnect); void reconnectAuthorizedDevice(); + // List Bridge-only devices (e.g. the WinUSB-bound X11) in the sidebar. + startBridgePolling(); } window.addEventListener("beforeunload", (event) => { diff --git a/src/device/types.ts b/src/device/types.ts index e73ce6b..61c57fd 100644 --- a/src/device/types.ts +++ b/src/device/types.ts @@ -51,6 +51,8 @@ export interface SidebarDevice { name: string; detail: string; selected: boolean; + /** Set for devices reached through the Bridge (no WebHID entry), e.g. the X11. */ + bridgeId?: string; } export interface DiagnosticsView { From 9910eb473365c0dc801f74adde1e6e91b23e7235 Mon Sep 17 00:00:00 2001 From: Youssef <151364628+viix0dev@users.noreply.github.com> Date: Mon, 17 Aug 2026 11:07:02 +0300 Subject: [PATCH 13/14] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f72954c..59d572e 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "preview": "vite preview" }, "dependencies": { - "@openmouse/protocol": "https://github.com/viix0dev/mouse-protocol.git", + "@openmouse/protocol": "https://github.com/OpenMouse-Project/mouse-protocol.git", "preact": "^10.29.8" }, "devDependencies": { From 87dc81b7d29618cf05d6e696b603640980bdb26a Mon Sep 17 00:00:00 2001 From: Youssef <151364628+viix0dev@users.noreply.github.com> Date: Mon, 17 Aug 2026 11:07:19 +0300 Subject: [PATCH 14/14] Update package-lock.json --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 66da98d..b353c6f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "openmouse", "version": "0.1.0", "dependencies": { - "@openmouse/protocol": "https://github.com/viix0dev/mouse-protocol.git", + "@openmouse/protocol": "github:OpenMouse-Project/mouse-protocol", "preact": "^10.29.8" }, "devDependencies": {