From 2e1115c05f6a234b3638e4894d94a3805cc92fc3 Mon Sep 17 00:00:00 2001
From: Timon Home
Date: Sat, 21 Mar 2026 15:03:17 +0100
Subject: [PATCH 1/4] Reworking Types, Config and Environment
---
package-lock.json | 4 +-
package.json | 2 +-
src/main/JRCEnvironment.ts | 43 ++++
src/main/RestAPI.routes.ts | 131 +++++++++++
src/main/RestAPI.ts | 206 +++--------------
src/main/ipc/Dev.ipc.ts | 39 ++++
src/main/ipc/Environment.ipc.ts | 41 ++++
src/main/ipc/GitHub.ipc.ts | 2 +-
src/main/ipc/Process.ipc.ts | 3 +-
src/main/ipc/Profile.ipc.ts | 2 +-
src/main/ipc/System.ipc.ts | 5 +-
src/main/ipc/_index.ts | 15 +-
src/main/main.ts | 67 ++++--
src/main/preload.ts | 5 +-
src/main/processManager.ts | 18 +-
src/main/shared/config/API.config.ts | 85 +++++++
src/main/shared/config/App.config.ts | 17 ++
src/main/shared/types.ts | 111 ---------
src/main/shared/types/App.types.ts | 21 ++
src/main/shared/{ => types}/GitHub.types.ts | 0
src/main/shared/types/Process.types.ts | 39 ++++
src/main/shared/types/Profile.types.ts | 31 +++
src/main/store.ts | 26 +--
src/renderer/App.tsx | 9 +-
src/renderer/components/MainLayout.tsx | 32 ++-
.../components/console/ConsoleTab.tsx | 5 +-
.../components/developer/DevApiExplorer.tsx | 216 ++++++++++++++++++
.../components/developer/DevDashboard.tsx | 164 +++++++++++++
.../components/developer/DevDiagnostics.tsx | 170 ++++++++++++++
.../components/developer/DevModeGate.tsx | 79 +++++++
.../components/developer/DevStorage.tsx | 174 ++++++++++++++
.../components/developer/DeveloperTab.tsx | 57 +++++
src/renderer/components/faq/FaqPanel.tsx | 4 +-
.../components/profiles/ConfigTab.tsx | 6 +-
.../components/profiles/ProfileSidebar.tsx | 40 +++-
.../components/profiles/ProfileTab.tsx | 2 +-
.../components/profiles/TemplateModal.tsx | 2 +-
.../components/settings/SettingsTab.tsx | 64 +++++-
.../settings/version/ReleaseModal.tsx | 2 +-
.../settings/version/VersionChecker.tsx | 2 +-
.../components/utils/ActivityLogPanel.tsx | 2 +-
.../components/utils/ScannerPanel.tsx | 2 +-
.../components/utils/UtilitiesTab.tsx | 7 +-
src/renderer/global.d.ts | 9 +
src/renderer/hooks/useDevMode.ts | 27 +++
src/renderer/index.html | 2 +-
src/renderer/store/AppStore.tsx | 15 +-
src/renderer/store/appReducer.ts | 4 +-
src/renderer/store/sessionLogs.ts | 2 +-
src/renderer/types/index.ts | 19 --
50 files changed, 1610 insertions(+), 420 deletions(-)
create mode 100644 src/main/JRCEnvironment.ts
create mode 100644 src/main/RestAPI.routes.ts
create mode 100644 src/main/ipc/Dev.ipc.ts
create mode 100644 src/main/ipc/Environment.ipc.ts
create mode 100644 src/main/shared/config/API.config.ts
create mode 100644 src/main/shared/config/App.config.ts
delete mode 100644 src/main/shared/types.ts
create mode 100644 src/main/shared/types/App.types.ts
rename src/main/shared/{ => types}/GitHub.types.ts (100%)
create mode 100644 src/main/shared/types/Process.types.ts
create mode 100644 src/main/shared/types/Profile.types.ts
create mode 100644 src/renderer/components/developer/DevApiExplorer.tsx
create mode 100644 src/renderer/components/developer/DevDashboard.tsx
create mode 100644 src/renderer/components/developer/DevDiagnostics.tsx
create mode 100644 src/renderer/components/developer/DevModeGate.tsx
create mode 100644 src/renderer/components/developer/DevStorage.tsx
create mode 100644 src/renderer/components/developer/DeveloperTab.tsx
create mode 100644 src/renderer/global.d.ts
create mode 100644 src/renderer/hooks/useDevMode.ts
delete mode 100644 src/renderer/types/index.ts
diff --git a/package-lock.json b/package-lock.json
index e89fe35..d62714c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "java-runner-client",
- "version": "2.1.2",
+ "version": "2.1.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "java-runner-client",
- "version": "2.1.2",
+ "version": "2.1.4",
"dependencies": {
"electron-store": "^8.1.0",
"framer-motion": "^12.38.0",
diff --git a/package.json b/package.json
index 406ee74..dfafd76 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "java-runner-client",
- "version": "2.1.3",
+ "version": "2.1.4",
"description": "Run and manage Java processes with profiles, console I/O, and system tray support",
"main": "dist/main/main.js",
"scripts": {
diff --git a/src/main/JRCEnvironment.ts b/src/main/JRCEnvironment.ts
new file mode 100644
index 0000000..3d14f5c
--- /dev/null
+++ b/src/main/JRCEnvironment.ts
@@ -0,0 +1,43 @@
+import { app, BrowserWindow } from 'electron'
+import { JRCEnvironment } from './shared/types/App.types'
+import { getSettings } from './Store'
+import { EnvironmentIPC } from './ipc/Environment.ipc'
+
+
+let env: JRCEnvironment = {
+ isReady: false,
+ devMode: null as unknown as JRCEnvironment['devMode'],
+ type: null as unknown as JRCEnvironment['type'],
+ startUpSource: null as unknown as JRCEnvironment['startUpSource'],
+}
+
+export function loadEnvironment() {
+ env = {
+ isReady: true,
+ devMode: getSettings().devModeEnabled,
+ type: app.isPackaged ? 'prod' : 'dev',
+ startUpSource: detectStartupSource(),
+ }
+
+ broadcast();
+}
+
+export function getEnvironment() {
+ return env
+}
+
+function broadcast(channel: string = EnvironmentIPC.change.channel) {
+ BrowserWindow.getAllWindows().forEach((w) =>
+ w.webContents.send(channel, env)
+ )
+}
+
+function detectStartupSource(): JRCEnvironment['startUpSource'] {
+ if (!app.isPackaged) return 'development'
+
+ const login = app.getLoginItemSettings()
+
+ if (login.wasOpenedAtLogin || process.argv.includes('--autostart')) return 'withSystem'
+
+ return 'userRequest'
+}
diff --git a/src/main/RestAPI.routes.ts b/src/main/RestAPI.routes.ts
new file mode 100644
index 0000000..2c7853a
--- /dev/null
+++ b/src/main/RestAPI.routes.ts
@@ -0,0 +1,131 @@
+import { routeConfig, type RouteKey } from './shared/config/API.config'
+import { ok, err } from './RestAPI'
+import { getAllProfiles, saveProfile, deleteProfile, getSettings, saveSettings } from './Store'
+import { processManager } from './ProcessManager'
+import { v4 as uuidv4 } from 'uuid'
+import type http from 'http'
+import { Profile } from './shared/types/Profile.types'
+import { AppSettings } from './shared/types/App.types'
+
+// ─── Context ──────────────────────────────────────────────────────────────────
+
+type Params = Record
+
+export interface Context {
+ req: http.IncomingMessage
+ res: http.ServerResponse
+ params: Params
+ body: unknown
+}
+
+type RouteHandler = (ctx: Context) => void | Promise
+
+// ─── Typed route builder ──────────────────────────────────────────────────────
+
+type BuiltRoute = (typeof routeConfig)[K] & {
+ handler: RouteHandler
+}
+
+function defineRoute(key: K, handler: RouteHandler): BuiltRoute {
+ return {
+ ...routeConfig[key],
+ handler,
+ }
+}
+
+// ─── Routes ───────────────────────────────────────────────────────────────────
+
+export const routes = [
+ defineRoute('status', ({ res }) =>
+ ok(res, {
+ ok: true,
+ version: process.env.npm_package_version ?? 'unknown',
+ profiles: getAllProfiles().length,
+ running: processManager.getStates().filter((s) => s.running).length,
+ })
+ ),
+
+ defineRoute('profiles_list', ({ res }) => ok(res, getAllProfiles())),
+
+ defineRoute('profiles_get', ({ res, params }) => {
+ const p = getAllProfiles().find((p) => p.id === params.id)
+ console.log(p, params, getAllProfiles())
+ p ? ok(res, p) : err(res, 'Profile not found', 404)
+ }),
+
+ defineRoute('profiles_create', ({ res, body }) => {
+ const b = body as Partial
+
+ const p: Profile = {
+ id: uuidv4(),
+ name: b.name ?? 'New Profile',
+ jarPath: b.jarPath ?? '',
+ workingDir: b.workingDir ?? '',
+ jvmArgs: b.jvmArgs ?? [],
+ systemProperties: b.systemProperties ?? [],
+ programArgs: b.programArgs ?? [],
+ javaPath: b.javaPath ?? '',
+ autoStart: b.autoStart ?? false,
+ autoRestart: b.autoRestart ?? false,
+ autoRestartInterval: b.autoRestartInterval ?? 10,
+ color: b.color ?? '#4ade80',
+ createdAt: Date.now(),
+ updatedAt: Date.now(),
+ }
+
+ saveProfile(p)
+ ok(res, p, 201)
+ }),
+
+ defineRoute('profiles_update', ({ res, params, body }) => {
+ const existing = getAllProfiles().find((p) => p.id === params.id)
+ if (!existing) return err(res, 'Profile not found', 404)
+
+ const updated: Profile = {
+ ...existing,
+ ...(body as Partial),
+ id: params.id,
+ updatedAt: Date.now(),
+ }
+
+ saveProfile(updated)
+ processManager.updateProfileSnapshot(updated)
+ ok(res, updated)
+ }),
+
+ defineRoute('profiles_delete', ({ res, params }) => {
+ if (!getAllProfiles().find((p) => p.id === params.id)) return err(res, 'Profile not found', 404)
+
+ deleteProfile(params.id)
+ ok(res)
+ }),
+
+ defineRoute('processes_list', ({ res }) => ok(res, processManager.getStates())),
+
+ defineRoute('processes_log', ({ res }) => ok(res, processManager.getActivityLog())),
+
+ defineRoute('processes_start', ({ res, params }) => {
+ const p = getAllProfiles().find((p) => p.id === params.id)
+ if (!p) return err(res, 'Profile not found', 404)
+ ok(res, processManager.start(p))
+ }),
+
+ defineRoute('processes_stop', ({ res, params }) => ok(res, processManager.stop(params.id))),
+
+ defineRoute('processes_clear', ({ res, params }) => {
+ processManager.clearConsoleForProfile(params.id)
+ ok(res)
+ }),
+
+ defineRoute('settings_get', ({ res }) => ok(res, getSettings())),
+
+ defineRoute('settings_update', ({ res, body }) => {
+ const updated: AppSettings = {
+ ...getSettings(),
+ ...(body as Partial),
+ }
+
+ saveSettings(updated)
+ ok(res, updated)
+ }),
+]
diff --git a/src/main/RestAPI.ts b/src/main/RestAPI.ts
index a4dc53f..b90a3df 100644
--- a/src/main/RestAPI.ts
+++ b/src/main/RestAPI.ts
@@ -1,50 +1,48 @@
import http from 'http'
-import { v4 as uuidv4 } from 'uuid'
-import type { Profile, AppSettings } from './shared/types'
-import { getAllProfiles, saveProfile, deleteProfile, getSettings, saveSettings } from './Store'
-import { processManager } from './ProcessManager'
+import { routes } from './RestAPI.routes'
+import { routeConfig } from './shared/config/API.config'
+import { getSettings } from './Store'
import { REST_API_CONFIG } from './shared/config/RestApi.config'
-// ─── Primitives ───────────────────────────────────────────────────────────────
-
type Params = Record
-interface Context {
- req: http.IncomingMessage
- res: http.ServerResponse
- params: Params
- body: unknown
-}
-
-type RouteHandler = (ctx: Context) => void | Promise
+// ─── Types ────────────────────────────────────────────────────────────────────
-interface RestRoute {
+type CompiledRoute = {
method: string
path: string
- handler: RouteHandler
+ pattern: RegExp
+ keys: string[]
+ handler: (ctx: {
+ req: http.IncomingMessage
+ res: http.ServerResponse
+ params: Params
+ body: unknown
+ }) => void | Promise
}
// ─── Helpers ──────────────────────────────────────────────────────────────────
function json(res: http.ServerResponse, data: unknown, status = 200) {
- res.writeHead(status, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' })
+ res.writeHead(status, {
+ 'Content-Type': 'application/json',
+ 'Access-Control-Allow-Origin': '*',
+ })
res.end(JSON.stringify(data))
}
-function ok(res: http.ServerResponse, data: unknown = { ok: true }, status = 200) {
+export function ok(res: http.ServerResponse, data: unknown = { ok: true }, status = 200) {
json(res, data, status)
}
-function err(res: http.ServerResponse, msg: string, status = 400) {
+export function err(res: http.ServerResponse, msg: string, status = 400) {
json(res, { error: msg }, status)
}
-function readBody(req: http.IncomingMessage): Promise {
+async function readBody(req: http.IncomingMessage): Promise {
return new Promise((resolve) => {
let raw = ''
- req.on('data', (c) => {
- raw += c
- })
+ req.on('data', (c) => (raw += c))
req.on('end', () => {
try {
resolve(JSON.parse(raw))
@@ -55,7 +53,7 @@ function readBody(req: http.IncomingMessage): Promise {
})
}
-function parsePattern(path: string): { pattern: RegExp; keys: string[] } {
+function parsePattern(path: string) {
const keys: string[] = []
const src = path.replace(/:([a-zA-Z]+)/g, (_m, k) => {
keys.push(k)
@@ -64,154 +62,18 @@ function parsePattern(path: string): { pattern: RegExp; keys: string[] } {
return { pattern: new RegExp(`^${src}$`), keys }
}
-// ─── Route definitions ────────────────────────────────────────────────────────
-//
-// Add a new endpoint here — nothing else to touch.
-//
-// ctx.params → path params (e.g. :id)
-// ctx.body → parsed JSON body (POST/PUT/PATCH only, otherwise {})
-// Use ok() / err() / json() to respond.
-
-const routes: RestRoute[] = [
- // ── Status ─────────────────────────────────────────────────────────────────
-
- {
- method: 'GET',
- path: '/api/status',
- handler: ({ res }) =>
- ok(res, {
- ok: true,
- version: process.env.npm_package_version ?? 'unknown',
- profiles: getAllProfiles().length,
- running: processManager.getStates().filter((s) => s.running).length,
- }),
- },
-
- // ── Profiles ───────────────────────────────────────────────────────────────
-
- {
- method: 'GET',
- path: '/api/profiles',
- handler: ({ res }) => ok(res, getAllProfiles()),
- },
- {
- method: 'GET',
- path: '/api/profiles/:id',
- handler: ({ res, params }) => {
- const p = getAllProfiles().find((p) => p.id === params.id)
- p ? ok(res, p) : err(res, 'Profile not found', 404)
- },
- },
- {
- method: 'POST',
- path: '/api/profiles',
- handler: ({ res, body }) => {
- const b = body as Partial
- const p: Profile = {
- id: uuidv4(),
- name: b.name ?? 'New Profile',
- jarPath: b.jarPath ?? '',
- workingDir: b.workingDir ?? '',
- jvmArgs: b.jvmArgs ?? [],
- systemProperties: b.systemProperties ?? [],
- programArgs: b.programArgs ?? [],
- javaPath: b.javaPath ?? '',
- autoStart: b.autoStart ?? false,
- autoRestart: b.autoRestart ?? false,
- autoRestartInterval: b.autoRestartInterval ?? 10,
- color: b.color ?? '#4ade80',
- createdAt: Date.now(),
- updatedAt: Date.now(),
- }
- saveProfile(p)
- ok(res, p, 201)
- },
- },
- {
- method: 'PUT',
- path: '/api/profiles/:id',
- handler: ({ res, params, body }) => {
- const existing = getAllProfiles().find((p) => p.id === params.id)
- if (!existing) return err(res, 'Profile not found', 404)
- const updated: Profile = {
- ...existing,
- ...(body as Partial),
- id: params.id,
- updatedAt: Date.now(),
- }
- saveProfile(updated)
- processManager.updateProfileSnapshot(updated)
- ok(res, updated)
- },
- },
- {
- method: 'DELETE',
- path: '/api/profiles/:id',
- handler: ({ res, params }) => {
- if (!getAllProfiles().find((p) => p.id === params.id))
- return err(res, 'Profile not found', 404)
- deleteProfile(params.id)
- ok(res)
- },
- },
-
- // ── Processes ──────────────────────────────────────────────────────────────
-
- {
- method: 'GET',
- path: '/api/processes',
- handler: ({ res }) => ok(res, processManager.getStates()),
- },
- {
- method: 'GET',
- path: '/api/processes/log',
- handler: ({ res }) => ok(res, processManager.getActivityLog()),
- },
- {
- method: 'POST',
- path: '/api/processes/:id/start',
- handler: ({ res, params }) => {
- const p = getAllProfiles().find((p) => p.id === params.id)
- if (!p) return err(res, 'Profile not found', 404)
- ok(res, processManager.start(p))
- },
- },
- {
- method: 'POST',
- path: '/api/processes/:id/stop',
- handler: ({ res, params }) => ok(res, processManager.stop(params.id)),
- },
- {
- method: 'POST',
- path: '/api/processes/:id/console/clear',
- handler: ({ res, params }) => {
- processManager.clearConsoleForProfile(params.id)
- ok(res)
- },
- },
-
- // ── Settings ───────────────────────────────────────────────────────────────
-
- {
- method: 'GET',
- path: '/api/settings',
- handler: ({ res }) => ok(res, getSettings()),
- },
- {
- method: 'PUT',
- path: '/api/settings',
- handler: ({ res, body }) => {
- const updated: AppSettings = { ...getSettings(), ...(body as Partial) }
- saveSettings(updated)
- ok(res, updated)
- },
- },
-]
+function compileRoutes(): CompiledRoute[] {
+ return routes.map((r) => ({
+ ...r,
+ ...parsePattern(r.path),
+ }))
+}
-const compiled = routes.map((r) => ({ ...r, ...parsePattern(r.path) }))
+// ─── Server ───────────────────────────────────────────────────────────────────
class RestApiServer {
private server: http.Server | null = null
+ private compiled = compileRoutes()
start(port: number): void {
if (this.server) return
@@ -228,16 +90,20 @@ class RestApiServer {
const url = req.url?.split('?')[0] ?? '/'
const method = req.method ?? 'GET'
- const body = ['POST', 'PUT', 'PATCH'].includes(method) ? await readBody(req) : {}
+ const body =
+ method === 'POST' || method === 'PUT' || method === 'PATCH' ? await readBody(req) : {}
- for (const route of compiled) {
+ for (const route of this.compiled) {
if (route.method !== method) continue
+
const match = url.match(route.pattern)
if (!match) continue
+
const params: Params = {}
route.keys.forEach((k, i) => {
params[k] = match[i + 1]
})
+
await route.handler({ req, res, params, body })
return
}
diff --git a/src/main/ipc/Dev.ipc.ts b/src/main/ipc/Dev.ipc.ts
new file mode 100644
index 0000000..54f9dbd
--- /dev/null
+++ b/src/main/ipc/Dev.ipc.ts
@@ -0,0 +1,39 @@
+import { BrowserWindow } from 'electron'
+import { DEFAULT_SETTINGS } from '../shared/config/App.config'
+import type { RouteMap } from '../shared/IPCController'
+import { getAllProfiles, getSettings, toggleDevMode } from '../Store'
+
+let getWindow: () => BrowserWindow | null = () => null
+
+export function initDevIPC(windowGetter: () => BrowserWindow | null) {
+ getWindow = windowGetter
+}
+
+export const DevIPC = {
+ getSysInfo: {
+ type: 'invoke',
+ channel: 'dev:getSysInfo',
+ handler: () => ({
+ platform: process.platform,
+ arch: process.arch,
+ nodeVersion: process.versions.node,
+ electronVersion: process.versions.electron,
+ chromeVersion: process.versions.chrome,
+ uptime: Math.floor(process.uptime()),
+ memoryUsageMB: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
+ }),
+ },
+
+ resetStore: {
+ type: 'invoke',
+ channel: 'dev:resetStore',
+ handler: async () => {
+ // Remove all profiles and reset settings to defaults
+ const profiles = getAllProfiles()
+ const Store = (await import('electron-store')).default
+ const store = new Store({ name: 'java-runner-config' })
+ store.set('profiles', [])
+ store.set('settings', DEFAULT_SETTINGS)
+ },
+ },
+} satisfies RouteMap
diff --git a/src/main/ipc/Environment.ipc.ts b/src/main/ipc/Environment.ipc.ts
new file mode 100644
index 0000000..aa058a1
--- /dev/null
+++ b/src/main/ipc/Environment.ipc.ts
@@ -0,0 +1,41 @@
+import { BrowserWindow } from 'electron'
+import { getEnvironment, loadEnvironment } from '../JRCEnvironment'
+import { RouteMap } from '../shared/IPCController'
+import { JRCEnvironment } from '../shared/types/App.types'
+import { toggleDevMode } from '../Store'
+
+export const EnvironmentIPC = {
+ get: {
+ type: 'invoke',
+ channel: 'env:get',
+ handler: () => getEnvironment(),
+ },
+
+ reload: {
+ type: 'send',
+ channel: 'env:reload',
+ handler: () => loadEnvironment(),
+ },
+
+ change: {
+ type: 'on',
+ channel: 'env:changed',
+ args: {} as (env: JRCEnvironment) => void,
+ },
+
+ toggleDevMode: {
+ type: 'invoke',
+ channel: 'env:toggleDevMode',
+ handler: (_e: Electron.IpcMainInvokeEvent, enabled: boolean) => {
+ const win = BrowserWindow.getAllWindows()[0]
+ if (!win) return
+
+ toggleDevMode(enabled)
+ loadEnvironment()
+
+ if (!enabled) {
+ win.webContents.closeDevTools()
+ }
+ },
+ },
+} satisfies RouteMap
diff --git a/src/main/ipc/GitHub.ipc.ts b/src/main/ipc/GitHub.ipc.ts
index 905e9ec..cf468fb 100644
--- a/src/main/ipc/GitHub.ipc.ts
+++ b/src/main/ipc/GitHub.ipc.ts
@@ -3,7 +3,7 @@ import https from 'https'
import { dialog, shell, BrowserWindow } from 'electron'
import type { RouteMap } from '../shared/IPCController'
import { latestReleaseUrl, templateListUrl, rawTemplateUrl } from '../shared/config/GitHub.config'
-import type { GitHubRelease, ProfileTemplate } from '../shared/GitHub.types'
+import type { GitHubRelease, ProfileTemplate } from '../shared/types/GitHub.types'
function httpsGet(url: string): Promise {
return new Promise((resolve, reject) => {
diff --git a/src/main/ipc/Process.ipc.ts b/src/main/ipc/Process.ipc.ts
index ebd6939..3f839c9 100644
--- a/src/main/ipc/Process.ipc.ts
+++ b/src/main/ipc/Process.ipc.ts
@@ -1,6 +1,7 @@
import type { RouteMap } from '../shared/IPCController'
import { processManager } from '../ProcessManager'
-import type { Profile, ProcessState, ConsoleLine } from '../shared/types'
+import { Profile } from '../shared/types/Profile.types'
+import { ConsoleLine, ProcessState } from '../shared/types/Process.types'
export const ProcessIPC = {
startProcess: {
diff --git a/src/main/ipc/Profile.ipc.ts b/src/main/ipc/Profile.ipc.ts
index 513081b..c6b46a5 100644
--- a/src/main/ipc/Profile.ipc.ts
+++ b/src/main/ipc/Profile.ipc.ts
@@ -1,7 +1,7 @@
import type { RouteMap } from '../shared/IPCController'
import { getAllProfiles, saveProfile, deleteProfile, reorderProfiles } from '../Store'
import { processManager } from '../ProcessManager'
-import type { Profile } from '../shared/types'
+import type { Profile } from '../shared/types/Profile.types'
export const ProfileIPC = {
getProfiles: { type: 'invoke', channel: 'profiles:getAll', handler: () => getAllProfiles() },
diff --git a/src/main/ipc/System.ipc.ts b/src/main/ipc/System.ipc.ts
index 1e4f002..8d70f6f 100644
--- a/src/main/ipc/System.ipc.ts
+++ b/src/main/ipc/System.ipc.ts
@@ -1,8 +1,9 @@
import { dialog, shell } from 'electron'
+import { restApiServer } from '../RestAPI'
import type { RouteMap } from '../shared/IPCController'
+import type { AppSettings, JRCEnvironment } from '../shared/types/App.types'
import { getSettings, saveSettings } from '../Store'
-import { restApiServer } from '../RestAPI'
-import type { AppSettings } from '../shared/types'
+import { getEnvironment } from './../JRCEnvironment'
// mainWindow is needed for dialogs — set via initSystemIPC() called from main.ts
let getWindow: () => Electron.BrowserWindow | null = () => null
diff --git a/src/main/ipc/_index.ts b/src/main/ipc/_index.ts
index bddd5f5..bfb7157 100644
--- a/src/main/ipc/_index.ts
+++ b/src/main/ipc/_index.ts
@@ -1,3 +1,4 @@
+import { EnvironmentIPC } from './Environment.ipc'
/**
* Central IPC registry.
*
@@ -12,17 +13,25 @@ export { ProcessIPC } from './Process.ipc'
export { ProfileIPC } from './Profile.ipc'
export { SystemIPC, initSystemIPC } from './System.ipc'
export { WindowIPC, initWindowIPC } from './Window.ipc'
+export { DevIPC, initDevIPC } from './Dev.ipc'
import { GitHubIPC } from './GitHub.ipc'
import { ProcessIPC } from './Process.ipc'
import { ProfileIPC } from './Profile.ipc'
import { SystemIPC } from './System.ipc'
import { WindowIPC } from './Window.ipc'
+import { DevIPC } from './Dev.ipc'
import type { InferAPI } from '../shared/IPCController'
-export const allRoutes = [GitHubIPC, ProcessIPC, ProfileIPC, SystemIPC, WindowIPC] as const
+export const allRoutes = [GitHubIPC, ProcessIPC, ProfileIPC, SystemIPC, WindowIPC, DevIPC] as const
-// The full inferred window.api type — used in global.d.ts
export type API = InferAPI<
- typeof GitHubIPC & typeof ProcessIPC & typeof ProfileIPC & typeof SystemIPC & typeof WindowIPC
+ typeof GitHubIPC &
+ typeof ProcessIPC &
+ typeof ProfileIPC &
+ typeof SystemIPC &
+ typeof WindowIPC &
+ typeof DevIPC
>
+
+export type Environment = InferAPI
diff --git a/src/main/main.ts b/src/main/main.ts
index 4aa275a..75799ae 100644
--- a/src/main/main.ts
+++ b/src/main/main.ts
@@ -1,23 +1,19 @@
-import { app, BrowserWindow, Tray, Menu, nativeImage } from 'electron'
-import path from 'path'
+import { app, BrowserWindow, Menu, nativeImage, Tray } from 'electron'
import fs from 'fs'
-import { getAllProfiles, getSettings } from './Store'
+import path from 'path'
+import { allRoutes, initDevIPC, initSystemIPC, initWindowIPC } from './ipc/_index'
+import { EnvironmentIPC } from './ipc/Environment.ipc'
+import { getEnvironment, loadEnvironment } from './JRCEnvironment'
import { processManager } from './ProcessManager'
import { restApiServer } from './RestAPI'
import { registerIPC } from './shared/IPCController'
-import { allRoutes, initSystemIPC, initWindowIPC } from './ipc/_index'
+import { getAllProfiles, getSettings } from './Store'
-const IS_DEV = !app.isPackaged
-const RESOURCES = IS_DEV
+loadEnvironment();
+
+const RESOURCES = getEnvironment().type === 'dev'
? path.join(__dirname, '../../resources')
: path.join(app.getAppPath(), 'resources')
-const isActiveLaunch =
- !process.argv.includes('--hidden') && !process.argv.includes('--squirrel-firstrun')
-const DEBUG = true
-
-const actualLog = console.log
-console.log =
- IS_DEV && DEBUG ? (...args) => actualLog(`[${new Date().toISOString()}]`, ...args) : () => {}
function getIconImage(): Electron.NativeImage {
const candidates =
@@ -47,27 +43,28 @@ function createWindow(): void {
frame: false,
backgroundColor: '#08090d',
icon: getIconImage(),
- show: IS_DEV ? true : false,
+ show: getEnvironment().startUpSource != "withSystem",
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
sandbox: false,
+ devTools: true,
},
})
- if (IS_DEV) mainWindow.loadURL('http://localhost:5173')
+ if (getEnvironment().type === 'dev') mainWindow.loadURL('http://localhost:5173')
else mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'))
mainWindow.once('ready-to-show', () => {
- const shouldStartHidden = getSettings().startMinimized && !IS_DEV && !isActiveLaunch
+ const shouldStartHidden = getSettings().startMinimized && getEnvironment().startUpSource === 'withSystem'
if (shouldStartHidden) mainWindow?.hide()
else mainWindow?.show()
})
mainWindow.on('close', (e) => {
if (forceQuit) return
- if (getSettings().minimizeToTray && !isActiveLaunch) {
+ if (getSettings().minimizeToTray) {
e.preventDefault()
mainWindow?.hide()
}
@@ -118,6 +115,9 @@ function updateTrayMenu(): void {
)
}
+let devToolsPressCount = 0
+let devToolsTimer: NodeJS.Timeout | null = null
+
const gotLock = app.requestSingleInstanceLock()
if (!gotLock) {
@@ -132,8 +132,39 @@ if (!gotLock) {
})
app.whenReady().then(() => {
+ if(getEnvironment().startUpSource === 'withSystem' && !getSettings().launchOnStartup) return;
+
createWindow()
createTray()
+ mainWindow?.webContents.on('before-input-event', (event, input) => {
+ const isDevToolsShortcut =
+ input.key === 'F12' ||
+ (input.control && input.shift && input.key === 'I') ||
+ (input.meta && input.alt && input.key === 'I')
+
+ if (!isDevToolsShortcut) return
+
+ event.preventDefault()
+
+ devToolsPressCount++
+
+ // reset counter after 1 second of inactivity
+ if (devToolsTimer) clearTimeout(devToolsTimer)
+ devToolsTimer = setTimeout(() => {
+ devToolsPressCount = 0
+ }, 1000)
+
+ if (devToolsPressCount >= 7) {
+ devToolsPressCount = 0
+ mainWindow?.webContents.openDevTools({ mode: 'detach' })
+ return
+ }
+
+ // normal single-press behavior only if devModeEnabled
+ if (getEnvironment().devMode) {
+ mainWindow?.webContents.openDevTools()
+ }
+ })
// ── IPC ────────────────────────────────────────────────────────────────────
initSystemIPC(() => mainWindow)
@@ -143,7 +174,9 @@ if (!gotLock) {
forceQuit = true
}
)
+ initDevIPC(() => mainWindow)
registerIPC([...allRoutes])
+ registerIPC([EnvironmentIPC])
// ──────────────────────────────────────────────────────────────────────────
const settings = getSettings()
diff --git a/src/main/preload.ts b/src/main/preload.ts
index 5eedd04..a74723e 100644
--- a/src/main/preload.ts
+++ b/src/main/preload.ts
@@ -1,5 +1,8 @@
import { contextBridge } from 'electron'
-import { buildPreloadAPI } from './shared/IPCController'
import { allRoutes } from './ipc/_index'
+import { EnvironmentIPC } from './ipc/Environment.ipc'
+import { buildPreloadAPI } from './shared/IPCController'
contextBridge.exposeInMainWorld('api', buildPreloadAPI([...allRoutes]))
+
+contextBridge.exposeInMainWorld('env', buildPreloadAPI([EnvironmentIPC]))
diff --git a/src/main/processManager.ts b/src/main/processManager.ts
index 14b7180..f3c87b0 100644
--- a/src/main/processManager.ts
+++ b/src/main/processManager.ts
@@ -2,15 +2,15 @@ import { spawn, execSync, ChildProcess } from 'child_process'
import { BrowserWindow } from 'electron'
import path from 'path'
import { v4 as uuidv4 } from 'uuid'
-import type {
- Profile,
- ProcessState,
+import { PROTECTED_PROCESS_NAMES } from './shared/config/Scanner.config'
+import {
ConsoleLine,
- ProcessLogEntry,
JavaProcessInfo,
-} from './shared/types'
-import { IPC } from './shared/types'
-import { PROTECTED_PROCESS_NAMES } from './shared/config/Scanner.config'
+ ProcessLogEntry,
+ ProcessState,
+} from './shared/types/Process.types'
+import { Profile } from './shared/types/Profile.types'
+import { ProcessIPC } from './ipc/Process.ipc'
const SELF_PROCESS_NAME = 'Java Client Runner'
@@ -216,7 +216,7 @@ class ProcessManager {
}
clearConsoleForProfile(profileId: string): void {
- this.window?.webContents.send(IPC.CONSOLE_CLEAR, profileId)
+ this.window?.webContents.send(ProcessIPC.consoleClear.channel, profileId)
}
private cancelRestartTimer(profileId: string): void {
@@ -454,7 +454,7 @@ class ProcessManager {
this.seenLineIds.set(profileId, new Set([id]))
}
}
- this.window?.webContents.send(IPC.CONSOLE_LINE, profileId, {
+ this.window?.webContents.send(ProcessIPC.consoleLine.channel, profileId, {
id,
text,
type,
diff --git a/src/main/shared/config/API.config.ts b/src/main/shared/config/API.config.ts
new file mode 100644
index 0000000..b445195
--- /dev/null
+++ b/src/main/shared/config/API.config.ts
@@ -0,0 +1,85 @@
+// ─── Base types ───────────────────────────────────────────────────────────────
+
+export type RouteDefinition = {
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE'
+ path: string
+ description: string
+ bodyTemplate?: string
+}
+
+// ─── Strongly typed route map ─────────────────────────────────────────────────
+
+export const routeConfig = {
+ status: {
+ method: 'GET',
+ path: '/api/status',
+ description: 'App status + version',
+ },
+
+ profiles_list: {
+ method: 'GET',
+ path: '/api/profiles',
+ description: 'List all profiles',
+ },
+ profiles_get: {
+ method: 'GET',
+ path: '/api/profiles/:id',
+ description: 'Get profile by ID',
+ },
+ profiles_create: {
+ method: 'POST',
+ path: '/api/profiles',
+ description: 'Create profile',
+ bodyTemplate: '{ "name": "New Profile", "jarPath": "" }',
+ },
+ profiles_update: {
+ method: 'PUT',
+ path: '/api/profiles/:id',
+ description: 'Update profile',
+ },
+ profiles_delete: {
+ method: 'DELETE',
+ path: '/api/profiles/:id',
+ description: 'Delete profile',
+ },
+
+ processes_list: {
+ method: 'GET',
+ path: '/api/processes',
+ description: 'Running process states',
+ },
+ processes_log: {
+ method: 'GET',
+ path: '/api/processes/log',
+ description: 'Activity log',
+ },
+ processes_start: {
+ method: 'POST',
+ path: '/api/processes/:id/start',
+ description: 'Start process',
+ },
+ processes_stop: {
+ method: 'POST',
+ path: '/api/processes/:id/stop',
+ description: 'Stop process',
+ },
+ processes_clear: {
+ method: 'POST',
+ path: '/api/processes/:id/console/clear',
+ description: 'Clear console',
+ },
+
+ settings_get: {
+ method: 'GET',
+ path: '/api/settings',
+ description: 'Get settings',
+ },
+ settings_update: {
+ method: 'PUT',
+ path: '/api/settings',
+ description: 'Update settings',
+ bodyTemplate: '{ "consoleFontSize": 13 }',
+ },
+} as const satisfies Record
+
+export type RouteKey = keyof typeof routeConfig
diff --git a/src/main/shared/config/App.config.ts b/src/main/shared/config/App.config.ts
new file mode 100644
index 0000000..afef533
--- /dev/null
+++ b/src/main/shared/config/App.config.ts
@@ -0,0 +1,17 @@
+import type { AppSettings } from '../types/App.types'
+import { REST_API_CONFIG } from './RestApi.config'
+
+export const DEFAULT_SETTINGS: AppSettings = {
+ launchOnStartup: false,
+ startMinimized: false,
+ minimizeToTray: true,
+ consoleFontSize: 13,
+ consoleMaxLines: 5000,
+ consoleWordWrap: false,
+ consoleLineNumbers: false,
+ consoleHistorySize: 200,
+ theme: 'dark',
+ restApiEnabled: false,
+ restApiPort: REST_API_CONFIG.defaultPort,
+ devModeEnabled: false,
+}
diff --git a/src/main/shared/types.ts b/src/main/shared/types.ts
deleted file mode 100644
index b746d5f..0000000
--- a/src/main/shared/types.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-export interface SystemProperty {
- key: string
- value: string
- enabled: boolean
-}
-export interface JvmArgument {
- value: string
- enabled: boolean
-}
-export interface ProgramArgument {
- value: string
- enabled: boolean
-}
-
-export interface Profile {
- id: string
- name: string
- jarPath: string
- workingDir: string
- jvmArgs: JvmArgument[]
- systemProperties: SystemProperty[]
- programArgs: ProgramArgument[]
- javaPath: string
- autoStart: boolean
- color: string
- createdAt: number
- updatedAt: number
- autoRestart: boolean
- autoRestartInterval: number
- order?: number
-}
-
-export interface AppSettings {
- launchOnStartup: boolean
- startMinimized: boolean
- minimizeToTray: boolean
- consoleFontSize: number
- consoleMaxLines: number
- consoleWordWrap: boolean
- consoleLineNumbers: boolean
- consoleHistorySize: number
- theme: 'dark'
- restApiEnabled: boolean
- restApiPort: number
-}
-
-export interface ConsoleLine {
- id: number
- text: string
- type: 'stdout' | 'stderr' | 'input' | 'system'
- timestamp: number
-}
-
-export interface ProcessState {
- profileId: string
- running: boolean
- pid?: number
- startedAt?: number
- exitCode?: number
-}
-
-export interface ProcessLogEntry {
- id: string
- profileId: string
- profileName: string
- jarPath: string
- pid: number
- startedAt: number
- stoppedAt?: number
- exitCode?: number
- signal?: string
-}
-
-export interface JavaProcessInfo {
- pid: number
- name: string
- command: string
- isJava: boolean
- managed: boolean
- protected: boolean
- memoryMB?: number
- startTime?: string
- threads?: number
- jarName?: string
-}
-
-export const IPC = {
- PROFILES_GET_ALL: 'profiles:getAll',
- PROFILES_SAVE: 'profiles:save',
- PROFILES_DELETE: 'profiles:delete',
- PROFILES_REORDER: 'profiles:reorder',
- PROCESS_START: 'process:start',
- PROCESS_STOP: 'process:stop',
- PROCESS_SEND_INPUT: 'process:sendInput',
- PROCESS_GET_STATES: 'process:getStates',
- PROCESS_GET_LOG: 'process:getLog',
- PROCESS_CLEAR_LOG: 'process:clearLog',
- PROCESS_SCAN_ALL: 'process:scanAll',
- PROCESS_KILL_PID: 'process:killPid',
- PROCESS_KILL_ALL_JAVA: 'process:killAllJava',
- CONSOLE_LINE: 'console:line',
- CONSOLE_CLEAR: 'console:clear',
- SETTINGS_GET: 'settings:get',
- SETTINGS_SAVE: 'settings:save',
- DIALOG_PICK_JAR: 'dialog:pickJar',
- DIALOG_PICK_DIR: 'dialog:pickDir',
- DIALOG_PICK_JAVA: 'dialog:pickJava',
- WINDOW_MINIMIZE: 'window:minimize',
- WINDOW_CLOSE: 'window:close',
- TRAY_SHOW: 'tray:show',
-} as const
diff --git a/src/main/shared/types/App.types.ts b/src/main/shared/types/App.types.ts
new file mode 100644
index 0000000..7244eb7
--- /dev/null
+++ b/src/main/shared/types/App.types.ts
@@ -0,0 +1,21 @@
+export interface AppSettings {
+ launchOnStartup: boolean
+ startMinimized: boolean
+ minimizeToTray: boolean
+ consoleFontSize: number
+ consoleMaxLines: number
+ consoleWordWrap: boolean
+ consoleLineNumbers: boolean
+ consoleHistorySize: number
+ theme: 'dark'
+ restApiEnabled: boolean
+ restApiPort: number
+ devModeEnabled: boolean
+}
+
+export interface JRCEnvironment {
+ isReady: boolean;
+ devMode: boolean;
+ type: "dev" | "prod";
+ startUpSource: 'userRequest' | 'withSystem' | 'development';
+}
diff --git a/src/main/shared/GitHub.types.ts b/src/main/shared/types/GitHub.types.ts
similarity index 100%
rename from src/main/shared/GitHub.types.ts
rename to src/main/shared/types/GitHub.types.ts
diff --git a/src/main/shared/types/Process.types.ts b/src/main/shared/types/Process.types.ts
new file mode 100644
index 0000000..a14e347
--- /dev/null
+++ b/src/main/shared/types/Process.types.ts
@@ -0,0 +1,39 @@
+export interface ProcessState {
+ profileId: string
+ running: boolean
+ pid?: number
+ startedAt?: number
+ exitCode?: number
+}
+
+export interface ProcessLogEntry {
+ id: string
+ profileId: string
+ profileName: string
+ jarPath: string
+ pid: number
+ startedAt: number
+ stoppedAt?: number
+ exitCode?: number
+ signal?: string
+}
+
+export interface JavaProcessInfo {
+ pid: number
+ name: string
+ command: string
+ isJava: boolean
+ managed: boolean
+ protected: boolean
+ memoryMB?: number
+ startTime?: string
+ threads?: number
+ jarName?: string
+}
+
+export interface ConsoleLine {
+ id: number
+ text: string
+ type: 'stdout' | 'stderr' | 'input' | 'system'
+ timestamp: number
+}
diff --git a/src/main/shared/types/Profile.types.ts b/src/main/shared/types/Profile.types.ts
new file mode 100644
index 0000000..70d4be6
--- /dev/null
+++ b/src/main/shared/types/Profile.types.ts
@@ -0,0 +1,31 @@
+export interface SystemProperty {
+ key: string
+ value: string
+ enabled: boolean
+}
+export interface JvmArgument {
+ value: string
+ enabled: boolean
+}
+export interface ProgramArgument {
+ value: string
+ enabled: boolean
+}
+
+export interface Profile {
+ id: string
+ name: string
+ jarPath: string
+ workingDir: string
+ jvmArgs: JvmArgument[]
+ systemProperties: SystemProperty[]
+ programArgs: ProgramArgument[]
+ javaPath: string
+ autoStart: boolean
+ color: string
+ createdAt: number
+ updatedAt: number
+ autoRestart: boolean
+ autoRestartInterval: number
+ order?: number
+}
diff --git a/src/main/store.ts b/src/main/store.ts
index eac34b3..8d18d46 100644
--- a/src/main/store.ts
+++ b/src/main/store.ts
@@ -1,26 +1,13 @@
import Store from 'electron-store'
-import type { Profile, AppSettings } from './shared/types'
-import { REST_API_CONFIG } from './shared/config/RestApi.config'
+import { DEFAULT_SETTINGS } from './shared/config/App.config'
+import { Profile } from './shared/types/Profile.types'
+import { AppSettings } from './shared/types/App.types'
interface StoreSchema {
profiles: Profile[]
settings: AppSettings
}
-const DEFAULT_SETTINGS: AppSettings = {
- launchOnStartup: false,
- startMinimized: false,
- minimizeToTray: true,
- consoleFontSize: 13,
- consoleMaxLines: 5000,
- consoleWordWrap: false,
- consoleLineNumbers: false,
- consoleHistorySize: 200,
- theme: 'dark',
- restApiEnabled: false,
- restApiPort: REST_API_CONFIG.defaultPort,
-}
-
const store = new Store({
name: 'java-runner-config',
defaults: { profiles: [], settings: DEFAULT_SETTINGS },
@@ -59,9 +46,16 @@ export function reorderProfiles(orderedIds: string[]): void {
store.set('profiles', updated)
}
+export function toggleDevMode(enabled: boolean): void {
+ const settings = getSettings()
+ settings.devModeEnabled = enabled
+ saveSettings(settings)
+}
+
export function getSettings(): AppSettings {
return { ...DEFAULT_SETTINGS, ...store.get('settings', DEFAULT_SETTINGS) }
}
+
export function saveSettings(settings: AppSettings): void {
store.set('settings', settings)
}
diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx
index 90982de..98f2113 100644
--- a/src/renderer/App.tsx
+++ b/src/renderer/App.tsx
@@ -1,13 +1,13 @@
-import React from 'react'
-import { HashRouter, Routes, Route, Navigate } from 'react-router-dom'
-import { AppProvider } from './store/AppStore'
+import { HashRouter, Navigate, Route, Routes } from 'react-router-dom'
import { TitleBar } from './components/common/TitleBar'
+import { DevModeGate } from './components/developer/DevModeGate'
import { MainLayout } from './components/MainLayout'
+import { AppProvider } from './store/AppStore'
function JavaRunnerFallback() {
return (
-
⚠️
+
!
App cannot run
The preload script failed to load, or you are trying to open this app in an incompatible
@@ -41,6 +41,7 @@ export default function App() {
} />
+
)
diff --git a/src/renderer/components/MainLayout.tsx b/src/renderer/components/MainLayout.tsx
index bb1c53e..51a6f02 100644
--- a/src/renderer/components/MainLayout.tsx
+++ b/src/renderer/components/MainLayout.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect } from 'react'
+import React, { useEffect, useState } from 'react'
import { Routes, Route, Navigate, useNavigate, useLocation } from 'react-router-dom'
import { ProfileSidebar } from './profiles/ProfileSidebar'
import { ConsoleTab } from './console/ConsoleTab'
@@ -7,9 +7,12 @@ import { ProfileTab } from './profiles/ProfileTab'
import { SettingsTab } from './settings/SettingsTab'
import { UtilitiesTab } from './utils/UtilitiesTab'
import { FaqPanel } from './faq/FaqPanel'
+import { DeveloperTab } from './developer/DeveloperTab'
import { useApp } from '../store/AppStore'
+import { useDevMode } from '../hooks/useDevMode'
import { VscTerminal, VscAccount } from 'react-icons/vsc'
import { LuList } from 'react-icons/lu'
+import { JRCEnvironment } from 'src/main/shared/types/App.types'
const MAIN_TABS = [
{ path: 'console', label: 'Console', Icon: VscTerminal },
@@ -17,15 +20,23 @@ const MAIN_TABS = [
{ path: 'profile', label: 'Profile', Icon: VscAccount },
] as const
-const SIDE_PANELS = ['settings', 'faq', 'utilities'] as const
+const SIDE_PANELS = ['settings', 'faq', 'utilities', 'developer'] as const
type SidePanel = (typeof SIDE_PANELS)[number]
function isSidePanel(seg: string): seg is SidePanel {
return (SIDE_PANELS as readonly string[]).includes(seg)
}
+const PANEL_LABELS: Record = {
+ settings: 'Application Settings',
+ faq: 'FAQ',
+ utilities: 'Utilities',
+ developer: 'Developer',
+}
+
export function MainLayout() {
const { state, activeProfile, isRunning, setActiveProfile } = useApp()
+ const devMode = useDevMode()
const navigate = useNavigate()
const location = useLocation()
@@ -37,6 +48,13 @@ export function MainLayout() {
const color = activeProfile?.color ?? '#4ade80'
const running = activeProfile ? isRunning(activeProfile.id) : false
+ // Redirect away from developer panel if dev mode is turned off
+ useEffect(() => {
+ if (!devMode && activePanel === 'developer') {
+ navigate('console', { replace: true })
+ }
+ }, [devMode, activePanel, navigate])
+
// When profile changes, go to console
const prevIdRef = React.useRef(state.activeProfileId)
useEffect(() => {
@@ -60,6 +78,7 @@ export function MainLayout() {
onOpenSettings={() => openPanel('settings')}
onOpenFaq={() => openPanel('faq')}
onOpenUtilities={() => openPanel('utilities')}
+ onOpenDeveloper={() => openPanel('developer')}
onProfileClick={handleProfileClick}
activeSidePanel={activePanel}
/>
@@ -67,7 +86,6 @@ export function MainLayout() {
{activePanel ? (
<>
- {/* Panel header with back navigation */}
- {
- { settings: 'Application Settings', faq: 'FAQ', utilities: 'Utilities' }[
- activePanel
- ]
- }
+ {PANEL_LABELS[activePanel]}
@@ -104,12 +118,12 @@ export function MainLayout() {
} />
} />
} />
+
} />
>
) : (
<>
- {/* Main tab bar */}
{MAIN_TABS.map((tab) => {
const isActive = activeTab === tab.path
diff --git a/src/renderer/components/console/ConsoleTab.tsx b/src/renderer/components/console/ConsoleTab.tsx
index 8841968..d6a4573 100644
--- a/src/renderer/components/console/ConsoleTab.tsx
+++ b/src/renderer/components/console/ConsoleTab.tsx
@@ -1,11 +1,8 @@
-/**
- * ConsoleTab — live output, stdin, history, Ctrl+L clear, Ctrl+F search.
- */
import React, { useRef, useEffect, useState, useCallback, useMemo, KeyboardEvent } from 'react'
import { useApp } from '../../store/AppStore'
import { Button } from '../common/Button'
import { VscSearch, VscChevronUp, VscChevronDown, VscClose } from 'react-icons/vsc'
-import type { ConsoleLine } from '../../types'
+import { ConsoleLine } from '../../../main/shared/types/Process.types'
export function ConsoleTab() {
const { state, activeProfile, startProcess, stopProcess, sendInput, clearConsole, isRunning } =
diff --git a/src/renderer/components/developer/DevApiExplorer.tsx b/src/renderer/components/developer/DevApiExplorer.tsx
new file mode 100644
index 0000000..78a8f1c
--- /dev/null
+++ b/src/renderer/components/developer/DevApiExplorer.tsx
@@ -0,0 +1,216 @@
+import { useState } from 'react'
+import { VscCheck, VscCopy, VscPlay } from 'react-icons/vsc'
+import { routeConfig, RouteDefinition } from '../../../main/shared/config/API.config'
+import { useApp } from '../../store/AppStore'
+import { Button } from '../common/Button'
+import { REST_API_CONFIG } from '../../../main/shared/config/RestApi.config'
+
+const METHOD_COLORS: Record
= {
+ GET: 'text-accent border-accent/30 bg-accent/10',
+ POST: 'text-blue-400 border-blue-400/30 bg-blue-400/10',
+ PUT: 'text-yellow-400 border-yellow-400/30 bg-yellow-400/10',
+ DELETE: 'text-red-400 border-red-400/30 bg-red-400/10',
+}
+
+const routes = Object.values(routeConfig)
+
+export function DevApiExplorer() {
+ const { state } = useApp()
+
+ const [selected, setSelected] = useState(null)
+ const [pathParams, setPathParams] = useState>({})
+ const [body, setBody] = useState('')
+ const [response, setResponse] = useState(null)
+ const [loading, setLoading] = useState(false)
+ const [copied, setCopied] = useState(false)
+
+ const port = state.settings?.restApiPort ?? 4444
+ const restEnabled = state.settings?.restApiEnabled ?? false
+
+ const handleSelect = (route: RouteDefinition) => {
+ setSelected(route)
+ setResponse(null)
+ setBody(route.bodyTemplate ?? '')
+
+ const params: Record = {}
+ const matches = route.path.matchAll(/:([a-zA-Z]+)/g)
+ for (const m of matches) params[m[1]] = ''
+ setPathParams(params)
+ }
+
+ const buildUrl = () => {
+ if (!selected) return ''
+ let path = selected.path
+ for (const [k, v] of Object.entries(pathParams)) {
+ path = path.replace(`:${k}`, v || `:${k}`)
+ }
+ return `http://${REST_API_CONFIG.host}:${port}${path}`
+ }
+
+ const handleCall = async () => {
+ if (!selected) return
+ setLoading(true)
+ setResponse(null)
+
+ try {
+ const url = buildUrl()
+ const opts: RequestInit = { method: selected.method }
+
+ if (body.trim() && ['POST', 'PUT', 'PATCH'].includes(selected.method)) {
+ opts.headers = { 'Content-Type': 'application/json' }
+ opts.body = body
+ }
+
+ const res = await fetch(url, opts)
+ const text = await res.text()
+
+ try {
+ setResponse(JSON.stringify(JSON.parse(text), null, 2))
+ } catch {
+ setResponse(text)
+ }
+ } catch (err) {
+ setResponse(`Error: ${err instanceof Error ? err.message : String(err)}`)
+ }
+
+ setLoading(false)
+ }
+
+ const copyUrl = () => {
+ navigator.clipboard.writeText(buildUrl())
+ setCopied(true)
+ setTimeout(() => setCopied(false), 1500)
+ }
+
+ return (
+
+ {/* Route list */}
+
+ {!restEnabled && (
+
+ REST API disabled in Settings
+
+ )}
+
+ {Object.entries(routeConfig).map(([key, route]) => (
+
handleSelect(route)}
+ className={[
+ 'w-full text-left px-3 py-2 transition-colors',
+ selected === route
+ ? 'bg-surface-raised border-r-2 border-accent'
+ : 'hover:bg-surface-raised/50',
+ ].join(' ')}
+ >
+
+
+ {route.method}
+
+
+
+ {route.path}
+ {route.description}
+
+ ))}
+
+
+ {/* Request + response */}
+
+ {!selected ? (
+
+ Select a route to inspect and call it
+
+ ) : (
+ <>
+ {/* URL bar */}
+
+
+
+ {selected.method}
+
+
+
+ {buildUrl()}
+
+
+
+ {copied ? : }
+
+
+
+
+ Send
+
+
+
+ {/* Path params */}
+ {Object.keys(pathParams).length > 0 && (
+
+ {Object.entries(pathParams).map(([k, v]) => (
+
+ :{k}
+ setPathParams((p) => ({ ...p, [k]: e.target.value }))}
+ placeholder="value"
+ className="w-32 bg-base-950 border border-surface-border rounded px-2 py-1 text-xs font-mono text-text-primary focus:outline-none focus:border-accent/40"
+ />
+
+ ))}
+
+ )}
+
+
+
+ {/* Body */}
+ {['POST', 'PUT', 'PATCH'].includes(selected.method) && (
+
+
+ Request Body (JSON)
+
+
+
+ )}
+
+ {/* Response */}
+
+
+ Response
+
+
+
+ {response ?? (
+
+ {loading ? 'Waiting...' : 'Press Send to call the API'}
+
+ )}
+
+
+
+ >
+ )}
+
+
+ )
+}
diff --git a/src/renderer/components/developer/DevDashboard.tsx b/src/renderer/components/developer/DevDashboard.tsx
new file mode 100644
index 0000000..46d1207
--- /dev/null
+++ b/src/renderer/components/developer/DevDashboard.tsx
@@ -0,0 +1,164 @@
+import React, { useEffect, useState } from 'react'
+import { useApp } from '../../store/AppStore'
+import { VscCircleFilled, VscCircle } from 'react-icons/vsc'
+
+declare const __APP_VERSION__: string
+
+interface SysInfo {
+ platform: string
+ arch: string
+ nodeVersion: string
+ electronVersion: string
+ chromeVersion: string
+ uptime: number
+ memoryUsageMB: number
+}
+
+function StatCard({
+ label,
+ value,
+ accent,
+ sub,
+}: {
+ label: string
+ value: string | number
+ accent?: boolean
+ sub?: string
+}) {
+ return (
+
+
{label}
+
+ {value}
+
+ {sub &&
{sub}
}
+
+ )
+}
+
+function Badge({ ok, label }: { ok: boolean; label: string }) {
+ return (
+
+ {ok ? : }
+ {label}
+
+ )
+}
+
+export function DevDashboard() {
+ const { state } = useApp()
+ const [sysInfo, setSysInfo] = useState(null)
+ const [tick, setTick] = useState(0)
+
+ useEffect(() => {
+ window.api.getSysInfo().then(setSysInfo)
+ const id = setInterval(() => {
+ window.api.getSysInfo().then(setSysInfo)
+ setTick((t) => t + 1)
+ }, 3000)
+ return () => clearInterval(id)
+ }, [])
+
+ const runningProfiles = state.processStates.filter((s) => s.running)
+ const totalConsoleLines = Object.values(state.consoleLogs).reduce((a, b) => a + b.length, 0)
+ const restEnabled = state.settings?.restApiEnabled ?? false
+
+ return (
+
+ {/* App state */}
+
+
+ {/* Feature flags */}
+
+
+ {/* Running processes */}
+
+ {runningProfiles.length === 0 ? (
+ No processes running
+ ) : (
+
+ {runningProfiles.map((s) => {
+ const profile = state.profiles.find((p) => p.id === s.profileId)
+ const uptimeSec = s.startedAt ? Math.floor((Date.now() - s.startedAt) / 1000) : 0
+ return (
+
+
+
+ {profile?.name ?? s.profileId}
+
+ PID {s.pid}
+ {uptimeSec}s
+
+ )
+ })}
+
+ )}
+
+
+ {/* System info */}
+
+ {sysInfo ? (
+
+
+
+
+
+
+ ) : (
+ Loading...
+ )}
+
+
+ )
+}
+
+function Section({ title, children }: { title: string; children: React.ReactNode }) {
+ return (
+
+ )
+}
diff --git a/src/renderer/components/developer/DevDiagnostics.tsx b/src/renderer/components/developer/DevDiagnostics.tsx
new file mode 100644
index 0000000..043b3de
--- /dev/null
+++ b/src/renderer/components/developer/DevDiagnostics.tsx
@@ -0,0 +1,170 @@
+import React, { useEffect, useRef, useState } from 'react'
+import { VscCheck, VscCopy } from 'react-icons/vsc'
+import { useApp } from '../../store/AppStore'
+import { Button } from '../common/Button'
+
+interface PerfSample {
+ timestamp: number
+ memMB: number
+}
+
+export function DevDiagnostics() {
+ const { state } = useApp()
+ const [perfSamples, setPerfSamples] = useState([])
+ const [copied, setCopied] = useState(false)
+ const [ipcLog, setIpcLog] = useState<{ ts: number; msg: string }[]>([])
+ const intervalRef = useRef | null>(null)
+
+ const samplePerf = () => {
+ if (window.performance && (performance as any).memory) {
+ const mem = (performance as any).memory
+ const mb = Math.round(mem.usedJSHeapSize / 1024 / 1024)
+ setPerfSamples((prev) => [...prev.slice(-29), { timestamp: Date.now(), memMB: mb }])
+ }
+ }
+
+ useEffect(() => {
+ samplePerf()
+ intervalRef.current = setInterval(samplePerf, 2000)
+ return () => {
+ if (intervalRef.current) clearInterval(intervalRef.current)
+ }
+ }, [])
+
+ const exportDiagReport = () => {
+ const report = {
+ timestamp: new Date().toISOString(),
+ appVersion: typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : 'unknown',
+ profiles: state.profiles.length,
+ runningProcesses: state.processStates.filter((s) => s.running).length,
+ settings: state.settings,
+ consoleLogCounts: Object.fromEntries(
+ Object.entries(state.consoleLogs).map(([id, lines]) => [id, lines.length])
+ ),
+ memorySnapshot: perfSamples[perfSamples.length - 1] ?? null,
+ }
+ const text = JSON.stringify(report, null, 2)
+ navigator.clipboard.writeText(text)
+ setCopied(true)
+ setTimeout(() => setCopied(false), 2000)
+ }
+
+ const latest = perfSamples[perfSamples.length - 1]
+ const maxMem = Math.max(...perfSamples.map((s) => s.memMB), 1)
+
+ return (
+
+ {/* Memory chart */}
+
+
+
+ {perfSamples.map((s, i) => {
+ const pct = (s.memMB / maxMem) * 100
+ return (
+
+ )
+ })}
+ {perfSamples.length === 0 && (
+
+ Collecting samples...
+
+ )}
+
+
+ {latest ? `${latest.memMB} MB used` : '—'}
+ max {maxMem} MB
+
+
+
+
+ {/* Renderer state summary */}
+
+
+
+
+
+
+ a + b.length, 0))}
+ />
+
+
+
+
+ {/* Profile detail */}
+
+ {state.profiles.length === 0 ? (
+ No profiles
+ ) : (
+
+ {state.profiles.map((p) => {
+ const lines = state.consoleLogs[p.id]?.length ?? 0
+ const running = state.processStates.some((s) => s.profileId === p.id && s.running)
+ return (
+
+
+
+ {p.name}
+
+
+ {lines.toLocaleString()} lines
+
+
+ )
+ })}
+
+ )}
+
+
+ {/* Export */}
+
+
+ Copy a JSON snapshot of the current app state to clipboard for bug reports.
+
+
+ {copied ? : }
+ {copied ? 'Copied!' : 'Copy Report to Clipboard'}
+
+
+
+ )
+}
+
+function Section({ title, children }: { title: string; children: React.ReactNode }) {
+ return (
+
+ )
+}
+
+function DiagRow({ label, value, mono }: { label: string; value: string; mono?: boolean }) {
+ return (
+
+ {label}
+
+ {value}
+
+
+ )
+}
+
+declare const __APP_VERSION__: string
diff --git a/src/renderer/components/developer/DevModeGate.tsx b/src/renderer/components/developer/DevModeGate.tsx
new file mode 100644
index 0000000..ae5ac46
--- /dev/null
+++ b/src/renderer/components/developer/DevModeGate.tsx
@@ -0,0 +1,79 @@
+import React, { useEffect, useState } from 'react'
+import { useDevMode } from '../../hooks/useDevMode'
+import { Button } from '../common/Button'
+import { VscCode } from 'react-icons/vsc'
+
+export function DevModeGate() {
+ const devEnabled = useDevMode()
+ const [dialogOpen, setDialogOpen] = useState(false)
+
+ useEffect(() => {
+ const keysPressed = new Set()
+
+ const keydownHandler = (e: KeyboardEvent) => {
+ keysPressed.add(e.code)
+
+ // right shift + 7
+ if (keysPressed.has('ShiftRight') && keysPressed.has('Digit7')) {
+ e.preventDefault()
+ setDialogOpen(true)
+ }
+ }
+
+ const keyupHandler = (e: KeyboardEvent) => {
+ keysPressed.delete(e.code)
+ }
+
+ window.addEventListener('keydown', keydownHandler)
+ window.addEventListener('keyup', keyupHandler)
+
+ return () => {
+ window.removeEventListener('keydown', keydownHandler)
+ window.removeEventListener('keyup', keyupHandler)
+ }
+ }, [])
+
+ if (!dialogOpen) return null
+
+ const isOn = devEnabled
+
+ return (
+
+
+
+
+
+
+
+
Developer Mode
+
+ {isOn ? 'Currently enabled' : 'Currently disabled'}
+
+
+
+
+
+ {isOn
+ ? 'Disable developer mode? This will hide the Developer panel and close DevTools.'
+ : 'Enable developer mode? This will show the Developer panel and enable DevTools for this session.'}
+
+
+
+ setDialogOpen(false)}>
+ Cancel
+
+ {
+ window.env.toggleDevMode(!isOn)
+ setDialogOpen(false)
+ }}
+ >
+ {isOn ? 'Disable' : 'Enable'}
+
+
+
+
+ )
+}
diff --git a/src/renderer/components/developer/DevStorage.tsx b/src/renderer/components/developer/DevStorage.tsx
new file mode 100644
index 0000000..b5cb345
--- /dev/null
+++ b/src/renderer/components/developer/DevStorage.tsx
@@ -0,0 +1,174 @@
+import React, { useState, useEffect } from 'react'
+import { Button } from '../common/Button'
+import { Dialog } from '../common/Dialog'
+import { useApp } from '../../store/AppStore'
+import { VscRefresh, VscTrash } from 'react-icons/vsc'
+
+interface SessionEntry {
+ key: string
+ sizeBytes: number
+ preview: string
+}
+
+function getSessionEntries(): SessionEntry[] {
+ const entries: SessionEntry[] = []
+ for (let i = 0; i < sessionStorage.length; i++) {
+ const key = sessionStorage.key(i)!
+ const raw = sessionStorage.getItem(key) ?? ''
+ entries.push({
+ key,
+ sizeBytes: new Blob([raw]).size,
+ preview: raw.slice(0, 80) + (raw.length > 80 ? '…' : ''),
+ })
+ }
+ return entries.sort((a, b) => b.sizeBytes - a.sizeBytes)
+}
+
+function formatBytes(n: number): string {
+ if (n < 1024) return `${n} B`
+ return `${(n / 1024).toFixed(1)} KB`
+}
+
+export function DevStorage() {
+ const { state } = useApp()
+ const [sessionEntries, setSessionEntries] = useState([])
+ const [confirmReset, setConfirmReset] = useState<'electron-store' | 'session' | null>(null)
+
+ const refresh = () => setSessionEntries(getSessionEntries())
+
+ useEffect(() => {
+ refresh()
+ }, [])
+
+ const totalSessionBytes = sessionEntries.reduce((a, b) => a + b.sizeBytes, 0)
+
+ return (
+
+ {/* Electron store */}
+
+
+
+
+
+
+
+ setConfirmReset('electron-store')}>
+
+ Reset Electron Store
+
+
+
+ {/* Session storage */}
+
+
+
{sessionEntries.length} keys
+
+
+
+
+ {sessionEntries.length === 0 ? (
+ Empty
+ ) : (
+
+ {sessionEntries.map((e) => (
+
+
+
{e.key}
+
{e.preview}
+
+
+
+ {formatBytes(e.sizeBytes)}
+
+ {
+ sessionStorage.removeItem(e.key)
+ refresh()
+ }}
+ className="text-text-muted hover:text-red-400 transition-colors"
+ >
+
+
+
+
+ ))}
+
+ )}
+ setConfirmReset('session')}
+ disabled={sessionEntries.length === 0}
+ >
+
+ Clear Session Storage
+
+
+
+
{
+ await window.api.resetStore()
+ setConfirmReset(null)
+ }}
+ onCancel={() => setConfirmReset(null)}
+ />
+
+ {
+ sessionStorage.clear()
+ refresh()
+ setConfirmReset(null)
+ }}
+ onCancel={() => setConfirmReset(null)}
+ />
+
+ )
+}
+
+function Section({ title, children }: { title: string; children: React.ReactNode }) {
+ return (
+
+ )
+}
+
+function StoreRow({ label, value, mono }: { label: string; value: string; mono?: boolean }) {
+ return (
+
+ {label}
+
+ {value}
+
+
+ )
+}
diff --git a/src/renderer/components/developer/DeveloperTab.tsx b/src/renderer/components/developer/DeveloperTab.tsx
new file mode 100644
index 0000000..51ea444
--- /dev/null
+++ b/src/renderer/components/developer/DeveloperTab.tsx
@@ -0,0 +1,57 @@
+import React, { useState } from 'react'
+import { VscDashboard, VscPlug, VscDatabase, VscBeaker } from 'react-icons/vsc'
+import { DevDashboard } from './DevDashboard'
+import { DevApiExplorer } from './DevApiExplorer'
+import { DevStorage } from './DevStorage'
+import { DevDiagnostics } from './DevDiagnostics'
+
+type Panel = 'dashboard' | 'api' | 'storage' | 'diagnostics'
+
+const PANELS: { id: Panel; label: string; Icon: React.ElementType }[] = [
+ { id: 'dashboard', label: 'Dashboard', Icon: VscDashboard },
+ { id: 'api', label: 'API Explorer', Icon: VscPlug },
+ { id: 'storage', label: 'Storage', Icon: VscDatabase },
+ { id: 'diagnostics', label: 'Diagnostics', Icon: VscBeaker },
+]
+
+export function DeveloperTab() {
+ const [panel, setPanel] = useState('dashboard')
+
+ return (
+
+ {/* Header banner */}
+
+
+
+ Developer Mode
+
+
+
+ {/* Sub-tab bar */}
+
+ {PANELS.map((p) => (
+
setPanel(p.id)}
+ className={[
+ 'flex items-center gap-1.5 px-3 py-2.5 text-xs font-mono border-b-2 -mb-px transition-colors',
+ panel === p.id
+ ? 'text-accent border-accent font-medium'
+ : 'text-text-muted border-transparent hover:text-text-primary',
+ ].join(' ')}
+ >
+
+ {p.label}
+
+ ))}
+
+
+
+ {panel === 'dashboard' && }
+ {panel === 'api' && }
+ {panel === 'storage' && }
+ {panel === 'diagnostics' && }
+
+
+ )
+}
diff --git a/src/renderer/components/faq/FaqPanel.tsx b/src/renderer/components/faq/FaqPanel.tsx
index 8c12d3d..d0320cd 100644
--- a/src/renderer/components/faq/FaqPanel.tsx
+++ b/src/renderer/components/faq/FaqPanel.tsx
@@ -1,6 +1,6 @@
-import React, { useState, useMemo } from 'react'
-import { FAQ_TOPICS } from '../../../main/shared/config/FAQ.config'
+import { useMemo, useState } from 'react'
import type { FaqItem, FaqTopic } from '../../../main/shared/config/FAQ.config'
+import { FAQ_TOPICS } from '../../../main/shared/config/FAQ.config'
export function FaqPanel() {
const [search, setSearch] = useState('')
diff --git a/src/renderer/components/profiles/ConfigTab.tsx b/src/renderer/components/profiles/ConfigTab.tsx
index 25b3b09..02a56b9 100644
--- a/src/renderer/components/profiles/ConfigTab.tsx
+++ b/src/renderer/components/profiles/ConfigTab.tsx
@@ -1,7 +1,3 @@
-/**
- * ConfigTab — per-profile configuration.
- * Sub-tabs: General | Files & Paths | JVM Args | Properties (-D) | Program Args
- */
import React, { useState, useEffect, useCallback, useMemo } from 'react'
import { useApp } from '../../store/AppStore'
import { Button } from '../common/Button'
@@ -10,7 +6,7 @@ import { Toggle } from '../common/Toggle'
import { ArgList } from '../common/ArgList'
import { PropList } from '../common/PropList'
import { Dialog } from '../common/Dialog'
-import type { Profile } from '../../types'
+import { Profile } from 'src/main/shared/types/Profile.types'
type Section = 'general' | 'files' | 'jvm' | 'props' | 'args'
diff --git a/src/renderer/components/profiles/ProfileSidebar.tsx b/src/renderer/components/profiles/ProfileSidebar.tsx
index 20f5bf1..2d50d81 100644
--- a/src/renderer/components/profiles/ProfileSidebar.tsx
+++ b/src/renderer/components/profiles/ProfileSidebar.tsx
@@ -4,8 +4,8 @@ import { useApp, PROFILE_COLORS } from '../../store/AppStore'
import { Dialog } from '../common/Dialog'
import { ContextMenu } from '../common/ContextMenu'
import { TemplateModal } from './TemplateModal'
+import { useDevMode } from '../../hooks/useDevMode'
import type { ContextMenuItem } from '../common/ContextMenu'
-import type { Profile } from '../../types'
import {
VscPlay,
VscDebugStop,
@@ -17,14 +17,17 @@ import {
VscTools,
VscAdd,
VscLayout,
+ VscCode,
} from 'react-icons/vsc'
+import { Profile } from '../../../main/shared/types/Profile.types'
interface Props {
onOpenSettings: () => void
onOpenFaq: () => void
onOpenUtilities: () => void
+ onOpenDeveloper: () => void
onProfileClick?: () => void
- activeSidePanel: 'settings' | 'faq' | 'utilities' | null
+ activeSidePanel: 'settings' | 'faq' | 'utilities' | 'developer' | null
}
interface CtxState {
@@ -37,6 +40,7 @@ export function ProfileSidebar({
onOpenSettings,
onOpenFaq,
onOpenUtilities,
+ onOpenDeveloper,
onProfileClick,
activeSidePanel,
}: Props) {
@@ -53,6 +57,7 @@ export function ProfileSidebar({
reorderProfiles,
} = useApp()
+ const devMode = useDevMode()
const [ctxMenu, setCtxMenu] = useState(null)
const [deleteTarget, setDeleteTarget] = useState(null)
const [actionError, setActionError] = useState(null)
@@ -126,12 +131,8 @@ export function ProfileSidebar({
disabled: !canDelete,
onClick: (e?: React.MouseEvent) => {
if (!canDelete) return
-
- if (e?.shiftKey) {
- deleteProfile(ctxProfile.id)
- } else {
- setDeleteTarget(ctxProfile)
- }
+ if (e?.shiftKey) deleteProfile(ctxProfile.id)
+ else setDeleteTarget(ctxProfile)
},
},
]
@@ -142,7 +143,7 @@ export function ProfileSidebar({
createProfile()}
className="w-full flex items-center gap-2 px-2.5 py-1.5 rounded-md text-xs font-mono
text-text-muted hover:text-accent hover:bg-surface-raised transition-colors border border-dashed border-surface-border hover:border-accent/40"
>
@@ -213,6 +214,15 @@ export function ProfileSidebar({
onClick={onOpenSettings}
icon={ }
/>
+ {devMode && (
+ }
+ accent
+ />
+ )}
@@ -275,7 +285,7 @@ function ProfileItem({
onClick={onClick}
onContextMenu={onContextMenu}
onMouseDown={(e) => {
- if (e.detail !== 0) e.preventDefault() // ignore keyboard-triggered clicks
+ if (e.detail !== 0) e.preventDefault()
}}
className={[
'w-full flex items-center gap-2.5 px-2.5 py-2 rounded-md text-left transition-colors',
@@ -317,11 +327,13 @@ function FooterButton({
active,
onClick,
icon,
+ accent,
}: {
label: string
active: boolean
onClick: () => void
icon: React.ReactNode
+ accent?: boolean
}) {
return (
{icon}
diff --git a/src/renderer/components/profiles/ProfileTab.tsx b/src/renderer/components/profiles/ProfileTab.tsx
index af295b5..095fee1 100644
--- a/src/renderer/components/profiles/ProfileTab.tsx
+++ b/src/renderer/components/profiles/ProfileTab.tsx
@@ -3,7 +3,7 @@ import { useApp, PROFILE_COLORS } from '../../store/AppStore'
import { Button } from '../common/Button'
import { Input } from '../common/Input'
import { Dialog } from '../common/Dialog'
-import type { Profile } from '../../types'
+import { Profile } from '../../../main/shared/types/Profile.types'
export function ProfileTab() {
const { activeProfile, saveProfile, deleteProfile } = useApp()
diff --git a/src/renderer/components/profiles/TemplateModal.tsx b/src/renderer/components/profiles/TemplateModal.tsx
index d20cd67..14c5662 100644
--- a/src/renderer/components/profiles/TemplateModal.tsx
+++ b/src/renderer/components/profiles/TemplateModal.tsx
@@ -2,9 +2,9 @@ import React, { useState, useEffect } from 'react'
import { Modal } from '../common/Modal'
import { Button } from '../common/Button'
import { useApp } from '../../store/AppStore'
-import type { ProfileTemplate } from '../../types'
import { VscPackage, VscTag, VscRefresh, VscAdd } from 'react-icons/vsc'
import { LuShield } from 'react-icons/lu'
+import { ProfileTemplate } from '../../../main/shared/types/GitHub.types'
const APP_TEMPLATE_VERSION = 1
diff --git a/src/renderer/components/settings/SettingsTab.tsx b/src/renderer/components/settings/SettingsTab.tsx
index 5ced91e..37661e9 100644
--- a/src/renderer/components/settings/SettingsTab.tsx
+++ b/src/renderer/components/settings/SettingsTab.tsx
@@ -3,19 +3,61 @@ import { useApp } from '../../store/AppStore'
import { Button } from '../common/Button'
import { Toggle } from '../common/Toggle'
import { VersionChecker } from './version/VersionChecker'
-import type { AppSettings } from '../../types'
import { REST_API_CONFIG } from '../../../main/shared/config/RestApi.config'
import { version } from '../../../../package.json'
+import { AppSettings, JRCEnvironment } from '../../../main/shared/types/App.types'
export function SettingsTab() {
const { state, saveSettings } = useApp()
const [draft, setDraft] = useState(null)
const [saved, setSaved] = useState(false)
+ const set = (patch: Partial) => {
+ setSaved(false)
+ setDraft((prev) => (prev ? { ...prev, ...patch } : prev))
+ }
+
useEffect(() => {
- if (state.settings) setDraft({ ...state.settings })
+ if (!state.settings) return
+
+ setDraft((prev) => {
+ if (!prev) return state.settings
+
+ // keep user changes, but refresh from store
+ return {
+ ...state.settings,
+ ...prev,
+ devModeEnabled: prev.devModeEnabled, // external wins
+ }
+ })
}, [state.settings])
+ useEffect(() => {
+ const listener = async (e: JRCEnvironment) => {
+ setSaved(false)
+
+ setDraft((prev) => {
+ if (!prev) return prev
+ if (prev.devModeEnabled === e.devMode) return prev
+
+ return {
+ ...prev,
+ devModeEnabled: e.devMode,
+ }
+ })
+
+ // 🔥 sync to store so isDirty stays correct
+ if (state.settings && state.settings.devModeEnabled !== e.devMode) {
+ await saveSettings({
+ ...state.settings,
+ devModeEnabled: e.devMode,
+ })
+ }
+ }
+
+ window.env.onChange(listener)
+ }, [state.settings, saveSettings])
+
const isDirty = useMemo(() => {
if (!draft || !state.settings) return false
return JSON.stringify(draft) !== JSON.stringify(state.settings)
@@ -23,13 +65,10 @@ export function SettingsTab() {
if (!draft) return null
- const set = (patch: Partial) => {
- setSaved(false)
- setDraft((prev) => (prev ? { ...prev, ...patch } : prev))
- }
const handleSave = async () => {
await saveSettings(draft)
+ window.env.reload();
setSaved(true)
setTimeout(() => setSaved(false), 2000)
}
@@ -133,7 +172,16 @@ export function SettingsTab() {
-
+
+
+ set({ devModeEnabled: v })}
+ />
+
Listening on{' '}
- http://127.0.0.1:{draft.restApiPort}/api
+ http://{REST_API_CONFIG.host}:{draft.restApiPort}/api
Endpoints: /status · /profiles · /processes · /settings
diff --git a/src/renderer/components/settings/version/ReleaseModal.tsx b/src/renderer/components/settings/version/ReleaseModal.tsx
index 44a9572..294947f 100644
--- a/src/renderer/components/settings/version/ReleaseModal.tsx
+++ b/src/renderer/components/settings/version/ReleaseModal.tsx
@@ -1,7 +1,6 @@
import React, { useState, useEffect, useCallback } from 'react'
import { Modal } from '../../common/Modal'
import { Button } from '../../common/Button'
-import type { GitHubRelease, GitHubAsset } from '../../../types'
import {
VscPackage,
VscGithub,
@@ -14,6 +13,7 @@ import {
VscClose,
} from 'react-icons/vsc'
import { LuDownload, LuExternalLink, LuCheck, LuRotateCcw } from 'react-icons/lu'
+import { GitHubAsset, GitHubRelease } from '../../../../main/shared/types/GitHub.types'
interface Props {
release: GitHubRelease
diff --git a/src/renderer/components/settings/version/VersionChecker.tsx b/src/renderer/components/settings/version/VersionChecker.tsx
index 10657fc..6cf0dea 100644
--- a/src/renderer/components/settings/version/VersionChecker.tsx
+++ b/src/renderer/components/settings/version/VersionChecker.tsx
@@ -2,8 +2,8 @@ import React, { useState, useCallback } from 'react'
import { Button } from '../../common/Button'
import { Tooltip } from '../../common/Tooltip'
import { ReleaseModal } from './ReleaseModal'
-import type { GitHubRelease } from '../../../types'
import { VscCheck, VscWarning, VscSync, VscCircleSlash } from 'react-icons/vsc'
+import { GitHubRelease } from '../../../../main/shared/types/GitHub.types'
interface Props {
currentVersion: string
diff --git a/src/renderer/components/utils/ActivityLogPanel.tsx b/src/renderer/components/utils/ActivityLogPanel.tsx
index 5a17bd7..8f880bb 100644
--- a/src/renderer/components/utils/ActivityLogPanel.tsx
+++ b/src/renderer/components/utils/ActivityLogPanel.tsx
@@ -2,7 +2,7 @@ import React, { useState, useCallback } from 'react'
import { Button } from '../common/Button'
import { Dialog } from '../common/Dialog'
import { VscListUnordered } from 'react-icons/vsc'
-import type { ProcessLogEntry } from '../../types'
+import { ProcessLogEntry } from '../../../main/shared/types/Process.types'
export function ActivityLogPanel() {
const [entries, setEntries] = useState(null)
diff --git a/src/renderer/components/utils/ScannerPanel.tsx b/src/renderer/components/utils/ScannerPanel.tsx
index 38cdf06..9299c3a 100644
--- a/src/renderer/components/utils/ScannerPanel.tsx
+++ b/src/renderer/components/utils/ScannerPanel.tsx
@@ -3,7 +3,7 @@ import { Button } from '../common/Button'
import { Dialog } from '../common/Dialog'
import { VscCheck } from 'react-icons/vsc'
import { LuScanLine } from 'react-icons/lu'
-import type { JavaProcessInfo } from '../../types'
+import { JavaProcessInfo } from '../../../main/shared/types/Process.types'
interface KillIntent {
proc: JavaProcessInfo
diff --git a/src/renderer/components/utils/UtilitiesTab.tsx b/src/renderer/components/utils/UtilitiesTab.tsx
index 1221030..1dd08f5 100644
--- a/src/renderer/components/utils/UtilitiesTab.tsx
+++ b/src/renderer/components/utils/UtilitiesTab.tsx
@@ -1,14 +1,9 @@
-/**
- * UtilitiesTab — Activity Log + Process Scanner.
- * Process Scanner: expandable rows, protected processes grayed out,
- * excluded from "Kill All Java".
- */
import React, { useState, useCallback } from 'react'
import { Dialog } from '../common/Dialog'
import { Button } from '../common/Button'
import { VscCheck, VscListUnordered } from 'react-icons/vsc'
import { LuScanLine } from 'react-icons/lu'
-import type { ProcessLogEntry, JavaProcessInfo } from '../../types'
+import { JavaProcessInfo, ProcessLogEntry } from '../../../main/shared/types/Process.types'
type Panel = 'log' | 'scanner'
diff --git a/src/renderer/global.d.ts b/src/renderer/global.d.ts
new file mode 100644
index 0000000..c71f68a
--- /dev/null
+++ b/src/renderer/global.d.ts
@@ -0,0 +1,9 @@
+// API type is fully inferred from the route definitions
+import type { API, Environment } from '../main/ipc/_index'
+
+declare global {
+ interface Window {
+ api: API
+ env: Environment
+ }
+}
diff --git a/src/renderer/hooks/useDevMode.ts b/src/renderer/hooks/useDevMode.ts
new file mode 100644
index 0000000..81ad4e4
--- /dev/null
+++ b/src/renderer/hooks/useDevMode.ts
@@ -0,0 +1,27 @@
+import { useState, useEffect } from 'react'
+import { JRCEnvironment } from 'src/main/shared/types/App.types'
+
+export function useDevMode(): boolean {
+ const [enabled, setEnabled] = useState(false)
+
+ useEffect(() => {
+ let mounted = true
+
+ // fetch initial value
+ window.env.get().then((value) => {
+ if (mounted) setEnabled(value.devMode)
+ })
+
+ // subscribe to changes
+ const handler = (value: JRCEnvironment) => {
+ if (mounted) setEnabled(value.devMode)
+ }
+ window.env.onChange(handler)
+
+ return () => {
+ mounted = false
+ }
+ }, [])
+
+ return enabled
+}
diff --git a/src/renderer/index.html b/src/renderer/index.html
index 2b930ef..7e74d02 100644
--- a/src/renderer/index.html
+++ b/src/renderer/index.html
@@ -11,7 +11,7 @@
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
font-src 'self';
- connect-src 'self';
+ connect-src 'self' http://127.0.0.1:* http://localhost:*;
"
/>
Java Runner Client
diff --git a/src/renderer/store/AppStore.tsx b/src/renderer/store/AppStore.tsx
index 21bb094..cbc1ebe 100644
--- a/src/renderer/store/AppStore.tsx
+++ b/src/renderer/store/AppStore.tsx
@@ -6,8 +6,10 @@ import React, {
useCallback,
type ReactNode,
} from 'react'
+import { AppSettings } from '../../main/shared/types/App.types'
+import { ConsoleLine, ProcessState } from '../../main/shared/types/Process.types'
+import { Profile } from '../../main/shared/types/Profile.types'
import { v4 as uuidv4 } from 'uuid'
-import type { Profile, AppSettings, ProcessState, ConsoleLine } from '../types'
const SS_KEY = (id: string) => `jrc:console:${id}`
function loadLogs(id: string, max: number): ConsoleLine[] {
@@ -104,7 +106,7 @@ interface AppContextValue {
setActiveProfile: (id: string) => void
saveProfile: (p: Profile) => Promise
deleteProfile: (id: string) => Promise
- createProfile: () => void
+ createProfile: (overrides?: Partial) => void
reorderProfiles: (profiles: Profile[]) => Promise
startProcess: (p: Profile) => Promise<{ ok: boolean; error?: string }>
stopProcess: (id: string) => Promise<{ ok: boolean; error?: string }>
@@ -176,7 +178,7 @@ export function AppProvider({ children }: { children: ReactNode }) {
)
const createProfile = useCallback(
- (overrides: Partial = {}) => {
+ async (overrides: Partial = {}) => {
const p: Profile = {
id: uuidv4(),
name: overrides.name ?? 'New Profile',
@@ -208,10 +210,9 @@ export function AppProvider({ children }: { children: ReactNode }) {
const startProcess = useCallback((p: Profile) => window.api.startProcess(p), [])
const stopProcess = useCallback((id: string) => window.api.stopProcess(id), [])
- const sendInput = useCallback(
- (profileId: string, input: string) => window.api.sendInput(profileId, input),
- []
- )
+ const sendInput = useCallback(async (profileId: string, input: string) => {
+ await window.api.sendInput(profileId, input)
+ }, [])
const clearConsole = useCallback(
(profileId: string) => dispatch({ type: 'CLEAR_LOG', profileId }),
[]
diff --git a/src/renderer/store/appReducer.ts b/src/renderer/store/appReducer.ts
index 6287272..fe08527 100644
--- a/src/renderer/store/appReducer.ts
+++ b/src/renderer/store/appReducer.ts
@@ -1,5 +1,7 @@
-import type { Profile, AppSettings, ProcessState, ConsoleLine } from '../types'
+import { Profile } from '../../main/shared/types/Profile.types'
import { saveLogs, clearLogs } from './sessionLogs'
+import { ConsoleLine, ProcessState } from '../../main/shared/types/Process.types'
+import { AppSettings } from '../../main/shared/types/App.types'
export interface AppState {
profiles: Profile[]
diff --git a/src/renderer/store/sessionLogs.ts b/src/renderer/store/sessionLogs.ts
index c93a989..3d0a71b 100644
--- a/src/renderer/store/sessionLogs.ts
+++ b/src/renderer/store/sessionLogs.ts
@@ -1,4 +1,4 @@
-import type { ConsoleLine } from '../types'
+import { ConsoleLine } from '../../main/shared/types/Process.types'
const key = (id: string) => `jrc:console:${id}`
diff --git a/src/renderer/types/index.ts b/src/renderer/types/index.ts
deleted file mode 100644
index 8c3accd..0000000
--- a/src/renderer/types/index.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-export type {
- Profile,
- AppSettings,
- ConsoleLine,
- ProcessState,
- ProcessLogEntry,
- JavaProcessInfo,
-} from '../../main/shared/types'
-
-export type { GitHubRelease, GitHubAsset, ProfileTemplate } from '../../main/shared/GitHub.types'
-
-// API type is fully inferred from the route definitions — never write this by hand again.
-import type { API } from '../../main/ipc/_index'
-
-declare global {
- interface Window {
- api: API
- }
-}
From e08a0f74d6f787eb08c71b047851d3d0cce96a36 Mon Sep 17 00:00:00 2001
From: Timon Home
Date: Sat, 21 Mar 2026 15:21:39 +0100
Subject: [PATCH 2/4] Unexpose IPCController from renderer
---
src/main/{shared => }/IPCController.ts | 0
src/main/JRCEnvironment.ts | 7 +--
src/main/ipc/Dev.ipc.ts | 2 +-
src/main/ipc/Environment.ipc.ts | 56 +++++++++----------
src/main/ipc/GitHub.ipc.ts | 2 +-
src/main/ipc/Process.ipc.ts | 2 +-
src/main/ipc/Profile.ipc.ts | 2 +-
src/main/ipc/System.ipc.ts | 2 +-
src/main/ipc/Window.ipc.ts | 2 +-
src/main/ipc/_index.ts | 2 +-
src/main/main.ts | 18 +++---
src/main/preload.ts | 2 +-
src/main/shared/types/App.types.ts | 8 +--
.../components/settings/SettingsTab.tsx | 12 ++--
14 files changed, 57 insertions(+), 60 deletions(-)
rename src/main/{shared => }/IPCController.ts (100%)
diff --git a/src/main/shared/IPCController.ts b/src/main/IPCController.ts
similarity index 100%
rename from src/main/shared/IPCController.ts
rename to src/main/IPCController.ts
diff --git a/src/main/JRCEnvironment.ts b/src/main/JRCEnvironment.ts
index 3d14f5c..65482e7 100644
--- a/src/main/JRCEnvironment.ts
+++ b/src/main/JRCEnvironment.ts
@@ -3,7 +3,6 @@ import { JRCEnvironment } from './shared/types/App.types'
import { getSettings } from './Store'
import { EnvironmentIPC } from './ipc/Environment.ipc'
-
let env: JRCEnvironment = {
isReady: false,
devMode: null as unknown as JRCEnvironment['devMode'],
@@ -19,7 +18,7 @@ export function loadEnvironment() {
startUpSource: detectStartupSource(),
}
- broadcast();
+ broadcast()
}
export function getEnvironment() {
@@ -27,9 +26,7 @@ export function getEnvironment() {
}
function broadcast(channel: string = EnvironmentIPC.change.channel) {
- BrowserWindow.getAllWindows().forEach((w) =>
- w.webContents.send(channel, env)
- )
+ BrowserWindow.getAllWindows().forEach((w) => w.webContents.send(channel, env))
}
function detectStartupSource(): JRCEnvironment['startUpSource'] {
diff --git a/src/main/ipc/Dev.ipc.ts b/src/main/ipc/Dev.ipc.ts
index 54f9dbd..a24c06f 100644
--- a/src/main/ipc/Dev.ipc.ts
+++ b/src/main/ipc/Dev.ipc.ts
@@ -1,6 +1,6 @@
import { BrowserWindow } from 'electron'
import { DEFAULT_SETTINGS } from '../shared/config/App.config'
-import type { RouteMap } from '../shared/IPCController'
+import type { RouteMap } from '../IPCController'
import { getAllProfiles, getSettings, toggleDevMode } from '../Store'
let getWindow: () => BrowserWindow | null = () => null
diff --git a/src/main/ipc/Environment.ipc.ts b/src/main/ipc/Environment.ipc.ts
index aa058a1..97e473f 100644
--- a/src/main/ipc/Environment.ipc.ts
+++ b/src/main/ipc/Environment.ipc.ts
@@ -1,41 +1,41 @@
import { BrowserWindow } from 'electron'
import { getEnvironment, loadEnvironment } from '../JRCEnvironment'
-import { RouteMap } from '../shared/IPCController'
+import { RouteMap } from '../IPCController'
import { JRCEnvironment } from '../shared/types/App.types'
import { toggleDevMode } from '../Store'
export const EnvironmentIPC = {
- get: {
- type: 'invoke',
- channel: 'env:get',
- handler: () => getEnvironment(),
- },
+ get: {
+ type: 'invoke',
+ channel: 'env:get',
+ handler: () => getEnvironment(),
+ },
- reload: {
- type: 'send',
- channel: 'env:reload',
- handler: () => loadEnvironment(),
- },
+ reload: {
+ type: 'send',
+ channel: 'env:reload',
+ handler: () => loadEnvironment(),
+ },
- change: {
- type: 'on',
- channel: 'env:changed',
- args: {} as (env: JRCEnvironment) => void,
- },
+ change: {
+ type: 'on',
+ channel: 'env:changed',
+ args: {} as (env: JRCEnvironment) => void,
+ },
- toggleDevMode: {
- type: 'invoke',
- channel: 'env:toggleDevMode',
- handler: (_e: Electron.IpcMainInvokeEvent, enabled: boolean) => {
- const win = BrowserWindow.getAllWindows()[0]
- if (!win) return
+ toggleDevMode: {
+ type: 'invoke',
+ channel: 'env:toggleDevMode',
+ handler: (_e: Electron.IpcMainInvokeEvent, enabled: boolean) => {
+ const win = BrowserWindow.getAllWindows()[0]
+ if (!win) return
- toggleDevMode(enabled)
- loadEnvironment()
+ toggleDevMode(enabled)
+ loadEnvironment()
- if (!enabled) {
- win.webContents.closeDevTools()
- }
- },
+ if (!enabled) {
+ win.webContents.closeDevTools()
+ }
},
+ },
} satisfies RouteMap
diff --git a/src/main/ipc/GitHub.ipc.ts b/src/main/ipc/GitHub.ipc.ts
index cf468fb..2c439ec 100644
--- a/src/main/ipc/GitHub.ipc.ts
+++ b/src/main/ipc/GitHub.ipc.ts
@@ -1,7 +1,7 @@
import fs from 'fs'
import https from 'https'
import { dialog, shell, BrowserWindow } from 'electron'
-import type { RouteMap } from '../shared/IPCController'
+import type { RouteMap } from '../IPCController'
import { latestReleaseUrl, templateListUrl, rawTemplateUrl } from '../shared/config/GitHub.config'
import type { GitHubRelease, ProfileTemplate } from '../shared/types/GitHub.types'
diff --git a/src/main/ipc/Process.ipc.ts b/src/main/ipc/Process.ipc.ts
index 3f839c9..7edeeec 100644
--- a/src/main/ipc/Process.ipc.ts
+++ b/src/main/ipc/Process.ipc.ts
@@ -1,4 +1,4 @@
-import type { RouteMap } from '../shared/IPCController'
+import type { RouteMap } from '../IPCController'
import { processManager } from '../ProcessManager'
import { Profile } from '../shared/types/Profile.types'
import { ConsoleLine, ProcessState } from '../shared/types/Process.types'
diff --git a/src/main/ipc/Profile.ipc.ts b/src/main/ipc/Profile.ipc.ts
index c6b46a5..6f0d9ee 100644
--- a/src/main/ipc/Profile.ipc.ts
+++ b/src/main/ipc/Profile.ipc.ts
@@ -1,4 +1,4 @@
-import type { RouteMap } from '../shared/IPCController'
+import type { RouteMap } from '../IPCController'
import { getAllProfiles, saveProfile, deleteProfile, reorderProfiles } from '../Store'
import { processManager } from '../ProcessManager'
import type { Profile } from '../shared/types/Profile.types'
diff --git a/src/main/ipc/System.ipc.ts b/src/main/ipc/System.ipc.ts
index 8d70f6f..30a73ae 100644
--- a/src/main/ipc/System.ipc.ts
+++ b/src/main/ipc/System.ipc.ts
@@ -1,6 +1,6 @@
import { dialog, shell } from 'electron'
import { restApiServer } from '../RestAPI'
-import type { RouteMap } from '../shared/IPCController'
+import type { RouteMap } from '../IPCController'
import type { AppSettings, JRCEnvironment } from '../shared/types/App.types'
import { getSettings, saveSettings } from '../Store'
import { getEnvironment } from './../JRCEnvironment'
diff --git a/src/main/ipc/Window.ipc.ts b/src/main/ipc/Window.ipc.ts
index b303d4b..b32f7db 100644
--- a/src/main/ipc/Window.ipc.ts
+++ b/src/main/ipc/Window.ipc.ts
@@ -1,5 +1,5 @@
import { app } from 'electron'
-import type { RouteMap } from '../shared/IPCController'
+import type { RouteMap } from '../IPCController'
import { getSettings } from '../Store'
// Injected from main.ts — avoids a circular import on mainWindow/forceQuit
diff --git a/src/main/ipc/_index.ts b/src/main/ipc/_index.ts
index bfb7157..ae0ef78 100644
--- a/src/main/ipc/_index.ts
+++ b/src/main/ipc/_index.ts
@@ -21,7 +21,7 @@ import { ProfileIPC } from './Profile.ipc'
import { SystemIPC } from './System.ipc'
import { WindowIPC } from './Window.ipc'
import { DevIPC } from './Dev.ipc'
-import type { InferAPI } from '../shared/IPCController'
+import type { InferAPI } from '../IPCController'
export const allRoutes = [GitHubIPC, ProcessIPC, ProfileIPC, SystemIPC, WindowIPC, DevIPC] as const
diff --git a/src/main/main.ts b/src/main/main.ts
index 75799ae..3026307 100644
--- a/src/main/main.ts
+++ b/src/main/main.ts
@@ -6,14 +6,15 @@ import { EnvironmentIPC } from './ipc/Environment.ipc'
import { getEnvironment, loadEnvironment } from './JRCEnvironment'
import { processManager } from './ProcessManager'
import { restApiServer } from './RestAPI'
-import { registerIPC } from './shared/IPCController'
+import { registerIPC } from './IPCController'
import { getAllProfiles, getSettings } from './Store'
-loadEnvironment();
+loadEnvironment()
-const RESOURCES = getEnvironment().type === 'dev'
- ? path.join(__dirname, '../../resources')
- : path.join(app.getAppPath(), 'resources')
+const RESOURCES =
+ getEnvironment().type === 'dev'
+ ? path.join(__dirname, '../../resources')
+ : path.join(app.getAppPath(), 'resources')
function getIconImage(): Electron.NativeImage {
const candidates =
@@ -43,7 +44,7 @@ function createWindow(): void {
frame: false,
backgroundColor: '#08090d',
icon: getIconImage(),
- show: getEnvironment().startUpSource != "withSystem",
+ show: getEnvironment().startUpSource != 'withSystem',
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
@@ -57,7 +58,8 @@ function createWindow(): void {
else mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'))
mainWindow.once('ready-to-show', () => {
- const shouldStartHidden = getSettings().startMinimized && getEnvironment().startUpSource === 'withSystem'
+ const shouldStartHidden =
+ getSettings().startMinimized && getEnvironment().startUpSource === 'withSystem'
if (shouldStartHidden) mainWindow?.hide()
else mainWindow?.show()
})
@@ -132,7 +134,7 @@ if (!gotLock) {
})
app.whenReady().then(() => {
- if(getEnvironment().startUpSource === 'withSystem' && !getSettings().launchOnStartup) return;
+ if (getEnvironment().startUpSource === 'withSystem' && !getSettings().launchOnStartup) return
createWindow()
createTray()
diff --git a/src/main/preload.ts b/src/main/preload.ts
index a74723e..091b927 100644
--- a/src/main/preload.ts
+++ b/src/main/preload.ts
@@ -1,7 +1,7 @@
import { contextBridge } from 'electron'
import { allRoutes } from './ipc/_index'
import { EnvironmentIPC } from './ipc/Environment.ipc'
-import { buildPreloadAPI } from './shared/IPCController'
+import { buildPreloadAPI } from './IPCController'
contextBridge.exposeInMainWorld('api', buildPreloadAPI([...allRoutes]))
diff --git a/src/main/shared/types/App.types.ts b/src/main/shared/types/App.types.ts
index 7244eb7..457bb03 100644
--- a/src/main/shared/types/App.types.ts
+++ b/src/main/shared/types/App.types.ts
@@ -14,8 +14,8 @@ export interface AppSettings {
}
export interface JRCEnvironment {
- isReady: boolean;
- devMode: boolean;
- type: "dev" | "prod";
- startUpSource: 'userRequest' | 'withSystem' | 'development';
+ isReady: boolean
+ devMode: boolean
+ type: 'dev' | 'prod'
+ startUpSource: 'userRequest' | 'withSystem' | 'development'
}
diff --git a/src/renderer/components/settings/SettingsTab.tsx b/src/renderer/components/settings/SettingsTab.tsx
index 37661e9..d88ef66 100644
--- a/src/renderer/components/settings/SettingsTab.tsx
+++ b/src/renderer/components/settings/SettingsTab.tsx
@@ -65,10 +65,9 @@ export function SettingsTab() {
if (!draft) return null
-
const handleSave = async () => {
await saveSettings(draft)
- window.env.reload();
+ window.env.reload()
setSaved(true)
setTimeout(() => setSaved(false), 2000)
}
@@ -177,10 +176,7 @@ export function SettingsTab() {
label="Toggle Developer Mode (Right-Shift + 7)"
hint="Enables the Developer tab and DevTools. Warning: may expose sensitive information and powerful features. Use with caution."
>
- set({ devModeEnabled: v })}
- />
+ set({ devModeEnabled: v })} />
Listening on{' '}
- http://{REST_API_CONFIG.host}:{draft.restApiPort}/api
+
+ http://{REST_API_CONFIG.host}:{draft.restApiPort}/api
+
Endpoints: /status · /profiles · /processes · /settings
From 7d0c92b8badc9e09c2fefa499785cadcda41b258 Mon Sep 17 00:00:00 2001
From: Timon Home
Date: Sat, 21 Mar 2026 15:24:16 +0100
Subject: [PATCH 3/4] Updated prettier formatting rules
---
.prettierrc.json | 6 +-
postcss.config.js | 2 +-
src/main/IPCController.ts | 64 +--
src/main/JRCEnvironment.ts | 26 +-
src/main/RestAPI.routes.ts | 85 ++--
src/main/RestAPI.ts | 110 +++---
src/main/ipc/Dev.ipc.ts | 24 +-
src/main/ipc/Environment.ipc.ts | 22 +-
src/main/ipc/GitHub.ipc.ts | 210 +++++-----
src/main/ipc/Process.ipc.ts | 10 +-
src/main/ipc/Profile.ipc.ts | 14 +-
src/main/ipc/System.ipc.ts | 42 +-
src/main/ipc/Window.ipc.ts | 22 +-
src/main/ipc/_index.ts | 34 +-
src/main/main.ts | 176 ++++-----
src/main/preload.ts | 12 +-
src/main/processManager.ts | 372 +++++++++---------
src/main/shared/config/API.config.ts | 14 +-
src/main/shared/config/App.config.ts | 6 +-
src/main/shared/config/FAQ.config.ts | 12 +-
src/main/shared/config/GitHub.config.ts | 10 +-
src/main/shared/config/RestApi.config.ts | 4 +-
src/main/shared/config/Scanner.config.ts | 2 +-
src/main/shared/types/App.types.ts | 32 +-
src/main/shared/types/GitHub.types.ts | 94 ++---
src/main/shared/types/Process.types.ts | 56 +--
src/main/shared/types/Profile.types.ts | 44 +--
src/main/store.ts | 50 +--
src/renderer/App.tsx | 16 +-
src/renderer/components/MainLayout.tsx | 86 ++--
src/renderer/components/common/ArgList.tsx | 44 +--
src/renderer/components/common/Button.tsx | 16 +-
.../components/common/ContextMenu.tsx | 68 ++--
src/renderer/components/common/Dialog.tsx | 24 +-
src/renderer/components/common/Input.tsx | 10 +-
src/renderer/components/common/Modal.tsx | 36 +-
src/renderer/components/common/PropList.tsx | 56 +--
src/renderer/components/common/TitleBar.tsx | 12 +-
src/renderer/components/common/Toggle.tsx | 10 +-
src/renderer/components/common/Tooltip.tsx | 116 +++---
.../components/console/ConsoleTab.tsx | 300 +++++++-------
.../components/developer/DevApiExplorer.tsx | 98 ++---
.../components/developer/DevDashboard.tsx | 68 ++--
.../components/developer/DevDiagnostics.tsx | 74 ++--
.../components/developer/DevModeGate.tsx | 48 +--
.../components/developer/DevStorage.tsx | 64 +--
.../components/developer/DeveloperTab.tsx | 20 +-
src/renderer/components/faq/FaqPanel.tsx | 54 +--
.../components/profiles/ConfigTab.tsx | 156 ++++----
.../components/profiles/ProfileSidebar.tsx | 134 +++----
.../components/profiles/ProfileTab.tsx | 54 +--
.../components/profiles/TemplateModal.tsx | 94 ++---
.../components/settings/SettingsTab.tsx | 102 ++---
.../settings/version/ReleaseModal.tsx | 146 +++----
.../settings/version/VersionChecker.tsx | 72 ++--
.../components/utils/ActivityLogPanel.tsx | 54 +--
.../components/utils/ScannerPanel.tsx | 116 +++---
.../components/utils/UtilitiesTab.tsx | 162 ++++----
src/renderer/global.d.ts | 6 +-
src/renderer/hooks/useDevMode.ts | 26 +-
src/renderer/main.tsx | 16 +-
src/renderer/store/AppStore.tsx | 202 +++++-----
src/renderer/store/appReducer.ts | 52 +--
src/renderer/store/sessionLogs.ts | 14 +-
tailwind.config.js | 2 +-
vite.config.ts | 12 +-
66 files changed, 2099 insertions(+), 2096 deletions(-)
diff --git a/.prettierrc.json b/.prettierrc.json
index 583ce10..3cbe1bf 100644
--- a/.prettierrc.json
+++ b/.prettierrc.json
@@ -1,8 +1,10 @@
{
- "semi": false,
+ "semi": true,
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 100,
"tabWidth": 2,
- "endOfLine": "lf"
+ "endOfLine": "lf",
+ "objectWrap": "preserve",
+ "bracketSameLine": false
}
diff --git a/postcss.config.js b/postcss.config.js
index 2959567..cce4985 100644
--- a/postcss.config.js
+++ b/postcss.config.js
@@ -1 +1 @@
-module.exports = { plugins: { tailwindcss: {}, autoprefixer: {} } }
+module.exports = { plugins: { tailwindcss: {}, autoprefixer: {} } };
diff --git a/src/main/IPCController.ts b/src/main/IPCController.ts
index 2f5dfba..cc4f542 100644
--- a/src/main/IPCController.ts
+++ b/src/main/IPCController.ts
@@ -13,35 +13,35 @@
* main pushes via webContents.send(channel, ...args)
*/
-import { ipcMain, ipcRenderer, IpcMainInvokeEvent, IpcMainEvent, IpcRendererEvent } from 'electron'
+import { ipcMain, ipcRenderer, IpcMainInvokeEvent, IpcMainEvent, IpcRendererEvent } from 'electron';
// ─── Route descriptors ────────────────────────────────────────────────────────
type InvokeRoute = {
- type: 'invoke'
- channel: string
- handler: (event: IpcMainInvokeEvent, ...args: any[]) => any
-}
+ type: 'invoke';
+ channel: string;
+ handler: (event: IpcMainInvokeEvent, ...args: any[]) => any;
+};
type SendRoute = {
- type: 'send'
- channel: string
- handler: (event: IpcMainEvent, ...args: any[]) => void
-}
+ type: 'send';
+ channel: string;
+ handler: (event: IpcMainEvent, ...args: any[]) => void;
+};
/** No handler — main pushes via webContents.send(channel, ...) */
type OnRoute = {
- type: 'on'
- channel: string
+ type: 'on';
+ channel: string;
/** Cast a function signature here to type the callback args on window.api.onFoo.
* e.g. `args: {} as (profileId: string, line: ConsoleLine) => void`
* Never called at runtime — purely a compile-time phantom. */
- args?: (...args: any[]) => void
-}
+ args?: (...args: any[]) => void;
+};
-type Route = InvokeRoute | SendRoute | OnRoute
+type Route = InvokeRoute | SendRoute | OnRoute;
-export type RouteMap = Record
+export type RouteMap = Record;
// ─── Type inference: RouteMap → window.api shape ──────────────────────────────
@@ -50,15 +50,15 @@ type InvokeAPI = R['handler'] extends (
...args: infer A
) => infer Ret
? (...args: A) => Promise>
- : never
+ : never;
type SendAPI = R['handler'] extends (_e: any, ...args: infer A) => any
? (...args: A) => void
- : never
+ : never;
type OnAPI = R extends { args: (...args: infer A) => void }
? { [key in `on${Capitalize}`]: (cb: (...args: A) => void) => () => void }
- : { [key in `on${Capitalize}`]: (cb: (...args: any[]) => void) => () => void }
+ : { [key in `on${Capitalize}`]: (cb: (...args: any[]) => void) => () => void };
/** Derives the full window.api type from a RouteMap. */
export type InferAPI = {
@@ -66,7 +66,7 @@ export type InferAPI = {
? InvokeAPI
: M[K] extends SendRoute
? SendAPI
- : never
+ : never;
} & {
[K in keyof M as M[K]['type'] extends 'on'
? `on${Capitalize}`
@@ -74,16 +74,16 @@ export type InferAPI = {
? M[K] extends { args: (...args: infer A) => void }
? (cb: (...args: A) => void) => () => void
: (cb: (...args: any[]) => void) => () => void
- : never
-}
+ : never;
+};
// ─── Main-process: register all routes onto ipcMain ──────────────────────────
export function registerIPC(routes: RouteMap[]): void {
for (const map of routes) {
for (const route of Object.values(map)) {
- if (route.type === 'invoke') ipcMain.handle(route.channel, route.handler)
- if (route.type === 'send') ipcMain.on(route.channel, route.handler)
+ if (route.type === 'invoke') ipcMain.handle(route.channel, route.handler);
+ if (route.type === 'send') ipcMain.on(route.channel, route.handler);
// 'on' routes are push-only from main — no listener to register
}
}
@@ -92,26 +92,26 @@ export function registerIPC(routes: RouteMap[]): void {
// ─── Preload: build the window.api object ────────────────────────────────────
export function buildPreloadAPI(routes: RouteMap[]): Record {
- const api: Record = {}
+ const api: Record = {};
for (const map of routes) {
for (const [key, route] of Object.entries(map)) {
if (route.type === 'invoke') {
- api[key] = (...args: unknown[]) => ipcRenderer.invoke(route.channel, ...args)
+ api[key] = (...args: unknown[]) => ipcRenderer.invoke(route.channel, ...args);
}
if (route.type === 'send') {
- api[key] = (...args: unknown[]) => ipcRenderer.send(route.channel, ...args)
+ api[key] = (...args: unknown[]) => ipcRenderer.send(route.channel, ...args);
}
if (route.type === 'on') {
- const cbKey = `on${key[0].toUpperCase()}${key.slice(1)}`
+ const cbKey = `on${key[0].toUpperCase()}${key.slice(1)}`;
api[cbKey] = (cb: (...args: unknown[]) => void) => {
- const handler = (_e: IpcRendererEvent, ...args: unknown[]) => cb(...args)
- ipcRenderer.on(route.channel, handler)
- return () => ipcRenderer.off(route.channel, handler)
- }
+ const handler = (_e: IpcRendererEvent, ...args: unknown[]) => cb(...args);
+ ipcRenderer.on(route.channel, handler);
+ return () => ipcRenderer.off(route.channel, handler);
+ };
}
}
}
- return api
+ return api;
}
diff --git a/src/main/JRCEnvironment.ts b/src/main/JRCEnvironment.ts
index 65482e7..1d19b2e 100644
--- a/src/main/JRCEnvironment.ts
+++ b/src/main/JRCEnvironment.ts
@@ -1,14 +1,14 @@
-import { app, BrowserWindow } from 'electron'
-import { JRCEnvironment } from './shared/types/App.types'
-import { getSettings } from './Store'
-import { EnvironmentIPC } from './ipc/Environment.ipc'
+import { app, BrowserWindow } from 'electron';
+import { JRCEnvironment } from './shared/types/App.types';
+import { getSettings } from './Store';
+import { EnvironmentIPC } from './ipc/Environment.ipc';
let env: JRCEnvironment = {
isReady: false,
devMode: null as unknown as JRCEnvironment['devMode'],
type: null as unknown as JRCEnvironment['type'],
startUpSource: null as unknown as JRCEnvironment['startUpSource'],
-}
+};
export function loadEnvironment() {
env = {
@@ -16,25 +16,25 @@ export function loadEnvironment() {
devMode: getSettings().devModeEnabled,
type: app.isPackaged ? 'prod' : 'dev',
startUpSource: detectStartupSource(),
- }
+ };
- broadcast()
+ broadcast();
}
export function getEnvironment() {
- return env
+ return env;
}
function broadcast(channel: string = EnvironmentIPC.change.channel) {
- BrowserWindow.getAllWindows().forEach((w) => w.webContents.send(channel, env))
+ BrowserWindow.getAllWindows().forEach((w) => w.webContents.send(channel, env));
}
function detectStartupSource(): JRCEnvironment['startUpSource'] {
- if (!app.isPackaged) return 'development'
+ if (!app.isPackaged) return 'development';
- const login = app.getLoginItemSettings()
+ const login = app.getLoginItemSettings();
- if (login.wasOpenedAtLogin || process.argv.includes('--autostart')) return 'withSystem'
+ if (login.wasOpenedAtLogin || process.argv.includes('--autostart')) return 'withSystem';
- return 'userRequest'
+ return 'userRequest';
}
diff --git a/src/main/RestAPI.routes.ts b/src/main/RestAPI.routes.ts
index 2c7853a..87c772f 100644
--- a/src/main/RestAPI.routes.ts
+++ b/src/main/RestAPI.routes.ts
@@ -1,36 +1,36 @@
-import { routeConfig, type RouteKey } from './shared/config/API.config'
-import { ok, err } from './RestAPI'
-import { getAllProfiles, saveProfile, deleteProfile, getSettings, saveSettings } from './Store'
-import { processManager } from './ProcessManager'
-import { v4 as uuidv4 } from 'uuid'
-import type http from 'http'
-import { Profile } from './shared/types/Profile.types'
-import { AppSettings } from './shared/types/App.types'
+import { routeConfig, type RouteKey } from './shared/config/API.config';
+import { ok, err } from './RestAPI';
+import { getAllProfiles, saveProfile, deleteProfile, getSettings, saveSettings } from './Store';
+import { processManager } from './ProcessManager';
+import { v4 as uuidv4 } from 'uuid';
+import type http from 'http';
+import { Profile } from './shared/types/Profile.types';
+import { AppSettings } from './shared/types/App.types';
// ─── Context ──────────────────────────────────────────────────────────────────
-type Params = Record
+type Params = Record;
export interface Context {
- req: http.IncomingMessage
- res: http.ServerResponse
- params: Params
- body: unknown
+ req: http.IncomingMessage;
+ res: http.ServerResponse;
+ params: Params;
+ body: unknown;
}
-type RouteHandler = (ctx: Context) => void | Promise
+type RouteHandler = (ctx: Context) => void | Promise;
// ─── Typed route builder ──────────────────────────────────────────────────────
type BuiltRoute = (typeof routeConfig)[K] & {
- handler: RouteHandler
-}
+ handler: RouteHandler;
+};
function defineRoute(key: K, handler: RouteHandler): BuiltRoute {
return {
...routeConfig[key],
handler,
- }
+ };
}
// ─── Routes ───────────────────────────────────────────────────────────────────
@@ -48,13 +48,13 @@ export const routes = [
defineRoute('profiles_list', ({ res }) => ok(res, getAllProfiles())),
defineRoute('profiles_get', ({ res, params }) => {
- const p = getAllProfiles().find((p) => p.id === params.id)
- console.log(p, params, getAllProfiles())
- p ? ok(res, p) : err(res, 'Profile not found', 404)
+ const p = getAllProfiles().find((p) => p.id === params.id);
+ console.log(p, params, getAllProfiles());
+ p ? ok(res, p) : err(res, 'Profile not found', 404);
}),
defineRoute('profiles_create', ({ res, body }) => {
- const b = body as Partial
+ const b = body as Partial;
const p: Profile = {
id: uuidv4(),
@@ -71,33 +71,34 @@ export const routes = [
color: b.color ?? '#4ade80',
createdAt: Date.now(),
updatedAt: Date.now(),
- }
+ };
- saveProfile(p)
- ok(res, p, 201)
+ saveProfile(p);
+ ok(res, p, 201);
}),
defineRoute('profiles_update', ({ res, params, body }) => {
- const existing = getAllProfiles().find((p) => p.id === params.id)
- if (!existing) return err(res, 'Profile not found', 404)
+ const existing = getAllProfiles().find((p) => p.id === params.id);
+ if (!existing) return err(res, 'Profile not found', 404);
const updated: Profile = {
...existing,
...(body as Partial),
id: params.id,
updatedAt: Date.now(),
- }
+ };
- saveProfile(updated)
- processManager.updateProfileSnapshot(updated)
- ok(res, updated)
+ saveProfile(updated);
+ processManager.updateProfileSnapshot(updated);
+ ok(res, updated);
}),
defineRoute('profiles_delete', ({ res, params }) => {
- if (!getAllProfiles().find((p) => p.id === params.id)) return err(res, 'Profile not found', 404)
+ if (!getAllProfiles().find((p) => p.id === params.id))
+ return err(res, 'Profile not found', 404);
- deleteProfile(params.id)
- ok(res)
+ deleteProfile(params.id);
+ ok(res);
}),
defineRoute('processes_list', ({ res }) => ok(res, processManager.getStates())),
@@ -105,16 +106,16 @@ export const routes = [
defineRoute('processes_log', ({ res }) => ok(res, processManager.getActivityLog())),
defineRoute('processes_start', ({ res, params }) => {
- const p = getAllProfiles().find((p) => p.id === params.id)
- if (!p) return err(res, 'Profile not found', 404)
- ok(res, processManager.start(p))
+ const p = getAllProfiles().find((p) => p.id === params.id);
+ if (!p) return err(res, 'Profile not found', 404);
+ ok(res, processManager.start(p));
}),
defineRoute('processes_stop', ({ res, params }) => ok(res, processManager.stop(params.id))),
defineRoute('processes_clear', ({ res, params }) => {
- processManager.clearConsoleForProfile(params.id)
- ok(res)
+ processManager.clearConsoleForProfile(params.id);
+ ok(res);
}),
defineRoute('settings_get', ({ res }) => ok(res, getSettings())),
@@ -123,9 +124,9 @@ export const routes = [
const updated: AppSettings = {
...getSettings(),
...(body as Partial),
- }
+ };
- saveSettings(updated)
- ok(res, updated)
+ saveSettings(updated);
+ ok(res, updated);
}),
-]
+];
diff --git a/src/main/RestAPI.ts b/src/main/RestAPI.ts
index b90a3df..49b73d0 100644
--- a/src/main/RestAPI.ts
+++ b/src/main/RestAPI.ts
@@ -1,25 +1,25 @@
-import http from 'http'
-import { routes } from './RestAPI.routes'
-import { routeConfig } from './shared/config/API.config'
-import { getSettings } from './Store'
-import { REST_API_CONFIG } from './shared/config/RestApi.config'
+import http from 'http';
+import { routes } from './RestAPI.routes';
+import { routeConfig } from './shared/config/API.config';
+import { getSettings } from './Store';
+import { REST_API_CONFIG } from './shared/config/RestApi.config';
-type Params = Record
+type Params = Record;
// ─── Types ────────────────────────────────────────────────────────────────────
type CompiledRoute = {
- method: string
- path: string
- pattern: RegExp
- keys: string[]
+ method: string;
+ path: string;
+ pattern: RegExp;
+ keys: string[];
handler: (ctx: {
- req: http.IncomingMessage
- res: http.ServerResponse
- params: Params
- body: unknown
- }) => void | Promise
-}
+ req: http.IncomingMessage;
+ res: http.ServerResponse;
+ params: Params;
+ body: unknown;
+ }) => void | Promise;
+};
// ─── Helpers ──────────────────────────────────────────────────────────────────
@@ -27,56 +27,56 @@ function json(res: http.ServerResponse, data: unknown, status = 200) {
res.writeHead(status, {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
- })
- res.end(JSON.stringify(data))
+ });
+ res.end(JSON.stringify(data));
}
export function ok(res: http.ServerResponse, data: unknown = { ok: true }, status = 200) {
- json(res, data, status)
+ json(res, data, status);
}
export function err(res: http.ServerResponse, msg: string, status = 400) {
- json(res, { error: msg }, status)
+ json(res, { error: msg }, status);
}
async function readBody(req: http.IncomingMessage): Promise {
return new Promise((resolve) => {
- let raw = ''
- req.on('data', (c) => (raw += c))
+ let raw = '';
+ req.on('data', (c) => (raw += c));
req.on('end', () => {
try {
- resolve(JSON.parse(raw))
+ resolve(JSON.parse(raw));
} catch {
- resolve({})
+ resolve({});
}
- })
- })
+ });
+ });
}
function parsePattern(path: string) {
- const keys: string[] = []
+ const keys: string[] = [];
const src = path.replace(/:([a-zA-Z]+)/g, (_m, k) => {
- keys.push(k)
- return '([^/]+)'
- })
- return { pattern: new RegExp(`^${src}$`), keys }
+ keys.push(k);
+ return '([^/]+)';
+ });
+ return { pattern: new RegExp(`^${src}$`), keys };
}
function compileRoutes(): CompiledRoute[] {
return routes.map((r) => ({
...r,
...parsePattern(r.path),
- }))
+ }));
}
// ─── Server ───────────────────────────────────────────────────────────────────
class RestApiServer {
- private server: http.Server | null = null
- private compiled = compileRoutes()
+ private server: http.Server | null = null;
+ private compiled = compileRoutes();
start(port: number): void {
- if (this.server) return
+ if (this.server) return;
this.server = http.createServer(async (req, res) => {
if (req.method === 'OPTIONS') {
@@ -84,42 +84,42 @@ class RestApiServer {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
- })
- return res.end()
+ });
+ return res.end();
}
- const url = req.url?.split('?')[0] ?? '/'
- const method = req.method ?? 'GET'
+ const url = req.url?.split('?')[0] ?? '/';
+ const method = req.method ?? 'GET';
const body =
- method === 'POST' || method === 'PUT' || method === 'PATCH' ? await readBody(req) : {}
+ method === 'POST' || method === 'PUT' || method === 'PATCH' ? await readBody(req) : {};
for (const route of this.compiled) {
- if (route.method !== method) continue
+ if (route.method !== method) continue;
- const match = url.match(route.pattern)
- if (!match) continue
+ const match = url.match(route.pattern);
+ if (!match) continue;
- const params: Params = {}
+ const params: Params = {};
route.keys.forEach((k, i) => {
- params[k] = match[i + 1]
- })
+ params[k] = match[i + 1];
+ });
- await route.handler({ req, res, params, body })
- return
+ await route.handler({ req, res, params, body });
+ return;
}
- err(res, 'Not found', 404)
- })
+ err(res, 'Not found', 404);
+ });
this.server.listen(port, REST_API_CONFIG.host, () => {
- console.log(`[JRC REST] Listening on ${REST_API_CONFIG.host}:${port}`)
- })
+ console.log(`[JRC REST] Listening on ${REST_API_CONFIG.host}:${port}`);
+ });
}
stop(): void {
- this.server?.close()
- this.server = null
+ this.server?.close();
+ this.server = null;
}
}
-export const restApiServer = new RestApiServer()
+export const restApiServer = new RestApiServer();
diff --git a/src/main/ipc/Dev.ipc.ts b/src/main/ipc/Dev.ipc.ts
index a24c06f..d7d56db 100644
--- a/src/main/ipc/Dev.ipc.ts
+++ b/src/main/ipc/Dev.ipc.ts
@@ -1,12 +1,12 @@
-import { BrowserWindow } from 'electron'
-import { DEFAULT_SETTINGS } from '../shared/config/App.config'
-import type { RouteMap } from '../IPCController'
-import { getAllProfiles, getSettings, toggleDevMode } from '../Store'
+import { BrowserWindow } from 'electron';
+import { DEFAULT_SETTINGS } from '../shared/config/App.config';
+import type { RouteMap } from '../IPCController';
+import { getAllProfiles, getSettings, toggleDevMode } from '../Store';
-let getWindow: () => BrowserWindow | null = () => null
+let getWindow: () => BrowserWindow | null = () => null;
export function initDevIPC(windowGetter: () => BrowserWindow | null) {
- getWindow = windowGetter
+ getWindow = windowGetter;
}
export const DevIPC = {
@@ -29,11 +29,11 @@ export const DevIPC = {
channel: 'dev:resetStore',
handler: async () => {
// Remove all profiles and reset settings to defaults
- const profiles = getAllProfiles()
- const Store = (await import('electron-store')).default
- const store = new Store({ name: 'java-runner-config' })
- store.set('profiles', [])
- store.set('settings', DEFAULT_SETTINGS)
+ const profiles = getAllProfiles();
+ const Store = (await import('electron-store')).default;
+ const store = new Store({ name: 'java-runner-config' });
+ store.set('profiles', []);
+ store.set('settings', DEFAULT_SETTINGS);
},
},
-} satisfies RouteMap
+} satisfies RouteMap;
diff --git a/src/main/ipc/Environment.ipc.ts b/src/main/ipc/Environment.ipc.ts
index 97e473f..3d9ebbb 100644
--- a/src/main/ipc/Environment.ipc.ts
+++ b/src/main/ipc/Environment.ipc.ts
@@ -1,8 +1,8 @@
-import { BrowserWindow } from 'electron'
-import { getEnvironment, loadEnvironment } from '../JRCEnvironment'
-import { RouteMap } from '../IPCController'
-import { JRCEnvironment } from '../shared/types/App.types'
-import { toggleDevMode } from '../Store'
+import { BrowserWindow } from 'electron';
+import { getEnvironment, loadEnvironment } from '../JRCEnvironment';
+import { RouteMap } from '../IPCController';
+import { JRCEnvironment } from '../shared/types/App.types';
+import { toggleDevMode } from '../Store';
export const EnvironmentIPC = {
get: {
@@ -27,15 +27,15 @@ export const EnvironmentIPC = {
type: 'invoke',
channel: 'env:toggleDevMode',
handler: (_e: Electron.IpcMainInvokeEvent, enabled: boolean) => {
- const win = BrowserWindow.getAllWindows()[0]
- if (!win) return
+ const win = BrowserWindow.getAllWindows()[0];
+ if (!win) return;
- toggleDevMode(enabled)
- loadEnvironment()
+ toggleDevMode(enabled);
+ loadEnvironment();
if (!enabled) {
- win.webContents.closeDevTools()
+ win.webContents.closeDevTools();
}
},
},
-} satisfies RouteMap
+} satisfies RouteMap;
diff --git a/src/main/ipc/GitHub.ipc.ts b/src/main/ipc/GitHub.ipc.ts
index 2c439ec..9245349 100644
--- a/src/main/ipc/GitHub.ipc.ts
+++ b/src/main/ipc/GitHub.ipc.ts
@@ -1,49 +1,49 @@
-import fs from 'fs'
-import https from 'https'
-import { dialog, shell, BrowserWindow } from 'electron'
-import type { RouteMap } from '../IPCController'
-import { latestReleaseUrl, templateListUrl, rawTemplateUrl } from '../shared/config/GitHub.config'
-import type { GitHubRelease, ProfileTemplate } from '../shared/types/GitHub.types'
+import fs from 'fs';
+import https from 'https';
+import { dialog, shell, BrowserWindow } from 'electron';
+import type { RouteMap } from '../IPCController';
+import { latestReleaseUrl, templateListUrl, rawTemplateUrl } from '../shared/config/GitHub.config';
+import type { GitHubRelease, ProfileTemplate } from '../shared/types/GitHub.types';
function httpsGet(url: string): Promise {
return new Promise((resolve, reject) => {
- const options = { headers: { 'User-Agent': 'java-runner-client' } }
+ const options = { headers: { 'User-Agent': 'java-runner-client' } };
const req = https.get(url, options, (res) => {
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
- resolve(httpsGet(res.headers.location))
- return
+ resolve(httpsGet(res.headers.location));
+ return;
}
- let data = ''
+ let data = '';
res.on('data', (c) => {
- data += c
- })
+ data += c;
+ });
res.on('end', () => {
try {
- resolve(JSON.parse(data))
+ resolve(JSON.parse(data));
} catch {
- reject(new Error('JSON parse error'))
+ reject(new Error('JSON parse error'));
}
- })
- })
- req.on('error', reject)
+ });
+ });
+ req.on('error', reject);
req.setTimeout(10000, () => {
- req.destroy()
- reject(new Error('Timeout'))
- })
- })
+ req.destroy();
+ reject(new Error('Timeout'));
+ });
+ });
}
interface ActiveDownload {
- req: ReturnType
- res: import('http').IncomingMessage
- fileStream: fs.WriteStream
- filePath: string
- bytesWritten: number
- totalBytes: number
- paused: boolean
+ req: ReturnType;
+ res: import('http').IncomingMessage;
+ fileStream: fs.WriteStream;
+ filePath: string;
+ bytesWritten: number;
+ totalBytes: number;
+ paused: boolean;
}
-const activeDownloads = new Map()
+const activeDownloads = new Map();
export const GitHubIPC = {
fetchLatestRelease: {
@@ -51,9 +51,9 @@ export const GitHubIPC = {
channel: 'github:latestRelease',
handler: async () => {
try {
- return { ok: true, data: (await httpsGet(latestReleaseUrl())) as GitHubRelease }
+ return { ok: true, data: (await httpsGet(latestReleaseUrl())) as GitHubRelease };
} catch (e) {
- return { ok: false, error: String(e) }
+ return { ok: false, error: String(e) };
}
},
},
@@ -63,23 +63,23 @@ export const GitHubIPC = {
channel: 'github:templates',
handler: async () => {
try {
- const raw = await httpsGet(templateListUrl())
+ const raw = await httpsGet(templateListUrl());
if (!Array.isArray(raw))
- return { ok: false, error: 'Templates folder not found or repo not configured' }
- const templates: Array<{ filename: string; template: ProfileTemplate }> = []
+ return { ok: false, error: 'Templates folder not found or repo not configured' };
+ const templates: Array<{ filename: string; template: ProfileTemplate }> = [];
for (const f of (raw as Array<{ name: string }>).filter((f) => f.name.endsWith('.json'))) {
try {
templates.push({
filename: f.name,
template: (await httpsGet(rawTemplateUrl(f.name))) as ProfileTemplate,
- })
+ });
} catch {
/* skip malformed */
}
}
- return { ok: true, data: templates }
+ return { ok: true, data: templates };
} catch (e) {
- return { ok: false, error: String(e) }
+ return { ok: false, error: String(e) };
}
},
},
@@ -91,17 +91,17 @@ export const GitHubIPC = {
const { canceled, filePath } = await dialog.showSaveDialog({
defaultPath: filename,
filters: [{ name: 'Installer', extensions: ['exe', 'dmg', 'AppImage', 'deb', '*'] }],
- })
- if (canceled || !filePath) return { ok: false }
+ });
+ if (canceled || !filePath) return { ok: false };
- const { sender } = e
+ const { sender } = e;
const sendProgress = (
dl: Pick,
status: 'downloading' | 'paused' | 'done' | 'error' | 'cancelled',
error?: string
) => {
- if (sender.isDestroyed()) return
- const { bytesWritten, totalBytes } = dl
+ if (sender.isDestroyed()) return;
+ const { bytesWritten, totalBytes } = dl;
sender.send('github:downloadProgress', {
filename,
bytesWritten,
@@ -109,12 +109,12 @@ export const GitHubIPC = {
percent: totalBytes > 0 ? Math.round((bytesWritten / totalBytes) * 100) : 0,
status,
error,
- })
- }
+ });
+ };
return new Promise<{ ok: boolean; error?: string }>((resolve) => {
const doRequest = (requestUrl: string) => {
- const options = { headers: { 'User-Agent': 'java-runner-client' } }
+ const options = { headers: { 'User-Agent': 'java-runner-client' } };
const req = https.get(requestUrl, options, (res) => {
// Follow redirect
if (
@@ -123,12 +123,12 @@ export const GitHubIPC = {
res.statusCode < 400 &&
res.headers.location
) {
- doRequest(res.headers.location)
- return
+ doRequest(res.headers.location);
+ return;
}
- const totalBytes = parseInt(res.headers['content-length'] ?? '0', 10)
- const file = fs.createWriteStream(filePath)
+ const totalBytes = parseInt(res.headers['content-length'] ?? '0', 10);
+ const file = fs.createWriteStream(filePath);
const dl: ActiveDownload = {
req,
@@ -138,47 +138,47 @@ export const GitHubIPC = {
bytesWritten: 0,
totalBytes,
paused: false,
- }
- activeDownloads.set(filename, dl)
+ };
+ activeDownloads.set(filename, dl);
- sendProgress(dl, 'downloading')
+ sendProgress(dl, 'downloading');
res.on('data', (chunk: Buffer) => {
- dl.bytesWritten += chunk.length
- file.write(chunk)
- sendProgress(dl, dl.paused ? 'paused' : 'downloading')
- })
+ dl.bytesWritten += chunk.length;
+ file.write(chunk);
+ sendProgress(dl, dl.paused ? 'paused' : 'downloading');
+ });
res.on('end', () => {
- file.end()
- })
+ file.end();
+ });
file.on('finish', () => {
- file.close()
- activeDownloads.delete(filename)
- sendProgress({ bytesWritten: dl.totalBytes, totalBytes: dl.totalBytes }, 'done')
- shell.showItemInFolder(filePath)
- resolve({ ok: true })
- })
+ file.close();
+ activeDownloads.delete(filename);
+ sendProgress({ bytesWritten: dl.totalBytes, totalBytes: dl.totalBytes }, 'done');
+ shell.showItemInFolder(filePath);
+ resolve({ ok: true });
+ });
req.on('error', (err) => {
- activeDownloads.delete(filename)
- fs.unlink(filePath, () => {})
- sendProgress(dl, 'error', err.message)
- resolve({ ok: false, error: err.message })
- })
+ activeDownloads.delete(filename);
+ fs.unlink(filePath, () => {});
+ sendProgress(dl, 'error', err.message);
+ resolve({ ok: false, error: err.message });
+ });
file.on('error', (err) => {
- activeDownloads.delete(filename)
- fs.unlink(filePath, () => {})
- sendProgress(dl, 'error', err.message)
- resolve({ ok: false, error: err.message })
- })
- })
- }
+ activeDownloads.delete(filename);
+ fs.unlink(filePath, () => {});
+ sendProgress(dl, 'error', err.message);
+ resolve({ ok: false, error: err.message });
+ });
+ });
+ };
- doRequest(url)
- })
+ doRequest(url);
+ });
},
},
@@ -186,10 +186,10 @@ export const GitHubIPC = {
type: 'invoke',
channel: 'github:pauseDownload',
handler: async (e: Electron.IpcMainInvokeEvent, filename: string) => {
- const dl = activeDownloads.get(filename)
- if (!dl) return { ok: false, error: 'No active download' }
- dl.res.pause()
- dl.paused = true
+ const dl = activeDownloads.get(filename);
+ if (!dl) return { ok: false, error: 'No active download' };
+ dl.res.pause();
+ dl.paused = true;
if (!e.sender.isDestroyed()) {
e.sender.send('github:downloadProgress', {
filename,
@@ -197,9 +197,9 @@ export const GitHubIPC = {
totalBytes: dl.totalBytes,
percent: dl.totalBytes > 0 ? Math.round((dl.bytesWritten / dl.totalBytes) * 100) : 0,
status: 'paused',
- })
+ });
}
- return { ok: true }
+ return { ok: true };
},
},
@@ -207,10 +207,10 @@ export const GitHubIPC = {
type: 'invoke',
channel: 'github:resumeDownload',
handler: async (e: Electron.IpcMainInvokeEvent, filename: string) => {
- const dl = activeDownloads.get(filename)
- if (!dl) return { ok: false, error: 'No active download' }
- dl.res.resume()
- dl.paused = false
+ const dl = activeDownloads.get(filename);
+ if (!dl) return { ok: false, error: 'No active download' };
+ dl.res.resume();
+ dl.paused = false;
if (!e.sender.isDestroyed()) {
e.sender.send('github:downloadProgress', {
filename,
@@ -218,9 +218,9 @@ export const GitHubIPC = {
totalBytes: dl.totalBytes,
percent: dl.totalBytes > 0 ? Math.round((dl.bytesWritten / dl.totalBytes) * 100) : 0,
status: 'downloading',
- })
+ });
}
- return { ok: true }
+ return { ok: true };
},
},
@@ -228,12 +228,12 @@ export const GitHubIPC = {
type: 'invoke',
channel: 'github:cancelDownload',
handler: async (e: Electron.IpcMainInvokeEvent, filename: string) => {
- const dl = activeDownloads.get(filename)
- if (!dl) return { ok: false, error: 'No active download' }
- dl.res.destroy()
- dl.fileStream.close()
- fs.unlink(dl.filePath, () => {})
- activeDownloads.delete(filename)
+ const dl = activeDownloads.get(filename);
+ if (!dl) return { ok: false, error: 'No active download' };
+ dl.res.destroy();
+ dl.fileStream.close();
+ fs.unlink(dl.filePath, () => {});
+ activeDownloads.delete(filename);
if (!e.sender.isDestroyed()) {
e.sender.send('github:downloadProgress', {
filename,
@@ -241,9 +241,9 @@ export const GitHubIPC = {
totalBytes: dl.totalBytes,
percent: dl.totalBytes > 0 ? Math.round((dl.bytesWritten / dl.totalBytes) * 100) : 0,
status: 'cancelled',
- })
+ });
}
- return { ok: true }
+ return { ok: true };
},
},
@@ -251,12 +251,12 @@ export const GitHubIPC = {
type: 'on',
channel: 'github:downloadProgress',
args: {} as (progress: {
- filename: string
- bytesWritten: number
- totalBytes: number
- percent: number
- status: 'downloading' | 'paused' | 'done' | 'error' | 'cancelled'
- error?: string
+ filename: string;
+ bytesWritten: number;
+ totalBytes: number;
+ percent: number;
+ status: 'downloading' | 'paused' | 'done' | 'error' | 'cancelled';
+ error?: string;
}) => void,
},
-} satisfies RouteMap
+} satisfies RouteMap;
diff --git a/src/main/ipc/Process.ipc.ts b/src/main/ipc/Process.ipc.ts
index 7edeeec..c475dfb 100644
--- a/src/main/ipc/Process.ipc.ts
+++ b/src/main/ipc/Process.ipc.ts
@@ -1,7 +1,7 @@
-import type { RouteMap } from '../IPCController'
-import { processManager } from '../ProcessManager'
-import { Profile } from '../shared/types/Profile.types'
-import { ConsoleLine, ProcessState } from '../shared/types/Process.types'
+import type { RouteMap } from '../IPCController';
+import { processManager } from '../ProcessManager';
+import { Profile } from '../shared/types/Profile.types';
+import { ConsoleLine, ProcessState } from '../shared/types/Process.types';
export const ProcessIPC = {
startProcess: {
@@ -63,4 +63,4 @@ export const ProcessIPC = {
channel: 'process:statesUpdate',
args: {} as (states: ProcessState[]) => void,
},
-} satisfies RouteMap
+} satisfies RouteMap;
diff --git a/src/main/ipc/Profile.ipc.ts b/src/main/ipc/Profile.ipc.ts
index 6f0d9ee..f69036b 100644
--- a/src/main/ipc/Profile.ipc.ts
+++ b/src/main/ipc/Profile.ipc.ts
@@ -1,7 +1,7 @@
-import type { RouteMap } from '../IPCController'
-import { getAllProfiles, saveProfile, deleteProfile, reorderProfiles } from '../Store'
-import { processManager } from '../ProcessManager'
-import type { Profile } from '../shared/types/Profile.types'
+import type { RouteMap } from '../IPCController';
+import { getAllProfiles, saveProfile, deleteProfile, reorderProfiles } from '../Store';
+import { processManager } from '../ProcessManager';
+import type { Profile } from '../shared/types/Profile.types';
export const ProfileIPC = {
getProfiles: { type: 'invoke', channel: 'profiles:getAll', handler: () => getAllProfiles() },
@@ -20,8 +20,8 @@ export const ProfileIPC = {
type: 'invoke',
channel: 'profiles:save',
handler: (_e: any, profile: Profile) => {
- saveProfile(profile)
- processManager.updateProfileSnapshot(profile)
+ saveProfile(profile);
+ processManager.updateProfileSnapshot(profile);
},
},
-} satisfies RouteMap
+} satisfies RouteMap;
diff --git a/src/main/ipc/System.ipc.ts b/src/main/ipc/System.ipc.ts
index 30a73ae..5c88b69 100644
--- a/src/main/ipc/System.ipc.ts
+++ b/src/main/ipc/System.ipc.ts
@@ -1,14 +1,14 @@
-import { dialog, shell } from 'electron'
-import { restApiServer } from '../RestAPI'
-import type { RouteMap } from '../IPCController'
-import type { AppSettings, JRCEnvironment } from '../shared/types/App.types'
-import { getSettings, saveSettings } from '../Store'
-import { getEnvironment } from './../JRCEnvironment'
+import { dialog, shell } from 'electron';
+import { restApiServer } from '../RestAPI';
+import type { RouteMap } from '../IPCController';
+import type { AppSettings, JRCEnvironment } from '../shared/types/App.types';
+import { getSettings, saveSettings } from '../Store';
+import { getEnvironment } from './../JRCEnvironment';
// mainWindow is needed for dialogs — set via initSystemIPC() called from main.ts
-let getWindow: () => Electron.BrowserWindow | null = () => null
+let getWindow: () => Electron.BrowserWindow | null = () => null;
export function initSystemIPC(windowGetter: () => Electron.BrowserWindow | null) {
- getWindow = windowGetter
+ getWindow = windowGetter;
}
export const SystemIPC = {
@@ -18,13 +18,13 @@ export const SystemIPC = {
type: 'invoke',
channel: 'settings:save',
handler: (_e: any, next: AppSettings) => {
- const prev = getSettings()
- saveSettings(next)
- if (!next.restApiEnabled && prev.restApiEnabled) restApiServer.stop()
- else if (next.restApiEnabled && !prev.restApiEnabled) restApiServer.start(next.restApiPort)
+ const prev = getSettings();
+ saveSettings(next);
+ if (!next.restApiEnabled && prev.restApiEnabled) restApiServer.stop();
+ else if (next.restApiEnabled && !prev.restApiEnabled) restApiServer.start(next.restApiPort);
else if (next.restApiEnabled && next.restApiPort !== prev.restApiPort) {
- restApiServer.stop()
- restApiServer.start(next.restApiPort)
+ restApiServer.stop();
+ restApiServer.start(next.restApiPort);
}
},
},
@@ -36,16 +36,16 @@ export const SystemIPC = {
const r = await dialog.showOpenDialog(getWindow()!, {
filters: [{ name: 'JAR', extensions: ['jar'] }],
properties: ['openFile'],
- })
- return r.canceled ? null : r.filePaths[0]
+ });
+ return r.canceled ? null : r.filePaths[0];
},
},
pickDir: {
type: 'invoke',
channel: 'dialog:pickDir',
handler: async () => {
- const r = await dialog.showOpenDialog(getWindow()!, { properties: ['openDirectory'] })
- return r.canceled ? null : r.filePaths[0]
+ const r = await dialog.showOpenDialog(getWindow()!, { properties: ['openDirectory'] });
+ return r.canceled ? null : r.filePaths[0];
},
},
pickJava: {
@@ -55,8 +55,8 @@ export const SystemIPC = {
const r = await dialog.showOpenDialog(getWindow()!, {
filters: [{ name: 'Executable', extensions: ['exe', '*'] }],
properties: ['openFile'],
- })
- return r.canceled ? null : r.filePaths[0]
+ });
+ return r.canceled ? null : r.filePaths[0];
},
},
@@ -65,4 +65,4 @@ export const SystemIPC = {
channel: 'shell:openExternal',
handler: (_e: any, url: string) => shell.openExternal(url),
},
-} satisfies RouteMap
+} satisfies RouteMap;
diff --git a/src/main/ipc/Window.ipc.ts b/src/main/ipc/Window.ipc.ts
index b32f7db..7cc2eb6 100644
--- a/src/main/ipc/Window.ipc.ts
+++ b/src/main/ipc/Window.ipc.ts
@@ -1,17 +1,17 @@
-import { app } from 'electron'
-import type { RouteMap } from '../IPCController'
-import { getSettings } from '../Store'
+import { app } from 'electron';
+import type { RouteMap } from '../IPCController';
+import { getSettings } from '../Store';
// Injected from main.ts — avoids a circular import on mainWindow/forceQuit
-let getWindow: () => Electron.BrowserWindow | null = () => null
-let setForceQuit: () => void = () => {}
+let getWindow: () => Electron.BrowserWindow | null = () => null;
+let setForceQuit: () => void = () => {};
export function initWindowIPC(
windowGetter: () => Electron.BrowserWindow | null,
forceQuitSetter: () => void
) {
- getWindow = windowGetter
- setForceQuit = forceQuitSetter
+ getWindow = windowGetter;
+ setForceQuit = forceQuitSetter;
}
export const WindowIPC = {
@@ -25,11 +25,11 @@ export const WindowIPC = {
type: 'send',
channel: 'window:close',
handler: () => {
- if (getSettings().minimizeToTray) getWindow()?.hide()
+ if (getSettings().minimizeToTray) getWindow()?.hide();
else {
- setForceQuit()
- app.quit()
+ setForceQuit();
+ app.quit();
}
},
},
-} satisfies RouteMap
+} satisfies RouteMap;
diff --git a/src/main/ipc/_index.ts b/src/main/ipc/_index.ts
index ae0ef78..dfec313 100644
--- a/src/main/ipc/_index.ts
+++ b/src/main/ipc/_index.ts
@@ -1,4 +1,4 @@
-import { EnvironmentIPC } from './Environment.ipc'
+import { EnvironmentIPC } from './Environment.ipc';
/**
* Central IPC registry.
*
@@ -8,22 +8,22 @@ import { EnvironmentIPC } from './Environment.ipc'
* 3. That's it — main, preload, and types all update automatically
*/
-export { GitHubIPC } from './GitHub.ipc'
-export { ProcessIPC } from './Process.ipc'
-export { ProfileIPC } from './Profile.ipc'
-export { SystemIPC, initSystemIPC } from './System.ipc'
-export { WindowIPC, initWindowIPC } from './Window.ipc'
-export { DevIPC, initDevIPC } from './Dev.ipc'
+export { GitHubIPC } from './GitHub.ipc';
+export { ProcessIPC } from './Process.ipc';
+export { ProfileIPC } from './Profile.ipc';
+export { SystemIPC, initSystemIPC } from './System.ipc';
+export { WindowIPC, initWindowIPC } from './Window.ipc';
+export { DevIPC, initDevIPC } from './Dev.ipc';
-import { GitHubIPC } from './GitHub.ipc'
-import { ProcessIPC } from './Process.ipc'
-import { ProfileIPC } from './Profile.ipc'
-import { SystemIPC } from './System.ipc'
-import { WindowIPC } from './Window.ipc'
-import { DevIPC } from './Dev.ipc'
-import type { InferAPI } from '../IPCController'
+import { GitHubIPC } from './GitHub.ipc';
+import { ProcessIPC } from './Process.ipc';
+import { ProfileIPC } from './Profile.ipc';
+import { SystemIPC } from './System.ipc';
+import { WindowIPC } from './Window.ipc';
+import { DevIPC } from './Dev.ipc';
+import type { InferAPI } from '../IPCController';
-export const allRoutes = [GitHubIPC, ProcessIPC, ProfileIPC, SystemIPC, WindowIPC, DevIPC] as const
+export const allRoutes = [GitHubIPC, ProcessIPC, ProfileIPC, SystemIPC, WindowIPC, DevIPC] as const;
export type API = InferAPI<
typeof GitHubIPC &
@@ -32,6 +32,6 @@ export type API = InferAPI<
typeof SystemIPC &
typeof WindowIPC &
typeof DevIPC
->
+>;
-export type Environment = InferAPI
+export type Environment = InferAPI;
diff --git a/src/main/main.ts b/src/main/main.ts
index 3026307..8837451 100644
--- a/src/main/main.ts
+++ b/src/main/main.ts
@@ -1,39 +1,39 @@
-import { app, BrowserWindow, Menu, nativeImage, Tray } from 'electron'
-import fs from 'fs'
-import path from 'path'
-import { allRoutes, initDevIPC, initSystemIPC, initWindowIPC } from './ipc/_index'
-import { EnvironmentIPC } from './ipc/Environment.ipc'
-import { getEnvironment, loadEnvironment } from './JRCEnvironment'
-import { processManager } from './ProcessManager'
-import { restApiServer } from './RestAPI'
-import { registerIPC } from './IPCController'
-import { getAllProfiles, getSettings } from './Store'
-
-loadEnvironment()
+import { app, BrowserWindow, Menu, nativeImage, Tray } from 'electron';
+import fs from 'fs';
+import path from 'path';
+import { allRoutes, initDevIPC, initSystemIPC, initWindowIPC } from './ipc/_index';
+import { EnvironmentIPC } from './ipc/Environment.ipc';
+import { getEnvironment, loadEnvironment } from './JRCEnvironment';
+import { processManager } from './ProcessManager';
+import { restApiServer } from './RestAPI';
+import { registerIPC } from './IPCController';
+import { getAllProfiles, getSettings } from './Store';
+
+loadEnvironment();
const RESOURCES =
getEnvironment().type === 'dev'
? path.join(__dirname, '../../resources')
- : path.join(app.getAppPath(), 'resources')
+ : path.join(app.getAppPath(), 'resources');
function getIconImage(): Electron.NativeImage {
const candidates =
- process.platform === 'win32' ? ['icon.ico', 'icon.png'] : ['icon.png', 'icon.ico']
+ process.platform === 'win32' ? ['icon.ico', 'icon.png'] : ['icon.png', 'icon.ico'];
for (const name of candidates) {
- const p = path.join(RESOURCES, name)
+ const p = path.join(RESOURCES, name);
if (fs.existsSync(p)) {
- const img = nativeImage.createFromPath(p)
- if (!img.isEmpty()) return img
+ const img = nativeImage.createFromPath(p);
+ if (!img.isEmpty()) return img;
}
}
return nativeImage.createFromDataURL(
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='
- )
+ );
}
-let mainWindow: BrowserWindow | null = null
-let tray: Tray | null = null
-let forceQuit = false
+let mainWindow: BrowserWindow | null = null;
+let tray: Tray | null = null;
+let forceQuit = false;
function createWindow(): void {
mainWindow = new BrowserWindow({
@@ -52,54 +52,54 @@ function createWindow(): void {
sandbox: false,
devTools: true,
},
- })
+ });
- if (getEnvironment().type === 'dev') mainWindow.loadURL('http://localhost:5173')
- else mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'))
+ if (getEnvironment().type === 'dev') mainWindow.loadURL('http://localhost:5173');
+ else mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'));
mainWindow.once('ready-to-show', () => {
const shouldStartHidden =
- getSettings().startMinimized && getEnvironment().startUpSource === 'withSystem'
- if (shouldStartHidden) mainWindow?.hide()
- else mainWindow?.show()
- })
+ getSettings().startMinimized && getEnvironment().startUpSource === 'withSystem';
+ if (shouldStartHidden) mainWindow?.hide();
+ else mainWindow?.show();
+ });
mainWindow.on('close', (e) => {
- if (forceQuit) return
+ if (forceQuit) return;
if (getSettings().minimizeToTray) {
- e.preventDefault()
- mainWindow?.hide()
+ e.preventDefault();
+ mainWindow?.hide();
}
- })
+ });
- processManager.setWindow(mainWindow)
+ processManager.setWindow(mainWindow);
}
function createTray(): void {
- tray = new Tray(getIconImage().resize({ width: 16, height: 16 }))
- tray.setToolTip('Java Runner Client')
- updateTrayMenu()
+ tray = new Tray(getIconImage().resize({ width: 16, height: 16 }));
+ tray.setToolTip('Java Runner Client');
+ updateTrayMenu();
tray.on('double-click', () => {
- mainWindow?.show()
- mainWindow?.focus()
- })
+ mainWindow?.show();
+ mainWindow?.focus();
+ });
}
function updateTrayMenu(): void {
- if (!tray) return
- const states = processManager.getStates()
- const profiles = getAllProfiles()
+ if (!tray) return;
+ const states = processManager.getStates();
+ const profiles = getAllProfiles();
const items = states.map((s) => ({
label: ` ${profiles.find((p) => p.id === s.profileId)?.name ?? s.profileId} (PID ${s.pid ?? '?'})`,
enabled: false,
- }))
+ }));
tray.setContextMenu(
Menu.buildFromTemplate([
{
label: 'Open Java Runner Client',
click: () => {
- mainWindow?.show()
- mainWindow?.focus()
+ mainWindow?.show();
+ mainWindow?.focus();
},
},
{ type: 'separator' },
@@ -109,93 +109,93 @@ function updateTrayMenu(): void {
{
label: 'Quit',
click: () => {
- forceQuit = true
- app.quit()
+ forceQuit = true;
+ app.quit();
},
},
])
- )
+ );
}
-let devToolsPressCount = 0
-let devToolsTimer: NodeJS.Timeout | null = null
+let devToolsPressCount = 0;
+let devToolsTimer: NodeJS.Timeout | null = null;
-const gotLock = app.requestSingleInstanceLock()
+const gotLock = app.requestSingleInstanceLock();
if (!gotLock) {
- app.quit()
+ app.quit();
} else {
app.on('second-instance', () => {
if (mainWindow) {
- if (mainWindow.isMinimized()) mainWindow.restore()
- mainWindow.show()
- mainWindow.focus()
+ if (mainWindow.isMinimized()) mainWindow.restore();
+ mainWindow.show();
+ mainWindow.focus();
}
- })
+ });
app.whenReady().then(() => {
- if (getEnvironment().startUpSource === 'withSystem' && !getSettings().launchOnStartup) return
+ if (getEnvironment().startUpSource === 'withSystem' && !getSettings().launchOnStartup) return;
- createWindow()
- createTray()
+ createWindow();
+ createTray();
mainWindow?.webContents.on('before-input-event', (event, input) => {
const isDevToolsShortcut =
input.key === 'F12' ||
(input.control && input.shift && input.key === 'I') ||
- (input.meta && input.alt && input.key === 'I')
+ (input.meta && input.alt && input.key === 'I');
- if (!isDevToolsShortcut) return
+ if (!isDevToolsShortcut) return;
- event.preventDefault()
+ event.preventDefault();
- devToolsPressCount++
+ devToolsPressCount++;
// reset counter after 1 second of inactivity
- if (devToolsTimer) clearTimeout(devToolsTimer)
+ if (devToolsTimer) clearTimeout(devToolsTimer);
devToolsTimer = setTimeout(() => {
- devToolsPressCount = 0
- }, 1000)
+ devToolsPressCount = 0;
+ }, 1000);
if (devToolsPressCount >= 7) {
- devToolsPressCount = 0
- mainWindow?.webContents.openDevTools({ mode: 'detach' })
- return
+ devToolsPressCount = 0;
+ mainWindow?.webContents.openDevTools({ mode: 'detach' });
+ return;
}
// normal single-press behavior only if devModeEnabled
if (getEnvironment().devMode) {
- mainWindow?.webContents.openDevTools()
+ mainWindow?.webContents.openDevTools();
}
- })
+ });
// ── IPC ────────────────────────────────────────────────────────────────────
- initSystemIPC(() => mainWindow)
+ initSystemIPC(() => mainWindow);
initWindowIPC(
() => mainWindow,
() => {
- forceQuit = true
+ forceQuit = true;
}
- )
- initDevIPC(() => mainWindow)
- registerIPC([...allRoutes])
- registerIPC([EnvironmentIPC])
+ );
+ initDevIPC(() => mainWindow);
+ registerIPC([...allRoutes]);
+ registerIPC([EnvironmentIPC]);
// ──────────────────────────────────────────────────────────────────────────
- const settings = getSettings()
- if (settings.restApiEnabled) restApiServer.start(settings.restApiPort)
- for (const p of getAllProfiles()) if (p.autoStart && p.jarPath) processManager.start(p)
+ const settings = getSettings();
+ if (settings.restApiEnabled) restApiServer.start(settings.restApiPort);
+ for (const p of getAllProfiles()) if (p.autoStart && p.jarPath) processManager.start(p);
- mainWindow?.webContents.on('did-finish-load', updateTrayMenu)
- processManager.setTrayUpdater(updateTrayMenu)
- })
+ mainWindow?.webContents.on('did-finish-load', updateTrayMenu);
+ processManager.setTrayUpdater(updateTrayMenu);
+ });
}
app.on('window-all-closed', () => {
/* keep alive in tray */
-})
+});
app.on('before-quit', () => {
- forceQuit = true
-})
+ forceQuit = true;
+});
app.on('activate', () => {
- mainWindow?.show()
-})
+ mainWindow?.show();
+});
diff --git a/src/main/preload.ts b/src/main/preload.ts
index 091b927..49769fa 100644
--- a/src/main/preload.ts
+++ b/src/main/preload.ts
@@ -1,8 +1,8 @@
-import { contextBridge } from 'electron'
-import { allRoutes } from './ipc/_index'
-import { EnvironmentIPC } from './ipc/Environment.ipc'
-import { buildPreloadAPI } from './IPCController'
+import { contextBridge } from 'electron';
+import { allRoutes } from './ipc/_index';
+import { EnvironmentIPC } from './ipc/Environment.ipc';
+import { buildPreloadAPI } from './IPCController';
-contextBridge.exposeInMainWorld('api', buildPreloadAPI([...allRoutes]))
+contextBridge.exposeInMainWorld('api', buildPreloadAPI([...allRoutes]));
-contextBridge.exposeInMainWorld('env', buildPreloadAPI([EnvironmentIPC]))
+contextBridge.exposeInMainWorld('env', buildPreloadAPI([EnvironmentIPC]));
diff --git a/src/main/processManager.ts b/src/main/processManager.ts
index f3c87b0..92386f6 100644
--- a/src/main/processManager.ts
+++ b/src/main/processManager.ts
@@ -1,18 +1,18 @@
-import { spawn, execSync, ChildProcess } from 'child_process'
-import { BrowserWindow } from 'electron'
-import path from 'path'
-import { v4 as uuidv4 } from 'uuid'
-import { PROTECTED_PROCESS_NAMES } from './shared/config/Scanner.config'
+import { spawn, execSync, ChildProcess } from 'child_process';
+import { BrowserWindow } from 'electron';
+import path from 'path';
+import { v4 as uuidv4 } from 'uuid';
+import { PROTECTED_PROCESS_NAMES } from './shared/config/Scanner.config';
import {
ConsoleLine,
JavaProcessInfo,
ProcessLogEntry,
ProcessState,
-} from './shared/types/Process.types'
-import { Profile } from './shared/types/Profile.types'
-import { ProcessIPC } from './ipc/Process.ipc'
+} from './shared/types/Process.types';
+import { Profile } from './shared/types/Profile.types';
+import { ProcessIPC } from './ipc/Process.ipc';
-const SELF_PROCESS_NAME = 'Java Client Runner'
+const SELF_PROCESS_NAME = 'Java Client Runner';
type SystemMessageType =
| 'start'
@@ -23,57 +23,57 @@ type SystemMessageType =
| 'error-runtime'
| 'info-pid'
| 'info-workdir'
- | 'info-restart'
+ | 'info-restart';
interface ManagedProcess {
- process: ChildProcess
- profileId: string
- profileName: string
- jarPath: string
- startedAt: number
- intentionallyStopped: boolean
+ process: ChildProcess;
+ profileId: string;
+ profileName: string;
+ jarPath: string;
+ startedAt: number;
+ intentionallyStopped: boolean;
}
class ProcessManager {
- private processes = new Map()
- private activityLog: ProcessLogEntry[] = []
- private window: BrowserWindow | null = null
- private restartTimers = new Map>()
- private profileSnapshots = new Map()
- private lineCounters = new Map()
- private seenLineIds = new Map>()
- private onTrayUpdate?: () => void
+ private processes = new Map();
+ private activityLog: ProcessLogEntry[] = [];
+ private window: BrowserWindow | null = null;
+ private restartTimers = new Map>();
+ private profileSnapshots = new Map();
+ private lineCounters = new Map();
+ private seenLineIds = new Map>();
+ private onTrayUpdate?: () => void;
setWindow(win: BrowserWindow): void {
- this.window = win
+ this.window = win;
}
private buildArgs(profile: Profile): { 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())
+ 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());
for (const p of profile.systemProperties)
if (p.enabled && p.key.trim())
- args.push(p.value.trim() ? `-D${p.key.trim()}=${p.value.trim()}` : `-D${p.key.trim()}`)
- args.push('-jar', profile.jarPath)
- for (const a of profile.programArgs) if (a.enabled && a.value.trim()) args.push(a.value.trim())
- return { cmd, args }
+ args.push(p.value.trim() ? `-D${p.key.trim()}=${p.value.trim()}` : `-D${p.key.trim()}`);
+ args.push('-jar', profile.jarPath);
+ for (const a of profile.programArgs) if (a.enabled && a.value.trim()) args.push(a.value.trim());
+ return { cmd, args };
}
start(profile: Profile): { ok: boolean; error?: string } {
- if (this.processes.has(profile.id)) return { ok: false, error: 'Process already running' }
- if (!profile.jarPath) return { ok: false, error: 'No JAR file specified' }
+ if (this.processes.has(profile.id)) return { ok: false, error: 'Process already running' };
+ if (!profile.jarPath) return { ok: false, error: 'No JAR file specified' };
- this.cancelRestartTimer(profile.id)
- this.profileSnapshots.set(profile.id, profile)
+ this.cancelRestartTimer(profile.id);
+ this.profileSnapshots.set(profile.id, profile);
- const { cmd, args } = this.buildArgs(profile)
- const cwd = profile.workingDir || path.dirname(profile.jarPath)
+ const { cmd, args } = this.buildArgs(profile);
+ const cwd = profile.workingDir || path.dirname(profile.jarPath);
- this.pushSystem('start', profile.id, 'pending', `Starting: ${cmd} ${args.join(' ')}`)
- this.pushSystem('info-workdir', profile.id, 'pending', `Working dir: ${cwd}`)
+ this.pushSystem('start', profile.id, 'pending', `Starting: ${cmd} ${args.join(' ')}`);
+ this.pushSystem('info-workdir', profile.id, 'pending', `Working dir: ${cwd}`);
- let proc: ChildProcess
+ let proc: ChildProcess;
try {
proc = spawn(cmd, args, {
cwd,
@@ -81,11 +81,11 @@ class ProcessManager {
shell: false,
detached: false,
stdio: ['pipe', 'pipe', 'pipe'],
- })
+ });
} catch (err: unknown) {
- const msg = err instanceof Error ? err.message : String(err)
- this.pushSystem('error-starting', profile.id, 'pending', `Failed to start: ${msg}`)
- return { ok: false, error: msg }
+ const msg = err instanceof Error ? err.message : String(err);
+ this.pushSystem('error-starting', profile.id, 'pending', `Failed to start: ${msg}`);
+ return { ok: false, error: msg };
}
const managed: ManagedProcess = {
@@ -95,16 +95,16 @@ class ProcessManager {
jarPath: profile.jarPath,
startedAt: Date.now(),
intentionallyStopped: false,
- }
- this.processes.set(profile.id, managed)
+ };
+ this.processes.set(profile.id, managed);
if (!this.lineCounters.has(profile.id)) {
- this.lineCounters.set(profile.id, 0)
- this.seenLineIds.set(profile.id, new Set())
+ this.lineCounters.set(profile.id, 0);
+ this.seenLineIds.set(profile.id, new Set());
}
- const pid = proc.pid ?? 0
- this.pushSystem('info-pid', profile.id, String(pid), `PID: ${pid}`)
+ const pid = proc.pid ?? 0;
+ this.pushSystem('info-pid', profile.id, String(pid), `PID: ${pid}`);
const logEntry: ProcessLogEntry = {
id: uuidv4(),
@@ -113,128 +113,128 @@ class ProcessManager {
jarPath: profile.jarPath,
pid,
startedAt: managed.startedAt,
- }
- this.activityLog.unshift(logEntry)
- if (this.activityLog.length > 500) this.activityLog.pop()
+ };
+ this.activityLog.unshift(logEntry);
+ if (this.activityLog.length > 500) this.activityLog.pop();
- proc.stdout?.setEncoding('utf8')
+ proc.stdout?.setEncoding('utf8');
proc.stdout?.on('data', (chunk: string) =>
this.pushOutput(profile.id, chunk, 'stdout', managed)
- )
- proc.stderr?.setEncoding('utf8')
+ );
+ proc.stderr?.setEncoding('utf8');
proc.stderr?.on('data', (chunk: string) =>
this.pushOutput(profile.id, chunk, 'stderr', managed)
- )
+ );
proc.on('error', (err) =>
this.pushSystem('error-runtime', profile.id, String(pid), `Error: ${err.message}`)
- )
+ );
proc.on('exit', (code, signal) => {
- this.processes.delete(profile.id)
+ this.processes.delete(profile.id);
this.pushSystem(
'stopped',
profile.id,
String(pid),
`Process stopped (${signal ? `signal ${signal}` : `exit code ${code ?? '?'}`})`
- )
- const entry = this.activityLog.find((e) => e.profileId === profile.id && !e.stoppedAt)
+ );
+ const entry = this.activityLog.find((e) => e.profileId === profile.id && !e.stoppedAt);
if (entry) {
- entry.stoppedAt = Date.now()
- entry.exitCode = code ?? undefined
- entry.signal = signal ?? undefined
+ entry.stoppedAt = Date.now();
+ entry.exitCode = code ?? undefined;
+ entry.signal = signal ?? undefined;
}
- this.broadcastStates()
- this.updateTray()
+ this.broadcastStates();
+ this.updateTray();
if (!managed.intentionallyStopped && code !== 0) {
- const snapshot = this.profileSnapshots.get(profile.id)
+ const snapshot = this.profileSnapshots.get(profile.id);
if (snapshot?.autoRestart) {
- const delaySec = Math.max(1, snapshot.autoRestartInterval ?? 10)
+ const delaySec = Math.max(1, snapshot.autoRestartInterval ?? 10);
this.pushSystem(
'info-restart',
profile.id,
String(pid),
`Auto-restart in ${delaySec}s...`
- )
+ );
const timer = setTimeout(() => {
- this.restartTimers.delete(profile.id)
- const latest = this.profileSnapshots.get(profile.id) ?? snapshot
- this.start(latest)
- }, delaySec * 1000)
- this.restartTimers.set(profile.id, timer)
+ this.restartTimers.delete(profile.id);
+ const latest = this.profileSnapshots.get(profile.id) ?? snapshot;
+ this.start(latest);
+ }, delaySec * 1000);
+ this.restartTimers.set(profile.id, timer);
}
}
- })
+ });
- this.broadcastStates()
- this.updateTray()
- return { ok: true }
+ this.broadcastStates();
+ this.updateTray();
+ return { ok: true };
}
stop(profileId: string): { ok: boolean; error?: string } {
- const m = this.processes.get(profileId)
- if (!m) return { ok: false, error: 'Not running' }
+ const m = this.processes.get(profileId);
+ if (!m) return { ok: false, error: 'Not running' };
- m.intentionallyStopped = true
- this.cancelRestartTimer(profileId)
+ m.intentionallyStopped = true;
+ this.cancelRestartTimer(profileId);
- this.pushSystem('stopping', profileId, String(m.process.pid ?? 0), 'Stopping process...')
- const pid = m.process.pid
+ this.pushSystem('stopping', profileId, String(m.process.pid ?? 0), 'Stopping process...');
+ const pid = m.process.pid;
if (process.platform === 'win32' && pid) {
try {
- execSync(`taskkill /PID ${pid} /T /F`, { timeout: 5000 })
+ execSync(`taskkill /PID ${pid} /T /F`, { timeout: 5000 });
} catch {
try {
- m.process.kill('SIGTERM')
+ m.process.kill('SIGTERM');
} catch {
/* ignore */
}
}
} else {
try {
- m.process.kill('SIGTERM')
+ m.process.kill('SIGTERM');
} catch {
/* ignore */
}
setTimeout(() => {
if (this.processes.has(profileId))
try {
- m.process.kill('SIGKILL')
+ m.process.kill('SIGKILL');
} catch {
/* ignore */
}
- }, 5000)
+ }, 5000);
}
- this.updateTray()
- return { ok: true }
+ this.updateTray();
+ return { ok: true };
}
updateProfileSnapshot(profile: Profile): void {
if (this.profileSnapshots.has(profile.id)) {
- this.profileSnapshots.set(profile.id, profile)
+ this.profileSnapshots.set(profile.id, profile);
}
}
clearConsoleForProfile(profileId: string): void {
- this.window?.webContents.send(ProcessIPC.consoleClear.channel, profileId)
+ this.window?.webContents.send(ProcessIPC.consoleClear.channel, profileId);
}
private cancelRestartTimer(profileId: string): void {
- const t = this.restartTimers.get(profileId)
+ const t = this.restartTimers.get(profileId);
if (t) {
- clearTimeout(t)
- this.restartTimers.delete(profileId)
+ clearTimeout(t);
+ this.restartTimers.delete(profileId);
}
}
sendInput(profileId: string, input: string): { ok: boolean; error?: string } {
- const m = this.processes.get(profileId)
- if (!m) return { ok: false, error: 'Not running' }
- m.process.stdin?.write(input.endsWith('\n') ? input : `${input}\n`)
- const counter = (this.lineCounters.get(profileId) ?? 0) + 1
- this.lineCounters.set(profileId, counter)
- this.pushLine(profileId, input, 'input', counter)
- return { ok: true }
+ const m = this.processes.get(profileId);
+ if (!m) return { ok: false, error: 'Not running' };
+ m.process.stdin?.write(input.endsWith('\n') ? input : `${input}\n`);
+ const counter = (this.lineCounters.get(profileId) ?? 0) + 1;
+ this.lineCounters.set(profileId, counter);
+ this.pushLine(profileId, input, 'input', counter);
+ return { ok: true };
}
getStates(): ProcessState[] {
@@ -243,14 +243,14 @@ class ProcessManager {
running: true,
pid: m.process.pid,
startedAt: m.startedAt,
- }))
+ }));
}
getActivityLog(): ProcessLogEntry[] {
- return this.activityLog
+ return this.activityLog;
}
clearActivityLog(): void {
- this.activityLog = []
+ this.activityLog = [];
}
// ── Process Scanner ──────────────────────────────────────────────────────────
@@ -259,17 +259,17 @@ class ProcessManager {
return PROTECTED_PROCESS_NAMES.some(
(n) =>
name.toLowerCase().includes(n.toLowerCase()) || cmd.toLowerCase().includes(n.toLowerCase())
- )
+ );
}
private isSelf(name: string, cmd: string): boolean {
- return cmd.includes(SELF_PROCESS_NAME) || name.includes(SELF_PROCESS_NAME)
+ return cmd.includes(SELF_PROCESS_NAME) || name.includes(SELF_PROCESS_NAME);
}
private parseJarName(cmd: string): string | undefined {
- const m = cmd.match(/-jar\s+([^\s]+)/i)
- if (!m) return undefined
- return m[1].split(/[/\\]/).pop()
+ const m = cmd.match(/-jar\s+([^\s]+)/i);
+ if (!m) return undefined;
+ return m[1].split(/[/\\]/).pop();
}
scanAllProcesses(): JavaProcessInfo[] {
@@ -277,9 +277,9 @@ class ProcessManager {
Array.from(this.processes.values())
.map((m) => m.process.pid)
.filter((p): p is number => p != null)
- )
- if (process.platform === 'win32') return this.scanAllWindows(managedPids)
- return this.scanAllUnix(managedPids)
+ );
+ if (process.platform === 'win32') return this.scanAllWindows(managedPids);
+ return this.scanAllUnix(managedPids);
}
private scanAllWindows(managedPids: Set): JavaProcessInfo[] {
@@ -293,30 +293,30 @@ class ProcessManager {
' $st = if ($_.StartTime) { $_.StartTime.ToString("yyyy-MM-dd HH:mm:ss") } else { "" }',
' [PSCustomObject]@{ Id=$_.Id; Name=$_.ProcessName; Cmd=$cmd; MemMB=$mem; Threads=$thr; Start=$st }',
'} | ConvertTo-Json -Compress -Depth 2',
- ].join('; ')
+ ].join('; ');
- const encoded = Buffer.from(psScript, 'utf16le').toString('base64')
+ const encoded = Buffer.from(psScript, 'utf16le').toString('base64');
try {
const raw_out = execSync(
`powershell -NoProfile -NonInteractive -OutputFormat Text -EncodedCommand ${encoded}`,
{ encoding: 'utf8', timeout: 20000 }
- )
+ );
const jsonLine = raw_out.split('\n').find((l) => {
- const t = l.trim()
- return t.startsWith('[') || t.startsWith('{')
- })
- if (!jsonLine) return this.scanAllWindowsTasklist(managedPids)
+ const t = l.trim();
+ return t.startsWith('[') || t.startsWith('{');
+ });
+ if (!jsonLine) return this.scanAllWindowsTasklist(managedPids);
- const raw = JSON.parse(jsonLine.trim())
- const procs = Array.isArray(raw) ? raw : [raw]
+ const raw = JSON.parse(jsonLine.trim());
+ const procs = Array.isArray(raw) ? raw : [raw];
return procs
.map((p: Record) => {
- const pid = Number(p.Id)
- const name = String(p.Name ?? '')
- const cmd = String(p.Cmd ?? name)
- if (isNaN(pid) || pid <= 0) return null
- if (this.isSelf(name, cmd)) return null
- const isJava = /java/i.test(name) || /java/i.test(cmd)
+ const pid = Number(p.Id);
+ const name = String(p.Name ?? '');
+ const cmd = String(p.Cmd ?? name);
+ if (isNaN(pid) || pid <= 0) return null;
+ if (this.isSelf(name, cmd)) return null;
+ const isJava = /java/i.test(name) || /java/i.test(cmd);
return {
pid,
name,
@@ -328,29 +328,29 @@ class ProcessManager {
threads: typeof p.Threads === 'number' ? p.Threads : undefined,
startTime: p.Start ? String(p.Start) : undefined,
jarName: this.parseJarName(cmd),
- } as JavaProcessInfo
+ } as JavaProcessInfo;
})
.filter((x): x is JavaProcessInfo => x !== null)
- .sort((a, b) => (b.isJava ? 1 : 0) - (a.isJava ? 1 : 0))
+ .sort((a, b) => (b.isJava ? 1 : 0) - (a.isJava ? 1 : 0));
} catch {
- return this.scanAllWindowsTasklist(managedPids)
+ return this.scanAllWindowsTasklist(managedPids);
}
}
private scanAllWindowsTasklist(managedPids: Set): JavaProcessInfo[] {
try {
- const out = execSync('tasklist /fo csv /nh', { encoding: 'utf8', timeout: 8000 })
- const results: JavaProcessInfo[] = []
+ const out = execSync('tasklist /fo csv /nh', { encoding: 'utf8', timeout: 8000 });
+ const results: JavaProcessInfo[] = [];
for (const line of out.split('\n')) {
- const t = line.trim()
- if (!t) continue
- const parts = t.replace(/"/g, '').split(',')
- if (parts.length < 2) continue
- const name = parts[0].trim()
- const pid = parseInt(parts[1].trim(), 10)
- if (isNaN(pid) || pid <= 0) continue
- if (this.isSelf(name, name)) continue
- const isJava = /java/i.test(name)
+ const t = line.trim();
+ if (!t) continue;
+ const parts = t.replace(/"/g, '').split(',');
+ if (parts.length < 2) continue;
+ const name = parts[0].trim();
+ const pid = parseInt(parts[1].trim(), 10);
+ if (isNaN(pid) || pid <= 0) continue;
+ if (this.isSelf(name, name)) continue;
+ const isJava = /java/i.test(name);
results.push({
pid,
name,
@@ -358,29 +358,29 @@ class ProcessManager {
isJava,
managed: managedPids.has(pid),
protected: this.isProtected(name, name),
- })
+ });
}
- return results.sort((a, b) => (b.isJava ? 1 : 0) - (a.isJava ? 1 : 0))
+ return results.sort((a, b) => (b.isJava ? 1 : 0) - (a.isJava ? 1 : 0));
} catch {
- return []
+ return [];
}
}
private scanAllUnix(managedPids: Set): JavaProcessInfo[] {
try {
- const out = execSync('ps -eo pid,comm,args', { encoding: 'utf8', timeout: 5000 })
+ const out = execSync('ps -eo pid,comm,args', { encoding: 'utf8', timeout: 5000 });
return out
.split('\n')
.slice(1)
.filter(Boolean)
.map((line) => {
- const parts = line.trim().split(/\s+/)
- const pid = parseInt(parts[0], 10)
- const name = parts[1] ?? ''
- const cmd = parts.slice(2).join(' ').slice(0, 400)
- if (isNaN(pid)) return null
- if (this.isSelf(name, cmd)) return null
- const isJava = /java/i.test(name) || /java/i.test(cmd)
+ const parts = line.trim().split(/\s+/);
+ const pid = parseInt(parts[0], 10);
+ const name = parts[1] ?? '';
+ const cmd = parts.slice(2).join(' ').slice(0, 400);
+ if (isNaN(pid)) return null;
+ if (this.isSelf(name, cmd)) return null;
+ const isJava = /java/i.test(name) || /java/i.test(cmd);
return {
pid,
name,
@@ -389,39 +389,39 @@ class ProcessManager {
managed: managedPids.has(pid),
protected: this.isProtected(name, cmd),
jarName: this.parseJarName(cmd),
- } as JavaProcessInfo
+ } as JavaProcessInfo;
})
.filter((x): x is JavaProcessInfo => x !== null)
- .sort((a, b) => (b.isJava ? 1 : 0) - (a.isJava ? 1 : 0))
+ .sort((a, b) => (b.isJava ? 1 : 0) - (a.isJava ? 1 : 0));
} catch {
- return []
+ return [];
}
}
killPid(pid: number): { ok: boolean; error?: string } {
try {
- if (process.platform === 'win32') execSync(`taskkill /PID ${pid} /T /F`, { timeout: 5000 })
- else process.kill(pid, 'SIGKILL')
+ if (process.platform === 'win32') execSync(`taskkill /PID ${pid} /T /F`, { timeout: 5000 });
+ else process.kill(pid, 'SIGKILL');
for (const [id, m] of this.processes.entries()) {
if (m.process.pid === pid) {
- this.processes.delete(id)
- break
+ this.processes.delete(id);
+ break;
}
}
- this.broadcastStates()
- this.updateTray()
- return { ok: true }
+ this.broadcastStates();
+ this.updateTray();
+ return { ok: true };
} catch (err: unknown) {
- return { ok: false, error: err instanceof Error ? err.message : String(err) }
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
}
}
// Only kills non-protected java processes
killAllJava(): { ok: boolean; killed: number } {
- const procs = this.scanAllProcesses().filter((p) => p.isJava && !p.protected)
- let killed = 0
- for (const p of procs) if (this.killPid(p.pid).ok) killed++
- return { ok: true, killed }
+ const procs = this.scanAllProcesses().filter((p) => p.isJava && !p.protected);
+ let killed = 0;
+ for (const p of procs) if (this.killPid(p.pid).ok) killed++;
+ return { ok: true, killed };
}
private pushOutput(
@@ -431,10 +431,10 @@ class ProcessManager {
m: ManagedProcess
) {
for (const [i, text] of chunk.split(/\r?\n/).entries()) {
- if (i === chunk.split(/\r?\n/).length - 1 && text === '') continue
- const counter = (this.lineCounters.get(profileId) ?? 0) + 1
- this.lineCounters.set(profileId, counter)
- this.pushLine(profileId, text, type, counter)
+ if (i === chunk.split(/\r?\n/).length - 1 && text === '') continue;
+ const counter = (this.lineCounters.get(profileId) ?? 0) + 1;
+ this.lineCounters.set(profileId, counter);
+ this.pushLine(profileId, text, type, counter);
}
}
@@ -444,14 +444,14 @@ class ProcessManager {
type: ConsoleLine['type'],
id: number | string
) {
- const seenIds = this.seenLineIds.get(profileId)
+ const seenIds = this.seenLineIds.get(profileId);
if (seenIds?.has(id)) {
- throw new Error(`Duplicate line ID detected for profile ${profileId}: ${id}`)
+ throw new Error(`Duplicate line ID detected for profile ${profileId}: ${id}`);
}
if (seenIds) {
- seenIds.add(id)
+ seenIds.add(id);
if (seenIds.size > 10000) {
- this.seenLineIds.set(profileId, new Set([id]))
+ this.seenLineIds.set(profileId, new Set([id]));
}
}
this.window?.webContents.send(ProcessIPC.consoleLine.channel, profileId, {
@@ -459,26 +459,26 @@ class ProcessManager {
text,
type,
timestamp: Date.now(),
- })
+ });
}
private pushSystem(_type: SystemMessageType, profileId: string, _pid: string, text: string) {
- const counter = (this.lineCounters.get(profileId) ?? 0) + 1
- this.lineCounters.set(profileId, counter)
- this.pushLine(profileId, text, 'system', counter)
+ const counter = (this.lineCounters.get(profileId) ?? 0) + 1;
+ this.lineCounters.set(profileId, counter);
+ this.pushLine(profileId, text, 'system', counter);
}
private broadcastStates() {
- this.window?.webContents.send('process:statesUpdate', this.getStates())
+ this.window?.webContents.send('process:statesUpdate', this.getStates());
}
setTrayUpdater(fn: () => void) {
- this.onTrayUpdate = fn
+ this.onTrayUpdate = fn;
}
private updateTray() {
- this.onTrayUpdate?.()
+ this.onTrayUpdate?.();
}
}
-export const processManager = new ProcessManager()
+export const processManager = new ProcessManager();
diff --git a/src/main/shared/config/API.config.ts b/src/main/shared/config/API.config.ts
index b445195..3042c03 100644
--- a/src/main/shared/config/API.config.ts
+++ b/src/main/shared/config/API.config.ts
@@ -1,11 +1,11 @@
// ─── Base types ───────────────────────────────────────────────────────────────
export type RouteDefinition = {
- method: 'GET' | 'POST' | 'PUT' | 'DELETE'
- path: string
- description: string
- bodyTemplate?: string
-}
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE';
+ path: string;
+ description: string;
+ bodyTemplate?: string;
+};
// ─── Strongly typed route map ─────────────────────────────────────────────────
@@ -80,6 +80,6 @@ export const routeConfig = {
description: 'Update settings',
bodyTemplate: '{ "consoleFontSize": 13 }',
},
-} as const satisfies Record
+} as const satisfies Record;
-export type RouteKey = keyof typeof routeConfig
+export type RouteKey = keyof typeof routeConfig;
diff --git a/src/main/shared/config/App.config.ts b/src/main/shared/config/App.config.ts
index afef533..791b885 100644
--- a/src/main/shared/config/App.config.ts
+++ b/src/main/shared/config/App.config.ts
@@ -1,5 +1,5 @@
-import type { AppSettings } from '../types/App.types'
-import { REST_API_CONFIG } from './RestApi.config'
+import type { AppSettings } from '../types/App.types';
+import { REST_API_CONFIG } from './RestApi.config';
export const DEFAULT_SETTINGS: AppSettings = {
launchOnStartup: false,
@@ -14,4 +14,4 @@ export const DEFAULT_SETTINGS: AppSettings = {
restApiEnabled: false,
restApiPort: REST_API_CONFIG.defaultPort,
devModeEnabled: false,
-}
+};
diff --git a/src/main/shared/config/FAQ.config.ts b/src/main/shared/config/FAQ.config.ts
index 1349bff..feb5611 100644
--- a/src/main/shared/config/FAQ.config.ts
+++ b/src/main/shared/config/FAQ.config.ts
@@ -1,11 +1,11 @@
export interface FaqItem {
- q: string
- a: string
+ q: string;
+ a: string;
}
export interface FaqTopic {
- id: string
- label: string
- items: FaqItem[]
+ id: string;
+ label: string;
+ items: FaqItem[];
}
export const FAQ_TOPICS: FaqTopic[] = [
@@ -97,4 +97,4 @@ export const FAQ_TOPICS: FaqTopic[] = [
},
],
},
-]
+];
diff --git a/src/main/shared/config/GitHub.config.ts b/src/main/shared/config/GitHub.config.ts
index 7b3d6f7..2c9875d 100644
--- a/src/main/shared/config/GitHub.config.ts
+++ b/src/main/shared/config/GitHub.config.ts
@@ -4,20 +4,20 @@ export const GITHUB_CONFIG = {
templatesPath: 'profile-templates',
templateMinVersion: 1,
apiBase: 'https://api.github.com',
-} as const
+} as const;
export function releasesUrl(): string {
- return `${GITHUB_CONFIG.apiBase}/repos/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}/releases`
+ return `${GITHUB_CONFIG.apiBase}/repos/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}/releases`;
}
export function latestReleaseUrl(): string {
- return `${GITHUB_CONFIG.apiBase}/repos/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}/releases/latest`
+ return `${GITHUB_CONFIG.apiBase}/repos/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}/releases/latest`;
}
export function templateListUrl(): string {
- return `${GITHUB_CONFIG.apiBase}/repos/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}/contents/${GITHUB_CONFIG.templatesPath}`
+ return `${GITHUB_CONFIG.apiBase}/repos/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}/contents/${GITHUB_CONFIG.templatesPath}`;
}
export function rawTemplateUrl(filename: string): string {
- return `https://raw.githubusercontent.com/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}/main/${GITHUB_CONFIG.templatesPath}/${filename}`
+ return `https://raw.githubusercontent.com/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}/main/${GITHUB_CONFIG.templatesPath}/${filename}`;
}
diff --git a/src/main/shared/config/RestApi.config.ts b/src/main/shared/config/RestApi.config.ts
index 9271f4a..c3b3eb2 100644
--- a/src/main/shared/config/RestApi.config.ts
+++ b/src/main/shared/config/RestApi.config.ts
@@ -1,7 +1,7 @@
export const REST_API_CONFIG = {
defaultPort: 4444,
host: '127.0.0.1',
-} as const
+} as const;
export const REST_ROUTES = {
// Info
@@ -20,4 +20,4 @@ export const REST_ROUTES = {
processStop: '/api/processes/:id/stop',
consoleClear: '/api/processes/:id/console/clear',
settingsUpdate: '/api/settings',
-} as const
+} as const;
diff --git a/src/main/shared/config/Scanner.config.ts b/src/main/shared/config/Scanner.config.ts
index eb7444a..7adc8ac 100644
--- a/src/main/shared/config/Scanner.config.ts
+++ b/src/main/shared/config/Scanner.config.ts
@@ -3,4 +3,4 @@
* and excluded from "Kill All Java".
* Add entries by exact process name (case-insensitive match).
*/
-export const PROTECTED_PROCESS_NAMES: string[] = ['Java Runner Client']
+export const PROTECTED_PROCESS_NAMES: string[] = ['Java Runner Client'];
diff --git a/src/main/shared/types/App.types.ts b/src/main/shared/types/App.types.ts
index 457bb03..06f77ff 100644
--- a/src/main/shared/types/App.types.ts
+++ b/src/main/shared/types/App.types.ts
@@ -1,21 +1,21 @@
export interface AppSettings {
- launchOnStartup: boolean
- startMinimized: boolean
- minimizeToTray: boolean
- consoleFontSize: number
- consoleMaxLines: number
- consoleWordWrap: boolean
- consoleLineNumbers: boolean
- consoleHistorySize: number
- theme: 'dark'
- restApiEnabled: boolean
- restApiPort: number
- devModeEnabled: boolean
+ launchOnStartup: boolean;
+ startMinimized: boolean;
+ minimizeToTray: boolean;
+ consoleFontSize: number;
+ consoleMaxLines: number;
+ consoleWordWrap: boolean;
+ consoleLineNumbers: boolean;
+ consoleHistorySize: number;
+ theme: 'dark';
+ restApiEnabled: boolean;
+ restApiPort: number;
+ devModeEnabled: boolean;
}
export interface JRCEnvironment {
- isReady: boolean
- devMode: boolean
- type: 'dev' | 'prod'
- startUpSource: 'userRequest' | 'withSystem' | 'development'
+ isReady: boolean;
+ devMode: boolean;
+ type: 'dev' | 'prod';
+ startUpSource: 'userRequest' | 'withSystem' | 'development';
}
diff --git a/src/main/shared/types/GitHub.types.ts b/src/main/shared/types/GitHub.types.ts
index 016069f..44cfa76 100644
--- a/src/main/shared/types/GitHub.types.ts
+++ b/src/main/shared/types/GitHub.types.ts
@@ -1,60 +1,60 @@
export interface GitHubAsset {
- id: number
- name: string
- label: string | null
- content_type: string
- state: string
- size: number
- download_count: number
- browser_download_url: string
- created_at: string
- updated_at: string
+ id: number;
+ name: string;
+ label: string | null;
+ content_type: string;
+ state: string;
+ size: number;
+ download_count: number;
+ browser_download_url: string;
+ created_at: string;
+ updated_at: string;
}
export interface GitHubRelease {
- id: number
- tag_name: string
- name: string | null
- body: string | null
- draft: boolean
- prerelease: boolean
- created_at: string
- published_at: string
- html_url: string
- tarball_url: string
- zipball_url: string
- assets: GitHubAsset[]
+ id: number;
+ tag_name: string;
+ name: string | null;
+ body: string | null;
+ draft: boolean;
+ prerelease: boolean;
+ created_at: string;
+ published_at: string;
+ html_url: string;
+ tarball_url: string;
+ zipball_url: string;
+ assets: GitHubAsset[];
author: {
- login: string
- avatar_url: string
- html_url: string
- }
+ login: string;
+ avatar_url: string;
+ html_url: string;
+ };
}
export interface ProfileTemplate {
- templateVersion: number
- minAppVersion: string
- name: string
- description: string
- category: string
- tags: string[]
+ templateVersion: number;
+ minAppVersion: string;
+ name: string;
+ description: string;
+ category: string;
+ tags: string[];
defaults: {
- jvmArgs: { value: string; enabled: boolean }[]
- systemProperties: { key: string; value: string; enabled: boolean }[]
- programArgs: { value: string; enabled: boolean }[]
- javaPath: string
- autoStart: boolean
- autoRestart: boolean
- autoRestartInterval: number
- color: string
- }
+ jvmArgs: { value: string; enabled: boolean }[];
+ systemProperties: { key: string; value: string; enabled: boolean }[];
+ programArgs: { value: string; enabled: boolean }[];
+ javaPath: string;
+ autoStart: boolean;
+ autoRestart: boolean;
+ autoRestartInterval: number;
+ color: string;
+ };
}
export interface DownloadProgress {
- filename: string
- bytesWritten: number
- totalBytes: number
- percent: number
- status: 'downloading' | 'paused' | 'done' | 'error' | 'cancelled'
- error?: string
+ filename: string;
+ bytesWritten: number;
+ totalBytes: number;
+ percent: number;
+ status: 'downloading' | 'paused' | 'done' | 'error' | 'cancelled';
+ error?: string;
}
diff --git a/src/main/shared/types/Process.types.ts b/src/main/shared/types/Process.types.ts
index a14e347..5bd8f7e 100644
--- a/src/main/shared/types/Process.types.ts
+++ b/src/main/shared/types/Process.types.ts
@@ -1,39 +1,39 @@
export interface ProcessState {
- profileId: string
- running: boolean
- pid?: number
- startedAt?: number
- exitCode?: number
+ profileId: string;
+ running: boolean;
+ pid?: number;
+ startedAt?: number;
+ exitCode?: number;
}
export interface ProcessLogEntry {
- id: string
- profileId: string
- profileName: string
- jarPath: string
- pid: number
- startedAt: number
- stoppedAt?: number
- exitCode?: number
- signal?: string
+ id: string;
+ profileId: string;
+ profileName: string;
+ jarPath: string;
+ pid: number;
+ startedAt: number;
+ stoppedAt?: number;
+ exitCode?: number;
+ signal?: string;
}
export interface JavaProcessInfo {
- pid: number
- name: string
- command: string
- isJava: boolean
- managed: boolean
- protected: boolean
- memoryMB?: number
- startTime?: string
- threads?: number
- jarName?: string
+ pid: number;
+ name: string;
+ command: string;
+ isJava: boolean;
+ managed: boolean;
+ protected: boolean;
+ memoryMB?: number;
+ startTime?: string;
+ threads?: number;
+ jarName?: string;
}
export interface ConsoleLine {
- id: number
- text: string
- type: 'stdout' | 'stderr' | 'input' | 'system'
- timestamp: number
+ id: number;
+ text: string;
+ type: 'stdout' | 'stderr' | 'input' | 'system';
+ timestamp: number;
}
diff --git a/src/main/shared/types/Profile.types.ts b/src/main/shared/types/Profile.types.ts
index 70d4be6..6a11c49 100644
--- a/src/main/shared/types/Profile.types.ts
+++ b/src/main/shared/types/Profile.types.ts
@@ -1,31 +1,31 @@
export interface SystemProperty {
- key: string
- value: string
- enabled: boolean
+ key: string;
+ value: string;
+ enabled: boolean;
}
export interface JvmArgument {
- value: string
- enabled: boolean
+ value: string;
+ enabled: boolean;
}
export interface ProgramArgument {
- value: string
- enabled: boolean
+ value: string;
+ enabled: boolean;
}
export interface Profile {
- id: string
- name: string
- jarPath: string
- workingDir: string
- jvmArgs: JvmArgument[]
- systemProperties: SystemProperty[]
- programArgs: ProgramArgument[]
- javaPath: string
- autoStart: boolean
- color: string
- createdAt: number
- updatedAt: number
- autoRestart: boolean
- autoRestartInterval: number
- order?: number
+ id: string;
+ name: string;
+ jarPath: string;
+ workingDir: string;
+ jvmArgs: JvmArgument[];
+ systemProperties: SystemProperty[];
+ programArgs: ProgramArgument[];
+ javaPath: string;
+ autoStart: boolean;
+ color: string;
+ createdAt: number;
+ updatedAt: number;
+ autoRestart: boolean;
+ autoRestartInterval: number;
+ order?: number;
}
diff --git a/src/main/store.ts b/src/main/store.ts
index 8d18d46..1269540 100644
--- a/src/main/store.ts
+++ b/src/main/store.ts
@@ -1,61 +1,61 @@
-import Store from 'electron-store'
-import { DEFAULT_SETTINGS } from './shared/config/App.config'
-import { Profile } from './shared/types/Profile.types'
-import { AppSettings } from './shared/types/App.types'
+import Store from 'electron-store';
+import { DEFAULT_SETTINGS } from './shared/config/App.config';
+import { Profile } from './shared/types/Profile.types';
+import { AppSettings } from './shared/types/App.types';
interface StoreSchema {
- profiles: Profile[]
- settings: AppSettings
+ profiles: Profile[];
+ settings: AppSettings;
}
const store = new Store({
name: 'java-runner-config',
defaults: { profiles: [], settings: DEFAULT_SETTINGS },
-})
+});
export function getAllProfiles(): Profile[] {
- const profiles = store.get('profiles', [])
- return [...profiles].sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
+ const profiles = store.get('profiles', []);
+ return [...profiles].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
}
export function saveProfile(profile: Profile): void {
- const profiles = getAllProfiles()
- const idx = profiles.findIndex((p) => p.id === profile.id)
- profile.updatedAt = Date.now()
- if (idx >= 0) profiles[idx] = profile
+ const profiles = getAllProfiles();
+ const idx = profiles.findIndex((p) => p.id === profile.id);
+ profile.updatedAt = Date.now();
+ if (idx >= 0) profiles[idx] = profile;
else {
- profile.order = profiles.length
- profiles.push(profile)
+ profile.order = profiles.length;
+ profiles.push(profile);
}
- store.set('profiles', profiles)
+ store.set('profiles', profiles);
}
export function deleteProfile(id: string): void {
store.set(
'profiles',
getAllProfiles().filter((p) => p.id !== id)
- )
+ );
}
export function reorderProfiles(orderedIds: string[]): void {
- const profiles = getAllProfiles()
+ const profiles = getAllProfiles();
const updated = profiles.map((p) => ({
...p,
order: orderedIds.indexOf(p.id),
- }))
- store.set('profiles', updated)
+ }));
+ store.set('profiles', updated);
}
export function toggleDevMode(enabled: boolean): void {
- const settings = getSettings()
- settings.devModeEnabled = enabled
- saveSettings(settings)
+ const settings = getSettings();
+ settings.devModeEnabled = enabled;
+ saveSettings(settings);
}
export function getSettings(): AppSettings {
- return { ...DEFAULT_SETTINGS, ...store.get('settings', DEFAULT_SETTINGS) }
+ return { ...DEFAULT_SETTINGS, ...store.get('settings', DEFAULT_SETTINGS) };
}
export function saveSettings(settings: AppSettings): void {
- store.set('settings', settings)
+ store.set('settings', settings);
}
diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx
index 98f2113..621c7ba 100644
--- a/src/renderer/App.tsx
+++ b/src/renderer/App.tsx
@@ -1,8 +1,8 @@
-import { HashRouter, Navigate, Route, Routes } from 'react-router-dom'
-import { TitleBar } from './components/common/TitleBar'
-import { DevModeGate } from './components/developer/DevModeGate'
-import { MainLayout } from './components/MainLayout'
-import { AppProvider } from './store/AppStore'
+import { HashRouter, Navigate, Route, Routes } from 'react-router-dom';
+import { TitleBar } from './components/common/TitleBar';
+import { DevModeGate } from './components/developer/DevModeGate';
+import { MainLayout } from './components/MainLayout';
+import { AppProvider } from './store/AppStore';
function JavaRunnerFallback() {
return (
@@ -20,11 +20,11 @@ function JavaRunnerFallback() {
window.api is undefined
- )
+ );
}
export default function App() {
- if (!window.api) return
+ if (!window.api) return ;
return (
@@ -44,5 +44,5 @@ export default function App() {
- )
+ );
}
diff --git a/src/renderer/components/MainLayout.tsx b/src/renderer/components/MainLayout.tsx
index 51a6f02..c864f84 100644
--- a/src/renderer/components/MainLayout.tsx
+++ b/src/renderer/components/MainLayout.tsx
@@ -1,30 +1,30 @@
-import React, { useEffect, useState } from 'react'
-import { Routes, Route, Navigate, useNavigate, useLocation } from 'react-router-dom'
-import { ProfileSidebar } from './profiles/ProfileSidebar'
-import { ConsoleTab } from './console/ConsoleTab'
-import { ConfigTab } from './profiles/ConfigTab'
-import { ProfileTab } from './profiles/ProfileTab'
-import { SettingsTab } from './settings/SettingsTab'
-import { UtilitiesTab } from './utils/UtilitiesTab'
-import { FaqPanel } from './faq/FaqPanel'
-import { DeveloperTab } from './developer/DeveloperTab'
-import { useApp } from '../store/AppStore'
-import { useDevMode } from '../hooks/useDevMode'
-import { VscTerminal, VscAccount } from 'react-icons/vsc'
-import { LuList } from 'react-icons/lu'
-import { JRCEnvironment } from 'src/main/shared/types/App.types'
+import React, { useEffect, useState } from 'react';
+import { Routes, Route, Navigate, useNavigate, useLocation } from 'react-router-dom';
+import { ProfileSidebar } from './profiles/ProfileSidebar';
+import { ConsoleTab } from './console/ConsoleTab';
+import { ConfigTab } from './profiles/ConfigTab';
+import { ProfileTab } from './profiles/ProfileTab';
+import { SettingsTab } from './settings/SettingsTab';
+import { UtilitiesTab } from './utils/UtilitiesTab';
+import { FaqPanel } from './faq/FaqPanel';
+import { DeveloperTab } from './developer/DeveloperTab';
+import { useApp } from '../store/AppStore';
+import { useDevMode } from '../hooks/useDevMode';
+import { VscTerminal, VscAccount } from 'react-icons/vsc';
+import { LuList } from 'react-icons/lu';
+import { JRCEnvironment } from 'src/main/shared/types/App.types';
const MAIN_TABS = [
{ path: 'console', label: 'Console', Icon: VscTerminal },
{ path: 'config', label: 'Configure', Icon: LuList },
{ path: 'profile', label: 'Profile', Icon: VscAccount },
-] as const
+] as const;
-const SIDE_PANELS = ['settings', 'faq', 'utilities', 'developer'] as const
-type SidePanel = (typeof SIDE_PANELS)[number]
+const SIDE_PANELS = ['settings', 'faq', 'utilities', 'developer'] as const;
+type SidePanel = (typeof SIDE_PANELS)[number];
function isSidePanel(seg: string): seg is SidePanel {
- return (SIDE_PANELS as readonly string[]).includes(seg)
+ return (SIDE_PANELS as readonly string[]).includes(seg);
}
const PANEL_LABELS: Record = {
@@ -32,45 +32,45 @@ const PANEL_LABELS: Record = {
faq: 'FAQ',
utilities: 'Utilities',
developer: 'Developer',
-}
+};
export function MainLayout() {
- const { state, activeProfile, isRunning, setActiveProfile } = useApp()
- const devMode = useDevMode()
- const navigate = useNavigate()
- const location = useLocation()
+ const { state, activeProfile, isRunning, setActiveProfile } = useApp();
+ const devMode = useDevMode();
+ const navigate = useNavigate();
+ const location = useLocation();
- const segments = location.pathname.replace(/^\//, '').split('/')
- const firstSeg = segments[0] ?? 'console'
- const activePanel = isSidePanel(firstSeg) ? firstSeg : null
- const activeTab = activePanel ? 'console' : firstSeg
+ const segments = location.pathname.replace(/^\//, '').split('/');
+ const firstSeg = segments[0] ?? 'console';
+ const activePanel = isSidePanel(firstSeg) ? firstSeg : null;
+ const activeTab = activePanel ? 'console' : firstSeg;
- const color = activeProfile?.color ?? '#4ade80'
- const running = activeProfile ? isRunning(activeProfile.id) : false
+ const color = activeProfile?.color ?? '#4ade80';
+ const running = activeProfile ? isRunning(activeProfile.id) : false;
// Redirect away from developer panel if dev mode is turned off
useEffect(() => {
if (!devMode && activePanel === 'developer') {
- navigate('console', { replace: true })
+ navigate('console', { replace: true });
}
- }, [devMode, activePanel, navigate])
+ }, [devMode, activePanel, navigate]);
// When profile changes, go to console
- const prevIdRef = React.useRef(state.activeProfileId)
+ const prevIdRef = React.useRef(state.activeProfileId);
useEffect(() => {
if (state.activeProfileId !== prevIdRef.current) {
- prevIdRef.current = state.activeProfileId
- if (!activePanel) navigate('console', { replace: true })
+ prevIdRef.current = state.activeProfileId;
+ if (!activePanel) navigate('console', { replace: true });
}
- }, [state.activeProfileId, activePanel, navigate])
+ }, [state.activeProfileId, activePanel, navigate]);
const openPanel = (panel: SidePanel) => {
- navigate(activePanel === panel ? 'console' : panel)
- }
+ navigate(activePanel === panel ? 'console' : panel);
+ };
const handleProfileClick = () => {
- if (activePanel) navigate('console')
- }
+ if (activePanel) navigate('console');
+ };
return (
@@ -126,7 +126,7 @@ export function MainLayout() {
<>
{MAIN_TABS.map((tab) => {
- const isActive = activeTab === tab.path
+ const isActive = activeTab === tab.path;
return (
)}
- )
+ );
})}
{activeProfile && (
@@ -170,5 +170,5 @@ export function MainLayout() {
)}
- )
+ );
}
diff --git a/src/renderer/components/common/ArgList.tsx b/src/renderer/components/common/ArgList.tsx
index 7f3dcef..9c3a22d 100644
--- a/src/renderer/components/common/ArgList.tsx
+++ b/src/renderer/components/common/ArgList.tsx
@@ -1,39 +1,39 @@
-import React, { useState } from 'react'
-import { Button } from './Button'
+import React, { useState } from 'react';
+import { Button } from './Button';
export interface ArgItem {
- value: string
- enabled: boolean
+ value: string;
+ enabled: boolean;
}
interface Props {
- items: ArgItem[]
- onChange: (items: ArgItem[]) => void
- onPendingChange?: (hasPending: boolean) => void
- placeholder?: string
+ items: ArgItem[];
+ onChange: (items: ArgItem[]) => void;
+ onPendingChange?: (hasPending: boolean) => void;
+ placeholder?: string;
}
export function ArgList({ items, onChange, onPendingChange, placeholder = '--arg' }: Props) {
- const [draft, setDraft] = useState('')
+ const [draft, setDraft] = useState('');
const setDraftAndNotify = (v: string) => {
- setDraft(v)
- onPendingChange?.(v.trim().length > 0)
- }
+ setDraft(v);
+ onPendingChange?.(v.trim().length > 0);
+ };
const add = () => {
- const v = draft.trim()
- if (!v) return
- onChange([...items, { value: v, enabled: true }])
- setDraft('')
- onPendingChange?.(false)
- }
+ const v = draft.trim();
+ if (!v) return;
+ onChange([...items, { value: v, enabled: true }]);
+ setDraft('');
+ onPendingChange?.(false);
+ };
- const remove = (i: number) => onChange(items.filter((_, idx) => idx !== i))
+ const remove = (i: number) => onChange(items.filter((_, idx) => idx !== i));
const toggle = (i: number) =>
- onChange(items.map((it, idx) => (idx === i ? { ...it, enabled: !it.enabled } : it)))
+ onChange(items.map((it, idx) => (idx === i ? { ...it, enabled: !it.enabled } : it)));
const edit = (i: number, value: string) =>
- onChange(items.map((it, idx) => (idx === i ? { ...it, value } : it)))
+ onChange(items.map((it, idx) => (idx === i ? { ...it, value } : it)));
return (
@@ -104,5 +104,5 @@ export function ArgList({ items, onChange, onPendingChange, placeholder = '--arg
- )
+ );
}
diff --git a/src/renderer/components/common/Button.tsx b/src/renderer/components/common/Button.tsx
index dcec728..5307bdc 100644
--- a/src/renderer/components/common/Button.tsx
+++ b/src/renderer/components/common/Button.tsx
@@ -1,9 +1,9 @@
-import React from 'react'
+import React from 'react';
interface Props extends React.ButtonHTMLAttributes {
- variant?: 'primary' | 'ghost' | 'danger'
- size?: 'sm' | 'md'
- loading?: boolean
+ variant?: 'primary' | 'ghost' | 'danger';
+ size?: 'sm' | 'md';
+ loading?: boolean;
}
export function Button({
@@ -16,14 +16,14 @@ export function Button({
...rest
}: Props) {
const base =
- 'inline-flex items-center justify-center gap-1.5 rounded-md border font-mono transition-colors focus:outline-none disabled:opacity-40 disabled:cursor-not-allowed'
- const sizes = { sm: 'px-2.5 py-1 text-xs', md: 'px-3.5 py-1.5 text-sm' }
+ 'inline-flex items-center justify-center gap-1.5 rounded-md border font-mono transition-colors focus:outline-none disabled:opacity-40 disabled:cursor-not-allowed';
+ const sizes = { sm: 'px-2.5 py-1 text-xs', md: 'px-3.5 py-1.5 text-sm' };
const variants = {
primary: 'bg-accent border-accent text-base-950 hover:brightness-110',
ghost:
'bg-transparent border-surface-border text-text-secondary hover:text-text-primary hover:border-text-muted',
danger: 'bg-transparent border-red-500/30 text-red-400 hover:border-red-400 hover:text-red-300',
- }
+ };
return (
{loading ? ... : children}
- )
+ );
}
diff --git a/src/renderer/components/common/ContextMenu.tsx b/src/renderer/components/common/ContextMenu.tsx
index 50ae5b2..60f53a4 100644
--- a/src/renderer/components/common/ContextMenu.tsx
+++ b/src/renderer/components/common/ContextMenu.tsx
@@ -1,61 +1,61 @@
-import React, { useEffect, useRef } from 'react'
+import React, { useEffect, useRef } from 'react';
export interface ContextMenuItem {
- label?: string
- icon?: React.ReactNode
- danger?: boolean
- disabled?: boolean
- type?: 'separator'
- onClick?: (e: React.MouseEvent) => void
+ label?: string;
+ icon?: React.ReactNode;
+ danger?: boolean;
+ disabled?: boolean;
+ type?: 'separator';
+ onClick?: (e: React.MouseEvent) => void;
}
interface Props {
- x: number
- y: number
- items: ContextMenuItem[]
- onClose: () => void
+ x: number;
+ y: number;
+ items: ContextMenuItem[];
+ onClose: () => void;
}
export function ContextMenu({ x, y, items, onClose }: Props) {
- const ref = useRef(null)
+ const ref = useRef(null);
useEffect(() => {
const handleClick = (e: MouseEvent) => {
- if (!ref.current) return
+ if (!ref.current) return;
// only close if clicking OUTSIDE
if (!ref.current.contains(e.target as Node)) {
- onClose()
+ onClose();
}
- }
+ };
const handleKey = (e: KeyboardEvent) => {
- if (e.key === 'Escape') onClose()
- }
+ if (e.key === 'Escape') onClose();
+ };
- document.addEventListener('click', handleClick)
- document.addEventListener('keydown', handleKey)
+ document.addEventListener('click', handleClick);
+ document.addEventListener('keydown', handleKey);
return () => {
- document.removeEventListener('click', handleClick)
- document.removeEventListener('keydown', handleKey)
- }
- }, [onClose])
+ document.removeEventListener('click', handleClick);
+ document.removeEventListener('keydown', handleKey);
+ };
+ }, [onClose]);
- const style: React.CSSProperties = { position: 'fixed', zIndex: 1000 }
+ const style: React.CSSProperties = { position: 'fixed', zIndex: 1000 };
- if (x + 180 > window.innerWidth) style.right = window.innerWidth - x
- else style.left = x
+ if (x + 180 > window.innerWidth) style.right = window.innerWidth - x;
+ else style.left = x;
- if (y + items.length * 32 > window.innerHeight) style.bottom = window.innerHeight - y
- else style.top = y
+ if (y + items.length * 32 > window.innerHeight) style.bottom = window.innerHeight - y;
+ else style.top = y;
return (
{items.map((item, i) => {
if (item.type === 'separator') {
- return
+ return
;
}
return (
@@ -64,9 +64,9 @@ export function ContextMenu({ x, y, items, onClose }: Props) {
disabled={item.disabled}
onMouseDown={(e) => e.preventDefault()} // prevent focus jump
onClick={(e) => {
- if (item.disabled || !item.onClick) return
- item.onClick(e)
- onClose()
+ if (item.disabled || !item.onClick) return;
+ item.onClick(e);
+ onClose();
}}
className={[
'w-full flex items-center gap-2.5 px-3 py-1.5 text-xs text-left transition-colors',
@@ -80,9 +80,9 @@ export function ContextMenu({ x, y, items, onClose }: Props) {
{item.icon &&
{item.icon} }
{item.label}
- )
+ );
})}
- )
+ );
}
diff --git a/src/renderer/components/common/Dialog.tsx b/src/renderer/components/common/Dialog.tsx
index 26aee4e..fbffe54 100644
--- a/src/renderer/components/common/Dialog.tsx
+++ b/src/renderer/components/common/Dialog.tsx
@@ -1,15 +1,15 @@
-import React from 'react'
-import { Button } from './Button'
+import React from 'react';
+import { Button } from './Button';
interface Props {
- open: boolean
- title: string
- message: string
- confirmLabel?: string
- cancelLabel?: string
- danger?: boolean
- onConfirm: () => void
- onCancel: () => void
+ open: boolean;
+ title: string;
+ message: string;
+ confirmLabel?: string;
+ cancelLabel?: string;
+ danger?: boolean;
+ onConfirm: () => void;
+ onCancel: () => void;
}
export function Dialog({
@@ -22,7 +22,7 @@ export function Dialog({
onConfirm,
onCancel,
}: Props) {
- if (!open) return null
+ if (!open) return null;
return (
@@ -38,5 +38,5 @@ export function Dialog({
- )
+ );
}
diff --git a/src/renderer/components/common/Input.tsx b/src/renderer/components/common/Input.tsx
index eef09e4..cafb17c 100644
--- a/src/renderer/components/common/Input.tsx
+++ b/src/renderer/components/common/Input.tsx
@@ -1,9 +1,9 @@
-import React from 'react'
+import React from 'react';
interface Props extends React.InputHTMLAttributes {
- label?: string
- hint?: string
- rightElement?: React.ReactNode
+ label?: string;
+ hint?: string;
+ rightElement?: React.ReactNode;
}
export function Input({ label, hint, rightElement, className = '', ...rest }: Props) {
@@ -26,5 +26,5 @@ export function Input({ label, hint, rightElement, className = '', ...rest }: Pr
{hint && {hint}
}
- )
+ );
}
diff --git a/src/renderer/components/common/Modal.tsx b/src/renderer/components/common/Modal.tsx
index b44560d..7477a1f 100644
--- a/src/renderer/components/common/Modal.tsx
+++ b/src/renderer/components/common/Modal.tsx
@@ -1,12 +1,12 @@
-import React, { useEffect, useRef } from 'react'
-import { VscClose } from 'react-icons/vsc'
+import React, { useEffect, useRef } from 'react';
+import { VscClose } from 'react-icons/vsc';
interface Props {
- open: boolean
- title: string
- onClose: () => void
- width?: 'sm' | 'md' | 'lg' | 'xl'
- children: React.ReactNode
+ open: boolean;
+ title: string;
+ onClose: () => void;
+ width?: 'sm' | 'md' | 'lg' | 'xl';
+ children: React.ReactNode;
}
const WIDTHS = {
@@ -14,27 +14,27 @@ const WIDTHS = {
md: 'max-w-md',
lg: 'max-w-lg',
xl: 'max-w-2xl',
-}
+};
export function Modal({ open, title, onClose, width = 'md', children }: Props) {
- const ref = useRef(null)
+ const ref = useRef(null);
useEffect(() => {
- if (!open) return
+ if (!open) return;
const handler = (e: KeyboardEvent) => {
- if (e.key === 'Escape') onClose()
- }
- window.addEventListener('keydown', handler)
- return () => window.removeEventListener('keydown', handler)
- }, [open, onClose])
+ if (e.key === 'Escape') onClose();
+ };
+ window.addEventListener('keydown', handler);
+ return () => window.removeEventListener('keydown', handler);
+ }, [open, onClose]);
- if (!open) return null
+ if (!open) return null;
return (
{
- if (e.target === e.currentTarget) onClose()
+ if (e.target === e.currentTarget) onClose();
}}
>
{children}
- )
+ );
}
diff --git a/src/renderer/components/common/PropList.tsx b/src/renderer/components/common/PropList.tsx
index 3c4d901..474b0ff 100644
--- a/src/renderer/components/common/PropList.tsx
+++ b/src/renderer/components/common/PropList.tsx
@@ -1,48 +1,48 @@
-import React, { useState } from 'react'
-import { Button } from './Button'
+import React, { useState } from 'react';
+import { Button } from './Button';
export interface PropItem {
- key: string
- value: string
- enabled: boolean
+ key: string;
+ value: string;
+ enabled: boolean;
}
interface Props {
- items: PropItem[]
- onChange: (items: PropItem[]) => void
- onPendingChange?: (hasPending: boolean) => void
+ items: PropItem[];
+ onChange: (items: PropItem[]) => void;
+ onPendingChange?: (hasPending: boolean) => void;
}
export function PropList({ items, onChange, onPendingChange }: Props) {
- const [draftKey, setDraftKey] = useState('')
- const [draftValue, setDraftValue] = useState('')
+ const [draftKey, setDraftKey] = useState('');
+ const [draftValue, setDraftValue] = useState('');
const notify = (k: string, v: string) =>
- onPendingChange?.(k.trim().length > 0 || v.trim().length > 0)
+ onPendingChange?.(k.trim().length > 0 || v.trim().length > 0);
const setKey = (v: string) => {
- setDraftKey(v)
- notify(v, draftValue)
- }
+ setDraftKey(v);
+ notify(v, draftValue);
+ };
const setVal = (v: string) => {
- setDraftValue(v)
- notify(draftKey, v)
- }
+ setDraftValue(v);
+ notify(draftKey, v);
+ };
const add = () => {
- if (!draftKey.trim()) return
- onChange([...items, { key: draftKey.trim(), value: draftValue.trim(), enabled: true }])
- setDraftKey('')
- setDraftValue('')
- onPendingChange?.(false)
- }
+ if (!draftKey.trim()) return;
+ onChange([...items, { key: draftKey.trim(), value: draftValue.trim(), enabled: true }]);
+ setDraftKey('');
+ setDraftValue('');
+ onPendingChange?.(false);
+ };
- const remove = (i: number) => onChange(items.filter((_, idx) => idx !== i))
+ const remove = (i: number) => onChange(items.filter((_, idx) => idx !== i));
const toggle = (i: number) =>
- onChange(items.map((it, idx) => (idx === i ? { ...it, enabled: !it.enabled } : it)))
+ onChange(items.map((it, idx) => (idx === i ? { ...it, enabled: !it.enabled } : it)));
const editKey = (i: number, key: string) =>
- onChange(items.map((it, idx) => (idx === i ? { ...it, key } : it)))
+ onChange(items.map((it, idx) => (idx === i ? { ...it, key } : it)));
const editValue = (i: number, value: string) =>
- onChange(items.map((it, idx) => (idx === i ? { ...it, value } : it)))
+ onChange(items.map((it, idx) => (idx === i ? { ...it, value } : it)));
return (
@@ -146,5 +146,5 @@ export function PropList({ items, onChange, onPendingChange }: Props) {
- )
+ );
}
diff --git a/src/renderer/components/common/TitleBar.tsx b/src/renderer/components/common/TitleBar.tsx
index a68ff7a..7168620 100644
--- a/src/renderer/components/common/TitleBar.tsx
+++ b/src/renderer/components/common/TitleBar.tsx
@@ -1,10 +1,10 @@
-import React from 'react'
-import { useApp } from '../../store/AppStore'
-import { VscChromeMinimize, VscChromeClose } from 'react-icons/vsc'
+import React from 'react';
+import { useApp } from '../../store/AppStore';
+import { VscChromeMinimize, VscChromeClose } from 'react-icons/vsc';
export function TitleBar() {
- const { state } = useApp()
- const runningCount = state.processStates.filter((s) => s.running).length
+ const { state } = useApp();
+ const runningCount = state.processStates.filter((s) => s.running).length;
return (
- )
+ );
}
diff --git a/src/renderer/components/common/Toggle.tsx b/src/renderer/components/common/Toggle.tsx
index 463f731..ed22c64 100644
--- a/src/renderer/components/common/Toggle.tsx
+++ b/src/renderer/components/common/Toggle.tsx
@@ -1,9 +1,9 @@
-import React from 'react'
+import React from 'react';
interface Props {
- checked: boolean
- onChange: (v: boolean) => void
- disabled?: boolean
+ checked: boolean;
+ onChange: (v: boolean) => void;
+ disabled?: boolean;
}
export function Toggle({ checked, onChange, disabled }: Props) {
@@ -25,5 +25,5 @@ export function Toggle({ checked, onChange, disabled }: Props) {
].join(' ')}
/>
- )
+ );
}
diff --git a/src/renderer/components/common/Tooltip.tsx b/src/renderer/components/common/Tooltip.tsx
index a44a2c0..1eacc2c 100644
--- a/src/renderer/components/common/Tooltip.tsx
+++ b/src/renderer/components/common/Tooltip.tsx
@@ -1,99 +1,99 @@
-import React, { useState, useRef, useEffect } from 'react'
+import React, { useState, useRef, useEffect } from 'react';
interface Props {
- content: React.ReactNode
- children: React.ReactElement
- delay?: number
- side?: 'top' | 'bottom' | 'left' | 'right'
+ content: React.ReactNode;
+ children: React.ReactElement;
+ delay?: number;
+ side?: 'top' | 'bottom' | 'left' | 'right';
}
export function Tooltip({ content, children, delay = 400, side = 'top' }: Props) {
- const [visible, setVisible] = useState(false)
- const [pos, setPos] = useState({ x: 0, y: 0 })
- const timerRef = useRef | null>(null)
- const targetRef = useRef(null)
- const tipRef = useRef(null)
+ const [visible, setVisible] = useState(false);
+ const [pos, setPos] = useState({ x: 0, y: 0 });
+ const timerRef = useRef | null>(null);
+ const targetRef = useRef(null);
+ const tipRef = useRef(null);
const show = (e: React.MouseEvent) => {
- const rect = (e.currentTarget as HTMLElement).getBoundingClientRect()
- targetRef.current = e.currentTarget as HTMLElement
+ const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
+ targetRef.current = e.currentTarget as HTMLElement;
timerRef.current = setTimeout(() => {
- const tip = tipRef.current
- const tw = tip?.offsetWidth ?? 0
- const th = tip?.offsetHeight ?? 0
- let x = 0
- let y = 0
+ const tip = tipRef.current;
+ const tw = tip?.offsetWidth ?? 0;
+ const th = tip?.offsetHeight ?? 0;
+ let x = 0;
+ let y = 0;
if (side === 'top') {
- x = rect.left + rect.width / 2 - tw / 2
- y = rect.top - th - 6
+ x = rect.left + rect.width / 2 - tw / 2;
+ y = rect.top - th - 6;
}
if (side === 'bottom') {
- x = rect.left + rect.width / 2 - tw / 2
- y = rect.bottom + 6
+ x = rect.left + rect.width / 2 - tw / 2;
+ y = rect.bottom + 6;
}
if (side === 'left') {
- x = rect.left - tw - 6
- y = rect.top + rect.height / 2 - th / 2
+ x = rect.left - tw - 6;
+ y = rect.top + rect.height / 2 - th / 2;
}
if (side === 'right') {
- x = rect.right + 6
- y = rect.top + rect.height / 2 - th / 2
+ x = rect.right + 6;
+ y = rect.top + rect.height / 2 - th / 2;
}
// Clamp to viewport
- x = Math.max(6, Math.min(window.innerWidth - tw - 6, x))
- y = Math.max(6, Math.min(window.innerHeight - th - 6, y))
- setPos({ x, y })
- setVisible(true)
- }, delay)
- }
+ x = Math.max(6, Math.min(window.innerWidth - tw - 6, x));
+ y = Math.max(6, Math.min(window.innerHeight - th - 6, y));
+ setPos({ x, y });
+ setVisible(true);
+ }, delay);
+ };
const hide = () => {
- if (timerRef.current) clearTimeout(timerRef.current)
- setVisible(false)
- }
+ if (timerRef.current) clearTimeout(timerRef.current);
+ setVisible(false);
+ };
useEffect(
() => () => {
- if (timerRef.current) clearTimeout(timerRef.current)
+ if (timerRef.current) clearTimeout(timerRef.current);
},
[]
- )
+ );
// Recalculate position when tip dimensions are known
useEffect(() => {
- if (!visible || !targetRef.current) return
- const rect = targetRef.current.getBoundingClientRect()
- const tip = tipRef.current
- if (!tip) return
- const tw = tip.offsetWidth
- const th = tip.offsetHeight
- let x = 0
- let y = 0
+ if (!visible || !targetRef.current) return;
+ const rect = targetRef.current.getBoundingClientRect();
+ const tip = tipRef.current;
+ if (!tip) return;
+ const tw = tip.offsetWidth;
+ const th = tip.offsetHeight;
+ let x = 0;
+ let y = 0;
if (side === 'top') {
- x = rect.left + rect.width / 2 - tw / 2
- y = rect.top - th - 6
+ x = rect.left + rect.width / 2 - tw / 2;
+ y = rect.top - th - 6;
}
if (side === 'bottom') {
- x = rect.left + rect.width / 2 - tw / 2
- y = rect.bottom + 6
+ x = rect.left + rect.width / 2 - tw / 2;
+ y = rect.bottom + 6;
}
if (side === 'left') {
- x = rect.left - tw - 6
- y = rect.top + rect.height / 2 - th / 2
+ x = rect.left - tw - 6;
+ y = rect.top + rect.height / 2 - th / 2;
}
if (side === 'right') {
- x = rect.right + 6
- y = rect.top + rect.height / 2 - th / 2
+ x = rect.right + 6;
+ y = rect.top + rect.height / 2 - th / 2;
}
- x = Math.max(6, Math.min(window.innerWidth - tw - 6, x))
- y = Math.max(6, Math.min(window.innerHeight - th - 6, y))
- setPos({ x, y })
- }, [visible, side])
+ x = Math.max(6, Math.min(window.innerWidth - tw - 6, x));
+ y = Math.max(6, Math.min(window.innerHeight - th - 6, y));
+ setPos({ x, y });
+ }, [visible, side]);
const child = React.cloneElement(children, {
onMouseEnter: show,
onMouseLeave: hide,
- })
+ });
return (
<>
@@ -110,5 +110,5 @@ export function Tooltip({ content, children, delay = 400, side = 'top' }: Props)
{content}
>
- )
+ );
}
diff --git a/src/renderer/components/console/ConsoleTab.tsx b/src/renderer/components/console/ConsoleTab.tsx
index d6a4573..a7faded 100644
--- a/src/renderer/components/console/ConsoleTab.tsx
+++ b/src/renderer/components/console/ConsoleTab.tsx
@@ -1,182 +1,182 @@
-import React, { useRef, useEffect, useState, useCallback, useMemo, KeyboardEvent } from 'react'
-import { useApp } from '../../store/AppStore'
-import { Button } from '../common/Button'
-import { VscSearch, VscChevronUp, VscChevronDown, VscClose } from 'react-icons/vsc'
-import { ConsoleLine } from '../../../main/shared/types/Process.types'
+import React, { useRef, useEffect, useState, useCallback, useMemo, KeyboardEvent } from 'react';
+import { useApp } from '../../store/AppStore';
+import { Button } from '../common/Button';
+import { VscSearch, VscChevronUp, VscChevronDown, VscClose } from 'react-icons/vsc';
+import { ConsoleLine } from '../../../main/shared/types/Process.types';
export function ConsoleTab() {
const { state, activeProfile, startProcess, stopProcess, sendInput, clearConsole, isRunning } =
- useApp()
-
- const profileId = activeProfile?.id ?? ''
- const running = isRunning(profileId)
- const lines = state.consoleLogs[profileId] ?? []
- const settings = state.settings
- const color = activeProfile?.color ?? '#4ade80'
- const processState = state.processStates.find((s) => s.profileId === profileId)
- const pid = processState?.pid
-
- const [inputValue, setInputValue] = useState('')
- const [historyIdx, setHistoryIdx] = useState(-1)
- const [cmdHistory, setCmdHistory] = useState([])
- const [autoScroll, setAutoScroll] = useState(true)
- const [starting, setStarting] = useState(false)
- const [errorMsg, setErrorMsg] = useState(null)
- const [searchOpen, setSearchOpen] = useState(false)
- const [searchQuery, setSearchQuery] = useState('')
- const [searchIdx, setSearchIdx] = useState(0)
-
- const scrollRef = useRef(null)
- const inputRef = useRef(null)
- const searchRef = useRef(null)
- const bottomRef = useRef(null)
- const matchRefs = useRef<(HTMLDivElement | null)[]>([])
+ useApp();
+
+ const profileId = activeProfile?.id ?? '';
+ const running = isRunning(profileId);
+ const lines = state.consoleLogs[profileId] ?? [];
+ const settings = state.settings;
+ const color = activeProfile?.color ?? '#4ade80';
+ const processState = state.processStates.find((s) => s.profileId === profileId);
+ const pid = processState?.pid;
+
+ const [inputValue, setInputValue] = useState('');
+ const [historyIdx, setHistoryIdx] = useState(-1);
+ const [cmdHistory, setCmdHistory] = useState([]);
+ const [autoScroll, setAutoScroll] = useState(true);
+ const [starting, setStarting] = useState(false);
+ const [errorMsg, setErrorMsg] = useState(null);
+ const [searchOpen, setSearchOpen] = useState(false);
+ const [searchQuery, setSearchQuery] = useState('');
+ const [searchIdx, setSearchIdx] = useState(0);
+
+ const scrollRef = useRef(null);
+ const inputRef = useRef(null);
+ const searchRef = useRef(null);
+ const bottomRef = useRef(null);
+ const matchRefs = useRef<(HTMLDivElement | null)[]>([]);
useEffect(() => {
- setInputValue('')
- setHistoryIdx(-1)
- setErrorMsg(null)
- setSearchOpen(false)
- setSearchQuery('')
- setSearchIdx(0)
- }, [profileId])
+ setInputValue('');
+ setHistoryIdx(-1);
+ setErrorMsg(null);
+ setSearchOpen(false);
+ setSearchQuery('');
+ setSearchIdx(0);
+ }, [profileId]);
useEffect(() => {
- if (autoScroll && !searchOpen) bottomRef.current?.scrollIntoView({ behavior: 'instant' })
- }, [lines.length, autoScroll, searchOpen])
+ if (autoScroll && !searchOpen) bottomRef.current?.scrollIntoView({ behavior: 'instant' });
+ }, [lines.length, autoScroll, searchOpen]);
const handleScroll = useCallback(() => {
- const el = scrollRef.current
- if (!el) return
- setAutoScroll(el.scrollHeight - el.scrollTop - el.clientHeight < 40)
- }, [])
+ const el = scrollRef.current;
+ if (!el) return;
+ setAutoScroll(el.scrollHeight - el.scrollTop - el.clientHeight < 40);
+ }, []);
- const searchTerm = searchQuery.trim().toLowerCase()
+ const searchTerm = searchQuery.trim().toLowerCase();
const matchIndices = useMemo(() => {
- if (!searchTerm) return []
+ if (!searchTerm) return [];
return lines.reduce((acc, line, i) => {
- if (line.text.toLowerCase().includes(searchTerm)) acc.push(i)
- return acc
- }, [])
- }, [lines, searchTerm])
+ if (line.text.toLowerCase().includes(searchTerm)) acc.push(i);
+ return acc;
+ }, []);
+ }, [lines, searchTerm]);
const clampedIdx =
matchIndices.length > 0
? ((searchIdx % matchIndices.length) + matchIndices.length) % matchIndices.length
- : 0
+ : 0;
const scrollToMatch = useCallback((idx: number) => {
- const el = matchRefs.current[idx]
- if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' })
- }, [])
+ const el = matchRefs.current[idx];
+ if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ }, []);
useEffect(() => {
- if (matchIndices.length > 0) scrollToMatch(clampedIdx)
- }, [clampedIdx, matchIndices, scrollToMatch])
+ if (matchIndices.length > 0) scrollToMatch(clampedIdx);
+ }, [clampedIdx, matchIndices, scrollToMatch]);
const openSearch = useCallback(() => {
- setSearchOpen(true)
- setAutoScroll(false)
- setTimeout(() => searchRef.current?.focus(), 50)
- }, [])
+ setSearchOpen(true);
+ setAutoScroll(false);
+ setTimeout(() => searchRef.current?.focus(), 50);
+ }, []);
const closeSearch = useCallback(() => {
- setSearchOpen(false)
- setSearchQuery('')
- setSearchIdx(0)
- setAutoScroll(true)
- }, [])
+ setSearchOpen(false);
+ setSearchQuery('');
+ setSearchIdx(0);
+ setAutoScroll(true);
+ }, []);
- const goNext = useCallback(() => setSearchIdx((i) => i + 1), [])
- const goPrev = useCallback(() => setSearchIdx((i) => i - 1), [])
+ const goNext = useCallback(() => setSearchIdx((i) => i + 1), []);
+ const goPrev = useCallback(() => setSearchIdx((i) => i - 1), []);
const handleToggle = useCallback(async () => {
- if (!activeProfile) return
- setErrorMsg(null)
+ if (!activeProfile) return;
+ setErrorMsg(null);
if (running) {
- await stopProcess(profileId)
+ await stopProcess(profileId);
} else {
if (!activeProfile.jarPath) {
- setErrorMsg('No JAR file selected. Go to Configure to set one.')
- return
+ setErrorMsg('No JAR file selected. Go to Configure to set one.');
+ return;
}
- setStarting(true)
- const res = await startProcess(activeProfile)
- setStarting(false)
- if (!res.ok) setErrorMsg(res.error ?? 'Failed to start')
+ setStarting(true);
+ const res = await startProcess(activeProfile);
+ setStarting(false);
+ if (!res.ok) setErrorMsg(res.error ?? 'Failed to start');
}
- }, [activeProfile, running, profileId, stopProcess, startProcess])
+ }, [activeProfile, running, profileId, stopProcess, startProcess]);
const handleSend = useCallback(async () => {
- const cmd = inputValue.trim()
- if (!cmd || !running) return
- await sendInput(profileId, cmd)
+ const cmd = inputValue.trim();
+ if (!cmd || !running) return;
+ await sendInput(profileId, cmd);
setCmdHistory((prev) =>
[cmd, ...prev.filter((c) => c !== cmd)].slice(0, settings?.consoleHistorySize ?? 200)
- )
- setInputValue('')
- setHistoryIdx(-1)
- }, [inputValue, running, profileId, sendInput, settings])
+ );
+ setInputValue('');
+ setHistoryIdx(-1);
+ }, [inputValue, running, profileId, sendInput, settings]);
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
if (e.key === 'Enter') {
- e.preventDefault()
- handleSend()
- return
+ e.preventDefault();
+ handleSend();
+ return;
}
if (e.key === 'ArrowUp') {
- e.preventDefault()
- const n = Math.min(historyIdx + 1, cmdHistory.length - 1)
- setHistoryIdx(n)
- setInputValue(cmdHistory[n] ?? '')
- return
+ e.preventDefault();
+ const n = Math.min(historyIdx + 1, cmdHistory.length - 1);
+ setHistoryIdx(n);
+ setInputValue(cmdHistory[n] ?? '');
+ return;
}
if (e.key === 'ArrowDown') {
- e.preventDefault()
- const n = Math.max(historyIdx - 1, -1)
- setHistoryIdx(n)
- setInputValue(n === -1 ? '' : (cmdHistory[n] ?? ''))
- return
+ e.preventDefault();
+ const n = Math.max(historyIdx - 1, -1);
+ setHistoryIdx(n);
+ setInputValue(n === -1 ? '' : (cmdHistory[n] ?? ''));
+ return;
}
if (e.key === 'l' && e.ctrlKey) {
- e.preventDefault()
- clearConsole(profileId)
+ e.preventDefault();
+ clearConsole(profileId);
}
if (e.key === 'f' && e.ctrlKey) {
- e.preventDefault()
- openSearch()
+ e.preventDefault();
+ openSearch();
}
},
[handleSend, historyIdx, cmdHistory, clearConsole, profileId, openSearch]
- )
+ );
useEffect(() => {
const handler = (e: globalThis.KeyboardEvent) => {
if (e.ctrlKey && e.key === 'f') {
- e.preventDefault()
- openSearch()
+ e.preventDefault();
+ openSearch();
}
- if (e.key === 'Escape' && searchOpen) closeSearch()
- }
- window.addEventListener('keydown', handler)
- return () => window.removeEventListener('keydown', handler)
- }, [openSearch, closeSearch, searchOpen])
+ if (e.key === 'Escape' && searchOpen) closeSearch();
+ };
+ window.addEventListener('keydown', handler);
+ return () => window.removeEventListener('keydown', handler);
+ }, [openSearch, closeSearch, searchOpen]);
- const fontSize = settings?.consoleFontSize ?? 13
- const wordWrap = settings?.consoleWordWrap ?? false
- const lineNums = settings?.consoleLineNumbers ?? false
+ const fontSize = settings?.consoleFontSize ?? 13;
+ const wordWrap = settings?.consoleWordWrap ?? false;
+ const lineNums = settings?.consoleLineNumbers ?? false;
if (!activeProfile) {
return (
No profile selected
- )
+ );
}
- matchRefs.current = new Array(matchIndices.length).fill(null)
+ matchRefs.current = new Array(matchIndices.length).fill(null);
return (
@@ -206,8 +206,8 @@ export function ConsoleTab() {
{!autoScroll && !searchOpen && (
{
- setAutoScroll(true)
- bottomRef.current?.scrollIntoView({ behavior: 'smooth' })
+ setAutoScroll(true);
+ bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
}}
className="text-xs font-mono transition-colors"
style={{ color }}
@@ -244,15 +244,15 @@ export function ConsoleTab() {
type="text"
value={searchQuery}
onChange={(e) => {
- setSearchQuery(e.target.value)
- setSearchIdx(0)
+ setSearchQuery(e.target.value);
+ setSearchIdx(0);
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
- e.preventDefault()
- e.shiftKey ? goPrev() : goNext()
+ e.preventDefault();
+ e.shiftKey ? goPrev() : goNext();
}
- if (e.key === 'Escape') closeSearch()
+ if (e.key === 'Escape') closeSearch();
}}
placeholder="Search console... (Enter next, Shift+Enter prev)"
className="flex-1 bg-base-950 border border-surface-border rounded px-2.5 py-1
@@ -314,9 +314,9 @@ export function ConsoleTab() {
)}
{lines.map((line, i) => {
- const matchPos = matchIndices.indexOf(i)
- const isCurrentMatch = matchPos === clampedIdx && matchPos !== -1
- const isAnyMatch = matchPos !== -1
+ const matchPos = matchIndices.indexOf(i);
+ const isCurrentMatch = matchPos === clampedIdx && matchPos !== -1;
+ const isAnyMatch = matchPos !== -1;
return (
{
- matchRefs.current[matchPos] = el
+ matchRefs.current[matchPos] = el;
}
: undefined
}
/>
- )
+ );
})}
@@ -361,7 +361,7 @@ export function ConsoleTab() {
/>
- )
+ );
}
const LINE_COLORS: Record = {
@@ -369,24 +369,24 @@ const LINE_COLORS: Record = {
stderr: 'text-console-error',
input: 'text-console-input',
system: 'text-text-muted',
-}
+};
const ConsoleLineRow = React.forwardRef<
HTMLDivElement,
{
- line: ConsoleLine
- lineNum: number
- showLineNum: boolean
- wordWrap: boolean
- searchTerm: string
- isCurrentMatch: boolean
- isAnyMatch: boolean
+ line: ConsoleLine;
+ lineNum: number;
+ showLineNum: boolean;
+ wordWrap: boolean;
+ searchTerm: string;
+ isCurrentMatch: boolean;
+ isAnyMatch: boolean;
}
>(({ line, lineNum, showLineNum, wordWrap, searchTerm, isCurrentMatch, isAnyMatch }, ref) => {
- const text = line.text || ' '
+ const text = line.text || ' ';
const content =
- searchTerm && isAnyMatch ? renderHighlighted(text, searchTerm, isCurrentMatch) : text
+ searchTerm && isAnyMatch ? renderHighlighted(text, searchTerm, isCurrentMatch) : text;
return (
- )
-})
-ConsoleLineRow.displayName = 'ConsoleLineRow'
+ );
+});
+ConsoleLineRow.displayName = 'ConsoleLineRow';
function renderHighlighted(text: string, term: string, isCurrent: boolean): React.ReactNode {
- const parts: React.ReactNode[] = []
- const lower = text.toLowerCase()
- let last = 0
- let idx = lower.indexOf(term)
- let key = 0
+ const parts: React.ReactNode[] = [];
+ const lower = text.toLowerCase();
+ let last = 0;
+ let idx = lower.indexOf(term);
+ let key = 0;
while (idx !== -1) {
- if (idx > last) parts.push(text.slice(last, idx))
+ if (idx > last) parts.push(text.slice(last, idx));
parts.push(
{text.slice(idx, idx + term.length)}
- )
- last = idx + term.length
- idx = lower.indexOf(term, last)
+ );
+ last = idx + term.length;
+ idx = lower.indexOf(term, last);
}
- if (last < text.length) parts.push(text.slice(last))
- return parts
+ if (last < text.length) parts.push(text.slice(last));
+ return parts;
}
diff --git a/src/renderer/components/developer/DevApiExplorer.tsx b/src/renderer/components/developer/DevApiExplorer.tsx
index 78a8f1c..fea6237 100644
--- a/src/renderer/components/developer/DevApiExplorer.tsx
+++ b/src/renderer/components/developer/DevApiExplorer.tsx
@@ -1,86 +1,86 @@
-import { useState } from 'react'
-import { VscCheck, VscCopy, VscPlay } from 'react-icons/vsc'
-import { routeConfig, RouteDefinition } from '../../../main/shared/config/API.config'
-import { useApp } from '../../store/AppStore'
-import { Button } from '../common/Button'
-import { REST_API_CONFIG } from '../../../main/shared/config/RestApi.config'
+import { useState } from 'react';
+import { VscCheck, VscCopy, VscPlay } from 'react-icons/vsc';
+import { routeConfig, RouteDefinition } from '../../../main/shared/config/API.config';
+import { useApp } from '../../store/AppStore';
+import { Button } from '../common/Button';
+import { REST_API_CONFIG } from '../../../main/shared/config/RestApi.config';
const METHOD_COLORS: Record = {
GET: 'text-accent border-accent/30 bg-accent/10',
POST: 'text-blue-400 border-blue-400/30 bg-blue-400/10',
PUT: 'text-yellow-400 border-yellow-400/30 bg-yellow-400/10',
DELETE: 'text-red-400 border-red-400/30 bg-red-400/10',
-}
+};
-const routes = Object.values(routeConfig)
+const routes = Object.values(routeConfig);
export function DevApiExplorer() {
- const { state } = useApp()
+ const { state } = useApp();
- const [selected, setSelected] = useState(null)
- const [pathParams, setPathParams] = useState>({})
- const [body, setBody] = useState('')
- const [response, setResponse] = useState(null)
- const [loading, setLoading] = useState(false)
- const [copied, setCopied] = useState(false)
+ const [selected, setSelected] = useState(null);
+ const [pathParams, setPathParams] = useState>({});
+ const [body, setBody] = useState('');
+ const [response, setResponse] = useState(null);
+ const [loading, setLoading] = useState(false);
+ const [copied, setCopied] = useState(false);
- const port = state.settings?.restApiPort ?? 4444
- const restEnabled = state.settings?.restApiEnabled ?? false
+ const port = state.settings?.restApiPort ?? 4444;
+ const restEnabled = state.settings?.restApiEnabled ?? false;
const handleSelect = (route: RouteDefinition) => {
- setSelected(route)
- setResponse(null)
- setBody(route.bodyTemplate ?? '')
+ setSelected(route);
+ setResponse(null);
+ setBody(route.bodyTemplate ?? '');
- const params: Record = {}
- const matches = route.path.matchAll(/:([a-zA-Z]+)/g)
- for (const m of matches) params[m[1]] = ''
- setPathParams(params)
- }
+ const params: Record = {};
+ const matches = route.path.matchAll(/:([a-zA-Z]+)/g);
+ for (const m of matches) params[m[1]] = '';
+ setPathParams(params);
+ };
const buildUrl = () => {
- if (!selected) return ''
- let path = selected.path
+ if (!selected) return '';
+ let path = selected.path;
for (const [k, v] of Object.entries(pathParams)) {
- path = path.replace(`:${k}`, v || `:${k}`)
+ path = path.replace(`:${k}`, v || `:${k}`);
}
- return `http://${REST_API_CONFIG.host}:${port}${path}`
- }
+ return `http://${REST_API_CONFIG.host}:${port}${path}`;
+ };
const handleCall = async () => {
- if (!selected) return
- setLoading(true)
- setResponse(null)
+ if (!selected) return;
+ setLoading(true);
+ setResponse(null);
try {
- const url = buildUrl()
- const opts: RequestInit = { method: selected.method }
+ const url = buildUrl();
+ const opts: RequestInit = { method: selected.method };
if (body.trim() && ['POST', 'PUT', 'PATCH'].includes(selected.method)) {
- opts.headers = { 'Content-Type': 'application/json' }
- opts.body = body
+ opts.headers = { 'Content-Type': 'application/json' };
+ opts.body = body;
}
- const res = await fetch(url, opts)
- const text = await res.text()
+ const res = await fetch(url, opts);
+ const text = await res.text();
try {
- setResponse(JSON.stringify(JSON.parse(text), null, 2))
+ setResponse(JSON.stringify(JSON.parse(text), null, 2));
} catch {
- setResponse(text)
+ setResponse(text);
}
} catch (err) {
- setResponse(`Error: ${err instanceof Error ? err.message : String(err)}`)
+ setResponse(`Error: ${err instanceof Error ? err.message : String(err)}`);
}
- setLoading(false)
- }
+ setLoading(false);
+ };
const copyUrl = () => {
- navigator.clipboard.writeText(buildUrl())
- setCopied(true)
- setTimeout(() => setCopied(false), 1500)
- }
+ navigator.clipboard.writeText(buildUrl());
+ setCopied(true);
+ setTimeout(() => setCopied(false), 1500);
+ };
return (
@@ -212,5 +212,5 @@ export function DevApiExplorer() {
)}
- )
+ );
}
diff --git a/src/renderer/components/developer/DevDashboard.tsx b/src/renderer/components/developer/DevDashboard.tsx
index 46d1207..103c3f4 100644
--- a/src/renderer/components/developer/DevDashboard.tsx
+++ b/src/renderer/components/developer/DevDashboard.tsx
@@ -1,17 +1,17 @@
-import React, { useEffect, useState } from 'react'
-import { useApp } from '../../store/AppStore'
-import { VscCircleFilled, VscCircle } from 'react-icons/vsc'
+import React, { useEffect, useState } from 'react';
+import { useApp } from '../../store/AppStore';
+import { VscCircleFilled, VscCircle } from 'react-icons/vsc';
-declare const __APP_VERSION__: string
+declare const __APP_VERSION__: string;
interface SysInfo {
- platform: string
- arch: string
- nodeVersion: string
- electronVersion: string
- chromeVersion: string
- uptime: number
- memoryUsageMB: number
+ platform: string;
+ arch: string;
+ nodeVersion: string;
+ electronVersion: string;
+ chromeVersion: string;
+ uptime: number;
+ memoryUsageMB: number;
}
function StatCard({
@@ -20,10 +20,10 @@ function StatCard({
accent,
sub,
}: {
- label: string
- value: string | number
- accent?: boolean
- sub?: string
+ label: string;
+ value: string | number;
+ accent?: boolean;
+ sub?: string;
}) {
return (
@@ -38,7 +38,7 @@ function StatCard({
{sub &&
{sub}
}
- )
+ );
}
function Badge({ ok, label }: { ok: boolean; label: string }) {
@@ -54,26 +54,26 @@ function Badge({ ok, label }: { ok: boolean; label: string }) {
{ok ? : }
{label}
- )
+ );
}
export function DevDashboard() {
- const { state } = useApp()
- const [sysInfo, setSysInfo] = useState(null)
- const [tick, setTick] = useState(0)
+ const { state } = useApp();
+ const [sysInfo, setSysInfo] = useState(null);
+ const [tick, setTick] = useState(0);
useEffect(() => {
- window.api.getSysInfo().then(setSysInfo)
+ window.api.getSysInfo().then(setSysInfo);
const id = setInterval(() => {
- window.api.getSysInfo().then(setSysInfo)
- setTick((t) => t + 1)
- }, 3000)
- return () => clearInterval(id)
- }, [])
+ window.api.getSysInfo().then(setSysInfo);
+ setTick((t) => t + 1);
+ }, 3000);
+ return () => clearInterval(id);
+ }, []);
- const runningProfiles = state.processStates.filter((s) => s.running)
- const totalConsoleLines = Object.values(state.consoleLogs).reduce((a, b) => a + b.length, 0)
- const restEnabled = state.settings?.restApiEnabled ?? false
+ const runningProfiles = state.processStates.filter((s) => s.running);
+ const totalConsoleLines = Object.values(state.consoleLogs).reduce((a, b) => a + b.length, 0);
+ const restEnabled = state.settings?.restApiEnabled ?? false;
return (
@@ -117,8 +117,8 @@ export function DevDashboard() {
) : (
{runningProfiles.map((s) => {
- const profile = state.profiles.find((p) => p.id === s.profileId)
- const uptimeSec = s.startedAt ? Math.floor((Date.now() - s.startedAt) / 1000) : 0
+ const profile = state.profiles.find((p) => p.id === s.profileId);
+ const uptimeSec = s.startedAt ? Math.floor((Date.now() - s.startedAt) / 1000) : 0;
return (
PID {s.pid}
{uptimeSec}s
- )
+ );
})}
)}
@@ -151,7 +151,7 @@ export function DevDashboard() {
)}
- )
+ );
}
function Section({ title, children }: { title: string; children: React.ReactNode }) {
@@ -160,5 +160,5 @@ function Section({ title, children }: { title: string; children: React.ReactNode
{title}
{children}
- )
+ );
}
diff --git a/src/renderer/components/developer/DevDiagnostics.tsx b/src/renderer/components/developer/DevDiagnostics.tsx
index 043b3de..819a213 100644
--- a/src/renderer/components/developer/DevDiagnostics.tsx
+++ b/src/renderer/components/developer/DevDiagnostics.tsx
@@ -1,35 +1,35 @@
-import React, { useEffect, useRef, useState } from 'react'
-import { VscCheck, VscCopy } from 'react-icons/vsc'
-import { useApp } from '../../store/AppStore'
-import { Button } from '../common/Button'
+import React, { useEffect, useRef, useState } from 'react';
+import { VscCheck, VscCopy } from 'react-icons/vsc';
+import { useApp } from '../../store/AppStore';
+import { Button } from '../common/Button';
interface PerfSample {
- timestamp: number
- memMB: number
+ timestamp: number;
+ memMB: number;
}
export function DevDiagnostics() {
- const { state } = useApp()
- const [perfSamples, setPerfSamples] = useState([])
- const [copied, setCopied] = useState(false)
- const [ipcLog, setIpcLog] = useState<{ ts: number; msg: string }[]>([])
- const intervalRef = useRef | null>(null)
+ const { state } = useApp();
+ const [perfSamples, setPerfSamples] = useState([]);
+ const [copied, setCopied] = useState(false);
+ const [ipcLog, setIpcLog] = useState<{ ts: number; msg: string }[]>([]);
+ const intervalRef = useRef | null>(null);
const samplePerf = () => {
if (window.performance && (performance as any).memory) {
- const mem = (performance as any).memory
- const mb = Math.round(mem.usedJSHeapSize / 1024 / 1024)
- setPerfSamples((prev) => [...prev.slice(-29), { timestamp: Date.now(), memMB: mb }])
+ const mem = (performance as any).memory;
+ const mb = Math.round(mem.usedJSHeapSize / 1024 / 1024);
+ setPerfSamples((prev) => [...prev.slice(-29), { timestamp: Date.now(), memMB: mb }]);
}
- }
+ };
useEffect(() => {
- samplePerf()
- intervalRef.current = setInterval(samplePerf, 2000)
+ samplePerf();
+ intervalRef.current = setInterval(samplePerf, 2000);
return () => {
- if (intervalRef.current) clearInterval(intervalRef.current)
- }
- }, [])
+ if (intervalRef.current) clearInterval(intervalRef.current);
+ };
+ }, []);
const exportDiagReport = () => {
const report = {
@@ -42,15 +42,15 @@ export function DevDiagnostics() {
Object.entries(state.consoleLogs).map(([id, lines]) => [id, lines.length])
),
memorySnapshot: perfSamples[perfSamples.length - 1] ?? null,
- }
- const text = JSON.stringify(report, null, 2)
- navigator.clipboard.writeText(text)
- setCopied(true)
- setTimeout(() => setCopied(false), 2000)
- }
+ };
+ const text = JSON.stringify(report, null, 2);
+ navigator.clipboard.writeText(text);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ };
- const latest = perfSamples[perfSamples.length - 1]
- const maxMem = Math.max(...perfSamples.map((s) => s.memMB), 1)
+ const latest = perfSamples[perfSamples.length - 1];
+ const maxMem = Math.max(...perfSamples.map((s) => s.memMB), 1);
return (
@@ -59,7 +59,7 @@ export function DevDiagnostics() {
{perfSamples.map((s, i) => {
- const pct = (s.memMB / maxMem) * 100
+ const pct = (s.memMB / maxMem) * 100;
return (
- )
+ );
})}
{perfSamples.length === 0 && (
@@ -107,8 +107,8 @@ export function DevDiagnostics() {
) : (
{state.profiles.map((p) => {
- const lines = state.consoleLogs[p.id]?.length ?? 0
- const running = state.processStates.some((s) => s.profileId === p.id && s.running)
+ const lines = state.consoleLogs[p.id]?.length ?? 0;
+ const running = state.processStates.some((s) => s.profileId === p.id && s.running);
return (
- )
+ );
})}
)}
@@ -139,7 +139,7 @@ export function DevDiagnostics() {
- )
+ );
}
function Section({ title, children }: { title: string; children: React.ReactNode }) {
@@ -148,7 +148,7 @@ function Section({ title, children }: { title: string; children: React.ReactNode
{title}
{children}
- )
+ );
}
function DiagRow({ label, value, mono }: { label: string; value: string; mono?: boolean }) {
@@ -164,7 +164,7 @@ function DiagRow({ label, value, mono }: { label: string; value: string; mono?:
{value}
- )
+ );
}
-declare const __APP_VERSION__: string
+declare const __APP_VERSION__: string;
diff --git a/src/renderer/components/developer/DevModeGate.tsx b/src/renderer/components/developer/DevModeGate.tsx
index ae5ac46..e95d148 100644
--- a/src/renderer/components/developer/DevModeGate.tsx
+++ b/src/renderer/components/developer/DevModeGate.tsx
@@ -1,41 +1,41 @@
-import React, { useEffect, useState } from 'react'
-import { useDevMode } from '../../hooks/useDevMode'
-import { Button } from '../common/Button'
-import { VscCode } from 'react-icons/vsc'
+import React, { useEffect, useState } from 'react';
+import { useDevMode } from '../../hooks/useDevMode';
+import { Button } from '../common/Button';
+import { VscCode } from 'react-icons/vsc';
export function DevModeGate() {
- const devEnabled = useDevMode()
- const [dialogOpen, setDialogOpen] = useState(false)
+ const devEnabled = useDevMode();
+ const [dialogOpen, setDialogOpen] = useState(false);
useEffect(() => {
- const keysPressed = new Set()
+ const keysPressed = new Set();
const keydownHandler = (e: KeyboardEvent) => {
- keysPressed.add(e.code)
+ keysPressed.add(e.code);
// right shift + 7
if (keysPressed.has('ShiftRight') && keysPressed.has('Digit7')) {
- e.preventDefault()
- setDialogOpen(true)
+ e.preventDefault();
+ setDialogOpen(true);
}
- }
+ };
const keyupHandler = (e: KeyboardEvent) => {
- keysPressed.delete(e.code)
- }
+ keysPressed.delete(e.code);
+ };
- window.addEventListener('keydown', keydownHandler)
- window.addEventListener('keyup', keyupHandler)
+ window.addEventListener('keydown', keydownHandler);
+ window.addEventListener('keyup', keyupHandler);
return () => {
- window.removeEventListener('keydown', keydownHandler)
- window.removeEventListener('keyup', keyupHandler)
- }
- }, [])
+ window.removeEventListener('keydown', keydownHandler);
+ window.removeEventListener('keyup', keyupHandler);
+ };
+ }, []);
- if (!dialogOpen) return null
+ if (!dialogOpen) return null;
- const isOn = devEnabled
+ const isOn = devEnabled;
return (
@@ -66,8 +66,8 @@ export function DevModeGate() {
variant="primary"
size="sm"
onClick={() => {
- window.env.toggleDevMode(!isOn)
- setDialogOpen(false)
+ window.env.toggleDevMode(!isOn);
+ setDialogOpen(false);
}}
>
{isOn ? 'Disable' : 'Enable'}
@@ -75,5 +75,5 @@ export function DevModeGate() {
- )
+ );
}
diff --git a/src/renderer/components/developer/DevStorage.tsx b/src/renderer/components/developer/DevStorage.tsx
index b5cb345..37fcd89 100644
--- a/src/renderer/components/developer/DevStorage.tsx
+++ b/src/renderer/components/developer/DevStorage.tsx
@@ -1,46 +1,46 @@
-import React, { useState, useEffect } from 'react'
-import { Button } from '../common/Button'
-import { Dialog } from '../common/Dialog'
-import { useApp } from '../../store/AppStore'
-import { VscRefresh, VscTrash } from 'react-icons/vsc'
+import React, { useState, useEffect } from 'react';
+import { Button } from '../common/Button';
+import { Dialog } from '../common/Dialog';
+import { useApp } from '../../store/AppStore';
+import { VscRefresh, VscTrash } from 'react-icons/vsc';
interface SessionEntry {
- key: string
- sizeBytes: number
- preview: string
+ key: string;
+ sizeBytes: number;
+ preview: string;
}
function getSessionEntries(): SessionEntry[] {
- const entries: SessionEntry[] = []
+ const entries: SessionEntry[] = [];
for (let i = 0; i < sessionStorage.length; i++) {
- const key = sessionStorage.key(i)!
- const raw = sessionStorage.getItem(key) ?? ''
+ const key = sessionStorage.key(i)!;
+ const raw = sessionStorage.getItem(key) ?? '';
entries.push({
key,
sizeBytes: new Blob([raw]).size,
preview: raw.slice(0, 80) + (raw.length > 80 ? '…' : ''),
- })
+ });
}
- return entries.sort((a, b) => b.sizeBytes - a.sizeBytes)
+ return entries.sort((a, b) => b.sizeBytes - a.sizeBytes);
}
function formatBytes(n: number): string {
- if (n < 1024) return `${n} B`
- return `${(n / 1024).toFixed(1)} KB`
+ if (n < 1024) return `${n} B`;
+ return `${(n / 1024).toFixed(1)} KB`;
}
export function DevStorage() {
- const { state } = useApp()
- const [sessionEntries, setSessionEntries] = useState([])
- const [confirmReset, setConfirmReset] = useState<'electron-store' | 'session' | null>(null)
+ const { state } = useApp();
+ const [sessionEntries, setSessionEntries] = useState([]);
+ const [confirmReset, setConfirmReset] = useState<'electron-store' | 'session' | null>(null);
- const refresh = () => setSessionEntries(getSessionEntries())
+ const refresh = () => setSessionEntries(getSessionEntries());
useEffect(() => {
- refresh()
- }, [])
+ refresh();
+ }, []);
- const totalSessionBytes = sessionEntries.reduce((a, b) => a + b.sizeBytes, 0)
+ const totalSessionBytes = sessionEntries.reduce((a, b) => a + b.sizeBytes, 0);
return (
@@ -95,8 +95,8 @@ export function DevStorage() {
{
- sessionStorage.removeItem(e.key)
- refresh()
+ sessionStorage.removeItem(e.key);
+ refresh();
}}
className="text-text-muted hover:text-red-400 transition-colors"
>
@@ -125,8 +125,8 @@ export function DevStorage() {
confirmLabel="Reset"
danger
onConfirm={async () => {
- await window.api.resetStore()
- setConfirmReset(null)
+ await window.api.resetStore();
+ setConfirmReset(null);
}}
onCancel={() => setConfirmReset(null)}
/>
@@ -138,14 +138,14 @@ export function DevStorage() {
confirmLabel="Clear"
danger
onConfirm={() => {
- sessionStorage.clear()
- refresh()
- setConfirmReset(null)
+ sessionStorage.clear();
+ refresh();
+ setConfirmReset(null);
}}
onCancel={() => setConfirmReset(null)}
/>
- )
+ );
}
function Section({ title, children }: { title: string; children: React.ReactNode }) {
@@ -154,7 +154,7 @@ function Section({ title, children }: { title: string; children: React.ReactNode
{title}
{children}
- )
+ );
}
function StoreRow({ label, value, mono }: { label: string; value: string; mono?: boolean }) {
@@ -170,5 +170,5 @@ function StoreRow({ label, value, mono }: { label: string; value: string; mono?:
{value}
- )
+ );
}
diff --git a/src/renderer/components/developer/DeveloperTab.tsx b/src/renderer/components/developer/DeveloperTab.tsx
index 51ea444..300218d 100644
--- a/src/renderer/components/developer/DeveloperTab.tsx
+++ b/src/renderer/components/developer/DeveloperTab.tsx
@@ -1,21 +1,21 @@
-import React, { useState } from 'react'
-import { VscDashboard, VscPlug, VscDatabase, VscBeaker } from 'react-icons/vsc'
-import { DevDashboard } from './DevDashboard'
-import { DevApiExplorer } from './DevApiExplorer'
-import { DevStorage } from './DevStorage'
-import { DevDiagnostics } from './DevDiagnostics'
+import React, { useState } from 'react';
+import { VscDashboard, VscPlug, VscDatabase, VscBeaker } from 'react-icons/vsc';
+import { DevDashboard } from './DevDashboard';
+import { DevApiExplorer } from './DevApiExplorer';
+import { DevStorage } from './DevStorage';
+import { DevDiagnostics } from './DevDiagnostics';
-type Panel = 'dashboard' | 'api' | 'storage' | 'diagnostics'
+type Panel = 'dashboard' | 'api' | 'storage' | 'diagnostics';
const PANELS: { id: Panel; label: string; Icon: React.ElementType }[] = [
{ id: 'dashboard', label: 'Dashboard', Icon: VscDashboard },
{ id: 'api', label: 'API Explorer', Icon: VscPlug },
{ id: 'storage', label: 'Storage', Icon: VscDatabase },
{ id: 'diagnostics', label: 'Diagnostics', Icon: VscBeaker },
-]
+];
export function DeveloperTab() {
- const [panel, setPanel] = useState('dashboard')
+ const [panel, setPanel] = useState('dashboard');
return (
@@ -53,5 +53,5 @@ export function DeveloperTab() {
{panel === 'diagnostics' && }
- )
+ );
}
diff --git a/src/renderer/components/faq/FaqPanel.tsx b/src/renderer/components/faq/FaqPanel.tsx
index d0320cd..0195c06 100644
--- a/src/renderer/components/faq/FaqPanel.tsx
+++ b/src/renderer/components/faq/FaqPanel.tsx
@@ -1,30 +1,30 @@
-import { useMemo, useState } from 'react'
-import type { FaqItem, FaqTopic } from '../../../main/shared/config/FAQ.config'
-import { FAQ_TOPICS } from '../../../main/shared/config/FAQ.config'
+import { useMemo, useState } from 'react';
+import type { FaqItem, FaqTopic } from '../../../main/shared/config/FAQ.config';
+import { FAQ_TOPICS } from '../../../main/shared/config/FAQ.config';
export function FaqPanel() {
- const [search, setSearch] = useState('')
- const [activeTopic, setActiveTopic] = useState(FAQ_TOPICS[0]?.id ?? '')
- const [expandedIdx, setExpandedIdx] = useState(null)
+ const [search, setSearch] = useState('');
+ const [activeTopic, setActiveTopic] = useState(FAQ_TOPICS[0]?.id ?? '');
+ const [expandedIdx, setExpandedIdx] = useState(null);
- const searchTrimmed = search.trim().toLowerCase()
+ const searchTrimmed = search.trim().toLowerCase();
const searchResults = useMemo(() => {
- if (!searchTrimmed) return []
+ if (!searchTrimmed) return [];
return FAQ_TOPICS.flatMap((t) => t.items).filter(
(item) =>
item.q.toLowerCase().includes(searchTrimmed) || item.a.toLowerCase().includes(searchTrimmed)
- )
- }, [searchTrimmed])
+ );
+ }, [searchTrimmed]);
- const activeTopic_ = FAQ_TOPICS.find((t) => t.id === activeTopic) ?? FAQ_TOPICS[0]
- const displayItems = searchTrimmed ? searchResults : (activeTopic_?.items ?? [])
+ const activeTopic_ = FAQ_TOPICS.find((t) => t.id === activeTopic) ?? FAQ_TOPICS[0];
+ const displayItems = searchTrimmed ? searchResults : (activeTopic_?.items ?? []);
const handleTopicClick = (id: string) => {
- setActiveTopic(id)
- setExpandedIdx(null)
- setSearch('')
- }
+ setActiveTopic(id);
+ setExpandedIdx(null);
+ setSearch('');
+ };
return (
@@ -33,8 +33,8 @@ export function FaqPanel() {
type="text"
value={search}
onChange={(e) => {
- setSearch(e.target.value)
- setExpandedIdx(null)
+ setSearch(e.target.value);
+ setExpandedIdx(null);
}}
placeholder="Search FAQ..."
className="w-full bg-base-950 border border-surface-border rounded-md px-3 py-1.5 text-xs text-text-primary placeholder:text-text-muted focus:outline-none focus:border-accent/40 transition-colors font-mono"
@@ -70,7 +70,7 @@ export function FaqPanel() {
- )
+ );
}
function TopicButton({
@@ -78,9 +78,9 @@ function TopicButton({
active,
onClick,
}: {
- topic: FaqTopic
- active: boolean
- onClick: () => void
+ topic: FaqTopic;
+ active: boolean;
+ onClick: () => void;
}) {
return (
{topic.label}
- )
+ );
}
function FaqEntry({
@@ -102,9 +102,9 @@ function FaqEntry({
open,
onToggle,
}: {
- item: FaqItem
- open: boolean
- onToggle: () => void
+ item: FaqItem;
+ open: boolean;
+ onToggle: () => void;
}) {
return (
)}
- )
+ );
}
diff --git a/src/renderer/components/profiles/ConfigTab.tsx b/src/renderer/components/profiles/ConfigTab.tsx
index 02a56b9..be01543 100644
--- a/src/renderer/components/profiles/ConfigTab.tsx
+++ b/src/renderer/components/profiles/ConfigTab.tsx
@@ -1,14 +1,14 @@
-import React, { useState, useEffect, useCallback, useMemo } from 'react'
-import { useApp } from '../../store/AppStore'
-import { Button } from '../common/Button'
-import { Input } from '../common/Input'
-import { Toggle } from '../common/Toggle'
-import { ArgList } from '../common/ArgList'
-import { PropList } from '../common/PropList'
-import { Dialog } from '../common/Dialog'
-import { Profile } from 'src/main/shared/types/Profile.types'
+import React, { useState, useEffect, useCallback, useMemo } from 'react';
+import { useApp } from '../../store/AppStore';
+import { Button } from '../common/Button';
+import { Input } from '../common/Input';
+import { Toggle } from '../common/Toggle';
+import { ArgList } from '../common/ArgList';
+import { PropList } from '../common/PropList';
+import { Dialog } from '../common/Dialog';
+import { Profile } from 'src/main/shared/types/Profile.types';
-type Section = 'general' | 'files' | 'jvm' | 'props' | 'args'
+type Section = 'general' | 'files' | 'jvm' | 'props' | 'args';
const SECTIONS: { id: Section; label: string }[] = [
{ id: 'general', label: 'General' },
@@ -16,72 +16,72 @@ const SECTIONS: { id: Section; label: string }[] = [
{ id: 'jvm', label: 'JVM Args' },
{ id: 'props', label: 'Properties (-D)' },
{ id: 'args', label: 'Program Args' },
-]
+];
export function ConfigTab() {
- const { activeProfile, saveProfile, isRunning, startProcess, stopProcess } = useApp()
+ const { activeProfile, saveProfile, isRunning, startProcess, stopProcess } = useApp();
- const [draft, setDraft] = useState(null)
- const [saved, setSaved] = useState(false)
- const [section, setSection] = useState('general')
- const [pendingArg, setPendingArg] = useState(false)
- const [pendingChange, setPendingChange] = useState(null)
+ const [draft, setDraft] = useState(null);
+ const [saved, setSaved] = useState(false);
+ const [section, setSection] = useState('general');
+ const [pendingArg, setPendingArg] = useState(false);
+ const [pendingChange, setPendingChange] = useState(null);
useEffect(() => {
if (activeProfile) {
- setDraft({ ...activeProfile })
- setSaved(false)
- setPendingArg(false)
+ setDraft({ ...activeProfile });
+ setSaved(false);
+ setPendingArg(false);
}
- }, [activeProfile?.id])
+ }, [activeProfile?.id]);
const isDirty = useMemo(() => {
- if (!draft || !activeProfile) return false
- return JSON.stringify(draft) !== JSON.stringify(activeProfile)
- }, [draft, activeProfile])
+ if (!draft || !activeProfile) return false;
+ return JSON.stringify(draft) !== JSON.stringify(activeProfile);
+ }, [draft, activeProfile]);
const handleSave = useCallback(async () => {
- if (!draft) return
- await saveProfile(draft)
+ if (!draft) return;
+ await saveProfile(draft);
- activeProfile && Object.assign(activeProfile, draft)
+ activeProfile && Object.assign(activeProfile, draft);
- setSaved(true)
- setTimeout(() => setSaved(false), 1800)
- }, [draft, saveProfile, activeProfile])
+ setSaved(true);
+ setTimeout(() => setSaved(false), 1800);
+ }, [draft, saveProfile, activeProfile]);
const requestSectionChange = useCallback(
(next: Section) => {
if (pendingArg && next !== section) {
- setPendingChange(next)
- return
+ setPendingChange(next);
+ return;
}
- setPendingArg(false)
- setSection(next)
+ setPendingArg(false);
+ setSection(next);
},
[pendingArg, section]
- )
+ );
const handleRestart = useCallback(async () => {
- if (!draft) return
- await stopProcess(draft.id)
- setTimeout(() => startProcess(draft), 800)
- }, [draft, stopProcess, startProcess])
+ if (!draft) return;
+ await stopProcess(draft.id);
+ setTimeout(() => startProcess(draft), 800);
+ }, [draft, stopProcess, startProcess]);
if (!draft || !activeProfile) {
return (
No profile selected
- )
+ );
}
- const running = isRunning(draft.id)
- const color = draft.color || '#4ade80'
+ const running = isRunning(draft.id);
+ const color = draft.color || '#4ade80';
const update = (patch: Partial) => {
- setSaved(false)
- setDraft((prev) => (prev ? { ...prev, ...patch } : prev))
- }
+ setSaved(false);
+ setDraft((prev) => (prev ? { ...prev, ...patch } : prev));
+ };
return (
<>
@@ -196,15 +196,15 @@ export function ConfigTab() {
cancelLabel="Stay"
onConfirm={() => {
if (pendingChange) {
- setPendingArg(false)
- setSection(pendingChange)
+ setPendingArg(false);
+ setSection(pendingChange);
}
- setPendingChange(null)
+ setPendingChange(null);
}}
onCancel={() => setPendingChange(null)}
/>
>
- )
+ );
}
function GeneralSection({
@@ -214,11 +214,11 @@ function GeneralSection({
color,
onRestart,
}: {
- draft: Profile
- update: (p: Partial) => void
- running: boolean
- color: string
- onRestart: () => void
+ draft: Profile;
+ update: (p: Partial) => void;
+ running: boolean;
+ color: string;
+ onRestart: () => void;
}) {
return (
@@ -289,28 +289,28 @@ function GeneralSection({
)}
- )
+ );
}
function FilesSection({
draft,
update,
}: {
- draft: Profile
- update: (p: Partial) => void
+ draft: Profile;
+ update: (p: Partial) => void;
}) {
const handlePickJar = async () => {
- const p = await window.api.pickJar()
- if (p) update({ jarPath: p })
- }
+ const p = await window.api.pickJar();
+ if (p) update({ jarPath: p });
+ };
const handlePickDir = async () => {
- const p = await window.api.pickDir()
- if (p) update({ workingDir: p })
- }
+ const p = await window.api.pickDir();
+ if (p) update({ workingDir: p });
+ };
const handlePickJava = async () => {
- const p = await window.api.pickJava()
- if (p) update({ javaPath: p })
- }
+ const p = await window.api.pickJava();
+ if (p) update({ javaPath: p });
+ };
return (
@@ -339,7 +339,7 @@ function FilesSection({
rightElement={ }
/>
- )
+ );
}
function ArgSection({
@@ -347,9 +347,9 @@ function ArgSection({
hint,
children,
}: {
- title: string
- hint: string
- children: React.ReactNode
+ title: string;
+ hint: string;
+ children: React.ReactNode;
}) {
return (
@@ -359,7 +359,7 @@ function ArgSection({
{children}
- )
+ );
}
function FolderBtn({ onClick }: { onClick: () => void }) {
@@ -377,16 +377,16 @@ function FolderBtn({ onClick }: { onClick: () => void }) {
- )
+ );
}
function buildCmdPreview(p: Profile): string {
- const parts: string[] = [p.javaPath || 'java']
- p.jvmArgs.filter((a) => a.enabled && a.value).forEach((a) => parts.push(a.value))
+ const parts: string[] = [p.javaPath || 'java'];
+ p.jvmArgs.filter((a) => a.enabled && a.value).forEach((a) => parts.push(a.value));
p.systemProperties
.filter((a) => a.enabled && a.key)
- .forEach((a) => parts.push(a.value ? `-D${a.key}=${a.value}` : `-D${a.key}`))
- parts.push('-jar', p.jarPath || '')
- p.programArgs.filter((a) => a.enabled && a.value).forEach((a) => parts.push(a.value))
- return parts.join(' ')
+ .forEach((a) => parts.push(a.value ? `-D${a.key}=${a.value}` : `-D${a.key}`));
+ parts.push('-jar', p.jarPath || '');
+ p.programArgs.filter((a) => a.enabled && a.value).forEach((a) => parts.push(a.value));
+ return parts.join(' ');
}
diff --git a/src/renderer/components/profiles/ProfileSidebar.tsx b/src/renderer/components/profiles/ProfileSidebar.tsx
index 2d50d81..adaeb1d 100644
--- a/src/renderer/components/profiles/ProfileSidebar.tsx
+++ b/src/renderer/components/profiles/ProfileSidebar.tsx
@@ -1,11 +1,11 @@
-import React, { useState, useCallback } from 'react'
-import { Reorder } from 'framer-motion'
-import { useApp, PROFILE_COLORS } from '../../store/AppStore'
-import { Dialog } from '../common/Dialog'
-import { ContextMenu } from '../common/ContextMenu'
-import { TemplateModal } from './TemplateModal'
-import { useDevMode } from '../../hooks/useDevMode'
-import type { ContextMenuItem } from '../common/ContextMenu'
+import React, { useState, useCallback } from 'react';
+import { Reorder } from 'framer-motion';
+import { useApp, PROFILE_COLORS } from '../../store/AppStore';
+import { Dialog } from '../common/Dialog';
+import { ContextMenu } from '../common/ContextMenu';
+import { TemplateModal } from './TemplateModal';
+import { useDevMode } from '../../hooks/useDevMode';
+import type { ContextMenuItem } from '../common/ContextMenu';
import {
VscPlay,
VscDebugStop,
@@ -18,22 +18,22 @@ import {
VscAdd,
VscLayout,
VscCode,
-} from 'react-icons/vsc'
-import { Profile } from '../../../main/shared/types/Profile.types'
+} from 'react-icons/vsc';
+import { Profile } from '../../../main/shared/types/Profile.types';
interface Props {
- onOpenSettings: () => void
- onOpenFaq: () => void
- onOpenUtilities: () => void
- onOpenDeveloper: () => void
- onProfileClick?: () => void
- activeSidePanel: 'settings' | 'faq' | 'utilities' | 'developer' | null
+ onOpenSettings: () => void;
+ onOpenFaq: () => void;
+ onOpenUtilities: () => void;
+ onOpenDeveloper: () => void;
+ onProfileClick?: () => void;
+ activeSidePanel: 'settings' | 'faq' | 'utilities' | 'developer' | null;
}
interface CtxState {
- profileId: string
- x: number
- y: number
+ profileId: string;
+ x: number;
+ y: number;
}
export function ProfileSidebar({
@@ -55,44 +55,44 @@ export function ProfileSidebar({
clearConsole,
isRunning,
reorderProfiles,
- } = useApp()
+ } = useApp();
- const devMode = useDevMode()
- const [ctxMenu, setCtxMenu] = useState(null)
- const [deleteTarget, setDeleteTarget] = useState(null)
- const [actionError, setActionError] = useState(null)
- const [templateOpen, setTemplateOpen] = useState(false)
- const [draggingId, setDraggingId] = useState(null)
+ const devMode = useDevMode();
+ const [ctxMenu, setCtxMenu] = useState(null);
+ const [deleteTarget, setDeleteTarget] = useState(null);
+ const [actionError, setActionError] = useState(null);
+ const [templateOpen, setTemplateOpen] = useState(false);
+ const [draggingId, setDraggingId] = useState(null);
- const canDelete = state.profiles.length > 1
+ const canDelete = state.profiles.length > 1;
const handleContextMenu = useCallback((e: React.MouseEvent, profile: Profile) => {
- e.preventDefault()
- setCtxMenu({ profileId: profile.id, x: e.clientX, y: e.clientY })
- }, [])
+ e.preventDefault();
+ setCtxMenu({ profileId: profile.id, x: e.clientX, y: e.clientY });
+ }, []);
- const ctxProfile = ctxMenu ? state.profiles.find((p) => p.id === ctxMenu.profileId) : null
- const ctxRunning = ctxProfile ? isRunning(ctxProfile.id) : false
+ const ctxProfile = ctxMenu ? state.profiles.find((p) => p.id === ctxMenu.profileId) : null;
+ const ctxRunning = ctxProfile ? isRunning(ctxProfile.id) : false;
const handleStart = useCallback(
async (profile: Profile) => {
if (!profile.jarPath) {
- setActionError(`"${profile.name}" has no JAR configured.`)
- return
+ setActionError(`"${profile.name}" has no JAR configured.`);
+ return;
}
- const res = await startProcess(profile)
- if (!res.ok) setActionError(res.error ?? 'Failed to start')
+ const res = await startProcess(profile);
+ if (!res.ok) setActionError(res.error ?? 'Failed to start');
},
[startProcess]
- )
+ );
const handleStop = useCallback(
async (profile: Profile) => {
- const res = await stopProcess(profile.id)
- if (!res.ok) setActionError(res.error ?? 'Failed to stop')
+ const res = await stopProcess(profile.id);
+ if (!res.ok) setActionError(res.error ?? 'Failed to stop');
},
[stopProcess]
- )
+ );
const ctxItems: ContextMenuItem[] = ctxProfile
? [
@@ -114,8 +114,8 @@ export function ProfileSidebar({
label: 'Select',
icon: ,
onClick: () => {
- setActiveProfile(ctxProfile.id)
- onProfileClick?.()
+ setActiveProfile(ctxProfile.id);
+ onProfileClick?.();
},
},
{
@@ -130,13 +130,13 @@ export function ProfileSidebar({
danger: true,
disabled: !canDelete,
onClick: (e?: React.MouseEvent) => {
- if (!canDelete) return
- if (e?.shiftKey) deleteProfile(ctxProfile.id)
- else setDeleteTarget(ctxProfile)
+ if (!canDelete) return;
+ if (e?.shiftKey) deleteProfile(ctxProfile.id);
+ else setDeleteTarget(ctxProfile);
},
},
]
- : []
+ : [];
return (
<>
@@ -186,8 +186,8 @@ export function ProfileSidebar({
running={isRunning(profile.id)}
isDragging={draggingId === profile.id}
onClick={() => {
- setActiveProfile(profile.id)
- onProfileClick?.()
+ setActiveProfile(profile.id);
+ onProfileClick?.();
}}
onContextMenu={(e) => handleContextMenu(e, profile)}
/>
@@ -253,13 +253,13 @@ export function ProfileSidebar({
confirmLabel="Delete"
danger
onConfirm={async () => {
- if (deleteTarget) await deleteProfile(deleteTarget.id)
- setDeleteTarget(null)
+ if (deleteTarget) await deleteProfile(deleteTarget.id);
+ setDeleteTarget(null);
}}
onCancel={() => setDeleteTarget(null)}
/>
>
- )
+ );
}
function ProfileItem({
@@ -270,22 +270,22 @@ function ProfileItem({
onClick,
onContextMenu,
}: {
- profile: Profile
- active: boolean
- running: boolean
- isDragging: boolean
- onClick: () => void
- onContextMenu: (e: React.MouseEvent) => void
+ profile: Profile;
+ active: boolean;
+ running: boolean;
+ isDragging: boolean;
+ onClick: () => void;
+ onContextMenu: (e: React.MouseEvent) => void;
}) {
- const color = profile.color || PROFILE_COLORS[0]
- const jarName = profile.jarPath?.split(/[/\\]/).pop() ?? ''
+ const color = profile.color || PROFILE_COLORS[0];
+ const jarName = profile.jarPath?.split(/[/\\]/).pop() ?? '';
return (
{
- if (e.detail !== 0) e.preventDefault()
+ if (e.detail !== 0) e.preventDefault();
}}
className={[
'w-full flex items-center gap-2.5 px-2.5 py-2 rounded-md text-left transition-colors',
@@ -319,7 +319,7 @@ function ProfileItem({
{running && }
- )
+ );
}
function FooterButton({
@@ -329,11 +329,11 @@ function FooterButton({
icon,
accent,
}: {
- label: string
- active: boolean
- onClick: () => void
- icon: React.ReactNode
- accent?: boolean
+ label: string;
+ active: boolean;
+ onClick: () => void;
+ icon: React.ReactNode;
+ accent?: boolean;
}) {
return (
- )
+ );
}
diff --git a/src/renderer/components/profiles/ProfileTab.tsx b/src/renderer/components/profiles/ProfileTab.tsx
index 095fee1..e30bda1 100644
--- a/src/renderer/components/profiles/ProfileTab.tsx
+++ b/src/renderer/components/profiles/ProfileTab.tsx
@@ -1,40 +1,40 @@
-import React, { useState, useEffect } from 'react'
-import { useApp, PROFILE_COLORS } from '../../store/AppStore'
-import { Button } from '../common/Button'
-import { Input } from '../common/Input'
-import { Dialog } from '../common/Dialog'
-import { Profile } from '../../../main/shared/types/Profile.types'
+import React, { useState, useEffect } from 'react';
+import { useApp, PROFILE_COLORS } from '../../store/AppStore';
+import { Button } from '../common/Button';
+import { Input } from '../common/Input';
+import { Dialog } from '../common/Dialog';
+import { Profile } from '../../../main/shared/types/Profile.types';
export function ProfileTab() {
- const { activeProfile, saveProfile, deleteProfile } = useApp()
- const [draft, setDraft] = useState(null)
- const [saved, setSaved] = useState(false)
- const [showDelete, setShowDelete] = useState(false)
+ const { activeProfile, saveProfile, deleteProfile } = useApp();
+ const [draft, setDraft] = useState(null);
+ const [saved, setSaved] = useState(false);
+ const [showDelete, setShowDelete] = useState(false);
useEffect(() => {
- if (activeProfile) setDraft({ ...activeProfile })
- }, [activeProfile?.id])
+ if (activeProfile) setDraft({ ...activeProfile });
+ }, [activeProfile?.id]);
if (!draft || !activeProfile)
return (
No profile selected
- )
+ );
const update = (patch: Partial) =>
- setDraft((prev) => (prev ? { ...prev, ...patch } : prev))
- const color = draft.color || PROFILE_COLORS[0]
+ setDraft((prev) => (prev ? { ...prev, ...patch } : prev));
+ const color = draft.color || PROFILE_COLORS[0];
const handleSave = async () => {
- await saveProfile(draft)
- setSaved(true)
- setTimeout(() => setSaved(false), 1800)
- }
+ await saveProfile(draft);
+ setSaved(true);
+ setTimeout(() => setSaved(false), 1800);
+ };
const handleDelete = async () => {
- await deleteProfile(draft.id)
- setShowDelete(false)
- }
+ await deleteProfile(draft.id);
+ setShowDelete(false);
+ };
return (
<>
@@ -108,7 +108,7 @@ export function ProfileTab() {
onCancel={() => setShowDelete(false)}
/>
>
- )
+ );
}
function Section({
@@ -116,9 +116,9 @@ function Section({
hint,
children,
}: {
- title: string
- hint?: string
- children: React.ReactNode
+ title: string;
+ hint?: string;
+ children: React.ReactNode;
}) {
return (
@@ -128,5 +128,5 @@ function Section({
{children}
- )
+ );
}
diff --git a/src/renderer/components/profiles/TemplateModal.tsx b/src/renderer/components/profiles/TemplateModal.tsx
index 14c5662..b84c888 100644
--- a/src/renderer/components/profiles/TemplateModal.tsx
+++ b/src/renderer/components/profiles/TemplateModal.tsx
@@ -1,54 +1,54 @@
-import React, { useState, useEffect } from 'react'
-import { Modal } from '../common/Modal'
-import { Button } from '../common/Button'
-import { useApp } from '../../store/AppStore'
-import { VscPackage, VscTag, VscRefresh, VscAdd } from 'react-icons/vsc'
-import { LuShield } from 'react-icons/lu'
-import { ProfileTemplate } from '../../../main/shared/types/GitHub.types'
+import React, { useState, useEffect } from 'react';
+import { Modal } from '../common/Modal';
+import { Button } from '../common/Button';
+import { useApp } from '../../store/AppStore';
+import { VscPackage, VscTag, VscRefresh, VscAdd } from 'react-icons/vsc';
+import { LuShield } from 'react-icons/lu';
+import { ProfileTemplate } from '../../../main/shared/types/GitHub.types';
-const APP_TEMPLATE_VERSION = 1
+const APP_TEMPLATE_VERSION = 1;
interface TemplateEntry {
- filename: string
- template: ProfileTemplate
+ filename: string;
+ template: ProfileTemplate;
}
interface Props {
- open: boolean
- onClose: () => void
+ open: boolean;
+ onClose: () => void;
}
function isCompatible(t: ProfileTemplate): boolean {
- return t.templateVersion <= APP_TEMPLATE_VERSION
+ return t.templateVersion <= APP_TEMPLATE_VERSION;
}
export function TemplateModal({ open, onClose }: Props) {
- const { createProfile } = useApp()
- const [loading, setLoading] = useState(false)
- const [error, setError] = useState(null)
- const [templates, setTemplates] = useState(null)
- const [selected, setSelected] = useState(null)
- const [filter, setFilter] = useState('')
+ const { createProfile } = useApp();
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [templates, setTemplates] = useState(null);
+ const [selected, setSelected] = useState(null);
+ const [filter, setFilter] = useState('');
const load = async () => {
- setLoading(true)
- setError(null)
- const res = await window.api.fetchTemplates()
- setLoading(false)
+ setLoading(true);
+ setError(null);
+ const res = await window.api.fetchTemplates();
+ setLoading(false);
if (!res.ok || !res.data) {
- setError(res.error ?? 'Failed to load templates')
- return
+ setError(res.error ?? 'Failed to load templates');
+ return;
}
- setTemplates(res.data)
- }
+ setTemplates(res.data);
+ };
useEffect(() => {
- if (open && templates === null) load()
- }, [open])
+ if (open && templates === null) load();
+ }, [open]);
const handleCreate = () => {
- if (!selected) return
- const tpl = selected.template
+ if (!selected) return;
+ const tpl = selected.template;
createProfile({
name: tpl.name,
jvmArgs: tpl.defaults.jvmArgs,
@@ -59,9 +59,9 @@ export function TemplateModal({ open, onClose }: Props) {
autoRestart: tpl.defaults.autoRestart,
autoRestartInterval: tpl.defaults.autoRestartInterval,
color: tpl.defaults.color,
- })
- onClose()
- }
+ });
+ onClose();
+ };
const filtered =
templates?.filter(
@@ -70,15 +70,15 @@ export function TemplateModal({ open, onClose }: Props) {
e.template.name.toLowerCase().includes(filter.toLowerCase()) ||
e.template.category.toLowerCase().includes(filter.toLowerCase()) ||
e.template.tags.some((t) => t.toLowerCase().includes(filter.toLowerCase()))
- ) ?? []
+ ) ?? [];
// Group by category
const grouped = filtered.reduce>((acc, e) => {
- const cat = e.template.category || 'Other'
- if (!acc[cat]) acc[cat] = []
- acc[cat].push(e)
- return acc
- }, {})
+ const cat = e.template.category || 'Other';
+ if (!acc[cat]) acc[cat] = [];
+ acc[cat].push(e);
+ return acc;
+ }, {});
return (
@@ -124,7 +124,7 @@ export function TemplateModal({ open, onClose }: Props) {
{cat}
{entries.map((entry) => {
- const compat = isCompatible(entry.template)
+ const compat = isCompatible(entry.template);
return (
)}
- )
+ );
})}
))}
@@ -241,7 +241,7 @@ export function TemplateModal({ open, onClose }: Props) {
- )
+ );
}
function Section({ title, children }: { title: string; children: React.ReactNode }) {
@@ -250,7 +250,7 @@ function Section({ title, children }: { title: string; children: React.ReactNode
{title}
{children}
- )
+ );
}
function Pill({ value, enabled }: { value: string; enabled: boolean }) {
@@ -265,12 +265,12 @@ function Pill({ value, enabled }: { value: string; enabled: boolean }) {
>
{value}
- )
+ );
}
function Divider() {
- return
+ return
;
}
function Empty() {
- return none
+ return none ;
}
diff --git a/src/renderer/components/settings/SettingsTab.tsx b/src/renderer/components/settings/SettingsTab.tsx
index d88ef66..f6f74e9 100644
--- a/src/renderer/components/settings/SettingsTab.tsx
+++ b/src/renderer/components/settings/SettingsTab.tsx
@@ -1,76 +1,76 @@
-import React, { useState, useEffect, useMemo } from 'react'
-import { useApp } from '../../store/AppStore'
-import { Button } from '../common/Button'
-import { Toggle } from '../common/Toggle'
-import { VersionChecker } from './version/VersionChecker'
-import { REST_API_CONFIG } from '../../../main/shared/config/RestApi.config'
-import { version } from '../../../../package.json'
-import { AppSettings, JRCEnvironment } from '../../../main/shared/types/App.types'
+import React, { useState, useEffect, useMemo } from 'react';
+import { useApp } from '../../store/AppStore';
+import { Button } from '../common/Button';
+import { Toggle } from '../common/Toggle';
+import { VersionChecker } from './version/VersionChecker';
+import { REST_API_CONFIG } from '../../../main/shared/config/RestApi.config';
+import { version } from '../../../../package.json';
+import { AppSettings, JRCEnvironment } from '../../../main/shared/types/App.types';
export function SettingsTab() {
- const { state, saveSettings } = useApp()
- const [draft, setDraft] = useState(null)
- const [saved, setSaved] = useState(false)
+ const { state, saveSettings } = useApp();
+ const [draft, setDraft] = useState(null);
+ const [saved, setSaved] = useState(false);
const set = (patch: Partial) => {
- setSaved(false)
- setDraft((prev) => (prev ? { ...prev, ...patch } : prev))
- }
+ setSaved(false);
+ setDraft((prev) => (prev ? { ...prev, ...patch } : prev));
+ };
useEffect(() => {
- if (!state.settings) return
+ if (!state.settings) return;
setDraft((prev) => {
- if (!prev) return state.settings
+ if (!prev) return state.settings;
// keep user changes, but refresh from store
return {
...state.settings,
...prev,
devModeEnabled: prev.devModeEnabled, // external wins
- }
- })
- }, [state.settings])
+ };
+ });
+ }, [state.settings]);
useEffect(() => {
const listener = async (e: JRCEnvironment) => {
- setSaved(false)
+ setSaved(false);
setDraft((prev) => {
- if (!prev) return prev
- if (prev.devModeEnabled === e.devMode) return prev
+ if (!prev) return prev;
+ if (prev.devModeEnabled === e.devMode) return prev;
return {
...prev,
devModeEnabled: e.devMode,
- }
- })
+ };
+ });
// 🔥 sync to store so isDirty stays correct
if (state.settings && state.settings.devModeEnabled !== e.devMode) {
await saveSettings({
...state.settings,
devModeEnabled: e.devMode,
- })
+ });
}
- }
+ };
- window.env.onChange(listener)
- }, [state.settings, saveSettings])
+ window.env.onChange(listener);
+ }, [state.settings, saveSettings]);
const isDirty = useMemo(() => {
- if (!draft || !state.settings) return false
- return JSON.stringify(draft) !== JSON.stringify(state.settings)
- }, [draft, state.settings])
+ if (!draft || !state.settings) return false;
+ return JSON.stringify(draft) !== JSON.stringify(state.settings);
+ }, [draft, state.settings]);
- if (!draft) return null
+ if (!draft) return null;
const handleSave = async () => {
- await saveSettings(draft)
- window.env.reload()
- setSaved(true)
- setTimeout(() => setSaved(false), 2000)
- }
+ await saveSettings(draft);
+ window.env.reload();
+ setSaved(true);
+ setTimeout(() => setSaved(false), 2000);
+ };
return (
@@ -227,7 +227,7 @@ export function SettingsTab() {
- )
+ );
}
function Section({ title, children }: { title: string; children: React.ReactNode }) {
@@ -236,7 +236,7 @@ function Section({ title, children }: { title: string; children: React.ReactNode
{title}
{children}
- )
+ );
}
function Row({
@@ -245,10 +245,10 @@ function Row({
sub,
children,
}: {
- label: string
- hint?: string
- sub?: boolean
- children?: React.ReactNode
+ label: string;
+ hint?: string;
+ sub?: boolean;
+ children?: React.ReactNode;
}) {
return (
- )
+ );
}
function Divider() {
- return
+ return
;
}
function NumInput({
@@ -274,11 +274,11 @@ function NumInput({
step,
onChange,
}: {
- value: number
- min: number
- max: number
- step: number
- onChange: (v: number) => void
+ value: number;
+ min: number;
+ max: number;
+ step: number;
+ onChange: (v: number) => void;
}) {
return (
onChange(Number(e.target.value))}
className="w-24 bg-transparent border border-surface-border rounded-md px-2.5 py-1.5 text-sm font-mono text-text-primary text-right focus:outline-none focus:border-accent/40 transition-colors"
/>
- )
+ );
}
diff --git a/src/renderer/components/settings/version/ReleaseModal.tsx b/src/renderer/components/settings/version/ReleaseModal.tsx
index 294947f..fd7439c 100644
--- a/src/renderer/components/settings/version/ReleaseModal.tsx
+++ b/src/renderer/components/settings/version/ReleaseModal.tsx
@@ -1,6 +1,6 @@
-import React, { useState, useEffect, useCallback } from 'react'
-import { Modal } from '../../common/Modal'
-import { Button } from '../../common/Button'
+import React, { useState, useEffect, useCallback } from 'react';
+import { Modal } from '../../common/Modal';
+import { Button } from '../../common/Button';
import {
VscPackage,
VscGithub,
@@ -11,29 +11,29 @@ import {
VscDebugPause,
VscDebugContinue,
VscClose,
-} from 'react-icons/vsc'
-import { LuDownload, LuExternalLink, LuCheck, LuRotateCcw } from 'react-icons/lu'
-import { GitHubAsset, GitHubRelease } from '../../../../main/shared/types/GitHub.types'
+} from 'react-icons/vsc';
+import { LuDownload, LuExternalLink, LuCheck, LuRotateCcw } from 'react-icons/lu';
+import { GitHubAsset, GitHubRelease } from '../../../../main/shared/types/GitHub.types';
interface Props {
- release: GitHubRelease
- open: boolean
- onClose: () => void
+ release: GitHubRelease;
+ open: boolean;
+ onClose: () => void;
}
interface DownloadProgress {
- filename: string
- bytesWritten: number
- totalBytes: number
- percent: number
- status: 'downloading' | 'paused' | 'done' | 'error' | 'cancelled'
- error?: string
+ filename: string;
+ bytesWritten: number;
+ totalBytes: number;
+ percent: number;
+ status: 'downloading' | 'paused' | 'done' | 'error' | 'cancelled';
+ error?: string;
}
function formatBytes(bytes: number): string {
- if (bytes < 1024) return `${bytes} B`
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
- return `${(bytes / 1024 / 1024).toFixed(1)} MB`
+ if (bytes < 1024) return `${bytes} B`;
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
+ return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
}
function formatDate(iso: string): string {
@@ -41,24 +41,24 @@ function formatDate(iso: string): string {
year: 'numeric',
month: 'long',
day: 'numeric',
- })
+ });
}
function getPlatformAsset(assets?: GitHubAsset[]): GitHubAsset | undefined {
- if (!assets || !Array.isArray(assets)) return undefined
+ if (!assets || !Array.isArray(assets)) return undefined;
return (
assets.find((a) => a.name.endsWith('.exe') || a.name.endsWith('.msi')) ??
assets.find((a) => a.name.endsWith('.dmg') || a.name.endsWith('.pkg')) ??
assets.find((a) => a.name.endsWith('.AppImage') || a.name.endsWith('.deb'))
- )
+ );
}
function DownloadProgressBar({ progress }: { progress: DownloadProgress }) {
- const { status, percent, bytesWritten, totalBytes, error } = progress
- const isDone = status === 'done'
- const isError = status === 'error'
- const isPaused = status === 'paused'
- const isCancelled = status === 'cancelled'
+ const { status, percent, bytesWritten, totalBytes, error } = progress;
+ const isDone = status === 'done';
+ const isError = status === 'error';
+ const isPaused = status === 'paused';
+ const isCancelled = status === 'cancelled';
const barColor =
isError || isCancelled
@@ -67,7 +67,7 @@ function DownloadProgressBar({ progress }: { progress: DownloadProgress }) {
? 'bg-green-500'
: isPaused
? 'bg-yellow-400'
- : 'bg-accent'
+ : 'bg-accent';
const label = isError
? (error ?? 'Error')
@@ -77,9 +77,9 @@ function DownloadProgressBar({ progress }: { progress: DownloadProgress }) {
? 'Complete'
: isPaused
? 'Paused'
- : `${formatBytes(bytesWritten)}${totalBytes > 0 ? ` / ${formatBytes(totalBytes)}` : ''}`
+ : `${formatBytes(bytesWritten)}${totalBytes > 0 ? ` / ${formatBytes(totalBytes)}` : ''}`;
- const percentLabel = isDone ? '100%' : `${percent}%`
+ const percentLabel = isDone ? '100%' : `${percent}%`;
const percentColor = isDone
? 'text-green-400'
@@ -87,7 +87,7 @@ function DownloadProgressBar({ progress }: { progress: DownloadProgress }) {
? 'text-red-400'
: isPaused
? 'text-yellow-400'
- : 'text-accent'
+ : 'text-accent';
return (
@@ -104,63 +104,63 @@ function DownloadProgressBar({ progress }: { progress: DownloadProgress }) {
{percentLabel}
- )
+ );
}
export function ReleaseModal({ release, open, onClose }: Props) {
- const [downloads, setDownloads] = useState>(new Map())
+ const [downloads, setDownloads] = useState>(new Map());
const updateDownload = useCallback((payload: DownloadProgress) => {
- setDownloads((prev) => new Map(prev).set(payload.filename, payload))
- }, [])
+ setDownloads((prev) => new Map(prev).set(payload.filename, payload));
+ }, []);
const resetDownload = useCallback((filename: string) => {
setDownloads((prev) => {
- const next = new Map(prev)
- next.delete(filename)
- return next
- })
- }, [])
+ const next = new Map(prev);
+ next.delete(filename);
+ return next;
+ });
+ }, []);
useEffect(() => {
const unsub = window.api.onDownloadProgress((progress: DownloadProgress) => {
- updateDownload(progress)
- })
- return () => unsub()
- }, [updateDownload])
+ updateDownload(progress);
+ });
+ return () => unsub();
+ }, [updateDownload]);
- const platformAsset = getPlatformAsset(release.assets)
+ const platformAsset = getPlatformAsset(release.assets);
const handleDownload = async (asset: GitHubAsset) => {
// Don't touch download state here — main process drives everything via events
- await window.api.downloadAsset(asset.browser_download_url, asset.name)
- }
+ await window.api.downloadAsset(asset.browser_download_url, asset.name);
+ };
const handlePause = async (filename: string) => {
- const dl = downloads.get(filename)
- if (!dl) return
+ const dl = downloads.get(filename);
+ if (!dl) return;
if (dl.status === 'paused') {
- await window.api.resumeDownload(filename)
+ await window.api.resumeDownload(filename);
} else {
- await window.api.pauseDownload(filename)
+ await window.api.pauseDownload(filename);
}
// State update comes from the progress event sent by main
- }
+ };
const handleCancel = async (filename: string) => {
- await window.api.cancelDownload(filename)
+ await window.api.cancelDownload(filename);
// State update comes from the progress event sent by main
- }
+ };
- const getDl = (name: string) => downloads.get(name)
+ const getDl = (name: string) => downloads.get(name);
const isActive = (name: string) => {
- const s = downloads.get(name)?.status
- return s === 'downloading' || s === 'paused'
- }
+ const s = downloads.get(name)?.status;
+ return s === 'downloading' || s === 'paused';
+ };
const renderControls = (asset: GitHubAsset, variant: 'primary' | 'ghost') => {
- const dl = getDl(asset.name)
- const active = isActive(asset.name)
+ const dl = getDl(asset.name);
+ const active = isActive(asset.name);
return (
@@ -210,8 +210,8 @@ export function ReleaseModal({ release, open, onClose }: Props) {
variant={variant}
size="sm"
onClick={() => {
- resetDownload(asset.name)
- handleDownload(asset)
+ resetDownload(asset.name);
+ handleDownload(asset);
}}
>
Retry
@@ -225,10 +225,10 @@ export function ReleaseModal({ release, open, onClose }: Props) {
)}
- )
- }
+ );
+ };
- const bodyLines = (release.body ?? '').split('\n')
+ const bodyLines = (release.body ?? '').split('\n');
return (
@@ -310,34 +310,34 @@ export function ReleaseModal({ release, open, onClose }: Props) {
{bodyLines.map((line, i) => {
- const h2 = line.startsWith('## ')
- const h3 = line.startsWith('### ')
- const li = line.startsWith('- ') || line.startsWith('* ')
- if (!line.trim()) return
+ const h2 = line.startsWith('## ');
+ const h3 = line.startsWith('### ');
+ const li = line.startsWith('- ') || line.startsWith('* ');
+ if (!line.trim()) return
;
if (h2)
return (
{line.slice(3)}
- )
+ );
if (h3)
return (
{line.slice(4)}
- )
+ );
if (li)
return (
·
{line.slice(2)}
- )
+ );
return (
{line}
- )
+ );
})}
@@ -392,5 +392,5 @@ export function ReleaseModal({ release, open, onClose }: Props) {
- )
+ );
}
diff --git a/src/renderer/components/settings/version/VersionChecker.tsx b/src/renderer/components/settings/version/VersionChecker.tsx
index 6cf0dea..10a23fa 100644
--- a/src/renderer/components/settings/version/VersionChecker.tsx
+++ b/src/renderer/components/settings/version/VersionChecker.tsx
@@ -1,44 +1,44 @@
-import React, { useState, useCallback } from 'react'
-import { Button } from '../../common/Button'
-import { Tooltip } from '../../common/Tooltip'
-import { ReleaseModal } from './ReleaseModal'
-import { VscCheck, VscWarning, VscSync, VscCircleSlash } from 'react-icons/vsc'
-import { GitHubRelease } from '../../../../main/shared/types/GitHub.types'
+import React, { useState, useCallback } from 'react';
+import { Button } from '../../common/Button';
+import { Tooltip } from '../../common/Tooltip';
+import { ReleaseModal } from './ReleaseModal';
+import { VscCheck, VscWarning, VscSync, VscCircleSlash } from 'react-icons/vsc';
+import { GitHubRelease } from '../../../../main/shared/types/GitHub.types';
interface Props {
- currentVersion: string
+ currentVersion: string;
}
-type CheckState = 'idle' | 'checking' | 'up-to-date' | 'update-available' | 'error'
+type CheckState = 'idle' | 'checking' | 'up-to-date' | 'update-available' | 'error';
function semverGt(a: string, b: string): boolean {
- const parse = (v: string) => v.replace(/^v/, '').split('.').map(Number)
- const [am, an, ap] = parse(a)
- const [bm, bn, bp] = parse(b)
- if (am !== bm) return am > bm
- if (an !== bn) return an > bn
- return ap > bp
+ const parse = (v: string) => v.replace(/^v/, '').split('.').map(Number);
+ const [am, an, ap] = parse(a);
+ const [bm, bn, bp] = parse(b);
+ if (am !== bm) return am > bm;
+ if (an !== bn) return an > bn;
+ return ap > bp;
}
export function VersionChecker({ currentVersion }: Props) {
- const [checkState, setCheckState] = useState('idle')
- const [release, setRelease] = useState(null)
- const [modalOpen, setModalOpen] = useState(false)
- const [errorMsg, setErrorMsg] = useState(null)
+ const [checkState, setCheckState] = useState('idle');
+ const [release, setRelease] = useState(null);
+ const [modalOpen, setModalOpen] = useState(false);
+ const [errorMsg, setErrorMsg] = useState(null);
const check = useCallback(async () => {
- setCheckState('checking')
- setErrorMsg(null)
- const res = await window.api.fetchLatestRelease()
+ setCheckState('checking');
+ setErrorMsg(null);
+ const res = await window.api.fetchLatestRelease();
if (!res.ok || !res.data) {
- setCheckState('error')
- setErrorMsg(res.error ?? 'Could not reach GitHub')
- return
+ setCheckState('error');
+ setErrorMsg(res.error ?? 'Could not reach GitHub');
+ return;
}
- setRelease(res.data)
- const remoteVersion = (res.data.tag_name ?? '').replace(/^v/, '')
- setCheckState(semverGt(remoteVersion, currentVersion) ? 'update-available' : 'up-to-date')
- }, [currentVersion])
+ setRelease(res.data);
+ const remoteVersion = (res.data.tag_name ?? '').replace(/^v/, '');
+ setCheckState(semverGt(remoteVersion, currentVersion) ? 'update-available' : 'up-to-date');
+ }, [currentVersion]);
const Icon = {
idle: VscSync,
@@ -46,7 +46,7 @@ export function VersionChecker({ currentVersion }: Props) {
'up-to-date': VscCheck,
'update-available': VscWarning,
error: VscCircleSlash,
- }[checkState]
+ }[checkState];
const iconColor = {
idle: 'text-text-muted',
@@ -54,7 +54,7 @@ export function VersionChecker({ currentVersion }: Props) {
'up-to-date': 'text-accent',
'update-available': 'text-yellow-400',
error: 'text-red-400',
- }[checkState]
+ }[checkState];
const tooltipContent = {
idle: 'Click to check for updates',
@@ -64,16 +64,16 @@ export function VersionChecker({ currentVersion }: Props) {
? `${release.tag_name} is available — click for details`
: 'Update available',
error: errorMsg ?? 'Check failed',
- }[checkState]
+ }[checkState];
const handleClick = () => {
if (checkState === 'idle' || checkState === 'error') {
- check()
- return
+ check();
+ return;
}
if ((checkState === 'up-to-date' || checkState === 'update-available') && release)
- setModalOpen(true)
- }
+ setModalOpen(true);
+ };
return (
<>
@@ -106,5 +106,5 @@ export function VersionChecker({ currentVersion }: Props) {
setModalOpen(false)} />
)}
>
- )
+ );
}
diff --git a/src/renderer/components/utils/ActivityLogPanel.tsx b/src/renderer/components/utils/ActivityLogPanel.tsx
index 8f880bb..e5efbf1 100644
--- a/src/renderer/components/utils/ActivityLogPanel.tsx
+++ b/src/renderer/components/utils/ActivityLogPanel.tsx
@@ -1,23 +1,23 @@
-import React, { useState, useCallback } from 'react'
-import { Button } from '../common/Button'
-import { Dialog } from '../common/Dialog'
-import { VscListUnordered } from 'react-icons/vsc'
-import { ProcessLogEntry } from '../../../main/shared/types/Process.types'
+import React, { useState, useCallback } from 'react';
+import { Button } from '../common/Button';
+import { Dialog } from '../common/Dialog';
+import { VscListUnordered } from 'react-icons/vsc';
+import { ProcessLogEntry } from '../../../main/shared/types/Process.types';
export function ActivityLogPanel() {
- const [entries, setEntries] = useState(null)
- const [loading, setLoading] = useState(false)
- const [confirmClear, setConfirmClear] = useState(false)
+ const [entries, setEntries] = useState(null);
+ const [loading, setLoading] = useState(false);
+ const [confirmClear, setConfirmClear] = useState(false);
const load = useCallback(async () => {
- setLoading(true)
- setEntries(await window.api.getProcessLog())
- setLoading(false)
- }, [])
+ setLoading(true);
+ setEntries(await window.api.getProcessLog());
+ setLoading(false);
+ }, []);
React.useEffect(() => {
- load()
- }, [load])
+ load();
+ }, [load]);
return (
<>
@@ -66,19 +66,19 @@ export function ActivityLogPanel() {
confirmLabel="Clear"
danger
onConfirm={async () => {
- await window.api.clearProcessLog()
- setEntries([])
- setConfirmClear(false)
+ await window.api.clearProcessLog();
+ setEntries([]);
+ setConfirmClear(false);
}}
onCancel={() => setConfirmClear(false)}
/>
>
- )
+ );
}
function LogEntryRow({ entry }: { entry: ProcessLogEntry }) {
- const duration = entry.stoppedAt ? formatDuration(entry.stoppedAt - entry.startedAt) : null
- const jarName = entry.jarPath.split(/[/\\]/).pop() ?? entry.jarPath
+ const duration = entry.stoppedAt ? formatDuration(entry.stoppedAt - entry.startedAt) : null;
+ const jarName = entry.jarPath.split(/[/\\]/).pop() ?? entry.jarPath;
return (
@@ -118,7 +118,7 @@ function LogEntryRow({ entry }: { entry: ProcessLogEntry }) {
- )
+ );
}
function EmptyState({ icon, text }: { icon: React.ReactNode; text: string }) {
@@ -127,7 +127,7 @@ function EmptyState({ icon, text }: { icon: React.ReactNode; text: string }) {
{icon}
{text}
- )
+ );
}
function formatTime(ts: number): string {
@@ -135,12 +135,12 @@ function formatTime(ts: number): string {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
- })
+ });
}
function formatDuration(ms: number): string {
- const s = Math.floor(ms / 1000)
- if (s < 60) return `${s}s`
- if (s < 3600) return `${Math.floor(s / 60)}m ${s % 60}s`
- return `${Math.floor(s / 3600)}h ${Math.floor((s % 3600) / 60)}m`
+ const s = Math.floor(ms / 1000);
+ if (s < 60) return `${s}s`;
+ if (s < 3600) return `${Math.floor(s / 60)}m ${s % 60}s`;
+ return `${Math.floor(s / 3600)}h ${Math.floor((s % 3600) / 60)}m`;
}
diff --git a/src/renderer/components/utils/ScannerPanel.tsx b/src/renderer/components/utils/ScannerPanel.tsx
index 9299c3a..51c94a9 100644
--- a/src/renderer/components/utils/ScannerPanel.tsx
+++ b/src/renderer/components/utils/ScannerPanel.tsx
@@ -1,66 +1,66 @@
-import React, { useState, useCallback } from 'react'
-import { Button } from '../common/Button'
-import { Dialog } from '../common/Dialog'
-import { VscCheck } from 'react-icons/vsc'
-import { LuScanLine } from 'react-icons/lu'
-import { JavaProcessInfo } from '../../../main/shared/types/Process.types'
+import React, { useState, useCallback } from 'react';
+import { Button } from '../common/Button';
+import { Dialog } from '../common/Dialog';
+import { VscCheck } from 'react-icons/vsc';
+import { LuScanLine } from 'react-icons/lu';
+import { JavaProcessInfo } from '../../../main/shared/types/Process.types';
interface KillIntent {
- proc: JavaProcessInfo
- nonJava: boolean
+ proc: JavaProcessInfo;
+ nonJava: boolean;
}
-type Filter = 'java' | 'all'
+type Filter = 'java' | 'all';
export function ScannerPanel() {
- const [results, setResults] = useState(null)
- const [scanning, setScanning] = useState(false)
- const [killIntent, setKillIntent] = useState(null)
- const [killAllConfirm, setKillAllConfirm] = useState(false)
- const [statusMsg, setStatusMsg] = useState<{ text: string; ok: boolean } | null>(null)
- const [killedPids, setKilledPids] = useState>(new Set())
- const [filter, setFilter] = useState('java')
- const [search, setSearch] = useState('')
- const [expandedPid, setExpandedPid] = useState(null)
+ const [results, setResults] = useState(null);
+ const [scanning, setScanning] = useState(false);
+ const [killIntent, setKillIntent] = useState(null);
+ const [killAllConfirm, setKillAllConfirm] = useState(false);
+ const [statusMsg, setStatusMsg] = useState<{ text: string; ok: boolean } | null>(null);
+ const [killedPids, setKilledPids] = useState>(new Set());
+ const [filter, setFilter] = useState('java');
+ const [search, setSearch] = useState('');
+ const [expandedPid, setExpandedPid] = useState(null);
const scan = useCallback(async () => {
- setScanning(true)
- setStatusMsg(null)
- setKilledPids(new Set())
- setSearch('')
- setExpandedPid(null)
+ setScanning(true);
+ setStatusMsg(null);
+ setKilledPids(new Set());
+ setSearch('');
+ setExpandedPid(null);
- const found = await window.api.scanAllProcesses()
- setResults(found)
- setScanning(false)
+ const found = await window.api.scanAllProcesses();
+ setResults(found);
+ setScanning(false);
- const javaCount = found.filter((p) => p.isJava).length
- setStatusMsg({ text: `Found ${found.length} processes — ${javaCount} java`, ok: true })
- }, [])
+ const javaCount = found.filter((p) => p.isJava).length;
+ setStatusMsg({ text: `Found ${found.length} processes — ${javaCount} java`, ok: true });
+ }, []);
const handleKill = async () => {
- if (!killIntent) return
- const res = await window.api.killPid(killIntent.proc.pid)
+ if (!killIntent) return;
+ const res = await window.api.killPid(killIntent.proc.pid);
if (res.ok) {
- setKilledPids((prev) => new Set([...prev, killIntent.proc.pid]))
- setStatusMsg({ text: `Killed PID ${killIntent.proc.pid}`, ok: true })
+ setKilledPids((prev) => new Set([...prev, killIntent.proc.pid]));
+ setStatusMsg({ text: `Killed PID ${killIntent.proc.pid}`, ok: true });
} else {
- setStatusMsg({ text: `Failed to kill PID ${killIntent.proc.pid}: ${res.error}`, ok: false })
+ setStatusMsg({ text: `Failed to kill PID ${killIntent.proc.pid}: ${res.error}`, ok: false });
}
- setKillIntent(null)
- }
+ setKillIntent(null);
+ };
const handleKillAll = async () => {
- const res = await window.api.killAllJava()
+ const res = await window.api.killAllJava();
setStatusMsg({
text: `Killed ${res.killed} java process${res.killed === 1 ? '' : 'es'}`,
ok: true,
- })
- setKillAllConfirm(false)
- setTimeout(scan, 800)
- }
+ });
+ setKillAllConfirm(false);
+ setTimeout(scan, 800);
+ };
- const searchLower = search.trim().toLowerCase()
+ const searchLower = search.trim().toLowerCase();
const visible = results
? (filter === 'java' ? results.filter((r) => r.isJava) : results)
.filter((r) => !killedPids.has(r.pid))
@@ -70,9 +70,9 @@ export function ScannerPanel() {
r.command.toLowerCase().includes(searchLower) ||
String(r.pid).includes(searchLower)
)
- : null
+ : null;
- const javaVisible = visible?.some((r) => r.isJava) ?? false
+ const javaVisible = visible?.some((r) => r.isJava) ?? false;
return (
@@ -170,7 +170,7 @@ export function ScannerPanel() {
onCancel={() => setKillAllConfirm(false)}
/>
- )
+ );
}
function FilterToggle({ value, onChange }: { value: Filter; onChange: (f: Filter) => void }) {
@@ -191,7 +191,7 @@ function FilterToggle({ value, onChange }: { value: Filter; onChange: (f: Filter
))}
- )
+ );
}
function ProcessRow({
@@ -200,10 +200,10 @@ function ProcessRow({
onToggle,
onKill,
}: {
- proc: JavaProcessInfo
- expanded: boolean
- onToggle: () => void
- onKill: () => void
+ proc: JavaProcessInfo;
+ expanded: boolean;
+ onToggle: () => void;
+ onKill: () => void;
}) {
return (
)}
- )
+ );
}
function ExpandChevron({ expanded, onClick }: { expanded: boolean; onClick: () => void }) {
@@ -292,7 +292,7 @@ function ExpandChevron({ expanded, onClick }: { expanded: boolean; onClick: () =
- )
+ );
}
function DetailRow({
@@ -301,10 +301,10 @@ function DetailRow({
mono,
wrap,
}: {
- label: string
- value: string
- mono?: boolean
- wrap?: boolean
+ label: string;
+ value: string;
+ mono?: boolean;
+ wrap?: boolean;
}) {
return (
@@ -319,7 +319,7 @@ function DetailRow({
{value}
- )
+ );
}
function EmptyState({ icon, text }: { icon: React.ReactNode; text: string }) {
@@ -328,5 +328,5 @@ function EmptyState({ icon, text }: { icon: React.ReactNode; text: string }) {
{icon}
{text}
- )
+ );
}
diff --git a/src/renderer/components/utils/UtilitiesTab.tsx b/src/renderer/components/utils/UtilitiesTab.tsx
index 1dd08f5..d736c7b 100644
--- a/src/renderer/components/utils/UtilitiesTab.tsx
+++ b/src/renderer/components/utils/UtilitiesTab.tsx
@@ -1,19 +1,19 @@
-import React, { useState, useCallback } from 'react'
-import { Dialog } from '../common/Dialog'
-import { Button } from '../common/Button'
-import { VscCheck, VscListUnordered } from 'react-icons/vsc'
-import { LuScanLine } from 'react-icons/lu'
-import { JavaProcessInfo, ProcessLogEntry } from '../../../main/shared/types/Process.types'
+import React, { useState, useCallback } from 'react';
+import { Dialog } from '../common/Dialog';
+import { Button } from '../common/Button';
+import { VscCheck, VscListUnordered } from 'react-icons/vsc';
+import { LuScanLine } from 'react-icons/lu';
+import { JavaProcessInfo, ProcessLogEntry } from '../../../main/shared/types/Process.types';
-type Panel = 'log' | 'scanner'
+type Panel = 'log' | 'scanner';
const PANELS = [
{ id: 'log', label: 'Activity Log', Icon: VscListUnordered },
{ id: 'scanner', label: 'Process Scanner', Icon: LuScanLine },
-]
+];
export function UtilitiesTab() {
- const [panel, setPanel] = useState('log')
+ const [panel, setPanel] = useState('log');
return (
@@ -38,25 +38,25 @@ export function UtilitiesTab() {
{panel === 'scanner' && }
- )
+ );
}
// ── Activity Log ──────────────────────────────────────────────────────────────
function ActivityLogPanel() {
- const [entries, setEntries] = useState(null)
- const [loading, setLoading] = useState(false)
- const [confirmClear, setConfirmClear] = useState(false)
+ const [entries, setEntries] = useState(null);
+ const [loading, setLoading] = useState(false);
+ const [confirmClear, setConfirmClear] = useState(false);
const load = useCallback(async () => {
- setLoading(true)
- setEntries(await window.api.getProcessLog())
- setLoading(false)
- }, [])
+ setLoading(true);
+ setEntries(await window.api.getProcessLog());
+ setLoading(false);
+ }, []);
React.useEffect(() => {
- load()
- }, [load])
+ load();
+ }, [load]);
return (
<>
@@ -103,19 +103,19 @@ function ActivityLogPanel() {
confirmLabel="Clear"
danger
onConfirm={async () => {
- await window.api.clearProcessLog()
- setEntries([])
- setConfirmClear(false)
+ await window.api.clearProcessLog();
+ setEntries([]);
+ setConfirmClear(false);
}}
onCancel={() => setConfirmClear(false)}
/>
>
- )
+ );
}
function LogEntryRow({ entry }: { entry: ProcessLogEntry }) {
- const duration = entry.stoppedAt ? formatDuration(entry.stoppedAt - entry.startedAt) : null
- const jarName = entry.jarPath.split(/[/\\]/).pop() ?? entry.jarPath
+ const duration = entry.stoppedAt ? formatDuration(entry.stoppedAt - entry.startedAt) : null;
+ const jarName = entry.jarPath.split(/[/\\]/).pop() ?? entry.jarPath;
return (
@@ -154,63 +154,63 @@ function LogEntryRow({ entry }: { entry: ProcessLogEntry }) {
- )
+ );
}
// ── Process Scanner ───────────────────────────────────────────────────────────
interface KillIntent {
- proc: JavaProcessInfo
- nonJava: boolean
+ proc: JavaProcessInfo;
+ nonJava: boolean;
}
function ScannerPanel() {
- const [results, setResults] = useState(null)
- const [scanning, setScanning] = useState(false)
- const [killIntent, setKillIntent] = useState(null)
- const [killAllConfirm, setKillAllConfirm] = useState(false)
- const [statusMsg, setStatusMsg] = useState<{ text: string; ok: boolean } | null>(null)
- const [killedPids, setKilledPids] = useState>(new Set())
- const [filter, setFilter] = useState<'all' | 'java'>('java')
- const [search, setSearch] = useState('')
- const [expandedPid, setExpandedPid] = useState(null)
+ const [results, setResults] = useState(null);
+ const [scanning, setScanning] = useState(false);
+ const [killIntent, setKillIntent] = useState(null);
+ const [killAllConfirm, setKillAllConfirm] = useState(false);
+ const [statusMsg, setStatusMsg] = useState<{ text: string; ok: boolean } | null>(null);
+ const [killedPids, setKilledPids] = useState>(new Set());
+ const [filter, setFilter] = useState<'all' | 'java'>('java');
+ const [search, setSearch] = useState('');
+ const [expandedPid, setExpandedPid] = useState(null);
const scan = useCallback(async () => {
- setScanning(true)
- setStatusMsg(null)
- setKilledPids(new Set())
- setSearch('')
- setExpandedPid(null)
- const found = await window.api.scanAllProcesses()
- setResults(found)
- setScanning(false)
- const javaCount = found.filter((p) => p.isJava).length
- setStatusMsg({ text: `Found ${found.length} processes - ${javaCount} java`, ok: true })
- }, [])
+ setScanning(true);
+ setStatusMsg(null);
+ setKilledPids(new Set());
+ setSearch('');
+ setExpandedPid(null);
+ const found = await window.api.scanAllProcesses();
+ setResults(found);
+ setScanning(false);
+ const javaCount = found.filter((p) => p.isJava).length;
+ setStatusMsg({ text: `Found ${found.length} processes - ${javaCount} java`, ok: true });
+ }, []);
const handleKill = async () => {
- if (!killIntent) return
- const res = await window.api.killPid(killIntent.proc.pid)
+ if (!killIntent) return;
+ const res = await window.api.killPid(killIntent.proc.pid);
if (res.ok) {
- setKilledPids((prev) => new Set([...prev, killIntent.proc.pid]))
- setStatusMsg({ text: `Killed PID ${killIntent.proc.pid}`, ok: true })
+ setKilledPids((prev) => new Set([...prev, killIntent.proc.pid]));
+ setStatusMsg({ text: `Killed PID ${killIntent.proc.pid}`, ok: true });
} else {
- setStatusMsg({ text: `Failed to kill PID ${killIntent.proc.pid}: ${res.error}`, ok: false })
+ setStatusMsg({ text: `Failed to kill PID ${killIntent.proc.pid}: ${res.error}`, ok: false });
}
- setKillIntent(null)
- }
+ setKillIntent(null);
+ };
const handleKillAll = async () => {
- const res = await window.api.killAllJava()
+ const res = await window.api.killAllJava();
setStatusMsg({
text: `Killed ${res.killed} java process${res.killed === 1 ? '' : 'es'} (protected processes skipped)`,
ok: true,
- })
- setKillAllConfirm(false)
- setTimeout(scan, 800)
- }
+ });
+ setKillAllConfirm(false);
+ setTimeout(scan, 800);
+ };
- const searchLower = search.trim().toLowerCase()
+ const searchLower = search.trim().toLowerCase();
const visible = results
? (filter === 'java' ? results.filter((r) => r.isJava) : results)
.filter((r) => !killedPids.has(r.pid))
@@ -220,10 +220,10 @@ function ScannerPanel() {
r.command.toLowerCase().includes(searchLower) ||
String(r.pid).includes(searchLower)
)
- : null
+ : null;
// Only killable (non-protected) java processes shown in the "Kill All" button
- const killableJavaVisible = visible?.some((r) => r.isJava && !r.protected) ?? false
+ const killableJavaVisible = visible?.some((r) => r.isJava && !r.protected) ?? false;
return (
@@ -349,7 +349,7 @@ function ScannerPanel() {
onCancel={() => setKillAllConfirm(false)}
/>
- )
+ );
}
function ProcessRow({
@@ -358,12 +358,12 @@ function ProcessRow({
onToggle,
onKill,
}: {
- proc: JavaProcessInfo
- expanded: boolean
- onToggle: () => void
- onKill: () => void
+ proc: JavaProcessInfo;
+ expanded: boolean;
+ onToggle: () => void;
+ onKill: () => void;
}) {
- const isProtected = proc.protected
+ const isProtected = proc.protected;
return (
)}
- )
+ );
}
function DetailRow({
@@ -468,10 +468,10 @@ function DetailRow({
mono,
wrap,
}: {
- label: string
- value: string
- mono?: boolean
- wrap?: boolean
+ label: string;
+ value: string;
+ mono?: boolean;
+ wrap?: boolean;
}) {
return (
@@ -486,7 +486,7 @@ function DetailRow({
{value}
- )
+ );
}
function EmptyState({ icon, text }: { icon: React.ReactNode; text: string }) {
@@ -495,7 +495,7 @@ function EmptyState({ icon, text }: { icon: React.ReactNode; text: string }) {
{icon}
{text}
- )
+ );
}
function formatTime(ts: number): string {
@@ -503,12 +503,12 @@ function formatTime(ts: number): string {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
- })
+ });
}
function formatDuration(ms: number): string {
- const s = Math.floor(ms / 1000)
- if (s < 60) return `${s}s`
- if (s < 3600) return `${Math.floor(s / 60)}m ${s % 60}s`
- return `${Math.floor(s / 3600)}h ${Math.floor((s % 3600) / 60)}m`
+ const s = Math.floor(ms / 1000);
+ if (s < 60) return `${s}s`;
+ if (s < 3600) return `${Math.floor(s / 60)}m ${s % 60}s`;
+ return `${Math.floor(s / 3600)}h ${Math.floor((s % 3600) / 60)}m`;
}
diff --git a/src/renderer/global.d.ts b/src/renderer/global.d.ts
index c71f68a..3d6f874 100644
--- a/src/renderer/global.d.ts
+++ b/src/renderer/global.d.ts
@@ -1,9 +1,9 @@
// API type is fully inferred from the route definitions
-import type { API, Environment } from '../main/ipc/_index'
+import type { API, Environment } from '../main/ipc/_index';
declare global {
interface Window {
- api: API
- env: Environment
+ api: API;
+ env: Environment;
}
}
diff --git a/src/renderer/hooks/useDevMode.ts b/src/renderer/hooks/useDevMode.ts
index 81ad4e4..62998c1 100644
--- a/src/renderer/hooks/useDevMode.ts
+++ b/src/renderer/hooks/useDevMode.ts
@@ -1,27 +1,27 @@
-import { useState, useEffect } from 'react'
-import { JRCEnvironment } from 'src/main/shared/types/App.types'
+import { useState, useEffect } from 'react';
+import { JRCEnvironment } from 'src/main/shared/types/App.types';
export function useDevMode(): boolean {
- const [enabled, setEnabled] = useState(false)
+ const [enabled, setEnabled] = useState(false);
useEffect(() => {
- let mounted = true
+ let mounted = true;
// fetch initial value
window.env.get().then((value) => {
- if (mounted) setEnabled(value.devMode)
- })
+ if (mounted) setEnabled(value.devMode);
+ });
// subscribe to changes
const handler = (value: JRCEnvironment) => {
- if (mounted) setEnabled(value.devMode)
- }
- window.env.onChange(handler)
+ if (mounted) setEnabled(value.devMode);
+ };
+ window.env.onChange(handler);
return () => {
- mounted = false
- }
- }, [])
+ mounted = false;
+ };
+ }, []);
- return enabled
+ return enabled;
}
diff --git a/src/renderer/main.tsx b/src/renderer/main.tsx
index be88595..dfa8b77 100644
--- a/src/renderer/main.tsx
+++ b/src/renderer/main.tsx
@@ -1,9 +1,9 @@
-import React from 'react'
-import ReactDOM from 'react-dom/client'
-import App from './App'
-import './styles/globals.css'
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import App from './App';
+import './styles/globals.css';
-import { name, version } from '../../package.json'
+import { name, version } from '../../package.json';
async function printVersionInfo() {
console.log(
@@ -12,7 +12,7 @@ async function printVersionInfo() {
'color: magenta;', // name
'color: lightgreen;', // version
'color: gray;' // reset
- )
+ );
}
printVersionInfo().then(() => {
@@ -20,5 +20,5 @@ printVersionInfo().then(() => {
- )
-})
+ );
+});
diff --git a/src/renderer/store/AppStore.tsx b/src/renderer/store/AppStore.tsx
index cbc1ebe..ff12f55 100644
--- a/src/renderer/store/AppStore.tsx
+++ b/src/renderer/store/AppStore.tsx
@@ -5,43 +5,43 @@ import React, {
useEffect,
useCallback,
type ReactNode,
-} from 'react'
-import { AppSettings } from '../../main/shared/types/App.types'
-import { ConsoleLine, ProcessState } from '../../main/shared/types/Process.types'
-import { Profile } from '../../main/shared/types/Profile.types'
-import { v4 as uuidv4 } from 'uuid'
+} from 'react';
+import { AppSettings } from '../../main/shared/types/App.types';
+import { ConsoleLine, ProcessState } from '../../main/shared/types/Process.types';
+import { Profile } from '../../main/shared/types/Profile.types';
+import { v4 as uuidv4 } from 'uuid';
-const SS_KEY = (id: string) => `jrc:console:${id}`
+const SS_KEY = (id: string) => `jrc:console:${id}`;
function loadLogs(id: string, max: number): ConsoleLine[] {
try {
- const r = sessionStorage.getItem(SS_KEY(id))
- return r ? (JSON.parse(r) as ConsoleLine[]).slice(-max) : []
+ const r = sessionStorage.getItem(SS_KEY(id));
+ return r ? (JSON.parse(r) as ConsoleLine[]).slice(-max) : [];
} catch {
- return []
+ return [];
}
}
function saveLogs(id: string, lines: ConsoleLine[]): void {
try {
- sessionStorage.setItem(SS_KEY(id), JSON.stringify(lines))
+ sessionStorage.setItem(SS_KEY(id), JSON.stringify(lines));
} catch {
/* quota */
}
}
function clearLogs(id: string): void {
try {
- sessionStorage.removeItem(SS_KEY(id))
+ sessionStorage.removeItem(SS_KEY(id));
} catch {
/* ignore */
}
}
interface AppState {
- profiles: Profile[]
- activeProfileId: string
- processStates: ProcessState[]
- settings: AppSettings | null
- consoleLogs: Record
- loading: boolean
+ profiles: Profile[];
+ activeProfileId: string;
+ processStates: ProcessState[];
+ settings: AppSettings | null;
+ consoleLogs: Record;
+ loading: boolean;
}
const INITIAL_STATE: AppState = {
@@ -51,7 +51,7 @@ const INITIAL_STATE: AppState = {
settings: null,
consoleLogs: {},
loading: true,
-}
+};
type Action =
| { type: 'INIT'; profiles: Profile[]; settings: AppSettings; states: ProcessState[] }
@@ -61,7 +61,7 @@ type Action =
| { type: 'SET_SETTINGS'; settings: AppSettings }
| { type: 'LOAD_LOG'; profileId: string; lines: ConsoleLine[] }
| { type: 'APPEND_LOG'; profileId: string; line: ConsoleLine; maxLines: number }
- | { type: 'CLEAR_LOG'; profileId: string }
+ | { type: 'CLEAR_LOG'; profileId: string };
function reducer(state: AppState, action: Action): AppState {
switch (action.type) {
@@ -73,109 +73,109 @@ function reducer(state: AppState, action: Action): AppState {
processStates: action.states,
settings: action.settings,
loading: false,
- }
+ };
case 'SET_PROFILES':
- return { ...state, profiles: action.profiles }
+ return { ...state, profiles: action.profiles };
case 'SET_ACTIVE':
- return { ...state, activeProfileId: action.id }
+ return { ...state, activeProfileId: action.id };
case 'SET_STATES':
- return { ...state, processStates: action.states }
+ return { ...state, processStates: action.states };
case 'SET_SETTINGS':
- return { ...state, settings: action.settings }
+ return { ...state, settings: action.settings };
case 'LOAD_LOG':
- return { ...state, consoleLogs: { ...state.consoleLogs, [action.profileId]: action.lines } }
+ return { ...state, consoleLogs: { ...state.consoleLogs, [action.profileId]: action.lines } };
case 'APPEND_LOG': {
- const prev = state.consoleLogs[action.profileId] ?? []
- const next = [...prev, action.line]
+ const prev = state.consoleLogs[action.profileId] ?? [];
+ const next = [...prev, action.line];
const trimmed =
- next.length > action.maxLines ? next.slice(next.length - action.maxLines) : next
- saveLogs(action.profileId, trimmed)
- return { ...state, consoleLogs: { ...state.consoleLogs, [action.profileId]: trimmed } }
+ next.length > action.maxLines ? next.slice(next.length - action.maxLines) : next;
+ saveLogs(action.profileId, trimmed);
+ return { ...state, consoleLogs: { ...state.consoleLogs, [action.profileId]: trimmed } };
}
case 'CLEAR_LOG':
- clearLogs(action.profileId)
- return { ...state, consoleLogs: { ...state.consoleLogs, [action.profileId]: [] } }
+ clearLogs(action.profileId);
+ return { ...state, consoleLogs: { ...state.consoleLogs, [action.profileId]: [] } };
default:
- return state
+ return state;
}
}
interface AppContextValue {
- state: AppState
- activeProfile: Profile | undefined
- setActiveProfile: (id: string) => void
- saveProfile: (p: Profile) => Promise
- deleteProfile: (id: string) => Promise
- createProfile: (overrides?: Partial) => void
- reorderProfiles: (profiles: Profile[]) => Promise
- startProcess: (p: Profile) => Promise<{ ok: boolean; error?: string }>
- stopProcess: (id: string) => Promise<{ ok: boolean; error?: string }>
- sendInput: (profileId: string, input: string) => Promise
- clearConsole: (profileId: string) => void
- saveSettings: (s: AppSettings) => Promise
- isRunning: (profileId: string) => boolean
+ state: AppState;
+ activeProfile: Profile | undefined;
+ setActiveProfile: (id: string) => void;
+ saveProfile: (p: Profile) => Promise;
+ deleteProfile: (id: string) => Promise;
+ createProfile: (overrides?: Partial) => void;
+ reorderProfiles: (profiles: Profile[]) => Promise;
+ startProcess: (p: Profile) => Promise<{ ok: boolean; error?: string }>;
+ stopProcess: (id: string) => Promise<{ ok: boolean; error?: string }>;
+ sendInput: (profileId: string, input: string) => Promise;
+ clearConsole: (profileId: string) => void;
+ saveSettings: (s: AppSettings) => Promise;
+ isRunning: (profileId: string) => boolean;
}
-const AppContext = createContext(null)
+const AppContext = createContext(null);
export function AppProvider({ children }: { children: ReactNode }) {
- const [state, dispatch] = useReducer(reducer, INITIAL_STATE)
+ const [state, dispatch] = useReducer(reducer, INITIAL_STATE);
useEffect(() => {
- if (!window.api) return
+ if (!window.api) return;
async function init() {
const [profiles, settings, states] = await Promise.all([
window.api.getProfiles(),
window.api.getSettings(),
window.api.getStates(),
- ])
- dispatch({ type: 'INIT', profiles, settings, states })
- const max = settings?.consoleMaxLines ?? 5000
+ ]);
+ dispatch({ type: 'INIT', profiles, settings, states });
+ const max = settings?.consoleMaxLines ?? 5000;
for (const p of profiles) {
- const lines = loadLogs(p.id, max)
- if (lines.length > 0) dispatch({ type: 'LOAD_LOG', profileId: p.id, lines })
+ const lines = loadLogs(p.id, max);
+ if (lines.length > 0) dispatch({ type: 'LOAD_LOG', profileId: p.id, lines });
}
}
- init()
- }, [])
+ init();
+ }, []);
useEffect(() => {
- if (!window.api) return
- const max = state.settings?.consoleMaxLines ?? 5000
+ if (!window.api) return;
+ const max = state.settings?.consoleMaxLines ?? 5000;
return window.api.onConsoleLine((profileId: string, line: unknown) => {
- dispatch({ type: 'APPEND_LOG', profileId, line: line as ConsoleLine, maxLines: max })
- })
- }, [state.settings?.consoleMaxLines])
+ dispatch({ type: 'APPEND_LOG', profileId, line: line as ConsoleLine, maxLines: max });
+ });
+ }, [state.settings?.consoleMaxLines]);
useEffect(() => {
- if (!window.api) return
+ if (!window.api) return;
return window.api.onConsoleClear((profileId) => {
- dispatch({ type: 'CLEAR_LOG', profileId })
- })
- }, [])
+ dispatch({ type: 'CLEAR_LOG', profileId });
+ });
+ }, []);
useEffect(() => {
- if (!window.api) return
- return window.api.onStatesUpdate((states) => dispatch({ type: 'SET_STATES', states }))
- }, [])
+ if (!window.api) return;
+ return window.api.onStatesUpdate((states) => dispatch({ type: 'SET_STATES', states }));
+ }, []);
- const setActiveProfile = useCallback((id: string) => dispatch({ type: 'SET_ACTIVE', id }), [])
+ const setActiveProfile = useCallback((id: string) => dispatch({ type: 'SET_ACTIVE', id }), []);
const saveProfile = useCallback(async (p: Profile) => {
- await window.api.saveProfile(p)
- dispatch({ type: 'SET_PROFILES', profiles: await window.api.getProfiles() })
- }, [])
+ await window.api.saveProfile(p);
+ dispatch({ type: 'SET_PROFILES', profiles: await window.api.getProfiles() });
+ }, []);
const deleteProfile = useCallback(
async (id: string) => {
- clearLogs(id)
- await window.api.deleteProfile(id)
- const profiles = await window.api.getProfiles()
- dispatch({ type: 'SET_PROFILES', profiles })
- if (state.activeProfileId === id) dispatch({ type: 'SET_ACTIVE', id: profiles[0]?.id ?? '' })
+ clearLogs(id);
+ await window.api.deleteProfile(id);
+ const profiles = await window.api.getProfiles();
+ dispatch({ type: 'SET_PROFILES', profiles });
+ if (state.activeProfileId === id) dispatch({ type: 'SET_ACTIVE', id: profiles[0]?.id ?? '' });
},
[state.activeProfileId]
- )
+ );
const createProfile = useCallback(
async (overrides: Partial = {}) => {
@@ -195,37 +195,37 @@ export function AppProvider({ children }: { children: ReactNode }) {
order: state.profiles.length,
createdAt: Date.now(),
updatedAt: Date.now(),
- }
- dispatch({ type: 'SET_PROFILES', profiles: [...state.profiles, p] })
- dispatch({ type: 'SET_ACTIVE', id: p.id })
- window.api.saveProfile(p)
+ };
+ dispatch({ type: 'SET_PROFILES', profiles: [...state.profiles, p] });
+ dispatch({ type: 'SET_ACTIVE', id: p.id });
+ window.api.saveProfile(p);
},
[state.profiles]
- )
+ );
const reorderProfiles = useCallback(async (profiles: Profile[]) => {
- dispatch({ type: 'SET_PROFILES', profiles })
- await window.api.reorderProfiles(profiles.map((p) => p.id))
- }, [])
+ dispatch({ type: 'SET_PROFILES', profiles });
+ await window.api.reorderProfiles(profiles.map((p) => p.id));
+ }, []);
- const startProcess = useCallback((p: Profile) => window.api.startProcess(p), [])
- const stopProcess = useCallback((id: string) => window.api.stopProcess(id), [])
+ const startProcess = useCallback((p: Profile) => window.api.startProcess(p), []);
+ const stopProcess = useCallback((id: string) => window.api.stopProcess(id), []);
const sendInput = useCallback(async (profileId: string, input: string) => {
- await window.api.sendInput(profileId, input)
- }, [])
+ await window.api.sendInput(profileId, input);
+ }, []);
const clearConsole = useCallback(
(profileId: string) => dispatch({ type: 'CLEAR_LOG', profileId }),
[]
- )
+ );
const saveSettings = useCallback(async (s: AppSettings) => {
- await window.api.saveSettings(s)
- dispatch({ type: 'SET_SETTINGS', settings: s })
- }, [])
+ await window.api.saveSettings(s);
+ dispatch({ type: 'SET_SETTINGS', settings: s });
+ }, []);
const isRunning = useCallback(
(profileId: string) => state.processStates.some((s) => s.profileId === profileId && s.running),
[state.processStates]
- )
- const activeProfile = state.profiles.find((p) => p.id === state.activeProfileId)
+ );
+ const activeProfile = state.profiles.find((p) => p.id === state.activeProfileId);
return (
{children}
- )
+ );
}
export function useApp(): AppContextValue {
- const ctx = useContext(AppContext)
- if (!ctx) throw new Error('useApp must be used within AppProvider')
- return ctx
+ const ctx = useContext(AppContext);
+ if (!ctx) throw new Error('useApp must be used within AppProvider');
+ return ctx;
}
export const PROFILE_COLORS = [
@@ -265,4 +265,4 @@ export const PROFILE_COLORS = [
'#34d399',
'#fbbf24',
'#f87171',
-]
+];
diff --git a/src/renderer/store/appReducer.ts b/src/renderer/store/appReducer.ts
index fe08527..ff0195e 100644
--- a/src/renderer/store/appReducer.ts
+++ b/src/renderer/store/appReducer.ts
@@ -1,15 +1,15 @@
-import { Profile } from '../../main/shared/types/Profile.types'
-import { saveLogs, clearLogs } from './sessionLogs'
-import { ConsoleLine, ProcessState } from '../../main/shared/types/Process.types'
-import { AppSettings } from '../../main/shared/types/App.types'
+import { Profile } from '../../main/shared/types/Profile.types';
+import { saveLogs, clearLogs } from './sessionLogs';
+import { ConsoleLine, ProcessState } from '../../main/shared/types/Process.types';
+import { AppSettings } from '../../main/shared/types/App.types';
export interface AppState {
- profiles: Profile[]
- activeProfileId: string
- processStates: ProcessState[]
- settings: AppSettings | null
- consoleLogs: Record
- loading: boolean
+ profiles: Profile[];
+ activeProfileId: string;
+ processStates: ProcessState[];
+ settings: AppSettings | null;
+ consoleLogs: Record;
+ loading: boolean;
}
export const INITIAL_STATE: AppState = {
@@ -19,7 +19,7 @@ export const INITIAL_STATE: AppState = {
settings: null,
consoleLogs: {},
loading: true,
-}
+};
export type Action =
| { type: 'INIT'; profiles: Profile[]; settings: AppSettings; states: ProcessState[] }
@@ -29,7 +29,7 @@ export type Action =
| { type: 'SET_SETTINGS'; settings: AppSettings }
| { type: 'LOAD_LOG'; profileId: string; lines: ConsoleLine[] }
| { type: 'APPEND_LOG'; profileId: string; line: ConsoleLine; maxLines: number }
- | { type: 'CLEAR_LOG'; profileId: string }
+ | { type: 'CLEAR_LOG'; profileId: string };
export function reducer(state: AppState, action: Action): AppState {
switch (action.type) {
@@ -41,37 +41,37 @@ export function reducer(state: AppState, action: Action): AppState {
processStates: action.states,
settings: action.settings,
loading: false,
- }
+ };
case 'SET_PROFILES':
- return { ...state, profiles: action.profiles }
+ return { ...state, profiles: action.profiles };
case 'SET_ACTIVE':
- return { ...state, activeProfileId: action.id }
+ return { ...state, activeProfileId: action.id };
case 'SET_STATES':
- return { ...state, processStates: action.states }
+ return { ...state, processStates: action.states };
case 'SET_SETTINGS':
- return { ...state, settings: action.settings }
+ return { ...state, settings: action.settings };
case 'LOAD_LOG':
- return { ...state, consoleLogs: { ...state.consoleLogs, [action.profileId]: action.lines } }
+ return { ...state, consoleLogs: { ...state.consoleLogs, [action.profileId]: action.lines } };
case 'APPEND_LOG': {
- const prev = state.consoleLogs[action.profileId] ?? []
- const next = [...prev, action.line]
+ const prev = state.consoleLogs[action.profileId] ?? [];
+ const next = [...prev, action.line];
const trimmed =
- next.length > action.maxLines ? next.slice(next.length - action.maxLines) : next
- saveLogs(action.profileId, trimmed)
- return { ...state, consoleLogs: { ...state.consoleLogs, [action.profileId]: trimmed } }
+ next.length > action.maxLines ? next.slice(next.length - action.maxLines) : next;
+ saveLogs(action.profileId, trimmed);
+ return { ...state, consoleLogs: { ...state.consoleLogs, [action.profileId]: trimmed } };
}
case 'CLEAR_LOG':
- clearLogs(action.profileId)
- return { ...state, consoleLogs: { ...state.consoleLogs, [action.profileId]: [] } }
+ clearLogs(action.profileId);
+ return { ...state, consoleLogs: { ...state.consoleLogs, [action.profileId]: [] } };
default:
- return state
+ return state;
}
}
diff --git a/src/renderer/store/sessionLogs.ts b/src/renderer/store/sessionLogs.ts
index 3d0a71b..5398d59 100644
--- a/src/renderer/store/sessionLogs.ts
+++ b/src/renderer/store/sessionLogs.ts
@@ -1,19 +1,19 @@
-import { ConsoleLine } from '../../main/shared/types/Process.types'
+import { ConsoleLine } from '../../main/shared/types/Process.types';
-const key = (id: string) => `jrc:console:${id}`
+const key = (id: string) => `jrc:console:${id}`;
export function loadLogs(id: string, max: number): ConsoleLine[] {
try {
- const raw = sessionStorage.getItem(key(id))
- return raw ? (JSON.parse(raw) as ConsoleLine[]).slice(-max) : []
+ const raw = sessionStorage.getItem(key(id));
+ return raw ? (JSON.parse(raw) as ConsoleLine[]).slice(-max) : [];
} catch {
- return []
+ return [];
}
}
export function saveLogs(id: string, lines: ConsoleLine[]): void {
try {
- sessionStorage.setItem(key(id), JSON.stringify(lines))
+ sessionStorage.setItem(key(id), JSON.stringify(lines));
} catch {
/* quota */
}
@@ -21,7 +21,7 @@ export function saveLogs(id: string, lines: ConsoleLine[]): void {
export function clearLogs(id: string): void {
try {
- sessionStorage.removeItem(key(id))
+ sessionStorage.removeItem(key(id));
} catch {
/* ignore */
}
diff --git a/tailwind.config.js b/tailwind.config.js
index 0350ffb..9f77c71 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -21,4 +21,4 @@ module.exports = {
},
},
plugins: [],
-}
+};
diff --git a/vite.config.ts b/vite.config.ts
index 624a6df..36ef9f6 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,8 +1,8 @@
-import { defineConfig } from 'vite'
-import react from '@vitejs/plugin-react'
-import path from 'path'
-import { readFileSync } from 'fs'
-const { version } = JSON.parse(readFileSync(path.resolve(__dirname, 'package.json'), 'utf-8'))
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import path from 'path';
+import { readFileSync } from 'fs';
+const { version } = JSON.parse(readFileSync(path.resolve(__dirname, 'package.json'), 'utf-8'));
export default defineConfig({
plugins: [react()],
base: './',
@@ -27,4 +27,4 @@ export default defineConfig({
},
resolve: { alias: { '@': path.resolve(__dirname, 'src/renderer') } },
server: { port: 5173 },
-})
+});
From 55fa0149302152f56a38a6685442da015bc8d605 Mon Sep 17 00:00:00 2001
From: Timon Home
Date: Sun, 22 Mar 2026 18:45:59 +0100
Subject: [PATCH 4/4] Fix statup/autostart logic
---
package.json | 2 +-
src/main/JRCEnvironment.ts | 6 +-
src/main/RestAPI.routes.ts | 35 +--
src/main/RestAPI.ts | 4 +-
src/main/ipc/Dev.ipc.ts | 1 +
src/main/main.ts | 71 ++---
src/main/shared/config/API.config.ts | 7 +-
src/main/shared/config/App.config.ts | 2 +-
src/main/shared/config/RestApi.config.ts | 23 --
src/main/store.ts | 17 ++
src/renderer/App.tsx | 2 +-
src/renderer/components/MainLayout.tsx | 8 +-
.../components/developer/DevApiExplorer.tsx | 266 ++++++++++++++++--
.../components/developer/DevDashboard.tsx | 175 ++++++------
.../components/developer/DeveloperTab.tsx | 2 +-
.../components/settings/SettingsTab.tsx | 2 +-
16 files changed, 429 insertions(+), 194 deletions(-)
delete mode 100644 src/main/shared/config/RestApi.config.ts
diff --git a/package.json b/package.json
index dfafd76..406ee74 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "java-runner-client",
- "version": "2.1.4",
+ "version": "2.1.3",
"description": "Run and manage Java processes with profiles, console I/O, and system tray support",
"main": "dist/main/main.js",
"scripts": {
diff --git a/src/main/JRCEnvironment.ts b/src/main/JRCEnvironment.ts
index 1d19b2e..b544fcc 100644
--- a/src/main/JRCEnvironment.ts
+++ b/src/main/JRCEnvironment.ts
@@ -1,7 +1,7 @@
import { app, BrowserWindow } from 'electron';
+import { EnvironmentIPC } from './ipc/Environment.ipc';
import { JRCEnvironment } from './shared/types/App.types';
import { getSettings } from './Store';
-import { EnvironmentIPC } from './ipc/Environment.ipc';
let env: JRCEnvironment = {
isReady: false,
@@ -25,6 +25,10 @@ export function getEnvironment() {
return env;
}
+export function shouldStartMinimized(): boolean {
+ return process.argv.includes('--minimized');
+}
+
function broadcast(channel: string = EnvironmentIPC.change.channel) {
BrowserWindow.getAllWindows().forEach((w) => w.webContents.send(channel, env));
}
diff --git a/src/main/RestAPI.routes.ts b/src/main/RestAPI.routes.ts
index 87c772f..cb82f2c 100644
--- a/src/main/RestAPI.routes.ts
+++ b/src/main/RestAPI.routes.ts
@@ -35,8 +35,8 @@ function defineRoute(key: K, handler: RouteHandler): BuiltRo
// ─── Routes ───────────────────────────────────────────────────────────────────
-export const routes = [
- defineRoute('status', ({ res }) =>
+export const routes: { [K in RouteKey]: BuiltRoute } = {
+ status: defineRoute('status', ({ res }) =>
ok(res, {
ok: true,
version: process.env.npm_package_version ?? 'unknown',
@@ -45,15 +45,14 @@ export const routes = [
})
),
- defineRoute('profiles_list', ({ res }) => ok(res, getAllProfiles())),
+ profiles_list: defineRoute('profiles_list', ({ res }) => ok(res, getAllProfiles())),
- defineRoute('profiles_get', ({ res, params }) => {
+ profiles_get: defineRoute('profiles_get', ({ res, params }) => {
const p = getAllProfiles().find((p) => p.id === params.id);
- console.log(p, params, getAllProfiles());
p ? ok(res, p) : err(res, 'Profile not found', 404);
}),
- defineRoute('profiles_create', ({ res, body }) => {
+ profiles_create: defineRoute('profiles_create', ({ res, body }) => {
const b = body as Partial;
const p: Profile = {
@@ -77,7 +76,7 @@ export const routes = [
ok(res, p, 201);
}),
- defineRoute('profiles_update', ({ res, params, body }) => {
+ profiles_update: defineRoute('profiles_update', ({ res, params, body }) => {
const existing = getAllProfiles().find((p) => p.id === params.id);
if (!existing) return err(res, 'Profile not found', 404);
@@ -93,7 +92,7 @@ export const routes = [
ok(res, updated);
}),
- defineRoute('profiles_delete', ({ res, params }) => {
+ profiles_delete: defineRoute('profiles_delete', ({ res, params }) => {
if (!getAllProfiles().find((p) => p.id === params.id))
return err(res, 'Profile not found', 404);
@@ -101,26 +100,30 @@ export const routes = [
ok(res);
}),
- defineRoute('processes_list', ({ res }) => ok(res, processManager.getStates())),
+ processes_list: defineRoute('processes_list', ({ res }) => ok(res, processManager.getStates())),
- defineRoute('processes_log', ({ res }) => ok(res, processManager.getActivityLog())),
+ processes_log: defineRoute('processes_log', ({ res }) =>
+ ok(res, processManager.getActivityLog())
+ ),
- defineRoute('processes_start', ({ res, params }) => {
+ processes_start: defineRoute('processes_start', ({ res, params }) => {
const p = getAllProfiles().find((p) => p.id === params.id);
if (!p) return err(res, 'Profile not found', 404);
ok(res, processManager.start(p));
}),
- defineRoute('processes_stop', ({ res, params }) => ok(res, processManager.stop(params.id))),
+ processes_stop: defineRoute('processes_stop', ({ res, params }) =>
+ ok(res, processManager.stop(params.id))
+ ),
- defineRoute('processes_clear', ({ res, params }) => {
+ processes_clear: defineRoute('processes_clear', ({ res, params }) => {
processManager.clearConsoleForProfile(params.id);
ok(res);
}),
- defineRoute('settings_get', ({ res }) => ok(res, getSettings())),
+ settings_get: defineRoute('settings_get', ({ res }) => ok(res, getSettings())),
- defineRoute('settings_update', ({ res, body }) => {
+ settings_update: defineRoute('settings_update', ({ res, body }) => {
const updated: AppSettings = {
...getSettings(),
...(body as Partial),
@@ -129,4 +132,4 @@ export const routes = [
saveSettings(updated);
ok(res, updated);
}),
-];
+};
diff --git a/src/main/RestAPI.ts b/src/main/RestAPI.ts
index 49b73d0..dde996d 100644
--- a/src/main/RestAPI.ts
+++ b/src/main/RestAPI.ts
@@ -2,7 +2,7 @@ import http from 'http';
import { routes } from './RestAPI.routes';
import { routeConfig } from './shared/config/API.config';
import { getSettings } from './Store';
-import { REST_API_CONFIG } from './shared/config/RestApi.config';
+import { REST_API_CONFIG } from './shared/config/API.config';
type Params = Record;
@@ -63,7 +63,7 @@ function parsePattern(path: string) {
}
function compileRoutes(): CompiledRoute[] {
- return routes.map((r) => ({
+ return Object.entries(routes).map(([_, r]) => ({
...r,
...parsePattern(r.path),
}));
diff --git a/src/main/ipc/Dev.ipc.ts b/src/main/ipc/Dev.ipc.ts
index d7d56db..840ff7b 100644
--- a/src/main/ipc/Dev.ipc.ts
+++ b/src/main/ipc/Dev.ipc.ts
@@ -18,6 +18,7 @@ export const DevIPC = {
arch: process.arch,
nodeVersion: process.versions.node,
electronVersion: process.versions.electron,
+ argv: process.argv,
chromeVersion: process.versions.chrome,
uptime: Math.floor(process.uptime()),
memoryUsageMB: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
diff --git a/src/main/main.ts b/src/main/main.ts
index 8837451..2fbc4cc 100644
--- a/src/main/main.ts
+++ b/src/main/main.ts
@@ -1,13 +1,13 @@
-import { app, BrowserWindow, Menu, nativeImage, Tray } from 'electron';
+import { app, BrowserWindow, Input, Menu, nativeImage, Tray } from 'electron';
import fs from 'fs';
import path from 'path';
import { allRoutes, initDevIPC, initSystemIPC, initWindowIPC } from './ipc/_index';
import { EnvironmentIPC } from './ipc/Environment.ipc';
-import { getEnvironment, loadEnvironment } from './JRCEnvironment';
+import { getEnvironment, loadEnvironment, shouldStartMinimized } from './JRCEnvironment';
import { processManager } from './ProcessManager';
import { restApiServer } from './RestAPI';
import { registerIPC } from './IPCController';
-import { getAllProfiles, getSettings } from './Store';
+import { getAllProfiles, getSettings, syncLoginItem } from './Store';
loadEnvironment();
@@ -44,7 +44,7 @@ function createWindow(): void {
frame: false,
backgroundColor: '#08090d',
icon: getIconImage(),
- show: getEnvironment().startUpSource != 'withSystem',
+ show: getEnvironment().startUpSource !== 'withSystem',
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
@@ -58,8 +58,7 @@ function createWindow(): void {
else mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'));
mainWindow.once('ready-to-show', () => {
- const shouldStartHidden =
- getSettings().startMinimized && getEnvironment().startUpSource === 'withSystem';
+ const shouldStartHidden = shouldStartMinimized();
if (shouldStartHidden) mainWindow?.hide();
else mainWindow?.show();
});
@@ -134,39 +133,17 @@ if (!gotLock) {
});
app.whenReady().then(() => {
+ // If launched by the OS at login but the user has since disabled autostart, bail out.
+ // This handles the edge case where the registry entry outlives the setting.
if (getEnvironment().startUpSource === 'withSystem' && !getSettings().launchOnStartup) return;
+ // Ensure the OS login item always reflects the stored setting
+ syncLoginItem(getSettings().launchOnStartup, getSettings().startMinimized);
+
createWindow();
createTray();
- mainWindow?.webContents.on('before-input-event', (event, input) => {
- const isDevToolsShortcut =
- input.key === 'F12' ||
- (input.control && input.shift && input.key === 'I') ||
- (input.meta && input.alt && input.key === 'I');
-
- if (!isDevToolsShortcut) return;
-
- event.preventDefault();
- devToolsPressCount++;
-
- // reset counter after 1 second of inactivity
- if (devToolsTimer) clearTimeout(devToolsTimer);
- devToolsTimer = setTimeout(() => {
- devToolsPressCount = 0;
- }, 1000);
-
- if (devToolsPressCount >= 7) {
- devToolsPressCount = 0;
- mainWindow?.webContents.openDevTools({ mode: 'detach' });
- return;
- }
-
- // normal single-press behavior only if devModeEnabled
- if (getEnvironment().devMode) {
- mainWindow?.webContents.openDevTools();
- }
- });
+ mainWindow?.webContents.on('before-input-event', handleBeforeInputEvent);
// ── IPC ────────────────────────────────────────────────────────────────────
initSystemIPC(() => mainWindow);
@@ -199,3 +176,29 @@ app.on('before-quit', () => {
app.on('activate', () => {
mainWindow?.show();
});
+
+const handleBeforeInputEvent = (event: Electron.Event, input: Input) => {
+ const isDevToolsShortcut =
+ input.key === 'F12' ||
+ (input.control && input.shift && input.key.toUpperCase() === 'I') ||
+ (input.meta && input.alt && input.key.toUpperCase() === 'I');
+
+ if (!isDevToolsShortcut) return;
+
+ event.preventDefault();
+
+ devToolsPressCount++;
+
+ if (devToolsTimer) clearTimeout(devToolsTimer);
+ devToolsTimer = setTimeout(() => (devToolsPressCount = 0), 1000);
+
+ if (devToolsPressCount >= 7) {
+ devToolsPressCount = 0;
+ mainWindow?.webContents.openDevTools({ mode: 'detach' });
+ return;
+ }
+
+ if (getEnvironment().devMode) {
+ mainWindow?.webContents.openDevTools();
+ }
+};
diff --git a/src/main/shared/config/API.config.ts b/src/main/shared/config/API.config.ts
index 3042c03..fd683ca 100644
--- a/src/main/shared/config/API.config.ts
+++ b/src/main/shared/config/API.config.ts
@@ -1,4 +1,7 @@
-// ─── Base types ───────────────────────────────────────────────────────────────
+export const REST_API_CONFIG = {
+ defaultPort: 4444,
+ host: '127.0.0.1',
+} as const;
export type RouteDefinition = {
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
@@ -7,8 +10,6 @@ export type RouteDefinition = {
bodyTemplate?: string;
};
-// ─── Strongly typed route map ─────────────────────────────────────────────────
-
export const routeConfig = {
status: {
method: 'GET',
diff --git a/src/main/shared/config/App.config.ts b/src/main/shared/config/App.config.ts
index 791b885..22ab8e4 100644
--- a/src/main/shared/config/App.config.ts
+++ b/src/main/shared/config/App.config.ts
@@ -1,5 +1,5 @@
import type { AppSettings } from '../types/App.types';
-import { REST_API_CONFIG } from './RestApi.config';
+import { REST_API_CONFIG } from './API.config';
export const DEFAULT_SETTINGS: AppSettings = {
launchOnStartup: false,
diff --git a/src/main/shared/config/RestApi.config.ts b/src/main/shared/config/RestApi.config.ts
deleted file mode 100644
index c3b3eb2..0000000
--- a/src/main/shared/config/RestApi.config.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-export const REST_API_CONFIG = {
- defaultPort: 4444,
- host: '127.0.0.1',
-} as const;
-
-export const REST_ROUTES = {
- // Info
- status: '/api/status',
- profiles: '/api/profiles',
- profile: '/api/profiles/:id',
- settings: '/api/settings',
- processStates: '/api/processes',
- processLog: '/api/processes/log',
-
- // Actions
- profileCreate: '/api/profiles',
- profileUpdate: '/api/profiles/:id',
- profileDelete: '/api/profiles/:id',
- processStart: '/api/processes/:id/start',
- processStop: '/api/processes/:id/stop',
- consoleClear: '/api/processes/:id/console/clear',
- settingsUpdate: '/api/settings',
-} as const;
diff --git a/src/main/store.ts b/src/main/store.ts
index 1269540..6a10ea8 100644
--- a/src/main/store.ts
+++ b/src/main/store.ts
@@ -1,3 +1,4 @@
+import { app } from 'electron';
import Store from 'electron-store';
import { DEFAULT_SETTINGS } from './shared/config/App.config';
import { Profile } from './shared/types/Profile.types';
@@ -57,5 +58,21 @@ export function getSettings(): AppSettings {
}
export function saveSettings(settings: AppSettings): void {
+ const prev = getSettings();
store.set('settings', settings);
+ if (
+ settings.launchOnStartup !== prev.launchOnStartup ||
+ settings.startMinimized !== prev.startMinimized
+ ) {
+ syncLoginItem(settings.launchOnStartup, settings.startMinimized);
+ }
+}
+
+export function syncLoginItem(openAtLogin: boolean, startMinimized: boolean): void {
+ if (!app.isPackaged) return;
+ const args = ['--autostart', startMinimized && '--minimized'].filter(Boolean) as string[];
+ app.setLoginItemSettings({
+ openAtLogin,
+ args,
+ });
}
diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx
index 621c7ba..2c6e703 100644
--- a/src/renderer/App.tsx
+++ b/src/renderer/App.tsx
@@ -34,7 +34,7 @@ export default function App() {
v7_relativeSplatPath: false,
}}
>
-
+
} />
diff --git a/src/renderer/components/MainLayout.tsx b/src/renderer/components/MainLayout.tsx
index c864f84..97b1829 100644
--- a/src/renderer/components/MainLayout.tsx
+++ b/src/renderer/components/MainLayout.tsx
@@ -73,7 +73,7 @@ export function MainLayout() {
};
return (
-
+
openPanel('settings')}
onOpenFaq={() => openPanel('faq')}
@@ -83,7 +83,7 @@ export function MainLayout() {
activeSidePanel={activePanel}
/>
-
+
{activePanel ? (
<>
@@ -113,7 +113,7 @@ export function MainLayout() {
-
+
} />
} />
@@ -158,7 +158,7 @@ export function MainLayout() {
)}
-
+
} />
} />
diff --git a/src/renderer/components/developer/DevApiExplorer.tsx b/src/renderer/components/developer/DevApiExplorer.tsx
index fea6237..e00cdd6 100644
--- a/src/renderer/components/developer/DevApiExplorer.tsx
+++ b/src/renderer/components/developer/DevApiExplorer.tsx
@@ -1,9 +1,10 @@
-import { useState } from 'react';
-import { VscCheck, VscCopy, VscPlay } from 'react-icons/vsc';
+import { useState, useCallback } from 'react';
+import { VscCheck, VscCopy, VscPlay, VscEdit, VscCode } from 'react-icons/vsc';
import { routeConfig, RouteDefinition } from '../../../main/shared/config/API.config';
import { useApp } from '../../store/AppStore';
import { Button } from '../common/Button';
-import { REST_API_CONFIG } from '../../../main/shared/config/RestApi.config';
+import { REST_API_CONFIG } from '../../../main/shared/config/API.config';
+import { ContextMenu, ContextMenuItem } from '../common/ContextMenu';
const METHOD_COLORS: Record = {
GET: 'text-accent border-accent/30 bg-accent/10',
@@ -12,7 +13,108 @@ const METHOD_COLORS: Record = {
DELETE: 'text-red-400 border-red-400/30 bg-red-400/10',
};
-const routes = Object.values(routeConfig);
+// ─── JSON Syntax Highlighter ────────────────────────────────────────────────
+
+type Token =
+ | { type: 'key'; value: string }
+ | { type: 'string'; value: string }
+ | { type: 'number'; value: string }
+ | { type: 'boolean'; value: string }
+ | { type: 'null'; value: string }
+ | { type: 'punct'; value: string }
+ | { type: 'plain'; value: string };
+
+function tokenizeJson(text: string): Token[] {
+ const tokens: Token[] = [];
+ let i = 0;
+
+ while (i < text.length) {
+ const ws = text.slice(i).match(/^[\s,:\[\]{}]+/);
+ if (ws) {
+ const chunk = ws[0];
+ for (const ch of chunk) {
+ if ('{}[],:'.includes(ch)) tokens.push({ type: 'punct', value: ch });
+ else tokens.push({ type: 'plain', value: ch });
+ }
+ i += chunk.length;
+ continue;
+ }
+
+ if (text[i] === '"') {
+ let j = i + 1;
+ while (j < text.length) {
+ if (text[j] === '\\') {
+ j += 2;
+ continue;
+ }
+ if (text[j] === '"') {
+ j++;
+ break;
+ }
+ j++;
+ }
+ const raw = text.slice(i, j);
+ const afterStr = text.slice(j).match(/^\s*:/);
+ tokens.push({ type: afterStr ? 'key' : 'string', value: raw });
+ i = j;
+ continue;
+ }
+
+ const num = text.slice(i).match(/^-?\d+(\.\d+)?([eE][+-]?\d+)?/);
+ if (num) {
+ tokens.push({ type: 'number', value: num[0] });
+ i += num[0].length;
+ continue;
+ }
+
+ const keyword = text.slice(i).match(/^(true|false|null)/);
+ if (keyword) {
+ tokens.push({ type: keyword[0] === 'null' ? 'null' : 'boolean', value: keyword[0] });
+ i += keyword[0].length;
+ continue;
+ }
+
+ tokens.push({ type: 'plain', value: text[i] });
+ i++;
+ }
+
+ return tokens;
+}
+
+const TOKEN_CLASS: Record = {
+ key: 'text-blue-300',
+ string: 'text-emerald-400',
+ number: 'text-amber-400',
+ boolean: 'text-purple-400',
+ null: 'text-red-400/80',
+ punct: 'text-text-muted',
+ plain: 'text-text-secondary',
+};
+
+function JsonHighlight({ text }: { text: string }) {
+ let isJson = false;
+ try {
+ JSON.parse(text);
+ isJson = true;
+ } catch {
+ /* not JSON */
+ }
+
+ if (!isJson) return {text} ;
+
+ const tokens = tokenizeJson(text);
+ return (
+ <>
+ {tokens.map((tok, idx) => (
+
+ {tok.value}
+
+ ))}
+ >
+ );
+}
+
+// ─── Component ───────────────────────────────────────────────────────────────
export function DevApiExplorer() {
const { state } = useApp();
@@ -22,14 +124,46 @@ export function DevApiExplorer() {
const [body, setBody] = useState('');
const [response, setResponse] = useState(null);
const [loading, setLoading] = useState(false);
- const [copied, setCopied] = useState(false);
+
+ const [urlCopied, setUrlCopied] = useState(false);
+ const [responseCopied, setResponseCopied] = useState(false);
+ const [isEditing, setIsEditing] = useState(false);
+
+ const [ctxMenu, setCtxMenu] = useState<{
+ x: number;
+ y: number;
+ items: ContextMenuItem[];
+ } | null>(null);
const port = state.settings?.restApiPort ?? 4444;
const restEnabled = state.settings?.restApiEnabled ?? false;
+ // ── Helpers ──────────────────────────────────────────────────────────────
+
+ const handleContextMenu = useCallback(
+ (e: React.MouseEvent, extraItems: ContextMenuItem[] = []) => {
+ const selection = window.getSelection()?.toString() ?? '';
+
+ const items: ContextMenuItem[] = [
+ {
+ label: 'Copy',
+ icon: ,
+ disabled: !selection,
+ onClick: () => navigator.clipboard.writeText(selection),
+ },
+ ...extraItems,
+ ];
+
+ e.preventDefault();
+ setCtxMenu({ x: e.clientX, y: e.clientY, items });
+ },
+ []
+ );
+
const handleSelect = (route: RouteDefinition) => {
setSelected(route);
setResponse(null);
+ setIsEditing(false);
setBody(route.bodyTemplate ?? '');
const params: Record = {};
@@ -51,6 +185,7 @@ export function DevApiExplorer() {
if (!selected) return;
setLoading(true);
setResponse(null);
+ setIsEditing(false);
try {
const url = buildUrl();
@@ -78,13 +213,41 @@ export function DevApiExplorer() {
const copyUrl = () => {
navigator.clipboard.writeText(buildUrl());
- setCopied(true);
- setTimeout(() => setCopied(false), 1500);
+ setUrlCopied(true);
+ setTimeout(() => setUrlCopied(false), 1500);
};
+ const copyResponse = useCallback(() => {
+ if (!response) return;
+ navigator.clipboard.writeText(response);
+ setResponseCopied(true);
+ setTimeout(() => setResponseCopied(false), 1500);
+ }, [response]);
+
+ const toggleEdit = () => setIsEditing((v) => !v);
+
+ // ── Response context menu items (extend here in the future) ──────────────
+
+ const responseCtxItems = useCallback(
+ (): ContextMenuItem[] =>
+ response
+ ? [
+ { type: 'separator' },
+ {
+ label: 'Copy all',
+ icon: ,
+ onClick: () => navigator.clipboard.writeText(response),
+ },
+ ]
+ : [],
+ [response]
+ );
+
+ // ─────────────────────────────────────────────────────────────────────────
+
return (
- {/* Route list */}
+ {/* ── Route list ──────────────────────────────────────────────────── */}
{!restEnabled && (
@@ -113,14 +276,13 @@ export function DevApiExplorer() {
{route.method}
-
{route.path}
{route.description}
))}
- {/* Request + response */}
+ {/* ── Request + response ──────────────────────────────────────────── */}
{!selected ? (
@@ -140,7 +302,10 @@ export function DevApiExplorer() {
{selected.method}
-
+ handleContextMenu(e)}
+ className="flex-1 text-xs font-mono text-text-primary bg-base-950 border border-surface-border rounded px-2.5 py-1.5 truncate select-text"
+ >
{buildUrl()}
@@ -149,7 +314,11 @@ export function DevApiExplorer() {
className="text-text-muted hover:text-accent transition-colors p-1"
title="Copy URL"
>
- {copied ? : }
+ {urlCopied ? (
+
+ ) : (
+
+ )}
@@ -183,7 +352,6 @@ export function DevApiExplorer() {
Request Body (JSON)
-
)}
- {/* Response */}
+ {/* ── Response panel ────────────────────────────────────── */}
-
+ {/* Titlebar */}
+
Response
-
-
- {response ?? (
-
- {loading ? 'Waiting...' : 'Press Send to call the API'}
-
+ {response && (
+
+
+ {isEditing ? : }
+
+
+
+ {responseCopied ? (
+
+ ) : (
+
+ )}
+
+
)}
-
+
+
+ {/* Highlighted view or editable textarea */}
+ {isEditing ? (
+
>
)}
+
+ {/* ── Context menu ────────────────────────────────────────────────── */}
+ {ctxMenu && (
+ setCtxMenu(null)}
+ />
+ )}
);
}
diff --git a/src/renderer/components/developer/DevDashboard.tsx b/src/renderer/components/developer/DevDashboard.tsx
index 103c3f4..18ee59f 100644
--- a/src/renderer/components/developer/DevDashboard.tsx
+++ b/src/renderer/components/developer/DevDashboard.tsx
@@ -1,6 +1,7 @@
import React, { useEffect, useState } from 'react';
import { useApp } from '../../store/AppStore';
import { VscCircleFilled, VscCircle } from 'react-icons/vsc';
+import { JRCEnvironment } from '../../../main/shared/types/App.types';
declare const __APP_VERSION__: string;
@@ -10,6 +11,7 @@ interface SysInfo {
nodeVersion: string;
electronVersion: string;
chromeVersion: string;
+ argv: string[];
uptime: number;
memoryUsageMB: number;
}
@@ -57,108 +59,113 @@ function Badge({ ok, label }: { ok: boolean; label: string }) {
);
}
+function Section({ title, children }: { title: string; children: React.ReactNode }) {
+ return (
+
+ );
+}
+
export function DevDashboard() {
const { state } = useApp();
const [sysInfo, setSysInfo] = useState
(null);
- const [tick, setTick] = useState(0);
+ const [env, setEnv] = useState();
useEffect(() => {
- window.api.getSysInfo().then(setSysInfo);
- const id = setInterval(() => {
- window.api.getSysInfo().then(setSysInfo);
- setTick((t) => t + 1);
- }, 3000);
+ const load = () => window.api.getSysInfo().then(setSysInfo);
+ load();
+ const id = setInterval(load, 3000);
return () => clearInterval(id);
}, []);
+ useEffect(() => {
+ const loadEnv = async () => {
+ const environment = await window.env.get();
+ setEnv(environment);
+ };
+ loadEnv();
+ }, []);
+
const runningProfiles = state.processStates.filter((s) => s.running);
const totalConsoleLines = Object.values(state.consoleLogs).reduce((a, b) => a + b.length, 0);
const restEnabled = state.settings?.restApiEnabled ?? false;
return (
-
- {/* App state */}
-
-
- {/* Feature flags */}
-
-
- {/* Running processes */}
-
- {runningProfiles.length === 0 ? (
- No processes running
- ) : (
-
- {runningProfiles.map((s) => {
- const profile = state.profiles.find((p) => p.id === s.profileId);
- const uptimeSec = s.startedAt ? Math.floor((Date.now() - s.startedAt) / 1000) : 0;
- return (
-
-
-
- {profile?.name ?? s.profileId}
-
- PID {s.pid}
- {uptimeSec}s
-
- );
- })}
+
+
+ {/* Application State */}
+
+
- {/* System info */}
-
- {sysInfo ? (
-
-
-
-
-
+ {sysInfo && (
+
+
+ Process Argv
+
+
+ {sysInfo.argv.join(' ')}
+
- ) : (
-
Loading...
)}
-
-
- );
-}
+ {/* Feature Flags */}
+
-function Section({ title, children }: { title: string; children: React.ReactNode }) {
- return (
-
-
{title}
- {children}
+ {/* Running Processes */}
+
+ {runningProfiles.length === 0 ? (
+ No processes running
+ ) : (
+
+ {runningProfiles.map((s) => {
+ const profile = state.profiles.find((p) => p.id === s.profileId);
+ const uptimeSec = s.startedAt ? Math.floor((Date.now() - s.startedAt) / 1000) : 0;
+
+ return (
+
+
+
+ {profile?.name ?? s.profileId}
+
+ PID {s.pid}
+ {uptimeSec}s
+
+ );
+ })}
+
+ )}
+
+
+ {/* System */}
+
+ {sysInfo ? (
+
+
+
+
+
+
+ ) : (
+ Loading...
+ )}
+
+
);
}
diff --git a/src/renderer/components/developer/DeveloperTab.tsx b/src/renderer/components/developer/DeveloperTab.tsx
index 300218d..08eb879 100644
--- a/src/renderer/components/developer/DeveloperTab.tsx
+++ b/src/renderer/components/developer/DeveloperTab.tsx
@@ -46,7 +46,7 @@ export function DeveloperTab() {
))}
-
+
{panel === 'dashboard' && }
{panel === 'api' && }
{panel === 'storage' && }
diff --git a/src/renderer/components/settings/SettingsTab.tsx b/src/renderer/components/settings/SettingsTab.tsx
index f6f74e9..13afb75 100644
--- a/src/renderer/components/settings/SettingsTab.tsx
+++ b/src/renderer/components/settings/SettingsTab.tsx
@@ -3,7 +3,7 @@ import { useApp } from '../../store/AppStore';
import { Button } from '../common/Button';
import { Toggle } from '../common/Toggle';
import { VersionChecker } from './version/VersionChecker';
-import { REST_API_CONFIG } from '../../../main/shared/config/RestApi.config';
+import { REST_API_CONFIG } from '../../../main/shared/config/API.config';
import { version } from '../../../../package.json';
import { AppSettings, JRCEnvironment } from '../../../main/shared/types/App.types';