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.
- package/.claude/settings.local.json +77 -0
- package/.node-version +1 -0
- package/CLAUDE.md +435 -0
- package/README.md +404 -0
- package/TODO.md +655 -0
- package/dist/index.cjs +25108 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +25085 -0
- package/docs/building-libretro-cores-arm-mac.md +237 -0
- package/docs/config-file-format.md +488 -0
- package/docs/cores-trd.md +425 -0
- package/docs/headless-hardware-rendering-trd.md +676 -0
- package/docs/libretro-cores-trd.md +997 -0
- package/docs/mupen64-software-rendering-trd.md +751 -0
- package/docs/n64-support-trd.md +306 -0
- package/docs/native-rendering-trd.md +540 -0
- package/docs/native-ui-rendering-trd.md +1195 -0
- package/docs/netplay-trd.md +665 -0
- package/docs/retroarch-netplay-docs.md +277 -0
- package/docs/save-state-format.md +666 -0
- package/eslint.config.js +111 -0
- package/icon/icon.png +0 -0
- package/icon/icon.pxd +0 -0
- package/package.json +63 -0
- package/pnpm-workspace.yaml +10 -0
- package/src/Emulator/consts.ts +14 -0
- package/src/Emulator/index.ts +2496 -0
- package/src/Emulator/saveState/index.ts +155 -0
- package/src/Emulator/screenshot/index.ts +160 -0
- package/src/Emulator/terminalDimensions/index.ts +79 -0
- package/src/Emulator/types.ts +83 -0
- package/src/cli/commands/consts.ts +10 -0
- package/src/cli/commands/index.ts +462 -0
- package/src/cli/parseArgs/consts.ts +17 -0
- package/src/cli/parseArgs/index.ts +457 -0
- package/src/cli/parseArgs/types.ts +61 -0
- package/src/cli/runEmulator/index.ts +406 -0
- package/src/cli/runEmulator/types.ts +7 -0
- package/src/consts.ts +19 -0
- package/src/core/button/consts.ts +35 -0
- package/src/core/button/index.ts +123 -0
- package/src/core/core.ts +300 -0
- package/src/core/index.ts +19 -0
- package/src/cores/libretro/api/index.ts +334 -0
- package/src/cores/libretro/api/types.ts +148 -0
- package/src/cores/libretro/callbacks/consts.ts +41 -0
- package/src/cores/libretro/callbacks/index.ts +456 -0
- package/src/cores/libretro/consts.ts +45 -0
- package/src/cores/libretro/coreOptions/consts.ts +36 -0
- package/src/cores/libretro/coreOptions/index.ts +222 -0
- package/src/cores/libretro/environment/consts.ts +118 -0
- package/src/cores/libretro/environment/index.ts +1095 -0
- package/src/cores/libretro/index.ts +937 -0
- package/src/cores/libretro/loader/index.ts +496 -0
- package/src/cores/libretro/pixelFormat/consts.ts +43 -0
- package/src/cores/libretro/pixelFormat/index.ts +397 -0
- package/src/cores/libretro/types.ts +339 -0
- package/src/frontend/AudioManager/index.ts +420 -0
- package/src/frontend/SettingsManager/index.ts +250 -0
- package/src/frontend/config/index.ts +608 -0
- package/src/frontend/config/tests.ts +354 -0
- package/src/frontend/config/types.ts +36 -0
- package/src/frontend/consts.ts +114 -0
- package/src/frontend/coreBuilder/index.ts +644 -0
- package/src/frontend/coreBuilder/types.ts +15 -0
- package/src/frontend/coreDownloader/index.ts +620 -0
- package/src/frontend/coreDownloader/types.ts +17 -0
- package/src/frontend/corePreferences/index.ts +69 -0
- package/src/frontend/coreRegistry/index.ts +276 -0
- package/src/frontend/directoryCache/index.ts +75 -0
- package/src/frontend/index.ts +79 -0
- package/src/frontend/notifications/consts.ts +14 -0
- package/src/frontend/notifications/index.ts +250 -0
- package/src/frontend/playlist/consts.ts +168 -0
- package/src/frontend/playlist/index.ts +899 -0
- package/src/frontend/playlist/labelFormatter/consts.ts +15 -0
- package/src/frontend/playlist/labelFormatter/index.ts +155 -0
- package/src/frontend/playlist/labelFormatter/tests.ts +153 -0
- package/src/frontend/playlist/reader/index.ts +559 -0
- package/src/frontend/playlist/sync/index.ts +511 -0
- package/src/frontend/playlist/systemLookup/index.ts +233 -0
- package/src/frontend/playlist/utils/index.ts +50 -0
- package/src/frontend/romScanner/consts.ts +348 -0
- package/src/frontend/romScanner/index.ts +1957 -0
- package/src/frontend/saveServices/consts.ts +2 -0
- package/src/frontend/saveServices/index.ts +313 -0
- package/src/frontend/serviceProvider/index.ts +108 -0
- package/src/frontend/serviceProvider/types.ts +13 -0
- package/src/index.ts +428 -0
- package/src/input/Controller/consts.ts +50 -0
- package/src/input/Controller/index.ts +81 -0
- package/src/input/GamepadManager/consts.ts +22 -0
- package/src/input/GamepadManager/index.ts +418 -0
- package/src/input/InputManager/consts.ts +86 -0
- package/src/input/InputManager/index.ts +593 -0
- package/src/input/InputMapper/consts.ts +33 -0
- package/src/input/InputMapper/index.ts +436 -0
- package/src/input/consts.ts +410 -0
- package/src/input/gamepadProfiles/index.ts +1100 -0
- package/src/input/index.ts +38 -0
- package/src/input/inputUtils/index.ts +31 -0
- package/src/netplay/FrameBuffer/consts.ts +2 -0
- package/src/netplay/FrameBuffer/index.ts +364 -0
- package/src/netplay/FrameBuffer/tests.ts +286 -0
- package/src/netplay/InputBuffer/consts.ts +7 -0
- package/src/netplay/InputBuffer/index.ts +347 -0
- package/src/netplay/InputBuffer/tests.ts +166 -0
- package/src/netplay/NetplayClient/index.ts +976 -0
- package/src/netplay/NetplayConnection/index.ts +536 -0
- package/src/netplay/NetplayDiscovery/consts.ts +41 -0
- package/src/netplay/NetplayDiscovery/index.ts +525 -0
- package/src/netplay/NetplayServer/index.ts +1407 -0
- package/src/netplay/SyncManager/index.ts +984 -0
- package/src/netplay/SyncManager/tests.ts +419 -0
- package/src/netplay/consts.ts +371 -0
- package/src/netplay/crc32/consts.ts +14 -0
- package/src/netplay/crc32/index.ts +97 -0
- package/src/netplay/crc32/tests.ts +40 -0
- package/src/netplay/index.ts +41 -0
- package/src/netplay/netplayLogger/consts.ts +30 -0
- package/src/netplay/netplayLogger/index.ts +345 -0
- package/src/netplay/protocol/consts.ts +86 -0
- package/src/netplay/protocol/index.ts +1280 -0
- package/src/netplay/protocol/tests.ts +606 -0
- package/src/netplay/protocol/types.ts +20 -0
- package/src/netplay/types.ts +395 -0
- package/src/rendering/KittyRenderer/index.ts +616 -0
- package/src/rendering/NativeRenderer/index.ts +279 -0
- package/src/rendering/NativeRenderer/tests.ts +133 -0
- package/src/rendering/TerminalRenderer/index.ts +770 -0
- package/src/rendering/consts.ts +401 -0
- package/src/rendering/fonts/CozetteVector.ttf +0 -0
- package/src/rendering/index.ts +26 -0
- package/src/rendering/nativeUi/NativeWindowManager/index.ts +158 -0
- package/src/rendering/nativeUi/NativeWindowManager/tests.ts +81 -0
- package/src/rendering/nativeUi/consts.ts +6 -0
- package/src/rendering/nativeUi/index.ts +20 -0
- package/src/rendering/postProcessing/consts.ts +38 -0
- package/src/rendering/postProcessing/index.ts +923 -0
- package/src/rendering/shared/ansi/consts.ts +12 -0
- package/src/rendering/shared/ansi/index.ts +104 -0
- package/src/rendering/shared/consts.ts +2 -0
- package/src/rendering/shared/fitToTerminal/index.ts +67 -0
- package/src/ui/AddRomsPrompt/consts.ts +13 -0
- package/src/ui/AddRomsPrompt/index.tsx +781 -0
- package/src/ui/App/consts.ts +2 -0
- package/src/ui/App/index.tsx +456 -0
- package/src/ui/AppCapabilities/index.tsx +67 -0
- package/src/ui/ConfigContext/index.tsx +56 -0
- package/src/ui/CoreManager/consts.ts +11 -0
- package/src/ui/CoreManager/index.tsx +779 -0
- package/src/ui/CoreSelector/consts.ts +2 -0
- package/src/ui/CoreSelector/index.tsx +251 -0
- package/src/ui/DialogContainer/index.tsx +42 -0
- package/src/ui/DialogOptionsList/index.tsx +61 -0
- package/src/ui/DuplicateCrcPrompt/consts.ts +5 -0
- package/src/ui/DuplicateCrcPrompt/index.tsx +146 -0
- package/src/ui/GamepadContext/consts.ts +15 -0
- package/src/ui/GamepadContext/index.tsx +295 -0
- package/src/ui/NativeDialog/index.tsx +120 -0
- package/src/ui/NetplayDisconnectedDialog/index.tsx +93 -0
- package/src/ui/NetplayPauseMenu/consts.ts +2 -0
- package/src/ui/NetplayPauseMenu/index.tsx +97 -0
- package/src/ui/RomBrowser/NetplayPanel/consts.ts +24 -0
- package/src/ui/RomBrowser/NetplayPanel/index.tsx +520 -0
- package/src/ui/RomBrowser/SettingsPanel/index.tsx +478 -0
- package/src/ui/RomBrowser/consts.ts +61 -0
- package/src/ui/RomBrowser/index.tsx +1164 -0
- package/src/ui/RomBrowser/settingsConfig/index.ts +320 -0
- package/src/ui/RomBrowser/types.ts +67 -0
- package/src/ui/SaveStateDialog/consts.ts +2 -0
- package/src/ui/SaveStateDialog/index.tsx +225 -0
- package/src/ui/WarningDialog/index.tsx +113 -0
- package/src/ui/consts.ts +27 -0
- package/src/ui/hooks/useClearTerminal/consts.ts +2 -0
- package/src/ui/hooks/useClearTerminal/index.ts +37 -0
- package/src/ui/hooks/useDialogNavigation/index.ts +99 -0
- package/src/ui/hooks/useGamepad/consts.ts +21 -0
- package/src/ui/hooks/useGamepad/index.ts +194 -0
- package/src/ui/index.ts +27 -0
- package/src/utils/buffer/consts.ts +17 -0
- package/src/utils/buffer/index.ts +129 -0
- package/src/utils/color/consts.ts +58 -0
- package/src/utils/color/index.ts +183 -0
- package/src/utils/compression/consts.ts +50 -0
- package/src/utils/compression/index.ts +101 -0
- package/src/utils/consts.ts +2 -0
- package/src/utils/crc32/consts.ts +22 -0
- package/src/utils/crc32/index.ts +83 -0
- package/src/utils/ensureDirectory/index.ts +10 -0
- package/src/utils/format/consts.ts +8 -0
- package/src/utils/format/index.ts +53 -0
- package/src/utils/getErrorMessage/index.ts +10 -0
- package/src/utils/index.ts +113 -0
- package/src/utils/ini/index.ts +200 -0
- package/src/utils/kitty/consts.ts +13 -0
- package/src/utils/kitty/index.ts +181 -0
- package/src/utils/logger/consts.ts +35 -0
- package/src/utils/logger/index.ts +217 -0
- package/src/utils/paths/consts.ts +18 -0
- package/src/utils/paths/index.ts +151 -0
- package/src/utils/png/consts.ts +34 -0
- package/src/utils/png/index.ts +131 -0
- package/src/utils/readJsonFile/index.ts +16 -0
- package/src/utils/rotateLogFile/index.ts +44 -0
- package/src/utils/safeClose/index.ts +10 -0
- package/src/utils/terminal/consts.ts +8 -0
- package/src/utils/terminal/index.ts +102 -0
- package/src/utils/thumbnailRenderer/consts.ts +2 -0
- package/src/utils/thumbnailRenderer/index.ts +147 -0
- package/src/utils/typedError/index.ts +26 -0
- package/tsconfig.json +31 -0
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* General-Purpose Logger
|
|
3
|
+
*
|
|
4
|
+
* Writes debug logs to a file for debugging and monitoring.
|
|
5
|
+
* Log file is stored in the platform-specific config directory.
|
|
6
|
+
* Format matches RetroArch's log format for consistency.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { appendFileSync, writeFileSync } from 'fs';
|
|
10
|
+
import { ensureDirectory } from '../ensureDirectory';
|
|
11
|
+
import { rotateLogFile } from '../rotateLogFile';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
import { getConfigDirectory } from '../paths';
|
|
14
|
+
|
|
15
|
+
/** Log levels */
|
|
16
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
17
|
+
|
|
18
|
+
export * from './consts';
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
LOG_LEVEL_PRIORITY,
|
|
22
|
+
LOG_LEVEL_TAGS,
|
|
23
|
+
MAX_LOG_SIZE_BYTES,
|
|
24
|
+
MAX_BACKUP_FILES,
|
|
25
|
+
DATE_PAD_WIDTH,
|
|
26
|
+
} from './consts';
|
|
27
|
+
|
|
28
|
+
/** Get the default log directory */
|
|
29
|
+
const getDefaultLogDirectory = (): string => join(getConfigDirectory(), 'logs');
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Format current date/time for timestamped log filename
|
|
33
|
+
* Format: emoemu__2026_01_22__02_53_37.log
|
|
34
|
+
*/
|
|
35
|
+
const formatLogTimestamp = (): string => {
|
|
36
|
+
const now = new Date();
|
|
37
|
+
const pad = (n: number): string => String(n).padStart(DATE_PAD_WIDTH, '0');
|
|
38
|
+
const year = now.getFullYear();
|
|
39
|
+
const month = pad(now.getMonth() + 1);
|
|
40
|
+
const day = pad(now.getDate());
|
|
41
|
+
const hour = pad(now.getHours());
|
|
42
|
+
const minute = pad(now.getMinutes());
|
|
43
|
+
const second = pad(now.getSeconds());
|
|
44
|
+
return `emoemu__${year}_${month}_${day}__${hour}_${minute}_${second}.log`;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Logger provides structured logging for application events.
|
|
49
|
+
* Output format matches RetroArch: [LEVEL] [Category]: message
|
|
50
|
+
*/
|
|
51
|
+
class Logger {
|
|
52
|
+
private logPath: string;
|
|
53
|
+
private minLevel: LogLevel = 'debug';
|
|
54
|
+
private enabled = false;
|
|
55
|
+
private initialized = false;
|
|
56
|
+
private logToStderr = false;
|
|
57
|
+
private logToFile = true;
|
|
58
|
+
private useTimestampedFile = false;
|
|
59
|
+
private customLogDir: string | null = null;
|
|
60
|
+
|
|
61
|
+
constructor() {
|
|
62
|
+
this.logPath = join(getDefaultLogDirectory(), 'emoemu.log');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Enable or disable logging */
|
|
66
|
+
setEnabled(enabled: boolean): void {
|
|
67
|
+
this.enabled = enabled;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Check if logging is enabled */
|
|
71
|
+
isEnabled(): boolean {
|
|
72
|
+
return this.enabled;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Set minimum log level */
|
|
76
|
+
setMinLevel(level: LogLevel): void {
|
|
77
|
+
this.minLevel = level;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Enable logging to stderr (for --verbose mode) */
|
|
81
|
+
setLogToStderr(enabled: boolean): void {
|
|
82
|
+
this.logToStderr = enabled;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Set whether to write logs to file (true) or console (false) */
|
|
86
|
+
setLogToFile(enabled: boolean): void {
|
|
87
|
+
this.logToFile = enabled;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Set whether to use timestamped log files */
|
|
91
|
+
setUseTimestampedFile(enabled: boolean): void {
|
|
92
|
+
this.useTimestampedFile = enabled;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Set custom log directory */
|
|
96
|
+
setLogDirectory(dir: string): void {
|
|
97
|
+
this.customLogDir = dir || null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Get the log file path */
|
|
101
|
+
getLogPath(): string {
|
|
102
|
+
return this.logPath;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Get the log directory */
|
|
106
|
+
getLogDirectory(): string {
|
|
107
|
+
return this.customLogDir || getDefaultLogDirectory();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Initialize the log file (creates directory and sets up log file) */
|
|
111
|
+
private initialize(): void {
|
|
112
|
+
if (this.initialized) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const logDir = this.getLogDirectory();
|
|
117
|
+
|
|
118
|
+
ensureDirectory(logDir);
|
|
119
|
+
|
|
120
|
+
// Set up log file path
|
|
121
|
+
if (this.useTimestampedFile) {
|
|
122
|
+
// Use timestamped filename
|
|
123
|
+
this.logPath = join(logDir, formatLogTimestamp());
|
|
124
|
+
} else {
|
|
125
|
+
// Use fixed filename, clear on startup
|
|
126
|
+
this.logPath = join(logDir, 'emoemu.log');
|
|
127
|
+
// Clear the log file by writing empty content
|
|
128
|
+
try {
|
|
129
|
+
writeFileSync(this.logPath, '');
|
|
130
|
+
} catch {
|
|
131
|
+
// Ignore errors
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Check if rotation is needed (only for non-timestamped files)
|
|
136
|
+
if (!this.useTimestampedFile) {
|
|
137
|
+
this.rotateIfNeeded();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
this.initialized = true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Rotate log file if it exceeds max size */
|
|
144
|
+
private rotateIfNeeded(): void {
|
|
145
|
+
rotateLogFile(this.logPath, MAX_LOG_SIZE_BYTES, MAX_BACKUP_FILES);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Write a log entry in RetroArch format */
|
|
149
|
+
private write(level: LogLevel, message: string, category?: string): void {
|
|
150
|
+
if (!this.enabled) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (LOG_LEVEL_PRIORITY[level] < LOG_LEVEL_PRIORITY[this.minLevel]) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const levelTag = LOG_LEVEL_TAGS[level];
|
|
159
|
+
const logLine = category
|
|
160
|
+
? `[${levelTag}] [${category}]: ${message}`
|
|
161
|
+
: `[${levelTag}] ${message}`;
|
|
162
|
+
|
|
163
|
+
// Write to stderr if verbose mode is enabled
|
|
164
|
+
if (this.logToStderr) {
|
|
165
|
+
process.stderr.write(logLine + '\n');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (this.logToFile) {
|
|
169
|
+
// Write to file
|
|
170
|
+
this.initialize();
|
|
171
|
+
try {
|
|
172
|
+
appendFileSync(this.logPath, logLine + '\n');
|
|
173
|
+
} catch {
|
|
174
|
+
// Silently ignore write errors
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
// Output to console based on log level
|
|
178
|
+
switch (level) {
|
|
179
|
+
case 'debug':
|
|
180
|
+
console.debug(logLine);
|
|
181
|
+
break;
|
|
182
|
+
case 'info':
|
|
183
|
+
console.info(logLine);
|
|
184
|
+
break;
|
|
185
|
+
case 'warn':
|
|
186
|
+
console.warn(logLine);
|
|
187
|
+
break;
|
|
188
|
+
case 'error':
|
|
189
|
+
console.error(logLine);
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Log a debug message */
|
|
196
|
+
debug(message: string, category?: string): void {
|
|
197
|
+
this.write('debug', message, category);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Log an info message */
|
|
201
|
+
info(message: string, category?: string): void {
|
|
202
|
+
this.write('info', message, category);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Log a warning message */
|
|
206
|
+
warn(message: string, category?: string): void {
|
|
207
|
+
this.write('warn', message, category);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Log an error message */
|
|
211
|
+
error(message: string, category?: string): void {
|
|
212
|
+
this.write('error', message, category);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Singleton logger instance */
|
|
217
|
+
export const logger = new Logger();
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ThumbnailType } from '.';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Characters that must be replaced with underscore in RetroArch thumbnail filenames.
|
|
5
|
+
* These characters are not allowed in playlist labels used as filenames:
|
|
6
|
+
* & * / : < > ? \ | "
|
|
7
|
+
*/
|
|
8
|
+
export const THUMBNAIL_FORBIDDEN_CHARS = /[&*\/:<>?\\|"]/g;
|
|
9
|
+
|
|
10
|
+
/** Directory names for each thumbnail type (RetroArch-compatible) */
|
|
11
|
+
export const THUMBNAIL_TYPE_DIRS: Record<ThumbnailType, string> = {
|
|
12
|
+
boxart: 'Named_Boxarts',
|
|
13
|
+
snap: 'Named_Snaps',
|
|
14
|
+
title: 'Named_Titles',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** All thumbnail types in display priority order (snap > title > boxart) */
|
|
18
|
+
export const THUMBNAIL_TYPES: ThumbnailType[] = ['snap', 'title', 'boxart'];
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform Path Utilities
|
|
3
|
+
*
|
|
4
|
+
* Platform-specific directory resolution following standard conventions:
|
|
5
|
+
* - macOS: ~/Library/Application Support/emoemu
|
|
6
|
+
* - Linux: ~/.config/emoemu (or $XDG_CONFIG_HOME/emoemu)
|
|
7
|
+
* - Windows: %APPDATA%\emoemu
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
import { homedir, platform } from 'os';
|
|
12
|
+
import { pipe, filter, isTruthy } from 'remeda';
|
|
13
|
+
|
|
14
|
+
export * from './consts';
|
|
15
|
+
|
|
16
|
+
import { THUMBNAIL_FORBIDDEN_CHARS, THUMBNAIL_TYPE_DIRS } from './consts';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Expand a path, resolving ~ to the user's home directory.
|
|
20
|
+
* Useful for config values that may contain ~ shorthand.
|
|
21
|
+
*
|
|
22
|
+
* @param path Path that may start with ~
|
|
23
|
+
* @returns Expanded path with ~ replaced by home directory
|
|
24
|
+
*/
|
|
25
|
+
export const expandPath = (path: string): string => {
|
|
26
|
+
if (path.startsWith("~")) {
|
|
27
|
+
return join(homedir(), path.slice(1));
|
|
28
|
+
}
|
|
29
|
+
return path;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get the platform-specific config directory path
|
|
34
|
+
*/
|
|
35
|
+
export const getConfigDirectory = (): string => {
|
|
36
|
+
switch (platform()) {
|
|
37
|
+
case 'darwin':
|
|
38
|
+
return join(homedir(), 'Library', 'Application Support', 'emoemu');
|
|
39
|
+
case 'win32':
|
|
40
|
+
return join(process.env.APPDATA || join(homedir(), 'AppData', 'Roaming'), 'emoemu');
|
|
41
|
+
default:
|
|
42
|
+
// Linux and other Unix-like systems
|
|
43
|
+
return join(process.env.XDG_CONFIG_HOME || join(homedir(), '.config'), 'emoemu');
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get the default config file path for the current platform
|
|
49
|
+
* RetroArch-compatible location: {config_dir}/config/emoemu.cfg
|
|
50
|
+
*/
|
|
51
|
+
export const getDefaultConfigPath = (): string => join(getConfigDirectory(), 'config', 'emoemu.cfg');
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get the default playlists directory path for the current platform
|
|
55
|
+
* RetroArch-compatible location: {config_dir}/playlists
|
|
56
|
+
*/
|
|
57
|
+
export const getDefaultPlaylistsDirectory = (): string => join(getConfigDirectory(), 'playlists');
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get the default save states directory path for the current platform
|
|
61
|
+
* RetroArch-compatible location: {config_dir}/states
|
|
62
|
+
*/
|
|
63
|
+
export const getDefaultSavestatesDirectory = (): string => join(getConfigDirectory(), 'states');
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get the default save files (battery saves) directory path for the current platform
|
|
67
|
+
* RetroArch-compatible location: {config_dir}/saves
|
|
68
|
+
*/
|
|
69
|
+
export const getDefaultSavefilesDirectory = (): string => join(getConfigDirectory(), 'saves');
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get the default thumbnails directory path for the current platform
|
|
73
|
+
* RetroArch-compatible location: {config_dir}/thumbnails
|
|
74
|
+
*/
|
|
75
|
+
export const getDefaultThumbnailsDirectory = (): string => join(getConfigDirectory(), 'thumbnails');
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get the default logs directory path for the current platform
|
|
79
|
+
* Location: {config_dir}/logs
|
|
80
|
+
*/
|
|
81
|
+
export const getDefaultLogsDirectory = (): string => join(getConfigDirectory(), 'logs');
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Sanitize a filename for RetroArch thumbnail compatibility.
|
|
86
|
+
* Replaces forbidden characters with underscores.
|
|
87
|
+
*/
|
|
88
|
+
export const sanitizeThumbnailFilename = (name: string): string =>
|
|
89
|
+
name.replace(THUMBNAIL_FORBIDDEN_CHARS, '_');
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* RetroArch thumbnail types.
|
|
93
|
+
* Each type is stored in a separate subdirectory under the system's thumbnail folder.
|
|
94
|
+
*
|
|
95
|
+
* - boxart: Box art / cover images (Named_Boxarts/)
|
|
96
|
+
* - snap: In-game screenshots (Named_Snaps/)
|
|
97
|
+
* - title: Title screen images (Named_Titles/)
|
|
98
|
+
*/
|
|
99
|
+
export type ThumbnailType = 'boxart' | 'snap' | 'title';
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get the path to a ROM's thumbnail.
|
|
104
|
+
*
|
|
105
|
+
* RetroArch stores thumbnails in: {thumbnails_dir}/{System Name}/{Type}/{Label}.png
|
|
106
|
+
* where Type is one of: Named_Boxarts, Named_Snaps, Named_Titles
|
|
107
|
+
*
|
|
108
|
+
* @param systemName RetroArch system name (e.g., "Nintendo - Nintendo Entertainment System")
|
|
109
|
+
* @param romLabel ROM label/title (will be sanitized)
|
|
110
|
+
* @param type Thumbnail type: 'boxart', 'snap', or 'title' (default: 'snap')
|
|
111
|
+
* @returns Full path to the thumbnail PNG
|
|
112
|
+
*/
|
|
113
|
+
export const getThumbnailPath = (
|
|
114
|
+
systemName: string,
|
|
115
|
+
romLabel: string,
|
|
116
|
+
type: ThumbnailType = 'snap'
|
|
117
|
+
): string => {
|
|
118
|
+
const sanitizedLabel = sanitizeThumbnailFilename(romLabel);
|
|
119
|
+
const typeDir = THUMBNAIL_TYPE_DIRS[type];
|
|
120
|
+
return join(getDefaultThumbnailsDirectory(), systemName, typeDir, `${sanitizedLabel}.png`);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get paths to all thumbnail types for a ROM.
|
|
125
|
+
* Useful for checking if any thumbnail exists or for cleanup.
|
|
126
|
+
*
|
|
127
|
+
* @param systemName RetroArch system name
|
|
128
|
+
* @param romLabel ROM label/title (will be sanitized)
|
|
129
|
+
* @returns Object with paths for each thumbnail type
|
|
130
|
+
*/
|
|
131
|
+
export const getAllThumbnailPaths = (
|
|
132
|
+
systemName: string,
|
|
133
|
+
romLabel: string
|
|
134
|
+
): Record<ThumbnailType, string> => ({
|
|
135
|
+
boxart: getThumbnailPath(systemName, romLabel, 'boxart'),
|
|
136
|
+
snap: getThumbnailPath(systemName, romLabel, 'snap'),
|
|
137
|
+
title: getThumbnailPath(systemName, romLabel, 'title'),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get all possible config file paths in order of precedence (highest first)
|
|
142
|
+
*/
|
|
143
|
+
export const getConfigPaths = (customPath?: string): string[] => pipe(
|
|
144
|
+
[
|
|
145
|
+
customPath, // 1. Custom path from --config flag
|
|
146
|
+
process.env.EMOEMU_CONFIG, // 2. Environment variable
|
|
147
|
+
join(process.cwd(), 'emoemu.cfg'), // 3. Current working directory
|
|
148
|
+
getDefaultConfigPath(), // 4. Platform-specific default
|
|
149
|
+
],
|
|
150
|
+
filter(isTruthy)
|
|
151
|
+
);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** PNG chunk header size: length (4) + type (4) */
|
|
2
|
+
export const PNG_CHUNK_HEADER_SIZE = 8;
|
|
3
|
+
|
|
4
|
+
/** PNG chunk footer size: CRC32 (4) */
|
|
5
|
+
export const PNG_CHUNK_FOOTER_SIZE = 4;
|
|
6
|
+
|
|
7
|
+
/** PNG chunk overhead: header + footer */
|
|
8
|
+
export const PNG_CHUNK_OVERHEAD = 12;
|
|
9
|
+
|
|
10
|
+
/** Offset for chunk type in PNG chunk buffer */
|
|
11
|
+
export const PNG_CHUNK_TYPE_OFFSET = 4;
|
|
12
|
+
|
|
13
|
+
/** Offset for chunk data in PNG chunk buffer */
|
|
14
|
+
export const PNG_CHUNK_DATA_OFFSET = 8;
|
|
15
|
+
|
|
16
|
+
/** Size of chunk type field */
|
|
17
|
+
export const PNG_CHUNK_TYPE_SIZE = 4;
|
|
18
|
+
|
|
19
|
+
/** Maximum colors in indexed PNG palette */
|
|
20
|
+
export const PNG_MAX_PALETTE_COLORS = 256;
|
|
21
|
+
|
|
22
|
+
/** Size of RGB triplet in palette */
|
|
23
|
+
export const RGB_TRIPLET_SIZE = 3;
|
|
24
|
+
|
|
25
|
+
/** Maximum palette buffer size (256 colors * 3 bytes) */
|
|
26
|
+
export const PNG_PALETTE_BUFFER_SIZE = 768;
|
|
27
|
+
|
|
28
|
+
// Bit manipulation constants for byte operations
|
|
29
|
+
|
|
30
|
+
/** Bit shift for second byte (green channel in RGB24) */
|
|
31
|
+
export const BYTE_SHIFT_1 = 8;
|
|
32
|
+
|
|
33
|
+
/** Bit shift for third byte (red channel in RGB24) */
|
|
34
|
+
export const BYTE_SHIFT_2 = 16;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PNG Encoding Utilities
|
|
3
|
+
*
|
|
4
|
+
* Shared utilities for PNG chunk creation and color indexing,
|
|
5
|
+
* used by both the main Kitty renderer and the worker thread.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
PNG_CHUNK_OVERHEAD,
|
|
10
|
+
PNG_CHUNK_TYPE_OFFSET,
|
|
11
|
+
PNG_CHUNK_DATA_OFFSET,
|
|
12
|
+
PNG_CHUNK_TYPE_SIZE,
|
|
13
|
+
PNG_MAX_PALETTE_COLORS,
|
|
14
|
+
RGB_TRIPLET_SIZE,
|
|
15
|
+
PNG_PALETTE_BUFFER_SIZE,
|
|
16
|
+
BYTE_SHIFT_1,
|
|
17
|
+
BYTE_SHIFT_2,
|
|
18
|
+
} from './consts';
|
|
19
|
+
|
|
20
|
+
export * from './consts';
|
|
21
|
+
|
|
22
|
+
import { crc32 } from '../crc32';
|
|
23
|
+
|
|
24
|
+
// Re-export crc32 for backwards compatibility
|
|
25
|
+
export { crc32 };
|
|
26
|
+
|
|
27
|
+
/* eslint-disable @typescript-eslint/no-magic-numbers */
|
|
28
|
+
// PNG file signature (magic bytes)
|
|
29
|
+
export const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Create a PNG chunk with the given type and data.
|
|
33
|
+
* PNG chunks have the format: length (4) + type (4) + data + crc32 (4)
|
|
34
|
+
*/
|
|
35
|
+
export const createPngChunk = (type: string, data: Buffer): Buffer => {
|
|
36
|
+
const chunk = Buffer.alloc(PNG_CHUNK_OVERHEAD + data.length);
|
|
37
|
+
chunk.writeUInt32BE(data.length, 0);
|
|
38
|
+
chunk.write(type, PNG_CHUNK_TYPE_OFFSET, PNG_CHUNK_TYPE_SIZE, 'ascii');
|
|
39
|
+
data.copy(chunk, PNG_CHUNK_DATA_OFFSET);
|
|
40
|
+
const crcData = Buffer.alloc(PNG_CHUNK_TYPE_SIZE + data.length);
|
|
41
|
+
crcData.write(type, 0, PNG_CHUNK_TYPE_SIZE, 'ascii');
|
|
42
|
+
data.copy(crcData, PNG_CHUNK_TYPE_SIZE);
|
|
43
|
+
chunk.writeUInt32BE(crc32(crcData), PNG_CHUNK_DATA_OFFSET + data.length);
|
|
44
|
+
return chunk;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Result of RGB to indexed color conversion
|
|
49
|
+
*/
|
|
50
|
+
export interface IndexedResult {
|
|
51
|
+
indices: Uint8Array;
|
|
52
|
+
palette: Uint8Array; // RGB triplets
|
|
53
|
+
colorCount: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Internal buffers for worker-style buffer management
|
|
57
|
+
let internalIndexedBuffer: Uint8Array | null = null;
|
|
58
|
+
let internalPaletteBuffer: Uint8Array | null = null;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Convert RGB buffer to indexed format (max 256 colors).
|
|
62
|
+
* Returns null if more than 256 unique colors.
|
|
63
|
+
*
|
|
64
|
+
* @param rgb RGB pixel data (3 bytes per pixel)
|
|
65
|
+
* @param width Image width
|
|
66
|
+
* @param height Image height
|
|
67
|
+
* @param indexedBuffer Optional pre-allocated buffer for indices (will allocate if not provided)
|
|
68
|
+
* @param paletteBuffer Optional pre-allocated buffer for palette (will allocate if not provided)
|
|
69
|
+
*/
|
|
70
|
+
export const rgbToIndexed = (
|
|
71
|
+
rgb: Uint8Array,
|
|
72
|
+
width: number,
|
|
73
|
+
height: number,
|
|
74
|
+
indexedBuffer?: Uint8Array,
|
|
75
|
+
paletteBuffer?: Uint8Array
|
|
76
|
+
): IndexedResult | null => {
|
|
77
|
+
const pixelCount = width * height;
|
|
78
|
+
|
|
79
|
+
// Use provided buffers or manage internal ones
|
|
80
|
+
let indices: Uint8Array;
|
|
81
|
+
let palette: Uint8Array;
|
|
82
|
+
|
|
83
|
+
if (indexedBuffer && paletteBuffer) {
|
|
84
|
+
indices = indexedBuffer;
|
|
85
|
+
palette = paletteBuffer;
|
|
86
|
+
} else {
|
|
87
|
+
// Reuse internal buffers (worker-style)
|
|
88
|
+
if (!internalIndexedBuffer || internalIndexedBuffer.length < pixelCount) {
|
|
89
|
+
internalIndexedBuffer = new Uint8Array(pixelCount);
|
|
90
|
+
}
|
|
91
|
+
if (!internalPaletteBuffer || internalPaletteBuffer.length < PNG_PALETTE_BUFFER_SIZE) {
|
|
92
|
+
internalPaletteBuffer = new Uint8Array(PNG_PALETTE_BUFFER_SIZE);
|
|
93
|
+
}
|
|
94
|
+
indices = internalIndexedBuffer;
|
|
95
|
+
palette = internalPaletteBuffer;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Map RGB color (as 24-bit int) to palette index
|
|
99
|
+
const colorMap = new Map<number, number>();
|
|
100
|
+
let colorCount = 0;
|
|
101
|
+
|
|
102
|
+
for (let i = 0; i < pixelCount; i++) {
|
|
103
|
+
const rgbIdx = i * RGB_TRIPLET_SIZE;
|
|
104
|
+
const r = rgb[rgbIdx];
|
|
105
|
+
const g = rgb[rgbIdx + 1];
|
|
106
|
+
const b = rgb[rgbIdx + 2];
|
|
107
|
+
const colorKey = (r << BYTE_SHIFT_2) | (g << BYTE_SHIFT_1) | b;
|
|
108
|
+
|
|
109
|
+
let paletteIdx = colorMap.get(colorKey);
|
|
110
|
+
if (paletteIdx === undefined) {
|
|
111
|
+
if (colorCount >= PNG_MAX_PALETTE_COLORS) {
|
|
112
|
+
// Too many colors, fall back to RGB
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
paletteIdx = colorCount;
|
|
116
|
+
colorMap.set(colorKey, paletteIdx);
|
|
117
|
+
palette[colorCount * RGB_TRIPLET_SIZE] = r;
|
|
118
|
+
palette[colorCount * RGB_TRIPLET_SIZE + 1] = g;
|
|
119
|
+
palette[colorCount * RGB_TRIPLET_SIZE + 2] = b;
|
|
120
|
+
colorCount++;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
indices[i] = paletteIdx;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
indices,
|
|
128
|
+
palette: palette.subarray(0, colorCount * RGB_TRIPLET_SIZE),
|
|
129
|
+
colorCount,
|
|
130
|
+
};
|
|
131
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read and parse a JSON file, returning null if it doesn't exist or fails to parse.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, readFileSync } from 'fs';
|
|
6
|
+
|
|
7
|
+
export const readJsonFile = (path: string): unknown => {
|
|
8
|
+
if (!existsSync(path)) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(readFileSync(path, 'utf-8')) as unknown;
|
|
13
|
+
} catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Log file rotation utility.
|
|
3
|
+
*
|
|
4
|
+
* Rotates a log file when it exceeds a maximum size, keeping a
|
|
5
|
+
* configurable number of backup files (e.g., log.1, log.2, log.3).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, statSync, renameSync, unlinkSync } from 'fs';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Rotate a log file if it exceeds maxSizeBytes.
|
|
12
|
+
* Keeps up to maxBackups numbered backups (log.1, log.2, ...).
|
|
13
|
+
*/
|
|
14
|
+
export const rotateLogFile = (logPath: string, maxSizeBytes: number, maxBackups: number): void => {
|
|
15
|
+
if (!existsSync(logPath)) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const stats = statSync(logPath);
|
|
21
|
+
|
|
22
|
+
if (stats.size < maxSizeBytes) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Rotate existing backups
|
|
27
|
+
for (let i = maxBackups - 1; i >= 1; i--) {
|
|
28
|
+
const oldPath = `${logPath}.${i}`;
|
|
29
|
+
const newPath = `${logPath}.${i + 1}`;
|
|
30
|
+
if (existsSync(oldPath)) {
|
|
31
|
+
if (i === maxBackups - 1) {
|
|
32
|
+
unlinkSync(oldPath);
|
|
33
|
+
} else {
|
|
34
|
+
renameSync(oldPath, newPath);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Move current log to .1
|
|
40
|
+
renameSync(logPath, `${logPath}.1`);
|
|
41
|
+
} catch {
|
|
42
|
+
// Ignore rotation errors
|
|
43
|
+
}
|
|
44
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Default terminal width in columns */
|
|
2
|
+
export const DEFAULT_TERMINAL_WIDTH = 80;
|
|
3
|
+
|
|
4
|
+
/** Default terminal height in rows */
|
|
5
|
+
export const DEFAULT_TERMINAL_HEIGHT = 24;
|
|
6
|
+
|
|
7
|
+
/** Delay in milliseconds for stdin release after Ink cleanup */
|
|
8
|
+
export const INK_CLEANUP_DELAY_MS = 100;
|