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,608 @@
1
+ /**
2
+ * Configuration file system for emoemu
3
+ *
4
+ * Provides loading and saving of user settings in a RetroArch-compatible
5
+ * INI-style format. Config files are stored in platform-specific locations.
6
+ */
7
+
8
+ import { existsSync, readFileSync, writeFileSync } from "fs";
9
+ import { ensureDirectory } from '../../utils/ensureDirectory';
10
+ import { dirname, join, resolve } from "path";
11
+ import {
12
+ getConfigDirectory,
13
+ getDefaultConfigPath,
14
+ getConfigPaths,
15
+ getDefaultPlaylistsDirectory,
16
+ getDefaultSavestatesDirectory,
17
+ getDefaultSavefilesDirectory,
18
+ } from "../../utils/paths";
19
+ import {
20
+ parseIniLine,
21
+ formatIniValue,
22
+ updateIniLine,
23
+ commentOutIniLine,
24
+ parseIniBool,
25
+ parseIniNumber,
26
+ parseIniNullableNumber,
27
+ } from "../../utils/ini";
28
+ import { pipe, filter, map, isNonNull, fromEntries } from "remeda";
29
+ import { logger } from "../../utils/logger";
30
+ import { DEFAULT_PNG_COMPRESSION } from "../../rendering";
31
+
32
+ /**
33
+ * Get the platform-specific cores directory path
34
+ *
35
+ * This follows the same convention as RetroArch:
36
+ * - macOS: ~/Library/Application Support/emoemu/cores
37
+ * - Linux: ~/.config/emoemu/cores
38
+ * - Windows: %APPDATA%\emoemu\cores
39
+ */
40
+ export const getCoresDirectory = (): string => join(getConfigDirectory(), "cores");
41
+
42
+ /**
43
+ * Get the effective playlists directory path (always absolute)
44
+ * Uses config value if set, otherwise platform-specific default
45
+ */
46
+ export const getPlaylistsDirectory = (config: Config): string =>
47
+ resolve(config.playlist_directory || getDefaultPlaylistsDirectory());
48
+
49
+ /**
50
+ * Get the effective save states directory path (always absolute)
51
+ * Uses config value if set, otherwise platform-specific default:
52
+ * - macOS: ~/Library/Application Support/emoemu/states
53
+ * - Linux: ~/.config/emoemu/states
54
+ * - Windows: %APPDATA%\emoemu\states
55
+ */
56
+ export const getSavestatesDirectory = (config: Config): string =>
57
+ resolve(config.savestate_directory || getDefaultSavestatesDirectory());
58
+
59
+ /**
60
+ * Get the effective save files (battery saves) directory path (always absolute)
61
+ * Uses config value if set, otherwise platform-specific default:
62
+ * - macOS: ~/Library/Application Support/emoemu/saves
63
+ * - Linux: ~/.config/emoemu/saves
64
+ * - Windows: %APPDATA%\emoemu\saves
65
+ */
66
+ export const getSavefilesDirectory = (config: Config): string =>
67
+ resolve(config.savefile_directory || getDefaultSavefilesDirectory());
68
+
69
+ /**
70
+ * Determine the directory to look for save states based on config settings.
71
+ * - If savestates_in_content_dir is true (or config is not provided): use content (ROM) directory
72
+ * - If savestates_in_content_dir is false: use configured savestate_directory or platform default
73
+ *
74
+ * @param contentDir The ROM's directory (dirname of romPath)
75
+ * @param config Optional config to determine save state directory
76
+ */
77
+ export const resolveSaveStateDir = (contentDir: string, config?: Config): string => {
78
+ if (!config || config.savestates_in_content_dir !== false) {
79
+ return contentDir;
80
+ }
81
+ return getSavestatesDirectory(config);
82
+ };
83
+
84
+ /**
85
+ * Determine the directory to look for battery saves based on config settings.
86
+ * - If savefiles_in_content_dir is true (or config is not provided): use content (ROM) directory
87
+ * - If savefiles_in_content_dir is false: use configured savefile_directory or platform default
88
+ *
89
+ * @param contentDir The ROM's directory (dirname of romPath)
90
+ * @param config Optional config to determine save file directory
91
+ */
92
+ export const resolveSaveFileDir = (contentDir: string, config?: Config): string => {
93
+ if (!config || config.savefiles_in_content_dir !== false) {
94
+ return contentDir;
95
+ }
96
+ return getSavefilesDirectory(config);
97
+ };
98
+
99
+ // Re-export for convenience
100
+ export { getDefaultPlaylistsDirectory, getDefaultSavestatesDirectory, getDefaultSavefilesDirectory };
101
+
102
+ export * from './types';
103
+
104
+ import type { VideoDriver, PostProcessingMode } from './types';
105
+ import { isVideoDriver } from './types';
106
+
107
+ /**
108
+ * Configuration interface matching the documented format
109
+ */
110
+ export interface Config {
111
+ // Video
112
+ video_driver: VideoDriver | null; // null = Auto (use system-specific default)
113
+ video_scale: number | null; // null = Auto (use system-specific default)
114
+ video_smooth: boolean;
115
+ video_fullscreen: boolean;
116
+ custom_viewport_width: number | null;
117
+ custom_viewport_height: number | null;
118
+ video_color_enable: boolean;
119
+ video_diff_render: boolean;
120
+ menu_scale_factor: number | null; // UI scale factor for native mode (null = auto-detect from display)
121
+
122
+ // Post-processing
123
+ video_postprocessing_mode: PostProcessingMode; // off, crt, or custom
124
+
125
+ // CRT preset values (used when video_postprocessing_mode is 'crt')
126
+ crt_gamma: number;
127
+ crt_scanlines: number;
128
+ crt_saturation: number;
129
+ crt_vignette: number;
130
+ crt_ntsc: number;
131
+ crt_curvature: number;
132
+ crt_chromatic_aberration: number;
133
+
134
+ // Custom effect values (used when video_postprocessing_mode is 'custom')
135
+ video_shader_enable: boolean;
136
+ video_gamma: number;
137
+ video_scanlines: number;
138
+ video_saturation: number;
139
+ video_brightness: number;
140
+ video_contrast: number;
141
+ video_vignette: number;
142
+ video_bloom: number;
143
+ video_bloom_threshold: number;
144
+ video_ntsc: number;
145
+ video_curvature: number;
146
+ video_chromatic_aberration: number;
147
+
148
+ // Kitty
149
+ kitty_png_level: number;
150
+
151
+ // Audio
152
+ audio_enable: boolean;
153
+ audio_volume: number;
154
+ audio_mute_enable: boolean;
155
+
156
+ // Input
157
+ input_joypad_enable: boolean;
158
+ input_autodetect_enable: boolean;
159
+
160
+ // Save data
161
+ savestate_auto_load: boolean;
162
+ savestate_auto_save: boolean;
163
+ savestate_compression: boolean;
164
+ savestate_directory: string;
165
+ savefile_directory: string;
166
+ savefiles_in_content_dir: boolean; // When true, battery saves (.srm) go to ROM directory
167
+ savestates_in_content_dir: boolean; // When true, save states (.state.auto) go to ROM directory
168
+ battery_save_enable: boolean;
169
+
170
+ // Directories
171
+ system_directory: string;
172
+ screenshot_directory: string;
173
+ playlist_directory: string; // Output directory for generated playlists
174
+
175
+ // Emulation
176
+ fps_show_enable: boolean;
177
+ fps_limit: number;
178
+ video_frame_limit: number; // Limit rendering to N fps (0=off/unlimited, 30, 60, or any positive integer)
179
+
180
+ // Core
181
+ core_default: string;
182
+ libretro_directory: string;
183
+ retroarch_cores_enable: boolean;
184
+
185
+ // Auto-crop
186
+ video_auto_crop_cores: string; // Comma-separated list of core IDs for auto-crop (e.g., "mupen64plus_next")
187
+
188
+ // Browser
189
+ browser_scan_depth: number; // Max depth to scan for ROMs (0=dir only, 1=+subdirs, -1=unlimited)
190
+
191
+ // Notifications
192
+ notifications_enable: boolean;
193
+
194
+ // UI/Menu colors
195
+ menu_highlight_bg: string; // Background color for highlighted menu items
196
+ menu_highlight_fg: string; // Foreground (text) color for highlighted menu items
197
+
198
+ // Logging
199
+ log_verbosity: boolean; // Enable logging (default: true)
200
+ log_to_file: boolean; // Write logs to file (default: true), false = output to console
201
+ log_to_file_timestamp: boolean; // Use timestamped log files instead of overwriting (default: false)
202
+ log_dir: string; // Custom log directory (empty = platform default)
203
+ }
204
+
205
+ /**
206
+ * Default configuration values
207
+ */
208
+ export const DEFAULT_CONFIG: Config = {
209
+ // Video
210
+ video_driver: null, // Auto (use system-specific default)
211
+ video_scale: null, // Auto (use system-specific default)
212
+ video_smooth: false,
213
+ video_fullscreen: false,
214
+ custom_viewport_width: null,
215
+ custom_viewport_height: null,
216
+ video_color_enable: true,
217
+ video_diff_render: true,
218
+ menu_scale_factor: null, // Auto-detect from display
219
+
220
+ // Post-processing
221
+ video_postprocessing_mode: "off",
222
+
223
+ // CRT preset values (used when video_postprocessing_mode is 'crt')
224
+ crt_gamma: 1.3,
225
+ crt_scanlines: 0.1,
226
+ crt_saturation: 1.0,
227
+ crt_vignette: 0.5,
228
+ crt_ntsc: 1.0,
229
+ crt_curvature: 0.1,
230
+ crt_chromatic_aberration: 0,
231
+
232
+ video_shader_enable: false,
233
+ video_gamma: 1.0,
234
+ video_scanlines: 0,
235
+ video_saturation: 1.0,
236
+ video_brightness: 1.0,
237
+ video_contrast: 1.0,
238
+ video_vignette: 0,
239
+ video_bloom: 0,
240
+ video_bloom_threshold: 0.6,
241
+ video_ntsc: 0,
242
+ video_curvature: 0,
243
+ video_chromatic_aberration: 0,
244
+
245
+ // Kitty
246
+ kitty_png_level: DEFAULT_PNG_COMPRESSION,
247
+
248
+ // Audio
249
+ audio_enable: true,
250
+ audio_volume: 1.0,
251
+ audio_mute_enable: false,
252
+
253
+ // Input
254
+ input_joypad_enable: true,
255
+ input_autodetect_enable: true,
256
+
257
+ // Save data
258
+ savestate_auto_load: true,
259
+ savestate_auto_save: true,
260
+ savestate_compression: true,
261
+ savestate_directory: "",
262
+ savefile_directory: "",
263
+ savefiles_in_content_dir: true, // Default: battery saves (.srm) go to ROM directory
264
+ savestates_in_content_dir: true, // Default: save states (.state.auto) go to ROM directory
265
+ battery_save_enable: true,
266
+
267
+ // Directories
268
+ system_directory: "",
269
+ screenshot_directory: "",
270
+ playlist_directory: "", // Empty = use platform default (~/Library/Application Support/emoemu/playlists, etc.)
271
+
272
+ // Emulation
273
+ fps_show_enable: false,
274
+ fps_limit: 0,
275
+ video_frame_limit: 0, // Off by default (no render limit)
276
+
277
+ // Core
278
+ core_default: "",
279
+ libretro_directory: "",
280
+ retroarch_cores_enable: false,
281
+
282
+ // Auto-crop
283
+ video_auto_crop_cores: "mupen64plus_next", // Only N64 cores by default
284
+
285
+ // Browser
286
+ browser_scan_depth: 1, // Scan current dir + immediate subdirs by default
287
+
288
+ // Notifications
289
+ notifications_enable: true,
290
+
291
+ // UI/Menu colors
292
+ menu_highlight_bg: "cyan", // Cyan background
293
+ menu_highlight_fg: "black", // Black text
294
+
295
+ // Logging
296
+ log_verbosity: true, // Enable logging by default
297
+ log_to_file: true, // Write logs to file by default
298
+ log_to_file_timestamp: false, // Overwrite emoemu.log by default
299
+ log_dir: "", // Empty = use platform default
300
+ };
301
+
302
+ /** Type guard for valid config keys */
303
+ const isConfigKey = (key: string): key is keyof Config => key in DEFAULT_CONFIG;
304
+
305
+ /** Keys that are nullable strings (null = auto/default) */
306
+ const NULLABLE_STRING_KEYS: Set<keyof Config> = new Set(['video_driver']);
307
+
308
+ const parseValue = (key: keyof Config, value: string): Config[keyof Config] => {
309
+ const defaultValue = DEFAULT_CONFIG[key];
310
+ const type = typeof defaultValue;
311
+
312
+ // Handle nullable string keys (like video_driver)
313
+ if (NULLABLE_STRING_KEYS.has(key)) {
314
+ const trimmed = value.toLowerCase().trim();
315
+ if (trimmed === 'null' || trimmed === '') {
316
+ return null as Config[keyof Config];
317
+ }
318
+ // video_driver must be a known driver; unknown/legacy values (e.g. the removed "sdl") fall back to Auto
319
+ if (key === 'video_driver' && !isVideoDriver(trimmed)) {
320
+ return null as Config[keyof Config];
321
+ }
322
+ return trimmed as Config[keyof Config];
323
+ }
324
+
325
+ // Handle nullable number keys (like video_scale, custom_viewport_width/height)
326
+ if (defaultValue === null) {
327
+ return parseIniNullableNumber(value) as Config[keyof Config];
328
+ }
329
+
330
+ switch (type) {
331
+ case "boolean":
332
+ return parseIniBool(value) as Config[keyof Config];
333
+ case "number":
334
+ return parseIniNumber(value, defaultValue as number) as Config[keyof Config];
335
+ case "string":
336
+ return value as Config[keyof Config];
337
+ default:
338
+ return value as Config[keyof Config];
339
+ }
340
+ };
341
+
342
+ /**
343
+ * Parse config file content into a partial Config object
344
+ */
345
+ const parseConfig = (content: string): Partial<Config> => pipe(
346
+ content.split("\n"),
347
+ map(parseIniLine),
348
+ filter(isNonNull),
349
+ filter((entry): entry is { key: keyof Config; value: string } => isConfigKey(entry.key)),
350
+ map(({ key, value }) => [key, parseValue(key, value)] as const),
351
+ fromEntries
352
+ ) as Partial<Config>;
353
+
354
+ /**
355
+ * Load configuration from file
356
+ *
357
+ * Searches config paths in order of precedence and returns the merged config.
358
+ * Values from higher-precedence files override lower ones.
359
+ *
360
+ * If a custom path is provided but doesn't exist, returns defaults without
361
+ * falling back to other paths (explicit path takes precedence).
362
+ *
363
+ * @param customPath Optional custom config path (highest precedence)
364
+ * @returns The loaded config merged with defaults, and the path that was loaded
365
+ */
366
+ export const loadConfig = (customPath?: string): { config: Config; loadedFrom: string | null } => {
367
+ // If a custom path is explicitly provided, only check that path
368
+ // Don't fall back to other paths - explicit path takes precedence
369
+ if (customPath !== undefined) {
370
+ if (existsSync(customPath)) {
371
+ try {
372
+ const content = readFileSync(customPath, "utf-8");
373
+ const parsed = parseConfig(content);
374
+ logger.info(`Loading config file: "${customPath}"`, 'Config');
375
+ return {
376
+ config: { ...DEFAULT_CONFIG, ...parsed },
377
+ loadedFrom: customPath,
378
+ };
379
+ } catch {
380
+ // Failed to read/parse, return defaults
381
+ logger.warn(`Failed to parse config file: "${customPath}"`, 'Config');
382
+ return { config: { ...DEFAULT_CONFIG }, loadedFrom: null };
383
+ }
384
+ }
385
+ // Custom path doesn't exist, return defaults
386
+ logger.debug(`Config file not found: "${customPath}"`, 'Config');
387
+ return { config: { ...DEFAULT_CONFIG }, loadedFrom: null };
388
+ }
389
+
390
+ // No custom path specified, search standard locations
391
+ const paths = getConfigPaths();
392
+
393
+ // Find the first existing config file
394
+ for (const path of paths) {
395
+ if (existsSync(path)) {
396
+ try {
397
+ const content = readFileSync(path, "utf-8");
398
+ const parsed = parseConfig(content);
399
+ logger.info(`Loading config file: "${path}"`, 'Config');
400
+ return {
401
+ config: { ...DEFAULT_CONFIG, ...parsed },
402
+ loadedFrom: path,
403
+ };
404
+ } catch {
405
+ // Failed to read/parse, try next path
406
+ logger.warn(`Failed to parse config file: "${path}"`, 'Config');
407
+ continue;
408
+ }
409
+ }
410
+ }
411
+
412
+ // No config file found, use defaults
413
+ logger.debug('No config file found, using defaults', 'Config');
414
+ return { config: { ...DEFAULT_CONFIG }, loadedFrom: null };
415
+ };
416
+
417
+
418
+ /**
419
+ * Generate config file template with all settings commented out.
420
+ */
421
+ const generateConfigTemplate = (): string => {
422
+ const d = DEFAULT_CONFIG;
423
+ return `# emoemu Configuration
424
+ # https://github.com/tuxracer/emoemu
425
+ #
426
+ # Settings are commented out by default and use built-in defaults.
427
+ # Uncomment and modify settings you want to customize.
428
+
429
+ # # Video settings
430
+ # video_driver = null # Auto (native, kitty, terminal, ascii, emoji)
431
+ # video_scale = null # Auto (use system-specific default, or set to 0.25, 0.5, 1, 2, 3, 4)
432
+ # video_smooth = ${formatIniValue(d.video_smooth)}
433
+ # video_fullscreen = ${formatIniValue(d.video_fullscreen)}
434
+ # custom_viewport_width = null
435
+ # custom_viewport_height = null
436
+ # video_color_enable = ${formatIniValue(d.video_color_enable)}
437
+ # video_diff_render = ${formatIniValue(d.video_diff_render)}
438
+
439
+ # # Post-processing effects (mode: off, crt, or custom)
440
+ # video_postprocessing_mode = ${formatIniValue(d.video_postprocessing_mode)}
441
+
442
+ # # CRT preset values (used when video_postprocessing_mode is 'crt')
443
+ # crt_gamma = ${formatIniValue(d.crt_gamma)}
444
+ # crt_scanlines = ${formatIniValue(d.crt_scanlines)}
445
+ # crt_saturation = ${formatIniValue(d.crt_saturation)}
446
+ # crt_vignette = ${formatIniValue(d.crt_vignette)}
447
+ # crt_ntsc = ${formatIniValue(d.crt_ntsc)}
448
+ # crt_curvature = ${formatIniValue(d.crt_curvature)}
449
+ # crt_chromatic_aberration = ${formatIniValue(d.crt_chromatic_aberration)}
450
+
451
+ # # Custom effect values (used when video_postprocessing_mode is 'custom')
452
+ # video_shader_enable = ${formatIniValue(d.video_shader_enable)}
453
+ # video_gamma = ${formatIniValue(d.video_gamma)}
454
+ # video_scanlines = ${formatIniValue(d.video_scanlines)}
455
+ # video_saturation = ${formatIniValue(d.video_saturation)}
456
+ # video_brightness = ${formatIniValue(d.video_brightness)}
457
+ # video_contrast = ${formatIniValue(d.video_contrast)}
458
+ # video_vignette = ${formatIniValue(d.video_vignette)}
459
+ # video_bloom = ${formatIniValue(d.video_bloom)}
460
+ # video_bloom_threshold = ${formatIniValue(d.video_bloom_threshold)}
461
+ # video_ntsc = ${formatIniValue(d.video_ntsc)}
462
+ # video_curvature = ${formatIniValue(d.video_curvature)}
463
+ # video_chromatic_aberration = ${formatIniValue(d.video_chromatic_aberration)}
464
+
465
+ # # Kitty-specific settings
466
+ # kitty_png_level = ${formatIniValue(d.kitty_png_level)}
467
+
468
+ # # Audio settings
469
+ # audio_enable = ${formatIniValue(d.audio_enable)}
470
+ # audio_volume = ${formatIniValue(d.audio_volume)}
471
+ # audio_mute_enable = ${formatIniValue(d.audio_mute_enable)}
472
+
473
+ # # Input settings
474
+ # input_joypad_enable = ${formatIniValue(d.input_joypad_enable)}
475
+ # input_autodetect_enable = ${formatIniValue(d.input_autodetect_enable)}
476
+
477
+ # # Save data settings
478
+ # savestate_auto_load = ${formatIniValue(d.savestate_auto_load)}
479
+ # savestate_auto_save = ${formatIniValue(d.savestate_auto_save)}
480
+ # savestate_compression = ${formatIniValue(d.savestate_compression)}
481
+ # savestate_directory = ${formatIniValue(d.savestate_directory)}
482
+ # savefile_directory = ${formatIniValue(d.savefile_directory)}
483
+ # savefiles_in_content_dir = ${formatIniValue(d.savefiles_in_content_dir)}
484
+ # savestates_in_content_dir = ${formatIniValue(d.savestates_in_content_dir)}
485
+ # battery_save_enable = ${formatIniValue(d.battery_save_enable)}
486
+
487
+ # # Directory settings
488
+ # system_directory = ${formatIniValue(d.system_directory)}
489
+ # screenshot_directory = ${formatIniValue(d.screenshot_directory)}
490
+
491
+ # # Emulation settings
492
+ # fps_show_enable = ${formatIniValue(d.fps_show_enable)}
493
+ # fps_limit = ${formatIniValue(d.fps_limit)}
494
+ # video_frame_limit = ${formatIniValue(d.video_frame_limit)}
495
+
496
+ # # Core settings
497
+ # core_default = ${formatIniValue(d.core_default)}
498
+ # libretro_directory = ${formatIniValue(d.libretro_directory)}
499
+ # retroarch_cores_enable = ${formatIniValue(d.retroarch_cores_enable)}
500
+
501
+ # # Browser settings
502
+ # browser_scan_depth = ${formatIniValue(d.browser_scan_depth)}
503
+
504
+ # # Notifications
505
+ # notifications_enable = ${formatIniValue(d.notifications_enable)}
506
+
507
+ # # UI/Menu colors (ANSI color names: black, red, green, yellow, blue, magenta, cyan, white,
508
+ # # or bright variants: blackBright, redBright, greenBright, yellowBright, blueBright, etc.)
509
+ # menu_highlight_bg = ${formatIniValue(d.menu_highlight_bg)}
510
+ # menu_highlight_fg = ${formatIniValue(d.menu_highlight_fg)}
511
+
512
+ # # Logging settings
513
+ # log_verbosity = ${formatIniValue(d.log_verbosity)}
514
+ # log_to_file = ${formatIniValue(d.log_to_file)}
515
+ # log_to_file_timestamp = ${formatIniValue(d.log_to_file_timestamp)}
516
+ # log_dir = ${formatIniValue(d.log_dir)}
517
+ `;
518
+ };
519
+
520
+ /**
521
+ * Save raw content to config file
522
+ *
523
+ * @param content The content to write
524
+ * @param path Path to save to
525
+ */
526
+ const saveConfigContent = (content: string, path: string): void => {
527
+ const dir = dirname(path);
528
+
529
+ ensureDirectory(dir);
530
+
531
+ writeFileSync(path, content, "utf-8");
532
+ };
533
+
534
+ /**
535
+ * Check if a config file exists at any of the search paths
536
+ */
537
+ export const configExists = (customPath?: string): boolean => {
538
+ const paths = getConfigPaths(customPath);
539
+ return paths.some((path) => existsSync(path));
540
+ };
541
+
542
+ /**
543
+ * Create a default config file if none exists.
544
+ * Creates a template with all settings commented out.
545
+ */
546
+ export const ensureConfigExists = (): string => {
547
+ const defaultPath = getDefaultConfigPath();
548
+
549
+ if (!existsSync(defaultPath)) {
550
+ saveConfigContent(generateConfigTemplate(), defaultPath);
551
+ }
552
+
553
+ return defaultPath;
554
+ };
555
+
556
+ /**
557
+ * Update a single config value and save to file
558
+ *
559
+ * This reads the existing config file, updates (or uncomments) the specified
560
+ * key, and writes back. If the key doesn't exist, it's appended.
561
+ * If no config file exists, creates one from template first.
562
+ *
563
+ * @param key The config key to update
564
+ * @param value The new value
565
+ * @param customPath Optional custom config path
566
+ */
567
+ export const updateConfigValue = <K extends keyof Config>(key: K, value: Config[K], customPath?: string): void => {
568
+ const targetPath = customPath || getDefaultConfigPath();
569
+
570
+ // Read existing content or create template
571
+ let content: string;
572
+ if (existsSync(targetPath)) {
573
+ content = readFileSync(targetPath, "utf-8");
574
+ } else {
575
+ content = generateConfigTemplate();
576
+ }
577
+
578
+ // Update the specific line
579
+ const formattedValue = formatIniValue(value);
580
+ const updatedContent = updateIniLine(content, key, formattedValue);
581
+
582
+ saveConfigContent(updatedContent, targetPath);
583
+ };
584
+
585
+ /**
586
+ * Reset a config value to default by commenting it out
587
+ *
588
+ * Instead of writing the default value, this comments out the setting
589
+ * so the app will use DEFAULT_CONFIG at runtime. This ensures users
590
+ * get updated defaults if they change in future versions.
591
+ *
592
+ * @param key The config key to reset
593
+ * @param customPath Optional custom config path
594
+ */
595
+ export const resetConfigValue = <K extends keyof Config>(key: K, customPath?: string): void => {
596
+ const targetPath = customPath || getDefaultConfigPath();
597
+
598
+ // Only process if config file exists
599
+ if (!existsSync(targetPath)) {
600
+ return;
601
+ }
602
+
603
+ const content = readFileSync(targetPath, "utf-8");
604
+ const updatedContent = commentOutIniLine(content, key);
605
+
606
+ saveConfigContent(updatedContent, targetPath);
607
+ };
608
+