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,462 @@
1
+ import { GamepadManager } from '../../input/GamepadManager';
2
+ import HID from 'node-hid';
3
+ import { existsSync, readdirSync, unlinkSync } from 'fs';
4
+ import { getDefaultLogsDirectory } from '../../utils/paths';
5
+ import { logger } from '../../utils/logger';
6
+ import { getErrorMessage } from '../../utils/getErrorMessage';
7
+ import { join } from 'path';
8
+ import {
9
+ listCores,
10
+ getSupportedExtensions,
11
+ unregisterCore,
12
+ } from '../../frontend/coreRegistry';
13
+ import { HEX_RADIX, BYTE_DECIMAL_PAD_WIDTH } from '../../frontend';
14
+ import { scanDirectory } from '../../frontend/romScanner';
15
+ import {
16
+ generatePlaylistsBySystem,
17
+ generateConsolidatedPlaylist,
18
+ } from '../../frontend/playlist';
19
+ import type { PlaylistOptions } from '../../frontend/playlist';
20
+ import { downloadCore } from '../../frontend/coreDownloader';
21
+ import { registerLibretroCore, unloadLibretroCore } from '../../cores/libretro/loader';
22
+ import {
23
+ LINE_CLEAR_WIDTH,
24
+ PERCENT_MULTIPLIER,
25
+ BYTES_PER_KB,
26
+ PERCENT_SUFFIX_WIDTH,
27
+ ELLIPSIS_LENGTH,
28
+ } from './consts';
29
+
30
+ export * from './consts';
31
+
32
+ export const printUsage = (): void => {
33
+ // Get supported extensions from core registry
34
+ const extensions = getSupportedExtensions().join(", ");
35
+
36
+ console.log(`
37
+ emoemu - Terminal Retro Emulator
38
+
39
+ Usage: emoemu <rom> [options]
40
+
41
+ Options:
42
+ --config <path> Use a custom config file path
43
+ --core <id> Use a specific emulator core (see --list-cores)
44
+ --list-cores List available emulator cores and exit
45
+ --install-core <name> Download/build and install a libretro core by name
46
+ (e.g., --install-core mupen64plus_next)
47
+ --remove-core <id> Remove an installed libretro core (use same ID as --core)
48
+ --retroarch Also load libretro cores from RetroArch installation paths
49
+ --native Use native window rendering (best performance)
50
+ --kitty Use Kitty graphics protocol (default, best quality)
51
+ --terminal Use terminal character rendering (Unicode half-blocks)
52
+ --ascii Use colored ASCII character rendering
53
+ --emoji Use emoji character rendering
54
+ --no-color Disable colors (use with --ascii or --terminal)
55
+ --scale <n> Internal render scale for Kitty mode (default: 2)
56
+ --png-level <n> PNG compression level 1-9 for Kitty mode (default: 4)
57
+ Higher = smaller output, reduces terminal I/O bottleneck.
58
+ --crt CRT effect preset: --scale 1 --ntsc 1.0 --scanlines 0.1 --gamma 1.3 --vignette 0.5
59
+ --curvature 0.1 --chromatic-aberration 0.3
60
+ Individual flags can override these defaults
61
+ --gamma <n> Gamma correction (default: 1.0)
62
+ Values > 1.0 darken midtones (CRT-like), try 1.1-1.4
63
+ --scanlines [n] Scanline intensity (default: 0.3 if enabled)
64
+ Values 0.0-1.0, try 0.2-0.4 for subtle CRT effect
65
+ --saturation <n> Color saturation (default: 1.0)
66
+ Values > 1.0 boost colors (CRT-like), try 1.1-1.3
67
+ --brightness <n> Brightness multiplier (default: 1.0)
68
+ Values > 1.0 brighten, < 1.0 darken
69
+ --contrast <n> Contrast adjustment (default: 1.0)
70
+ Values > 1.0 increase contrast, < 1.0 decrease
71
+ --vignette [n] Vignette edge darkening (default: 1.0 if enabled)
72
+ Values 0.3-0.5 for subtle CRT-like edge darkening
73
+ --bloom [n] Phosphor bloom/glow for Kitty mode only (default: 0.5 if enabled)
74
+ Values 0.3-0.6 for subtle CRT phosphor glow
75
+ --bloom-threshold <n> Brightness threshold for bloom (default: 0.6)
76
+ Pixels brighter than this emit glow (range 0-1)
77
+ --ntsc [n] NTSC color artifacts for Kitty mode only (default: 1.0 if enabled)
78
+ Simulates horizontal color bleeding from composite video
79
+ --curvature [n] CRT screen curvature for Kitty mode only (default: 0.1 if enabled)
80
+ Barrel distortion to simulate curved CRT glass, try 0.1-0.3
81
+ --chromatic-aberration [n] RGB color fringing for Kitty mode only (default: 0.5 if enabled)
82
+ Simulates CRT electron beam convergence errors, try 0.3-1.0
83
+ --width <n> Set display width in characters (terminal/ascii mode)
84
+ --height <n> Set display height in characters (terminal/ascii mode)
85
+ --list-gamepads List detected gamepad/controller devices and exit
86
+ --no-gamepad Disable gamepad support
87
+ --no-audio Disable audio output
88
+ --no-save-state Disable save state loading and saving
89
+ --no-battery-save Disable battery save (.srm) loading and saving
90
+ --status Show the status bar (disabled by default)
91
+ --no-diff-render Disable diff-based rendering optimization
92
+ --no-render Disable video rendering (for debugging, audio/emulation still run)
93
+ --fps-limit <n> Override FPS limit (0 = uncapped, default: core native)
94
+ --frame-limit <n> Limit rendering to N fps (0 = off, default: 0)
95
+ Common values: 30, 60. Reduces terminal I/O while
96
+ emulation runs at full speed. Useful over SSH.
97
+ --debug-gamepad Show raw gamepad HID data (for debugging)
98
+ --clear-logs Delete all emoemu log files
99
+ --verbose Enable verbose logging to stderr (RetroArch-style)
100
+ --help Show this help message
101
+
102
+ Browser:
103
+ --scan-depth <n> Max depth to scan for ROMs (default: 1)
104
+ 0 = only specified directory
105
+ 1 = directory + immediate subdirectories
106
+ -1 = unlimited (scan all subdirectories)
107
+
108
+ Playlist Generation (RetroArch-compatible .lpl files):
109
+ --generate-playlist [path] Scan directory for ROMs and generate a playlist
110
+ If path is omitted, uses current directory
111
+ Creates per-system playlists (e.g., "Nintendo - NES.lpl")
112
+ --playlist-output <dir> Output directory for playlists (default: ./playlists)
113
+ --single-playlist <name> Generate one consolidated playlist instead of per-system
114
+ Specify the playlist name (without .lpl extension)
115
+ --windows-paths Use Windows-style backslash separators in paths
116
+
117
+ Netplay (RetroArch-compatible multiplayer):
118
+ --netplay-host Host a netplay session (server mode)
119
+ --netplay-connect [host] Connect to a netplay server (host or host:port)
120
+ If host is omitted, auto-discovers LAN hosts
121
+ --netplay-port <n> Port for netplay (default: 55435)
122
+ --netplay-password <pw> Password for netplay session
123
+ --netplay-spectate Join as spectator (view only)
124
+ --netplay-nick <name> Set your nickname (default: Player)
125
+ --netplay-frames <n> Input delay frames 0-16 (default: 0)
126
+ Higher values reduce rollbacks at cost of latency
127
+
128
+ Supported ROM formats: ${extensions}
129
+
130
+ Controls:
131
+ W/Arrow Up D-Pad Up
132
+ S/Arrow Down D-Pad Down
133
+ A/Arrow Left D-Pad Left
134
+ D/Arrow Right D-Pad Right
135
+ K/Z A Button (Jump/Action)
136
+ J/X B Button (Run)
137
+ Enter Start
138
+ Space Select
139
+ Escape/Ctrl+C Quit
140
+
141
+ Note: You can hold buttons and press multiple buttons simultaneously
142
+ (e.g., hold D + J to run right, then press K to jump)
143
+ `);
144
+ };
145
+
146
+ export const debugGamepad = (): void => {
147
+ console.log("Gamepad Debug Mode");
148
+ console.log("==================");
149
+ console.log("Press Ctrl+C to exit\n");
150
+
151
+ const devices = GamepadManager.listDevices();
152
+ if (devices.length === 0) {
153
+ console.log("No gamepad devices found.");
154
+ process.exit(1);
155
+ }
156
+
157
+ const deviceInfo = devices[0];
158
+ console.log(`Connecting to: ${deviceInfo.product}`);
159
+ console.log(`Path: ${deviceInfo.path}\n`);
160
+
161
+ try {
162
+ const device = new HID.HID(deviceInfo.path);
163
+ let lastData = "";
164
+
165
+ device.on("data", (data: Buffer) => {
166
+ // Only print if data changed (reduces noise)
167
+ const hexStr = Array.from(data)
168
+ .map(
169
+ (b, i) =>
170
+ `${i.toString().padStart(2)}:${b.toString(HEX_RADIX).padStart(2, "0")}`
171
+ )
172
+ .join(" ");
173
+
174
+ if (hexStr !== lastData) {
175
+ // Show byte index and value in both hex and decimal
176
+ console.log(`\nBytes (${data.length}):`);
177
+ const parts: string[] = [];
178
+ for (let i = 0; i < data.length; i++) {
179
+ parts.push(
180
+ `[${i}]=0x${data[i].toString(HEX_RADIX).padStart(2, "0")}(${data[i]
181
+ .toString()
182
+ .padStart(BYTE_DECIMAL_PAD_WIDTH)})`
183
+ );
184
+ }
185
+ console.log(parts.join(" "));
186
+ lastData = hexStr;
187
+ }
188
+ });
189
+
190
+ device.on("error", (err) => {
191
+ console.error("Device error:", err);
192
+ process.exit(1);
193
+ });
194
+
195
+ // Keep running
196
+ process.on("SIGINT", () => {
197
+ device.close();
198
+ process.exit(0);
199
+ });
200
+ } catch (err) {
201
+ console.error("Failed to open device:", err);
202
+ process.exit(1);
203
+ }
204
+ };
205
+
206
+ export const listGamepads = (): void => {
207
+ console.log("Detected Gamepad Devices");
208
+ console.log("========================\n");
209
+
210
+ const devices = GamepadManager.listDevices();
211
+
212
+ if (devices.length === 0) {
213
+ console.log("No gamepad devices detected.\n");
214
+ console.log("Tips:");
215
+ console.log(
216
+ " - Make sure your controller is connected and paired (for Bluetooth)"
217
+ );
218
+ console.log(" - Try pressing a button on the controller to wake it up");
219
+ console.log(
220
+ ' - On Linux, you may need to add your user to the "input" group\n'
221
+ );
222
+ } else {
223
+ const HEX_ID_WIDTH = 4;
224
+ for (const device of devices) {
225
+ console.log(`${device.product}`);
226
+ console.log(` Manufacturer: ${device.manufacturer}`);
227
+ console.log(
228
+ ` Vendor ID: 0x${device.vendorId.toString(HEX_RADIX).padStart(HEX_ID_WIDTH, "0")}`
229
+ );
230
+ console.log(
231
+ ` Product ID: 0x${device.productId.toString(HEX_RADIX).padStart(HEX_ID_WIDTH, "0")}`
232
+ );
233
+ console.log(` Profile: ${device.profile}`);
234
+ console.log("");
235
+ }
236
+ }
237
+
238
+ console.log("Supported Controllers:");
239
+ for (const profile of GamepadManager.getSupportedProfiles()) {
240
+ console.log(` - ${profile}`);
241
+ }
242
+ console.log(" - Generic USB Gamepad (fallback)");
243
+ };
244
+
245
+ export const listCoresCommand = (): void => {
246
+ console.log("Available Emulator Cores");
247
+ console.log("========================\n");
248
+
249
+ const cores = listCores();
250
+
251
+ if (cores.length === 0) {
252
+ console.log("No cores registered.\n");
253
+ } else {
254
+ for (const core of cores) {
255
+ console.log(`${core.name} (--core ${core.id})`);
256
+ console.log(` Extensions: ${core.extensions.join(", ")}`);
257
+ console.log(` Path: ${core.path}`);
258
+ console.log("");
259
+ }
260
+ }
261
+
262
+ console.log("Note: Cores are auto-detected by ROM file extension.");
263
+ console.log(" Use --core <id> to override auto-detection.");
264
+ };
265
+
266
+ /** Format bytes as human-readable string */
267
+ const formatBytes = (bytes: number): string => {
268
+ const mb = BYTES_PER_KB * BYTES_PER_KB;
269
+ if (bytes >= mb) {
270
+ return `${(bytes / mb).toFixed(1)} MB`;
271
+ } else if (bytes >= BYTES_PER_KB) {
272
+ return `${(bytes / BYTES_PER_KB).toFixed(1)} KB`;
273
+ }
274
+ return `${bytes} B`;
275
+ };
276
+
277
+ /** Install/build a libretro core by name */
278
+ export const installCoreCommand = async (coreName: string): Promise<void> => {
279
+ console.log(`Installing core: ${coreName}`);
280
+ console.log("");
281
+
282
+ try {
283
+ const corePath = await downloadCore(coreName, (progress) => {
284
+ if (progress.phase === "downloading") {
285
+ const percent = progress.totalBytes
286
+ ? Math.round((progress.bytesDownloaded / progress.totalBytes) * PERCENT_MULTIPLIER)
287
+ : 0;
288
+ const downloaded = formatBytes(progress.bytesDownloaded);
289
+ const total = progress.totalBytes ? formatBytes(progress.totalBytes) : "unknown";
290
+ // Use carriage return to update the same line
291
+ process.stdout.write(`\rDownloading: ${downloaded} / ${total} (${percent}%)`);
292
+ } else if (progress.phase === "extracting") {
293
+ // Clear the download line and show extracting status
294
+ process.stdout.write("\r" + " ".repeat(LINE_CLEAR_WIDTH) + "\r");
295
+ console.log("Extracting...");
296
+ } else if (progress.phase === "building") {
297
+ // Building from source - show progress percentage with build output
298
+ if (progress.buildProgressPercent !== undefined && progress.buildMessage) {
299
+ const percent = progress.buildProgressPercent;
300
+ // Truncate message to fit on one line with percentage
301
+ const maxMsgLen = LINE_CLEAR_WIDTH - PERCENT_SUFFIX_WIDTH;
302
+ const msg = progress.buildMessage.length > maxMsgLen
303
+ ? progress.buildMessage.slice(0, maxMsgLen - ELLIPSIS_LENGTH) + "..."
304
+ : progress.buildMessage.padEnd(maxMsgLen);
305
+ // Use carriage return to update the same line
306
+ process.stdout.write(`\r${msg} (${percent}%)`);
307
+ } else if (progress.buildMessage) {
308
+ // No progress data - just log the message (used during initial analysis)
309
+ process.stdout.write("\r" + " ".repeat(LINE_CLEAR_WIDTH) + "\r");
310
+ console.log(progress.buildMessage);
311
+ }
312
+ } else {
313
+ // phase === "complete" - clear any partial line
314
+ process.stdout.write("\r" + " ".repeat(LINE_CLEAR_WIDTH) + "\r");
315
+ }
316
+ });
317
+
318
+ // Register the newly installed core so it can be used immediately
319
+ registerLibretroCore(corePath);
320
+
321
+ console.log(`Successfully installed: ${corePath}`);
322
+ } catch (error) {
323
+ const errorMessage = getErrorMessage(error);
324
+ console.error(`\nError installing core: ${errorMessage}`);
325
+ logger.error(`Failed to install core ${coreName}: ${errorMessage}`, "CLI");
326
+ process.exit(1);
327
+ }
328
+ };
329
+
330
+ /**
331
+ * Remove an installed libretro core by ID
332
+ * Uses the same core ID shown in --list-cores and accepted by --core
333
+ */
334
+ export const removeCoreCommand = (coreId: string): void => {
335
+ // Look up the core in the registry by ID
336
+ const cores = listCores();
337
+ const core = cores.find(c => c.id === coreId);
338
+
339
+ if (!core) {
340
+ console.error(`Core "${coreId}" is not installed.`);
341
+ console.error("Use --list-cores to see installed cores.");
342
+ process.exit(1);
343
+ }
344
+
345
+ // Don't allow removing native cores
346
+ try {
347
+ // Delete the core file
348
+ unlinkSync(core.path);
349
+ // Unregister from the core registry
350
+ unregisterCore(coreId);
351
+ // Clean up libretro loader tracking
352
+ unloadLibretroCore(core.path, coreId);
353
+ console.log(`Successfully removed: ${core.path}`);
354
+ logger.info(`Removed core ${coreId} from ${core.path}`, "CLI");
355
+ } catch (error) {
356
+ const errorMessage = getErrorMessage(error);
357
+ console.error(`Error removing core: ${errorMessage}`);
358
+ logger.error(`Failed to remove core ${coreId}: ${errorMessage}`, "CLI");
359
+ process.exit(1);
360
+ }
361
+ };
362
+
363
+ /** Delete all emoemu log files */
364
+ export const clearLogsCommand = (): void => {
365
+ const logsDir = getDefaultLogsDirectory();
366
+
367
+ if (!existsSync(logsDir)) {
368
+ console.log("No logs directory found. Nothing to delete.");
369
+ return;
370
+ }
371
+
372
+ const files = readdirSync(logsDir);
373
+ const logFiles = files.filter(f => f.endsWith('.log') || /\.log\.\d+$/.test(f));
374
+
375
+ if (logFiles.length === 0) {
376
+ console.log("No log files found.");
377
+ return;
378
+ }
379
+
380
+ let deleted = 0;
381
+ for (const file of logFiles) {
382
+ try {
383
+ unlinkSync(join(logsDir, file));
384
+ deleted++;
385
+ } catch {
386
+ console.error(`Failed to delete: ${file}`);
387
+ }
388
+ }
389
+
390
+ console.log(`Deleted ${deleted} log file${deleted === 1 ? '' : 's'} from ${logsDir}`);
391
+ };
392
+
393
+ /** Generate RetroArch-compatible playlists from scanned ROMs */
394
+ export const generatePlaylistCommand = (
395
+ scanPath: string,
396
+ scanDepth: number,
397
+ outputDir: string,
398
+ singlePlaylist: string | undefined,
399
+ windowsPaths: boolean
400
+ ): void => {
401
+ console.log("Generating RetroArch Playlists");
402
+ console.log("==============================\n");
403
+
404
+ console.log(`Scanning: ${scanPath}`);
405
+ console.log(`Scan depth: ${scanDepth === -1 ? 'unlimited' : scanDepth}`);
406
+ console.log(`Output: ${outputDir}`);
407
+ if (singlePlaylist) {
408
+ console.log(`Mode: single playlist (${singlePlaylist}.lpl)`);
409
+ } else {
410
+ console.log(`Mode: per-system playlists`);
411
+ }
412
+ console.log("");
413
+
414
+ // Scan for ROMs (CRC cache built automatically from playlists)
415
+ console.log("Scanning for ROMs...");
416
+ const roms = scanDirectory(scanPath, scanDepth);
417
+
418
+ if (roms.length === 0) {
419
+ console.log("No ROMs found.\n");
420
+ console.log("Make sure you have libretro cores installed to detect ROM types.");
421
+ console.log("Use --retroarch to load cores from RetroArch installation.");
422
+ return;
423
+ }
424
+
425
+ console.log(`Found ${roms.length} ROM(s)\n`);
426
+
427
+ // Build playlist options
428
+ const playlistOptions: PlaylistOptions = {
429
+ windowsPaths,
430
+ };
431
+
432
+ // Generate playlists
433
+ if (singlePlaylist) {
434
+ // Single consolidated playlist
435
+ const outputPath = join(outputDir, singlePlaylist);
436
+ const result = generateConsolidatedPlaylist(roms, outputPath, playlistOptions);
437
+
438
+ if (result.success) {
439
+ console.log(`Created: ${result.outputPath} (${result.entryCount} entries)`);
440
+ } else {
441
+ console.error(`Error: ${result.error}`);
442
+ }
443
+ } else {
444
+ // Per-system playlists
445
+ const results = generatePlaylistsBySystem(roms, outputDir, playlistOptions);
446
+
447
+ let successCount = 0;
448
+ let totalEntries = 0;
449
+
450
+ for (const result of results) {
451
+ if (result.success) {
452
+ console.log(`Created: ${result.outputPath} (${result.entryCount} entries)`);
453
+ successCount++;
454
+ totalEntries += result.entryCount;
455
+ } else {
456
+ console.error(`Error: ${result.error}`);
457
+ }
458
+ }
459
+
460
+ console.log(`\nGenerated ${successCount} playlist(s) with ${totalEntries} total entries.`);
461
+ }
462
+ };
@@ -0,0 +1,17 @@
1
+ // Default values for optional CLI flags
2
+ export const DEFAULT_SCANLINES = 0.3;
3
+ export const DEFAULT_VIGNETTE = 1.0;
4
+ export const DEFAULT_BLOOM = 0.5;
5
+ export const MIN_FRAME_LIMIT = 1; // Minimum allowed frame limit (fps)
6
+ export const DEFAULT_NTSC = 1.0;
7
+ export const DEFAULT_CURVATURE = 0.1;
8
+ export const DEFAULT_CHROMATIC_ABERRATION = 0.5;
9
+
10
+ // Default values for --crt shortcut
11
+ export const CRT_SCALE = 1;
12
+ export const CRT_NTSC = 1.0;
13
+ export const CRT_SCANLINES = 0.1;
14
+ export const CRT_GAMMA = 1.3;
15
+ export const CRT_VIGNETTE = 0.5;
16
+ export const CRT_CURVATURE = 0.1;
17
+ export const CRT_CHROMATIC_ABERRATION = 0.3;