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.
- package/.claude/settings.local.json +77 -0
- package/.node-version +1 -0
- package/CLAUDE.md +435 -0
- package/README.md +404 -0
- package/TODO.md +655 -0
- package/dist/index.cjs +25108 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +25085 -0
- package/docs/building-libretro-cores-arm-mac.md +237 -0
- package/docs/config-file-format.md +488 -0
- package/docs/cores-trd.md +425 -0
- package/docs/headless-hardware-rendering-trd.md +676 -0
- package/docs/libretro-cores-trd.md +997 -0
- package/docs/mupen64-software-rendering-trd.md +751 -0
- package/docs/n64-support-trd.md +306 -0
- package/docs/native-rendering-trd.md +540 -0
- package/docs/native-ui-rendering-trd.md +1195 -0
- package/docs/netplay-trd.md +665 -0
- package/docs/retroarch-netplay-docs.md +277 -0
- package/docs/save-state-format.md +666 -0
- package/eslint.config.js +111 -0
- package/icon/icon.png +0 -0
- package/icon/icon.pxd +0 -0
- package/package.json +63 -0
- package/pnpm-workspace.yaml +10 -0
- package/src/Emulator/consts.ts +14 -0
- package/src/Emulator/index.ts +2496 -0
- package/src/Emulator/saveState/index.ts +155 -0
- package/src/Emulator/screenshot/index.ts +160 -0
- package/src/Emulator/terminalDimensions/index.ts +79 -0
- package/src/Emulator/types.ts +83 -0
- package/src/cli/commands/consts.ts +10 -0
- package/src/cli/commands/index.ts +462 -0
- package/src/cli/parseArgs/consts.ts +17 -0
- package/src/cli/parseArgs/index.ts +457 -0
- package/src/cli/parseArgs/types.ts +61 -0
- package/src/cli/runEmulator/index.ts +406 -0
- package/src/cli/runEmulator/types.ts +7 -0
- package/src/consts.ts +19 -0
- package/src/core/button/consts.ts +35 -0
- package/src/core/button/index.ts +123 -0
- package/src/core/core.ts +300 -0
- package/src/core/index.ts +19 -0
- package/src/cores/libretro/api/index.ts +334 -0
- package/src/cores/libretro/api/types.ts +148 -0
- package/src/cores/libretro/callbacks/consts.ts +41 -0
- package/src/cores/libretro/callbacks/index.ts +456 -0
- package/src/cores/libretro/consts.ts +45 -0
- package/src/cores/libretro/coreOptions/consts.ts +36 -0
- package/src/cores/libretro/coreOptions/index.ts +222 -0
- package/src/cores/libretro/environment/consts.ts +118 -0
- package/src/cores/libretro/environment/index.ts +1095 -0
- package/src/cores/libretro/index.ts +937 -0
- package/src/cores/libretro/loader/index.ts +496 -0
- package/src/cores/libretro/pixelFormat/consts.ts +43 -0
- package/src/cores/libretro/pixelFormat/index.ts +397 -0
- package/src/cores/libretro/types.ts +339 -0
- package/src/frontend/AudioManager/index.ts +420 -0
- package/src/frontend/SettingsManager/index.ts +250 -0
- package/src/frontend/config/index.ts +608 -0
- package/src/frontend/config/tests.ts +354 -0
- package/src/frontend/config/types.ts +36 -0
- package/src/frontend/consts.ts +114 -0
- package/src/frontend/coreBuilder/index.ts +644 -0
- package/src/frontend/coreBuilder/types.ts +15 -0
- package/src/frontend/coreDownloader/index.ts +620 -0
- package/src/frontend/coreDownloader/types.ts +17 -0
- package/src/frontend/corePreferences/index.ts +69 -0
- package/src/frontend/coreRegistry/index.ts +276 -0
- package/src/frontend/directoryCache/index.ts +75 -0
- package/src/frontend/index.ts +79 -0
- package/src/frontend/notifications/consts.ts +14 -0
- package/src/frontend/notifications/index.ts +250 -0
- package/src/frontend/playlist/consts.ts +168 -0
- package/src/frontend/playlist/index.ts +899 -0
- package/src/frontend/playlist/labelFormatter/consts.ts +15 -0
- package/src/frontend/playlist/labelFormatter/index.ts +155 -0
- package/src/frontend/playlist/labelFormatter/tests.ts +153 -0
- package/src/frontend/playlist/reader/index.ts +559 -0
- package/src/frontend/playlist/sync/index.ts +511 -0
- package/src/frontend/playlist/systemLookup/index.ts +233 -0
- package/src/frontend/playlist/utils/index.ts +50 -0
- package/src/frontend/romScanner/consts.ts +348 -0
- package/src/frontend/romScanner/index.ts +1957 -0
- package/src/frontend/saveServices/consts.ts +2 -0
- package/src/frontend/saveServices/index.ts +313 -0
- package/src/frontend/serviceProvider/index.ts +108 -0
- package/src/frontend/serviceProvider/types.ts +13 -0
- package/src/index.ts +428 -0
- package/src/input/Controller/consts.ts +50 -0
- package/src/input/Controller/index.ts +81 -0
- package/src/input/GamepadManager/consts.ts +22 -0
- package/src/input/GamepadManager/index.ts +418 -0
- package/src/input/InputManager/consts.ts +86 -0
- package/src/input/InputManager/index.ts +593 -0
- package/src/input/InputMapper/consts.ts +33 -0
- package/src/input/InputMapper/index.ts +436 -0
- package/src/input/consts.ts +410 -0
- package/src/input/gamepadProfiles/index.ts +1100 -0
- package/src/input/index.ts +38 -0
- package/src/input/inputUtils/index.ts +31 -0
- package/src/netplay/FrameBuffer/consts.ts +2 -0
- package/src/netplay/FrameBuffer/index.ts +364 -0
- package/src/netplay/FrameBuffer/tests.ts +286 -0
- package/src/netplay/InputBuffer/consts.ts +7 -0
- package/src/netplay/InputBuffer/index.ts +347 -0
- package/src/netplay/InputBuffer/tests.ts +166 -0
- package/src/netplay/NetplayClient/index.ts +976 -0
- package/src/netplay/NetplayConnection/index.ts +536 -0
- package/src/netplay/NetplayDiscovery/consts.ts +41 -0
- package/src/netplay/NetplayDiscovery/index.ts +525 -0
- package/src/netplay/NetplayServer/index.ts +1407 -0
- package/src/netplay/SyncManager/index.ts +984 -0
- package/src/netplay/SyncManager/tests.ts +419 -0
- package/src/netplay/consts.ts +371 -0
- package/src/netplay/crc32/consts.ts +14 -0
- package/src/netplay/crc32/index.ts +97 -0
- package/src/netplay/crc32/tests.ts +40 -0
- package/src/netplay/index.ts +41 -0
- package/src/netplay/netplayLogger/consts.ts +30 -0
- package/src/netplay/netplayLogger/index.ts +345 -0
- package/src/netplay/protocol/consts.ts +86 -0
- package/src/netplay/protocol/index.ts +1280 -0
- package/src/netplay/protocol/tests.ts +606 -0
- package/src/netplay/protocol/types.ts +20 -0
- package/src/netplay/types.ts +395 -0
- package/src/rendering/KittyRenderer/index.ts +616 -0
- package/src/rendering/NativeRenderer/index.ts +279 -0
- package/src/rendering/NativeRenderer/tests.ts +133 -0
- package/src/rendering/TerminalRenderer/index.ts +770 -0
- package/src/rendering/consts.ts +401 -0
- package/src/rendering/fonts/CozetteVector.ttf +0 -0
- package/src/rendering/index.ts +26 -0
- package/src/rendering/nativeUi/NativeWindowManager/index.ts +158 -0
- package/src/rendering/nativeUi/NativeWindowManager/tests.ts +81 -0
- package/src/rendering/nativeUi/consts.ts +6 -0
- package/src/rendering/nativeUi/index.ts +20 -0
- package/src/rendering/postProcessing/consts.ts +38 -0
- package/src/rendering/postProcessing/index.ts +923 -0
- package/src/rendering/shared/ansi/consts.ts +12 -0
- package/src/rendering/shared/ansi/index.ts +104 -0
- package/src/rendering/shared/consts.ts +2 -0
- package/src/rendering/shared/fitToTerminal/index.ts +67 -0
- package/src/ui/AddRomsPrompt/consts.ts +13 -0
- package/src/ui/AddRomsPrompt/index.tsx +781 -0
- package/src/ui/App/consts.ts +2 -0
- package/src/ui/App/index.tsx +456 -0
- package/src/ui/AppCapabilities/index.tsx +67 -0
- package/src/ui/ConfigContext/index.tsx +56 -0
- package/src/ui/CoreManager/consts.ts +11 -0
- package/src/ui/CoreManager/index.tsx +779 -0
- package/src/ui/CoreSelector/consts.ts +2 -0
- package/src/ui/CoreSelector/index.tsx +251 -0
- package/src/ui/DialogContainer/index.tsx +42 -0
- package/src/ui/DialogOptionsList/index.tsx +61 -0
- package/src/ui/DuplicateCrcPrompt/consts.ts +5 -0
- package/src/ui/DuplicateCrcPrompt/index.tsx +146 -0
- package/src/ui/GamepadContext/consts.ts +15 -0
- package/src/ui/GamepadContext/index.tsx +295 -0
- package/src/ui/NativeDialog/index.tsx +120 -0
- package/src/ui/NetplayDisconnectedDialog/index.tsx +93 -0
- package/src/ui/NetplayPauseMenu/consts.ts +2 -0
- package/src/ui/NetplayPauseMenu/index.tsx +97 -0
- package/src/ui/RomBrowser/NetplayPanel/consts.ts +24 -0
- package/src/ui/RomBrowser/NetplayPanel/index.tsx +520 -0
- package/src/ui/RomBrowser/SettingsPanel/index.tsx +478 -0
- package/src/ui/RomBrowser/consts.ts +61 -0
- package/src/ui/RomBrowser/index.tsx +1164 -0
- package/src/ui/RomBrowser/settingsConfig/index.ts +320 -0
- package/src/ui/RomBrowser/types.ts +67 -0
- package/src/ui/SaveStateDialog/consts.ts +2 -0
- package/src/ui/SaveStateDialog/index.tsx +225 -0
- package/src/ui/WarningDialog/index.tsx +113 -0
- package/src/ui/consts.ts +27 -0
- package/src/ui/hooks/useClearTerminal/consts.ts +2 -0
- package/src/ui/hooks/useClearTerminal/index.ts +37 -0
- package/src/ui/hooks/useDialogNavigation/index.ts +99 -0
- package/src/ui/hooks/useGamepad/consts.ts +21 -0
- package/src/ui/hooks/useGamepad/index.ts +194 -0
- package/src/ui/index.ts +27 -0
- package/src/utils/buffer/consts.ts +17 -0
- package/src/utils/buffer/index.ts +129 -0
- package/src/utils/color/consts.ts +58 -0
- package/src/utils/color/index.ts +183 -0
- package/src/utils/compression/consts.ts +50 -0
- package/src/utils/compression/index.ts +101 -0
- package/src/utils/consts.ts +2 -0
- package/src/utils/crc32/consts.ts +22 -0
- package/src/utils/crc32/index.ts +83 -0
- package/src/utils/ensureDirectory/index.ts +10 -0
- package/src/utils/format/consts.ts +8 -0
- package/src/utils/format/index.ts +53 -0
- package/src/utils/getErrorMessage/index.ts +10 -0
- package/src/utils/index.ts +113 -0
- package/src/utils/ini/index.ts +200 -0
- package/src/utils/kitty/consts.ts +13 -0
- package/src/utils/kitty/index.ts +181 -0
- package/src/utils/logger/consts.ts +35 -0
- package/src/utils/logger/index.ts +217 -0
- package/src/utils/paths/consts.ts +18 -0
- package/src/utils/paths/index.ts +151 -0
- package/src/utils/png/consts.ts +34 -0
- package/src/utils/png/index.ts +131 -0
- package/src/utils/readJsonFile/index.ts +16 -0
- package/src/utils/rotateLogFile/index.ts +44 -0
- package/src/utils/safeClose/index.ts +10 -0
- package/src/utils/terminal/consts.ts +8 -0
- package/src/utils/terminal/index.ts +102 -0
- package/src/utils/thumbnailRenderer/consts.ts +2 -0
- package/src/utils/thumbnailRenderer/index.ts +147 -0
- package/src/utils/typedError/index.ts +26 -0
- package/tsconfig.json +31 -0
- 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;
|