dualsense-ts 2.2.0 → 3.0.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.
Files changed (124) hide show
  1. package/README.md +9 -2
  2. package/dist/comparators.d.ts +13 -0
  3. package/dist/comparators.d.ts.map +1 -0
  4. package/dist/comparators.js +27 -0
  5. package/dist/comparators.js.map +1 -0
  6. package/dist/dualsense.d.ts +10 -1
  7. package/dist/dualsense.d.ts.map +1 -1
  8. package/dist/dualsense.js +46 -45
  9. package/dist/dualsense.js.map +1 -1
  10. package/dist/elements/analog.d.ts +1 -1
  11. package/dist/elements/analog.d.ts.map +1 -1
  12. package/dist/elements/analog.js +13 -3
  13. package/dist/elements/analog.js.map +1 -1
  14. package/dist/elements/dpad.d.ts +1 -1
  15. package/dist/elements/dpad.d.ts.map +1 -1
  16. package/dist/elements/dpad.js +6 -5
  17. package/dist/elements/dpad.js.map +1 -1
  18. package/dist/elements/touch.d.ts +1 -1
  19. package/dist/elements/touch.d.ts.map +1 -1
  20. package/dist/elements/touch.js.map +1 -1
  21. package/dist/elements/touchpad.d.ts +1 -1
  22. package/dist/elements/touchpad.d.ts.map +1 -1
  23. package/dist/elements/touchpad.js.map +1 -1
  24. package/dist/elements/trigger.d.ts.map +1 -1
  25. package/dist/elements/trigger.js +1 -1
  26. package/dist/elements/trigger.js.map +1 -1
  27. package/dist/elements/unisense.d.ts +1 -1
  28. package/dist/elements/unisense.d.ts.map +1 -1
  29. package/dist/elements/unisense.js +12 -5
  30. package/dist/elements/unisense.js.map +1 -1
  31. package/dist/hid/dualsense_hid.d.ts +16 -58
  32. package/dist/hid/dualsense_hid.d.ts.map +1 -1
  33. package/dist/hid/dualsense_hid.js +25 -101
  34. package/dist/hid/dualsense_hid.js.map +1 -1
  35. package/dist/hid/hid_provider.d.ts +86 -0
  36. package/dist/hid/hid_provider.d.ts.map +1 -0
  37. package/dist/hid/hid_provider.js +45 -0
  38. package/dist/hid/hid_provider.js.map +1 -0
  39. package/dist/hid/index.d.ts +4 -1
  40. package/dist/hid/index.d.ts.map +1 -1
  41. package/dist/hid/index.js +4 -1
  42. package/dist/hid/index.js.map +1 -1
  43. package/dist/hid/node_hid_provider.d.ts +11 -0
  44. package/dist/hid/node_hid_provider.d.ts.map +1 -0
  45. package/dist/hid/node_hid_provider.js +97 -0
  46. package/dist/hid/node_hid_provider.js.map +1 -0
  47. package/dist/hid/platform_hid_provider.d.ts +4 -0
  48. package/dist/hid/platform_hid_provider.d.ts.map +1 -0
  49. package/dist/hid/platform_hid_provider.js +7 -0
  50. package/dist/hid/platform_hid_provider.js.map +1 -0
  51. package/dist/hid/web_hid_provider.d.ts +10 -0
  52. package/dist/hid/web_hid_provider.d.ts.map +1 -0
  53. package/dist/hid/web_hid_provider.js +102 -0
  54. package/dist/hid/web_hid_provider.js.map +1 -0
  55. package/dist/{hid/ids.d.ts → id.d.ts} +6 -2
  56. package/dist/id.d.ts.map +1 -0
  57. package/dist/{hid/ids.js → id.js} +1 -1
  58. package/dist/id.js.map +1 -0
  59. package/dist/index.d.ts +2 -0
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +2 -0
  62. package/dist/index.js.map +1 -1
  63. package/dist/input.d.ts +81 -36
  64. package/dist/input.d.ts.map +1 -1
  65. package/dist/input.js +128 -89
  66. package/dist/input.js.map +1 -1
  67. package/docs/Analog.md +81 -131
  68. package/docs/AnalogParams.md +7 -9
  69. package/docs/Axis.md +72 -79
  70. package/docs/Brightness.md +8 -8
  71. package/docs/CommandScopeA.md +16 -16
  72. package/docs/CommandScopeB.md +14 -14
  73. package/docs/Dpad.md +74 -81
  74. package/docs/DpadParams.md +8 -8
  75. package/docs/DualSenseCommand.md +45 -45
  76. package/docs/Dualsense.md +83 -90
  77. package/docs/DualsenseHID.md +47 -42
  78. package/docs/DualsenseHIDState.md +41 -41
  79. package/docs/DualsenseParams.md +17 -17
  80. package/docs/Exports.md +18 -21
  81. package/docs/Haptic.md +1 -1
  82. package/docs/Home.md +29 -31
  83. package/docs/Indicator.md +2 -2
  84. package/docs/Input.md +70 -79
  85. package/docs/InputId.md +84 -84
  86. package/docs/InputParams.md +4 -4
  87. package/docs/LedOptions.md +10 -10
  88. package/docs/Momentary.md +70 -77
  89. package/docs/Motion.md +2 -2
  90. package/docs/Mute.md +71 -89
  91. package/docs/PlayerID.md +12 -12
  92. package/docs/PulseOptions.md +8 -8
  93. package/docs/Touchpad.md +120 -94
  94. package/docs/Trigger.md +75 -82
  95. package/docs/TriggerMode.md +22 -22
  96. package/docs/Unisense.md +74 -81
  97. package/docs/UnisenseParams.md +7 -7
  98. package/package.json +16 -10
  99. package/src/comparators.ts +26 -0
  100. package/src/dualsense.ts +61 -58
  101. package/src/elements/analog.spec.ts +17 -0
  102. package/src/elements/analog.ts +14 -8
  103. package/src/elements/axis.spec.ts +31 -0
  104. package/src/elements/dpad.ts +7 -6
  105. package/src/elements/touch.ts +1 -1
  106. package/src/elements/touchpad.ts +1 -1
  107. package/src/elements/trigger.ts +1 -1
  108. package/src/elements/unisense.ts +16 -15
  109. package/src/hid/dualsense_hid.ts +25 -156
  110. package/src/hid/{dualsense_hid.spec.ts → hid_provider.spec.ts} +1 -1
  111. package/src/hid/hid_provider.ts +100 -0
  112. package/src/hid/index.ts +4 -1
  113. package/src/hid/node_hid_provider.ts +108 -0
  114. package/src/hid/platform_hid_provider.ts +4 -0
  115. package/src/hid/web_hid_provider.ts +116 -0
  116. package/src/{hid/ids.ts → id.ts} +6 -1
  117. package/src/index.ts +2 -0
  118. package/src/input.ts +178 -142
  119. package/src/readme.spec.ts +6 -8
  120. package/webpack.config.js +42 -0
  121. package/dist/hid/ids.d.ts.map +0 -1
  122. package/dist/hid/ids.js.map +0 -1
  123. package/docs/Increment.md +0 -1554
  124. package/docs/Touch.md +0 -1824
package/src/hid/index.ts CHANGED
@@ -4,4 +4,7 @@
4
4
 
5
5
  export * from "./command";
6
6
  export * from "./dualsense_hid";
7
- export * from "./ids";
7
+ export * from "./hid_provider";
8
+ export * from "./node_hid_provider";
9
+ export * from "./platform_hid_provider";
10
+ export * from "./web_hid_provider";
@@ -0,0 +1,108 @@
1
+ import { HID, devices } from "node-hid";
2
+
3
+ import {
4
+ HIDProvider,
5
+ DualsenseHIDState,
6
+ InputId,
7
+ mapAxis,
8
+ mapTrigger,
9
+ } from "./hid_provider";
10
+
11
+ export class NodeHIDProvider extends HIDProvider {
12
+ private device?: HID;
13
+ public wireless: boolean = false;
14
+
15
+ connect(): void {
16
+ this.disconnect();
17
+
18
+ const controllers = devices(HIDProvider.vendorId, HIDProvider.productId);
19
+ if (controllers.length === 0 || !controllers[0].path) {
20
+ return this.onError(
21
+ new Error(`No controllers (${devices().length} other devices)`)
22
+ );
23
+ }
24
+
25
+ if (controllers[0].interface === -1) this.wireless = true;
26
+
27
+ this.device = new HID(controllers[0].path);
28
+ this.device.on("data", (arg: Buffer) => {
29
+ this.onData(this.process(arg));
30
+ });
31
+ this.device.on("error", (err: Error) => {
32
+ this.onError(err);
33
+ });
34
+ }
35
+
36
+ get connected(): boolean {
37
+ return this.device !== undefined;
38
+ }
39
+
40
+ disconnect(): void {
41
+ if (this.device) {
42
+ this.device.removeAllListeners();
43
+ this.device.close();
44
+ this.device = undefined;
45
+ this.wireless = false;
46
+ }
47
+ }
48
+
49
+ process(buffer: Buffer): DualsenseHIDState {
50
+ // Bluetooth buffer starts with an extra byte
51
+ const report = buffer.subarray(this.wireless ? 2 : 1);
52
+
53
+ const mainButtons = report.readUint8(7);
54
+ const miscButtons = report.readUint8(8);
55
+ const lastButtons = report.readUint8(9);
56
+ const dpad = (mainButtons << 4) >> 4;
57
+
58
+ return {
59
+ [InputId.LeftAnalogX]: mapAxis(report.readUint8(0)),
60
+ [InputId.LeftAnalogY]: -mapAxis(report.readUint8(1)),
61
+ [InputId.RightAnalogX]: mapAxis(report.readUint8(2)),
62
+ [InputId.RightAnalogY]: -mapAxis(report.readUint8(3)),
63
+ [InputId.LeftTrigger]: mapTrigger(report.readUint8(4)),
64
+ [InputId.RightTrigger]: mapTrigger(report.readUint8(5)),
65
+ // 6 is a sequence byte
66
+ [InputId.Triangle]: (mainButtons & 128) > 0,
67
+ [InputId.Circle]: (mainButtons & 64) > 0,
68
+ [InputId.Cross]: (mainButtons & 32) > 0,
69
+ [InputId.Square]: (mainButtons & 16) > 0,
70
+ [InputId.Dpad]: dpad,
71
+ [InputId.Up]: dpad < 2 || dpad === 7,
72
+ [InputId.Down]: dpad > 2 && dpad < 6,
73
+ [InputId.Left]: dpad > 4 && dpad < 8,
74
+ [InputId.Right]: dpad > 0 && dpad < 4,
75
+ [InputId.LeftTriggerButton]: (miscButtons & 4) > 0,
76
+ [InputId.RightTriggerButton]: (miscButtons & 8) > 0,
77
+ [InputId.LeftBumper]: (miscButtons & 1) > 0,
78
+ [InputId.RightBumper]: (miscButtons & 2) > 0,
79
+ [InputId.Create]: (miscButtons & 16) > 0,
80
+ [InputId.Options]: (miscButtons & 32) > 0,
81
+ [InputId.LeftAnalogButton]: (miscButtons & 64) > 0,
82
+ [InputId.RightAnalogButton]: (miscButtons & 128) > 0,
83
+ [InputId.Playstation]: (lastButtons & 1) > 0,
84
+ [InputId.TouchButton]: (lastButtons & 2) > 0,
85
+ [InputId.Mute]: (lastButtons & 4) > 0,
86
+ // The other 5 bits are unused
87
+ // 5 reserved bytes
88
+ [InputId.GyroX]: report.readUint16LE(15),
89
+ [InputId.GyroY]: report.readUint16LE(17),
90
+ [InputId.GyroZ]: report.readUint16LE(19),
91
+ [InputId.AccelX]: report.readUint16LE(21),
92
+ [InputId.AccelY]: report.readUint16LE(23),
93
+ [InputId.AccelZ]: report.readUint16LE(25),
94
+ // 4 bytes for sensor timestamp (32LE)
95
+ // 1 reserved byte
96
+ [InputId.TouchId0]: report.readUint8(32) & 0x7f,
97
+ [InputId.TouchContact0]: (report.readUint8(32) & 0x80) === 0,
98
+ [InputId.TouchX0]: mapAxis((report.readUint16LE(33) << 20) >> 20, 1920),
99
+ [InputId.TouchY0]: mapAxis(report.readUint16LE(34) >> 4, 1080),
100
+ [InputId.TouchId1]: report.readUint8(36) & 0x7f,
101
+ [InputId.TouchContact1]: (report.readUint8(36) & 0x80) === 0,
102
+ [InputId.TouchX1]: mapAxis((report.readUint16LE(37) << 20) >> 20, 1920),
103
+ [InputId.TouchY1]: mapAxis(report.readUint16LE(38) >> 4, 1080),
104
+ // 12 reserved bytes
105
+ [InputId.Status]: (report.readUint8(53) & 4) > 0,
106
+ };
107
+ }
108
+ }
@@ -0,0 +1,4 @@
1
+ import { NodeHIDProvider } from './node_hid_provider'
2
+ import { WebHIDProvider } from './web_hid_provider'
3
+
4
+ export const PlatformHIDProvider = typeof window === 'undefined' ? NodeHIDProvider : WebHIDProvider;
@@ -0,0 +1,116 @@
1
+ import {
2
+ HIDProvider,
3
+ DualsenseHIDState,
4
+ InputId,
5
+ mapAxis,
6
+ mapTrigger,
7
+ } from "./hid_provider";
8
+
9
+ export class WebHIDProvider extends HIDProvider {
10
+ private device?: HIDDevice;
11
+ public wireless: boolean = true; // TODO: Not sure what to check
12
+
13
+ connect(): void {
14
+ this.disconnect();
15
+
16
+ navigator.hid
17
+ .requestDevice({
18
+ filters: [
19
+ { vendorId: HIDProvider.vendorId, productId: HIDProvider.productId },
20
+ ],
21
+ })
22
+ .then((devices: HIDDevice[]) => {
23
+ if (devices.length === 0) {
24
+ return this.onError(new Error(`No controllers available`));
25
+ }
26
+
27
+ devices[0]
28
+ .open()
29
+ .then(() => {
30
+ this.device = devices[0];
31
+ this.device.addEventListener("inputreport", ({ data }) => {
32
+ this.onData(this.process(data));
33
+ });
34
+ })
35
+ .catch((err: Error) => {
36
+ this.onError(err);
37
+ });
38
+ })
39
+ .catch((err: Error) => {
40
+ this.onError(err);
41
+ });
42
+ }
43
+
44
+ get connected(): boolean {
45
+ return this.device !== undefined;
46
+ }
47
+
48
+ disconnect(): void {
49
+ if (this.device) {
50
+ this.device.close().finally(() => {
51
+ this.device = undefined;
52
+ this.wireless = false;
53
+ });
54
+ }
55
+ }
56
+
57
+ process(buffer: DataView): DualsenseHIDState {
58
+ // Bluetooth buffer starts with an extra byte
59
+ const report = new DataView(buffer.buffer, this.wireless ? 2 : 1);
60
+
61
+ const mainButtons = report.getUint8(7);
62
+ const miscButtons = report.getUint8(8);
63
+ const lastButtons = report.getUint8(9);
64
+ const dpad = (mainButtons << 4) >> 4;
65
+
66
+ return {
67
+ [InputId.LeftAnalogX]: mapAxis(report.getUint8(0)),
68
+ [InputId.LeftAnalogY]: -mapAxis(report.getUint8(1)),
69
+ [InputId.RightAnalogX]: mapAxis(report.getUint8(2)),
70
+ [InputId.RightAnalogY]: -mapAxis(report.getUint8(3)),
71
+ [InputId.LeftTrigger]: mapTrigger(report.getUint8(4)),
72
+ [InputId.RightTrigger]: mapTrigger(report.getUint8(5)),
73
+ [InputId.Triangle]: (mainButtons & 128) > 0,
74
+ [InputId.Circle]: (mainButtons & 64) > 0,
75
+ [InputId.Cross]: (mainButtons & 32) > 0,
76
+ [InputId.Square]: (mainButtons & 16) > 0,
77
+ [InputId.Dpad]: dpad,
78
+ [InputId.Up]: dpad < 2 || dpad === 7,
79
+ [InputId.Down]: dpad > 2 && dpad < 6,
80
+ [InputId.Left]: dpad > 4 && dpad < 8,
81
+ [InputId.Right]: dpad > 0 && dpad < 4,
82
+ [InputId.LeftTriggerButton]: (miscButtons & 4) > 0,
83
+ [InputId.RightTriggerButton]: (miscButtons & 8) > 0,
84
+ [InputId.LeftBumper]: (miscButtons & 1) > 0,
85
+ [InputId.RightBumper]: (miscButtons & 2) > 0,
86
+ [InputId.Create]: (miscButtons & 16) > 0,
87
+ [InputId.Options]: (miscButtons & 32) > 0,
88
+ [InputId.LeftAnalogButton]: (miscButtons & 64) > 0,
89
+ [InputId.RightAnalogButton]: (miscButtons & 128) > 0,
90
+ [InputId.Playstation]: (lastButtons & 1) > 0,
91
+ [InputId.TouchButton]: (lastButtons & 2) > 0,
92
+ [InputId.Mute]: (lastButtons & 4) > 0,
93
+ [InputId.GyroX]: report.getUint16(15, true),
94
+ [InputId.GyroY]: report.getUint16(17, true),
95
+ [InputId.GyroZ]: report.getUint16(19, true),
96
+ [InputId.AccelX]: report.getUint16(21, true),
97
+ [InputId.AccelY]: report.getUint16(23, true),
98
+ [InputId.AccelZ]: report.getUint16(25, true),
99
+ [InputId.TouchId0]: report.getUint8(32) & 0x7f,
100
+ [InputId.TouchContact0]: (report.getUint8(32) & 0x80) === 0,
101
+ [InputId.TouchX0]: mapAxis(
102
+ (report.getUint16(33, true) << 20) >> 20,
103
+ 1920
104
+ ),
105
+ [InputId.TouchY0]: mapAxis(report.getUint16(34, true) >> 4, 1080),
106
+ [InputId.TouchId1]: report.getUint8(36) & 0x7f,
107
+ [InputId.TouchContact1]: (report.getUint8(36) & 0x80) === 0,
108
+ [InputId.TouchX1]: mapAxis(
109
+ (report.getUint16(37, true) << 20) >> 20,
110
+ 1920
111
+ ),
112
+ [InputId.TouchY1]: mapAxis(report.getUint16(38, true) >> 4, 1080),
113
+ [InputId.Status]: (report.getUint8(53) & 4) > 0,
114
+ };
115
+ }
116
+ }
@@ -1,4 +1,6 @@
1
- // Names for real controller inputs
1
+ /**
2
+ * IDs for real and virtual controller inputs.
3
+ */
2
4
  export const enum InputId {
3
5
  Options = "Options",
4
6
  Create = "Create",
@@ -52,4 +54,7 @@ export const enum InputId {
52
54
  AccelX = "AccelX",
53
55
  AccelY = "AccelY",
54
56
  AccelZ = "AccelZ",
57
+
58
+ // For placeholder inputs
59
+ Unknown = "Unknown",
55
60
  }
package/src/index.ts CHANGED
@@ -2,7 +2,9 @@
2
2
  * @file Automatically generated by barrelsby.
3
3
  */
4
4
 
5
+ export * from "./comparators";
5
6
  export * from "./dualsense";
7
+ export * from "./id";
6
8
  export * from "./input";
7
9
  export * from "./math";
8
10
  export * from "./elements/index";
package/src/input.ts CHANGED
@@ -1,83 +1,177 @@
1
- import { EventEmitter } from "events";
2
- import { inspect } from "util";
1
+ import { InputId } from "./id";
2
+ import {
3
+ VirtualComparator,
4
+ ThresholdComparator,
5
+ BasicComparator,
6
+ } from "./comparators";
7
+
8
+ export { InputId } from "./id";
3
9
 
4
10
  export interface InputParams {
5
11
  name?: string;
6
12
  icon?: string;
7
13
  threshold?: number;
8
- parent?: Input<unknown>;
9
14
  }
10
15
 
11
- const InputDefaults: Omit<Required<InputParams>, "parent"> = {
12
- name: "???",
13
- icon: "???",
14
- threshold: 0,
15
- };
16
+ export type InputChangeType = "change" | "press" | "release";
17
+ export type InputEventType = InputChangeType | "input";
16
18
 
17
- // Private properties
18
- const InputAdopt = Symbol("InputAdopt");
19
- const InputChildless = Symbol("InputChildless");
20
- const InputParent = Symbol("InputParent");
21
- const InputSetComparison = Symbol("InputSetComparison");
22
- const InputChangedPrimitive = Symbol("InputChangedPrimitive");
23
- const InputChangedThreshold = Symbol("InputChangedThreshold");
24
- const InputChangedVirtual = Symbol("InputChangedVirtual");
25
-
26
- // Optional abstract properties
27
- export const InputChanged = Symbol("InputChanged");
19
+ export type InputCallback<Instance> = (
20
+ input: Instance,
21
+ changed: Instance | Input<unknown>
22
+ ) => unknown | Promise<unknown>;
28
23
 
29
- // Utilities
24
+ /**
25
+ * Private utilities
26
+ */
27
+
28
+ export const InputSetComparator = Symbol("InputSetComparator");
29
+ export const InputChanged = Symbol("InputChanged");
30
30
  export const InputSet = Symbol("InputSet");
31
31
  export const InputName = Symbol("InputName");
32
32
  export const InputIcon = Symbol("InputIcon");
33
33
 
34
- export type InputEvent = "change" | "input" | "press" | "release";
35
-
36
- export declare interface Input<Type> {
37
- on(
38
- event: InputEvent,
39
- listener: (
40
- input: Input<Type>,
41
- changed: Input<unknown>
42
- ) => unknown | Promise<unknown>
43
- ): this;
44
- emit(
45
- event: InputEvent,
46
- ...args: [Input<Type>, Input<unknown> | Input<Type>]
47
- ): boolean;
48
- }
34
+ /**
35
+ * Private properties
36
+ */
37
+
38
+ const InputOns = Symbol("InputOns");
39
+ const InputOnces = Symbol("InputOnces");
40
+ const InputAdopt = Symbol("InputAdopt");
41
+ const InputParents = Symbol("InputParents");
42
+ const InputComparator = Symbol("InputComparator");
49
43
 
50
44
  /**
51
45
  * Input manages the state of a single device input,
52
46
  * a virtual input, or a group of Input children.
53
47
  */
54
- export abstract class Input<Type>
55
- extends EventEmitter
56
- implements AsyncIterator<Input<Type>>
57
- {
58
- public readonly id: symbol;
59
-
60
- // Timestamp of the last received input that changed the state.
61
- public lastChange: number = Date.now();
48
+ export abstract class Input<Type> implements AsyncIterator<Input<Type>> {
49
+ public readonly id: InputId = InputId.Unknown;
62
50
 
63
- // Timestamp of the last received input, even if it didn't change the state.
64
- public lastInput: number = Date.now();
65
-
66
- // For numeric inputs, ignore state changes smaller than this threshold.
51
+ /**
52
+ * For numeric inputs, ignore state changes smaller than this threshold.
53
+ */
67
54
  public threshold: number = 0;
68
55
 
69
- // Provide the type and default value for the input.
56
+ /**
57
+ * Provide the type and default value for the input.
58
+ */
70
59
  public abstract state: Type;
71
60
 
72
- // Implement a function that returns true if the user is actively engaged with the input.
61
+ /**
62
+ * Stores event listeners.
63
+ */
64
+ private [InputOns] = new Map<InputEventType, InputCallback<this>[]>();
65
+
66
+ /**
67
+ * Stores callbacks waiting for one-time events.
68
+ */
69
+ private [InputOnces] = new Map<InputChangeType, InputCallback<this>[]>();
70
+
71
+ constructor(params: InputParams = {}) {
72
+ const { name, icon, threshold } = {
73
+ icon: "???",
74
+ name: "Nameless Input",
75
+ threshold: 0,
76
+ ...params,
77
+ };
78
+
79
+ this[InputName] = name;
80
+ this[InputIcon] = icon;
81
+ this.threshold = threshold;
82
+
83
+ setTimeout(() => {
84
+ this[InputSetComparator]();
85
+ Object.values(this).forEach((value) => {
86
+ if (value === this) return;
87
+ if (value instanceof Input) value[InputAdopt](this as Input<unknown>);
88
+ });
89
+ });
90
+ }
91
+
92
+ /**
93
+ * Implement a function that returns true if the user is actively engaged with the input.
94
+ */
73
95
  public abstract get active(): boolean;
74
96
 
97
+ /**
98
+ * Register a callback to recieve state updates from this Input.
99
+ */
100
+ public on(event: InputEventType, listener: InputCallback<this>): this {
101
+ const listeners = this[InputOns].get(event);
102
+ if (!listeners) {
103
+ this[InputOns].set(event, []);
104
+ return this.on(event, listener);
105
+ }
106
+ listeners.push(listener);
107
+ return this;
108
+ }
109
+
110
+ /**
111
+ * Register a callback to recieve the next specified update.
112
+ */
113
+ public once(event: InputChangeType, listener: InputCallback<this>): this {
114
+ const listeners = this[InputOnces].get(event);
115
+ if (!listeners) {
116
+ this[InputOnces].set(event, []);
117
+ return this.once(event, listener);
118
+ }
119
+ listeners.push(listener);
120
+ return this;
121
+ }
122
+
123
+ /**
124
+ * Notify listeners and parents of a state change.
125
+ */
126
+ private emit(event: InputEventType, changed: Input<unknown> | this): void {
127
+ const listeners = this[InputOns].get(event) || [];
128
+ listeners.forEach((callback) => {
129
+ callback(this, changed);
130
+ });
131
+
132
+ if (event !== "input") {
133
+ this.emitOnce(event, changed);
134
+ this[InputParents].forEach((input) => {
135
+ input.emit(event, changed as Input<unknown>);
136
+ });
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Notify one-time listeners of a state change.
142
+ */
143
+ private emitOnce(
144
+ event: InputChangeType,
145
+ changed: this | Input<unknown> = this
146
+ ): void {
147
+ const listeners = this[InputOnces].get(event) || [];
148
+ this[InputOnces].set(event, []);
149
+ listeners.forEach((callback) => {
150
+ callback(this, changed as Input<unknown>);
151
+ });
152
+ }
153
+
154
+ /**
155
+ * Register a callback to recieve state updates from this Input.
156
+ */
157
+ public addEventListener(
158
+ event: InputEventType,
159
+ listener: InputCallback<this>,
160
+ { once }: { once: boolean } = { once: false }
161
+ ): this {
162
+ if (once) {
163
+ if (event === "input") {
164
+ throw new Error("Can't listen once to `input` events");
165
+ }
166
+ return this.once(event, listener);
167
+ }
168
+ return this.on(event, listener);
169
+ }
170
+
75
171
  /**
76
172
  * Resolves on the next change to this input's state.
77
173
  */
78
- public next(
79
- type: "press" | "release" | "change" = "change"
80
- ): Promise<IteratorResult<this>> {
174
+ public next(type: InputChangeType = "change"): Promise<IteratorResult<this>> {
81
175
  return new Promise<IteratorResult<this>>((resolve) => {
82
176
  this.once(type, () => {
83
177
  resolve({ value: this, done: false });
@@ -97,44 +191,16 @@ export abstract class Input<Type>
97
191
  }
98
192
 
99
193
  /**
100
- * Render a convenient debugging string.
194
+ * Render a debugging string.
101
195
  */
102
196
  public toString(): string {
103
197
  return `${this[InputIcon]} [${this.active ? "X" : "_"}]`;
104
198
  }
105
199
 
106
- constructor(params?: InputParams) {
107
- super();
108
-
109
- const { icon, name, parent, threshold } = {
110
- ...InputDefaults,
111
- ...(params || {}),
112
- };
113
- this[InputName] = name;
114
- this[InputIcon] = icon;
115
- this[InputParent] = parent;
116
-
117
- this.threshold = threshold;
118
- this.id = Symbol(this[InputName]);
119
-
120
- this[InputChanged] = this[InputSetComparison]();
121
- setImmediate(() => {
122
- this[InputAdopt]();
123
- this[InputChanged] = this[InputSetComparison]();
124
- });
125
- }
126
-
127
- // Optionally, implement a function that returns true if the provided state is worth an event
128
- [InputChanged]: (state: Type, newState: Type) => boolean;
129
-
130
- // TODO Support params for nested inputs
131
- [inspect.custom](): string {
132
- return `${this[InputName]} ${this[InputIcon]}: ${JSON.stringify(
133
- this.state instanceof Input && this.state.id === this.id
134
- ? "virtual"
135
- : this.state
136
- )}`;
137
- }
200
+ /**
201
+ * Returns true if the provided state is worth an event
202
+ */
203
+ [InputComparator]: (state: Type, newState: Type) => boolean = BasicComparator;
138
204
 
139
205
  [Symbol.asyncIterator](): AsyncIterator<this> {
140
206
  return this;
@@ -150,62 +216,38 @@ export abstract class Input<Type>
150
216
  return this.toString();
151
217
  }
152
218
 
153
- // A name for this input
219
+ /**
220
+ * The name of this input.
221
+ */
154
222
  readonly [InputName]: string;
155
223
 
156
- // A short name for this input
224
+ /**
225
+ * A short name for this input.
226
+ */
157
227
  readonly [InputIcon]: string;
158
228
 
159
- // The Input's parent, if any
160
- [InputParent]?: Input<unknown>;
161
-
162
- [InputChildless]: boolean = true;
163
-
164
- /**
165
- * Cascade events from nested Inputs.
166
- * And decide if this is the root Input.
167
- */
168
- [InputAdopt](): void {
169
- Object.values(this).forEach((value) => {
170
- if (value === this) return;
171
- if (value instanceof Input) {
172
- this[InputChildless] = false;
173
- if (!value[InputChildless]) return;
174
- value.on("change", (that, value) => {
175
- that;
176
- this.emit("change", this, value);
177
- });
178
- value.on("input", (that, value) => {
179
- that;
180
- this.emit("input", this, value);
181
- });
182
- }
183
- });
184
- }
185
-
186
- [InputChangedVirtual](): boolean {
187
- return true;
188
- }
189
-
190
- [InputChangedPrimitive](state: Type, newState: Type): boolean {
191
- return state !== newState;
192
- }
229
+ /**
230
+ * Other Inputs that contain this one.
231
+ */
232
+ private [InputParents] = new Set<Input<unknown>>();
193
233
 
194
- [InputChangedThreshold](state: number, newState: number): boolean {
195
- return Math.abs(state - newState) > this.threshold;
234
+ /**
235
+ * Links Inputs to bubble up events.
236
+ */
237
+ [InputAdopt](parent: Input<unknown>): void {
238
+ this[InputParents].add(parent);
196
239
  }
197
240
 
198
- // Sets a default comparison type for the Input based on the generic type.
199
- [InputSetComparison](): (state: Type, newState: Type) => boolean {
241
+ /**
242
+ * Sets a default comparison type for the Input by discovering the generic type.
243
+ */
244
+ [InputSetComparator](): void {
200
245
  if (typeof this.state === "number") {
201
- return this[InputChangedThreshold] as unknown as (
202
- state: Type,
203
- newState: Type
204
- ) => boolean;
246
+ this[InputComparator] = ThresholdComparator.bind(this, this.threshold);
205
247
  } else if (this.state instanceof Input) {
206
- return this[InputChangedVirtual];
248
+ this[InputComparator] = VirtualComparator;
207
249
  } else {
208
- return this[InputChangedPrimitive];
250
+ this[InputComparator] = BasicComparator;
209
251
  }
210
252
  }
211
253
 
@@ -213,18 +255,12 @@ export abstract class Input<Type>
213
255
  * Update the input's state and trigger all necessary callbacks.
214
256
  */
215
257
  [InputSet](state: Type): void {
216
- this.lastInput = Date.now();
217
- if (this[InputChanged](this.state, state)) {
258
+ if (this[InputComparator](this.state, state)) {
218
259
  this.state = state;
219
- this.lastChange = Date.now();
220
- this.emit("change", this, this);
221
- if (typeof state === "boolean") {
222
- state
223
- ? this.emit("press", this, this)
224
- : this.emit("release", this, this);
225
- }
260
+ this.emit("change", this);
261
+ if (typeof state === "boolean")
262
+ this.emit(state ? "press" : "release", this);
226
263
  }
227
-
228
- this.emit("input", this, this);
264
+ this.emit("input", this);
229
265
  }
230
266
  }