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,779 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core Manager Component
|
|
3
|
+
*
|
|
4
|
+
* Allows users to view, install, and delete libretro cores.
|
|
5
|
+
* Two tabs: Installed Cores and Download Cores.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useState, useEffect, useCallback, useMemo } from 'react';
|
|
9
|
+
import { Box, Text, useInput, useApp } from 'ink';
|
|
10
|
+
import { Spinner, ProgressBar } from '@inkjs/ui';
|
|
11
|
+
import { unlink } from 'fs/promises';
|
|
12
|
+
import { listCores, unregisterCore } from '../../frontend/coreRegistry';
|
|
13
|
+
import {
|
|
14
|
+
registerLibretroCore,
|
|
15
|
+
unloadLibretroCore,
|
|
16
|
+
isInUserCoresDirectory,
|
|
17
|
+
} from '../../cores/libretro';
|
|
18
|
+
import { getSystemName } from '../../frontend/playlist';
|
|
19
|
+
import {
|
|
20
|
+
fetchAvailableCores,
|
|
21
|
+
downloadCore,
|
|
22
|
+
RECOMMENDED_CORE_NAMES,
|
|
23
|
+
RECOMMENDED_CORES,
|
|
24
|
+
type AvailableCoreInfo,
|
|
25
|
+
type DownloadProgress,
|
|
26
|
+
} from '../../frontend/coreDownloader';
|
|
27
|
+
import { notify } from '../../frontend/notifications';
|
|
28
|
+
import { logger } from '../../utils/logger';
|
|
29
|
+
import { getErrorMessage } from '../../utils/getErrorMessage';
|
|
30
|
+
import { useGamepadContext } from '../GamepadContext';
|
|
31
|
+
import { useClearTerminal } from '../hooks/useClearTerminal';
|
|
32
|
+
import {
|
|
33
|
+
TAB_INSTALLED,
|
|
34
|
+
TAB_DOWNLOAD,
|
|
35
|
+
MAX_VISIBLE_ITEMS,
|
|
36
|
+
BYTES_PER_KB,
|
|
37
|
+
} from './consts';
|
|
38
|
+
|
|
39
|
+
export * from './consts';
|
|
40
|
+
|
|
41
|
+
interface CoreManagerProps {
|
|
42
|
+
onClose: () => void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Information about an installed core */
|
|
46
|
+
interface InstalledCore {
|
|
47
|
+
id: string;
|
|
48
|
+
name: string;
|
|
49
|
+
extensions: string[];
|
|
50
|
+
path: string;
|
|
51
|
+
canDelete: boolean;
|
|
52
|
+
systemDescription: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Progress bar percentage constants */
|
|
56
|
+
const PROGRESS_FULL = 100;
|
|
57
|
+
|
|
58
|
+
export const CoreManager = ({ onClose }: CoreManagerProps) => {
|
|
59
|
+
const ready = useClearTerminal();
|
|
60
|
+
const { exit } = useApp();
|
|
61
|
+
|
|
62
|
+
// Tab state
|
|
63
|
+
const [activeTab, setActiveTab] = useState(TAB_INSTALLED);
|
|
64
|
+
|
|
65
|
+
// Installed cores state
|
|
66
|
+
const [installedCores, setInstalledCores] = useState<InstalledCore[]>([]);
|
|
67
|
+
const [installedSelectedIndex, setInstalledSelectedIndex] = useState(0);
|
|
68
|
+
|
|
69
|
+
// Download cores state
|
|
70
|
+
const [availableCores, setAvailableCores] = useState<AvailableCoreInfo[]>([]);
|
|
71
|
+
const [downloadSelectedIndex, setDownloadSelectedIndex] = useState(0);
|
|
72
|
+
const [isLoadingCores, setIsLoadingCores] = useState(false);
|
|
73
|
+
const [hasAttemptedLoad, setHasAttemptedLoad] = useState(false);
|
|
74
|
+
const [loadError, setLoadError] = useState<string | null>(null);
|
|
75
|
+
const [showAllCores, setShowAllCores] = useState(false);
|
|
76
|
+
|
|
77
|
+
// Download progress state
|
|
78
|
+
const [downloadingCore, setDownloadingCore] = useState<string | null>(null);
|
|
79
|
+
const [downloadProgress, setDownloadProgress] = useState<DownloadProgress | null>(null);
|
|
80
|
+
|
|
81
|
+
// Delete confirmation state
|
|
82
|
+
const [confirmDelete, setConfirmDelete] = useState<InstalledCore | null>(null);
|
|
83
|
+
|
|
84
|
+
// Core info display state (for non-deletable cores)
|
|
85
|
+
const [showCoreInfo, setShowCoreInfo] = useState<InstalledCore | null>(null);
|
|
86
|
+
|
|
87
|
+
// Install error state (for showing error screen with retry option)
|
|
88
|
+
const [installError, setInstallError] = useState<{
|
|
89
|
+
core: AvailableCoreInfo;
|
|
90
|
+
message: string;
|
|
91
|
+
} | null>(null);
|
|
92
|
+
|
|
93
|
+
// Get system description for a core based on its extensions
|
|
94
|
+
const getSystemDescription = useCallback((coreId: string, extensions: string[]): string => {
|
|
95
|
+
// Check if it's a known recommended core
|
|
96
|
+
// Core IDs now match buildbot names directly (e.g., "mgba", "mupen64plus-next")
|
|
97
|
+
const recommended = RECOMMENDED_CORES.find(c => c.name === coreId);
|
|
98
|
+
if (recommended) {
|
|
99
|
+
return recommended.description;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Fall back to system name from first extension
|
|
103
|
+
if (extensions.length > 0) {
|
|
104
|
+
const systemName = getSystemName(extensions[0]);
|
|
105
|
+
// Remove "Nintendo - " or similar prefixes for brevity
|
|
106
|
+
return systemName.replace(/^[^-]+ - /, '');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return 'Unknown system';
|
|
110
|
+
}, []);
|
|
111
|
+
|
|
112
|
+
// Load installed cores
|
|
113
|
+
const loadInstalledCores = useCallback(() => {
|
|
114
|
+
const cores = listCores();
|
|
115
|
+
const processed: InstalledCore[] = cores.map(core => ({
|
|
116
|
+
id: core.id,
|
|
117
|
+
name: core.name,
|
|
118
|
+
extensions: core.extensions,
|
|
119
|
+
path: core.path,
|
|
120
|
+
canDelete: isInUserCoresDirectory(core.path),
|
|
121
|
+
systemDescription: getSystemDescription(core.id, core.extensions),
|
|
122
|
+
}));
|
|
123
|
+
setInstalledCores(processed);
|
|
124
|
+
}, [getSystemDescription]);
|
|
125
|
+
|
|
126
|
+
// Load available cores from buildbot
|
|
127
|
+
const loadAvailableCores = useCallback(async () => {
|
|
128
|
+
setIsLoadingCores(true);
|
|
129
|
+
setLoadError(null);
|
|
130
|
+
setHasAttemptedLoad(true);
|
|
131
|
+
try {
|
|
132
|
+
const cores = await fetchAvailableCores();
|
|
133
|
+
setAvailableCores(cores);
|
|
134
|
+
} catch (error) {
|
|
135
|
+
setLoadError(getErrorMessage(error));
|
|
136
|
+
} finally {
|
|
137
|
+
setIsLoadingCores(false);
|
|
138
|
+
}
|
|
139
|
+
}, []);
|
|
140
|
+
|
|
141
|
+
// Initial load
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
loadInstalledCores();
|
|
144
|
+
}, [loadInstalledCores]);
|
|
145
|
+
|
|
146
|
+
// Load available cores when switching to download tab (only once)
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
if (activeTab === TAB_DOWNLOAD && !hasAttemptedLoad && !isLoadingCores) {
|
|
149
|
+
void loadAvailableCores();
|
|
150
|
+
}
|
|
151
|
+
}, [activeTab, hasAttemptedLoad, isLoadingCores, loadAvailableCores]);
|
|
152
|
+
|
|
153
|
+
// Get installed core names for filtering available cores
|
|
154
|
+
// Core IDs now match buildbot names directly (e.g., "mgba", "mupen64plus-next")
|
|
155
|
+
const installedCoreNames = useMemo(() => {
|
|
156
|
+
return new Set(installedCores.map(c => c.id));
|
|
157
|
+
}, [installedCores]);
|
|
158
|
+
|
|
159
|
+
// Filter available cores (exclude installed, optionally show only recommended)
|
|
160
|
+
const filteredAvailableCores = useMemo(() => {
|
|
161
|
+
let coresToShow = availableCores.filter(c => !installedCoreNames.has(c.name));
|
|
162
|
+
if (!showAllCores) {
|
|
163
|
+
coresToShow = coresToShow.filter(c => c.isRecommended);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Sort: recommended first, then alphabetically
|
|
167
|
+
return coresToShow.sort((a, b) => {
|
|
168
|
+
if (a.isRecommended && !b.isRecommended) { return -1; }
|
|
169
|
+
if (!a.isRecommended && b.isRecommended) { return 1; }
|
|
170
|
+
return a.name.localeCompare(b.name);
|
|
171
|
+
});
|
|
172
|
+
}, [availableCores, installedCoreNames, showAllCores]);
|
|
173
|
+
|
|
174
|
+
// Count of available but not shown cores
|
|
175
|
+
const hiddenCoresCount = useMemo(() => {
|
|
176
|
+
if (showAllCores) { return 0; }
|
|
177
|
+
const allNotInstalled = availableCores.filter(c => !installedCoreNames.has(c.name));
|
|
178
|
+
return allNotInstalled.length - filteredAvailableCores.length;
|
|
179
|
+
}, [availableCores, installedCoreNames, filteredAvailableCores.length, showAllCores]);
|
|
180
|
+
|
|
181
|
+
// Handle core deletion
|
|
182
|
+
const handleDeleteCore = useCallback(async (core: InstalledCore) => {
|
|
183
|
+
try {
|
|
184
|
+
await unlink(core.path);
|
|
185
|
+
unregisterCore(core.id);
|
|
186
|
+
unloadLibretroCore(core.path, core.id);
|
|
187
|
+
notify({ message: `Deleted ${core.name}`, severity: 'info' });
|
|
188
|
+
loadInstalledCores();
|
|
189
|
+
setConfirmDelete(null);
|
|
190
|
+
// Reset selection if needed
|
|
191
|
+
setInstalledSelectedIndex(prev => Math.min(prev, installedCores.length - 2));
|
|
192
|
+
} catch (error) {
|
|
193
|
+
notify({
|
|
194
|
+
message: `Failed to delete ${core.name}: ${getErrorMessage(error)}`,
|
|
195
|
+
severity: 'error',
|
|
196
|
+
});
|
|
197
|
+
setConfirmDelete(null);
|
|
198
|
+
}
|
|
199
|
+
}, [loadInstalledCores, installedCores.length]);
|
|
200
|
+
|
|
201
|
+
// Handle core download
|
|
202
|
+
const handleDownloadCore = useCallback(async (core: AvailableCoreInfo) => {
|
|
203
|
+
setDownloadingCore(core.name);
|
|
204
|
+
setDownloadProgress(null);
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
const corePath = await downloadCore(core.name, (progress) => {
|
|
208
|
+
setDownloadProgress(progress);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// Dynamically register the newly downloaded core
|
|
212
|
+
const coreId = registerLibretroCore(corePath);
|
|
213
|
+
if (coreId) {
|
|
214
|
+
notify({ message: `Installed ${core.name}`, severity: 'info' });
|
|
215
|
+
// Refresh installed cores list to show the new core
|
|
216
|
+
loadInstalledCores();
|
|
217
|
+
} else {
|
|
218
|
+
notify({ message: `Downloaded ${core.name} (already registered)`, severity: 'info' });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Remove from available cores list
|
|
222
|
+
setAvailableCores(prev => prev.filter(c => c.name !== core.name));
|
|
223
|
+
|
|
224
|
+
// Reset selection if needed
|
|
225
|
+
setDownloadSelectedIndex(prev => Math.min(prev, filteredAvailableCores.length - 2));
|
|
226
|
+
} catch (error) {
|
|
227
|
+
const errorMessage = getErrorMessage(error);
|
|
228
|
+
logger.error(`Failed to install core ${core.name}: ${errorMessage}`, 'CoreManager');
|
|
229
|
+
console.error(`Failed to install ${core.name}: ${errorMessage}`);
|
|
230
|
+
setInstallError({ core, message: errorMessage });
|
|
231
|
+
} finally {
|
|
232
|
+
setDownloadingCore(null);
|
|
233
|
+
setDownloadProgress(null);
|
|
234
|
+
}
|
|
235
|
+
}, [loadInstalledCores]);
|
|
236
|
+
|
|
237
|
+
// Calculate scroll offset for lists
|
|
238
|
+
const getScrollOffset = (selectedIndex: number, totalItems: number): number => {
|
|
239
|
+
if (totalItems <= MAX_VISIBLE_ITEMS) { return 0; }
|
|
240
|
+
const halfVisible = Math.floor(MAX_VISIBLE_ITEMS / 2);
|
|
241
|
+
if (selectedIndex <= halfVisible) { return 0; }
|
|
242
|
+
if (selectedIndex >= totalItems - halfVisible) { return totalItems - MAX_VISIBLE_ITEMS; }
|
|
243
|
+
return selectedIndex - halfVisible;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// Keyboard input handling
|
|
247
|
+
useInput((input, key) => {
|
|
248
|
+
// CTRL-C exits the app
|
|
249
|
+
if (input === '\x03' || (key.ctrl && input === 'c')) {
|
|
250
|
+
exit();
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Handle delete confirmation dialog
|
|
255
|
+
if (confirmDelete) {
|
|
256
|
+
if (key.escape || input.toLowerCase() === 'n') {
|
|
257
|
+
setConfirmDelete(null);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (key.return || input.toLowerCase() === 'y') {
|
|
261
|
+
void handleDeleteCore(confirmDelete);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Handle core info dialog (for non-deletable cores)
|
|
268
|
+
if (showCoreInfo) {
|
|
269
|
+
if (key.escape || key.return) {
|
|
270
|
+
setShowCoreInfo(null);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Handle install error screen
|
|
277
|
+
if (installError) {
|
|
278
|
+
if (key.escape || input.toLowerCase() === 'c') {
|
|
279
|
+
setInstallError(null);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (key.return || input.toLowerCase() === 'r') {
|
|
283
|
+
const coreToRetry = installError.core;
|
|
284
|
+
setInstallError(null);
|
|
285
|
+
void handleDownloadCore(coreToRetry);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Downloading - ignore most input
|
|
292
|
+
if (downloadingCore) {
|
|
293
|
+
if (key.escape) {
|
|
294
|
+
// Could implement cancel here if needed
|
|
295
|
+
}
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Close panel
|
|
300
|
+
if (key.escape) {
|
|
301
|
+
onClose();
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Tab switching
|
|
306
|
+
if (key.tab || key.leftArrow || key.rightArrow) {
|
|
307
|
+
setActiveTab(prev => prev === TAB_INSTALLED ? TAB_DOWNLOAD : TAB_INSTALLED);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Navigation
|
|
312
|
+
if (activeTab === TAB_INSTALLED) {
|
|
313
|
+
if (key.upArrow) {
|
|
314
|
+
setInstalledSelectedIndex(prev => Math.max(0, prev - 1));
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (key.downArrow) {
|
|
318
|
+
setInstalledSelectedIndex(prev => Math.min(installedCores.length - 1, prev + 1));
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
// Select core with Enter to show delete confirmation or core info
|
|
322
|
+
if (key.return && installedCores[installedSelectedIndex]) {
|
|
323
|
+
const core = installedCores[installedSelectedIndex];
|
|
324
|
+
if (core.canDelete) {
|
|
325
|
+
setConfirmDelete(core);
|
|
326
|
+
} else {
|
|
327
|
+
setShowCoreInfo(core);
|
|
328
|
+
}
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
} else {
|
|
332
|
+
// Download tab
|
|
333
|
+
if (key.upArrow) {
|
|
334
|
+
setDownloadSelectedIndex(prev => Math.max(0, prev - 1));
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (key.downArrow) {
|
|
338
|
+
setDownloadSelectedIndex(prev => Math.min(filteredAvailableCores.length - 1, prev + 1));
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
// Download with Enter
|
|
342
|
+
if (key.return && filteredAvailableCores[downloadSelectedIndex]) {
|
|
343
|
+
void handleDownloadCore(filteredAvailableCores[downloadSelectedIndex]);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
// Toggle show all cores
|
|
347
|
+
if (input === 'a' || input === 'A') {
|
|
348
|
+
setShowAllCores(prev => !prev);
|
|
349
|
+
setDownloadSelectedIndex(0);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
// Refresh available cores
|
|
353
|
+
if (input === 'r' || input === 'R') {
|
|
354
|
+
setHasAttemptedLoad(false);
|
|
355
|
+
setLoadError(null);
|
|
356
|
+
void loadAvailableCores();
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// Gamepad support
|
|
363
|
+
useGamepadContext({
|
|
364
|
+
onUp: () => {
|
|
365
|
+
if (confirmDelete || downloadingCore || installError) { return; }
|
|
366
|
+
if (activeTab === TAB_INSTALLED) {
|
|
367
|
+
setInstalledSelectedIndex(prev => Math.max(0, prev - 1));
|
|
368
|
+
} else {
|
|
369
|
+
setDownloadSelectedIndex(prev => Math.max(0, prev - 1));
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
onDown: () => {
|
|
373
|
+
if (confirmDelete || downloadingCore || installError) { return; }
|
|
374
|
+
if (activeTab === TAB_INSTALLED) {
|
|
375
|
+
setInstalledSelectedIndex(prev => Math.min(installedCores.length - 1, prev + 1));
|
|
376
|
+
} else {
|
|
377
|
+
setDownloadSelectedIndex(prev => Math.min(filteredAvailableCores.length - 1, prev + 1));
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
onLeft: () => {
|
|
381
|
+
if (confirmDelete || downloadingCore || installError) { return; }
|
|
382
|
+
setActiveTab(TAB_INSTALLED);
|
|
383
|
+
},
|
|
384
|
+
onRight: () => {
|
|
385
|
+
if (confirmDelete || downloadingCore || installError) { return; }
|
|
386
|
+
setActiveTab(TAB_DOWNLOAD);
|
|
387
|
+
},
|
|
388
|
+
onConfirm: () => {
|
|
389
|
+
if (confirmDelete) {
|
|
390
|
+
void handleDeleteCore(confirmDelete);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (showCoreInfo) {
|
|
394
|
+
setShowCoreInfo(null);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (installError) {
|
|
398
|
+
const coreToRetry = installError.core;
|
|
399
|
+
setInstallError(null);
|
|
400
|
+
void handleDownloadCore(coreToRetry);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if (downloadingCore) { return; }
|
|
404
|
+
if (activeTab === TAB_INSTALLED && installedCores[installedSelectedIndex]) {
|
|
405
|
+
const core = installedCores[installedSelectedIndex];
|
|
406
|
+
if (core.canDelete) {
|
|
407
|
+
setConfirmDelete(core);
|
|
408
|
+
} else {
|
|
409
|
+
setShowCoreInfo(core);
|
|
410
|
+
}
|
|
411
|
+
} else if (activeTab === TAB_DOWNLOAD && filteredAvailableCores[downloadSelectedIndex]) {
|
|
412
|
+
void handleDownloadCore(filteredAvailableCores[downloadSelectedIndex]);
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
onCancel: () => {
|
|
416
|
+
if (confirmDelete) {
|
|
417
|
+
setConfirmDelete(null);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (showCoreInfo) {
|
|
421
|
+
setShowCoreInfo(null);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
if (installError) {
|
|
425
|
+
setInstallError(null);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (downloadingCore) { return; }
|
|
429
|
+
onClose();
|
|
430
|
+
},
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
// Wait for terminal clear
|
|
434
|
+
if (!ready) {
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Delete confirmation dialog
|
|
439
|
+
if (confirmDelete) {
|
|
440
|
+
return (
|
|
441
|
+
<Box flexDirection="column" padding={1}>
|
|
442
|
+
<Box marginBottom={1}>
|
|
443
|
+
<Text bold color="yellow">{'\u26A0'} Delete Core</Text>
|
|
444
|
+
</Box>
|
|
445
|
+
|
|
446
|
+
<Box marginBottom={1}>
|
|
447
|
+
<Text color="white">Are you sure you want to delete {confirmDelete.name}?</Text>
|
|
448
|
+
</Box>
|
|
449
|
+
|
|
450
|
+
<Box marginBottom={1}>
|
|
451
|
+
<Text color="gray">{confirmDelete.path}</Text>
|
|
452
|
+
</Box>
|
|
453
|
+
|
|
454
|
+
<Box marginTop={1}>
|
|
455
|
+
<Box marginRight={2}>
|
|
456
|
+
<Text color="red" bold>[Y]</Text>
|
|
457
|
+
<Text color="gray"> Delete</Text>
|
|
458
|
+
</Box>
|
|
459
|
+
<Box>
|
|
460
|
+
<Text color="green" bold>[N]</Text>
|
|
461
|
+
<Text color="gray"> Cancel</Text>
|
|
462
|
+
</Box>
|
|
463
|
+
</Box>
|
|
464
|
+
</Box>
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Core info dialog (for non-deletable cores)
|
|
469
|
+
if (showCoreInfo) {
|
|
470
|
+
return (
|
|
471
|
+
<Box flexDirection="column" padding={1}>
|
|
472
|
+
<Box marginBottom={1}>
|
|
473
|
+
<Text bold color="cyan">{'\u2139'} Core Info</Text>
|
|
474
|
+
</Box>
|
|
475
|
+
|
|
476
|
+
<Box marginBottom={1}>
|
|
477
|
+
<Text color="white" bold>{showCoreInfo.name}</Text>
|
|
478
|
+
</Box>
|
|
479
|
+
|
|
480
|
+
<Box marginBottom={1}>
|
|
481
|
+
<Text color="gray">{showCoreInfo.systemDescription}</Text>
|
|
482
|
+
</Box>
|
|
483
|
+
|
|
484
|
+
<Box marginBottom={1}>
|
|
485
|
+
<Text color="gray" dimColor>
|
|
486
|
+
{'This core was not installed by emoemu and cannot be removed from here.'}
|
|
487
|
+
</Text>
|
|
488
|
+
</Box>
|
|
489
|
+
|
|
490
|
+
<Box marginTop={1}>
|
|
491
|
+
<Text color="gray" dimColor>Press Enter or ESC to close</Text>
|
|
492
|
+
</Box>
|
|
493
|
+
</Box>
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Download progress overlay
|
|
498
|
+
if (downloadingCore) {
|
|
499
|
+
const progressPercent = downloadProgress?.totalBytes
|
|
500
|
+
? Math.round((downloadProgress.bytesDownloaded / downloadProgress.totalBytes) * PROGRESS_FULL)
|
|
501
|
+
: 0;
|
|
502
|
+
|
|
503
|
+
const isBuilding = downloadProgress?.phase === 'building';
|
|
504
|
+
const headerText = isBuilding ? 'Building Core' : 'Downloading Core';
|
|
505
|
+
const headerEmoji = isBuilding ? '\u{1F6E0}' : '\u{1F4E5}'; // 🛠 vs 📥
|
|
506
|
+
const statusText = isBuilding
|
|
507
|
+
? `Building ${downloadingCore} from source...`
|
|
508
|
+
: `Downloading ${downloadingCore}...`;
|
|
509
|
+
|
|
510
|
+
return (
|
|
511
|
+
<Box flexDirection="column" padding={1}>
|
|
512
|
+
<Box marginBottom={1}>
|
|
513
|
+
<Text bold color="cyan">{headerEmoji} {headerText}</Text>
|
|
514
|
+
</Box>
|
|
515
|
+
|
|
516
|
+
<Box marginBottom={1}>
|
|
517
|
+
<Text>{statusText}</Text>
|
|
518
|
+
</Box>
|
|
519
|
+
|
|
520
|
+
{downloadProgress?.phase === 'downloading' && downloadProgress.totalBytes && (
|
|
521
|
+
<Box marginBottom={1}>
|
|
522
|
+
<ProgressBar value={progressPercent} />
|
|
523
|
+
<Text color="gray"> {progressPercent}%</Text>
|
|
524
|
+
</Box>
|
|
525
|
+
)}
|
|
526
|
+
|
|
527
|
+
{downloadProgress?.phase === 'downloading' && !downloadProgress.totalBytes && (
|
|
528
|
+
<Box marginBottom={1}>
|
|
529
|
+
<Spinner label={`${Math.round(downloadProgress.bytesDownloaded / BYTES_PER_KB)} KB downloaded`} />
|
|
530
|
+
</Box>
|
|
531
|
+
)}
|
|
532
|
+
|
|
533
|
+
{downloadProgress?.phase === 'extracting' && (
|
|
534
|
+
<Box marginBottom={1}>
|
|
535
|
+
<Spinner label="Extracting..." />
|
|
536
|
+
</Box>
|
|
537
|
+
)}
|
|
538
|
+
|
|
539
|
+
{downloadProgress?.phase === 'building' && (
|
|
540
|
+
<Box marginBottom={1} flexDirection="column">
|
|
541
|
+
{downloadProgress.buildProgressPercent !== undefined ? (
|
|
542
|
+
<>
|
|
543
|
+
<Box>
|
|
544
|
+
<ProgressBar value={downloadProgress.buildProgressPercent} />
|
|
545
|
+
<Text color="gray"> {downloadProgress.buildProgressPercent}%</Text>
|
|
546
|
+
</Box>
|
|
547
|
+
<Box marginTop={1}>
|
|
548
|
+
<Text color="gray" dimColor>
|
|
549
|
+
{downloadProgress.buildMessage ?? 'Building...'}
|
|
550
|
+
</Text>
|
|
551
|
+
</Box>
|
|
552
|
+
</>
|
|
553
|
+
) : (
|
|
554
|
+
<Spinner label={downloadProgress.buildMessage ?? 'Building from source...'} />
|
|
555
|
+
)}
|
|
556
|
+
</Box>
|
|
557
|
+
)}
|
|
558
|
+
</Box>
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Install error screen
|
|
563
|
+
if (installError) {
|
|
564
|
+
return (
|
|
565
|
+
<Box flexDirection="column" padding={1}>
|
|
566
|
+
<Box marginBottom={1}>
|
|
567
|
+
<Text bold color="red">{'\u{274C}'} Installation Failed</Text>
|
|
568
|
+
</Box>
|
|
569
|
+
|
|
570
|
+
<Box marginBottom={1}>
|
|
571
|
+
<Text>Failed to install <Text bold>{installError.core.name}</Text></Text>
|
|
572
|
+
</Box>
|
|
573
|
+
|
|
574
|
+
<Box marginBottom={1} flexDirection="column">
|
|
575
|
+
<Text color="gray">Error:</Text>
|
|
576
|
+
<Text color="red">{installError.message}</Text>
|
|
577
|
+
</Box>
|
|
578
|
+
|
|
579
|
+
<Box marginTop={1} flexDirection="column">
|
|
580
|
+
<Text color="cyan">[R] Retry</Text>
|
|
581
|
+
<Text color="gray">[C] Cancel</Text>
|
|
582
|
+
</Box>
|
|
583
|
+
</Box>
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Calculate scroll offsets
|
|
588
|
+
const installedScrollOffset = getScrollOffset(installedSelectedIndex, installedCores.length);
|
|
589
|
+
const downloadScrollOffset = getScrollOffset(downloadSelectedIndex, filteredAvailableCores.length);
|
|
590
|
+
|
|
591
|
+
// Visible items for installed tab
|
|
592
|
+
const visibleInstalledCores = installedCores.slice(
|
|
593
|
+
installedScrollOffset,
|
|
594
|
+
installedScrollOffset + MAX_VISIBLE_ITEMS
|
|
595
|
+
);
|
|
596
|
+
|
|
597
|
+
// Visible items for download tab
|
|
598
|
+
const visibleDownloadCores = filteredAvailableCores.slice(
|
|
599
|
+
downloadScrollOffset,
|
|
600
|
+
downloadScrollOffset + MAX_VISIBLE_ITEMS
|
|
601
|
+
);
|
|
602
|
+
|
|
603
|
+
return (
|
|
604
|
+
<Box flexDirection="column" padding={1}>
|
|
605
|
+
{/* Header */}
|
|
606
|
+
<Box marginBottom={1}>
|
|
607
|
+
<Text bold color="cyan">{'\u{1F9E9}'} Manage Cores</Text>
|
|
608
|
+
</Box>
|
|
609
|
+
|
|
610
|
+
{/* Tabs */}
|
|
611
|
+
<Box marginBottom={1}>
|
|
612
|
+
<Box marginRight={2}>
|
|
613
|
+
<Text
|
|
614
|
+
backgroundColor={activeTab === TAB_INSTALLED ? 'cyan' : undefined}
|
|
615
|
+
color={activeTab === TAB_INSTALLED ? 'black' : 'gray'}
|
|
616
|
+
bold={activeTab === TAB_INSTALLED}
|
|
617
|
+
>
|
|
618
|
+
{' '}Installed ({installedCores.length}){' '}
|
|
619
|
+
</Text>
|
|
620
|
+
</Box>
|
|
621
|
+
<Box>
|
|
622
|
+
<Text
|
|
623
|
+
backgroundColor={activeTab === TAB_DOWNLOAD ? 'cyan' : undefined}
|
|
624
|
+
color={activeTab === TAB_DOWNLOAD ? 'black' : 'gray'}
|
|
625
|
+
bold={activeTab === TAB_DOWNLOAD}
|
|
626
|
+
>
|
|
627
|
+
{' '}Download{' '}
|
|
628
|
+
</Text>
|
|
629
|
+
</Box>
|
|
630
|
+
</Box>
|
|
631
|
+
|
|
632
|
+
{/* Tab content */}
|
|
633
|
+
{activeTab === TAB_INSTALLED ? (
|
|
634
|
+
<Box flexDirection="column">
|
|
635
|
+
{installedCores.length === 0 ? (
|
|
636
|
+
<Text color="gray">No cores installed</Text>
|
|
637
|
+
) : (
|
|
638
|
+
<>
|
|
639
|
+
{/* Scroll indicator - top */}
|
|
640
|
+
{installedScrollOffset > 0 && (
|
|
641
|
+
<Box>
|
|
642
|
+
<Text color="gray" dimColor> {'\u25B2'} {installedScrollOffset} more above</Text>
|
|
643
|
+
</Box>
|
|
644
|
+
)}
|
|
645
|
+
|
|
646
|
+
{visibleInstalledCores.map((core, index) => {
|
|
647
|
+
const globalIndex = installedScrollOffset + index;
|
|
648
|
+
const isSelected = globalIndex === installedSelectedIndex;
|
|
649
|
+
return (
|
|
650
|
+
<Box key={core.id} flexDirection="column">
|
|
651
|
+
<Box>
|
|
652
|
+
<Text
|
|
653
|
+
color={isSelected ? 'cyan' : 'white'}
|
|
654
|
+
bold={isSelected}
|
|
655
|
+
>
|
|
656
|
+
{isSelected ? '\u25B6 ' : ' '}
|
|
657
|
+
{core.name}
|
|
658
|
+
</Text>
|
|
659
|
+
</Box>
|
|
660
|
+
<Box marginLeft={4}>
|
|
661
|
+
<Text color="gray" dimColor>{core.systemDescription}</Text>
|
|
662
|
+
</Box>
|
|
663
|
+
</Box>
|
|
664
|
+
);
|
|
665
|
+
})}
|
|
666
|
+
|
|
667
|
+
{/* Scroll indicator - bottom */}
|
|
668
|
+
{installedScrollOffset + MAX_VISIBLE_ITEMS < installedCores.length && (
|
|
669
|
+
<Box>
|
|
670
|
+
<Text color="gray" dimColor>
|
|
671
|
+
{' '}{'\u25BC'} {installedCores.length - installedScrollOffset - MAX_VISIBLE_ITEMS} more below
|
|
672
|
+
</Text>
|
|
673
|
+
</Box>
|
|
674
|
+
)}
|
|
675
|
+
</>
|
|
676
|
+
)}
|
|
677
|
+
</Box>
|
|
678
|
+
) : (
|
|
679
|
+
<Box flexDirection="column">
|
|
680
|
+
{isLoadingCores ? (
|
|
681
|
+
<Spinner label="Loading available cores..." />
|
|
682
|
+
) : loadError ? (
|
|
683
|
+
<Box flexDirection="column">
|
|
684
|
+
<Text color="red">Error: {loadError}</Text>
|
|
685
|
+
<Text color="gray" dimColor>Press R to retry</Text>
|
|
686
|
+
</Box>
|
|
687
|
+
) : filteredAvailableCores.length === 0 ? (
|
|
688
|
+
<Box flexDirection="column">
|
|
689
|
+
<Text color="gray">
|
|
690
|
+
{showAllCores ? 'All cores are already installed' : 'All recommended cores are installed'}
|
|
691
|
+
</Text>
|
|
692
|
+
{!showAllCores && hiddenCoresCount > 0 && (
|
|
693
|
+
<Text color="gray" dimColor>Press A to show {hiddenCoresCount} additional cores</Text>
|
|
694
|
+
)}
|
|
695
|
+
</Box>
|
|
696
|
+
) : (
|
|
697
|
+
<>
|
|
698
|
+
{/* Show all toggle info */}
|
|
699
|
+
{!showAllCores && hiddenCoresCount > 0 && (
|
|
700
|
+
<Box marginBottom={1}>
|
|
701
|
+
<Text color="gray" dimColor>
|
|
702
|
+
Showing recommended cores. Press A to show {hiddenCoresCount} more.
|
|
703
|
+
</Text>
|
|
704
|
+
</Box>
|
|
705
|
+
)}
|
|
706
|
+
{showAllCores && (
|
|
707
|
+
<Box marginBottom={1}>
|
|
708
|
+
<Text color="gray" dimColor>
|
|
709
|
+
Showing all cores. Press A to show recommended only.
|
|
710
|
+
</Text>
|
|
711
|
+
</Box>
|
|
712
|
+
)}
|
|
713
|
+
|
|
714
|
+
{/* Scroll indicator - top */}
|
|
715
|
+
{downloadScrollOffset > 0 && (
|
|
716
|
+
<Box>
|
|
717
|
+
<Text color="gray" dimColor> {'\u25B2'} {downloadScrollOffset} more above</Text>
|
|
718
|
+
</Box>
|
|
719
|
+
)}
|
|
720
|
+
|
|
721
|
+
{visibleDownloadCores.map((core, index) => {
|
|
722
|
+
const globalIndex = downloadScrollOffset + index;
|
|
723
|
+
const isSelected = globalIndex === downloadSelectedIndex;
|
|
724
|
+
return (
|
|
725
|
+
<Box key={core.name} flexDirection="column">
|
|
726
|
+
<Box>
|
|
727
|
+
<Text
|
|
728
|
+
color={isSelected ? 'cyan' : 'white'}
|
|
729
|
+
bold={isSelected}
|
|
730
|
+
>
|
|
731
|
+
{isSelected ? '\u25B6 ' : ' '}
|
|
732
|
+
{core.name}
|
|
733
|
+
</Text>
|
|
734
|
+
{RECOMMENDED_CORE_NAMES.has(core.name) && (
|
|
735
|
+
<Text color="yellow"> {'\u2605'}</Text>
|
|
736
|
+
)}
|
|
737
|
+
</Box>
|
|
738
|
+
{core.description && (
|
|
739
|
+
<Box marginLeft={4}>
|
|
740
|
+
<Text color="gray" dimColor>{core.description}</Text>
|
|
741
|
+
</Box>
|
|
742
|
+
)}
|
|
743
|
+
</Box>
|
|
744
|
+
);
|
|
745
|
+
})}
|
|
746
|
+
|
|
747
|
+
{/* Scroll indicator - bottom */}
|
|
748
|
+
{downloadScrollOffset + MAX_VISIBLE_ITEMS < filteredAvailableCores.length && (
|
|
749
|
+
<Box>
|
|
750
|
+
<Text color="gray" dimColor>
|
|
751
|
+
{' '}{'\u25BC'} {filteredAvailableCores.length - downloadScrollOffset - MAX_VISIBLE_ITEMS} more below
|
|
752
|
+
</Text>
|
|
753
|
+
</Box>
|
|
754
|
+
)}
|
|
755
|
+
</>
|
|
756
|
+
)}
|
|
757
|
+
</Box>
|
|
758
|
+
)}
|
|
759
|
+
|
|
760
|
+
{/* Help text */}
|
|
761
|
+
<Box marginTop={1} flexDirection="column">
|
|
762
|
+
<Text color="gray" dimColor>
|
|
763
|
+
{'\u2190\u2192'}/Tab: Switch tabs {'\u2191\u2193'}: Navigate ESC: Close
|
|
764
|
+
</Text>
|
|
765
|
+
{activeTab === TAB_INSTALLED && (
|
|
766
|
+
<Text color="gray" dimColor>
|
|
767
|
+
Enter: Remove selected core
|
|
768
|
+
</Text>
|
|
769
|
+
)}
|
|
770
|
+
{activeTab === TAB_DOWNLOAD && (
|
|
771
|
+
<Text color="gray" dimColor>
|
|
772
|
+
Enter: Download A: Toggle all cores R: Refresh
|
|
773
|
+
</Text>
|
|
774
|
+
)}
|
|
775
|
+
</Box>
|
|
776
|
+
|
|
777
|
+
</Box>
|
|
778
|
+
);
|
|
779
|
+
};
|