emoemu 0.1.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 (213) hide show
  1. package/.claude/settings.local.json +77 -0
  2. package/.node-version +1 -0
  3. package/CLAUDE.md +435 -0
  4. package/README.md +404 -0
  5. package/TODO.md +655 -0
  6. package/dist/index.cjs +25108 -0
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.js +25085 -0
  9. package/docs/building-libretro-cores-arm-mac.md +237 -0
  10. package/docs/config-file-format.md +488 -0
  11. package/docs/cores-trd.md +425 -0
  12. package/docs/headless-hardware-rendering-trd.md +676 -0
  13. package/docs/libretro-cores-trd.md +997 -0
  14. package/docs/mupen64-software-rendering-trd.md +751 -0
  15. package/docs/n64-support-trd.md +306 -0
  16. package/docs/native-rendering-trd.md +540 -0
  17. package/docs/native-ui-rendering-trd.md +1195 -0
  18. package/docs/netplay-trd.md +665 -0
  19. package/docs/retroarch-netplay-docs.md +277 -0
  20. package/docs/save-state-format.md +666 -0
  21. package/eslint.config.js +111 -0
  22. package/icon/icon.png +0 -0
  23. package/icon/icon.pxd +0 -0
  24. package/package.json +63 -0
  25. package/pnpm-workspace.yaml +10 -0
  26. package/src/Emulator/consts.ts +14 -0
  27. package/src/Emulator/index.ts +2496 -0
  28. package/src/Emulator/saveState/index.ts +155 -0
  29. package/src/Emulator/screenshot/index.ts +160 -0
  30. package/src/Emulator/terminalDimensions/index.ts +79 -0
  31. package/src/Emulator/types.ts +83 -0
  32. package/src/cli/commands/consts.ts +10 -0
  33. package/src/cli/commands/index.ts +462 -0
  34. package/src/cli/parseArgs/consts.ts +17 -0
  35. package/src/cli/parseArgs/index.ts +457 -0
  36. package/src/cli/parseArgs/types.ts +61 -0
  37. package/src/cli/runEmulator/index.ts +406 -0
  38. package/src/cli/runEmulator/types.ts +7 -0
  39. package/src/consts.ts +19 -0
  40. package/src/core/button/consts.ts +35 -0
  41. package/src/core/button/index.ts +123 -0
  42. package/src/core/core.ts +300 -0
  43. package/src/core/index.ts +19 -0
  44. package/src/cores/libretro/api/index.ts +334 -0
  45. package/src/cores/libretro/api/types.ts +148 -0
  46. package/src/cores/libretro/callbacks/consts.ts +41 -0
  47. package/src/cores/libretro/callbacks/index.ts +456 -0
  48. package/src/cores/libretro/consts.ts +45 -0
  49. package/src/cores/libretro/coreOptions/consts.ts +36 -0
  50. package/src/cores/libretro/coreOptions/index.ts +222 -0
  51. package/src/cores/libretro/environment/consts.ts +118 -0
  52. package/src/cores/libretro/environment/index.ts +1095 -0
  53. package/src/cores/libretro/index.ts +937 -0
  54. package/src/cores/libretro/loader/index.ts +496 -0
  55. package/src/cores/libretro/pixelFormat/consts.ts +43 -0
  56. package/src/cores/libretro/pixelFormat/index.ts +397 -0
  57. package/src/cores/libretro/types.ts +339 -0
  58. package/src/frontend/AudioManager/index.ts +420 -0
  59. package/src/frontend/SettingsManager/index.ts +250 -0
  60. package/src/frontend/config/index.ts +608 -0
  61. package/src/frontend/config/tests.ts +354 -0
  62. package/src/frontend/config/types.ts +36 -0
  63. package/src/frontend/consts.ts +114 -0
  64. package/src/frontend/coreBuilder/index.ts +644 -0
  65. package/src/frontend/coreBuilder/types.ts +15 -0
  66. package/src/frontend/coreDownloader/index.ts +620 -0
  67. package/src/frontend/coreDownloader/types.ts +17 -0
  68. package/src/frontend/corePreferences/index.ts +69 -0
  69. package/src/frontend/coreRegistry/index.ts +276 -0
  70. package/src/frontend/directoryCache/index.ts +75 -0
  71. package/src/frontend/index.ts +79 -0
  72. package/src/frontend/notifications/consts.ts +14 -0
  73. package/src/frontend/notifications/index.ts +250 -0
  74. package/src/frontend/playlist/consts.ts +168 -0
  75. package/src/frontend/playlist/index.ts +899 -0
  76. package/src/frontend/playlist/labelFormatter/consts.ts +15 -0
  77. package/src/frontend/playlist/labelFormatter/index.ts +155 -0
  78. package/src/frontend/playlist/labelFormatter/tests.ts +153 -0
  79. package/src/frontend/playlist/reader/index.ts +559 -0
  80. package/src/frontend/playlist/sync/index.ts +511 -0
  81. package/src/frontend/playlist/systemLookup/index.ts +233 -0
  82. package/src/frontend/playlist/utils/index.ts +50 -0
  83. package/src/frontend/romScanner/consts.ts +348 -0
  84. package/src/frontend/romScanner/index.ts +1957 -0
  85. package/src/frontend/saveServices/consts.ts +2 -0
  86. package/src/frontend/saveServices/index.ts +313 -0
  87. package/src/frontend/serviceProvider/index.ts +108 -0
  88. package/src/frontend/serviceProvider/types.ts +13 -0
  89. package/src/index.ts +428 -0
  90. package/src/input/Controller/consts.ts +50 -0
  91. package/src/input/Controller/index.ts +81 -0
  92. package/src/input/GamepadManager/consts.ts +22 -0
  93. package/src/input/GamepadManager/index.ts +418 -0
  94. package/src/input/InputManager/consts.ts +86 -0
  95. package/src/input/InputManager/index.ts +593 -0
  96. package/src/input/InputMapper/consts.ts +33 -0
  97. package/src/input/InputMapper/index.ts +436 -0
  98. package/src/input/consts.ts +410 -0
  99. package/src/input/gamepadProfiles/index.ts +1100 -0
  100. package/src/input/index.ts +38 -0
  101. package/src/input/inputUtils/index.ts +31 -0
  102. package/src/netplay/FrameBuffer/consts.ts +2 -0
  103. package/src/netplay/FrameBuffer/index.ts +364 -0
  104. package/src/netplay/FrameBuffer/tests.ts +286 -0
  105. package/src/netplay/InputBuffer/consts.ts +7 -0
  106. package/src/netplay/InputBuffer/index.ts +347 -0
  107. package/src/netplay/InputBuffer/tests.ts +166 -0
  108. package/src/netplay/NetplayClient/index.ts +976 -0
  109. package/src/netplay/NetplayConnection/index.ts +536 -0
  110. package/src/netplay/NetplayDiscovery/consts.ts +41 -0
  111. package/src/netplay/NetplayDiscovery/index.ts +525 -0
  112. package/src/netplay/NetplayServer/index.ts +1407 -0
  113. package/src/netplay/SyncManager/index.ts +984 -0
  114. package/src/netplay/SyncManager/tests.ts +419 -0
  115. package/src/netplay/consts.ts +371 -0
  116. package/src/netplay/crc32/consts.ts +14 -0
  117. package/src/netplay/crc32/index.ts +97 -0
  118. package/src/netplay/crc32/tests.ts +40 -0
  119. package/src/netplay/index.ts +41 -0
  120. package/src/netplay/netplayLogger/consts.ts +30 -0
  121. package/src/netplay/netplayLogger/index.ts +345 -0
  122. package/src/netplay/protocol/consts.ts +86 -0
  123. package/src/netplay/protocol/index.ts +1280 -0
  124. package/src/netplay/protocol/tests.ts +606 -0
  125. package/src/netplay/protocol/types.ts +20 -0
  126. package/src/netplay/types.ts +395 -0
  127. package/src/rendering/KittyRenderer/index.ts +616 -0
  128. package/src/rendering/NativeRenderer/index.ts +279 -0
  129. package/src/rendering/NativeRenderer/tests.ts +133 -0
  130. package/src/rendering/TerminalRenderer/index.ts +770 -0
  131. package/src/rendering/consts.ts +401 -0
  132. package/src/rendering/fonts/CozetteVector.ttf +0 -0
  133. package/src/rendering/index.ts +26 -0
  134. package/src/rendering/nativeUi/NativeWindowManager/index.ts +158 -0
  135. package/src/rendering/nativeUi/NativeWindowManager/tests.ts +81 -0
  136. package/src/rendering/nativeUi/consts.ts +6 -0
  137. package/src/rendering/nativeUi/index.ts +20 -0
  138. package/src/rendering/postProcessing/consts.ts +38 -0
  139. package/src/rendering/postProcessing/index.ts +923 -0
  140. package/src/rendering/shared/ansi/consts.ts +12 -0
  141. package/src/rendering/shared/ansi/index.ts +104 -0
  142. package/src/rendering/shared/consts.ts +2 -0
  143. package/src/rendering/shared/fitToTerminal/index.ts +67 -0
  144. package/src/ui/AddRomsPrompt/consts.ts +13 -0
  145. package/src/ui/AddRomsPrompt/index.tsx +781 -0
  146. package/src/ui/App/consts.ts +2 -0
  147. package/src/ui/App/index.tsx +456 -0
  148. package/src/ui/AppCapabilities/index.tsx +67 -0
  149. package/src/ui/ConfigContext/index.tsx +56 -0
  150. package/src/ui/CoreManager/consts.ts +11 -0
  151. package/src/ui/CoreManager/index.tsx +779 -0
  152. package/src/ui/CoreSelector/consts.ts +2 -0
  153. package/src/ui/CoreSelector/index.tsx +251 -0
  154. package/src/ui/DialogContainer/index.tsx +42 -0
  155. package/src/ui/DialogOptionsList/index.tsx +61 -0
  156. package/src/ui/DuplicateCrcPrompt/consts.ts +5 -0
  157. package/src/ui/DuplicateCrcPrompt/index.tsx +146 -0
  158. package/src/ui/GamepadContext/consts.ts +15 -0
  159. package/src/ui/GamepadContext/index.tsx +295 -0
  160. package/src/ui/NativeDialog/index.tsx +120 -0
  161. package/src/ui/NetplayDisconnectedDialog/index.tsx +93 -0
  162. package/src/ui/NetplayPauseMenu/consts.ts +2 -0
  163. package/src/ui/NetplayPauseMenu/index.tsx +97 -0
  164. package/src/ui/RomBrowser/NetplayPanel/consts.ts +24 -0
  165. package/src/ui/RomBrowser/NetplayPanel/index.tsx +520 -0
  166. package/src/ui/RomBrowser/SettingsPanel/index.tsx +478 -0
  167. package/src/ui/RomBrowser/consts.ts +61 -0
  168. package/src/ui/RomBrowser/index.tsx +1164 -0
  169. package/src/ui/RomBrowser/settingsConfig/index.ts +320 -0
  170. package/src/ui/RomBrowser/types.ts +67 -0
  171. package/src/ui/SaveStateDialog/consts.ts +2 -0
  172. package/src/ui/SaveStateDialog/index.tsx +225 -0
  173. package/src/ui/WarningDialog/index.tsx +113 -0
  174. package/src/ui/consts.ts +27 -0
  175. package/src/ui/hooks/useClearTerminal/consts.ts +2 -0
  176. package/src/ui/hooks/useClearTerminal/index.ts +37 -0
  177. package/src/ui/hooks/useDialogNavigation/index.ts +99 -0
  178. package/src/ui/hooks/useGamepad/consts.ts +21 -0
  179. package/src/ui/hooks/useGamepad/index.ts +194 -0
  180. package/src/ui/index.ts +27 -0
  181. package/src/utils/buffer/consts.ts +17 -0
  182. package/src/utils/buffer/index.ts +129 -0
  183. package/src/utils/color/consts.ts +58 -0
  184. package/src/utils/color/index.ts +183 -0
  185. package/src/utils/compression/consts.ts +50 -0
  186. package/src/utils/compression/index.ts +101 -0
  187. package/src/utils/consts.ts +2 -0
  188. package/src/utils/crc32/consts.ts +22 -0
  189. package/src/utils/crc32/index.ts +83 -0
  190. package/src/utils/ensureDirectory/index.ts +10 -0
  191. package/src/utils/format/consts.ts +8 -0
  192. package/src/utils/format/index.ts +53 -0
  193. package/src/utils/getErrorMessage/index.ts +10 -0
  194. package/src/utils/index.ts +113 -0
  195. package/src/utils/ini/index.ts +200 -0
  196. package/src/utils/kitty/consts.ts +13 -0
  197. package/src/utils/kitty/index.ts +181 -0
  198. package/src/utils/logger/consts.ts +35 -0
  199. package/src/utils/logger/index.ts +217 -0
  200. package/src/utils/paths/consts.ts +18 -0
  201. package/src/utils/paths/index.ts +151 -0
  202. package/src/utils/png/consts.ts +34 -0
  203. package/src/utils/png/index.ts +131 -0
  204. package/src/utils/readJsonFile/index.ts +16 -0
  205. package/src/utils/rotateLogFile/index.ts +44 -0
  206. package/src/utils/safeClose/index.ts +10 -0
  207. package/src/utils/terminal/consts.ts +8 -0
  208. package/src/utils/terminal/index.ts +102 -0
  209. package/src/utils/thumbnailRenderer/consts.ts +2 -0
  210. package/src/utils/thumbnailRenderer/index.ts +147 -0
  211. package/src/utils/typedError/index.ts +26 -0
  212. package/tsconfig.json +31 -0
  213. package/vitest.config.ts +13 -0
@@ -0,0 +1,593 @@
1
+ import { Controller, Button } from '../Controller';
2
+ import {
3
+ KITTY_ENABLE,
4
+ KITTY_DISABLE,
5
+ KITTY_QUERY,
6
+ KITTY_DETECT_TIMEOUT_MS,
7
+ LEGACY_KEY_RELEASE_TIME_MS,
8
+ KITTY_RESPONSE_CLEAR_DELAY_MS,
9
+ MAX_ESCAPE_SEQUENCE_LENGTH,
10
+ LEGACY_ARROW_KEY_SEQUENCE_LENGTH,
11
+ KITTY_EVENT_PRESS,
12
+ KITTY_EVENT_REPEAT,
13
+ KITTY_EVENT_RELEASE,
14
+ KITTY_KEY_ESCAPE,
15
+ KITTY_KEY_ARROW_UP,
16
+ KITTY_KEY_ARROW_DOWN,
17
+ KITTY_KEY_ARROW_LEFT,
18
+ KITTY_KEY_ARROW_RIGHT,
19
+ KITTY_KEY_F8,
20
+ KITTY_KEY_F12,
21
+ KEY_CODE_R_LOWER,
22
+ KEY_CODE_R_UPPER,
23
+ KEY_CODE_M_LOWER,
24
+ KEY_CODE_M_UPPER,
25
+ KEY_CODE_P_LOWER,
26
+ KEY_CODE_P_UPPER,
27
+ KEY_CODE_N_LOWER,
28
+ KEY_CODE_N_UPPER,
29
+ } from '..';
30
+
31
+ export * from './consts';
32
+
33
+ import {
34
+ KITTY_KEY_TO_BUTTON,
35
+ KITTY_SPECIAL_KEYS,
36
+ LEGACY_KEY_TO_BUTTON,
37
+ OPPOSITE_DIRECTIONS,
38
+ } from './consts';
39
+
40
+ // Result of processing input
41
+ export interface InputResult {
42
+ quit: boolean;
43
+ cycleRenderMode: boolean;
44
+ toggleAudio: boolean;
45
+ togglePostProcessing: boolean;
46
+ takeScreenshot: boolean;
47
+ testNotification: boolean;
48
+ }
49
+
50
+ /**
51
+ * InputManager with Kitty keyboard protocol detection.
52
+ * Uses true keydown/keyup events in Kitty mode.
53
+ * Falls back to auto-release timing in legacy mode.
54
+ */
55
+ export class InputManager {
56
+ private controller1: Controller;
57
+ private quitRequested: boolean = false;
58
+ private cycleRenderModeRequested: boolean = false;
59
+ private toggleAudioRequested: boolean = false;
60
+ private togglePostProcessingRequested: boolean = false;
61
+ private takeScreenshotRequested: boolean = false;
62
+ private testNotificationRequested: boolean = false;
63
+
64
+ // Track currently pressed keys (keycode -> button)
65
+ private pressedKeys: Map<number, Button> = new Map();
66
+
67
+ // Buffer for parsing escape sequences
68
+ private inputBuffer: string = '';
69
+
70
+ // Whether Kitty protocol is active and supported
71
+ private kittyMode: boolean = false;
72
+ private kittySupported: boolean | null = null; // null = not yet detected
73
+
74
+ // Legacy mode: track key press times for auto-release
75
+ private legacyKeyTimes: Map<string, number> = new Map();
76
+
77
+ constructor(
78
+ controller1: Controller,
79
+ _controller2: Controller
80
+ ) {
81
+ this.controller1 = controller1;
82
+ // controller2 reserved for future 2-player keyboard support
83
+ }
84
+
85
+ /**
86
+ * Detect if Kitty protocol is supported.
87
+ * Returns a promise that resolves to true if supported.
88
+ * Must be called AFTER stdin is configured (raw mode, resumed).
89
+ */
90
+ async detectKittySupport(): Promise<boolean> {
91
+ return new Promise((resolve) => {
92
+ let responded = false;
93
+ let responseData = '';
94
+
95
+ // Temporary handler to check for Kitty query response
96
+ const checkResponse = (data: Buffer) => {
97
+ const str = data.toString();
98
+ responseData += str;
99
+
100
+ // Kitty responds with: \x1b[?<flags>u
101
+ // We need to consume the entire response to prevent it leaking to input handler
102
+ if (responseData.includes('\x1b[?') && responseData.includes('u')) {
103
+ responded = true;
104
+ process.stdin.removeListener('data', checkResponse);
105
+
106
+ // Give a tiny bit of time for any additional response data to clear
107
+ setTimeout(() => resolve(true), KITTY_RESPONSE_CLEAR_DELAY_MS);
108
+ }
109
+ };
110
+
111
+ process.stdin.on('data', checkResponse);
112
+
113
+ // Send query
114
+ process.stdout.write(KITTY_QUERY);
115
+
116
+ // Timeout - no response means not supported
117
+ setTimeout(() => {
118
+ if (!responded) {
119
+ process.stdin.removeListener('data', checkResponse);
120
+ resolve(false);
121
+ }
122
+ }, KITTY_DETECT_TIMEOUT_MS);
123
+ });
124
+ }
125
+
126
+ /**
127
+ * Start listening for keyboard events.
128
+ * Detects Kitty support and enables appropriate mode.
129
+ */
130
+ async start(): Promise<void> {
131
+ // Detect Kitty protocol support
132
+ this.kittySupported = await this.detectKittySupport();
133
+
134
+ if (this.kittySupported) {
135
+ // Enable Kitty keyboard protocol with key release reporting
136
+ process.stdout.write(KITTY_ENABLE);
137
+ this.kittyMode = true;
138
+ } else {
139
+ // Legacy mode - no special setup needed
140
+ this.kittyMode = false;
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Start without detection (synchronous).
146
+ * Use when you already know the terminal capabilities.
147
+ */
148
+ startWithMode(useKitty: boolean): void {
149
+ if (useKitty) {
150
+ process.stdout.write(KITTY_ENABLE);
151
+ this.kittyMode = true;
152
+ this.kittySupported = true;
153
+ } else {
154
+ this.kittyMode = false;
155
+ this.kittySupported = false;
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Stop listening for keyboard events (disables Kitty protocol).
161
+ */
162
+ stop(): void {
163
+ if (this.kittyMode) {
164
+ process.stdout.write(KITTY_DISABLE);
165
+ this.kittyMode = false;
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Check if Kitty protocol is being used.
171
+ */
172
+ isKittyMode(): boolean {
173
+ return this.kittyMode;
174
+ }
175
+
176
+ /**
177
+ * Check if quit was requested (Escape).
178
+ */
179
+ shouldQuit(): boolean {
180
+ return this.quitRequested;
181
+ }
182
+
183
+ /**
184
+ * Clear the quit request flag.
185
+ * Used when showing a pause menu instead of immediately quitting.
186
+ */
187
+ clearQuitRequest(): void {
188
+ this.quitRequested = false;
189
+ }
190
+
191
+ /**
192
+ * Process raw input from stdin.
193
+ * Handles both Kitty protocol and legacy input.
194
+ */
195
+ processInput(input: string): InputResult {
196
+ // Reset per-frame flags
197
+ this.cycleRenderModeRequested = false;
198
+ this.toggleAudioRequested = false;
199
+ this.togglePostProcessingRequested = false;
200
+ this.takeScreenshotRequested = false;
201
+ this.testNotificationRequested = false;
202
+
203
+ if (this.kittyMode) {
204
+ return this.processKittyInput(input);
205
+ } else {
206
+ return this.processLegacyInput(input);
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Process input in Kitty protocol mode.
212
+ */
213
+ private processKittyInput(input: string): InputResult {
214
+ this.inputBuffer += input;
215
+
216
+ // Process all complete sequences in the buffer
217
+ while (this.inputBuffer.length > 0) {
218
+ // Check for Ctrl+C
219
+ if (this.inputBuffer[0] === '\u0003') {
220
+ this.quitRequested = true;
221
+ this.inputBuffer = this.inputBuffer.slice(1);
222
+ return { quit: true, cycleRenderMode: false, toggleAudio: false, togglePostProcessing: false, takeScreenshot: false, testNotification: false };
223
+ }
224
+
225
+ // Check for escape sequences
226
+ if (this.inputBuffer[0] === '\x1b') {
227
+ // Ignore Kitty protocol query responses: \x1b[?<flags>u
228
+ const queryResponse = this.inputBuffer.match(/^\x1b\[\?\d*u/);
229
+ if (queryResponse) {
230
+ this.inputBuffer = this.inputBuffer.slice(queryResponse[0].length);
231
+ continue;
232
+ }
233
+
234
+ // Try to parse Kitty keyboard protocol
235
+ // Format: CSI keycode ; modifiers:event-type u
236
+ const kittyMatch = this.inputBuffer.match(/^\x1b\[(\d+)(?:;(\d+)(?::(\d+))?)?u/);
237
+ if (kittyMatch) {
238
+ const keycode = parseInt(kittyMatch[1], 10);
239
+ const eventType = kittyMatch[3] ? parseInt(kittyMatch[3], 10) : KITTY_EVENT_PRESS;
240
+
241
+ this.handleKittyKey(keycode, eventType);
242
+ this.inputBuffer = this.inputBuffer.slice(kittyMatch[0].length);
243
+ continue;
244
+ }
245
+
246
+ // Check for arrow keys in various formats
247
+ const arrowMatch = this.inputBuffer.match(/^\x1b\[(?:1;(\d+)(?::(\d+))?)?([ABCD])/);
248
+ if (arrowMatch) {
249
+ const arrowMap: Record<string, { code: number; button: Button }> = {
250
+ 'A': { code: KITTY_KEY_ARROW_UP, button: Button.Up },
251
+ 'B': { code: KITTY_KEY_ARROW_DOWN, button: Button.Down },
252
+ 'C': { code: KITTY_KEY_ARROW_RIGHT, button: Button.Right },
253
+ 'D': { code: KITTY_KEY_ARROW_LEFT, button: Button.Left },
254
+ };
255
+ const eventType = arrowMatch[2] ? parseInt(arrowMatch[2], 10) : KITTY_EVENT_PRESS;
256
+ const arrowKey = arrowMatch[3];
257
+ const arrow = arrowKey && arrowKey in arrowMap ? arrowMap[arrowKey] : undefined;
258
+ if (arrow) {
259
+ if (eventType === KITTY_EVENT_RELEASE) {
260
+ this.handleKeyUp(arrow.code, arrow.button);
261
+ } else {
262
+ this.handleKeyDown(arrow.code, arrow.button);
263
+ }
264
+ }
265
+ this.inputBuffer = this.inputBuffer.slice(arrowMatch[0].length);
266
+ continue;
267
+ }
268
+
269
+ // Check for standalone Escape (quit)
270
+ if (this.inputBuffer.length === 1 || !this.inputBuffer[1].match(/[\[\]O]/)) {
271
+ this.quitRequested = true;
272
+ this.inputBuffer = this.inputBuffer.slice(1);
273
+ return { quit: true, cycleRenderMode: false, toggleAudio: false, togglePostProcessing: false, takeScreenshot: false, testNotification: false };
274
+ }
275
+
276
+ // Unknown escape sequence - wait for more data or skip
277
+ if (this.inputBuffer.length < MAX_ESCAPE_SEQUENCE_LENGTH) {
278
+ break;
279
+ }
280
+ this.inputBuffer = this.inputBuffer.slice(1);
281
+ continue;
282
+ }
283
+
284
+ // Regular character - key press event
285
+ const char = this.inputBuffer[0];
286
+ const charCode = char.charCodeAt(0);
287
+ this.inputBuffer = this.inputBuffer.slice(1);
288
+
289
+ // Check for render mode toggle (R/r key)
290
+ if (charCode === KEY_CODE_R_LOWER || charCode === KEY_CODE_R_UPPER) {
291
+ this.cycleRenderModeRequested = true;
292
+ continue;
293
+ }
294
+
295
+ // Check for audio toggle (M/m key)
296
+ if (charCode === KEY_CODE_M_LOWER || charCode === KEY_CODE_M_UPPER) {
297
+ this.toggleAudioRequested = true;
298
+ continue;
299
+ }
300
+
301
+ // Check for post-processing toggle (P/p key)
302
+ if (charCode === KEY_CODE_P_LOWER || charCode === KEY_CODE_P_UPPER) {
303
+ this.togglePostProcessingRequested = true;
304
+ continue;
305
+ }
306
+
307
+ // Check for test notification (N/n key)
308
+ if (charCode === KEY_CODE_N_LOWER || charCode === KEY_CODE_N_UPPER) {
309
+ this.testNotificationRequested = true;
310
+ continue;
311
+ }
312
+
313
+ const button = KITTY_KEY_TO_BUTTON.get(charCode);
314
+ if (button !== undefined) {
315
+ this.handleKeyDown(charCode, button);
316
+ }
317
+ }
318
+
319
+ return { quit: false, cycleRenderMode: this.cycleRenderModeRequested, toggleAudio: this.toggleAudioRequested, togglePostProcessing: this.togglePostProcessingRequested, takeScreenshot: this.takeScreenshotRequested, testNotification: this.testNotificationRequested };
320
+ }
321
+
322
+ /**
323
+ * Process input in legacy mode (non-Kitty terminals).
324
+ */
325
+ private processLegacyInput(input: string): InputResult {
326
+ // Check for Ctrl+C
327
+ if (input === '\u0003') {
328
+ this.quitRequested = true;
329
+ return { quit: true, cycleRenderMode: false, toggleAudio: false, togglePostProcessing: false, takeScreenshot: false, testNotification: false };
330
+ }
331
+
332
+ // Check for Escape
333
+ if (input === '\x1b') {
334
+ this.quitRequested = true;
335
+ return { quit: true, cycleRenderMode: false, toggleAudio: false, togglePostProcessing: false, takeScreenshot: false, testNotification: false };
336
+ }
337
+
338
+ // Check for F8 key (screenshot) - escape sequence \x1b[19~ or \x1bOP with modifiers
339
+ const f8Match = input.match(/^\x1b\[19~/) || input.match(/^\x1bO[Rw]/);
340
+ if (f8Match) {
341
+ this.takeScreenshotRequested = true;
342
+ const rest = input.slice(f8Match[0].length);
343
+ if (rest.length > 0) {
344
+ return this.processLegacyInput(rest);
345
+ }
346
+ return { quit: false, cycleRenderMode: false, toggleAudio: false, togglePostProcessing: false, takeScreenshot: true, testNotification: false };
347
+ }
348
+
349
+ // Check for F12 key (screenshot) - escape sequence \x1b[24~
350
+ const f12Match = input.match(/^\x1b\[24~/);
351
+ if (f12Match) {
352
+ this.takeScreenshotRequested = true;
353
+ const rest = input.slice(f12Match[0].length);
354
+ if (rest.length > 0) {
355
+ return this.processLegacyInput(rest);
356
+ }
357
+ return { quit: false, cycleRenderMode: false, toggleAudio: false, togglePostProcessing: false, takeScreenshot: true, testNotification: false };
358
+ }
359
+
360
+ // Try to match arrow keys first
361
+ const arrowMatch = input.match(/^\x1b\[([ABCD])/);
362
+ if (arrowMatch) {
363
+ const button = LEGACY_KEY_TO_BUTTON.get(`\x1b[${arrowMatch[1]}`);
364
+ if (button !== undefined) {
365
+ this.handleLegacyKeyPress(`arrow_${arrowMatch[1]}`, button);
366
+ }
367
+ // Process rest of input if any
368
+ if (input.length > LEGACY_ARROW_KEY_SEQUENCE_LENGTH) {
369
+ return this.processLegacyInput(input.slice(LEGACY_ARROW_KEY_SEQUENCE_LENGTH));
370
+ }
371
+ return { quit: false, cycleRenderMode: this.cycleRenderModeRequested, toggleAudio: this.toggleAudioRequested, togglePostProcessing: this.togglePostProcessingRequested, takeScreenshot: this.takeScreenshotRequested, testNotification: this.testNotificationRequested };
372
+ }
373
+
374
+ // Process each character
375
+ for (const char of input) {
376
+ if (char === '\x1b') {
377
+ // Standalone escape - quit
378
+ this.quitRequested = true;
379
+ return { quit: true, cycleRenderMode: false, toggleAudio: false, togglePostProcessing: false, takeScreenshot: false, testNotification: false };
380
+ }
381
+
382
+ // Check for render mode toggle (R/r key)
383
+ if (char === 'r' || char === 'R') {
384
+ this.cycleRenderModeRequested = true;
385
+ continue;
386
+ }
387
+
388
+ // Check for audio toggle (M/m key)
389
+ if (char === 'm' || char === 'M') {
390
+ this.toggleAudioRequested = true;
391
+ continue;
392
+ }
393
+
394
+ // Check for post-processing toggle (P/p key)
395
+ if (char === 'p' || char === 'P') {
396
+ this.togglePostProcessingRequested = true;
397
+ continue;
398
+ }
399
+
400
+ // Check for test notification (N/n key)
401
+ if (char === 'n' || char === 'N') {
402
+ this.testNotificationRequested = true;
403
+ continue;
404
+ }
405
+
406
+ const button = LEGACY_KEY_TO_BUTTON.get(char);
407
+ if (button !== undefined) {
408
+ this.handleLegacyKeyPress(char, button);
409
+ }
410
+ }
411
+
412
+ return { quit: false, cycleRenderMode: this.cycleRenderModeRequested, toggleAudio: this.toggleAudioRequested, togglePostProcessing: this.togglePostProcessingRequested, takeScreenshot: this.takeScreenshotRequested, testNotification: this.testNotificationRequested };
413
+ }
414
+
415
+ /**
416
+ * Handle key press in legacy mode with auto-release timing.
417
+ */
418
+ private handleLegacyKeyPress(key: string, button: Button): void {
419
+ const now = Date.now();
420
+
421
+ // Release opposite direction
422
+ const oppositeButton = OPPOSITE_DIRECTIONS.get(button);
423
+ if (oppositeButton !== undefined) {
424
+ this.controller1.setButton(oppositeButton, false);
425
+ // Remove any opposite direction keys from timing map
426
+ for (const [k] of this.legacyKeyTimes.entries()) {
427
+ const kButton = LEGACY_KEY_TO_BUTTON.get(k) ??
428
+ (k.startsWith('arrow_') ? this.getArrowButton(k) : undefined);
429
+ if (kButton === oppositeButton) {
430
+ this.legacyKeyTimes.delete(k);
431
+ }
432
+ }
433
+ }
434
+
435
+ // Press the button
436
+ this.controller1.setButton(button, true);
437
+ this.legacyKeyTimes.set(key, now);
438
+ }
439
+
440
+ /**
441
+ * Get button for arrow key string.
442
+ */
443
+ private getArrowButton(key: string): Button | undefined {
444
+ const map: Record<string, Button> = {
445
+ 'arrow_A': Button.Up,
446
+ 'arrow_B': Button.Down,
447
+ 'arrow_C': Button.Right,
448
+ 'arrow_D': Button.Left,
449
+ };
450
+ return map[key];
451
+ }
452
+
453
+ /**
454
+ * Handle Kitty keyboard protocol key event.
455
+ */
456
+ private handleKittyKey(keycode: number, eventType: number): void {
457
+ if (keycode === KITTY_KEY_ESCAPE) {
458
+ this.quitRequested = true;
459
+ return;
460
+ }
461
+
462
+ // Check for render mode toggle (R/r key) - only on key press, not release
463
+ if ((keycode === KEY_CODE_R_LOWER || keycode === KEY_CODE_R_UPPER) && (eventType === KITTY_EVENT_PRESS || eventType === KITTY_EVENT_REPEAT)) {
464
+ this.cycleRenderModeRequested = true;
465
+ return;
466
+ }
467
+
468
+ // Check for audio toggle (M/m key) - only on key press, not release
469
+ if ((keycode === KEY_CODE_M_LOWER || keycode === KEY_CODE_M_UPPER) && (eventType === KITTY_EVENT_PRESS || eventType === KITTY_EVENT_REPEAT)) {
470
+ this.toggleAudioRequested = true;
471
+ return;
472
+ }
473
+
474
+ // Check for post-processing toggle (P/p key) - only on key press, not release
475
+ if ((keycode === KEY_CODE_P_LOWER || keycode === KEY_CODE_P_UPPER) && (eventType === KITTY_EVENT_PRESS || eventType === KITTY_EVENT_REPEAT)) {
476
+ this.togglePostProcessingRequested = true;
477
+ return;
478
+ }
479
+
480
+ // Check for test notification (N/n key) - only on key press, not release
481
+ if ((keycode === KEY_CODE_N_LOWER || keycode === KEY_CODE_N_UPPER) && (eventType === KITTY_EVENT_PRESS || eventType === KITTY_EVENT_REPEAT)) {
482
+ this.testNotificationRequested = true;
483
+ return;
484
+ }
485
+
486
+ // Check for screenshot (F8=57383, F12=57387) - only on key press, not release
487
+ if ((keycode === KITTY_KEY_F8 || keycode === KITTY_KEY_F12) && (eventType === KITTY_EVENT_PRESS || eventType === KITTY_EVENT_REPEAT)) {
488
+ this.takeScreenshotRequested = true;
489
+ return;
490
+ }
491
+
492
+ let button = KITTY_KEY_TO_BUTTON.get(keycode);
493
+ if (button === undefined) {
494
+ button = KITTY_SPECIAL_KEYS.get(keycode);
495
+ }
496
+
497
+ if (button === undefined) {
498
+ return;
499
+ }
500
+
501
+ if (eventType === KITTY_EVENT_PRESS || eventType === KITTY_EVENT_REPEAT) {
502
+ this.handleKeyDown(keycode, button);
503
+ } else if (eventType === KITTY_EVENT_RELEASE) {
504
+ this.handleKeyUp(keycode, button);
505
+ }
506
+ }
507
+
508
+ /**
509
+ * Handle key down event (Kitty mode).
510
+ */
511
+ private handleKeyDown(keycode: number, button: Button): void {
512
+ const oppositeButton = OPPOSITE_DIRECTIONS.get(button);
513
+ if (oppositeButton !== undefined) {
514
+ for (const [pressedKeycode, pressedButton] of this.pressedKeys.entries()) {
515
+ if (pressedButton === oppositeButton) {
516
+ this.controller1.setButton(oppositeButton, false);
517
+ this.pressedKeys.delete(pressedKeycode);
518
+ break;
519
+ }
520
+ }
521
+ }
522
+
523
+ this.controller1.setButton(button, true);
524
+ this.pressedKeys.set(keycode, button);
525
+ }
526
+
527
+ /**
528
+ * Handle key up event (Kitty mode).
529
+ */
530
+ private handleKeyUp(keycode: number, button: Button): void {
531
+ if (this.pressedKeys.has(keycode)) {
532
+ this.controller1.setButton(button, false);
533
+ this.pressedKeys.delete(keycode);
534
+ }
535
+ }
536
+
537
+ /**
538
+ * Update - called each frame.
539
+ * In legacy mode, auto-releases keys that haven't been re-pressed.
540
+ */
541
+ update(): void {
542
+ if (!this.kittyMode) {
543
+ const now = Date.now();
544
+
545
+ // Check for keys that should be auto-released
546
+ for (const [key, pressTime] of this.legacyKeyTimes.entries()) {
547
+ if (now - pressTime > LEGACY_KEY_RELEASE_TIME_MS) {
548
+ const button = LEGACY_KEY_TO_BUTTON.get(key) ??
549
+ (key.startsWith('arrow_') ? this.getArrowButton(key) : undefined);
550
+ if (button !== undefined) {
551
+ this.controller1.setButton(button, false);
552
+ }
553
+ this.legacyKeyTimes.delete(key);
554
+ }
555
+ }
556
+ }
557
+ }
558
+
559
+ /**
560
+ * Get currently pressed buttons as a string for display.
561
+ */
562
+ getPressedButtons(): string {
563
+ return this.controller1.getPressedButtons();
564
+ }
565
+
566
+ /**
567
+ * Get debug info.
568
+ */
569
+ getDebugInfo(): string {
570
+ const mode = this.kittyMode ? 'Kitty' : 'Legacy';
571
+ const keys = this.kittyMode ? this.pressedKeys.size : this.legacyKeyTimes.size;
572
+ return `${mode} Keys:${keys}`;
573
+ }
574
+
575
+ /**
576
+ * Clear all input state.
577
+ */
578
+ clear(): void {
579
+ for (const button of this.pressedKeys.values()) {
580
+ this.controller1.setButton(button, false);
581
+ }
582
+ this.pressedKeys.clear();
583
+
584
+ for (const [key] of this.legacyKeyTimes) {
585
+ const button = LEGACY_KEY_TO_BUTTON.get(key) ??
586
+ (key.startsWith('arrow_') ? this.getArrowButton(key) : undefined);
587
+ if (button !== undefined) {
588
+ this.controller1.setButton(button, false);
589
+ }
590
+ }
591
+ this.legacyKeyTimes.clear();
592
+ }
593
+ }
@@ -0,0 +1,33 @@
1
+ import { StandardButton } from '../../core/button';
2
+
3
+ /** Mapping from button name patterns to StandardButton */
4
+ export const BUTTON_NAME_MAP: Array<{ names: string[]; button: StandardButton }> = [
5
+ { names: ['a'], button: StandardButton.A },
6
+ { names: ['b'], button: StandardButton.B },
7
+ { names: ['x'], button: StandardButton.X },
8
+ { names: ['y'], button: StandardButton.Y },
9
+ { names: ['l', 'l1', 'lb'], button: StandardButton.L },
10
+ { names: ['r', 'r1', 'rb'], button: StandardButton.R },
11
+ { names: ['l2', 'lt', 'z'], button: StandardButton.L2 },
12
+ { names: ['r2', 'rt'], button: StandardButton.R2 },
13
+ { names: ['l3', 'ls'], button: StandardButton.L3 },
14
+ { names: ['r3', 'rs'], button: StandardButton.R3 },
15
+ { names: ['start'], button: StandardButton.Start },
16
+ { names: ['select', 'back'], button: StandardButton.Select },
17
+ { names: ['up'], button: StandardButton.Up },
18
+ { names: ['down'], button: StandardButton.Down },
19
+ { names: ['left'], button: StandardButton.Left },
20
+ { names: ['right'], button: StandardButton.Right },
21
+ ];
22
+
23
+ /** Analog stick indices */
24
+ export const ANALOG_INDEX = {
25
+ LEFT: 0,
26
+ RIGHT: 1,
27
+ } as const;
28
+
29
+ /** Analog axis indices */
30
+ export const ANALOG_AXIS = {
31
+ X: 0,
32
+ Y: 1,
33
+ } as const;