@gjsify/gamepad 0.27.1 → 0.29.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 and degrades gracefully when libmanette is unavailable.
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.
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,6 +39,31 @@ for (const pad of gamepads) {
39
39
  }
40
40
  ```
41
41
 
42
+ ## Platform support — hosts without libmanette
43
+
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.
45
+
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`.
47
+
48
+ Ask the capability export instead of guessing from an empty list:
49
+
50
+ ```typescript
51
+ import { hasGamepadBackend } from '@gjsify/gamepad';
52
+
53
+ if (!(await hasGamepadBackend())) {
54
+ // No gamepad subsystem on this host — hide the controller UI rather than
55
+ // showing "no controller connected" forever.
56
+ }
57
+ ```
58
+
59
+ `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
+
61
+ * **no backend** (no Manette typelib; or `@gjsify/node-gi` not installed on the node target) → one `console.warn` naming what to install.
62
+ * **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
+ * **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
+
65
+ 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
+
42
67
  ## License
43
68
 
44
69
  MIT
package/globals.mjs CHANGED
@@ -5,12 +5,37 @@
5
5
  * the libmanette GJS backend stays GJS-only.
6
6
  *
7
7
  * The dynamic resolver in `@gjsify/resolve-npm/runtime-aliases.mjs` routes
8
- * `@gjsify/gamepad` here when `package.json#gjsify.runtimes.browser === "native"`.
8
+ * `@gjsify/gamepad` here when `package.json#gjsify.runtimes.browser === "native"`
9
+ * — and the same rule routes it here for `nativescript`. That makes this file the
10
+ * NON-GJS entry for the package root, so every root export a consumer may import
11
+ * has to be answerable from here or the routed bundle dies with `MISSING_EXPORT`.
9
12
  *
10
- * NOT used on Node — Node has no Gamepad global.
13
+ * NOT used on Node — Node has no Gamepad global (`gjsify.runtimes.node` is
14
+ * `"partial"`, which is not rewritten: the real GJS body runs through
15
+ * `@gjsify/node-gi`).
11
16
  */
12
17
 
13
18
  export const Gamepad = globalThis.Gamepad;
14
19
  export const GamepadButton = globalThis.GamepadButton;
15
20
  export const GamepadEvent = globalThis.GamepadEvent;
16
21
  export const GamepadHapticActuator = globalThis.GamepadHapticActuator;
22
+
23
+ /**
24
+ * Whether this host has a gamepad backend at all — the same question the GJS
25
+ * entry answers about `gi://Manette`, answered here about the runtime's own
26
+ * Gamepad API, which on these targets IS the implementation.
27
+ *
28
+ * Kept REAL rather than stubbed or omitted: the answer is platform-independent in
29
+ * the sense AGENTS.md means (§ Slot routing — "Exports whose answer IS
30
+ * platform-independent stay REAL", as `hasOcspSupport()` → `false` does on
31
+ * `@gjsify/tls`'s browser entry). Omitting it broke the README's own recommended
32
+ * usage at BUILD time on `--app browser` and `--app nativescript`; a thrower
33
+ * would break it at run time. `true` in a browser; `false` on NativeScript, which
34
+ * ships no Gamepad API — both are the honest answer for that host.
35
+ *
36
+ * `async` to match the root entry's `Promise<boolean>` signature, where the
37
+ * answer needs a dynamic `import('gi://Manette')`; here it is already known.
38
+ */
39
+ export async function hasGamepadBackend() {
40
+ return typeof globalThis.navigator?.getGamepads === 'function';
41
+ }
@@ -0,0 +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`)),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 +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{GamepadButton as a}from"./gamepad-button.js";import{GamepadEvent as o}from"./gamepad-event.js";import{Gamepad as s}from"./gamepad.js";import{ManetteHapticActuator as c}from"./haptic-actuator.js";var GamepadManager=class{_monitor=null;_slots=Array.from({length:4}).fill(null);_monitorSignalIds=[];_ManetteModule=null;_initPromise=null;_initialized=!1;_ensureInit(){this._initialized||(this._initPromise||=this._init())}async _init(){try{let e=await import(`gi://Manette`);this._ManetteModule=e.default}catch{this._initialized=!0;return}let e=new this._ManetteModule.Monitor;this._monitor=e;let t=e.iterate(),n=t.next();for(;n[0];){let e=n[1];e&&this._onDeviceConnected(e),n=t.next()}this._monitorSignalIds.push(e.connect(`device-connected`,(e,t)=>{this._onDeviceConnected(t)}),e.connect(`device-disconnected`,(e,t)=>{this._onDeviceDisconnected(t)})),this._initialized=!0}_onDeviceConnected(e){let t=-1;for(let e=0;e<4;e++)if(this._slots[e]===null){t=e;break}if(t===-1)return;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 c(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 o(`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 o(`gamepaddisconnected`,{gamepad:n}))}_onButtonPress(e,t){let[n,i]=t.get_button();if(!n)return;let a=r.get(i);a!==void 0&&(e.buttons[a]=1,e.buttonsPressed[a]=!0,e.timestamp=performance.now())}_onButtonRelease(e,t){let[n,i]=t.get_button();if(!n)return;let a=r.get(i);a!==void 0&&(e.buttons[a]=0,e.buttonsPressed[a]=!1,e.timestamp=performance.now())}_onAxisChange(r,a){let[o,s,c]=a.get_absolute();if(!o)return;let l=e.get(s);if(l!==void 0)r.axes[l]=c;else if(s===t.LEFT_TRIGGER){let e=(c+1)/2;r.buttons[i.LEFT_TRIGGER]=e,r.buttonsPressed[i.LEFT_TRIGGER]=e>n}else if(s===t.RIGHT_TRIGGER){let e=(c+1)/2;r.buttons[i.RIGHT_TRIGGER]=e,r.buttonsPressed[i.RIGHT_TRIGGER]=e>n}r.timestamp=performance.now()}_onHatChange(e,t){let[n,r,a]=t.get_hat();n&&(r===0?(e.buttonsPressed[i.DPAD_LEFT]=a<0,e.buttons[i.DPAD_LEFT]=+(a<0),e.buttonsPressed[i.DPAD_RIGHT]=a>0,e.buttons[i.DPAD_RIGHT]=+(a>0)):r===1&&(e.buttonsPressed[i.DPAD_UP]=a<0,e.buttons[i.DPAD_UP]=+(a<0),e.buttonsPressed[i.DPAD_DOWN]=a>0,e.buttons[i.DPAD_DOWN]=+(a>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 a(e.buttonsPressed[n],e.buttonsPressed[n]||e.buttons[n]>0,e.buttons[n]));return new s({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(){this._ensureInit();let e=[];for(let t=0;t<4;t++){let n=this._slots[t];e.push(n?this._createSnapshot(n):null)}return e}dispose(){for(let e of this._slots)if(e)for(let t of e.signalIds)e.device.disconnect(t);if(this._slots.fill(null),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{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};
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{LinuxButton as a,MANETTE_TO_W3C_BUTTON as o,W3CButton as s,W3C_BUTTON_COUNT as c}from"./button-mapping.js";import{GamepadButton as l}from"./gamepad-button.js";import{GamepadEvent as u}from"./gamepad-event.js";import{Gamepad as d}from"./gamepad.js";import{ManetteHapticActuator as f}from"./haptic-actuator.js";import{GamepadManager as p}from"./gamepad-manager.js";export{d as Gamepad,l as GamepadButton,u as GamepadEvent,p as GamepadManager,e as MANETTE_TO_W3C_AXIS,o as MANETTE_TO_W3C_BUTTON,t as ManetteAxis,a as ManetteButton,f as ManetteHapticActuator,n as TRIGGER_PRESS_THRESHOLD,r as W3CAxis,s as W3CButton,i as W3C_AXIS_COUNT,c as W3C_BUTTON_COUNT};
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 +1 @@
1
- import"./_virtual/_rolldown/runtime.js";import{GamepadEvent as e}from"./gamepad-event.js";import{GamepadManager as t}from"./gamepad-manager.js";const n=new t,r=globalThis;r.navigator===void 0&&(r.navigator={}),r.navigator.getGamepads=()=>n.getGamepads(),r.GamepadEvent===void 0&&(r.GamepadEvent=e);
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);
@@ -0,0 +1,108 @@
1
+ import type Manette from '@girs/manette-0.2';
2
+ /**
3
+ * What this host has.
4
+ *
5
+ * - `manette` — the Manette-0.2 typelib loaded and exposes `Monitor`.
6
+ * - `absent` — no backend reachable from here. Expected on a host without
7
+ * libmanette, on a `--app node` process without `@gjsify/node-gi`, and on the
8
+ * two targets where `gi://` is stubbed by design.
9
+ * - `failed` — a backend should have been reachable and was not. A fault.
10
+ */
11
+ export type GamepadBackendStatus = 'manette' | 'absent' | 'failed';
12
+ /** The resolved backend probe. `module` is non-null iff `status === 'manette'`. */
13
+ export interface GamepadBackend {
14
+ status: GamepadBackendStatus;
15
+ module: typeof Manette | null;
16
+ /** The original load error for `absent`/`failed`; `null` otherwise. */
17
+ error: unknown;
18
+ /**
19
+ * The one-time line the USE site emits for this outcome, or `null` when
20
+ * there is nothing to say. Carried rather than printed so the capability
21
+ * query stays silent — see the module header.
22
+ *
23
+ * `status: 'absent'` with a `null` diagnostic is the by-design `gi://` stub
24
+ * (see {@link isEmptiedGiModule}); `status: 'failed'` always prints, with
25
+ * the original error, and needs no text here.
26
+ */
27
+ diagnostic: string | null;
28
+ }
29
+ export interface LoadGamepadBackendOptions {
30
+ /**
31
+ * Override the `gi://Manette` import.
32
+ *
33
+ * Only for tests — it is the single seam that lets a suite exercise the
34
+ * absent and failed paths on a host where libmanette IS installed, which is
35
+ * every CI runner this package is tested on. Same role as
36
+ * `fillRandomBytes({ webcrypto })` in `@gjsify/webcrypto/random`.
37
+ */
38
+ importer?: () => Promise<{
39
+ default: typeof Manette;
40
+ }>;
41
+ }
42
+ /** Absent-vs-fault plus the text the use site should print (`null` = silence). */
43
+ interface GiLoadDiagnosis {
44
+ status: 'absent' | 'failed';
45
+ diagnostic: string | null;
46
+ }
47
+ /**
48
+ * Classify a `gi://` load failure and pick the text for it.
49
+ *
50
+ * Exported (underscored) for `backend.spec.ts`, which pins the absent wording
51
+ * against the LIVE loader — that is why the namespace is a parameter and not
52
+ * hardcoded: the test asks for a namespace that cannot exist and classifies the
53
+ * error the running GJS/girepository actually produced for it.
54
+ */
55
+ export declare function _diagnoseGiLoadError(error: unknown, namespace?: string): GiLoadDiagnosis;
56
+ /**
57
+ * Resolve (once) which gamepad backend this host has. Emits NOTHING — see the
58
+ * module header on why the diagnostic belongs to the use site.
59
+ *
60
+ * The probe runs at most once per process. Passing `importer` after it has run
61
+ * is a test-setup bug and THROWS rather than being silently ignored: the old
62
+ * `cached ??= probe(options)` let a suite that forgot `_resetGamepadBackendCache()`
63
+ * assert against the PREVIOUS test's injected module and pass.
64
+ */
65
+ export declare function loadGamepadBackend(options?: LoadGamepadBackendOptions): Promise<GamepadBackend>;
66
+ /**
67
+ * Emit this backend's one-time diagnostic — called by the USE site
68
+ * (`GamepadManager._init()`), never by the capability query.
69
+ *
70
+ * `absent` warns (a platform gap, expected, but never silent) and `failed`
71
+ * errors with the original attached: two levels for two situations, which is
72
+ * what makes the split visible rather than merely present. Deliberately not a
73
+ * rethrow — `GamepadManager` starts its init without awaiting it, so a rethrow
74
+ * becomes an unhandled rejection: unattributable on GJS and, with Node's default
75
+ * `--unhandled-rejections=throw`, a process kill. A half-installed typelib must
76
+ * not be able to take down the host application.
77
+ */
78
+ export declare function reportGamepadBackendOnce(backend: GamepadBackend): void;
79
+ /**
80
+ * The backend loaded and then the monitor did not come up.
81
+ *
82
+ * A DIFFERENT failure from a failed load, and it says so: everything after the
83
+ * probe — `new Manette.Monitor()`, the device walk, `connect()` — needs udev and
84
+ * `/dev/input`, which a sandbox can withhold from a process whose typelib and
85
+ * shared library are both perfectly fine. Routing it through the load-failure
86
+ * text would re-create, one layer up, exactly the conflation this module removed.
87
+ */
88
+ export declare function reportGamepadMonitorFault(error: unknown): void;
89
+ /**
90
+ * Whether this host has a gamepad backend at all.
91
+ *
92
+ * `false` means `navigator.getGamepads()` can never report a controller here, no
93
+ * matter what is connected — which the W3C surface itself cannot express (it
94
+ * returns the same list either way, and that is correct: see
95
+ * `GamepadManager.getGamepads()`). Answerable without constructing a monitor or
96
+ * plugging anything in, and QUIET: the diagnostic fires from the use, not from
97
+ * this question.
98
+ *
99
+ * It is `false` on every host without libmanette, which today means macOS and
100
+ * Windows: libmanette links libevdev unconditionally, and libevdev is
101
+ * Linux/FreeBSD-only. Like `hasNativeSab()` and `hasOcspSupport()` this reports
102
+ * that the BRIDGE is usable, not that every later call will succeed — a monitor
103
+ * can still fail to start (see {@link reportGamepadMonitorFault}).
104
+ */
105
+ export declare function hasGamepadBackend(): Promise<boolean>;
106
+ /** Reset the cached probe and its one-time diagnostic — tests only. */
107
+ export declare function _resetGamepadBackendCache(): void;
108
+ export {};
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
@@ -8,6 +8,11 @@ import { Gamepad } from './gamepad.js';
8
8
  */
9
9
  export declare class GamepadManager {
10
10
  private _monitor;
11
+ /**
12
+ * The spec's `Navigator.[[gamepads]]` — an EMPTY list until something
13
+ * connects, then one entry per index ever handed out, `null` where the slot
14
+ * is free. Never pre-filled: see the note on {@link getGamepads}.
15
+ */
11
16
  private _slots;
12
17
  private _monitorSignalIds;
13
18
  private _ManetteModule;
@@ -28,8 +33,48 @@ export declare class GamepadManager {
28
33
  private _findStateByDevice;
29
34
  private _createSnapshot;
30
35
  /**
31
- * Returns a snapshot array matching the W3C `navigator.getGamepads()` contract.
32
- * Each non-null entry is a frozen Gamepad object with current state.
36
+ * Returns a snapshot list matching the W3C `navigator.getGamepads()`
37
+ * contract. Each non-null entry is a frozen Gamepad object with current
38
+ * state.
39
+ *
40
+ * ## The list is EMPTY until something connects — not four nulls
41
+ *
42
+ * `Navigator.[[gamepads]]` "is initially the empty list" and grows only when
43
+ * an index is selected for a connected gamepad (W3C Gamepad, § Extensions to
44
+ * the `Navigator` Interface + § Selecting an unused gamepad index); step 6 of
45
+ * `getGamepads()` starts from an empty list and step 7 copies `[[gamepads]]`
46
+ * into it. So on a host where nothing has ever connected the conformant answer
47
+ * is `[]`, and that is what this returns.
48
+ *
49
+ * The pre-filled four-slot array this used to return is CHROME's shape, not
50
+ * the spec's, and the difference is measured rather than assumed — same page
51
+ * (`about:blank`), same machine, no controller attached:
52
+ *
53
+ * firefox → {"length":0,"json":"[]"}
54
+ * chromium → {"length":4,"json":"[null,null,null,null]"}
55
+ *
56
+ * WebKit agrees with Firefox by construction:
57
+ * `NavigatorGamepad::gamepads()` does `if (m_gamepads.isEmpty()) return
58
+ * m_gamepads;` (`Source/WebCore/Modules/gamepad/NavigatorGamepad.cpp`).
59
+ * Hard-coding four made `getGamepads().length` report four non-existent ports.
60
+ *
61
+ * ## On a host with NO gamepad backend it stays a list — do NOT make it throw
62
+ *
63
+ * The W3C Gamepad API has no state for "this platform has no gamepad
64
+ * subsystem". `getGamepads()`'s steps only ever *return a list*; the single
65
+ * `throw` in them is a `SecurityError` for the `"gamepad"` permission policy,
66
+ * and every other negative case — no `Document`, no user gesture, nothing
67
+ * connected — is spelled "return an empty list". A real browser on a machine
68
+ * with no gamepad driver does exactly that, structurally: WebKit compiles an
69
+ * `EmptyGamepadProvider` (`Source/WebCore/platform/gamepad/`) whose
70
+ * `platformGamepads()` returns a static empty vector on every port with no
71
+ * backend.
72
+ *
73
+ * So throwing here would be LESS conformant, not more: it would break every
74
+ * page that does `navigator.getGamepads().length`, which is the canonical
75
+ * poll. The reason behind an empty answer is exposed NEXT TO the conformant
76
+ * surface instead — `hasGamepadBackend()` (exported from the package root)
77
+ * plus the one-time diagnostic this manager emits on its first init.
33
78
  */
34
79
  getGamepads(): (Gamepad | null)[];
35
80
  /** Cleanup — disconnect all signal handlers. */
@@ -4,6 +4,8 @@ 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
9
  export { ManetteHapticActuator } from './haptic-actuator.js';
8
10
  export { MANETTE_TO_W3C_BUTTON, ManetteButton, W3CButton, W3C_BUTTON_COUNT } from './button-mapping.js';
9
11
  export { MANETTE_TO_W3C_AXIS, ManetteAxis, W3CAxis, W3C_AXIS_COUNT, TRIGGER_PRESS_THRESHOLD } from './axis-mapping.js';
@@ -0,0 +1,3 @@
1
+ import '@gjsify/gamepad/register';
2
+ declare const _default: () => Promise<void>;
3
+ export default _default;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/gamepad",
3
- "version": "0.27.1",
3
+ "version": "0.29.0",
4
4
  "description": "Gamepad Web API for GJS using libmanette as backend",
5
5
  "type": "module",
6
6
  "module": "lib/esm/index.js",
@@ -43,14 +43,14 @@
43
43
  "controller"
44
44
  ],
45
45
  "dependencies": {
46
- "@gjsify/dom-events": "^0.27.1"
46
+ "@gjsify/dom-events": "^0.29.0"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@girs/gjs": "^4.1.0",
50
50
  "@girs/glib-2.0": "^4.1.0",
51
51
  "@girs/manette-0.2": "^4.1.0",
52
- "@gjsify/cli": "^0.27.1",
53
- "@gjsify/unit": "^0.27.1",
52
+ "@gjsify/cli": "^0.29.0",
53
+ "@gjsify/unit": "^0.29.0",
54
54
  "@types/node": "^25.9.2",
55
55
  "typescript": "^6.0.3"
56
56
  },