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,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core Selector UI Component
|
|
3
|
+
*
|
|
4
|
+
* A dialog for selecting which emulator core to use when multiple cores
|
|
5
|
+
* support the same ROM format.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useState, useEffect } from 'react';
|
|
9
|
+
import { Box, Text, useInput, useApp, useStdout } from 'ink';
|
|
10
|
+
import type { CoreFactory } from '../../frontend/coreRegistry';
|
|
11
|
+
import { useGamepad } from '../hooks/useGamepad';
|
|
12
|
+
import { cleanupInkInstance } from '../../utils/terminal';
|
|
13
|
+
import { renderDialog, type DialogRenderOptions } from '../NativeDialog';
|
|
14
|
+
import {
|
|
15
|
+
DEFAULT_TERM_WIDTH,
|
|
16
|
+
DEFAULT_TERM_HEIGHT,
|
|
17
|
+
DIALOG_BOX_PADDING,
|
|
18
|
+
} from '..';
|
|
19
|
+
import { CORE_SELECTOR_MIN_WIDTH } from './consts';
|
|
20
|
+
|
|
21
|
+
export * from './consts';
|
|
22
|
+
|
|
23
|
+
interface CoreOption {
|
|
24
|
+
id: string;
|
|
25
|
+
factory: CoreFactory;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface CoreSelection {
|
|
29
|
+
id: string;
|
|
30
|
+
factory: CoreFactory;
|
|
31
|
+
remember: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface CoreSelectorProps {
|
|
35
|
+
cores: CoreOption[];
|
|
36
|
+
romName: string;
|
|
37
|
+
onSelect: (selection: CoreSelection) => void;
|
|
38
|
+
onCancel: () => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const CoreSelector = ({ cores, romName, onSelect, onCancel }: CoreSelectorProps) => {
|
|
42
|
+
const { exit } = useApp();
|
|
43
|
+
const { stdout } = useStdout();
|
|
44
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
45
|
+
const [rememberSelection, setRememberSelection] = useState(false);
|
|
46
|
+
|
|
47
|
+
// Force re-render on mount to trigger Ink's terminal setup before user interaction
|
|
48
|
+
const [, setMounted] = useState(false);
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
setMounted(true);
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
53
|
+
useInput((input, key) => {
|
|
54
|
+
if (key.escape) {
|
|
55
|
+
onCancel();
|
|
56
|
+
exit();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (key.upArrow) {
|
|
61
|
+
setSelectedIndex(prev => Math.max(0, prev - 1));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (key.downArrow) {
|
|
66
|
+
setSelectedIndex(prev => Math.min(cores.length - 1, prev + 1));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Tab or 'r' to toggle remember checkbox
|
|
71
|
+
if (key.tab || input === 'r') {
|
|
72
|
+
setRememberSelection(prev => !prev);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (key.return) {
|
|
77
|
+
const selected = cores[selectedIndex];
|
|
78
|
+
onSelect({ id: selected.id, factory: selected.factory, remember: rememberSelection });
|
|
79
|
+
exit();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Number keys for quick selection
|
|
84
|
+
const num = parseInt(input, 10);
|
|
85
|
+
if (num >= 1 && num <= cores.length) {
|
|
86
|
+
const selected = cores[num - 1];
|
|
87
|
+
onSelect({ id: selected.id, factory: selected.factory, remember: rememberSelection });
|
|
88
|
+
exit();
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Handle gamepad input
|
|
93
|
+
useGamepad({
|
|
94
|
+
onUp: () => setSelectedIndex(prev => Math.max(0, prev - 1)),
|
|
95
|
+
onDown: () => setSelectedIndex(prev => Math.min(cores.length - 1, prev + 1)),
|
|
96
|
+
onConfirm: () => {
|
|
97
|
+
const selected = cores[selectedIndex];
|
|
98
|
+
onSelect({ id: selected.id, factory: selected.factory, remember: rememberSelection });
|
|
99
|
+
exit();
|
|
100
|
+
},
|
|
101
|
+
onCancel: () => {
|
|
102
|
+
onCancel();
|
|
103
|
+
exit();
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Get terminal dimensions for full-screen layout - use Ink's stdout for native mode compatibility
|
|
108
|
+
const termWidth = stdout.columns || DEFAULT_TERM_WIDTH;
|
|
109
|
+
const termHeight = stdout.rows || DEFAULT_TERM_HEIGHT;
|
|
110
|
+
const boxWidth = Math.min(CORE_SELECTOR_MIN_WIDTH, termWidth - DIALOG_BOX_PADDING);
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<Box
|
|
114
|
+
flexDirection="column"
|
|
115
|
+
width={termWidth}
|
|
116
|
+
height={termHeight}
|
|
117
|
+
alignItems="center"
|
|
118
|
+
justifyContent="center"
|
|
119
|
+
>
|
|
120
|
+
{/* Header */}
|
|
121
|
+
<Box
|
|
122
|
+
flexDirection="column"
|
|
123
|
+
borderStyle="round"
|
|
124
|
+
borderColor="cyan"
|
|
125
|
+
paddingX={2}
|
|
126
|
+
paddingY={1}
|
|
127
|
+
width={boxWidth}
|
|
128
|
+
>
|
|
129
|
+
<Box justifyContent="center" marginBottom={1}>
|
|
130
|
+
<Text bold color="cyan">{'\u{1F3AE}'} Select Emulator Core</Text>
|
|
131
|
+
</Box>
|
|
132
|
+
|
|
133
|
+
<Box>
|
|
134
|
+
<Text color="gray">Multiple cores can play </Text>
|
|
135
|
+
<Text color="white" bold>{romName}</Text>
|
|
136
|
+
</Box>
|
|
137
|
+
</Box>
|
|
138
|
+
|
|
139
|
+
{/* Core Options */}
|
|
140
|
+
<Box
|
|
141
|
+
flexDirection="column"
|
|
142
|
+
borderStyle="round"
|
|
143
|
+
borderColor="gray"
|
|
144
|
+
paddingX={2}
|
|
145
|
+
paddingY={1}
|
|
146
|
+
marginTop={1}
|
|
147
|
+
width={boxWidth}
|
|
148
|
+
>
|
|
149
|
+
{cores.map((core, index) => {
|
|
150
|
+
const info = core.factory.getSystemInfo();
|
|
151
|
+
const isSelected = index === selectedIndex;
|
|
152
|
+
const corePath = core.factory.path ?? 'native';
|
|
153
|
+
const isNative = corePath === 'native';
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<Box key={core.id} marginBottom={index < cores.length - 1 ? 1 : 0}>
|
|
157
|
+
<Text
|
|
158
|
+
color={isSelected ? 'green' : 'gray'}
|
|
159
|
+
bold={isSelected}
|
|
160
|
+
>
|
|
161
|
+
{isSelected ? '\u25B6 ' : ' '}
|
|
162
|
+
{index + 1}. {info.name}
|
|
163
|
+
</Text>
|
|
164
|
+
{isNative && (
|
|
165
|
+
<Text color="green"> [native]</Text>
|
|
166
|
+
)}
|
|
167
|
+
{!isNative && (
|
|
168
|
+
<Text color="gray" dimColor> (libretro)</Text>
|
|
169
|
+
)}
|
|
170
|
+
</Box>
|
|
171
|
+
);
|
|
172
|
+
})}
|
|
173
|
+
</Box>
|
|
174
|
+
|
|
175
|
+
{/* Remember Selection */}
|
|
176
|
+
<Box
|
|
177
|
+
flexDirection="column"
|
|
178
|
+
borderStyle="round"
|
|
179
|
+
borderColor="gray"
|
|
180
|
+
paddingX={2}
|
|
181
|
+
paddingY={1}
|
|
182
|
+
marginTop={1}
|
|
183
|
+
width={boxWidth}
|
|
184
|
+
>
|
|
185
|
+
<Box>
|
|
186
|
+
<Text
|
|
187
|
+
color={rememberSelection ? 'green' : 'gray'}
|
|
188
|
+
>
|
|
189
|
+
{rememberSelection ? '\u2611' : '\u2610'} Remember this selection
|
|
190
|
+
</Text>
|
|
191
|
+
<Text color="gray" dimColor> (Tab to toggle)</Text>
|
|
192
|
+
</Box>
|
|
193
|
+
</Box>
|
|
194
|
+
|
|
195
|
+
{/* Footer */}
|
|
196
|
+
<Box marginTop={1}>
|
|
197
|
+
<Text color="gray" dimColor>
|
|
198
|
+
{'\u2191\u2193'} Navigate {'\u23CE'} Select 1-{cores.length} Quick select ESC Cancel
|
|
199
|
+
</Text>
|
|
200
|
+
</Box>
|
|
201
|
+
</Box>
|
|
202
|
+
);
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Launch the core selector dialog
|
|
207
|
+
*
|
|
208
|
+
* @param cores Array of matching cores
|
|
209
|
+
* @param romName Name of the ROM file for display
|
|
210
|
+
* @param options Optional dialog render options (e.g., nativeMode)
|
|
211
|
+
* @returns Promise that resolves to the CoreSelection, or null if cancelled
|
|
212
|
+
*/
|
|
213
|
+
export const selectCore = async (
|
|
214
|
+
cores: CoreOption[],
|
|
215
|
+
romName: string,
|
|
216
|
+
options: DialogRenderOptions = {}
|
|
217
|
+
): Promise<CoreSelection | null> => new Promise((resolve) => {
|
|
218
|
+
let selection: CoreSelection | null = null;
|
|
219
|
+
|
|
220
|
+
const handleSelect = (sel: CoreSelection) => {
|
|
221
|
+
selection = sel;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const handleCancel = () => {
|
|
225
|
+
selection = null;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
// Enter alternate screen buffer and clear (only in terminal mode)
|
|
229
|
+
if (!options.nativeMode) {
|
|
230
|
+
process.stdout.write('\x1b[?1049h\x1b[2J\x1b[H');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
void renderDialog(
|
|
234
|
+
<CoreSelector
|
|
235
|
+
cores={cores}
|
|
236
|
+
romName={romName}
|
|
237
|
+
onSelect={handleSelect}
|
|
238
|
+
onCancel={handleCancel}
|
|
239
|
+
/>,
|
|
240
|
+
{ ...options, title: options.title ?? 'emoemu - Select Core' }
|
|
241
|
+
).then(({ instance, cleanup }) => {
|
|
242
|
+
void instance.waitUntilExit().then(() => {
|
|
243
|
+
cleanup();
|
|
244
|
+
cleanupInkInstance(instance, resolve, selection, {
|
|
245
|
+
exitAlternateScreen: !options.nativeMode,
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
export default CoreSelector;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Box, useStdout } from 'ink';
|
|
2
|
+
import { useClearTerminal } from '../hooks/useClearTerminal';
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_TERM_WIDTH,
|
|
5
|
+
DEFAULT_TERM_HEIGHT,
|
|
6
|
+
DIALOG_BOX_PADDING,
|
|
7
|
+
DIALOG_BOX_MIN_WIDTH,
|
|
8
|
+
} from '..';
|
|
9
|
+
|
|
10
|
+
interface DialogContainerProps {
|
|
11
|
+
minWidth?: number;
|
|
12
|
+
children: (boxWidth: number) => React.ReactNode;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Shared container for dialogs that handles terminal dimension detection,
|
|
17
|
+
* screen clearing, ready state, and centering layout.
|
|
18
|
+
*/
|
|
19
|
+
export const DialogContainer = ({ minWidth = DIALOG_BOX_MIN_WIDTH, children }: DialogContainerProps): React.JSX.Element | null => {
|
|
20
|
+
const { stdout } = useStdout();
|
|
21
|
+
const ready = useClearTerminal();
|
|
22
|
+
|
|
23
|
+
const termWidth = stdout.columns || DEFAULT_TERM_WIDTH;
|
|
24
|
+
const termHeight = stdout.rows || DEFAULT_TERM_HEIGHT;
|
|
25
|
+
const boxWidth = Math.min(minWidth, termWidth - DIALOG_BOX_PADDING);
|
|
26
|
+
|
|
27
|
+
if (!ready) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<Box
|
|
33
|
+
flexDirection="column"
|
|
34
|
+
width={termWidth}
|
|
35
|
+
height={termHeight}
|
|
36
|
+
alignItems="center"
|
|
37
|
+
justifyContent="center"
|
|
38
|
+
>
|
|
39
|
+
{children(boxWidth)}
|
|
40
|
+
</Box>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Box, Text } from 'ink';
|
|
2
|
+
|
|
3
|
+
interface DialogOption {
|
|
4
|
+
label: string;
|
|
5
|
+
choice: string;
|
|
6
|
+
color: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface DialogOptionsListProps {
|
|
10
|
+
options: DialogOption[];
|
|
11
|
+
selectedIndex: number;
|
|
12
|
+
boxWidth: number;
|
|
13
|
+
/** Label shown after ESC in the footer (default: "Cancel") */
|
|
14
|
+
escLabel?: string;
|
|
15
|
+
/** Prompt shown above options, or false to hide (default: "What would you like to do?") */
|
|
16
|
+
prompt?: string | false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const DialogOptionsList = ({
|
|
20
|
+
options,
|
|
21
|
+
selectedIndex,
|
|
22
|
+
boxWidth,
|
|
23
|
+
escLabel = 'Cancel',
|
|
24
|
+
prompt = 'What would you like to do?',
|
|
25
|
+
}: DialogOptionsListProps): React.JSX.Element => (
|
|
26
|
+
<>
|
|
27
|
+
<Box
|
|
28
|
+
flexDirection="column"
|
|
29
|
+
borderStyle="round"
|
|
30
|
+
borderColor="gray"
|
|
31
|
+
paddingX={2}
|
|
32
|
+
paddingY={1}
|
|
33
|
+
marginTop={1}
|
|
34
|
+
width={boxWidth}
|
|
35
|
+
>
|
|
36
|
+
{prompt !== false && (
|
|
37
|
+
<Box marginBottom={1}>
|
|
38
|
+
<Text bold>{prompt}</Text>
|
|
39
|
+
</Box>
|
|
40
|
+
)}
|
|
41
|
+
|
|
42
|
+
{options.map((option, index) => (
|
|
43
|
+
<Box key={option.choice}>
|
|
44
|
+
<Text
|
|
45
|
+
color={selectedIndex === index ? option.color : 'gray'}
|
|
46
|
+
bold={selectedIndex === index}
|
|
47
|
+
>
|
|
48
|
+
{selectedIndex === index ? '\u25B6 ' : ' '}
|
|
49
|
+
{index + 1}. {option.label}
|
|
50
|
+
</Text>
|
|
51
|
+
</Box>
|
|
52
|
+
))}
|
|
53
|
+
</Box>
|
|
54
|
+
|
|
55
|
+
<Box marginTop={1}>
|
|
56
|
+
<Text color="gray" dimColor>
|
|
57
|
+
{'\u2191\u2193'} Navigate {'\u23CE'} Select ESC {escLabel}
|
|
58
|
+
</Text>
|
|
59
|
+
</Box>
|
|
60
|
+
</>
|
|
61
|
+
);
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Duplicate CRC Prompt Component
|
|
3
|
+
*
|
|
4
|
+
* Shows when importing a ROM that has the same CRC32 as an existing entry
|
|
5
|
+
* in the library, where both files exist. Prompts user to either update
|
|
6
|
+
* the path or skip the import.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Box, Text } from 'ink';
|
|
10
|
+
import { DialogOptionsList } from '../DialogOptionsList';
|
|
11
|
+
import { DialogContainer } from '../DialogContainer';
|
|
12
|
+
import { useDialogNavigation } from '../hooks/useDialogNavigation';
|
|
13
|
+
import { launchDialog, type DialogRenderOptions } from '../NativeDialog';
|
|
14
|
+
import { SEPARATOR_LINE_PADDING } from '..';
|
|
15
|
+
import {
|
|
16
|
+
DIALOG_BOX_MIN_WIDTH,
|
|
17
|
+
PATH_LABEL_WIDTH,
|
|
18
|
+
} from './consts';
|
|
19
|
+
|
|
20
|
+
export type DuplicateCrcChoice = 'update' | 'skip';
|
|
21
|
+
|
|
22
|
+
export interface DuplicateCrcInfo {
|
|
23
|
+
/** The new ROM path being imported */
|
|
24
|
+
newPath: string;
|
|
25
|
+
/** The existing ROM path in the library */
|
|
26
|
+
existingPath: string;
|
|
27
|
+
/** The game label/title */
|
|
28
|
+
label: string;
|
|
29
|
+
/** The shared CRC32 value */
|
|
30
|
+
crc32: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface DuplicateCrcDialogProps {
|
|
34
|
+
info: DuplicateCrcInfo;
|
|
35
|
+
onChoice: (choice: DuplicateCrcChoice) => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Truncate a path to fit within max length, showing ... in the middle */
|
|
39
|
+
const truncatePath = (path: string, maxLength: number): string => {
|
|
40
|
+
if (path.length <= maxLength) {
|
|
41
|
+
return path;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const ellipsis = '...';
|
|
45
|
+
const availableLength = maxLength - ellipsis.length;
|
|
46
|
+
const startLength = Math.ceil(availableLength / 2);
|
|
47
|
+
const endLength = Math.floor(availableLength / 2);
|
|
48
|
+
|
|
49
|
+
return path.slice(0, startLength) + ellipsis + path.slice(-endLength);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const DuplicateCrcDialog = ({ info, onChoice }: DuplicateCrcDialogProps) => {
|
|
53
|
+
const options: { label: string; choice: DuplicateCrcChoice; color: string }[] = [
|
|
54
|
+
{ label: 'Update path to new location', choice: 'update', color: 'green' },
|
|
55
|
+
{ label: 'Skip (keep existing)', choice: 'skip', color: 'gray' },
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const { selectedIndex } = useDialogNavigation({
|
|
59
|
+
itemCount: options.length,
|
|
60
|
+
onSelect: (index) => onChoice(options[index].choice),
|
|
61
|
+
onCancel: () => onChoice('skip'),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<DialogContainer minWidth={DIALOG_BOX_MIN_WIDTH}>
|
|
66
|
+
{(boxWidth) => {
|
|
67
|
+
// Calculate max path length for truncation (accounting for label and padding)
|
|
68
|
+
const pathMaxLength = boxWidth - SEPARATOR_LINE_PADDING - PATH_LABEL_WIDTH;
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<>
|
|
72
|
+
{/* Header */}
|
|
73
|
+
<Box
|
|
74
|
+
flexDirection="column"
|
|
75
|
+
borderStyle="round"
|
|
76
|
+
borderColor="yellow"
|
|
77
|
+
paddingX={2}
|
|
78
|
+
paddingY={1}
|
|
79
|
+
width={boxWidth}
|
|
80
|
+
>
|
|
81
|
+
<Box justifyContent="center" marginBottom={1}>
|
|
82
|
+
<Text bold color="yellow">{'\u26A0'} Duplicate ROM Detected</Text>
|
|
83
|
+
</Box>
|
|
84
|
+
|
|
85
|
+
{/* Explanation */}
|
|
86
|
+
<Box marginBottom={1}>
|
|
87
|
+
<Text color="white">
|
|
88
|
+
A ROM with the same checksum already exists in your library.
|
|
89
|
+
</Text>
|
|
90
|
+
</Box>
|
|
91
|
+
|
|
92
|
+
{/* Separator */}
|
|
93
|
+
<Text color="gray" dimColor>{'─'.repeat(boxWidth - SEPARATOR_LINE_PADDING)}</Text>
|
|
94
|
+
|
|
95
|
+
{/* Game info */}
|
|
96
|
+
<Box flexDirection="column" marginY={1}>
|
|
97
|
+
<Box>
|
|
98
|
+
<Text color="gray">{'Game: '}</Text>
|
|
99
|
+
<Text color="white" bold>{info.label}</Text>
|
|
100
|
+
</Box>
|
|
101
|
+
<Box>
|
|
102
|
+
<Text color="gray">{'CRC32: '}</Text>
|
|
103
|
+
<Text color="cyan">{info.crc32}</Text>
|
|
104
|
+
</Box>
|
|
105
|
+
</Box>
|
|
106
|
+
|
|
107
|
+
{/* Separator */}
|
|
108
|
+
<Text color="gray" dimColor>{'─'.repeat(boxWidth - SEPARATOR_LINE_PADDING)}</Text>
|
|
109
|
+
|
|
110
|
+
{/* Paths */}
|
|
111
|
+
<Box flexDirection="column" marginY={1}>
|
|
112
|
+
<Box>
|
|
113
|
+
<Text color="gray">{'Existing: '}</Text>
|
|
114
|
+
<Text color="white">{truncatePath(info.existingPath, pathMaxLength)}</Text>
|
|
115
|
+
</Box>
|
|
116
|
+
<Box>
|
|
117
|
+
<Text color="gray">{'New: '}</Text>
|
|
118
|
+
<Text color="green">{truncatePath(info.newPath, pathMaxLength)}</Text>
|
|
119
|
+
</Box>
|
|
120
|
+
</Box>
|
|
121
|
+
</Box>
|
|
122
|
+
|
|
123
|
+
<DialogOptionsList options={options} selectedIndex={selectedIndex} boxWidth={boxWidth} escLabel="Skip" />
|
|
124
|
+
</>
|
|
125
|
+
);
|
|
126
|
+
}}
|
|
127
|
+
</DialogContainer>
|
|
128
|
+
);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Show the duplicate CRC dialog and get user's choice
|
|
133
|
+
*/
|
|
134
|
+
export const showDuplicateCrcPrompt = (
|
|
135
|
+
info: DuplicateCrcInfo,
|
|
136
|
+
options: DialogRenderOptions = {}
|
|
137
|
+
): Promise<DuplicateCrcChoice> => launchDialog<DuplicateCrcChoice>(
|
|
138
|
+
(onChoice) => <DuplicateCrcDialog info={info} onChoice={onChoice} />,
|
|
139
|
+
'skip',
|
|
140
|
+
{ ...options, title: options.title ?? 'emoemu - Duplicate ROM' },
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
export default DuplicateCrcDialog;
|
|
144
|
+
|
|
145
|
+
// Re-export constants
|
|
146
|
+
export * from './consts';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Delay before repeating starts (ms) */
|
|
2
|
+
export const INITIAL_DELAY_MS = 400;
|
|
3
|
+
|
|
4
|
+
/** Starting repeat interval (ms) */
|
|
5
|
+
export const INITIAL_REPEAT_MS = 200;
|
|
6
|
+
|
|
7
|
+
/** Fastest repeat interval (ms) */
|
|
8
|
+
export const MIN_REPEAT_MS = 40;
|
|
9
|
+
|
|
10
|
+
/** Time to reach max speed (ms) */
|
|
11
|
+
export const ACCELERATION_TIME_MS = 1500;
|
|
12
|
+
|
|
13
|
+
/** Ease-in-out cubic smoothing factors */
|
|
14
|
+
export const EASE_CUBIC_FACTOR = 3;
|
|
15
|
+
export const EASE_CUBIC_DIVISOR = 2;
|