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,320 @@
1
+ import { isString, isBoolean } from 'remeda';
2
+ import { isVideoDriver, isPostProcessingMode, updateConfigValue, resetConfigValue } from '@/frontend/config';
3
+ import type { Config } from '@/frontend/config';
4
+ import { setNotificationsEnabled } from '@/frontend/notifications';
5
+ import type { SettingsOption, SettingsCategory } from '..';
6
+
7
+ /** Setter for config values via dynamic key */
8
+ export const setConfigField = (config: Config, key: keyof Config, value: Config[keyof Config]) => {
9
+ Object.assign(config, { [key]: value });
10
+ };
11
+
12
+ /** Factory for simple boolean toggle settings */
13
+ export const createToggleOption = (key: keyof Config, label: string): SettingsOption => ({
14
+ id: key,
15
+ label,
16
+ type: 'toggle',
17
+ getValue: (config) => isBoolean(config[key]) ? config[key] : false,
18
+ setValue: (config, value, configPath) => {
19
+ if (!isBoolean(value)) { return; }
20
+ setConfigField(config, key, value);
21
+ updateConfigValue(key, value, configPath);
22
+ },
23
+ });
24
+
25
+ /** Factory for numeric select settings (parsed with parseFloat) */
26
+ export const createFloatSelectOption = (
27
+ key: keyof Config,
28
+ label: string,
29
+ options: { value: string; label: string }[],
30
+ ): SettingsOption => ({
31
+ id: key,
32
+ label,
33
+ type: 'select',
34
+ options,
35
+ getValue: (config) => String(config[key]),
36
+ setValue: (config, value, configPath) => {
37
+ if (!isString(value)) { return; }
38
+ const parsed = parseFloat(value);
39
+ setConfigField(config, key, parsed);
40
+ updateConfigValue(key, parsed, configPath);
41
+ },
42
+ });
43
+
44
+ /** Factory for integer select settings (parsed with parseInt) */
45
+ export const createIntSelectOption = (
46
+ key: keyof Config,
47
+ label: string,
48
+ options: { value: string; label: string }[],
49
+ ): SettingsOption => ({
50
+ id: key,
51
+ label,
52
+ type: 'select',
53
+ options,
54
+ getValue: (config) => String(config[key]),
55
+ setValue: (config, value, configPath) => {
56
+ if (!isString(value)) { return; }
57
+ const parsed = parseInt(value, 10);
58
+ setConfigField(config, key, parsed);
59
+ updateConfigValue(key, parsed, configPath);
60
+ },
61
+ });
62
+
63
+ export const videoDriverOptions = [
64
+ { value: 'auto', label: 'Auto' },
65
+ { value: 'kitty', label: 'Kitty (best quality)' },
66
+ { value: 'terminal', label: 'Terminal (Unicode blocks)' },
67
+ { value: 'ascii', label: 'ASCII' },
68
+ { value: 'emoji', label: 'Emoji' },
69
+ { value: 'native', label: 'Native window (experimental)' },
70
+ ];
71
+
72
+ export const scaleOptions = [
73
+ { value: 'auto', label: 'Auto' },
74
+ { value: '0.25', label: '0.25x' },
75
+ { value: '0.5', label: '0.5x' },
76
+ { value: '1', label: '1x' },
77
+ { value: '2', label: '2x' },
78
+ { value: '3', label: '3x' },
79
+ { value: '4', label: '4x' },
80
+ ];
81
+
82
+ export const menuScaleOptions = [
83
+ { value: 'auto', label: 'Auto' },
84
+ { value: '1.0', label: '1x' },
85
+ { value: '1.5', label: '1.5x' },
86
+ { value: '2.0', label: '2x' },
87
+ { value: '2.5', label: '2.5x' },
88
+ { value: '3.0', label: '3x' },
89
+ { value: '4.0', label: '4x' },
90
+ ];
91
+
92
+ export const postProcessingModeOptions = [
93
+ { value: 'off', label: 'Off' },
94
+ { value: 'crt', label: 'CRT' },
95
+ { value: 'custom', label: 'Custom' },
96
+ ];
97
+
98
+ // Post-processing effect options that are only visible in 'custom' mode
99
+ export const customOnlyEffectIds = new Set([
100
+ 'video_gamma',
101
+ 'video_scanlines',
102
+ 'video_saturation',
103
+ 'video_vignette',
104
+ 'video_curvature',
105
+ 'video_chromatic_aberration',
106
+ ]);
107
+
108
+ // Settings that are only visible when native render mode is selected
109
+ export const nativeOnlyIds = new Set([
110
+ 'menu_scale_factor',
111
+ ]);
112
+
113
+ export const settingsCategories: SettingsCategory[] = [
114
+ {
115
+ name: 'Emulation',
116
+ options: [
117
+ {
118
+ id: 'video_driver',
119
+ label: 'Render Mode',
120
+ type: 'select',
121
+ options: videoDriverOptions,
122
+ getValue: (config) => config.video_driver === null ? 'auto' : config.video_driver,
123
+ setValue: (config, value, configPath) => {
124
+ if (value === 'auto') {
125
+ config.video_driver = null;
126
+ resetConfigValue('video_driver', configPath);
127
+ } else if (isVideoDriver(value)) {
128
+ config.video_driver = value;
129
+ updateConfigValue('video_driver', config.video_driver, configPath);
130
+ }
131
+ },
132
+ },
133
+ {
134
+ id: 'video_scale',
135
+ label: 'Video Scale',
136
+ type: 'select',
137
+ options: scaleOptions,
138
+ getValue: (config) => config.video_scale === null ? 'auto' : String(config.video_scale),
139
+ setValue: (config, value, configPath) => {
140
+ if (!isString(value)) { return; }
141
+ if (value === 'auto') {
142
+ config.video_scale = null;
143
+ resetConfigValue('video_scale', configPath);
144
+ } else {
145
+ config.video_scale = parseFloat(value);
146
+ updateConfigValue('video_scale', config.video_scale, configPath);
147
+ }
148
+ },
149
+ },
150
+ {
151
+ id: 'menu_scale_factor',
152
+ label: 'Native UI Scale',
153
+ type: 'select',
154
+ options: menuScaleOptions,
155
+ getValue: (config) => config.menu_scale_factor === null ? 'auto' : config.menu_scale_factor.toFixed(1),
156
+ setValue: (config, value, configPath) => {
157
+ if (!isString(value)) { return; }
158
+ if (value === 'auto') {
159
+ config.menu_scale_factor = null;
160
+ resetConfigValue('menu_scale_factor', configPath);
161
+ } else {
162
+ config.menu_scale_factor = parseFloat(value);
163
+ updateConfigValue('menu_scale_factor', config.menu_scale_factor, configPath);
164
+ }
165
+ },
166
+ },
167
+ {
168
+ id: 'audio_enable',
169
+ label: 'Audio',
170
+ type: 'toggle',
171
+ // Audio is effectively ON when enabled and not muted
172
+ getValue: (config) => config.audio_enable && !config.audio_mute_enable,
173
+ setValue: (config, value, configPath) => {
174
+ if (!isBoolean(value)) { return; }
175
+ // Toggle mute state (keep audio_enable as master switch)
176
+ // ON = unmute, OFF = mute
177
+ config.audio_mute_enable = !value;
178
+ updateConfigValue('audio_mute_enable', config.audio_mute_enable, configPath);
179
+ },
180
+ },
181
+ createToggleOption('input_joypad_enable', 'Gamepad Support'),
182
+ {
183
+ id: 'notifications_enable',
184
+ label: 'Notifications',
185
+ type: 'toggle',
186
+ getValue: (config) => config.notifications_enable,
187
+ setValue: (config, value, configPath) => {
188
+ if (!isBoolean(value)) { return; }
189
+ config.notifications_enable = value;
190
+ updateConfigValue('notifications_enable', value, configPath);
191
+ setNotificationsEnabled(value);
192
+ },
193
+ },
194
+ createToggleOption('savestate_auto_load', 'Auto-load Save States'),
195
+ createToggleOption('savestate_auto_save', 'Auto-save Save States'),
196
+ createToggleOption('savestates_in_content_dir', 'Save States to ROM Dir'),
197
+ createToggleOption('savefiles_in_content_dir', 'Battery Saves to ROM Dir'),
198
+ createToggleOption('fps_show_enable', 'Status Bar'),
199
+ createIntSelectOption('video_frame_limit', 'Frame Limit', [
200
+ { value: '15', label: '15 fps' },
201
+ { value: '30', label: '30 fps' },
202
+ { value: '0', label: 'Off' },
203
+ ]),
204
+ ],
205
+ },
206
+ {
207
+ name: 'Post-Processing',
208
+ options: [
209
+ {
210
+ id: 'video_postprocessing_mode',
211
+ label: 'Mode',
212
+ type: 'select',
213
+ options: postProcessingModeOptions,
214
+ getValue: (config) => config.video_postprocessing_mode,
215
+ setValue: (config, value, configPath) => {
216
+ if (!isPostProcessingMode(value)) { return; }
217
+ config.video_postprocessing_mode = value;
218
+ updateConfigValue('video_postprocessing_mode', config.video_postprocessing_mode, configPath);
219
+ },
220
+ },
221
+ createFloatSelectOption('video_gamma', 'Gamma', [
222
+ { value: '1.0', label: '1.0 (Linear)' },
223
+ { value: '1.1', label: '1.1' },
224
+ { value: '1.2', label: '1.2' },
225
+ { value: '1.3', label: '1.3 (CRT)' },
226
+ { value: '1.4', label: '1.4' },
227
+ { value: '1.8', label: '1.8 (Mac)' },
228
+ { value: '2.0', label: '2.0' },
229
+ { value: '2.2', label: '2.2 (sRGB)' },
230
+ { value: '2.4', label: '2.4 (Rec. 709)' },
231
+ ]),
232
+ createFloatSelectOption('video_scanlines', 'Scanlines', [
233
+ { value: '0', label: 'Off' },
234
+ { value: '0.1', label: '0.1 (Subtle)' },
235
+ { value: '0.2', label: '0.2' },
236
+ { value: '0.3', label: '0.3' },
237
+ { value: '0.4', label: '0.4 (Heavy)' },
238
+ ]),
239
+ createFloatSelectOption('video_saturation', 'Saturation', [
240
+ { value: '0.8', label: '0.8' },
241
+ { value: '0.9', label: '0.9' },
242
+ { value: '1.0', label: '1.0 (Default)' },
243
+ { value: '1.1', label: '1.1' },
244
+ { value: '1.2', label: '1.2' },
245
+ { value: '1.3', label: '1.3' },
246
+ ]),
247
+ createFloatSelectOption('video_vignette', 'Vignette', [
248
+ { value: '0', label: 'Off' },
249
+ { value: '0.2', label: '0.2 (Subtle)' },
250
+ { value: '0.3', label: '0.3' },
251
+ { value: '0.5', label: '0.5 (CRT)' },
252
+ { value: '0.7', label: '0.7 (Strong)' },
253
+ ]),
254
+ createFloatSelectOption('video_curvature', 'Curvature', [
255
+ { value: '0', label: 'Off' },
256
+ { value: '0.05', label: '0.05 (Subtle)' },
257
+ { value: '0.1', label: '0.1 (CRT)' },
258
+ { value: '0.15', label: '0.15' },
259
+ { value: '0.2', label: '0.2 (Strong)' },
260
+ ]),
261
+ createFloatSelectOption('video_chromatic_aberration', 'Chromatic Aberration', [
262
+ { value: '0', label: 'Off' },
263
+ { value: '0.3', label: '0.3 (CRT)' },
264
+ { value: '0.5', label: '0.5' },
265
+ { value: '1.0', label: '1.0' },
266
+ { value: '1.5', label: '1.5' },
267
+ { value: '2.0', label: '2.0' },
268
+ { value: '2.5', label: '2.5' },
269
+ { value: '3.0', label: '3.0' },
270
+ ]),
271
+ ],
272
+ },
273
+ ];
274
+
275
+ /**
276
+ * Filter settings categories based on post-processing mode and terminal capabilities.
277
+ * Custom effect options are only shown when mode is 'custom'.
278
+ * Kitty option is hidden if Kitty graphics protocol is not supported.
279
+ * Native window option is hidden if the native window backend is not available.
280
+ */
281
+ export const filterSettingsCategories = (isCustomMode: boolean, isNativeMode: boolean, kittySupported: boolean, nativeSupported: boolean): SettingsCategory[] =>
282
+ settingsCategories.map(cat => ({
283
+ ...cat,
284
+ options: cat.options.map(opt => {
285
+ // Filter unsupported video drivers from options
286
+ if (opt.id === 'video_driver' && opt.options) {
287
+ return {
288
+ ...opt,
289
+ options: opt.options.filter(o => {
290
+ if (o.value === 'kitty' && !kittySupported) {
291
+ return false;
292
+ }
293
+ if (o.value === 'native' && !nativeSupported) {
294
+ return false;
295
+ }
296
+ return true;
297
+ }),
298
+ };
299
+ }
300
+ return opt;
301
+ }).filter(opt =>
302
+ (!customOnlyEffectIds.has(opt.id) || isCustomMode) &&
303
+ (!nativeOnlyIds.has(opt.id) || isNativeMode)
304
+ ),
305
+ })).filter(cat => cat.options.length > 0);
306
+
307
+ // Flatten categories into a single list for navigation
308
+ export const allSettingsOptions = settingsCategories.flatMap(cat => cat.options);
309
+
310
+ // Action items for settings panel (dynamic based on whether there's a game to resume)
311
+ export const getSettingsActions = (hasResumeGame: boolean) => {
312
+ const actions = [];
313
+ if (hasResumeGame) {
314
+ actions.push({ id: 'resume', label: 'Resume Game', icon: '\u25B6' });
315
+ }
316
+ actions.push({ id: 'back', label: 'Back to Browser', icon: '\u2190' });
317
+ actions.push({ id: 'reset', label: 'Reset All Settings', icon: '\u21BA' });
318
+ actions.push({ id: 'exit', label: 'Exit emoemu', icon: '\u2717' });
319
+ return actions;
320
+ };
@@ -0,0 +1,67 @@
1
+ import type { RomInfo } from '../../frontend/romScanner';
2
+ import type { SaveStateDetails } from '../../frontend/saveServices';
3
+ import type { NetplayOptions } from '../App';
4
+ import type { Config } from '../../frontend/config';
5
+ import type { DiscoverySessionInfo } from '../../netplay/NetplayDiscovery';
6
+
7
+ export interface RomBrowserProps {
8
+ roms: RomInfo[];
9
+ playlistDirectory: string; // Directory containing playlists
10
+ scanDepth: number; // Max depth for scanning subdirectories
11
+ onSelect: (rom: RomInfo, currentFilter: string, resumeGame?: boolean, netplay?: NetplayOptions) => void;
12
+ onExit: (currentFilter: string) => void;
13
+ onRefresh: (currentFilter: string) => void; // Trigger a refresh of the ROM list
14
+ initialSelection?: string; // Path of ROM to select initially
15
+ initialFilter?: string; // Initial search filter to apply
16
+ showSettingsOnMount?: boolean; // Show settings panel immediately on mount
17
+ lastPlayedRom?: RomInfo; // ROM that was just played (for Resume Game option)
18
+ showNetplayOnMount?: boolean; // Show netplay panel immediately on mount
19
+ onScaleFactorChange?: (scaleFactor: number | null) => void; // Callback for native UI scale changes
20
+ }
21
+
22
+ // Action button definitions (app-wide actions)
23
+ export interface ActionButtonDef {
24
+ id: string;
25
+ label: string;
26
+ icon: string;
27
+ }
28
+
29
+ // Settings option definition (discriminated union for type-safe getValue/setValue)
30
+ export interface ToggleSettingsOption {
31
+ id: string;
32
+ label: string;
33
+ type: 'toggle';
34
+ options?: undefined;
35
+ getValue: (config: Config) => boolean;
36
+ setValue: (config: Config, value: boolean, configPath?: string) => void;
37
+ }
38
+
39
+ export interface SelectSettingsOption {
40
+ id: string;
41
+ label: string;
42
+ type: 'select';
43
+ options: { value: string; label: string }[];
44
+ getValue: (config: Config) => string;
45
+ setValue: (config: Config, value: string, configPath?: string) => void;
46
+ }
47
+
48
+ export type SettingsOption = ToggleSettingsOption | SelectSettingsOption;
49
+
50
+ // Settings category definition
51
+ export interface SettingsCategory {
52
+ name: string;
53
+ options: SettingsOption[];
54
+ }
55
+
56
+ export interface MetadataPanelProps {
57
+ rom: RomInfo | null | undefined;
58
+ width: number;
59
+ height: number;
60
+ saveStateDetails?: SaveStateDetails;
61
+ thumbnail?: string;
62
+ isKittySupported: boolean;
63
+ panelStartCol: number;
64
+ }
65
+
66
+ /** Discovered host info extended with address */
67
+ export type DiscoveredHost = DiscoverySessionInfo & { address: string };
@@ -0,0 +1,2 @@
1
+ /** Corrupted dialog box width */
2
+ export const CORRUPTED_DIALOG_MIN_WIDTH = 55;
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Save State Dialog Component
3
+ *
4
+ * Shows save state metadata and asks the user how to proceed.
5
+ */
6
+
7
+ import { Box, Text } from 'ink';
8
+ import { DialogOptionsList } from '../DialogOptionsList';
9
+ import { DialogContainer } from '../DialogContainer';
10
+ import { useDialogNavigation } from '../hooks/useDialogNavigation';
11
+ import { launchDialog, type DialogRenderOptions } from '../NativeDialog';
12
+ import { SEPARATOR_LINE_PADDING } from '..';
13
+ import { CORRUPTED_DIALOG_MIN_WIDTH } from './consts';
14
+
15
+ export * from './consts';
16
+
17
+ export interface SaveStateInfo {
18
+ path: string;
19
+ romName: string;
20
+ coreName: string;
21
+ }
22
+
23
+ export type SaveStateChoice = 'resume' | 'delete' | 'cancel';
24
+
25
+ interface SaveStateDialogProps {
26
+ info: SaveStateInfo;
27
+ onChoice: (choice: SaveStateChoice) => void;
28
+ }
29
+
30
+ const SaveStateDialog = ({ info, onChoice }: SaveStateDialogProps) => {
31
+ const options: { label: string; choice: SaveStateChoice; color: string }[] = [
32
+ { label: 'Resume from Save State', choice: 'resume', color: 'green' },
33
+ { label: 'Delete Save & Start Fresh', choice: 'delete', color: 'red' },
34
+ { label: 'Cancel', choice: 'cancel', color: 'gray' },
35
+ ];
36
+
37
+ const { selectedIndex } = useDialogNavigation({
38
+ itemCount: options.length,
39
+ onSelect: (index) => onChoice(options[index].choice),
40
+ onCancel: () => onChoice('cancel'),
41
+ });
42
+
43
+ return (
44
+ <DialogContainer>
45
+ {(boxWidth) => (
46
+ <>
47
+ {/* Header */}
48
+ <Box
49
+ flexDirection="column"
50
+ borderStyle="round"
51
+ borderColor="cyan"
52
+ paddingX={2}
53
+ paddingY={1}
54
+ width={boxWidth}
55
+ >
56
+ <Box justifyContent="center" marginBottom={1}>
57
+ <Text bold color="cyan">{'\u{1F4BE}'} Save State Found</Text>
58
+ </Box>
59
+
60
+ {/* ROM Name */}
61
+ <Box marginBottom={1}>
62
+ <Text color="gray">ROM: </Text>
63
+ <Text color="white" bold>{info.romName}</Text>
64
+ </Box>
65
+
66
+ {/* Core Name */}
67
+ <Box>
68
+ <Text color="gray">{'Core: '}</Text>
69
+ <Text color="white">{info.coreName}</Text>
70
+ </Box>
71
+ </Box>
72
+
73
+ <DialogOptionsList options={options} selectedIndex={selectedIndex} boxWidth={boxWidth} />
74
+ </>
75
+ )}
76
+ </DialogContainer>
77
+ );
78
+ };
79
+
80
+ /**
81
+ * Show the save state dialog and get user's choice
82
+ */
83
+ export const showSaveStateDialog = (
84
+ info: SaveStateInfo,
85
+ options: DialogRenderOptions = {}
86
+ ): Promise<SaveStateChoice> => launchDialog<SaveStateChoice>(
87
+ (onChoice) => <SaveStateDialog info={info} onChoice={onChoice} />,
88
+ 'cancel',
89
+ { ...options, title: options.title ?? 'emoemu - Save State' },
90
+ );
91
+
92
+ export default SaveStateDialog;
93
+
94
+ // ============================================================================
95
+ // Corrupted State Dialog
96
+ // ============================================================================
97
+
98
+ export interface CorruptedStateInfo {
99
+ path: string;
100
+ romName: string;
101
+ /** Whether the file could be read at all */
102
+ fileReadable: boolean;
103
+ /** Whether it's a binary file (libretro) or JSON (native core) */
104
+ isBinary: boolean;
105
+ /** Whether it was valid JSON (only relevant for native cores) */
106
+ validJson: boolean;
107
+ /** Whether we can attempt to load it (file readable and correct format) */
108
+ canAttemptLoad: boolean;
109
+ /** The specific error that caused validation to fail */
110
+ errorReason: string;
111
+ }
112
+
113
+ export type CorruptedStateChoice = 'try_load' | 'continue' | 'cancel';
114
+
115
+ interface CorruptedStateDialogProps {
116
+ info: CorruptedStateInfo;
117
+ onChoice: (choice: CorruptedStateChoice) => void;
118
+ }
119
+
120
+ const CorruptedStateDialog = ({ info, onChoice }: CorruptedStateDialogProps) => {
121
+ // Build options dynamically - offer "Try Loading" if we can attempt to load
122
+ const options: { label: string; choice: CorruptedStateChoice; color: string }[] = [];
123
+
124
+ if (info.canAttemptLoad) {
125
+ options.push({ label: 'Try Loading Anyway', choice: 'try_load', color: 'green' });
126
+ }
127
+ options.push({ label: 'Start Fresh (overwrites save)', choice: 'continue', color: 'yellow' });
128
+ options.push({ label: 'Cancel', choice: 'cancel', color: 'gray' });
129
+
130
+ const { selectedIndex } = useDialogNavigation({
131
+ itemCount: options.length,
132
+ onSelect: (index) => onChoice(options[index].choice),
133
+ onCancel: () => onChoice('cancel'),
134
+ });
135
+
136
+ return (
137
+ <DialogContainer minWidth={CORRUPTED_DIALOG_MIN_WIDTH}>
138
+ {(boxWidth) => (
139
+ <>
140
+ {/* Header */}
141
+ <Box
142
+ flexDirection="column"
143
+ borderStyle="round"
144
+ borderColor="yellow"
145
+ paddingX={2}
146
+ paddingY={1}
147
+ width={boxWidth}
148
+ >
149
+ <Box justifyContent="center" marginBottom={1}>
150
+ <Text bold color="yellow">{'\u26A0'} Corrupted Save State</Text>
151
+ </Box>
152
+
153
+ {/* ROM Name */}
154
+ <Box marginBottom={1}>
155
+ <Text color="gray">ROM: </Text>
156
+ <Text color="white" bold>{info.romName}</Text>
157
+ </Box>
158
+
159
+ {/* Parse Status Checklist */}
160
+ <Box flexDirection="column" marginBottom={1}>
161
+ <Box>
162
+ <Text color="gray">{'File readable: '}</Text>
163
+ <Text color={info.fileReadable ? 'green' : 'red'}>
164
+ {info.fileReadable ? '\u2713 Yes' : '\u2717 No'}
165
+ </Text>
166
+ </Box>
167
+ <Box>
168
+ <Text color="gray">{'Format: '}</Text>
169
+ <Text color="white">
170
+ {info.isBinary ? 'Binary (libretro)' : 'JSON (native)'}
171
+ </Text>
172
+ </Box>
173
+ {!info.isBinary && (
174
+ <Box>
175
+ <Text color="gray">{'Valid JSON: '}</Text>
176
+ <Text color={info.validJson ? 'green' : 'red'}>
177
+ {info.validJson ? '\u2713 Yes' : '\u2717 No'}
178
+ </Text>
179
+ </Box>
180
+ )}
181
+ </Box>
182
+
183
+ {/* Error reason */}
184
+ <Box flexDirection="column">
185
+ <Text color="red" dimColor>{'─'.repeat(boxWidth - SEPARATOR_LINE_PADDING)}</Text>
186
+ <Box marginTop={1}>
187
+ <Text color="red">Error: </Text>
188
+ <Text color="white">{info.errorReason}</Text>
189
+ </Box>
190
+ </Box>
191
+ </Box>
192
+
193
+ {/* Warning */}
194
+ <Box
195
+ flexDirection="column"
196
+ borderStyle="round"
197
+ borderColor="yellow"
198
+ paddingX={2}
199
+ paddingY={1}
200
+ marginTop={1}
201
+ width={boxWidth}
202
+ >
203
+ <Text color="yellow">
204
+ {'\u26A0'} Starting fresh will overwrite this save state
205
+ </Text>
206
+ </Box>
207
+
208
+ <DialogOptionsList options={options} selectedIndex={selectedIndex} boxWidth={boxWidth} />
209
+ </>
210
+ )}
211
+ </DialogContainer>
212
+ );
213
+ };
214
+
215
+ /**
216
+ * Show the corrupted state dialog and get user's choice
217
+ */
218
+ export const showCorruptedStateDialog = (
219
+ info: CorruptedStateInfo,
220
+ options: DialogRenderOptions = {}
221
+ ): Promise<CorruptedStateChoice> => launchDialog<CorruptedStateChoice>(
222
+ (onChoice) => <CorruptedStateDialog info={info} onChoice={onChoice} />,
223
+ 'cancel',
224
+ { ...options, title: options.title ?? 'emoemu - Corrupted Save State' },
225
+ );