diff --git a/languages/de.json b/languages/de.json index 2fe0ae6..0a17ceb 100644 --- a/languages/de.json +++ b/languages/de.json @@ -192,7 +192,7 @@ "panels.settings": "Anwendungseinstellungen", "panels.faq": "FAQ", "panels.utilities": "Werkzeuge", - "panels.developer": "Entwickler", + "panels.developer": "Entwickleroptionen", "dev.mode": "Entwicklermodus", "faq.searchPlaceholder": "FAQ durchsuchen...", "faq.noResults": "Keine Ergebnisse gefunden.", diff --git a/src/main/AssetManager.ts b/src/main/AssetManager.ts index d58d65f..2ec8a5b 100644 --- a/src/main/AssetManager.ts +++ b/src/main/AssetManager.ts @@ -23,12 +23,18 @@ function httpsGetJson(url: string): Promise { let data = ''; res.on('data', (c) => (data += c)); res.on('end', () => { - try { resolve(JSON.parse(data)); } - catch { reject(new Error('JSON parse error')); } + try { + resolve(JSON.parse(data)); + } catch { + reject(new Error('JSON parse error')); + } }); }); req.on('error', reject); - req.setTimeout(10000, () => { req.destroy(); reject(new Error('Timeout')); }); + req.setTimeout(10000, () => { + req.destroy(); + reject(new Error('Timeout')); + }); }); } @@ -80,7 +86,11 @@ export function setActiveTheme(themeId: string): ThemeDefinition { return getActiveTheme(); } -export async function fetchRemoteThemes(): Promise<{ ok: boolean; themes?: ThemeDefinition[]; error?: string }> { +export async function fetchRemoteThemes(): Promise<{ + ok: boolean; + themes?: ThemeDefinition[]; + error?: string; +}> { try { const listing = await httpsGetJson(contentsUrl(THEME_GITHUB_PATH)); if (!Array.isArray(listing)) return { ok: false, error: 'Themes folder not found' }; @@ -89,7 +99,9 @@ export async function fetchRemoteThemes(): Promise<{ ok: boolean; themes?: Theme try { const theme = (await httpsGetJson(rawUrl(THEME_GITHUB_PATH, f.name))) as ThemeDefinition; if (theme.id && theme.name && theme.colors) themes.push(theme); - } catch { /* skip */ } + } catch { + /* skip */ + } } return { ok: true, themes }; } catch (e) { @@ -97,16 +109,20 @@ export async function fetchRemoteThemes(): Promise<{ ok: boolean; themes?: Theme } } -export async function checkThemeUpdate(themeId: string): Promise<{ hasUpdate: boolean; remoteVersion: number; localVersion: number }> { +export async function checkThemeUpdate( + themeId: string +): Promise<{ hasUpdate: boolean; remoteVersion: number; localVersion: number }> { const state = loadThemeState(); const local = state.themes.find((t) => t.id === themeId); if (!local) return { hasUpdate: false, remoteVersion: 0, localVersion: 0 }; const result = await fetchRemoteThemes(); - if (!result.ok || !result.themes) return { hasUpdate: false, remoteVersion: local.version, localVersion: local.version }; + if (!result.ok || !result.themes) + return { hasUpdate: false, remoteVersion: local.version, localVersion: local.version }; const remote = result.themes.find((t) => t.id === themeId); - if (!remote) return { hasUpdate: false, remoteVersion: local.version, localVersion: local.version }; + if (!remote) + return { hasUpdate: false, remoteVersion: local.version, localVersion: local.version }; return { hasUpdate: remote.version > local.version, @@ -177,16 +193,24 @@ export function setActiveLanguage(langId: string): LanguageDefinition { return getActiveLanguage(); } -export async function fetchRemoteLanguages(): Promise<{ ok: boolean; languages?: LanguageDefinition[]; error?: string }> { +export async function fetchRemoteLanguages(): Promise<{ + ok: boolean; + languages?: LanguageDefinition[]; + error?: string; +}> { try { const listing = await httpsGetJson(contentsUrl(GITHUB_CONFIG.languagesPath)); if (!Array.isArray(listing)) return { ok: false, error: 'Languages folder not found' }; const languages: LanguageDefinition[] = []; for (const f of (listing as Array<{ name: string }>).filter((f) => f.name.endsWith('.json'))) { try { - const lang = (await httpsGetJson(rawUrl(GITHUB_CONFIG.languagesPath, f.name))) as LanguageDefinition; + const lang = (await httpsGetJson( + rawUrl(GITHUB_CONFIG.languagesPath, f.name) + )) as LanguageDefinition; if (lang.id && lang.name && lang.strings) languages.push(lang); - } catch { /* skip */ } + } catch { + /* skip */ + } } return { ok: true, languages }; } catch (e) { @@ -194,16 +218,20 @@ export async function fetchRemoteLanguages(): Promise<{ ok: boolean; languages?: } } -export async function checkLanguageUpdate(langId: string): Promise<{ hasUpdate: boolean; remoteVersion: number; localVersion: number }> { +export async function checkLanguageUpdate( + langId: string +): Promise<{ hasUpdate: boolean; remoteVersion: number; localVersion: number }> { const state = loadLanguageState(); const local = state.languages.find((l) => l.id === langId); if (!local) return { hasUpdate: false, remoteVersion: 0, localVersion: 0 }; const result = await fetchRemoteLanguages(); - if (!result.ok || !result.languages) return { hasUpdate: false, remoteVersion: local.version, localVersion: local.version }; + if (!result.ok || !result.languages) + return { hasUpdate: false, remoteVersion: local.version, localVersion: local.version }; const remote = result.languages.find((l) => l.id === langId); - if (!remote) return { hasUpdate: false, remoteVersion: local.version, localVersion: local.version }; + if (!remote) + return { hasUpdate: false, remoteVersion: local.version, localVersion: local.version }; return { hasUpdate: remote.version > local.version, @@ -212,7 +240,9 @@ export async function checkLanguageUpdate(langId: string): Promise<{ hasUpdate: }; } -export async function applyLanguageUpdate(langId: string): Promise<{ ok: boolean; error?: string }> { +export async function applyLanguageUpdate( + langId: string +): Promise<{ ok: boolean; error?: string }> { const result = await fetchRemoteLanguages(); if (!result.ok || !result.languages) return { ok: false, error: result.error ?? 'Fetch failed' }; @@ -244,14 +274,17 @@ function projectRoot(): string { export function loadLocalDevThemes(): ThemeDefinition[] { const dir = path.join(projectRoot(), 'themes'); if (!fs.existsSync(dir)) return []; - return fs.readdirSync(dir) + return fs + .readdirSync(dir) .filter((f) => f.endsWith('.json')) .map((f) => { try { const raw = fs.readFileSync(path.join(dir, f), 'utf8'); const theme = JSON.parse(raw) as ThemeDefinition; if (theme.id && theme.name && theme.colors) return theme; - } catch { /* skip */ } + } catch { + /* skip */ + } return null; }) .filter((t): t is ThemeDefinition => t !== null); @@ -260,14 +293,17 @@ export function loadLocalDevThemes(): ThemeDefinition[] { export function loadLocalDevLanguages(): LanguageDefinition[] { const dir = path.join(projectRoot(), 'languages'); if (!fs.existsSync(dir)) return []; - return fs.readdirSync(dir) + return fs + .readdirSync(dir) .filter((f) => f.endsWith('.json')) .map((f) => { try { const raw = fs.readFileSync(path.join(dir, f), 'utf8'); const lang = JSON.parse(raw) as LanguageDefinition; if (lang.id && lang.name && lang.strings) return lang; - } catch { /* skip */ } + } catch { + /* skip */ + } return null; }) .filter((l): l is LanguageDefinition => l !== null); @@ -286,4 +322,3 @@ export function syncLocalDevAssets(): { themes: number; languages: number } { } return { themes: tc, languages: lc }; } - diff --git a/src/main/FileLogger.ts b/src/main/FileLogger.ts index af22057..8dfb766 100644 --- a/src/main/FileLogger.ts +++ b/src/main/FileLogger.ts @@ -114,8 +114,18 @@ export function getLogFiles(profileId: string): LogFileInfo[] { filename, filePath, size: stat.size, - startedAt: match?.[1]?.replace(/_/g, ' ').replace(/-/g, ':').replace(/^(\d{4}):/, '$1-').replace(/:(\d{2}):/, '-$1 ') ?? '', - stoppedAt: match?.[2]?.replace(/_/g, ' ').replace(/-/g, ':').replace(/^(\d{4}):/, '$1-').replace(/:(\d{2}):/, '-$1 ') ?? undefined, + startedAt: + match?.[1] + ?.replace(/_/g, ' ') + .replace(/-/g, ':') + .replace(/^(\d{4}):/, '$1-') + .replace(/:(\d{2}):/, '-$1 ') ?? '', + stoppedAt: + match?.[2] + ?.replace(/_/g, ' ') + .replace(/-/g, ':') + .replace(/^(\d{4}):/, '$1-') + .replace(/:(\d{2}):/, '-$1 ') ?? undefined, }; }) .sort((a, b) => b.filename.localeCompare(a.filename)); diff --git a/src/main/ProcessManager.ts b/src/main/ProcessManager.ts index 748b890..71e9826 100644 --- a/src/main/ProcessManager.ts +++ b/src/main/ProcessManager.ts @@ -140,10 +140,7 @@ class ProcessManager { this.window = win; } - private buildArgs( - profile: Profile, - resolvedJarPath: string - ): { cmd: string; args: string[] } { + private buildArgs(profile: Profile, resolvedJarPath: string): { cmd: string; args: string[] } { const cmd = profile.javaPath || 'java'; const args: string[] = []; for (const a of profile.jvmArgs) if (a.enabled && a.value.trim()) args.push(a.value.trim()); @@ -303,7 +300,12 @@ class ProcessManager { m.intentionallyStopped = true; this.cancelRestartTimer(profileId); - this.pushSystem('stopping', profileId, String(m.process.pid ?? 0), 'Stopping process gracefully...'); + this.pushSystem( + 'stopping', + profileId, + String(m.process.pid ?? 0), + 'Stopping process gracefully...' + ); gracefulStop( m.process, @@ -587,11 +589,7 @@ class ProcessManager { } } - private flushPartial( - profileId: string, - type: 'stdout' | 'stderr', - m: ManagedProcess - ) { + private flushPartial(profileId: string, type: 'stdout' | 'stderr', m: ManagedProcess) { const partialKey = type === 'stdout' ? 'stdoutPartial' : 'stderrPartial'; const text = m[partialKey].trim(); if (!text) return; diff --git a/src/main/ipc/Process.ipc.ts b/src/main/ipc/Process.ipc.ts index a0c8d59..fc7f5a0 100644 --- a/src/main/ipc/Process.ipc.ts +++ b/src/main/ipc/Process.ipc.ts @@ -62,7 +62,8 @@ export const ProcessIPC = { handler: (_e: any, profileId: string) => { const profile = getAllProfiles().find((p) => p.id === profileId); if (!profile) return { ok: false, error: 'Profile not found' }; - const dir = profile.workingDir || (profile.jarPath ? require('path').dirname(profile.jarPath) : ''); + const dir = + profile.workingDir || (profile.jarPath ? require('path').dirname(profile.jarPath) : ''); if (!dir) return { ok: false, error: 'No working directory configured' }; shell.openPath(dir); return { ok: true }; diff --git a/src/main/shared/config/GitHub.config.ts b/src/main/shared/config/GitHub.config.ts index 0bb3001..5c6fcb9 100644 --- a/src/main/shared/config/GitHub.config.ts +++ b/src/main/shared/config/GitHub.config.ts @@ -14,9 +14,7 @@ export const GITHUB_CONFIG = { templateMinVersion: 1, apiBase: 'https://api.github.com', - trustedPublishers: [ - { login: 'timonmdy', label: 'Lead Developer' }, - ] as TrustedPublisher[], + trustedPublishers: [{ login: 'timonmdy', label: 'Lead Developer' }] as TrustedPublisher[], automationAccounts: ['github-actions[bot]', 'github-actions'], } as const; @@ -39,13 +37,11 @@ export function rawTemplateUrl(filename: string): string { export function getPublisherTrust(login: string): { level: TrustLevel; label: string } { const trusted = GITHUB_CONFIG.trustedPublishers.find( - (p) => p.login.toLowerCase() === login.toLowerCase(), + (p) => p.login.toLowerCase() === login.toLowerCase() ); if (trusted) return { level: 'trusted', label: trusted.label }; - if ( - GITHUB_CONFIG.automationAccounts.some((a) => a.toLowerCase() === login.toLowerCase()) - ) { + if (GITHUB_CONFIG.automationAccounts.some((a) => a.toLowerCase() === login.toLowerCase())) { return { level: 'automation', label: 'GitHub Actions' }; } diff --git a/src/main/shared/config/faq/FAQ.de.ts b/src/main/shared/config/faq/FAQ.de.ts new file mode 100644 index 0000000..18ff090 --- /dev/null +++ b/src/main/shared/config/faq/FAQ.de.ts @@ -0,0 +1,156 @@ +import { FaqTopic } from './_index'; + +export const FAQ_DE: FaqTopic[] = [ + { + id: 'general', + label: 'Allgemein', + items: [ + { + q: 'Was ist Java Runner Client?', + a: 'Java Runner Client (JRC) ermöglicht es, JAR-Dateien als dauerhafte Hintergrundprozesse zu starten und zu verwalten. Du erstellst ein Profil für jede JAR, konfigurierst die Argumente und startest/stoppst sie über den Konsolen-Tab.', + }, + { + q: 'Wie starte ich am schnellsten?', + a: '1. Klicke auf „Neues Profil" in der Seitenleiste.\n2. Gehe zu Konfigurieren -> Dateien & Pfade und wähle deine .jar aus.\n3. Gehe zur Konsole und klicke auf Starten.', + }, + { + q: 'Wo wird die Konfigurationsdatei gespeichert?', + a: 'Windows: %APPDATA%\\java-runner-client\\java-runner-config.json\nLinux: ~/.config/java-runner-client/\nmacOS: ~/Library/Application Support/java-runner-client/', + }, + { + q: 'Was ist ein „verwalteter" (durch JRC) Prozess?', + a: 'Ein „verwalteter" Prozess ist ein Java-Prozess, den JRC gestartet hat und überwacht. JRC erzeugt den Prozess direkt über Node.js und kapselt ihn in ein ManagedProcess-Objekt mit Metadaten (PID, Startzeit, Exit-Code). JRC erfasst alle stdout/stderr-Ausgaben in Echtzeit und überwacht den gesamten Prozesslebenszyklus.', + }, + ], + }, + { + id: 'setup', + label: 'Einrichtung & Konfiguration', + items: [ + { + q: 'Warum wird Java beim Starten nicht gefunden?', + a: 'Java ist nicht installiert oder nicht im System-PATH hinterlegt. Unter Konfigurieren -> Dateien & Pfade kannst du einen expliziten Pfad zur Java-Executable angeben, z.\u00A0B. C:\\Program Files\\Java\\jdk-21\\bin\\java.exe.', + }, + { + q: 'Wie stelle ich den JVM-Arbeitsspeicher ein?', + a: 'Unter Konfigurieren -> JVM-Args fügst du -Xmx2g (maximaler Heap 2 GB) und -Xms512m (initialer Heap 512 MB) hinzu. Jedes Argument steht in einer eigenen Zeile und lässt sich einzeln ein-/ausschalten.', + }, + { + q: 'Wie starte ich eine JAR automatisch beim App-Start?', + a: 'Öffne Konfigurieren -> Allgemein für das Profil und aktiviere „Automatisch starten beim App-Start". Zusätzlich kannst du in den Einstellungen „Beim Windows-Start starten" aktivieren.', + }, + { + q: 'Wie sortiere ich Profile in der Seitenleiste um?', + a: 'Ziehe Profile per Drag-and-Drop in der Seitenleiste an die gewünschte Position. Die neue Reihenfolge wird automatisch gespeichert — kein Bestätigen oder Speichern nötig.', + }, + { + q: 'Wie lösche ich ein Profil schnell?', + a: 'Rechtsklicke auf ein Profil und drücke Löschen. Halte Shift gedrückt, um die Bestätigung zu überspringen und es sofort zu entfernen. Die gleiche Shift-Abkürzung funktioniert auch beim Löschen-Button im Profil-Tab.', + }, + { + q: 'Wie nutze ich die „dynamische" JAR-Erkennung?', + a: 'Unter Konfigurieren -> Dateien & Pfade wählst du „Dynamisch" als JAR-Auswahlmethode. Damit wird die automatische JAR-Erkennung im Arbeitsverzeichnis aktiviert und du kannst das Suchmuster anpassen. Das ist nützlich für Projekte mit versionierten JARs oder wechselnden Dateinamen. Ändere den „app"-Teil im Dateinamensmuster auf den (statischen) Präfix deiner Anwendung und wähle die Art der Versionierung. Du kannst auch reguläre Ausdrücke (RegExp) verwenden, um volle Kontrolle über die Dateierkennung zu haben.', + }, + { + q: 'Wie setze ich Umgebungsvariablen für ein Profil?', + a: 'Gehe zu Konfigurieren -> Umgebung. Füge Schlüssel=Wert-Paare hinzu, die beim Start in die Prozessumgebung eingesetzt werden. Diese überschreiben gleichnamige System-Umgebungsvariablen. Jede Variable lässt sich einzeln ein- oder ausschalten.', + }, + { + q: 'Wie verwende ich eine eigene Farbe für mein Profil?', + a: 'Im Profil-Tab klickst du auf den „+"-Button am Ende der Farbpalette, um den nativen Farbwähler zu öffnen. Jede beliebige Hex-Farbe wird unterstützt.', + }, + ], + }, + { + id: 'console', + label: 'Konsole', + items: [ + { + q: 'Wie sende ich Befehle an einen laufenden Prozess?', + a: 'Im Konsolen-Tab tippst du in die Eingabezeile unten und drückst Enter. Mit Pfeil-hoch/runter navigierst du durch die Befehlshistorie. Strg+L leert die Ausgabe. Strg+F öffnet die Suche.', + }, + { + q: 'Wie kopiere ich eine einzelne Konsolenzeile?', + a: 'Rechtsklicke auf eine beliebige Zeile in der Konsole, um ein Kontextmenü mit „Zeile kopieren" und „Gesamte Ausgabe kopieren" zu öffnen.', + }, + { + q: 'Wie aktiviere ich Zeitstempel in der Konsole?', + a: 'Gehe zu Einstellungen -> Konsole und aktiviere „Zeitstempel anzeigen". Jede Zeile zeigt dann einen HH:MM:SS.mmm-Zeitstempel.', + }, + { + q: 'Was ist der Unterschied zwischen Stoppen und Sofort beenden?', + a: 'Stoppen sendet ein Shutdown-Signal (wie Strg+C im Terminal). Der Prozess bekommt die Möglichkeit, Daten zu speichern und aufzuräumen. Wenn er nicht innerhalb weniger Sekunden beendet, wird er zwangsweise terminiert.\n\nSofort beenden killt den Prozess sofort, ohne ihm eine Chance zum Aufräumen zu geben. Nutze das nur, wenn Stoppen nicht funktioniert.', + }, + { + q: 'Wie öffne ich das Arbeitsverzeichnis eines laufenden Prozesses?', + a: 'Klicke auf das Ordner-Symbol in der Konsolen-Toolbar. Damit wird das Arbeitsverzeichnis des Profils (oder das JAR-Verzeichnis, falls keins gesetzt ist) im Datei-Explorer geöffnet.', + }, + { + q: 'Warum sieht die Konsolenausgabe mit Sonderzeichen komisch aus?', + a: 'JRC verarbeitet ANSI-Escape-Sequenzen aus der Terminalausgabe. Die meisten Sequenzen (Farben, Cursorbewegung, Fortschrittsbalken) werden automatisch verarbeitet. Wenn ein Tool sehr ungewöhnliche Terminal-Sequenzen verwendet, können einzelne Zeichen durchrutschen.', + }, + ], + }, + { + id: 'logging', + label: 'Protokollierung', + items: [ + { + q: 'Wie speichere ich Konsolenausgaben in eine Datei?', + a: 'Gehe zu Konfigurieren -> Allgemein und aktiviere „Sitzungslogs in Datei speichern". Bei jedem Start und Stopp eines Prozesses wird eine .log-Datei im Konfigurationsverzeichnis unter logs// erstellt.', + }, + { + q: 'Wo werden Logdateien gespeichert?', + a: 'Logdateien befinden sich unter:\nWindows: %APPDATA%\\java-runner-client\\logs\\\\\nLinux: ~/.config/java-runner-client/logs//\n\nDateinamen enthalten Start- und Stopp-Zeitstempel.', + }, + { + q: 'Wie sehe ich vergangene Sitzungslogs ein?', + a: 'Gehe zum Logs-Tab (neben Konsole und Konfigurieren). Wähle eine Sitzung in der Seitenleiste aus, um ihren Inhalt anzuzeigen. Du kannst auch das gesamte Log kopieren oder alte Dateien löschen.', + }, + { + q: 'Kann ich alte Logdateien löschen?', + a: 'Ja. Im Logs-Tab wählst du eine Logdatei aus und klickst auf das Papierkorb-Symbol. Halte Shift gedrückt, um die Bestätigung zu überspringen. Du kannst auch auf das Ordner-Symbol klicken, um das Log-Verzeichnis zu öffnen und Dateien manuell zu verwalten.', + }, + ], + }, + { + id: 'usage', + label: 'Nutzung', + items: [ + { + q: 'Wie lasse ich JARs nach dem Schließen weiterlaufen?', + a: 'Aktiviere „Beim Schließen in den Tray minimieren" in den Einstellungen. Das Schließen des Fensters versteckt es dann im System-Tray, anstatt Prozesse zu stoppen.', + }, + { + q: 'Wie beende ich einen hängenden Prozess?', + a: 'Öffne Werkzeuge -> Prozess-Scanner -> Scannen. Java-Prozesse werden hervorgehoben. Klicke auf Beenden neben dem hängenden Prozess, oder nutze „Alle Java-Prozesse beenden" (geschützte Prozesse sind ausgenommen).', + }, + { + q: 'Was sind geschützte Prozesse im Scanner?', + a: 'Geschützte Prozesse (wie „Java Runner Client" selbst) werden im Scanner ausgegraut dargestellt und sind von „Alle Java-Prozesse beenden" ausgenommen. Du kannst sie trotzdem einzeln beenden, indem du auf ihren Beenden-Button klickst und bestätigst.', + }, + ], + }, + { + id: 'examples', + label: 'Beispiele', + items: [ + { + q: 'Wie starte ich einen Minecraft-Server?', + a: 'Erstelle ein Profil und setze den JAR-Pfad auf deine Server-.jar. Unter Programm-Args füge --nogui hinzu. Setze das Arbeitsverzeichnis auf deinen Server-Ordner. Füge -Xmx4g als JVM-Arg hinzu.', + }, + { + q: 'Wie starte ich eine Spring-Boot-App?', + a: 'Erstelle ein Profil und wähle deine JAR. Unter Properties (-D) füge spring.profiles.active = prod und server.port = 8080 hinzu.', + }, + { + q: 'Wie verwende ich eine Profilvorlage?', + a: 'Klicke auf „Aus Vorlage" in der Seitenleiste. Durchsuche die Vorlagen aus dem GitHub-Repository, wähle eine aus und klicke auf „Profil erstellen". Das neue Profil wird mit sinnvollen Standardwerten für den jeweiligen Anwendungsfall vorbefüllt.', + }, + { + q: 'Wie setze ich Umgebungsvariablen wie JAVA_HOME?', + a: 'Gehe zu Konfigurieren -> Umgebung und füge eine Zeile mit dem Schlüssel JAVA_HOME und dem Pfad zu deiner JDK-Installation hinzu. Schalte sie nach Bedarf ein oder aus, ohne sie entfernen zu müssen.', + }, + ], + }, +]; diff --git a/src/main/shared/config/FAQ.config.ts b/src/main/shared/config/faq/FAQ.en.ts similarity index 94% rename from src/main/shared/config/FAQ.config.ts rename to src/main/shared/config/faq/FAQ.en.ts index 0380c37..b84dd48 100644 --- a/src/main/shared/config/FAQ.config.ts +++ b/src/main/shared/config/faq/FAQ.en.ts @@ -1,14 +1,6 @@ -export interface FaqItem { - q: string; - a: string; -} -export interface FaqTopic { - id: string; - label: string; - items: FaqItem[]; -} +import { FaqTopic } from './_index'; -export const FAQ_TOPICS: FaqTopic[] = [ +export const FAQ_EN: FaqTopic[] = [ { id: 'general', label: 'General', @@ -53,7 +45,7 @@ export const FAQ_TOPICS: FaqTopic[] = [ }, { q: 'How can I quickly delete a profile?', - a: 'Right-click a profile and press Delete. Hold Shift while clicking Delete to skip the confirmation and remove it instantly. The same Shift shortcut works on the Profile tab\'s Delete button.', + a: "Right-click a profile and press Delete. Hold Shift while clicking Delete to skip the confirmation and remove it instantly. The same Shift shortcut works on the Profile tab's Delete button.", }, { q: 'How do I use "dynamic" JAR resolution?', @@ -91,7 +83,7 @@ export const FAQ_TOPICS: FaqTopic[] = [ }, { q: 'How do I open the working directory of a running process?', - a: 'Click the folder icon in the console toolbar. This opens the profile\'s working directory (or the JAR directory if none is set) in your system file explorer.', + a: "Click the folder icon in the console toolbar. This opens the profile's working directory (or the JAR directory if none is set) in your system file explorer.", }, { q: 'Why does console output look garbled with special characters?', diff --git a/src/main/shared/config/faq/_index.ts b/src/main/shared/config/faq/_index.ts new file mode 100644 index 0000000..d8f8c9c --- /dev/null +++ b/src/main/shared/config/faq/_index.ts @@ -0,0 +1,24 @@ +import { FAQ_DE } from './FAQ.de'; +import { FAQ_EN } from './FAQ.en'; + +export interface FaqItem { + q: string; + a: string; +} + +export interface FaqTopic { + id: string; + label: string; + items: FaqItem[]; +} + +export function getFAQ(languageId: string) { + switch (languageId) { + case 'en': + return FAQ_EN; + case 'de': + return FAQ_DE; + default: + return FAQ_EN; + } +} diff --git a/src/main/shared/types/UpdateCenter.types.ts b/src/main/shared/types/UpdateCenter.types.ts index b411cb7..718fbab 100644 --- a/src/main/shared/types/UpdateCenter.types.ts +++ b/src/main/shared/types/UpdateCenter.types.ts @@ -1,4 +1,11 @@ -export type UpdateStatus = 'idle' | 'checking' | 'up-to-date' | 'update-available' | 'updating' | 'done' | 'error'; +export type UpdateStatus = + | 'idle' + | 'checking' + | 'up-to-date' + | 'update-available' + | 'updating' + | 'done' + | 'error'; export interface UpdateCheckResult { hasUpdate: boolean; diff --git a/src/renderer/components/common/EnvVarList.tsx b/src/renderer/components/common/EnvVarList.tsx index 692d930..99b26bf 100644 --- a/src/renderer/components/common/EnvVarList.tsx +++ b/src/renderer/components/common/EnvVarList.tsx @@ -54,13 +54,8 @@ export function EnvVarList({ items, onChange, onPendingChange }: Props) { : 'border-surface-border/50 bg-base-900/30 opacity-60', ].join(' ')} > - handleToggle(i, v)} - /> - - {item.key} - + handleToggle(i, v)} /> + {item.key} = {item.value} diff --git a/src/renderer/components/console/ConsoleTab.tsx b/src/renderer/components/console/ConsoleTab.tsx index 88c026b..b2eb9ea 100644 --- a/src/renderer/components/console/ConsoleTab.tsx +++ b/src/renderer/components/console/ConsoleTab.tsx @@ -54,7 +54,7 @@ export function ConsoleTab() { const [searchQuery, setSearchQuery] = useState(''); const [searchIdx, setSearchIdx] = useState(0); const [lineCtxMenu, setLineCtxMenu] = useState<{ x: number; y: number; text: string } | null>( - null, + null ); const scrollRef = useRef(null); @@ -154,7 +154,7 @@ export function ConsoleTab() { if (!cmd || !running) return; await sendInput(profileId, cmd); setCmdHistory((prev) => - [cmd, ...prev.filter((c) => c !== cmd)].slice(0, settings?.consoleHistorySize ?? 200), + [cmd, ...prev.filter((c) => c !== cmd)].slice(0, settings?.consoleHistorySize ?? 200) ); setInputValue(''); setHistoryIdx(-1); @@ -190,7 +190,7 @@ export function ConsoleTab() { openSearch(); } }, - [handleSend, historyIdx, cmdHistory, clearConsole, profileId, openSearch], + [handleSend, historyIdx, cmdHistory, clearConsole, profileId, openSearch] ); useEffect(() => { @@ -475,7 +475,7 @@ const ConsoleLineRow = React.forwardRef< isAnyMatch, onContextMenu, }, - ref, + ref ) => { const text = line.text || ' '; const content = @@ -515,7 +515,7 @@ const ConsoleLineRow = React.forwardRef< ); - }, + } ); ConsoleLineRow.displayName = 'ConsoleLineRow'; @@ -538,7 +538,7 @@ function renderHighlighted(text: string, term: string, isCurrent: boolean): Reac } > {text.slice(idx, idx + term.length)} - , + ); last = idx + term.length; idx = lower.indexOf(term, last); diff --git a/src/renderer/components/developer/DevDiagnostics.tsx b/src/renderer/components/developer/DevDiagnostics.tsx index 386199c..222527d 100644 --- a/src/renderer/components/developer/DevDiagnostics.tsx +++ b/src/renderer/components/developer/DevDiagnostics.tsx @@ -106,10 +106,19 @@ export function DevDiagnostics() {
- - + + - +
diff --git a/src/renderer/components/developer/DevStorage.tsx b/src/renderer/components/developer/DevStorage.tsx index 7d8efd5..aff3ba0 100644 --- a/src/renderer/components/developer/DevStorage.tsx +++ b/src/renderer/components/developer/DevStorage.tsx @@ -167,10 +167,9 @@ function StoreRow({ label, value, mono }: { label: string; value: string; mono?:
{label} {value} diff --git a/src/renderer/components/faq/FaqPanel.tsx b/src/renderer/components/faq/FaqPanel.tsx index f1c0b09..37a9a22 100644 --- a/src/renderer/components/faq/FaqPanel.tsx +++ b/src/renderer/components/faq/FaqPanel.tsx @@ -1,24 +1,38 @@ -import React, { useMemo, useState } from 'react'; -import { FAQ_TOPICS } from '../../../main/shared/config/FAQ.config'; -import type { FaqItem } from '../../../main/shared/config/FAQ.config'; +import React, { useEffect, useMemo, useState } from 'react'; +import type { FaqItem } from '../../../main/shared/config/faq/_index'; import { SidebarLayout } from '../layout/SidebarLayout'; +import { FaqTopic, getFAQ } from '../../../main/shared/config/faq/_index'; export function FaqPanel() { + const [faqTopics, setFaqTopics] = useState(null); const [search, setSearch] = useState(''); - const [activeTopic, setActiveTopic] = useState(FAQ_TOPICS[0]?.id ?? ''); + const [activeTopic, setActiveTopic] = useState(''); const [expandedIdx, setExpandedIdx] = useState(null); const searchTrimmed = search.trim().toLowerCase(); + useEffect(() => { + window.api.getLanguageState().then((s) => { + const topics = getFAQ(s.activeLanguageId); + setFaqTopics(topics); + if (topics.length > 0) setActiveTopic(topics[0].id); + }); + }, []); + const searchResults = useMemo(() => { if (!searchTrimmed) return []; - return FAQ_TOPICS.flatMap((t) => t.items).filter( - (item) => - item.q.toLowerCase().includes(searchTrimmed) || item.a.toLowerCase().includes(searchTrimmed) + return ( + faqTopics + ?.flatMap((t) => t.items) + .filter( + (item) => + item.q.toLowerCase().includes(searchTrimmed) || + item.a.toLowerCase().includes(searchTrimmed) + ) ?? [] ); - }, [searchTrimmed]); + }, [searchTrimmed, faqTopics]); - const activeTopic_ = FAQ_TOPICS.find((t) => t.id === activeTopic) ?? FAQ_TOPICS[0]; + const activeTopic_ = faqTopics?.find((t) => t.id === activeTopic) ?? faqTopics?.[0]; const displayItems = searchTrimmed ? searchResults : (activeTopic_?.items ?? []); const handleTopicChange = (id: string) => { @@ -27,6 +41,8 @@ export function FaqPanel() { setSearch(''); }; + if (!faqTopics) return null; + return (
@@ -53,7 +69,7 @@ export function FaqPanel() {
) : ( @@ -83,19 +99,12 @@ function FaqList({ emptyLabel: string; }) { if (items.length === 0) { - return ( -

{emptyLabel}

- ); + return

{emptyLabel}

; } return ( <> {items.map((item, i) => ( - onToggle(i)} - /> + onToggle(i)} /> ))} ); diff --git a/src/renderer/components/logs/LogsTab.tsx b/src/renderer/components/logs/LogsTab.tsx index e661bbc..c259285 100644 --- a/src/renderer/components/logs/LogsTab.tsx +++ b/src/renderer/components/logs/LogsTab.tsx @@ -126,11 +126,7 @@ export function LogsTab() { ? 'bg-surface-raised border-r-2' : 'hover:bg-surface-raised/50', ].join(' ')} - style={ - selectedFile?.filePath === file.filePath - ? { borderRightColor: color } - : {} - } + style={selectedFile?.filePath === file.filePath ? { borderRightColor: color } : {}} >

{file.startedAt || file.filename} diff --git a/src/renderer/components/profiles/ConfigTab.tsx b/src/renderer/components/profiles/ConfigTab.tsx index 53b0f35..78951d6 100644 --- a/src/renderer/components/profiles/ConfigTab.tsx +++ b/src/renderer/components/profiles/ConfigTab.tsx @@ -362,7 +362,9 @@ function FilesSection({ return (

-

JAR Selection

+

+ JAR Selection +

)} - {running && ( - - )} + {running && } ); } diff --git a/src/renderer/components/profiles/ProfileTab.tsx b/src/renderer/components/profiles/ProfileTab.tsx index 86d7fb6..2041c26 100644 --- a/src/renderer/components/profiles/ProfileTab.tsx +++ b/src/renderer/components/profiles/ProfileTab.tsx @@ -79,9 +79,7 @@ export function ProfileTab() { className="w-7 h-7 rounded-full transition-all duration-150 hover:scale-110 cursor-pointer overflow-hidden border-2 border-dashed border-surface-border relative" style={{ backgroundColor: isCustomColor ? draft.color : 'transparent', - boxShadow: isCustomColor - ? `0 0 0 2px #08090d, 0 0 0 4px ${draft.color}` - : 'none', + boxShadow: isCustomColor ? `0 0 0 2px #08090d, 0 0 0 4px ${draft.color}` : 'none', transform: isCustomColor ? 'scale(1.15)' : undefined, }} title="Pick custom colour" @@ -106,8 +104,8 @@ export function ProfileTab() {

Delete profile

- Permanently removes this profile and all its configuration. - Hold Shift to skip confirmation. + Permanently removes this profile and all its configuration. Hold Shift to skip + confirmation.

{themeFetch === 'error' && ( -

- {t('appearance.fetchThemesFailed')} -

+

{t('appearance.fetchThemesFailed')}

)}
{allThemes.map((th) => ( @@ -125,9 +123,7 @@ export function AppearanceSection() {
{langFetch === 'error' && ( -

- {t('appearance.fetchLangsFailed')} -

+

{t('appearance.fetchLangsFailed')}

)}
{allLangs.map((l) => ( @@ -159,11 +155,7 @@ export function AppearanceSection() { onClick={handleDevSync} className="flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs font-mono text-text-muted hover:text-text-primary transition-colors" > - {devSynced ? ( - - ) : ( - - )} + {devSynced ? : } {devSynced ? t('appearance.synced') : t('appearance.sync')}
diff --git a/src/renderer/components/settings/sections/ConsoleSection.tsx b/src/renderer/components/settings/sections/ConsoleSection.tsx index 6f54b36..80997ba 100644 --- a/src/renderer/components/settings/sections/ConsoleSection.tsx +++ b/src/renderer/components/settings/sections/ConsoleSection.tsx @@ -36,10 +36,7 @@ export function ConsoleSection({ draft, set }: Props) { /> - set({ consoleTimestamps: v })} - /> + set({ consoleTimestamps: v })} /> set({ consoleWordWrap: v })} /> diff --git a/src/renderer/components/settings/sections/UpdatesSection.tsx b/src/renderer/components/settings/sections/UpdatesSection.tsx index 05089ef..919c8d0 100644 --- a/src/renderer/components/settings/sections/UpdatesSection.tsx +++ b/src/renderer/components/settings/sections/UpdatesSection.tsx @@ -1,6 +1,9 @@ import React, { useState, useCallback } from 'react'; import { useUpdateRegistry } from '../../../hooks/useUpdateRegistry'; -import type { UpdateStatus, UpdateCheckResult } from '../../../../main/shared/types/UpdateCenter.types'; +import type { + UpdateStatus, + UpdateCheckResult, +} from '../../../../main/shared/types/UpdateCenter.types'; import { Button } from '../../common/Button'; import { Section } from '../SettingsRow'; import { VscSync, VscCheck, VscWarning, VscCircleSlash, VscCloudDownload } from 'react-icons/vsc'; @@ -23,33 +26,39 @@ export function UpdatesSection() { setItems((prev) => ({ ...prev, [id]: { ...prev[id], ...patch } })); }; - const checkOne = useCallback(async (id: string) => { - const updatable = registry.find((u) => u.id === id); - if (!updatable) return; - updateItem(id, { status: 'checking' }); - try { - const result = await updatable.check(); - updateItem(id, { - status: result.hasUpdate ? 'update-available' : 'up-to-date', - result, - error: result.error, - }); - } catch (e) { - updateItem(id, { status: 'error', error: String(e) }); - } - }, [registry]); + const checkOne = useCallback( + async (id: string) => { + const updatable = registry.find((u) => u.id === id); + if (!updatable) return; + updateItem(id, { status: 'checking' }); + try { + const result = await updatable.check(); + updateItem(id, { + status: result.hasUpdate ? 'update-available' : 'up-to-date', + result, + error: result.error, + }); + } catch (e) { + updateItem(id, { status: 'error', error: String(e) }); + } + }, + [registry] + ); - const applyOne = useCallback(async (id: string) => { - const updatable = registry.find((u) => u.id === id); - if (!updatable) return; - updateItem(id, { status: 'updating' }); - try { - const res = await updatable.apply(); - updateItem(id, { status: res.ok ? 'done' : 'error', error: res.error }); - } catch (e) { - updateItem(id, { status: 'error', error: String(e) }); - } - }, [registry]); + const applyOne = useCallback( + async (id: string) => { + const updatable = registry.find((u) => u.id === id); + if (!updatable) return; + updateItem(id, { status: 'updating' }); + try { + const res = await updatable.apply(); + updateItem(id, { status: res.ok ? 'done' : 'error', error: res.error }); + } catch (e) { + updateItem(id, { status: 'error', error: String(e) }); + } + }, + [registry] + ); const checkAll = useCallback(async () => { setGlobalChecking(true); @@ -141,12 +150,16 @@ function UpdateItem({ return (
-
+
+ +

{label}

{status === 'up-to-date' && result && `v${result.currentVersion} -- latest`} - {status === 'update-available' && result && `v${result.currentVersion} -> v${result.remoteVersion}`} + {status === 'update-available' && + result && + `v${result.currentVersion} -> v${result.remoteVersion}`} {status === 'done' && 'Updated successfully'} {status === 'error' && (error ?? 'Check failed')} {status === 'idle' && description} diff --git a/src/renderer/config/UpdateCenter.config.ts b/src/renderer/config/UpdateCenter.config.ts index 7192c8d..7e370d3 100644 --- a/src/renderer/config/UpdateCenter.config.ts +++ b/src/renderer/config/UpdateCenter.config.ts @@ -16,7 +16,13 @@ const appUpdatable: Updatable = { description: 'Java Runner Client core application', check: async () => { const res = await window.api.fetchLatestRelease(); - if (!res.ok || !res.data) return { hasUpdate: false, currentVersion: version, remoteVersion: version, error: res.error }; + if (!res.ok || !res.data) + return { + hasUpdate: false, + currentVersion: version, + remoteVersion: version, + error: res.error, + }; const remote = (res.data.tag_name ?? '').replace(/^v/, ''); return { hasUpdate: semverGt(remote, version), currentVersion: version, remoteVersion: remote }; }, @@ -30,7 +36,11 @@ const themeUpdatable: Updatable = { check: async () => { const state = await window.api.getThemeState(); const res = await window.api.checkThemeUpdate(state.activeThemeId); - return { hasUpdate: res.hasUpdate, currentVersion: res.localVersion, remoteVersion: res.remoteVersion }; + return { + hasUpdate: res.hasUpdate, + currentVersion: res.localVersion, + remoteVersion: res.remoteVersion, + }; }, apply: async () => { const state = await window.api.getThemeState(); @@ -45,7 +55,11 @@ const languageUpdatable: Updatable = { check: async () => { const state = await window.api.getLanguageState(); const res = await window.api.checkLanguageUpdate(state.activeLanguageId); - return { hasUpdate: res.hasUpdate, currentVersion: res.localVersion, remoteVersion: res.remoteVersion }; + return { + hasUpdate: res.hasUpdate, + currentVersion: res.localVersion, + remoteVersion: res.remoteVersion, + }; }, apply: async () => { const state = await window.api.getLanguageState(); diff --git a/src/renderer/hooks/ThemeProvider.tsx b/src/renderer/hooks/ThemeProvider.tsx index 2f276aa..c16f2cf 100644 --- a/src/renderer/hooks/ThemeProvider.tsx +++ b/src/renderer/hooks/ThemeProvider.tsx @@ -50,7 +50,9 @@ function buildThemeCSS(colors: ThemeColors): string { lines.push(`.bg-${CSS.escape(twName)} { background-color: ${v} !important; }`); lines.push(`.text-${CSS.escape(twName)} { color: ${v} !important; }`); lines.push(`.border-${CSS.escape(twName)} { border-color: ${v} !important; }`); - lines.push(`.divide-${CSS.escape(twName)} > :not([hidden]) ~ :not([hidden]) { border-color: ${v} !important; }`); + lines.push( + `.divide-${CSS.escape(twName)} > :not([hidden]) ~ :not([hidden]) { border-color: ${v} !important; }` + ); } // Handle body background + color diff --git a/src/renderer/hooks/useUpdateRegistry.ts b/src/renderer/hooks/useUpdateRegistry.ts index 74f3830..05d4d85 100644 --- a/src/renderer/hooks/useUpdateRegistry.ts +++ b/src/renderer/hooks/useUpdateRegistry.ts @@ -28,9 +28,18 @@ const LOGIC: Record = { check: async () => { const res = await window.api.fetchLatestRelease(); if (!res.ok || !res.data) - return { hasUpdate: false, currentVersion: version, remoteVersion: version, error: res.error }; + return { + hasUpdate: false, + currentVersion: version, + remoteVersion: version, + error: res.error, + }; const remote = (res.data.tag_name ?? '').replace(/^v/, ''); - return { hasUpdate: semverGt(remote, version), currentVersion: version, remoteVersion: remote }; + return { + hasUpdate: semverGt(remote, version), + currentVersion: version, + remoteVersion: remote, + }; }, apply: async () => ({ ok: false, error: 'Use the release modal to download the installer' }), }, @@ -38,7 +47,11 @@ const LOGIC: Record = { check: async () => { const state = await window.api.getThemeState(); const res = await window.api.checkThemeUpdate(state.activeThemeId); - return { hasUpdate: res.hasUpdate, currentVersion: res.localVersion, remoteVersion: res.remoteVersion }; + return { + hasUpdate: res.hasUpdate, + currentVersion: res.localVersion, + remoteVersion: res.remoteVersion, + }; }, apply: async () => { const state = await window.api.getThemeState(); @@ -49,7 +62,11 @@ const LOGIC: Record = { check: async () => { const state = await window.api.getLanguageState(); const res = await window.api.checkLanguageUpdate(state.activeLanguageId); - return { hasUpdate: res.hasUpdate, currentVersion: res.localVersion, remoteVersion: res.remoteVersion }; + return { + hasUpdate: res.hasUpdate, + currentVersion: res.localVersion, + remoteVersion: res.remoteVersion, + }; }, apply: async () => { const state = await window.api.getLanguageState(); @@ -61,7 +78,9 @@ const LOGIC: Record = { export function useUpdateRegistry(): ResolvedUpdatable[] { return UPDATE_ITEMS.map((item) => ({ ...item, - check: LOGIC[item.id]?.check ?? (async () => ({ hasUpdate: false, currentVersion: '?', remoteVersion: '?' })), + check: + LOGIC[item.id]?.check ?? + (async () => ({ hasUpdate: false, currentVersion: '?', remoteVersion: '?' })), apply: LOGIC[item.id]?.apply ?? (async () => ({ ok: false, error: 'Not implemented' })), })); } diff --git a/src/renderer/i18n/I18nProvider.tsx b/src/renderer/i18n/I18nProvider.tsx index f769313..181dc7a 100644 --- a/src/renderer/i18n/I18nProvider.tsx +++ b/src/renderer/i18n/I18nProvider.tsx @@ -42,7 +42,7 @@ export function I18nProvider({ children }: { children: React.ReactNode }) { } return str; }, - [language], + [language] ); const setLanguage = useCallback(async (id: string) => { diff --git a/src/renderer/utils/ansi.ts b/src/renderer/utils/ansi.ts index db509b1..205ecaa 100644 --- a/src/renderer/utils/ansi.ts +++ b/src/renderer/utils/ansi.ts @@ -15,17 +15,41 @@ const ANSI_REGEX = /\x1b\[([0-9;]*)m/g; // Standard 8-color palette (30-37 fg, 40-47 bg) + bright variants (90-97, 100-107) const COLORS: Record = { - 30: '#4a4a4a', 31: '#f87171', 32: '#4ade80', 33: '#fbbf24', - 34: '#60a5fa', 35: '#c084fc', 36: '#34d399', 37: '#e8eaf2', - 90: '#6b7094', 91: '#fca5a5', 92: '#86efac', 93: '#fde68a', - 94: '#93c5fd', 95: '#d8b4fe', 96: '#6ee7b7', 97: '#ffffff', + 30: '#4a4a4a', + 31: '#f87171', + 32: '#4ade80', + 33: '#fbbf24', + 34: '#60a5fa', + 35: '#c084fc', + 36: '#34d399', + 37: '#e8eaf2', + 90: '#6b7094', + 91: '#fca5a5', + 92: '#86efac', + 93: '#fde68a', + 94: '#93c5fd', + 95: '#d8b4fe', + 96: '#6ee7b7', + 97: '#ffffff', }; const BG_COLORS: Record = { - 40: '#4a4a4a', 41: '#7f1d1d', 42: '#14532d', 43: '#78350f', - 44: '#1e3a5f', 45: '#581c87', 46: '#134e4a', 47: '#374151', - 100: '#374151', 101: '#991b1b', 102: '#166534', 103: '#92400e', - 104: '#1d4ed8', 105: '#7e22ce', 106: '#0f766e', 107: '#6b7280', + 40: '#4a4a4a', + 41: '#7f1d1d', + 42: '#14532d', + 43: '#78350f', + 44: '#1e3a5f', + 45: '#581c87', + 46: '#134e4a', + 47: '#374151', + 100: '#374151', + 101: '#991b1b', + 102: '#166534', + 103: '#92400e', + 104: '#1d4ed8', + 105: '#7e22ce', + 106: '#0f766e', + 107: '#6b7280', }; interface SgrState { @@ -46,19 +70,32 @@ function applyCodes(codes: number[], state: SgrState): SgrState { let i = 0; while (i < codes.length) { const c = codes[i]; - if (c === 0) { s = resetState(); } - else if (c === 1) { s.bold = true; } - else if (c === 2) { s.dim = true; } - else if (c === 3) { s.italic = true; } - else if (c === 4) { s.underline = true; } - else if (c === 22) { s.bold = false; s.dim = false; } - else if (c === 23) { s.italic = false; } - else if (c === 24) { s.underline = false; } - else if (c === 39) { s.color = undefined; } - else if (c === 49) { s.bgColor = undefined; } - else if (COLORS[c]) { s.color = COLORS[c]; } - else if (BG_COLORS[c]) { s.bgColor = BG_COLORS[c]; } - else if (c === 38 && codes[i + 1] === 5 && codes[i + 2] !== undefined) { + if (c === 0) { + s = resetState(); + } else if (c === 1) { + s.bold = true; + } else if (c === 2) { + s.dim = true; + } else if (c === 3) { + s.italic = true; + } else if (c === 4) { + s.underline = true; + } else if (c === 22) { + s.bold = false; + s.dim = false; + } else if (c === 23) { + s.italic = false; + } else if (c === 24) { + s.underline = false; + } else if (c === 39) { + s.color = undefined; + } else if (c === 49) { + s.bgColor = undefined; + } else if (COLORS[c]) { + s.color = COLORS[c]; + } else if (BG_COLORS[c]) { + s.bgColor = BG_COLORS[c]; + } else if (c === 38 && codes[i + 1] === 5 && codes[i + 2] !== undefined) { // 256-color fg: 38;5;n s.color = xterm256(codes[i + 2]); i += 2; @@ -106,7 +143,10 @@ export function parseAnsi(text: string): AnsiSpan[] { if (idx > last) { spans.push({ ...state, text: text.slice(last, idx) }); } - const codes = match[1].split(';').map(Number).filter((n) => !isNaN(n)); + const codes = match[1] + .split(';') + .map(Number) + .filter((n) => !isNaN(n)); state = applyCodes(codes.length ? codes : [0], state); last = idx + match[0].length; }