@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 +26 -1
- package/globals.mjs +27 -2
- package/lib/esm/backend.js +1 -0
- package/lib/esm/gamepad-manager.js +1 -1
- package/lib/esm/index.js +1 -1
- package/lib/esm/register.js +1 -1
- package/lib/types/backend.d.ts +108 -0
- package/lib/types/backend.spec.d.ts +2 -0
- package/lib/types/gamepad-manager.d.ts +47 -2
- package/lib/types/index.d.ts +2 -0
- package/lib/types/register.spec.d.ts +3 -0
- package/package.json +4 -4
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
|
|
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{
|
|
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
|
|
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};
|
package/lib/esm/register.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
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 {};
|
|
@@ -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
|
|
32
|
-
* Each non-null entry is a frozen Gamepad object with current
|
|
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. */
|
package/lib/types/index.d.ts
CHANGED
|
@@ -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';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/gamepad",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
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.
|
|
53
|
-
"@gjsify/unit": "^0.
|
|
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
|
},
|