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,276 @@
1
+ /**
2
+ * Core Registry
3
+ *
4
+ * Discovers and instantiates emulator cores based on ROM file extension.
5
+ * This allows automatic core selection when loading a ROM.
6
+ */
7
+
8
+ import { pipe, map, sortBy, flatMap, unique, identity } from 'remeda';
9
+ import type { Core, SystemInfo } from '../../core/core';
10
+ import { getDatabaseName, PLAYLIST_EXTENSION, DEFAULT_DATABASE_NAME } from '../playlist';
11
+
12
+ /**
13
+ * Options that can be passed when creating a core instance
14
+ */
15
+ export interface CoreCreateOptions {
16
+ /** Core-specific options (for libretro cores, in RetroArch format) */
17
+ coreOptions?: Record<string, string>;
18
+ /** System directory path for BIOS files */
19
+ systemDirectory?: string;
20
+ /** Save directory path */
21
+ saveDirectory?: string;
22
+ }
23
+
24
+ /**
25
+ * Factory function for creating a core instance
26
+ */
27
+ export interface CoreFactory {
28
+ /** Create a new instance of the core, optionally with configuration options */
29
+ create(options?: CoreCreateOptions): Core;
30
+
31
+ /** File extensions this core handles (lowercase, with dot) */
32
+ extensions: string[];
33
+
34
+ /** Get system info without creating a full core instance */
35
+ getSystemInfo(): SystemInfo;
36
+
37
+ /** Path to the core file */
38
+ path?: string;
39
+ }
40
+
41
+ /**
42
+ * Registry of available cores
43
+ */
44
+ const coreFactories = new Map<string, CoreFactory>();
45
+
46
+ /**
47
+ * Pre-computed map of extension → matching cores (sorted).
48
+ * Built lazily on first access, invalidated when cores are registered.
49
+ */
50
+ let extensionToCoresCache: Map<string, Array<{ id: string; factory: CoreFactory }>> | null = null;
51
+
52
+ /**
53
+ * Cached list of all supported extensions.
54
+ * Built lazily on first access, invalidated when cores are registered.
55
+ */
56
+ let supportedExtensionsCache: string[] | null = null;
57
+
58
+ /**
59
+ * Build the extension→cores lookup map from registered cores.
60
+ * Cores are pre-sorted: libretro first, then alphabetically.
61
+ */
62
+ const buildExtensionMap = (): Map<string, Array<{ id: string; factory: CoreFactory }>> => {
63
+ const extMap = new Map<string, Array<{ id: string; factory: CoreFactory }>>();
64
+
65
+ // Collect all cores for each extension
66
+ for (const [id, factory] of coreFactories) {
67
+ for (const ext of factory.extensions) {
68
+ const existing = extMap.get(ext) ?? [];
69
+ existing.push({ id, factory });
70
+ extMap.set(ext, existing);
71
+ }
72
+ }
73
+
74
+ // Sort each extension's cores alphabetically
75
+ for (const [ext, cores] of extMap) {
76
+ extMap.set(ext, pipe(
77
+ cores,
78
+ sortBy(
79
+ [({ id }) => id, 'asc']
80
+ )
81
+ ));
82
+ }
83
+
84
+ return extMap;
85
+ };
86
+
87
+ /**
88
+ * Get the extension→cores map, building it if necessary.
89
+ */
90
+ const getExtensionMap = (): Map<string, Array<{ id: string; factory: CoreFactory }>> => {
91
+ if (!extensionToCoresCache) {
92
+ extensionToCoresCache = buildExtensionMap();
93
+ }
94
+ return extensionToCoresCache;
95
+ };
96
+
97
+ /**
98
+ * Register a core factory.
99
+ * Called by each core module to make itself available.
100
+ *
101
+ * @param id Unique core identifier (e.g., "nes", "gba")
102
+ * @param factory Factory for creating core instances
103
+ */
104
+ export const registerCore = (id: string, factory: CoreFactory): void => {
105
+ if (coreFactories.has(id)) {
106
+ throw new Error(`Core '${id}' is already registered`);
107
+ }
108
+ coreFactories.set(id, factory);
109
+ // Invalidate caches so they're rebuilt on next access
110
+ extensionToCoresCache = null;
111
+ supportedExtensionsCache = null;
112
+ };
113
+
114
+ /**
115
+ * Unregister a core factory.
116
+ * Used when deleting a libretro core to remove it from the registry.
117
+ *
118
+ * @param id Core identifier to unregister
119
+ * @returns true if the core was registered and removed, false if not found
120
+ */
121
+ export const unregisterCore = (id: string): boolean => {
122
+ const existed = coreFactories.has(id);
123
+ coreFactories.delete(id);
124
+ // Invalidate caches so they're rebuilt on next access
125
+ extensionToCoresCache = null;
126
+ supportedExtensionsCache = null;
127
+ return existed;
128
+ };
129
+
130
+ /**
131
+ * Get a core factory by ID.
132
+ *
133
+ * @param id Core identifier
134
+ * @returns Core factory or undefined if not found
135
+ */
136
+ export const getCoreFactory = (id: string): CoreFactory | undefined => coreFactories.get(id);
137
+
138
+ /**
139
+ * Create a core instance by ID.
140
+ *
141
+ * @param id Core identifier
142
+ * @param options Optional configuration for the core (e.g., core options, directories)
143
+ * @returns New core instance or null if core not found
144
+ */
145
+ export const createCore = (id: string, options?: CoreCreateOptions): Core | null => {
146
+ const factory = coreFactories.get(id);
147
+ return factory ? factory.create(options) : null;
148
+ };
149
+
150
+ /**
151
+ * Find all cores that support a given ROM file extension.
152
+ * Uses pre-computed extension→cores map for O(1) lookup.
153
+ *
154
+ * @param romPath Path to the ROM file
155
+ * @returns Array of matching cores with their IDs and factories
156
+ */
157
+ export const findMatchingCores = (romPath: string): Array<{ id: string; factory: CoreFactory }> => {
158
+ const ext = romPath.toLowerCase().match(/\.[^.]+$/)?.[0];
159
+ if (!ext) {
160
+ return [];
161
+ }
162
+
163
+ // O(1) lookup from pre-computed map (already sorted)
164
+ return getExtensionMap().get(ext) ?? [];
165
+ };
166
+
167
+ /**
168
+ * Find all cores that support a given file extension directly.
169
+ * Useful when extension is already extracted.
170
+ *
171
+ * @param ext File extension (lowercase, with dot)
172
+ * @returns Array of matching cores with their IDs and factories
173
+ */
174
+ export const findMatchingCoresByExtension = (ext: string): Array<{ id: string; factory: CoreFactory }> => {
175
+ return getExtensionMap().get(ext.toLowerCase()) ?? [];
176
+ };
177
+
178
+ /**
179
+ * Detect the appropriate core for a ROM file based on extension.
180
+ *
181
+ * @param romPath Path to the ROM file
182
+ * @returns Core factory or undefined if no matching core found
183
+ */
184
+ export const detectCoreFactory = (romPath: string): CoreFactory | undefined => {
185
+ const matches = findMatchingCores(romPath);
186
+ return matches.length > 0 ? matches[0].factory : undefined;
187
+ };
188
+
189
+ /**
190
+ * Detect and create the appropriate core for a ROM file.
191
+ *
192
+ * @param romPath Path to the ROM file
193
+ * @param options Optional configuration for the core (e.g., core options, directories)
194
+ * @returns New core instance or null if no matching core found
195
+ */
196
+ export const detectCore = (romPath: string, options?: CoreCreateOptions): Core | null => {
197
+ const factory = detectCoreFactory(romPath);
198
+ return factory ? factory.create(options) : null;
199
+ };
200
+
201
+ /**
202
+ * Check if a ROM file extension is supported by any registered core.
203
+ *
204
+ * @param romPath Path to the ROM file
205
+ * @returns true if a core can handle this file type
206
+ */
207
+ export const isRomSupported = (romPath: string): boolean => detectCoreFactory(romPath) !== undefined;
208
+
209
+ /**
210
+ * Get list of all registered cores with their info.
211
+ *
212
+ * @returns Array of core information objects
213
+ */
214
+ export const listCores = (): Array<{
215
+ id: string;
216
+ name: string;
217
+ extensions: string[];
218
+ path: string;
219
+ }> => pipe(
220
+ Array.from(coreFactories.entries()),
221
+ map(([id, factory]) => ({
222
+ id,
223
+ name: factory.getSystemInfo().name,
224
+ extensions: factory.extensions,
225
+ path: factory.path ?? "",
226
+ }))
227
+ );
228
+
229
+ /**
230
+ * Get all supported file extensions across all cores.
231
+ * Result is cached and invalidated when cores are registered.
232
+ *
233
+ * @returns Array of supported extensions (lowercase, with dot)
234
+ */
235
+ export const getSupportedExtensions = (): string[] => {
236
+ if (!supportedExtensionsCache) {
237
+ supportedExtensionsCache = pipe(
238
+ Array.from(coreFactories.values()),
239
+ flatMap((factory) => factory.extensions),
240
+ unique(),
241
+ sortBy([identity(), 'asc'])
242
+ );
243
+ }
244
+ return supportedExtensionsCache;
245
+ };
246
+
247
+ /** System name for unknown/unmapped extensions */
248
+ const UNKNOWN_SYSTEM_NAME = DEFAULT_DATABASE_NAME.replace(PLAYLIST_EXTENSION, '');
249
+
250
+ /**
251
+ * Get unique supported system names based on installed cores.
252
+ * Returns RetroArch-style system names (e.g., "Nintendo - Nintendo Entertainment System").
253
+ */
254
+ export const getSupportedSystems = (): string[] => {
255
+ const systems = new Set<string>();
256
+ // Get extensions from installed cores and map to system names
257
+ for (const ext of getSupportedExtensions()) {
258
+ const dbName = getDatabaseName(ext);
259
+ // Remove .lpl extension to get system name
260
+ const systemName = dbName.replace(PLAYLIST_EXTENSION, '');
261
+ // Skip the fallback "Unknown System" entry
262
+ if (systemName !== UNKNOWN_SYSTEM_NAME) {
263
+ systems.add(systemName);
264
+ }
265
+ }
266
+ /** Unique system names, sorted alphabetically */
267
+ return Array.from(systems).sort();
268
+ };
269
+
270
+ /**
271
+ * Get the number of registered cores.
272
+ */
273
+ export const getCoreCount = (): number => coreFactories.size;
274
+
275
+ // Note: Cores register themselves when their modules are imported.
276
+ // This allows tree-shaking of unused cores in production builds.
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Directory Cache
3
+ *
4
+ * Shared utility for caching directory listings to avoid repeated stat() calls.
5
+ * Used by rom-scanner and playlist reader for efficient file existence checks.
6
+ */
7
+
8
+ import { readdirSync, statSync } from 'fs';
9
+ import { join } from 'path';
10
+
11
+ /**
12
+ * Cache for directory listings to avoid repeated stat() calls.
13
+ * Maps directory path -> filename -> file modification time.
14
+ */
15
+ export type DirectoryCache = Map<string, Map<string, Date>>;
16
+
17
+ /**
18
+ * Create a new empty directory cache.
19
+ */
20
+ export const createDirectoryCache = (): DirectoryCache => new Map();
21
+
22
+ /**
23
+ * Get or create a cached directory listing.
24
+ * Returns a map of filename -> mtime for all files in the directory.
25
+ * Skips hidden files (starting with '.').
26
+ */
27
+ export const getCachedDirectoryListing = (dirPath: string, cache: DirectoryCache): Map<string, Date> => {
28
+ const existing = cache.get(dirPath);
29
+ if (existing) {
30
+ return existing;
31
+ }
32
+
33
+ const listing = new Map<string, Date>();
34
+ try {
35
+ // Use withFileTypes to avoid stat() calls just to check isFile()
36
+ const entries = readdirSync(dirPath, { withFileTypes: true });
37
+ for (const entry of entries) {
38
+ // Skip hidden files and directories (only stat actual files)
39
+ if (entry.name.startsWith('.') || !entry.isFile()) {
40
+ continue;
41
+ }
42
+ try {
43
+ const fullPath = join(dirPath, entry.name);
44
+ const stats = statSync(fullPath);
45
+ listing.set(entry.name, stats.mtime);
46
+ } catch {
47
+ // Skip files we can't stat
48
+ }
49
+ }
50
+ } catch {
51
+ // Directory doesn't exist or can't be read
52
+ }
53
+ cache.set(dirPath, listing);
54
+ return listing;
55
+ };
56
+
57
+ /**
58
+ * Check if a file exists in a cached directory listing.
59
+ * Returns the file's modification time if found, undefined otherwise.
60
+ */
61
+ export const getFileFromCache = (
62
+ dirPath: string,
63
+ filename: string,
64
+ cache: DirectoryCache
65
+ ): Date | undefined => {
66
+ const listing = getCachedDirectoryListing(dirPath, cache);
67
+ return listing.get(filename);
68
+ };
69
+
70
+ /**
71
+ * Clear all entries from a directory cache.
72
+ */
73
+ export const clearDirectoryCache = (cache: DirectoryCache): void => {
74
+ cache.clear();
75
+ };
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Frontend Module Exports
3
+ *
4
+ * Shared frontend infrastructure for all emulator cores.
5
+ */
6
+
7
+ export * from './consts';
8
+ export { AudioManager } from './AudioManager';
9
+ export {
10
+ type DirectoryCache,
11
+ createDirectoryCache,
12
+ getCachedDirectoryListing,
13
+ getFileFromCache,
14
+ clearDirectoryCache,
15
+ } from './directoryCache';
16
+ export { SaveStateService, BatterySaveService } from './saveServices';
17
+ export type { SaveStateDetails, SaveStateCheckResult, BatterySaveCheckResult } from './saveServices';
18
+ export {
19
+ initializeServices,
20
+ updateServices,
21
+ getSaveStateService,
22
+ getBatterySaveService,
23
+ areServicesInitialized,
24
+ getConfig,
25
+ getPlaylistDirectory,
26
+ buildCrcCache,
27
+ } from './serviceProvider';
28
+ export {
29
+ registerCore,
30
+ getCoreFactory,
31
+ createCore,
32
+ detectCoreFactory,
33
+ detectCore,
34
+ isRomSupported,
35
+ listCores,
36
+ getSupportedExtensions,
37
+ getCoreCount,
38
+ } from './coreRegistry';
39
+ export type { CoreFactory } from './coreRegistry';
40
+ export {
41
+ loadConfig,
42
+ updateConfigValue,
43
+ configExists,
44
+ ensureConfigExists,
45
+ DEFAULT_CONFIG,
46
+ } from './config';
47
+ export type { Config, VideoDriver } from './config';
48
+ export { SettingsManager } from './SettingsManager';
49
+ export type { RuntimeSettings, RenderMode } from './SettingsManager';
50
+ export {
51
+ generatePlaylist,
52
+ writePlaylist,
53
+ generateAndWritePlaylist,
54
+ generatePlaylistsBySystem,
55
+ generateConsolidatedPlaylist,
56
+ createPlaylistEntry,
57
+ getDatabaseName,
58
+ updatePlaylistRuntime,
59
+ PLAYLIST_VERSION,
60
+ PLAYLIST_EXTENSION,
61
+ // Reader functions
62
+ readPlaylist,
63
+ playlistEntryToRomInfo,
64
+ playlistToRomInfoArray,
65
+ findPlaylistsInDirectory,
66
+ findPlaylistsForDirectory,
67
+ loadRomsFromPlaylists,
68
+ } from './playlist';
69
+ export type {
70
+ PlaylistEntry,
71
+ PlaylistFile,
72
+ PlaylistOptions,
73
+ PlaylistGenerationResult,
74
+ PlaylistUpdateResult,
75
+ // Reader types
76
+ PlaylistReadResult,
77
+ PlaylistInfo,
78
+ ConversionOptions,
79
+ } from './playlist';
@@ -0,0 +1,14 @@
1
+ export const APP_NAME = 'emoemu';
2
+ export const ICON_FILENAME = 'icon.png';
3
+ export const DEFAULT_NOTIFICATION_DURATION_MS = 3000;
4
+
5
+ /**
6
+ * Numeric values for notification severity (matches libretro SET_MESSAGE_EXT).
7
+ * Useful for converting from libretro numeric values.
8
+ */
9
+ export const NOTIFICATION_SEVERITY = {
10
+ DEBUG: 0,
11
+ INFO: 1,
12
+ WARN: 2,
13
+ ERROR: 3,
14
+ } as const;
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Unified notification system for emoemu.
3
+ * Sends notifications to both native OS (via node-notifier) and any subscribed listeners
4
+ * (e.g., the emulator's status bar). Any part of the app can send notifications through
5
+ * this module, and all subscribers will receive them.
6
+ */
7
+
8
+ import notifier from 'node-notifier';
9
+ import { existsSync, writeFileSync } from 'fs';
10
+ import { ensureDirectory } from '../../utils/ensureDirectory';
11
+ import { join } from 'path';
12
+ import { getConfigDirectory } from '../../utils/paths';
13
+
14
+ export * from './consts';
15
+
16
+ import { APP_NAME, ICON_FILENAME, DEFAULT_NOTIFICATION_DURATION_MS } from './consts';
17
+
18
+ // App icon embedded at build time as base64
19
+ declare const __APP_ICON_BASE64__: string;
20
+ const APP_ICON_BASE64 = typeof __APP_ICON_BASE64__ !== 'undefined' ? __APP_ICON_BASE64__ : '';
21
+
22
+ /** Whether notifications are enabled (default: true) */
23
+ let notificationsEnabled = true;
24
+
25
+ /** Cached path to the extracted icon */
26
+ let iconPath: string | null = null;
27
+
28
+ /**
29
+ * Notification severity - indicates how important/urgent a user-facing notification is.
30
+ * Values match libretro's severity levels for SET_MESSAGE_EXT compatibility.
31
+ */
32
+ export type NotificationSeverity = 'debug' | 'info' | 'warn' | 'error';
33
+
34
+ import { NOTIFICATION_SEVERITY } from './consts';
35
+
36
+ /**
37
+ * Convert numeric severity to NotificationSeverity string.
38
+ */
39
+ export const numericToSeverity = (value: number): NotificationSeverity => {
40
+ switch (value) {
41
+ case NOTIFICATION_SEVERITY.DEBUG: return 'debug';
42
+ case NOTIFICATION_SEVERITY.WARN: return 'warn';
43
+ case NOTIFICATION_SEVERITY.ERROR: return 'error';
44
+ default: return 'info';
45
+ }
46
+ };
47
+
48
+ /**
49
+ * App-wide notification that can be displayed in multiple places
50
+ * (native OS notifications, status bar, etc.)
51
+ */
52
+ export interface AppNotification {
53
+ /** Optional title (defaults to app name for OS notifications) */
54
+ title?: string;
55
+ /** Main message content */
56
+ message: string;
57
+ /** Duration in ms for transient displays like status bar (default: 3000) */
58
+ duration?: number;
59
+ /** Whether to play a sound (for OS notifications) */
60
+ sound?: boolean;
61
+ /** Severity/importance of the notification (default: 'info') */
62
+ severity?: NotificationSeverity;
63
+ }
64
+
65
+ /** Callback for receiving notifications */
66
+ export type NotificationListener = (notification: AppNotification) => void;
67
+
68
+ /** Registered notification listeners */
69
+ const listeners = new Set<NotificationListener>();
70
+
71
+ /**
72
+ * Subscribe to receive all app notifications.
73
+ * Useful for displaying notifications in UI elements like the status bar.
74
+ */
75
+ export const subscribeToNotifications = (listener: NotificationListener): void => {
76
+ listeners.add(listener);
77
+ };
78
+
79
+ /**
80
+ * Unsubscribe from notifications.
81
+ */
82
+ export const unsubscribeFromNotifications = (listener: NotificationListener): void => {
83
+ listeners.delete(listener);
84
+ };
85
+
86
+ /**
87
+ * Set whether notifications are enabled.
88
+ */
89
+ export const setNotificationsEnabled = (enabled: boolean): void => {
90
+ notificationsEnabled = enabled;
91
+ };
92
+
93
+ /**
94
+ * Check if notifications are enabled.
95
+ */
96
+ export const isNotificationsEnabled = (): boolean => notificationsEnabled;
97
+
98
+ /**
99
+ * Ensure the app icon exists in the config directory.
100
+ * Extracts the embedded icon on first run.
101
+ * @returns The absolute path to the icon, or undefined if not available
102
+ */
103
+ export const ensureIcon = (): string | undefined => {
104
+ // Return cached path if already extracted
105
+ if (iconPath !== null) {
106
+ return iconPath.length > 0 ? iconPath : undefined;
107
+ }
108
+
109
+ // No embedded icon available
110
+ if (!APP_ICON_BASE64) {
111
+ iconPath = '';
112
+ return undefined;
113
+ }
114
+
115
+ // Wrap all icon operations in try-catch - icon is nice-to-have, not required
116
+ try {
117
+ const configDir = getConfigDirectory();
118
+ const targetPath = join(configDir, ICON_FILENAME);
119
+
120
+ // Check if icon already exists
121
+ if (existsSync(targetPath)) {
122
+ iconPath = targetPath;
123
+ return iconPath;
124
+ }
125
+
126
+ ensureDirectory(configDir);
127
+
128
+ // Extract icon from embedded base64
129
+ const iconBuffer = Buffer.from(APP_ICON_BASE64, 'base64');
130
+ writeFileSync(targetPath, iconBuffer);
131
+ iconPath = targetPath;
132
+ return iconPath;
133
+ } catch {
134
+ // Any failure with icon detection/extraction - continue without it
135
+ iconPath = '';
136
+ return undefined;
137
+ }
138
+ };
139
+
140
+ /**
141
+ * Send a notification to all channels (native OS and subscribed listeners).
142
+ * This is the primary way to send notifications throughout the app.
143
+ */
144
+ export const notify = (options: AppNotification): void => {
145
+ if (!notificationsEnabled) {
146
+ return;
147
+ }
148
+
149
+ // Broadcast to all listeners (e.g., status bar)
150
+ const notification: AppNotification = {
151
+ title: options.title,
152
+ message: options.message,
153
+ duration: options.duration ?? DEFAULT_NOTIFICATION_DURATION_MS,
154
+ sound: options.sound,
155
+ severity: options.severity ?? 'info',
156
+ };
157
+ for (const listener of listeners) {
158
+ listener(notification);
159
+ }
160
+
161
+ // Send to native OS notification system
162
+ const icon = ensureIcon();
163
+ notifier.notify({
164
+ title: options.title ?? APP_NAME,
165
+ message: options.message,
166
+ sound: options.sound ?? false,
167
+ icon,
168
+ });
169
+ };
170
+
171
+ /**
172
+ * Notify that a screenshot was saved.
173
+ */
174
+ export const notifyScreenshotSaved = (filename: string): void => {
175
+ notify({
176
+ title: 'Screenshot Saved',
177
+ message: filename,
178
+ });
179
+ };
180
+
181
+ /**
182
+ * Notify that a gamepad was connected.
183
+ */
184
+ export const notifyGamepadConnected = (name: string, playerNumber: number): void => {
185
+ notify({
186
+ title: 'Gamepad Connected',
187
+ message: `${name} (Player ${playerNumber})`,
188
+ });
189
+ };
190
+
191
+ /**
192
+ * Notify that a gamepad was disconnected.
193
+ */
194
+ export const notifyGamepadDisconnected = (name: string, playerNumber: number): void => {
195
+ notify({
196
+ title: 'Gamepad Disconnected',
197
+ message: `${name} (Player ${playerNumber})`,
198
+ });
199
+ };
200
+
201
+ /**
202
+ * Notify with a core message (from libretro cores or internal events).
203
+ */
204
+ export const notifyCoreMessage = (message: string, title?: string): void => {
205
+ notify({
206
+ title: title ?? 'emoemu',
207
+ message,
208
+ });
209
+ };
210
+
211
+ /**
212
+ * Notify that a netplay client connected.
213
+ */
214
+ export const notifyNetplayClientConnected = (nickname: string, playerNumber: number): void => {
215
+ notify({
216
+ title: 'Player Joined',
217
+ message: `${nickname} joined as Player ${playerNumber}`,
218
+ });
219
+ };
220
+
221
+ /**
222
+ * Notify that a netplay client disconnected.
223
+ */
224
+ export const notifyNetplayClientDisconnected = (nickname: string): void => {
225
+ notify({
226
+ title: 'Player Left',
227
+ message: `${nickname} disconnected`,
228
+ });
229
+ };
230
+
231
+ /**
232
+ * Notify that a netplay spectator connected.
233
+ */
234
+ export const notifyNetplaySpectatorConnected = (nickname: string): void => {
235
+ notify({
236
+ title: 'Spectator Joined',
237
+ message: `${nickname} is now spectating`,
238
+ });
239
+ };
240
+
241
+ /**
242
+ * Notify that a netplay connection failed.
243
+ */
244
+ export const notifyNetplayConnectionFailed = (nickname: string, reason: string): void => {
245
+ notify({
246
+ title: 'Connection Failed',
247
+ message: `${nickname}: ${reason}`,
248
+ severity: 'warn',
249
+ });
250
+ };