@gjsify/gamepad 0.52.0 → 0.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @gjsify/gamepad
2
2
 
3
- The W3C Gamepad API for GJS, backed by libmanette 0.2. Provides `navigator.getGamepads()` polling, `Gamepad`, `GamepadButton`, `gamepadconnected`/`gamepaddisconnected` events, and dual-rumble haptics via `GamepadHapticActuator`. The Manette monitor is lazily initialised on the first `getGamepads()` call.
3
+ The W3C Gamepad API for GJS. Provides `navigator.getGamepads()` polling, `Gamepad`, `GamepadButton`, `gamepadconnected`/`gamepaddisconnected` events, and rumble via `GamepadHapticActuator`. The backend is SDL3 on macOS and Windows, through [`@gjsify/gamepad-native`](../gamepad-native/README.md), and libmanette 0.2 on Linux until SDL has been compared against it there; it starts lazily on the first `getGamepads()` call.
4
4
 
5
5
  Part of the [gjsify](https://github.com/gjsify/gjsify) project — Node.js and Web APIs for GJS (GNOME JavaScript).
6
6
 
@@ -39,11 +39,49 @@ for (const pad of gamepads) {
39
39
  }
40
40
  ```
41
41
 
42
- ## Platform support — hosts without libmanette
42
+ `vibrationActuator` follows the spec on both backends: parameters default to 0, an
43
+ out-of-range one rejects with `TypeError`, an effect the pad lacks with
44
+ `NotSupportedError`. `playEffect()` resolves `'complete'` once the effect has played
45
+ (`startDelay + duration`, at most 5 s), or `'preempted'` when a later `playEffect()` or
46
+ `reset()` interrupts it.
43
47
 
44
- The backend is libmanette, and libmanette is Linux-only: it links `libevdev` unconditionally (`dependency('libevdev')` in its `meson.build`, with no `required:` argument and no `host_machine` branch anywhere in the tree), and libevdev is packaged for Linux and FreeBSD only — homebrew-core's formula carries `depends_on :linux`, MacPorts has no port, nixpkgs declares `platforms = linux ++ freebsd`. **So on macOS and Windows there is no gamepad backend at all, and there cannot be one until a native backend is written.** That is a platform gap, not a bug in this package.
48
+ ## Platform support
45
49
 
46
- On such a host `navigator.getGamepads()` returns the **empty list** *because there is no backend* — indistinguishable, from the return value alone, from a Linux host with nothing plugged in. That is deliberate, and it is what the spec asks for: `Navigator.[[gamepads]]` "is initially the empty list" and grows only when an index is selected for a connected device, so `getGamepads()`'s steps only ever return a list (their one `throw` is a `SecurityError` for the `"gamepad"` permission policy), and a browser on a machine with no gamepad driver returns exactly the same empty answer — WebKit compiles an `EmptyGamepadProvider` for precisely that case. Making the call throw would break every page that polls `navigator.getGamepads().length`.
50
+ | host | backend | status |
51
+ |---|---|---|
52
+ | Linux | libmanette 0.2 (`gi://Manette`) by default; the SDL3 shim with `GJSIFY_GAMEPAD_BACKEND=sdl`, or both with `=compare` | supported (libmanette); the shim reads a virtual uinput pad in CI |
53
+ | macOS | SDL3 behind a GObject shim (`gi://GjsifyGamepad`, `@gjsify/gamepad-native`) | partial: builds, loads and enumerates; input from a real controller not yet verified |
54
+ | Windows | the same shim, from Node through `@gjsify/node-gi` | partial: builds, loads, and reads a virtual XInput pad in CI; a real controller not yet verified |
55
+
56
+ The decision is [ADR 0075](../../../docs/adr/0075-darwin-gamepad-backend-is-sdl3-behind-a-gobject-shim.md) and its Amendment 1: SDL3, statically linked and trimmed to the input subsystems, becomes the one backend on every OS, and replaces libmanette on Linux once it has been compared against it there with real controllers. libmanette itself cannot move: it links `libevdev` unconditionally, and libevdev is packaged for Linux and FreeBSD only.
57
+
58
+ On macOS and Windows the typelib and library come from the per-target optional dependency `@gjsify/gamepad-native-<os>-<arch>`, and a process finds them through `gjsify run`, which puts every installed prebuild on `GI_TYPELIB_PATH` (and, on Windows, on `PATH`). Neither probes for libmanette.
59
+
60
+ ### Comparing the two Linux backends
61
+
62
+ Amendment 1 removes libmanette only after the SDL3 shim has been compared against it on real controllers. `GJSIFY_GAMEPAD_BACKEND` selects the backend per process on Linux (macOS and Windows ignore it):
63
+
64
+ | value | the page sees | what else happens |
65
+ |---|---|---|
66
+ | unset / `manette` | libmanette | — |
67
+ | `sdl` | the SDL3 shim | — |
68
+ | `compare` | libmanette | the shim runs beside it on the same controllers; connects, disconnects and every control on which the two disagree for more than one poll go to stderr, prefixed `[@gjsify/gamepad compare]` |
69
+
70
+ Controllers are paired by USB vendor:product (read from each backend's GUID), then by connection order. A difference of up to 0.02 is scaling, not disagreement: libmanette normalises the evdev range, SDL rescales to int16 first. One difference is expected and is not a control: the two name the same pad differently (`Gamepad.id`), because SDL takes the name from its mapping database and libmanette from the kernel.
71
+
72
+ Found so far (ADR 0075, Amendment 1's measurements): on an 8BitDo N30 Pro 2 SDL is the correct one of the two. libmanette 0.2 reports no Home button and no right-stick Y for it, and fires the left trigger when the right stick is pushed down: its evdev backend matches a mapping's `aN` against the kernel axis CODE rather than the N-th axis the device has, which only coincides on pads whose axis codes have no gaps.
73
+
74
+ ### Node
75
+
76
+ `gjsify.runtimes.node` is `partial`, and the same source runs under Node: a `--app node` bundle reaches `gi://` through [`@gjsify/node-gi`](../../node-gi/node-gi), so with the macOS prebuild installed the shim works there too. `@gjsify/node-gi` is NOT a dependency — a plain-Node program without it is a supported configuration and answers `hasGamepadBackend() === false` with a `console.warn` naming it.
77
+
78
+ ```bash
79
+ npm install @gjsify/node-gi @gjsify/gamepad # plus @gjsify/gamepad-native-darwin-<arch>
80
+ ```
81
+
82
+ Measured on macOS 27 arm64 / Node 24.21.0: the probe classifies `sdl`, `hasGamepadBackend()` is `true`, and `SdlSource` initialises, enumerates zero controllers and tears down over repeated cycles (`gjsify workspace @gjsify/gamepad run test:gjs-on-node`, 4 tests / 28 assertions). Input from a real controller is unverified on this target, exactly as on GJS.
83
+
84
+ A host with no backend — Linux without libmanette, macOS or Windows without the prebuild — is a platform gap, not a bug in this package, and it is reported, not hidden. There, `navigator.getGamepads()` returns the **empty list** *because there is no backend* — indistinguishable, from the return value alone, from a Linux host with nothing plugged in. That is deliberate, and it is what the spec asks for: `Navigator.[[gamepads]]` "is initially the empty list" and grows only when an index is selected for a connected device, so `getGamepads()`'s steps only ever return a list (their one `throw` is a `SecurityError` for the `"gamepad"` permission policy), and a browser on a machine with no gamepad driver returns exactly the same empty answer — WebKit compiles an `EmptyGamepadProvider` for precisely that case. Making the call throw would break every page that polls `navigator.getGamepads().length`.
47
85
 
48
86
  Ask the capability export instead of guessing from an empty list:
49
87
 
@@ -58,10 +96,30 @@ if (!(await hasGamepadBackend())) {
58
96
 
59
97
  `hasGamepadBackend()` needs no monitor and no connected device, and it is **quiet**: asking the question prints nothing, so the recommended usage above costs no stderr line on a macOS or Windows start. The one-time explanation comes from the *use* instead — the first `getGamepads()` that actually wanted a monitor:
60
98
 
61
- * **no backend** (no Manette typelib; or `@gjsify/node-gi` not installed on the node target) → one `console.warn` naming what to install.
99
+ * **no backend** (no Manette typelib; no `@gjsify/gamepad-native` prebuild on macOS or Windows; or `@gjsify/node-gi` not installed on the node target) → one `console.warn` naming what to install.
62
100
  * **a fault** (a shared library that will not `dlopen`, a version or ABI skew) → one `console.error` carrying the original error. A broken setup is a fault, not a platform gap, and the two must not look alike.
63
101
  * **the monitor fails to start** after the backend loaded (no udev or `/dev/input` in a sandbox; flatpak: `--device=input`) → its own `console.error`, because that is a third, distinct failure.
64
102
 
103
+ ## Driving the manager from your own device source
104
+
105
+ `GamepadManager` reads devices through a `GamepadSource`: an object with `start(sink)`, `stop()` and an optional `poll()`, which reports `connected` / `disconnected` / `button` / `axis` in the W3C standard layout. Hand one to the constructor to test gamepad handling with no controller attached, or to feed input you already own:
106
+
107
+ ```typescript
108
+ import { GamepadManager, type GamepadSource, type GamepadSourceSink } from '@gjsify/gamepad';
109
+
110
+ const pad = { id: 'Scripted Pad', vibrationActuator: null };
111
+ let sink: GamepadSourceSink | null = null;
112
+ const source: GamepadSource = {
113
+ name: 'scripted',
114
+ start: (s) => { sink = s; s.connected(pad); },
115
+ stop: () => { sink = null; },
116
+ };
117
+
118
+ const manager = new GamepadManager({ source });
119
+ manager.getGamepads(); // starts the source: [Gamepad { id: 'Scripted Pad', index: 0, … }]
120
+ sink!.button(pad, 0, 1, true); // standard button 0 (A / Cross) pressed
121
+ ```
122
+
65
123
  On the browser and NativeScript targets the runtime's own Gamepad API is the implementation (`gjsify.runtimes` declares both `native`): the package root routes to `globals.mjs`, whose `hasGamepadBackend()` reports that native surface, and `@gjsify/gamepad/register` leaves an existing `navigator.getGamepads` untouched rather than replacing it.
66
124
 
67
125
  ## License
@@ -1 +1 @@
1
- const e={LEFT_X:0,LEFT_Y:1,RIGHT_X:2,RIGHT_Y:3,LEFT_TRIGGER:4,RIGHT_TRIGGER:5},t={LEFT_STICK_X:0,LEFT_STICK_Y:1,RIGHT_STICK_X:2,RIGHT_STICK_Y:3},n=4,r=new Map([[e.LEFT_X,t.LEFT_STICK_X],[e.LEFT_Y,t.LEFT_STICK_Y],[e.RIGHT_X,t.RIGHT_STICK_X],[e.RIGHT_Y,t.RIGHT_STICK_Y]]),i=.5;export{r as MANETTE_TO_W3C_AXIS,e as ManetteAxis,i as TRIGGER_PRESS_THRESHOLD,t as W3CAxis,n as W3C_AXIS_COUNT};
1
+ const e={LEFT_X:0,LEFT_Y:1,RIGHT_X:3,RIGHT_Y:4,LEFT_TRIGGER:2,RIGHT_TRIGGER:5},t={LEFT_STICK_X:0,LEFT_STICK_Y:1,RIGHT_STICK_X:2,RIGHT_STICK_Y:3},n=4,r=new Map([[e.LEFT_X,t.LEFT_STICK_X],[e.LEFT_Y,t.LEFT_STICK_Y],[e.RIGHT_X,t.RIGHT_STICK_X],[e.RIGHT_Y,t.RIGHT_STICK_Y]]),i=.5;export{r as MANETTE_TO_W3C_AXIS,e as ManetteAxis,i as TRIGGER_PRESS_THRESHOLD,t as W3CAxis,n as W3C_AXIS_COUNT};
@@ -1 +1 @@
1
- import"./_virtual/_rolldown/runtime.js";const e=`Manette`;function typelibAbsentNeedle(e){return`Typelib file for namespace '${e}'`}const t=`@gjsify/node-gi`;function noTypelibText(e){return`[@gjsify/gamepad] No gamepad backend on this host — the ${e}-0.2 typelib is absent, so getGamepads() reports no controllers no matter what is plugged in. Install libmanette + its typelib (Fedora: libmanette; Debian/Ubuntu: gir1.2-manette-0.2); macOS and Windows have no libmanette at all. Gate on hasGamepadBackend().`}function noBridgeText(){return`[@gjsify/gamepad] No gamepad backend in this process — ${t} is not installed, and it is how a --app node bundle reaches gi://${e}. getGamepads() reports no controllers until it is added (npm install ${t}, plus libmanette + its typelib). Gate on hasGamepadBackend().`}const n=`[@gjsify/gamepad] The gi://${e} gamepad backend failed to load — a fault on this host, NOT a platform without libmanette. getGamepads() reports no controllers until it is fixed.`;let r=null,i=!1;function isEmptiedGiModule(e){return typeof e==`object`&&!!e&&Object.getPrototypeOf(e)===Object.prototype&&Object.getOwnPropertyNames(e).length===0}function isMissingNodeGiBridge(e,n){if(!n.includes(t))return!1;let r=e?.code;return r===`MODULE_NOT_FOUND`||r===`ERR_MODULE_NOT_FOUND`||n.includes(`Cannot find module`)}function _diagnoseGiLoadError(t,n=e){let r=t instanceof Error?t.message:String(t);return r.includes(typelibAbsentNeedle(n))?{status:`absent`,diagnostic:noTypelibText(n)}:isMissingNodeGiBridge(t,r)?{status:`absent`,diagnostic:noBridgeText()}:{status:`failed`,diagnostic:null}}async function probeGamepadBackend(t){let n=t.importer??(()=>import(`gi://Manette?version=0.2`)),r;try{if(r=(await n()).default,typeof r?.Monitor!=`function`)return isEmptiedGiModule(r)?{status:`absent`,module:null,error:null,diagnostic:null}:{status:`failed`,module:null,error:Error(`gi://${e} resolved without a Monitor class — unexpected ${e} ABI`),diagnostic:null}}catch(e){let{status:t,diagnostic:n}=_diagnoseGiLoadError(e);return{status:t,module:null,error:e,diagnostic:n}}return{status:`manette`,module:r,error:null,diagnostic:null}}function loadGamepadBackend(e={}){if(r!==null){if(e.importer)throw Error(`loadGamepadBackend({ importer }) called after the probe already ran — the probe is cached per process, so the importer would be ignored. Call _resetGamepadBackendCache() first (tests only).`);return r}return r=probeGamepadBackend(e),r}function reportGamepadBackendOnce(e){if(!i){if(e.status===`failed`){i=!0,console.error(n,e.error);return}e.diagnostic!==null&&(i=!0,console.warn(e.diagnostic))}}function reportGamepadMonitorFault(t){console.error(`[@gjsify/gamepad] The gi://${e} backend loaded but the gamepad monitor could not be started — a fault AFTER the backend was available, not a missing backend. Manette.Monitor needs udev and /dev/input access, which a sandbox can withhold (flatpak: --device=input). getGamepads() reports no controllers until it is fixed.`,t)}async function hasGamepadBackend(){return(await loadGamepadBackend()).status===`manette`}function _resetGamepadBackendCache(){r=null,i=!1}export{_diagnoseGiLoadError,_resetGamepadBackendCache,hasGamepadBackend,loadGamepadBackend,reportGamepadBackendOnce,reportGamepadMonitorFault};
1
+ import"./_virtual/_rolldown/runtime.js";import{hostEnv as e,hostOs as t}from"@gjsify/utils/core";const n=`GJSIFY_GAMEPAD_BACKEND`,r=[`manette`,`sdl`,`compare`];function typelibAbsentNeedle(e){return`Typelib file for namespace '${e}'`}const i=`@gjsify/node-gi`;function loadFaultText(e,t){return`[@gjsify/gamepad] The gi://${e} gamepad backend failed to load — a fault on this host, NOT ${t}. getGamepads() reports no controllers until it is fixed.`}const a={status:`manette`,namespace:`Manette`,noTypelibText:`[@gjsify/gamepad] No gamepad backend on this host — the Manette-0.2 typelib is absent, so getGamepads() reports no controllers no matter what is plugged in. Install libmanette + its typelib (Fedora: libmanette; Debian/Ubuntu: gir1.2-manette-0.2); Windows has no libmanette at all. Gate on hasGamepadBackend().`,noBridgeText:`[@gjsify/gamepad] No gamepad backend in this process — ${i} is not installed, and it is how a --app node bundle reaches gi://Manette. getGamepads() reports no controllers until it is added (npm install ${i}, plus libmanette + its typelib). Gate on hasGamepadBackend().`,loadFaultText:loadFaultText(`Manette`,`a platform without libmanette`)},o={status:`sdl`,namespace:`GjsifyGamepad`,noTypelibText:"[@gjsify/gamepad] No gamepad backend on this host — the GjsifyGamepad-1.0 typelib of @gjsify/gamepad-native is not on the typelib path, so getGamepads() reports no controllers no matter what is plugged in. It ships as the optional dependency @gjsify/gamepad-native-<os>-<arch>: check that it installed, and start the program through `gjsify run` (or put its prebuilds/<os>-<arch> directory on GI_TYPELIB_PATH and on the library search path: PATH on Windows). Gate on hasGamepadBackend().",noBridgeText:`[@gjsify/gamepad] No gamepad backend in this process — ${i} is not installed, and it is how a --app node bundle reaches gi://GjsifyGamepad. getGamepads() reports no controllers until it is added (npm install ${i}). Gate on hasGamepadBackend().`,loadFaultText:loadFaultText(`GjsifyGamepad`,`a host without @gjsify/gamepad-native`)},s={manette:a,sdl:o};let c=null,l=!1;function isEmptiedGiModule(e){return typeof e==`object`&&!!e&&Object.getPrototypeOf(e)===Object.prototype&&Object.getOwnPropertyNames(e).length===0}function isMissingNodeGiBridge(e,t){if(!t.includes(i))return!1;let n=e?.code;return n===`MODULE_NOT_FOUND`||n===`ERR_MODULE_NOT_FOUND`||t.includes(`Cannot find module`)}function _diagnoseGiLoadError(e,t=a){let n=e instanceof Error?e.message:String(e);return n.includes(typelibAbsentNeedle(t.namespace))?{status:`absent`,diagnostic:t.noTypelibText}:isMissingNodeGiBridge(e,n)?{status:`absent`,diagnostic:t.noBridgeText}:{status:`failed`,diagnostic:t.loadFaultText}}async function probeGi(e,t){let n;try{if(n=(await t()).default,typeof n?.Monitor!=`function`)return isEmptiedGiModule(n)?{status:`absent`,module:null,error:null,diagnostic:null,shadow:null}:{status:`failed`,module:null,error:Error(`gi://${e.namespace} resolved without a Monitor class — unexpected ${e.namespace} ABI`),diagnostic:e.loadFaultText,shadow:null}}catch(t){let{status:n,diagnostic:r}=_diagnoseGiLoadError(t,e);return{status:n,module:null,error:t,diagnostic:r,shadow:null}}return e.status===`sdl`?{status:`sdl`,module:n,error:null,diagnostic:null,shadow:null}:{status:`manette`,module:n,error:null,diagnostic:null,shadow:null}}function readBackendChoice(e){let t=e(n);if(t===void 0||t===``)return{choice:`manette`,note:null};let i=t.trim().toLowerCase();return r.includes(i)?{choice:i,note:null}:{choice:`manette`,note:`[@gjsify/gamepad] ${n}=${JSON.stringify(t)} is not one of ${r.join(`, `)}; using manette.`}}async function probeGamepadBackend(n){let probeSdl=()=>probeGi(o,n.sdlImporter??(()=>import(`gi://GjsifyGamepad?version=1.0`))),probeManette=()=>probeGi(a,n.importer??(()=>import(`gi://Manette?version=0.2`))),r=(n.hostOs??t)();if(r===`darwin`||r===`win32`)return probeSdl();let{choice:i,note:s}=readBackendChoice(n.env??e);if(i===`sdl`)return probeSdl();let c=await probeManette();return s===null?i===`compare`?{...c,shadow:await probeSdl()}:c:{...c,diagnostic:c.diagnostic??s}}function loadGamepadBackend(e={}){if(c!==null){if(e.importer||e.sdlImporter||e.hostOs||e.env)throw Error(`loadGamepadBackend({ importer, sdlImporter, hostOs, env }) called after the probe already ran — the probe is cached per process, so the override would be ignored. Call _resetGamepadBackendCache() first (tests only).`);return c}return c=probeGamepadBackend(e),c}function reportGamepadBackendOnce(e){if(l)return;let t=e.shadow,r=t!==null&&t.status!==`sdl`?`[@gjsify/gamepad] ${n}=compare, but the SDL3 shim is ${t.status}: nothing to compare against. ${t.diagnostic??``}`.trim():null;if(e.status===`failed`){l=!0,console.error(e.diagnostic,e.error),r!==null&&warnWithCause(r,t?.error);return}e.diagnostic===null&&r===null||(l=!0,e.diagnostic!==null&&console.warn(e.diagnostic),r!==null&&warnWithCause(r,t?.error))}function warnWithCause(e,t){t==null?console.warn(e):console.warn(e,t)}function reportGamepadMonitorFault(e,t=null){let n=t?.name??`gamepad`,r=t?.startRequirements?`${t.startRequirements} `:``;console.error(`[@gjsify/gamepad] The ${n} backend loaded but the gamepad monitor could not be started — a fault AFTER the backend was available, not a missing backend. `+r+`getGamepads() reports no controllers until it is fixed.`,e)}async function hasGamepadBackend(){let{status:e}=await loadGamepadBackend();return e===`manette`||e===`sdl`}async function describeGamepadBackend(){let e=await loadGamepadBackend(),t={manette:`libmanette (gi://Manette)`,sdl:`SDL3 (gi://GjsifyGamepad)`};if(e.status===`absent`||e.status===`failed`)return`none (${e.status})`;let n=t[e.status];return e.shadow?.status===`sdl`?`${n}, compared against ${t.sdl}`:n}function _resetGamepadBackendCache(){c=null,l=!1}export{n as GAMEPAD_BACKEND_ENV,s as _GI_BACKENDS,_diagnoseGiLoadError,_resetGamepadBackendCache,describeGamepadBackend,hasGamepadBackend,loadGamepadBackend,reportGamepadBackendOnce,reportGamepadMonitorFault};
@@ -0,0 +1,2 @@
1
+ import"./_virtual/_rolldown/runtime.js";import"./axis-mapping.js";import"./button-mapping.js";const e=.02;var MirrorSink=class{_label;_report;_forward;devices=new Map;constructor(e,t,n){this._label=e,this._report=t,this._forward=n}connected(e){this.devices.has(e)||(this.devices.set(e,{device:e,buttons:Array.from({length:17}).fill(0),pressed:Array.from({length:17}).fill(!1),axes:Array.from({length:4}).fill(0)}),this._report(`${this._label}: connected "${e.id}"`)),this._forward?.connected(e)}disconnected(e){this.devices.delete(e)&&this._report(`${this._label}: disconnected "${e.id}"`),this._forward?.disconnected(e)}button(e,t,n,r){let i=this.devices.get(e);i&&t>=0&&t<17&&(i.buttons[t]=n,i.pressed[t]=r),this._forward?.button(e,t,n,r)}axis(e,t,n){let r=this.devices.get(e);r&&t>=0&&t<4&&(r.axes[t]=n),this._forward?.axis(e,t,n)}};function differences(t,n){let r=[];for(let i=0;i<17;i++){let a=t.buttons[i],o=n.buttons[i];(Math.abs(a-o)>e||t.pressed[i]!==n.pressed[i])&&r.push(`button ${i}: ${a.toFixed(2)}${t.pressed[i]?`*`:``} vs ${o.toFixed(2)}${n.pressed[i]?`*`:``}`)}for(let i=0;i<4;i++){let a=t.axes[i],o=n.axes[i];Math.abs(a-o)>e&&r.push(`axis ${i}: ${a.toFixed(2)} vs ${o.toFixed(2)}`)}return r}function pairUp(e,t){let n=[],r=[...t],i=[];for(let t of e){let e=t.device.model===void 0?-1:r.findIndex(e=>e.device.model===t.device.model);e>=0?n.push([t,r.splice(e,1)[0]]):i.push(t)}for(let e=0;e<Math.min(i.length,r.length);e++)n.push([i[e],r[e]]);return n}var ComparingSource=class{_primary;_shadow;_report;name;startRequirements;_primarySink=null;_shadowSink=null;_shadowStarted=!1;_pending=``;_reported=``;constructor(e,t,n=e=>console.warn(`[@gjsify/gamepad compare] ${e}`)){this._primary=e,this._shadow=t,this._report=n,this.name=`${e.name} (compared against ${t.name})`,this.startRequirements=e.startRequirements}start(e){this._primarySink=new MirrorSink(this._primary.name,this._report,e),this._shadowSink=new MirrorSink(this._shadow.name,this._report,null),this._primary.start(this._primarySink);try{this._shadow.start(this._shadowSink),this._shadowStarted=!0,this._report(`comparing ${this._primary.name} against ${this._shadow.name}`)}catch(e){this._report(`${this._shadow.name} did not start, nothing to compare against: ${String(e)}`),this._shadow.stop()}}poll(){this._primary.poll?.(),this._shadowStarted&&(this._shadow.poll?.(),this._compare())}stop(){this._primary.stop(),this._shadowStarted&&this._shadow.stop(),this._shadowStarted=!1,this._primarySink=null,this._shadowSink=null,this._pending=``,this._reported=``}_compare(){let e=[...this._primarySink?.devices.values()??[]],t=[...this._shadowSink?.devices.values()??[]],n=[];e.length!==t.length&&n.push(`${this._primary.name} sees ${e.length} controller(s), ${this._shadow.name} sees ${t.length}`);for(let[r,i]of pairUp(e,t)){let e=differences(r,i);e.length>0&&n.push(`"${r.device.id}" vs "${i.device.id}": ${e.join(`, `)}`)}let r=n.join(`
2
+ `),i=r===this._pending;if(this._pending=r,!(!i||r===this._reported))if(this._reported=r,r===``)this._report(`the two backends agree again`);else for(let e of n)this._report(e)}};export{ComparingSource};
@@ -1 +1 @@
1
- import"./_virtual/_rolldown/runtime.js";import{MANETTE_TO_W3C_AXIS as e,ManetteAxis as t,TRIGGER_PRESS_THRESHOLD as n}from"./axis-mapping.js";import{loadGamepadBackend as r,reportGamepadBackendOnce as i,reportGamepadMonitorFault as a}from"./backend.js";import{MANETTE_TO_W3C_BUTTON as o,W3CButton as s}from"./button-mapping.js";import{GamepadButton as c}from"./gamepad-button.js";import{GamepadEvent as l}from"./gamepad-event.js";import{Gamepad as u}from"./gamepad.js";import{ManetteHapticActuator as d}from"./haptic-actuator.js";var GamepadManager=class{_monitor=null;_slots=[];_monitorSignalIds=[];_ManetteModule=null;_initPromise=null;_initialized=!1;_ensureInit(){this._initialized||(this._initPromise||=this._init().catch(e=>{a(e),this._initialized=!0}))}async _init(){let e=await r();if(i(e),e.module===null){this._initialized=!0;return}this._ManetteModule=e.module;let t=new this._ManetteModule.Monitor;this._monitor=t;let n=t.iterate(),a=n.next();for(;a[0];){let e=a[1];e&&this._onDeviceConnected(e),a=n.next()}this._monitorSignalIds.push(t.connect(`device-connected`,(e,t)=>{this._onDeviceConnected(t)}),t.connect(`device-disconnected`,(e,t)=>{this._onDeviceDisconnected(t)})),this._initialized=!0}_onDeviceConnected(e){let t=this._slots.indexOf(null);t===-1&&(t=this._slots.length,this._slots.push(null));let n={device:e,index:t,connected:!0,timestamp:performance.now(),buttons:new Float64Array(17),buttonsPressed:Array.from({length:17}).fill(!1),axes:new Float64Array(4),hapticActuator:new d(e),signalIds:[]};n.signalIds.push(e.connect(`button-press-event`,(e,t)=>{this._onButtonPress(n,t)}),e.connect(`button-release-event`,(e,t)=>{this._onButtonRelease(n,t)}),e.connect(`absolute-axis-event`,(e,t)=>{this._onAxisChange(n,t)}),e.connect(`hat-axis-event`,(e,t)=>{this._onHatChange(n,t)}),e.connect(`disconnected`,()=>{this._onDeviceDisconnected(e)})),this._slots[t]=n;let r=this._createSnapshot(n);r&&globalThis.dispatchEvent?.(new l(`gamepadconnected`,{gamepad:r}))}_onDeviceDisconnected(e){let t=this._findStateByDevice(e);if(!t)return;for(let n of t.signalIds)e.disconnect(n);t.connected=!1;let n=this._createSnapshot(t);this._slots[t.index]=null,n&&globalThis.dispatchEvent?.(new l(`gamepaddisconnected`,{gamepad:n}))}_onButtonPress(e,t){let[n,r]=t.get_button();if(!n)return;let i=o.get(r);i!==void 0&&(e.buttons[i]=1,e.buttonsPressed[i]=!0,e.timestamp=performance.now())}_onButtonRelease(e,t){let[n,r]=t.get_button();if(!n)return;let i=o.get(r);i!==void 0&&(e.buttons[i]=0,e.buttonsPressed[i]=!1,e.timestamp=performance.now())}_onAxisChange(r,i){let[a,o,c]=i.get_absolute();if(!a)return;let l=e.get(o);if(l!==void 0)r.axes[l]=c;else if(o===t.LEFT_TRIGGER){let e=(c+1)/2;r.buttons[s.LEFT_TRIGGER]=e,r.buttonsPressed[s.LEFT_TRIGGER]=e>n}else if(o===t.RIGHT_TRIGGER){let e=(c+1)/2;r.buttons[s.RIGHT_TRIGGER]=e,r.buttonsPressed[s.RIGHT_TRIGGER]=e>n}r.timestamp=performance.now()}_onHatChange(e,t){let[n,r,i]=t.get_hat();n&&(r===0?(e.buttonsPressed[s.DPAD_LEFT]=i<0,e.buttons[s.DPAD_LEFT]=+(i<0),e.buttonsPressed[s.DPAD_RIGHT]=i>0,e.buttons[s.DPAD_RIGHT]=+(i>0)):r===1&&(e.buttonsPressed[s.DPAD_UP]=i<0,e.buttons[s.DPAD_UP]=+(i<0),e.buttonsPressed[s.DPAD_DOWN]=i>0,e.buttons[s.DPAD_DOWN]=+(i>0)),e.timestamp=performance.now())}_findStateByDevice(e){for(let t of this._slots)if(t&&t.device===e)return t;return null}_createSnapshot(e){let t=[];for(let n=0;n<17;n++)t.push(new c(e.buttonsPressed[n],e.buttonsPressed[n]||e.buttons[n]>0,e.buttons[n]));return new u({id:e.device.get_name()??`Gamepad (${e.device.get_guid()})`,index:e.index,connected:e.connected,timestamp:e.timestamp,mapping:`standard`,axes:Array.from(e.axes),buttons:t,vibrationActuator:e.hapticActuator})}getGamepads(){return this._ensureInit(),this._slots.map(e=>e?this._createSnapshot(e):null)}dispose(){for(let e of this._slots)if(e)for(let t of e.signalIds)e.device.disconnect(t);if(this._slots.length=0,this._monitor){for(let e of this._monitorSignalIds)this._monitor.disconnect(e);this._monitorSignalIds=[],this._monitor=null}this._initialized=!1,this._initPromise=null}};export{GamepadManager};
1
+ import"./_virtual/_rolldown/runtime.js";import"./axis-mapping.js";import{loadGamepadBackend as e,reportGamepadBackendOnce as t,reportGamepadMonitorFault as n}from"./backend.js";import"./button-mapping.js";import{ComparingSource as r}from"./compare-source.js";import{GamepadButton as i}from"./gamepad-button.js";import{GamepadEvent as a}from"./gamepad-event.js";import{Gamepad as o}from"./gamepad.js";import{ManetteSource as s}from"./manette-source.js";import{SdlSource as c}from"./sdl-source.js";var GamepadManager=class{_slots=[];_injectedSource;_source=null;_started=!1;_initPromise=null;_initialized=!1;_sink={connected:e=>this._onDeviceConnected(e),disconnected:e=>this._onDeviceDisconnected(e),button:(e,t,n,r)=>this._onButton(e,t,n,r),axis:(e,t,n)=>this._onAxis(e,t,n)};constructor(e={}){this._injectedSource=e.source??null}_ensureInit(){this._initialized||(this._initPromise||=this._init().catch(e=>{n(e,this._source),this._initialized=!0}))}async _init(){let n=this._injectedSource;if(n===null){let i=await e();if(t(i),i.status===`sdl`)n=new c(i.module);else if(i.status===`manette`)n=new s(i.module),i.shadow?.status===`sdl`&&(n=new r(n,new c(i.shadow.module)));else{this._initialized=!0;return}}this._source=n,n.start(this._sink),this._started=!0,this._initialized=!0}_onDeviceConnected(e){if(this._findState(e))return;let t=this._slots.indexOf(null);t===-1&&(t=this._slots.length,this._slots.push(null));let n={device:e,index:t,connected:!0,timestamp:performance.now(),buttons:new Float64Array(17),buttonsPressed:Array.from({length:17}).fill(!1),axes:new Float64Array(4)};this._slots[t]=n,globalThis.dispatchEvent?.(new a(`gamepadconnected`,{gamepad:this._createSnapshot(n)}))}_onDeviceDisconnected(e){let t=this._findState(e);if(!t)return;t.connected=!1;let n=this._createSnapshot(t);this._slots[t.index]=null,globalThis.dispatchEvent?.(new a(`gamepaddisconnected`,{gamepad:n}))}_onButton(e,t,n,r){let i=this._findState(e);!i||!Number.isInteger(t)||t<0||t>=17||(i.buttons[t]=n,i.buttonsPressed[t]=r,i.timestamp=performance.now())}_onAxis(e,t,n){let r=this._findState(e);!r||!Number.isInteger(t)||t<0||t>=4||(r.axes[t]=n,r.timestamp=performance.now())}_findState(e){for(let t of this._slots)if(t&&t.device===e)return t;return null}_createSnapshot(e){let t=[];for(let n=0;n<17;n++)t.push(new i(e.buttonsPressed[n],e.buttonsPressed[n]||e.buttons[n]>0,e.buttons[n]));return new o({id:e.device.id,index:e.index,connected:e.connected,timestamp:e.timestamp,mapping:`standard`,axes:Array.from(e.axes),buttons:t,vibrationActuator:e.device.vibrationActuator})}getGamepads(){return this._ensureInit(),this._started&&this._source?.poll?.(),this._slots.map(e=>e?this._createSnapshot(e):null)}dispose(){this._source?.stop(),this._source=null,this._started=!1,this._slots.length=0,this._initialized=!1,this._initPromise=null}};export{GamepadManager};
@@ -1 +1 @@
1
- import"./_virtual/_rolldown/runtime.js";var ManetteHapticActuator=class{effects;_device;constructor(e){this._device=e,this.effects=e.has_rumble()?[`dual-rumble`]:[]}playEffect(e,t){if(e!==`dual-rumble`||!this._device.has_rumble())return Promise.resolve(`complete`);let n=t?.duration??200,r=Math.round((t?.strongMagnitude??1)*65535),i=Math.round((t?.weakMagnitude??1)*65535);return this._device.rumble(r,i,Math.min(n,32767)),Promise.resolve(`complete`)}reset(){return this._device.has_rumble()&&this._device.rumble(0,0,0),Promise.resolve(`complete`)}};export{ManetteHapticActuator};
1
+ import"./_virtual/_rolldown/runtime.js";import{DOMException as e}from"@gjsify/dom-events";const t=5e3;function toMotor(e){return Math.round(e*65535)}function isUnitMagnitude(e){return Number.isFinite(e)&&e>=0&&e<=1}function isMilliseconds(e){return Number.isFinite(e)&&e>=0}var RumbleHapticActuator=class{_playing=null;playEffect(n,r={}){let i=r.duration??0,a=r.startDelay??0,o={strong:r.strongMagnitude??0,weak:r.weakMagnitude??0,leftTrigger:r.leftTrigger??0,rightTrigger:r.rightTrigger??0};if(n!==`dual-rumble`&&n!==`trigger-rumble`)return Promise.reject(TypeError(`Unknown haptic effect type '${String(n)}'.`));if(!isMilliseconds(i)||!isMilliseconds(a)||i+a>5e3)return Promise.reject(TypeError(`A haptic effect lasts 0..${t} ms including its startDelay.`));if(!(n===`dual-rumble`?[o.strong,o.weak]:Object.values(o)).every(isUnitMagnitude))return Promise.reject(TypeError(`Haptic effect magnitudes are 0..1.`));if(this._preempt(),!this.effects.includes(n))return Promise.reject(new e(`This gamepad cannot play a '${n}' effect.`,`NotSupportedError`));let s={strong:toMotor(o.strong),weak:toMotor(o.weak),leftTrigger:toMotor(o.leftTrigger),rightTrigger:toMotor(o.rightTrigger)};return new Promise(e=>{let t={resolve:e,timer:null};this._playing=t;let finish=()=>{this._playing===t&&(this._playing=null,e(`complete`))},start=()=>{if(this._playing===t){if(i===0){finish();return}this._start(n,s,Math.round(i)),t.timer=setTimeout(finish,i)}};a>0?t.timer=setTimeout(start,a):start()})}reset(){return this._preempt(),Promise.resolve(`complete`)}_preempt(){let e=this._playing;e&&(this._playing=null,e.timer!==null&&clearTimeout(e.timer),this._stop(),e.resolve(`preempted`))}},ManetteHapticActuator=class extends RumbleHapticActuator{_device;effects;constructor(e){super(),this._device=e,this.effects=e.has_rumble()?[`dual-rumble`]:[]}_start(e,t,n){this._device.rumble(t.strong,t.weak,n)}_stop(){this._device.rumble(0,0,0)}},SdlHapticActuator=class extends RumbleHapticActuator{_device;effects;constructor(e){super(),this._device=e;let t=[];e.has_rumble()&&t.push(`dual-rumble`),e.has_trigger_rumble()&&t.push(`trigger-rumble`),this.effects=t}_start(e,t,n){e===`trigger-rumble`&&(this._device.rumble_triggers(t.leftTrigger,t.rightTrigger,n),!this.effects.includes(`dual-rumble`))||this._device.rumble(t.strong,t.weak,n)}_stop(){this.effects.includes(`dual-rumble`)&&this._device.rumble(0,0,0),this.effects.includes(`trigger-rumble`)&&this._device.rumble_triggers(0,0,0)}};export{t as MAX_HAPTIC_EFFECT_DURATION_MS,ManetteHapticActuator,SdlHapticActuator};
package/lib/esm/index.js CHANGED
@@ -1 +1 @@
1
- import{MANETTE_TO_W3C_AXIS as e,ManetteAxis as t,TRIGGER_PRESS_THRESHOLD as n,W3CAxis as r,W3C_AXIS_COUNT as i}from"./axis-mapping.js";import{hasGamepadBackend as a}from"./backend.js";import{LinuxButton as o,MANETTE_TO_W3C_BUTTON as s,W3CButton as c,W3C_BUTTON_COUNT as l}from"./button-mapping.js";import{GamepadButton as u}from"./gamepad-button.js";import{GamepadEvent as d}from"./gamepad-event.js";import{Gamepad as f}from"./gamepad.js";import{ManetteHapticActuator as p}from"./haptic-actuator.js";import{GamepadManager as m}from"./gamepad-manager.js";export{f as Gamepad,u as GamepadButton,d as GamepadEvent,m as GamepadManager,e as MANETTE_TO_W3C_AXIS,s as MANETTE_TO_W3C_BUTTON,t as ManetteAxis,o as ManetteButton,p as ManetteHapticActuator,n as TRIGGER_PRESS_THRESHOLD,r as W3CAxis,c as W3CButton,i as W3C_AXIS_COUNT,l as W3C_BUTTON_COUNT,a as hasGamepadBackend};
1
+ import{MANETTE_TO_W3C_AXIS as e,ManetteAxis as t,TRIGGER_PRESS_THRESHOLD as n,W3CAxis as r,W3C_AXIS_COUNT as i}from"./axis-mapping.js";import{describeGamepadBackend as a,hasGamepadBackend as o}from"./backend.js";import{LinuxButton as s,MANETTE_TO_W3C_BUTTON as c,W3CButton as l,W3C_BUTTON_COUNT as u}from"./button-mapping.js";import{GamepadButton as d}from"./gamepad-button.js";import{GamepadEvent as f}from"./gamepad-event.js";import{Gamepad as p}from"./gamepad.js";import{ManetteHapticActuator as m}from"./haptic-actuator.js";import{GamepadManager as h}from"./gamepad-manager.js";export{p as Gamepad,d as GamepadButton,f as GamepadEvent,h as GamepadManager,e as MANETTE_TO_W3C_AXIS,c as MANETTE_TO_W3C_BUTTON,t as ManetteAxis,s as ManetteButton,m as ManetteHapticActuator,n as TRIGGER_PRESS_THRESHOLD,r as W3CAxis,l as W3CButton,i as W3C_AXIS_COUNT,u as W3C_BUTTON_COUNT,a as describeGamepadBackend,o as hasGamepadBackend};
@@ -0,0 +1 @@
1
+ import"./_virtual/_rolldown/runtime.js";import{MANETTE_TO_W3C_AXIS as e,ManetteAxis as t,TRIGGER_PRESS_THRESHOLD as n}from"./axis-mapping.js";import{MANETTE_TO_W3C_BUTTON as r,W3CButton as i}from"./button-mapping.js";import{ManetteHapticActuator as a}from"./haptic-actuator.js";import{modelFromGuid as o}from"./source.js";var ManetteSource=class{_Manette;name=`gi://Manette`;startRequirements=`Manette.Monitor needs udev and /dev/input access, which a sandbox can withhold (flatpak: --device=input).`;_monitor=null;_monitorSignalIds=[];_devices=new Map;constructor(e){this._Manette=e}start(e){let t=new this._Manette.Monitor;this._monitor=t;let n=t.iterate(),r=n.next();for(;r[0];){let t=r[1];t&&this._add(t,e),r=n.next()}this._monitorSignalIds.push(t.connect(`device-connected`,(t,n)=>{this._add(n,e)}),t.connect(`device-disconnected`,(t,n)=>{this._remove(n,e)}))}stop(){for(let[e,t]of this._devices)for(let n of t.signalIds)e.disconnect(n);if(this._devices.clear(),this._monitor){for(let e of this._monitorSignalIds)this._monitor.disconnect(e);this._monitor=null}this._monitorSignalIds=[]}_add(e,t){if(this._devices.has(e))return;let n={id:e.get_name()??`Gamepad (${e.get_guid()})`,vibrationActuator:new a(e),model:o(e.get_guid())},r={handle:n,signalIds:[]};this._devices.set(e,r),r.signalIds.push(e.connect(`button-press-event`,(e,r)=>{this._onButton(n,r,!0,t)}),e.connect(`button-release-event`,(e,r)=>{this._onButton(n,r,!1,t)}),e.connect(`absolute-axis-event`,(e,r)=>{this._onAxis(n,r,t)}),e.connect(`hat-axis-event`,(e,r)=>{this._onHat(n,r,t)}),e.connect(`disconnected`,()=>{this._remove(e,t)})),t.connected(n)}_remove(e,t){let n=this._devices.get(e);if(n){for(let t of n.signalIds)e.disconnect(t);this._devices.delete(e),t.disconnected(n.handle)}}_onButton(e,t,n,i){let[a,o]=t.get_button();if(!a)return;let s=r.get(o);s!==void 0&&i.button(e,s,+!!n,n)}_onAxis(r,a,o){let[s,c,l]=a.get_absolute();if(!s)return;let u=e.get(c);if(u!==void 0){o.axis(r,u,l);return}let d=c===t.LEFT_TRIGGER?i.LEFT_TRIGGER:c===t.RIGHT_TRIGGER?i.RIGHT_TRIGGER:void 0;if(d===void 0)return;let f=(l+1)/2;o.button(r,d,f,f>n)}_onHat(e,t,n){let[r,a,o]=t.get_hat();if(!r)return;let[s,c]=a===0?[i.DPAD_LEFT,i.DPAD_RIGHT]:a===1?[i.DPAD_UP,i.DPAD_DOWN]:[void 0,void 0];s===void 0||c===void 0||(n.button(e,s,+(o<0),o<0),n.button(e,c,+(o>0),o>0))}};export{ManetteSource};
@@ -1 +1 @@
1
- import{GamepadEvent as e}from"./gamepad-event.js";import{GamepadManager as t}from"./gamepad-manager.js";const n=globalThis;if(n.navigator===void 0&&(n.navigator={}),typeof n.navigator.getGamepads!=`function`){let e=new t;n.navigator.getGamepads=()=>e.getGamepads()}n.GamepadEvent===void 0&&(n.GamepadEvent=e);
1
+ import{GamepadEvent as e}from"./gamepad-event.js";import{GamepadManager as t}from"./gamepad-manager.js";import"@gjsify/node-globals/register/navigator";const n=globalThis;if(typeof n.navigator.getGamepads!=`function`){let e=new t;n.navigator.getGamepads=()=>e.getGamepads()}n.GamepadEvent===void 0&&(n.GamepadEvent=e);
File without changes
@@ -0,0 +1 @@
1
+ import"./_virtual/_rolldown/runtime.js";import{TRIGGER_PRESS_THRESHOLD as e}from"./axis-mapping.js";import{W3CButton as t}from"./button-mapping.js";import{SdlHapticActuator as n}from"./haptic-actuator.js";import{modelFromGuid as r}from"./source.js";function isPressed(n,r){return n===t.LEFT_TRIGGER||n===t.RIGHT_TRIGGER?r>e:r>0}var SdlSource=class{_GjsifyGamepad;name=`gi://GjsifyGamepad`;startRequirements=`SDL's gamepad subsystem has to start in this process; the error carries SDL's own reason.`;_monitor=null;_sink=null;_signalIds=[];_devices=new Map;constructor(e){this._GjsifyGamepad=e}start(e){let t=this._GjsifyGamepad.Monitor.new();this._monitor=t,this._sink=e,this._signalIds.push(t.connect(`device-added`,(t,n)=>this._add(n,e)),t.connect(`device-removed`,(t,n)=>this._remove(n,e))),t.update()}poll(){let e=this._monitor,t=this._sink;if(!(!e||!t)){e.update();for(let[e,n]of this._devices)this._report(e,n,t)}}stop(){let e=this._monitor;if(this._monitor=null,this._sink=null,e){for(let t of this._signalIds)e.disconnect(t);e.close()}this._signalIds=[],this._devices.clear()}_add(e,t){if(this._devices.has(e))return;let i={id:e.get_name(),vibrationActuator:new n(e),model:r(e.get_guid())},a={handle:i,buttons:Array.from({length:17}).fill(0),axes:Array.from({length:4}).fill(0)};this._devices.set(e,a),t.connected(i),this._report(e,a,t)}_remove(e,t){let n=this._devices.get(e);n&&(this._devices.delete(e),t.disconnected(n.handle))}_report(e,t,n){let r=e.get_buttons();for(let e=0;e<17;e++){let i=r[e]??0;i!==t.buttons[e]&&(t.buttons[e]=i,n.button(t.handle,e,i,isPressed(e,i)))}let i=e.get_axes();for(let e=0;e<4;e++){let r=i[e]??0;r!==t.axes[e]&&(t.axes[e]=r,n.axis(t.handle,e,r))}}};export{SdlSource};
@@ -0,0 +1 @@
1
+ import"./_virtual/_rolldown/runtime.js";function modelFromGuid(e){if(!e||e.length<20)return;let le16=t=>e.slice(t+2,t+4)+e.slice(t,t+2),t=le16(8);return t===`0000`?void 0:`${t}:${le16(16)}`.toLowerCase()}export{modelFromGuid};
@@ -1,13 +1,20 @@
1
1
  /**
2
- * SDL logical axis indices as reported by Manette.Event.get_absolute().
3
- * These are the SDL gamepad mapping indices, NOT Linux ABS_* hardware codes.
2
+ * The axis codes `Manette.Event.get_absolute()` reports: Linux `ABS_*` codes. A mapped
3
+ * device is translated by libmanette's own table (`manette-mapping.c`: `leftx`→ABS_X,
4
+ * `lefty`→ABS_Y, `rightx`→ABS_RX, `righty`→ABS_RY; the triggers become the BUTTONS
5
+ * BTN_TL2/BTN_TR2), an unmapped one passes its kernel codes through. Measured with a
6
+ * uinput pad under gi://Manette: the right stick arrives as 3 and 4.
7
+ *
8
+ * NOT SDL mapping indices. This table once said RIGHT_X 2 / RIGHT_Y 3 / LEFT_TRIGGER 4,
9
+ * which put the right stick's X on W3C axis 3 and its Y on the left trigger for every
10
+ * controller libmanette maps; an 8BitDo N30 Pro 2 compared against the SDL3 shim showed it.
4
11
  */
5
12
  export declare const ManetteAxis: {
6
13
  readonly LEFT_X: 0;
7
14
  readonly LEFT_Y: 1;
8
- readonly RIGHT_X: 2;
9
- readonly RIGHT_Y: 3;
10
- readonly LEFT_TRIGGER: 4;
15
+ readonly RIGHT_X: 3;
16
+ readonly RIGHT_Y: 4;
17
+ readonly LEFT_TRIGGER: 2;
11
18
  readonly RIGHT_TRIGGER: 5;
12
19
  };
13
20
  /**
@@ -23,8 +30,8 @@ export declare const W3CAxis: {
23
30
  /** Total number of axes in the W3C standard mapping. */
24
31
  export declare const W3C_AXIS_COUNT = 4;
25
32
  /**
26
- * Maps SDL logical axis → W3C axis index.
27
- * Stick axes (0–3) map 1:1. Trigger axes (4–5) are NOT in this map —
33
+ * Maps the libmanette axis code → W3C axis index.
34
+ * Only the four stick axes are in this map; the trigger codes are NOT —
28
35
  * they are handled separately in gamepad-manager as buttons[6]/buttons[7].
29
36
  */
30
37
  export declare const MANETTE_TO_W3C_AXIS: ReadonlyMap<number, number>;
@@ -1,29 +1,61 @@
1
1
  import type Manette from '@girs/manette-0.2';
2
+ import { type TargetOs } from '@gjsify/utils/core';
3
+ import type { GjsifyGamepadNamespace } from './sdl-namespace.js';
4
+ import type { GamepadSource } from './source.js';
2
5
  /**
3
6
  * What this host has.
4
7
  *
5
8
  * - `manette` — the Manette-0.2 typelib loaded and exposes `Monitor`.
6
- * - `absent` — no backend reachable from here: no libmanette, a `--app node` process
7
- * without `@gjsify/node-gi`, or a target where `gi://` is stubbed by design.
9
+ * - `sdl` — the GjsifyGamepad-1.0 typelib (`@gjsify/gamepad-native`) loaded and
10
+ * exposes `Monitor`.
11
+ * - `absent` — no backend reachable from here: no libmanette, no gamepad-native
12
+ * prebuild, a `--app node` process without `@gjsify/node-gi`, or a target where
13
+ * `gi://` is stubbed by design.
8
14
  * - `failed` — a backend should have been reachable and was not. A fault.
9
15
  */
10
- export type GamepadBackendStatus = 'manette' | 'absent' | 'failed';
11
- /** The resolved backend probe. `module` is non-null iff `status === 'manette'`. */
12
- export interface GamepadBackend {
13
- status: GamepadBackendStatus;
14
- module: typeof Manette | null;
16
+ export type GamepadBackendStatus = 'manette' | 'sdl' | 'absent' | 'failed';
17
+ /**
18
+ * Which backend a Linux process uses — `GJSIFY_GAMEPAD_BACKEND`, read once per process.
19
+ * darwin and win32 have only the SDL3 shim and ignore it.
20
+ *
21
+ * - `manette` (the default) — libmanette, as before Amendment 1.
22
+ * - `sdl` — the SDL3 shim alone.
23
+ * - `compare` — libmanette drives the page and the SDL3 shim runs beside it on the
24
+ * same controllers; every disagreement is written to stderr (`compare-source.ts`).
25
+ * This is the measurement Amendment 1 requires before libmanette is removed.
26
+ */
27
+ export type GamepadBackendChoice = 'manette' | 'sdl' | 'compare';
28
+ /** The environment variable behind {@link GamepadBackendChoice}. */
29
+ export declare const GAMEPAD_BACKEND_ENV = "GJSIFY_GAMEPAD_BACKEND";
30
+ /** What every probe result carries besides the module. */
31
+ interface GamepadBackendReport {
15
32
  /** The original load error for `absent`/`failed`; `null` otherwise. */
16
33
  error: unknown;
17
34
  /**
18
35
  * The one-time line the USE site emits, or `null` when there is nothing to say.
19
- * Carried rather than printed so the capability query stays silent.
20
- *
21
- * `'absent'` with a `null` diagnostic is the by-design `gi://` stub (see
22
- * {@link isEmptiedGiModule}); `'failed'` always prints, with the original error, and
23
- * needs no text here.
36
+ * Carried rather than printed so the capability query stays silent. `'absent'`
37
+ * with a `null` diagnostic is the by-design `gi://` stub (see
38
+ * {@link isEmptiedGiModule}).
24
39
  */
25
40
  diagnostic: string | null;
41
+ /**
42
+ * `compare` mode only: the SDL3 shim's own probe, run beside the primary. The
43
+ * manager compares against it when it is `sdl` and reports its diagnostic when it
44
+ * is not. `null` in every other mode.
45
+ */
46
+ shadow: GamepadBackend | null;
26
47
  }
48
+ /** The resolved backend probe: a module exactly for the two usable states. */
49
+ export type GamepadBackend = GamepadBackendReport & ({
50
+ status: 'manette';
51
+ module: typeof Manette;
52
+ } | {
53
+ status: 'sdl';
54
+ module: GjsifyGamepadNamespace;
55
+ } | {
56
+ status: 'absent' | 'failed';
57
+ module: null;
58
+ });
27
59
  export interface LoadGamepadBackendOptions {
28
60
  /**
29
61
  * Override the `gi://Manette` import. Tests only — the single seam that exercises the
@@ -33,19 +65,49 @@ export interface LoadGamepadBackendOptions {
33
65
  importer?: () => Promise<{
34
66
  default: typeof Manette;
35
67
  }>;
68
+ /** Override the darwin branch's `gi://GjsifyGamepad` import. Tests only. */
69
+ sdlImporter?: () => Promise<{
70
+ default: GjsifyGamepadNamespace;
71
+ }>;
72
+ /**
73
+ * Override the host OS the probe branches on. Tests only — it is how the darwin
74
+ * branch is exercised on a Linux runner and the Manette branch on a Mac.
75
+ */
76
+ hostOs?: () => TargetOs | undefined;
77
+ /** Override the environment read for {@link GAMEPAD_BACKEND_ENV}. Tests only. */
78
+ env?: (name: string) => string | undefined;
79
+ }
80
+ /**
81
+ * One `gi://` backend: its namespace, which every wording is scoped to, and its three
82
+ * texts. Exported (underscored) for `backend.spec.ts`.
83
+ */
84
+ export interface _GiBackend {
85
+ status: 'manette' | 'sdl';
86
+ namespace: string;
87
+ /** No typelib for {@link namespace} on this host: what to install. */
88
+ noTypelibText: string;
89
+ /** No `@gjsify/node-gi` in a `--app node` process: what to install. */
90
+ noBridgeText: string;
91
+ /** The load is a fault: said at `error` level, next to the original error. */
92
+ loadFaultText: string;
36
93
  }
37
94
  /** Absent-vs-fault plus the text the use site should print (`null` = silence). */
38
95
  interface GiLoadDiagnosis {
39
96
  status: 'absent' | 'failed';
40
97
  diagnostic: string | null;
41
98
  }
99
+ /** Both backends, for `backend.spec.ts`; the probe itself picks one by host OS. */
100
+ export declare const _GI_BACKENDS: {
101
+ readonly manette: _GiBackend;
102
+ readonly sdl: _GiBackend;
103
+ };
42
104
  /**
43
105
  * Classify a `gi://` load failure and pick the text for it. Exported (underscored) for
44
106
  * `backend.spec.ts`, which pins the absent wording against the LIVE loader — hence the
45
- * namespace parameter: the test asks for a namespace that cannot exist and classifies the
107
+ * backend parameter: the test asks for a namespace that cannot exist and classifies the
46
108
  * error the running GJS/girepository produced for it.
47
109
  */
48
- export declare function _diagnoseGiLoadError(error: unknown, namespace?: string): GiLoadDiagnosis;
110
+ export declare function _diagnoseGiLoadError(error: unknown, backend?: _GiBackend): GiLoadDiagnosis;
49
111
  /**
50
112
  * Resolve (once) which gamepad backend this host has. Emits NOTHING.
51
113
  *
@@ -68,13 +130,16 @@ export declare function loadGamepadBackend(options?: LoadGamepadBackendOptions):
68
130
  */
69
131
  export declare function reportGamepadBackendOnce(backend: GamepadBackend): void;
70
132
  /**
71
- * The backend loaded and then the monitor did not come up — a DIFFERENT failure from a
72
- * failed load: everything after the probe (`new Manette.Monitor()`, the device walk,
73
- * `connect()`) needs udev and `/dev/input`, which a sandbox can withhold from a process
74
- * whose typelib and shared library are both fine. Routing it through the load-failure text
75
- * would re-create the same conflation one layer up.
133
+ * The backend loaded and then its device source did not come up — a DIFFERENT failure
134
+ * from a failed load: everything after the probe (`new Manette.Monitor()`, the device
135
+ * walk, `connect()`) needs udev and `/dev/input`, which a sandbox can withhold from a
136
+ * process whose typelib and shared library are both fine. Routing it through the
137
+ * load-failure text would re-create the same conflation one layer up.
138
+ *
139
+ * The advice comes from the SOURCE (`startRequirements`), because it is per backend:
140
+ * udev is the right hint for Manette and the wrong one for anything else.
76
141
  */
77
- export declare function reportGamepadMonitorFault(error: unknown): void;
142
+ export declare function reportGamepadMonitorFault(error: unknown, source?: GamepadSource | null): void;
78
143
  /**
79
144
  * Whether this host has a gamepad backend at all.
80
145
  *
@@ -82,12 +147,17 @@ export declare function reportGamepadMonitorFault(error: unknown): void;
82
147
  * what is connected — something the W3C surface itself cannot express, since it returns
83
148
  * the same list either way. Answerable without constructing a monitor, and QUIET.
84
149
  *
85
- * `false` on every host without libmanette, which today means macOS and Windows:
86
- * libmanette links libevdev unconditionally and libevdev is Linux/FreeBSD-only. Reports
87
- * that the BRIDGE is usable, not that every later call succeeds — a monitor can still fail
88
- * to start (see {@link reportGamepadMonitorFault}).
150
+ * `false` on Linux without libmanette (or, with `GJSIFY_GAMEPAD_BACKEND=sdl`, without the
151
+ * shim), and on macOS and Windows without the `@gjsify/gamepad-native` prebuild. Reports that the BRIDGE is usable, not that every
152
+ * later call succeeds — a monitor can still fail to start (see
153
+ * {@link reportGamepadMonitorFault}).
89
154
  */
90
155
  export declare function hasGamepadBackend(): Promise<boolean>;
156
+ /**
157
+ * Which backend this process drives, in words — for a debug panel or a bug report, not
158
+ * for branching (use {@link hasGamepadBackend} for that). Quiet like the capability query.
159
+ */
160
+ export declare function describeGamepadBackend(): Promise<string>;
91
161
  /** Reset the cached probe and its one-time diagnostic — tests only. */
92
162
  export declare function _resetGamepadBackendCache(): void;
93
163
  export {};
@@ -0,0 +1,31 @@
1
+ import type { GamepadSource, GamepadSourceSink } from './source.js';
2
+ /** Where a comparison is written. `console.warn` unless a test captures it. */
3
+ export type CompareReport = (line: string) => void;
4
+ /**
5
+ * The primary drives the page; the shadow is compared against it.
6
+ *
7
+ * Devices are paired by model, then by connection order ({@link pairUp}). A count
8
+ * mismatch is itself reported.
9
+ *
10
+ * A difference is reported when it has held for two consecutive polls, and again
11
+ * only when the set of differences changes. Manette pushes from the main loop while
12
+ * SDL is pulled at the poll, so for one frame after a change the two legitimately
13
+ * disagree; a comparison that reported that would bury the real findings.
14
+ */
15
+ export declare class ComparingSource implements GamepadSource {
16
+ private readonly _primary;
17
+ private readonly _shadow;
18
+ private readonly _report;
19
+ readonly name: string;
20
+ readonly startRequirements?: string;
21
+ private _primarySink;
22
+ private _shadowSink;
23
+ private _shadowStarted;
24
+ private _pending;
25
+ private _reported;
26
+ constructor(_primary: GamepadSource, _shadow: GamepadSource, _report?: CompareReport);
27
+ start(sink: GamepadSourceSink): void;
28
+ poll(): void;
29
+ stop(): void;
30
+ private _compare;
31
+ }
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
@@ -1,31 +1,41 @@
1
1
  import { Gamepad } from './gamepad.js';
2
+ import type { GamepadSource } from './source.js';
3
+ export interface GamepadManagerOptions {
4
+ /**
5
+ * Drive this source instead of probing the host's backend. For tests — it is how the
6
+ * manager is exercised with no controller and no typelib — and for an embedder that
7
+ * already owns an input stack.
8
+ */
9
+ source?: GamepadSource;
10
+ }
2
11
  /**
3
- * Singleton manager wrapping `Manette.Monitor`. libmanette fires GObject signals on
4
- * button/axis changes, so this caches the latest state and `getGamepads()` returns a
5
- * snapshot, matching the W3C polling model.
12
+ * The W3C polling model over a {@link GamepadSource}: the source reports changes as they
13
+ * happen, this caches the latest state, and `getGamepads()` returns a snapshot.
6
14
  */
7
15
  export declare class GamepadManager {
8
- private _monitor;
9
16
  /**
10
17
  * The spec's `Navigator.[[gamepads]]` — an EMPTY list until something
11
18
  * connects, then one entry per index ever handed out, `null` where the slot
12
19
  * is free. Never pre-filled: see the note on {@link getGamepads}.
13
20
  */
14
21
  private _slots;
15
- private _monitorSignalIds;
16
- private _ManetteModule;
22
+ private readonly _injectedSource;
23
+ private _source;
24
+ /** Whether `_source.start()` RETURNED — a source whose start threw is never polled. */
25
+ private _started;
17
26
  private _initPromise;
18
27
  private _initialized;
19
- /** Lazily initialize the `Manette.Monitor`, on the first `getGamepads()`. */
28
+ /** The one sink every source reports through. */
29
+ private readonly _sink;
30
+ constructor(options?: GamepadManagerOptions);
31
+ /** Lazily start the device source, on the first `getGamepads()`. */
20
32
  private _ensureInit;
21
33
  private _init;
22
34
  private _onDeviceConnected;
23
35
  private _onDeviceDisconnected;
24
- private _onButtonPress;
25
- private _onButtonRelease;
26
- private _onAxisChange;
27
- private _onHatChange;
28
- private _findStateByDevice;
36
+ private _onButton;
37
+ private _onAxis;
38
+ private _findState;
29
39
  private _createSnapshot;
30
40
  /**
31
41
  * Returns a snapshot list matching the W3C `navigator.getGamepads()`
@@ -72,6 +82,6 @@ export declare class GamepadManager {
72
82
  * plus the one-time diagnostic this manager emits on its first init.
73
83
  */
74
84
  getGamepads(): (Gamepad | null)[];
75
- /** Cleanup — disconnect all signal handlers. */
85
+ /** Stop the source and drop every index. */
76
86
  dispose(): void;
77
87
  }
@@ -42,4 +42,8 @@ export interface GamepadEffectParameters {
42
42
  startDelay?: number;
43
43
  strongMagnitude?: number;
44
44
  weakMagnitude?: number;
45
+ /** `trigger-rumble` only: the left trigger's motor, 0..1. */
46
+ leftTrigger?: number;
47
+ /** `trigger-rumble` only: the right trigger's motor, 0..1. */
48
+ rightTrigger?: number;
45
49
  }
@@ -1,16 +1,67 @@
1
1
  import type Manette from '@girs/manette-0.2';
2
- import type { GamepadHapticActuator, GamepadHapticEffectType, GamepadEffectParameters, GamepadHapticsResult } from './gamepad.js';
2
+ import type { GamepadEffectParameters, GamepadHapticActuator, GamepadHapticEffectType, GamepadHapticsResult } from './gamepad.js';
3
+ import type { GjsifyGamepadDevice } from './sdl-namespace.js';
3
4
  /**
4
- * Wraps libmanette's rumble support as a W3C GamepadHapticActuator.
5
+ * The longest effect (`startDelay + duration`) accepted, in ms — the spec's recommended
6
+ * maximum, and what Chromium enforces. It is also below both backends' own cap (SDL:
7
+ * 0xFFFF ms; libmanette: a signed 16-bit ms count), so a valid effect is never cut short.
8
+ */
9
+ export declare const MAX_HAPTIC_EFFECT_DURATION_MS = 5000;
10
+ /** The motor intensities one effect asks for, each a uint16 both backends take. */
11
+ interface HapticMotors {
12
+ strong: number;
13
+ weak: number;
14
+ leftTrigger: number;
15
+ rightTrigger: number;
16
+ }
17
+ /**
18
+ * The W3C `playEffect()` / `reset()` algorithm, shared by every device source so a page
19
+ * behaves the same whichever backend its host has. A subclass only says which effects its
20
+ * device has and how to start and stop the motors.
5
21
  *
6
- * libmanette supports dual-rumble with strong/weak magnitude control
7
- * via `Device.rumble(strong_magnitude, weak_magnitude, milliseconds)`.
8
- * Magnitudes are in the range 0–65535 (uint16).
22
+ * What the spec asks for, and why each step is here rather than left to the device:
23
+ * - parameters default to 0 (the IDL's defaults) and are validated first: a magnitude
24
+ * outside 0..1 or an effect longer than {@link MAX_HAPTIC_EFFECT_DURATION_MS} rejects
25
+ * with a `TypeError` instead of being clamped into something the page did not ask for;
26
+ * - an effect the device cannot play rejects with `NotSupportedError` — resolving it
27
+ * `'complete'` would tell the page it rumbled when nothing moved;
28
+ * - the promise resolves when the effect has PLAYED (`startDelay + duration`), or
29
+ * `'preempted'` when a later `playEffect()` / `reset()` interrupts it — which is how a
30
+ * page sequences effects, and why resolving at once would break it.
9
31
  */
10
- export declare class ManetteHapticActuator implements GamepadHapticActuator {
11
- readonly effects: readonly GamepadHapticEffectType[];
12
- private _device;
13
- constructor(device: Manette.Device);
32
+ declare abstract class RumbleHapticActuator implements GamepadHapticActuator {
33
+ abstract readonly effects: readonly GamepadHapticEffectType[];
34
+ private _playing;
35
+ /** Start the motors for `durationMs`; the device stops them on its own afterwards. */
36
+ protected abstract _start(type: GamepadHapticEffectType, motors: HapticMotors, durationMs: number): void;
37
+ /** Stop every motor now. */
38
+ protected abstract _stop(): void;
14
39
  playEffect(type: GamepadHapticEffectType, params?: GamepadEffectParameters): Promise<GamepadHapticsResult>;
15
40
  reset(): Promise<GamepadHapticsResult>;
41
+ /** Stop the effect in flight, if any, and resolve its promise `'preempted'`. */
42
+ private _preempt;
43
+ }
44
+ /**
45
+ * libmanette's rumble as a W3C GamepadHapticActuator: `dual-rumble` only, via
46
+ * `Device.rumble(strong_magnitude, weak_magnitude, milliseconds)`.
47
+ */
48
+ export declare class ManetteHapticActuator extends RumbleHapticActuator {
49
+ private readonly _device;
50
+ readonly effects: readonly GamepadHapticEffectType[];
51
+ constructor(_device: Manette.Device);
52
+ protected _start(_type: GamepadHapticEffectType, motors: HapticMotors, durationMs: number): void;
53
+ protected _stop(): void;
54
+ }
55
+ /**
56
+ * The SDL3 shim's rumble as a W3C GamepadHapticActuator: `dual-rumble` drives the two
57
+ * body motors; `trigger-rumble` drives the impulse triggers (Xbox One/Series controllers)
58
+ * AND the body motors, since its parameters carry both.
59
+ */
60
+ export declare class SdlHapticActuator extends RumbleHapticActuator {
61
+ private readonly _device;
62
+ readonly effects: readonly GamepadHapticEffectType[];
63
+ constructor(_device: GjsifyGamepadDevice);
64
+ protected _start(type: GamepadHapticEffectType, motors: HapticMotors, durationMs: number): void;
65
+ protected _stop(): void;
16
66
  }
67
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -4,8 +4,10 @@ export type { GamepadMappingType, GamepadHapticActuator, GamepadHapticEffectType
4
4
  export { GamepadEvent } from './gamepad-event.js';
5
5
  export type { GamepadEventInit } from './gamepad-event.js';
6
6
  export { GamepadManager } from './gamepad-manager.js';
7
- export { hasGamepadBackend } from './backend.js';
8
- export type { GamepadBackendStatus } from './backend.js';
7
+ export type { GamepadManagerOptions } from './gamepad-manager.js';
8
+ export type { GamepadSource, GamepadSourceDevice, GamepadSourceSink } from './source.js';
9
+ export { describeGamepadBackend, hasGamepadBackend } from './backend.js';
10
+ export type { GamepadBackendChoice, GamepadBackendStatus } from './backend.js';
9
11
  export { ManetteHapticActuator } from './haptic-actuator.js';
10
12
  export { MANETTE_TO_W3C_BUTTON, ManetteButton, W3CButton, W3C_BUTTON_COUNT } from './button-mapping.js';
11
13
  export { MANETTE_TO_W3C_AXIS, ManetteAxis, W3CAxis, W3C_AXIS_COUNT, TRIGGER_PRESS_THRESHOLD } from './axis-mapping.js';
@@ -0,0 +1,19 @@
1
+ import type Manette from '@girs/manette-0.2';
2
+ import { type GamepadSource, type GamepadSourceSink } from './source.js';
3
+ /** `Manette.Monitor` as a {@link GamepadSource}. Push model: no `poll()`. */
4
+ export declare class ManetteSource implements GamepadSource {
5
+ private readonly _Manette;
6
+ readonly name = "gi://Manette";
7
+ readonly startRequirements = "Manette.Monitor needs udev and /dev/input access, which a sandbox can withhold (flatpak: --device=input).";
8
+ private _monitor;
9
+ private _monitorSignalIds;
10
+ private readonly _devices;
11
+ constructor(_Manette: typeof Manette);
12
+ start(sink: GamepadSourceSink): void;
13
+ stop(): void;
14
+ private _add;
15
+ private _remove;
16
+ private _onButton;
17
+ private _onAxis;
18
+ private _onHat;
19
+ }
@@ -1 +1 @@
1
- export {};
1
+ import '@gjsify/node-globals/register/navigator';
@@ -0,0 +1,28 @@
1
+ /** One controller: `GjsifyGamepad.Device`. */
2
+ export interface GjsifyGamepadDevice {
3
+ get_name(): string;
4
+ /** SDL's 32-hex-digit GUID; carries the USB vendor and product. */
5
+ get_guid(): string;
6
+ /** W3C standard buttons 0–16, 0..1 (the triggers 6/7 analog, the rest 0 or 1). */
7
+ get_buttons(): number[];
8
+ /** W3C standard axes 0–3, -1..1. */
9
+ get_axes(): number[];
10
+ has_rumble(): boolean;
11
+ has_trigger_rumble(): boolean;
12
+ rumble(lowFrequency: number, highFrequency: number, durationMs: number): boolean;
13
+ rumble_triggers(left: number, right: number, durationMs: number): boolean;
14
+ }
15
+ /** `GjsifyGamepad.Monitor` — the subset the source uses. */
16
+ export interface GjsifyGamepadMonitor {
17
+ update(): void;
18
+ close(): void;
19
+ connect(signal: 'device-added' | 'device-removed', callback: (monitor: GjsifyGamepadMonitor, device: GjsifyGamepadDevice) => void): number;
20
+ disconnect(id: number): void;
21
+ }
22
+ /** The `gi://GjsifyGamepad` namespace — the subset the source uses. */
23
+ export interface GjsifyGamepadNamespace {
24
+ /** A GI class: the probe checks `typeof Monitor === 'function'`. */
25
+ Monitor: {
26
+ new: () => GjsifyGamepadMonitor;
27
+ };
28
+ }
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
@@ -0,0 +1,29 @@
1
+ import type { GjsifyGamepadNamespace } from './sdl-namespace.js';
2
+ import { type GamepadSource, type GamepadSourceSink } from './source.js';
3
+ /**
4
+ * `GjsifyGamepad.Monitor` as a {@link GamepadSource}. Pull model: `poll()` pumps SDL and
5
+ * forwards the changed values, at exactly the W3C polling moment. Hotplug is also pushed
6
+ * between polls, by the monitor's own GLib timeout, so `gamepadconnected` reaches a page
7
+ * that has not polled yet.
8
+ */
9
+ export declare class SdlSource implements GamepadSource {
10
+ private readonly _GjsifyGamepad;
11
+ readonly name = "gi://GjsifyGamepad";
12
+ readonly startRequirements = "SDL's gamepad subsystem has to start in this process; the error carries SDL's own reason.";
13
+ private _monitor;
14
+ private _sink;
15
+ private _signalIds;
16
+ private readonly _devices;
17
+ constructor(_GjsifyGamepad: GjsifyGamepadNamespace);
18
+ start(sink: GamepadSourceSink): void;
19
+ poll(): void;
20
+ stop(): void;
21
+ private _add;
22
+ private _remove;
23
+ /**
24
+ * Forward the values that changed since the sink last heard. Unchanged values are
25
+ * not re-sent because every sink call stamps `Gamepad.timestamp`, which the W3C
26
+ * spec advances only when the device reported new data.
27
+ */
28
+ private _report;
29
+ }
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
@@ -0,0 +1,71 @@
1
+ import type { GamepadHapticActuator } from './gamepad.js';
2
+ /**
3
+ * `vvvv:pppp` from an SDL-style joystick GUID (libmanette and SDL both use that layout:
4
+ * little-endian 16-bit bus, CRC, vendor, 0, product, …), or `undefined` when the GUID
5
+ * carries no vendor (SDL then stores a name hash there instead).
6
+ */
7
+ export declare function modelFromGuid(guid: string | null | undefined): string | undefined;
8
+ /**
9
+ * One physical device, as a source reports it. The OBJECT is the identity: the manager
10
+ * keys its slot on it, so a source must hand the same object to every call about the
11
+ * same device.
12
+ */
13
+ export interface GamepadSourceDevice {
14
+ /** Becomes `Gamepad.id`. */
15
+ readonly id: string;
16
+ /** Becomes `Gamepad.vibrationActuator`; `null` when the source cannot drive one. */
17
+ readonly vibrationActuator: GamepadHapticActuator | null;
18
+ /**
19
+ * The controller MODEL as `vvvv:pppp` (USB vendor and product, hex), read from the
20
+ * SDL-style GUID both libmanette and SDL report. Optional: only `ComparingSource`
21
+ * reads it, to pair one controller across two backends that name it differently.
22
+ */
23
+ readonly model?: string;
24
+ }
25
+ /**
26
+ * Where a source reports to, in the W3C STANDARD mapping's vocabulary
27
+ * (https://w3c.github.io/gamepad/#remapping): `index` is a standard button or axis
28
+ * index, not a platform code. Calls about a device that is not connected, or with an
29
+ * index outside the standard layout, are ignored — a source may forward what it does not
30
+ * recognise without a guard of its own.
31
+ */
32
+ export interface GamepadSourceSink {
33
+ /** A device became available — selects an index for it and fires `gamepadconnected`. */
34
+ connected(device: GamepadSourceDevice): void;
35
+ /** The device went away — frees its index and fires `gamepaddisconnected`. */
36
+ disconnected(device: GamepadSourceDevice): void;
37
+ /**
38
+ * A button's state. `value` is 0..1; `pressed` is passed explicitly because only the
39
+ * source knows whether the control is digital (pressed ⇔ value 1) or analog, where
40
+ * the threshold is its call.
41
+ */
42
+ button(device: GamepadSourceDevice, index: number, value: number, pressed: boolean): void;
43
+ /** A stick axis, -1..1. Analog triggers are BUTTONS 6/7 in the standard layout. */
44
+ axis(device: GamepadSourceDevice, index: number, value: number): void;
45
+ }
46
+ /** A platform's gamepad subsystem, behind the one contract the manager drives. */
47
+ export interface GamepadSource {
48
+ /** Names the backend in diagnostics, e.g. `gi://Manette`. */
49
+ readonly name: string;
50
+ /**
51
+ * One sentence on what this backend needs to start, appended to the report when
52
+ * {@link start} throws. That report must not guess: udev advice for a darwin backend
53
+ * would be wrong advice.
54
+ */
55
+ readonly startRequirements?: string;
56
+ /**
57
+ * Begin reporting to `sink`. Devices that are already present are reported as
58
+ * `connected` too. May throw — the manager reports that as a monitor fault, distinct
59
+ * from a backend that failed to load. Called again after {@link stop} when a disposed
60
+ * manager is used again.
61
+ */
62
+ start(sink: GamepadSourceSink): void;
63
+ /**
64
+ * Refresh device state. A PULL backend (SDL is polled) implements it; the manager
65
+ * calls it at the top of every `getGamepads()`, which is exactly the W3C polling
66
+ * moment. A PUSH backend (Manette emits signals) omits it.
67
+ */
68
+ poll?(): void;
69
+ /** Stop reporting and release every native handle. Must be safe after a failed start. */
70
+ stop(): void;
71
+ }
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gjsify/gamepad",
3
- "version": "0.52.0",
4
- "description": "Gamepad Web API for GJS using libmanette as backend",
3
+ "version": "0.54.0",
4
+ "description": "Gamepad Web API for GJS: SDL3 (via @gjsify/gamepad-native) on macOS and Windows, libmanette on Linux until SDL is proven there",
5
5
  "type": "module",
6
6
  "module": "lib/esm/index.js",
7
7
  "types": "lib/types/index.d.ts",
@@ -25,35 +25,44 @@
25
25
  "./globals.mjs"
26
26
  ],
27
27
  "scripts": {
28
- "clear": "gjsify clear lib tsconfig.tsbuildinfo tsconfig.types.tsbuildinfo test.gjs.mjs",
28
+ "clear": "gjsify clear lib tsconfig.tsbuildinfo tsconfig.types.tsbuildinfo test.gjs.mjs test.hardware-check.gjs.mjs",
29
29
  "check": "gjsify tsc --noEmit",
30
30
  "build": "gjsify run build:gjsify && gjsify run build:types",
31
- "build:gjsify": "gjsify build --library 'src/**/*.{ts,js}' --exclude 'src/**/*.spec.{mts,ts}' 'src/test.{mts,ts}'",
31
+ "build:gjsify": "gjsify build --library 'src/**/*.{ts,js}' --exclude 'src/**/*.spec.{mts,ts}' 'src/test*.{mts,ts}'",
32
32
  "build:types": "gjsify tsc",
33
33
  "build:test": "gjsify run build:test:gjs",
34
34
  "build:test:gjs": "gjsify build src/test.mts --app gjs --outfile test.gjs.mjs",
35
35
  "build:test:browser": "gjsify build src/test.browser.mts --app browser --outfile dist/test.browser.mjs",
36
36
  "test": "gjsify run build:gjsify && gjsify run build:test && gjsify run test:gjs",
37
- "test:gjs": "gjsify run test.gjs.mjs"
37
+ "test:gjs": "gjsify run test.gjs.mjs",
38
+ "hardware-check": "gjsify build src/hardware-check.mts --app gjs --outfile test.hardware-check.gjs.mjs && gjsify run test.hardware-check.gjs.mjs",
39
+ "test:gjs-on-node": "gjsify build src/test.node-gi.mts --app node --outfile dist/test.node-gi.mjs && node dist/test.node-gi.mjs"
38
40
  },
39
41
  "keywords": [
40
42
  "gjs",
41
43
  "gamepad",
42
44
  "libmanette",
45
+ "sdl3",
43
46
  "controller"
44
47
  ],
45
48
  "dependencies": {
46
- "@gjsify/dom-events": "^0.52.0"
49
+ "@gjsify/dom-events": "^0.54.0",
50
+ "@gjsify/node-globals": "^0.54.0",
51
+ "@gjsify/utils": "^0.54.0"
47
52
  },
48
53
  "devDependencies": {
49
54
  "@girs/gjs": "5.4.0",
50
55
  "@girs/glib-2.0": "5.4.0",
51
56
  "@girs/manette-0.2": "5.4.0",
52
- "@gjsify/cli": "^0.52.0",
53
- "@gjsify/unit": "^0.52.0",
57
+ "@gjsify/cli": "^0.54.0",
58
+ "@gjsify/node-gi": "file:../../../packages/node-gi/node-gi",
59
+ "@gjsify/unit": "^0.54.0",
54
60
  "@types/node": "^25.9.2",
55
61
  "typescript": "^6.0.3"
56
62
  },
63
+ "optionalDependencies": {
64
+ "@gjsify/gamepad-native": "^0.54.0"
65
+ },
57
66
  "gjsify": {
58
67
  "runtimes": {
59
68
  "gjs": "polyfill",
@@ -61,6 +70,15 @@
61
70
  "browser": "native",
62
71
  "nativescript": "native"
63
72
  },
73
+ "os": {
74
+ "linux": "supported",
75
+ "darwin": "partial",
76
+ "win32": "partial"
77
+ },
78
+ "osNotes": {
79
+ "darwin": "The backend is SDL3 behind a GObject shim (@gjsify/gamepad-native, ADR 0075). It builds, loads and enumerates on macOS, but input from a real controller has not been verified yet, and its darwin prebuilds are not published yet. Without the prebuild, getGamepads() answers the conformant empty list and hasGamepadBackend() reports false.",
80
+ "win32": "The backend is the SDL3 shim (@gjsify/gamepad-native-win32-x64, ADR 0075 Amendment 1), reached from Node through @gjsify/node-gi since Windows has no GJS. It builds, loads and enumerates in CI, and a virtual XInput pad is read there, but input from a real controller has not been verified yet and the win32 prebuild is not published yet. Without it, getGamepads() answers the conformant empty list and hasGamepadBackend() reports false."
81
+ },
64
82
  "tier": 1
65
83
  },
66
84
  "license": "MIT",