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,77 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(npm install)",
5
+ "Bash(npm run build:*)",
6
+ "Bash(npm run typecheck:*)",
7
+ "Bash(npm test:*)",
8
+ "Bash(git add:*)",
9
+ "Bash(git commit -m \"$\\(cat <<''EOF''\nWire up CPU opcode execution\n\nConnect the CPU step\\(\\) method to the opcodes table to execute\ninstructions. The CPU now:\n- Reads opcode from memory at PC\n- Looks up instruction in the opcodes table\n- Sets base cycle count and executes the handler\n- Handles unknown opcodes as 2-cycle NOPs\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
10
+ "Bash(git commit -m \"$\\(cat <<''EOF''\nFix NMI to trigger only once per frame\n\nClear nmiOccurred flag after triggering NMI to prevent\nthe interrupt from firing repeatedly every PPU cycle.\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
11
+ "Bash(git commit:*)",
12
+ "Bash(xargs:*)",
13
+ "Bash(ls:*)",
14
+ "Bash(npm install:*)",
15
+ "Bash(npm uninstall:*)",
16
+ "Bash(play:*)",
17
+ "Bash(brew install:*)",
18
+ "Bash(node -e:*)",
19
+ "Bash(ffplay:*)",
20
+ "Bash(cat:*)",
21
+ "Bash(git checkout:*)",
22
+ "WebSearch",
23
+ "WebFetch(domain:www.npmjs.com)",
24
+ "WebFetch(domain:github.com)",
25
+ "Bash(node dist/index.js:*)",
26
+ "Bash(grep:*)",
27
+ "Bash(git -C /Users/tuxracer/Development/tui-nes status)",
28
+ "Bash(git -C /Users/tuxracer/Development/tui-nes diff --stat)",
29
+ "Bash(git -C /Users/tuxracer/Development/tui-nes add src/emulator.ts src/index.ts src/ppu/renderer.ts)",
30
+ "Bash(git -C /Users/tuxracer/Development/tui-nes commit -m \"$\\(cat <<''EOF''\nAdd dynamic terminal resize support for terminal/ASCII modes\n\nWhen running in terminal or ASCII mode without explicit --width/--height,\nthe emulator now automatically detects terminal size changes and adjusts\nthe rendering dimensions accordingly. Changes include:\n\n- Add calculateTerminalDimensions\\(\\) to compute optimal display size\n- Add setDimensions\\(\\)/getDimensions\\(\\) to TerminalRenderer\n- Poll terminal size each frame and resize when changed\n- Only pass explicit dimensions from CLI when user specifies them\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
31
+ "Bash(git reset:*)",
32
+ "Bash(git -C /Users/tuxracer/Development/tui-nes add src/ppu/kitty-renderer.ts TODO.md)",
33
+ "Bash(git -C /Users/tuxracer/Development/tui-nes commit -m \"$\\(cat <<''EOF''\nReuse RGB buffer in Kitty renderer to reduce GC pressure\n\nPre-allocate a 184KB RGB buffer as a class property instead of\ncreating a new Uint8Array on every frame. This eliminates ~11MB/sec\nof garbage collection pressure at 60 FPS.\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
34
+ "Bash(git -C /Users/tuxracer/Development/tui-nes add src/emulator.ts TODO.md)",
35
+ "Bash(git -C /Users/tuxracer/Development/tui-nes commit -m \"$\\(cat <<''EOF''\nAdd audio buffer pool to reduce allocation overhead\n\nUse a rotating pool of 3 pre-allocated buffers for audio output\ninstead of allocating a new buffer for each sample batch \\(~11×/frame\\).\nBuffers are lazily allocated on first use and reused thereafter.\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
36
+ "Bash(git -C /Users/tuxracer/Development/tui-nes add src/ppu/palette.ts TODO.md)",
37
+ "Bash(git -C /Users/tuxracer/Development/tui-nes commit -m \"$\\(cat <<''EOF''\nCache ANSI escape sequences for palette colors\n\nPre-compute lookup tables for foreground and background ANSI true color\nescape sequences at module load instead of generating strings per-pixel.\nEliminates 61,440+ string allocations per frame in terminal render mode.\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
38
+ "Bash(git -C /Users/tuxracer/Development/tui-nes add src/memory/bus.ts)",
39
+ "Bash(git -C /Users/tuxracer/Development/tui-nes commit -m \"$\\(cat <<''EOF''\nReuse DMA buffer to avoid allocation per transfer\n\nPre-allocate a 256-byte buffer as a class property instead of\ncreating a new Uint8Array for each OAM DMA transfer.\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
40
+ "Bash(git -C /Users/tuxracer/Development/tui-nes add src/ppu/ppu.ts)",
41
+ "Bash(git -C /Users/tuxracer/Development/tui-nes commit -m \"$\\(cat <<''EOF''\nAdd pre-computed bit reversal table for sprite flipping\n\nReplace runtime bit manipulation \\(6 operations per call\\) with\na 256-entry lookup table computed at module load. Called up to\n8 times per scanline for horizontally flipped sprites.\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
42
+ "Bash(git -C /Users/tuxracer/Development/tui-nes add src/ppu/renderer.ts src/ppu/palette.ts)",
43
+ "Bash(git -C /Users/tuxracer/Development/tui-nes commit -m \"$\\(cat <<''EOF''\nOptimize terminal renderer string building and luminance\n\n- Replace string concatenation \\(+=\\) with array + join\\(\\) to avoid\n O\\(n²\\) allocation overhead in hot rendering loop\n- Add pre-computed luminance lookup table for ASCII mode rendering,\n eliminating per-pixel luminance calculation\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
44
+ "Bash(git -C /Users/tuxracer/Development/tui-nes add TODO.md)",
45
+ "Bash(git -C /Users/tuxracer/Development/tui-nes commit -m \"$\\(cat <<''EOF''\nUpdate TODO.md with completed optimizations\n\nMark completed:\n- Reusable DMA temp buffer\n- Sprite bit reversal lookup table \n- String concatenation in renderer\n- ASCII luminance lookup table\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
46
+ "Bash(git -C /Users/tuxracer/Development/tui-nes add src/ppu/ppu.ts TODO.md)",
47
+ "Bash(git -C /Users/tuxracer/Development/tui-nes commit -m \"$\\(cat <<''EOF''\nCache tile data in PPU to reduce memory reads per pixel\n\nAdd tile caching to renderPixel\\(\\) that fetches tile pattern and\nattribute data once per 8-pixel tile span instead of per pixel.\nReduces PPU memory reads from 4 per pixel to 4 per 8 pixels,\ncutting background rendering memory access by ~87%.\n\nCache is invalidated at the start of each scanline via copyX\\(\\).\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
48
+ "Bash(git mv:*)",
49
+ "Bash(npm run test:run:*)",
50
+ "Bash(timeout 3 node:*)",
51
+ "Bash(pkill:*)",
52
+ "Bash(timeout 10 node:*)",
53
+ "Bash(timeout 5 node:*)",
54
+ "Bash(timeout 30 node:*)",
55
+ "Bash(timeout 4 node:*)",
56
+ "Bash(timeout 8 node:*)",
57
+ "Bash(timeout 6 node:*)",
58
+ "Bash(pnpm run build:*)",
59
+ "Bash(pnpm run typecheck:*)",
60
+ "Bash(git stash:*)",
61
+ "Bash(pnpm run start:*)",
62
+ "Bash(git rebase:*)",
63
+ "Bash(pnpm run test:run:*)",
64
+ "WebFetch(domain:www.nesdev.org)",
65
+ "WebFetch(domain:raw.githubusercontent.com)",
66
+ "WebFetch(domain:forums.nesdev.org)",
67
+ "Bash(npx tsx benchmark-diff.ts)",
68
+ "WebFetch(domain:snes.nesdev.org)",
69
+ "WebFetch(domain:byuu.org)",
70
+ "WebFetch(domain:sneslab.net)",
71
+ "WebFetch(domain:en.wikibooks.org)",
72
+ "Bash(xxd:*)",
73
+ "Bash(pnpm run check:*)",
74
+ "Bash(pnpm run lint:fix:*)"
75
+ ]
76
+ }
77
+ }
package/.node-version ADDED
@@ -0,0 +1 @@
1
+ 24
package/CLAUDE.md ADDED
@@ -0,0 +1,435 @@
1
+ # emoemu - Terminal Retro Emulator
2
+
3
+ A terminal-based multi-core emulator written in TypeScript that renders graphics using the Kitty graphics protocol, Unicode half-blocks, ASCII, or emoji characters. Supports any system via libretro cores (RetroArch cores).
4
+
5
+ For user-facing documentation (installation, usage, controls, CLI options), see **README.md**.
6
+
7
+ ## Quick Reference
8
+
9
+ ```bash
10
+ pnpm run build # Build the project
11
+ pnpm run start <rom> # Run a ROM (auto-detects core)
12
+ pnpm run check # Run typecheck, lint, and tests (run before commits)
13
+ pnpm run lint:fix # Auto-fix lint errors
14
+ ```
15
+
16
+ **Important:** Always run `pnpm run check` before committing. Use `pnpm run test:run` for single test runs (not `pnpm test -- --run` which stays in watch mode).
17
+
18
+ **For Claude:** When running the emulator to debug issues, always use `--no-render` to prevent video output from flooding the conversation. When debugging netplay, also use `--clear-logs` for fresh logs. See [Netplay Logging](#logging) for log file paths.
19
+
20
+ **Documentation**: When making major changes (architecture, new modules, API changes, file structure), update [docs/TRD.md](docs/TRD.md) to keep the technical reference accurate.
21
+
22
+ ## Project Structure
23
+
24
+ ```
25
+ src/
26
+ ├── index.ts # CLI entry point, main loop
27
+ ├── cli/ # CLI argument parsing, commands, emulator runner
28
+ │ ├── parseArgs/ # Argument parsing, config-to-options mapping
29
+ │ ├── commands/ # CLI commands (usage, install-core, playlist, etc.)
30
+ │ └── runEmulator/ # Emulator launch, state file validation
31
+ ├── Emulator/ # Main emulation loop, renderer orchestration
32
+ │ ├── saveState/ # Save state and battery save (.srm) management
33
+ │ ├── screenshot/ # Screenshot capture, thumbnails
34
+ │ └── terminalDimensions/ # Terminal display size calculation
35
+ ├── core/ # Multi-core interface definitions (Core, SystemInfo, AudioConfig)
36
+ ├── frontend/ # Shared infrastructure (audio, notifications, state management)
37
+ ├── cores/
38
+ │ └── libretro/ # Libretro wrapper (FFI via koffi, environment callbacks)
39
+ ├── input/ # Keyboard (Kitty protocol) and gamepad (node-hid) handling
40
+ ├── rendering/ # Kitty graphics, Unicode half-blocks, ASCII, emoji renderers
41
+ ├── netplay/ # RetroArch-compatible netplay (rollback, LAN discovery)
42
+ ├── ui/ # React/Ink TUI (ROM browser, settings, netplay)
43
+ └── types/ # Type declarations
44
+ ```
45
+
46
+ ## Source Structure
47
+
48
+ Each module is a directory named after its primary export. Module directories may **only** contain these files — no other files are allowed:
49
+
50
+ - `index.ts` (or `index.tsx`) — required, main logic and public API
51
+ - `consts.ts` — optional, module-specific constants
52
+ - `types.ts` — optional, type definitions and type guards
53
+ - `tests.ts` — optional, related tests
54
+
55
+ ## RetroArch Compatibility
56
+
57
+ We aim to be compatible with RetroArch conventions and terminology wherever practical. RetroArch is the most popular libretro frontend, so aligning with its patterns:
58
+
59
+ - Makes it easier for users to import existing RetroArch settings and data
60
+ - Feels familiar to users already experienced with RetroArch
61
+ - Allows sharing of cores, playlists, thumbnails, and save states
62
+
63
+ **Where we match RetroArch:**
64
+
65
+ | Area | Compatibility |
66
+ | ------------------- | ---------------------------------------------------------------------------------------- |
67
+ | Config keys | Use same names where applicable (`video_driver`, `audio_enable`, `libretro_directory`) |
68
+ | Core options | Same INI format and file locations (`retroarch-core-options.cfg`, per-core `.opt` files) |
69
+ | Directory structure | `/cores`, `/playlists`, `/thumbnails`, `/saves`, `/states`, `/system` |
70
+ | Playlist format | LPL JSON format, fully compatible with RetroArch |
71
+ | Save states | Same binary format and `.state`/`.state.auto` naming for libretro cores |
72
+ | Thumbnails | Same directory layout (`Named_Boxarts/`, `Named_Snaps/`, `Named_Titles/`) |
73
+ | Log format | `[LEVEL] [Category]: message` format |
74
+ | Netplay | Protocol v7 compatible, can connect to RetroArch hosts |
75
+
76
+ **Where we diverge:**
77
+
78
+ - Terminal-specific renderers (Kitty, Unicode, ASCII, emoji) instead of GPU
79
+ - Some config keys are emoemu-specific (e.g., `render_mode`, `kitty_scale`)
80
+
81
+ When adding new features, check how RetroArch handles it first and match their approach unless there's a good reason not to.
82
+
83
+ ## Multi-Core Architecture
84
+
85
+ The emulator uses a **libretro-based** architecture separating system-specific emulation (cores) from shared infrastructure (frontend).
86
+
87
+ All cores implement the `Core` interface in `src/core/core.ts`. Key methods: `loadRom()`, `runFrame()`, `getFramebuffer()`, `setButtonState()`, `getState()`/`setState()` for save states.
88
+
89
+ ### Core Registry
90
+
91
+ Cores self-register when imported. ROM files are auto-detected by extension. When multiple cores support the same extension, the CLI prompts for selection (use `--core <id>` to bypass).
92
+
93
+ ## Libretro Core Support
94
+
95
+ Loads native RetroArch cores via koffi FFI. **Not supported**: Cores requiring OpenGL/Vulkan.
96
+
97
+ ### API Flow
98
+
99
+ Load core → `retro_set_environment()` → `retro_init()` → `retro_load_game()` → main loop: `retro_run()` (fires video/audio callbacks) → cleanup.
100
+
101
+ ### Core Options
102
+
103
+ Core options configure libretro core behavior (e.g., video plugins, region settings). Uses RetroArch-compatible format and file locations.
104
+
105
+ **Key files:**
106
+
107
+ - `src/cores/libretro/core-options.ts` - Loading/saving options from config files
108
+ - `src/cores/libretro/environment/index.ts` - `GET_VARIABLE`/`SET_VARIABLES` handlers
109
+
110
+ **Config file locations** (RetroArch-compatible precedence, highest first):
111
+
112
+ 1. Game-specific: `<config>/config/<core>/<game>.opt`
113
+ 2. Core-specific: `<config>/config/<core>/<core>.opt`
114
+ 3. Global: `<config>/retroarch-core-options.cfg`
115
+
116
+ **Format** (INI-style, same as RetroArch):
117
+
118
+ ```ini
119
+ mupen64plus-rdp-plugin = "angrylion"
120
+ mupen64plus-rsp-plugin = "parallel"
121
+ genesis_plus_gx-region_detect = "auto"
122
+ ```
123
+
124
+ **Usage:**
125
+
126
+ ```typescript
127
+ // Load from config files
128
+ const options = loadCoreOptions("mupen64plus_next", "Super Mario 64");
129
+ const core = createCore("libretro-mupen64plus-next", { coreOptions: options });
130
+
131
+ // Set at runtime
132
+ core.setCoreOption("mupen64plus-rdp-plugin", "angrylion");
133
+
134
+ // Get available options (after core init)
135
+ const available = core.getAvailableCoreOptions();
136
+ // Returns: [{ key, description, values, defaultValue, currentValue }, ...]
137
+ ```
138
+
139
+ **Automatic defaults** (`DEFAULT_CORE_OPTIONS`):
140
+ Some cores need specific options to work with emoemu (terminal-based, no GPU). These are applied automatically:
141
+
142
+ - `mupen64plus` - Uses Angrylion software renderer (CPU-only, no GPU required)
143
+
144
+ ### Debugging
145
+
146
+ - Set `DEBUG_ENV = true` in `environment.ts` to log environment commands
147
+ - Check core file extension (`.dylib`/`.so`/`.dll`) and BIOS files in `./system/`
148
+
149
+ ## Input System
150
+
151
+ **Keyboard**: Auto-detects Kitty protocol for true keyup/keydown; falls back to legacy mode with 80ms auto-release.
152
+
153
+ **Gamepad**: Uses node-hid for raw HID access. Profiles in `gamepad-profiles.ts` for Xbox, PlayStation, Nintendo, 8BitDo controllers.
154
+
155
+ **Input Mapper**: Translates `StandardButton` enum to core-specific button IDs across different cores.
156
+
157
+ ## Rendering
158
+
159
+ All renderers use diff-based optimization (skip unchanged frames).
160
+
161
+ ### Dual Code Paths
162
+
163
+ **Important:** Terminal renderers have separate code paths for initial full-frame rendering vs. diff-based updates. When adding or modifying rendering options (e.g., color limits, effects), ensure changes are applied to **both**:
164
+
165
+ - Full-frame render loops (used for initial paint)
166
+ - `renderChar*` methods (used for diff-based updates)
167
+
168
+ Search for all usages of the relevant rendering functions to ensure consistency.
169
+
170
+ ### Performance Considerations
171
+
172
+ **Terminal I/O is the primary bottleneck.** Optimization priorities:
173
+
174
+ 1. **Minimize output size** - PNG compression is worth the CPU cost; indexed 256-color is ~3x smaller than RGB
175
+ 2. **Apply effects at native resolution** - Post-processing runs before scaling (4x fewer pixels at scale=2)
176
+ 3. **Avoid effects that expand palette** - Vignette/gradients force RGB fallback
177
+
178
+ ## Netplay
179
+
180
+ RetroArch-compatible (protocol v7). Works with libretro cores only.
181
+
182
+ ### Architecture
183
+
184
+ Deterministic lockstep with rollback: input exchange every frame → predict when delayed → rollback/replay when prediction wrong → CRC desync detection.
185
+
186
+ ### Logging
187
+
188
+ | Platform | Log Path |
189
+ | -------- | ------------------------------------------------------- |
190
+ | macOS | `~/Library/Application Support/emoemu/logs/netplay.log` |
191
+ | Linux | `~/.config/emoemu/logs/netplay.log` |
192
+ | Windows | `%APPDATA%\emoemu\logs\netplay.log` |
193
+
194
+ Use `--clear-logs` for fresh logs. Log categories: `SERVER`, `CLIENT`, `SYNC`, `DISCOVERY`, `SESSION`.
195
+
196
+ ## Save States
197
+
198
+ Raw binary (`.state.auto`), compatible with RetroArch.
199
+
200
+ ## Data Directories
201
+
202
+ emoemu stores data in platform-specific directories:
203
+
204
+ | Platform | Base Directory |
205
+ | -------- | --------------------------------------- |
206
+ | macOS | `~/Library/Application Support/emoemu/` |
207
+ | Linux | `~/.config/emoemu/` |
208
+ | Windows | `%APPDATA%\emoemu\` |
209
+
210
+ **Key subdirectories:**
211
+
212
+ | Directory | Purpose |
213
+ | --------- | -------------------------------------------------- |
214
+ | `cores/` | Installed libretro cores (`.dylib`, `.so`, `.dll`) |
215
+ | `logs/` | Log files including `emoemu.log` |
216
+ | `saves/` | Save data (SRAM, memory cards) |
217
+ | `states/` | Save states |
218
+ | `system/` | BIOS files and system data |
219
+
220
+ ## TypeScript Strict Mode
221
+
222
+ Unused variables fail the build (TS6133, TS6192). The underscore prefix only works for **function parameters**:
223
+
224
+ ```typescript
225
+ function resize(_color: string, size: number) { ... } // Valid: _color unused but needed for size
226
+ const _foo = true; // Invalid: still errors if unused
227
+ ```
228
+
229
+ ## Coding Standards
230
+
231
+ - **Never log sensitive data**: Do not log API keys, tokens, passwords, or other secrets. Use placeholder text like `[REDACTED]` if you need to indicate a value exists without revealing it
232
+ - **Arrow functions**: Use `const foo = () => { ... }` (enforced by ESLint, auto-fixable)
233
+ - **Reserve `use` prefix for React hooks**: The `useFoo` naming convention is reserved for React hooks. For boolean options or flags, use names like `systemFont`, `enableCache`, or `withValidation` instead of `useSystemFont`, `useCache`, or `useValidation`
234
+ - **Boolean naming**: Prefer `is`/`has`/`should` prefixes for boolean variables (e.g., `isEnabled`, `hasContent`, `shouldRestore`). For enable/disable flags in options interfaces, `fooEnabled` is also acceptable (e.g., `colorEnabled`, `diffRenderingEnabled`)
235
+ - **Named imports**: Use `import { pipe, filter } from 'remeda'` not `import * as R` (tree-shaking)
236
+ - **ESM imports only**: Always use `import` syntax, never `require()`. This is an ESM project and `require` will throw `ReferenceError: require is not defined`
237
+ - **Prefer `@/` path alias**: Use `import { foo } from '@/utils/logger'` instead of deep relative imports like `'../../../utils/logger'`. The `@/` alias maps to `src/` via tsconfig paths. Relative imports within the same module (e.g., `'./consts'`, `'.'`) are fine
238
+ - **Remeda utilities**: Prefer for array/object manipulation over manual loops
239
+ - **Named constants**: Use `const HEADER_SIZE = 16` not magic numbers; use underscores for large numbers (`100_000`)
240
+ - **DRY (Don't Repeat Yourself)**: When a pattern appears 3+ times, extract it into a helper function. Place shared utilities in `src/utils/` (e.g., `src/utils/findLibrary/index.ts`). This improves readability and maintainability without impacting performance
241
+ - **Module structure**: Each module (component, hook, utility) should be in its own directory with `index.ts` + `consts.ts` + `types.ts` + `tests.ts`:
242
+ - **Components**: Use PascalCase directories (e.g., `ui/NativeDialog/`, `ui/AddRomsPrompt/`)
243
+ - **Hooks**: Use camelCase directories (e.g., `hooks/useGamepad/`, `hooks/useClearTerminal/`)
244
+ - **Other modules**: Use PascalCase or camelCase directories (e.g., `rendering/nativeUi/`, `rendering/shared/`)
245
+ - **Never use kebab-case** for directory names
246
+ - Each directory contains `index.ts` (main logic, exports public API) and `consts.ts` (constants)
247
+ - **Re-export types and consts from index.ts**: Each module's `index.ts` should re-export all types and consts from `types.ts` and `consts.ts`. External code should import from the module, not directly from internal files:
248
+
249
+ ```typescript
250
+ // GOOD - import from the module
251
+ import { TICK_RATE, GameState } from "../Game";
252
+
253
+ // BAD - importing directly from internal module files
254
+ import { TICK_RATE } from "../Game/consts";
255
+ import type { GameState } from "../Game/types";
256
+ ```
257
+
258
+ In `Game/index.ts`:
259
+
260
+ ```typescript
261
+ export * from "./consts";
262
+ export * from "./types";
263
+ ```
264
+
265
+ - **Avoid barrel-only files**: Don't create `index.ts` files that only re-export from child modules. Import directly from the specific module instead (e.g., `import { useGamepad } from '../hooks/useGamepad'` not `from '../hooks'`).
266
+
267
+ ```
268
+ src/ui/
269
+ ├── hooks/
270
+ │ ├── useGamepad/
271
+ │ │ ├── index.ts # Hook implementation
272
+ │ │ └── consts.ts # INITIAL_DELAY_MS, MIN_REPEAT_MS, etc.
273
+ │ └── useClearTerminal/
274
+ │ ├── index.ts
275
+ │ └── consts.ts
276
+ ├── NativeDialog/
277
+ │ ├── index.tsx
278
+ │ ├── tests.ts
279
+ │ └── consts.ts
280
+ └── index.ts # Exports from this module (not just re-exports)
281
+ ```
282
+
283
+ - **User terminology**: Say "game library" not "playlist" in user-facing text
284
+ - **Settings**: Any setting exposed in the TUI settings menu must also apply immediately during gameplay. This requires:
285
+ 1. Adding the setting to `RuntimeSettings` interface in `src/frontend/settings-manager.ts`
286
+ 2. Adding the config key mapping to `SETTING_TO_CONFIG_KEY`
287
+ 3. Initializing it in the `SettingsManager` constructor and `reloadFromConfig`
288
+ 4. Adding an `onChange` listener in the Emulator to apply the change at runtime
289
+ 5. Adding the setting to `updateOptionsFromConfig()` in `src/index.ts` so it syncs from config when launching/resuming a game (CLI options are copied at startup; without this, settings changes won't take effect)
290
+ - **RetroArch compatibility**: Follow RetroArch conventions where practical (see [RetroArch Compatibility](#retroarch-compatibility))
291
+ - **React context over prop drilling**: For app-wide state that's needed across many components (e.g., capabilities, settings), use React context instead of passing props through multiple levels. See `src/ui/AppCapabilities` for an example. This keeps component interfaces clean and avoids threading props through intermediate components that don't use them.
292
+ - **JSDoc**: Skip `@param`/`@returns` tags (TypeScript provides types); use inline comments if needed
293
+ - **Loading indicators**: Delay by ~1 second to avoid flash for fast operations
294
+ - **Intl API**: Prefer `Intl.DateTimeFormat`, `Intl.NumberFormat`, etc. over manual formatting for dates, numbers, and currencies
295
+ - **Logging**: Use the centralized `logger` from `src/utils/logger.ts` for runtime logging. Three patterns:
296
+ 1. **CLI commands** (e.g., `--help`, `--list-cores`): Use `console.*` directly - output must always be shown regardless of log settings.
297
+ 2. **Internal debug/info**: Use `logger` only - controlled by `log_verbosity` config.
298
+ 3. **Runtime errors** (e.g., "Core rejected ROM", "Unsupported format"): Use BOTH `console.error` (user must see it) AND `logger.error` (recorded in log file for debugging). The user needs immediate feedback, but it should also be in logs.
299
+
300
+ **Log format**: Messages should follow `[LEVEL] [Category] message` pattern (e.g., `[ERROR] [Core] Failed to load ROM`). Don't indent log messages or use other prefixes. For multi-line diagnostic info, log each line separately so every line has the proper prefix. When throwing errors that will be caught and logged, don't also log the error message explicitly - only log additional diagnostic details to avoid duplicates.
301
+
302
+ - **Explicit conditionals for derived values**: When a value like `useTrueColor` is derived from another value like `limitColors`, use the source value in conditionals, not the derived value. This makes the logic clearer and avoids confusion:
303
+
304
+ ```typescript
305
+ // GOOD - explicit about what each branch handles
306
+ if (this.limitColors === 16) {
307
+ /* ANSI 16 */
308
+ } else if (this.limitColors === 256) {
309
+ /* ANSI 256 */
310
+ } else {
311
+ /* True color (limitColors === 0) */
312
+ }
313
+
314
+ // BAD - confusing because useTrueColor is derived from limitColors
315
+ if (this.limitColors === 16) {
316
+ /* ANSI 16 */
317
+ } else if (this.useTrueColor) {
318
+ /* True color */
319
+ } else {
320
+ /* ANSI 256 */
321
+ }
322
+ ```
323
+
324
+ - **Type guards over type assertions**: Never use `as` type assertions on values with unknown runtime types. Use type guards from Remeda (`isString`, `isNumber`, `isBoolean`, `isPlainObject`), existing custom guards from `src/utils/type-guards.ts`, or create a new custom type guard if none exist:
325
+
326
+ ```typescript
327
+ // GOOD - type guard validates at runtime
328
+ import { isString } from "remeda";
329
+
330
+ if (isString(value)) {
331
+ config.name = value;
332
+ }
333
+
334
+ // BAD - blind cast assumes type without validation
335
+ config.name = value as string;
336
+ ```
337
+
338
+ For union types (e.g., `"kitty" | "terminal" | "ascii"`), create a type guard that validates the actual values, not just the primitive type:
339
+
340
+ ```typescript
341
+ // GOOD - validates the value is one of the allowed options
342
+ import { isPostProcessingMode } from "../../utils/type-guards";
343
+
344
+ if (isPostProcessingMode(value)) {
345
+ config.video_postprocessing_mode = value; // No cast needed
346
+ }
347
+
348
+ // BAD - isString only checks primitive type, not valid union values
349
+ if (isString(value)) {
350
+ config.video_postprocessing_mode = value as PostProcessingMode; // Still a blind cast!
351
+ }
352
+ ```
353
+
354
+ When creating type guards for union types, use the named type in the return type annotation - don't hardcode the union:
355
+
356
+ ```typescript
357
+ // GOOD - uses the named type
358
+ import type { VideoDriver } from "../frontend/config";
359
+
360
+ const VIDEO_DRIVERS: readonly VideoDriver[] = [
361
+ "kitty",
362
+ "terminal",
363
+ "ascii",
364
+ "emoji",
365
+ ];
366
+
367
+ export const isVideoDriver = (value: unknown): value is VideoDriver => {
368
+ return isString(value) && VIDEO_DRIVERS.includes(value as VideoDriver);
369
+ };
370
+
371
+ // BAD - hardcodes the union type (duplicates the type definition)
372
+ export const isVideoDriver = (
373
+ value: unknown,
374
+ ): value is "kitty" | "terminal" | "ascii" | "emoji" => {
375
+ // ...
376
+ };
377
+ ```
378
+
379
+ - **Typed errors over string messages**: When throwing errors, create a custom error class with a typed `code` property instead of using plain `Error` with string messages. This enables type-safe error handling:
380
+
381
+ ```typescript
382
+ // GOOD - typed error with machine-readable code
383
+ type MyErrorCode = "NOT_FOUND" | "PERMISSION_DENIED" | "TIMEOUT";
384
+
385
+ class MyError extends Error {
386
+ readonly code: MyErrorCode;
387
+ constructor(code: MyErrorCode) {
388
+ super(code);
389
+ this.name = "MyError";
390
+ this.code = code;
391
+ }
392
+ }
393
+
394
+ const isMyError = (error: unknown): error is MyError => {
395
+ return error instanceof MyError;
396
+ };
397
+
398
+ // Usage - callers get autocomplete and type checking
399
+ try {
400
+ await doSomething();
401
+ } catch (error) {
402
+ if (isMyError(error)) {
403
+ switch (error.code) {
404
+ case "NOT_FOUND": // TypeScript knows valid codes
405
+ // ...
406
+ }
407
+ }
408
+ }
409
+
410
+ // BAD - string messages aren't type-safe
411
+ throw new Error("Not found");
412
+ throw new Error("Permission denied");
413
+ ```
414
+
415
+ - **Tests verify behavior, not implementation**: Tests should verify that code works correctly, not enshrine implementation details. Never write tests that just check constant values - if a constant matters, test the behavior it affects:
416
+
417
+ ```typescript
418
+ // BAD - tests implementation detail, provides no value
419
+ it("should have expected default value", () => {
420
+ expect(MAX_FRAMES_BEHIND).toBe(60);
421
+ });
422
+
423
+ // GOOD - tests actual behavior that depends on the constant
424
+ it("should trigger catchup when too far behind", () => {
425
+ // Simulate being far behind and verify the sync behavior
426
+ for (let i = 0; i < 70; i++) {
427
+ syncManager.advanceFrame();
428
+ }
429
+ expect(syncManager.needsCatchup).toBe(true);
430
+ });
431
+ ```
432
+
433
+ ## Linting
434
+
435
+ Run `pnpm run lint:fix` to auto-fix style issues. Key rules: arrow functions, `const`/`let` only, `===` equality, curly braces required, promise handling, `interface` over `type`.