Convert to use typescript, implement eslint
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
node_modules/
|
||||
build/
|
||||
dist/
|
||||
out/
|
||||
release/
|
||||
|
||||
33
README.md
33
README.md
@@ -18,7 +18,7 @@ cd corsair-lcd-control
|
||||
npm install
|
||||
```
|
||||
|
||||
`postinstall` automatically rebuilds native modules for Electron using `@electron/rebuild`.
|
||||
`postinstall` rebuilds native USB modules for Electron.
|
||||
|
||||
## Development
|
||||
|
||||
@@ -26,13 +26,15 @@ npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
Compiles TypeScript to `build/`, then launches Electron with the compiled output.
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
Produces platform-specific packages:
|
||||
Compiles TypeScript, then produces platform-specific packages via electron-builder:
|
||||
|
||||
| Platform | Format |
|
||||
|----------|--------|
|
||||
@@ -40,6 +42,14 @@ Produces platform-specific packages:
|
||||
| macOS | `.dmg` |
|
||||
| Windows | `.exe` (NSIS) |
|
||||
|
||||
## Lint
|
||||
|
||||
```bash
|
||||
npm run lint
|
||||
```
|
||||
|
||||
Runs ESLint with `typescript-eslint` (`recommendedTypeChecked`) across all `src/` TypeScript files.
|
||||
|
||||
## Rebuild Native Modules
|
||||
|
||||
If native modules fail to load after a Node.js or Electron upgrade:
|
||||
@@ -48,9 +58,26 @@ If native modules fail to load after a Node.js or Electron upgrade:
|
||||
npm run rebuild
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
├── src/
|
||||
│ ├── main.ts # Electron main process
|
||||
│ ├── preload.ts # Context bridge for IPC
|
||||
│ ├── renderer.ts # Renderer UI logic
|
||||
│ ├── index.html # Main window HTML
|
||||
│ └── styles.css # Dark theme styles
|
||||
├── assets/
|
||||
│ └── icon.png # System tray icon
|
||||
├── build/ # Compiled JS output (gitignored)
|
||||
├── tsconfig.json # TypeScript config
|
||||
├── eslint.config.js # ESLint config
|
||||
└── package.json
|
||||
```
|
||||
|
||||
## Capabilities
|
||||
|
||||
- **USB** — `usb` module enumerates and connects to devices
|
||||
- **USB** — enumerates and connects to devices via `usb`
|
||||
- **System Tray** — background process with context menu (Show/Hide/Quit)
|
||||
- **Preferences** — persisted via `electron-store` (JSON)
|
||||
- **File Pickers** — native OS dialogs via Electron's `dialog` API
|
||||
|
||||
BIN
assets/icon.png
Normal file
BIN
assets/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 110 B |
41
eslint.config.js
Normal file
41
eslint.config.js
Normal file
@@ -0,0 +1,41 @@
|
||||
const eslint = require('@eslint/js');
|
||||
const tseslint = require('typescript-eslint');
|
||||
|
||||
module.exports = tseslint.config(
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
||||
'@typescript-eslint/prefer-readonly': 'error',
|
||||
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
|
||||
'@typescript-eslint/no-useless-constructor': 'error',
|
||||
'@typescript-eslint/prefer-optional-chain': 'error',
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'error',
|
||||
'@typescript-eslint/no-require-imports': 'off',
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/no-misused-promises': ['error', { checksVoidReturn: false }],
|
||||
'@typescript-eslint/restrict-template-expressions': ['error', { allowNumber: true }],
|
||||
'no-useless-return': 'error',
|
||||
'no-var': 'error',
|
||||
'prefer-const': 'error',
|
||||
'no-console': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['src/renderer.ts'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-unsafe-assignment': 'off',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
ignores: ['build/', 'dist/', 'node_modules/', 'assets/'],
|
||||
}
|
||||
);
|
||||
1221
package-lock.json
generated
1221
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
25
package.json
25
package.json
@@ -4,20 +4,21 @@
|
||||
"author": "xerotacovix <xerotacovix@proton.me>",
|
||||
"homepage": "https://github.com/xerotacovix/corsair-lcd-control",
|
||||
"description": "Electron app to control Corsair LCD displays",
|
||||
"main": "main.js",
|
||||
"main": "build/main.js",
|
||||
"scripts": {
|
||||
"start": "electron .",
|
||||
"build": "electron-builder",
|
||||
"build:ts": "tsc && cp src/index.html src/styles.css build/",
|
||||
"start": "npm run build:ts && electron .",
|
||||
"build": "npm run build:ts && electron-builder",
|
||||
"postinstall": "CXXFLAGS='-std=c++17' npx @electron/rebuild",
|
||||
"rebuild": "CXXFLAGS='-std=c++17' npx @electron/rebuild"
|
||||
"rebuild": "CXXFLAGS='-std=c++17' npx @electron/rebuild",
|
||||
"lint": "eslint src/"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.corsair-lcd-control.app",
|
||||
"productName": "Corsair LCD Control",
|
||||
"files": [
|
||||
"main.js",
|
||||
"preload.js",
|
||||
"src/**/*"
|
||||
"build/**/*",
|
||||
"assets/**/*"
|
||||
],
|
||||
"linux": {
|
||||
"target": [
|
||||
@@ -42,8 +43,16 @@
|
||||
"usb": "^2.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@types/node": "^25.9.2",
|
||||
"@types/usb": "^2.0.4",
|
||||
"@typescript-eslint/eslint-plugin": "^8.61.0",
|
||||
"@typescript-eslint/parser": "^8.61.0",
|
||||
"electron": "^42.3.3",
|
||||
"electron-builder": "^24.9.0"
|
||||
"electron-builder": "^24.9.0",
|
||||
"eslint": "^10.4.1",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.61.0"
|
||||
},
|
||||
"allowScripts": {
|
||||
"usb@2.18.0": true,
|
||||
|
||||
21
preload.js
21
preload.js
@@ -1,21 +0,0 @@
|
||||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
// USB
|
||||
usbListDevices: () => ipcRenderer.invoke('usb:listDevices'),
|
||||
usbConnect: (vendorId, productId) => ipcRenderer.invoke('usb:connect', { vendorId, productId }),
|
||||
|
||||
// Preferences
|
||||
prefsGet: (key) => ipcRenderer.invoke('prefs:get', key),
|
||||
prefsSet: (key, value) => ipcRenderer.invoke('prefs:set', key, value),
|
||||
prefsDelete: (key) => ipcRenderer.invoke('prefs:delete', key),
|
||||
|
||||
// File Picker
|
||||
dialogOpenFile: (options) => ipcRenderer.invoke('dialog:openFile', options),
|
||||
dialogSaveFile: (options) => ipcRenderer.invoke('dialog:saveFile', options),
|
||||
|
||||
// System Info
|
||||
systemCpuTemp: () => ipcRenderer.invoke('system:cpuTemp'),
|
||||
systemRam: () => ipcRenderer.invoke('system:ram'),
|
||||
systemGpu: () => ipcRenderer.invoke('system:gpu'),
|
||||
});
|
||||
@@ -41,7 +41,7 @@
|
||||
<section id="preferences-section">
|
||||
<h2>Preferences</h2>
|
||||
<div>
|
||||
<label>Refresh Interval (ms):</label>
|
||||
<label for="refresh-interval">Refresh Interval (ms):</label>
|
||||
<input type="number" id="refresh-interval" value="5000" />
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -1,38 +1,34 @@
|
||||
const { app, BrowserWindow, ipcMain, Tray, Menu, nativeImage, dialog } = require('electron');
|
||||
const path = require('path');
|
||||
const Store = require('electron-store');
|
||||
const si = require('systeminformation');
|
||||
const usb = require('usb');
|
||||
import { app, BrowserWindow, ipcMain, Tray, Menu, nativeImage, dialog } from 'electron';
|
||||
import path = require('path');
|
||||
import Store from 'electron-store';
|
||||
import si = require('systeminformation');
|
||||
import usb = require('usb');
|
||||
|
||||
const store = new Store({ defaults: { preferences: {} } });
|
||||
|
||||
let mainWindow = null;
|
||||
let tray = null;
|
||||
|
||||
function createTrayIcon() {
|
||||
const size = 16;
|
||||
const canvas = Buffer.alloc(size * size * 4);
|
||||
for (let y = 0; y < size; y++) {
|
||||
for (let x = 0; x < size; x++) {
|
||||
const idx = (y * size + x) * 4;
|
||||
const cx = x - size / 2, cy = y - size / 2;
|
||||
if (Math.sqrt(cx * cx + cy * cy) < size / 2 - 1) {
|
||||
canvas[idx] = 0;
|
||||
canvas[idx + 1] = 120;
|
||||
canvas[idx + 2] = 255;
|
||||
canvas[idx + 3] = 255;
|
||||
} else {
|
||||
canvas[idx] = 0;
|
||||
canvas[idx + 1] = 0;
|
||||
canvas[idx + 2] = 0;
|
||||
canvas[idx + 3] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
return nativeImage.createFromBuffer(canvas, { width: size, height: size });
|
||||
interface DeviceEntry {
|
||||
vendorId: number;
|
||||
productId: number;
|
||||
displayName: string;
|
||||
manufacturer: string;
|
||||
product: string;
|
||||
serialNumber: string;
|
||||
}
|
||||
|
||||
function createWindow() {
|
||||
interface VendorMap {
|
||||
[key: number]: string;
|
||||
}
|
||||
|
||||
const store = new Store<Record<string, unknown>>({ defaults: { preferences: {} } });
|
||||
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
let tray: Tray | null = null;
|
||||
let isQuitting = false;
|
||||
|
||||
function createTrayIcon(): Electron.NativeImage {
|
||||
const iconPath = path.join(__dirname, 'assets', 'icon.png');
|
||||
return nativeImage.createFromPath(iconPath);
|
||||
}
|
||||
|
||||
function createWindow(): void {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 900,
|
||||
height: 600,
|
||||
@@ -42,29 +38,29 @@ function createWindow() {
|
||||
nodeIntegration: false,
|
||||
},
|
||||
});
|
||||
mainWindow.loadFile(path.join(__dirname, 'src', 'index.html'));
|
||||
mainWindow.on('close', (e) => {
|
||||
if (!app.isQuitting) {
|
||||
void mainWindow.loadFile(path.join(__dirname, 'index.html'));
|
||||
mainWindow.on('close', (e: Electron.Event) => {
|
||||
if (!isQuitting) {
|
||||
e.preventDefault();
|
||||
mainWindow.hide();
|
||||
mainWindow!.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function createTray() {
|
||||
function createTray(): void {
|
||||
tray = new Tray(createTrayIcon());
|
||||
tray.setToolTip('Corsair LCD Control');
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{ label: 'Show', click: () => mainWindow.show() },
|
||||
{ label: 'Hide', click: () => mainWindow.hide() },
|
||||
{ label: 'Show', click: () => { mainWindow!.show(); } },
|
||||
{ label: 'Hide', click: () => { mainWindow!.hide(); } },
|
||||
{ type: 'separator' },
|
||||
{ label: 'Quit', click: () => { app.isQuitting = true; app.quit(); } },
|
||||
{ label: 'Quit', click: () => { isQuitting = true; app.quit(); } },
|
||||
]);
|
||||
tray.setContextMenu(contextMenu);
|
||||
tray.on('double-click', () => mainWindow.show());
|
||||
tray.on('double-click', () => { mainWindow!.show(); });
|
||||
}
|
||||
|
||||
const VENDORS = {
|
||||
const VENDORS: VendorMap = {
|
||||
0x1B1C: 'Corsair',
|
||||
0x046D: 'Logitech',
|
||||
0x0A5C: 'Broadcom',
|
||||
@@ -77,18 +73,18 @@ const VENDORS = {
|
||||
0x5964: 'Asmedia',
|
||||
};
|
||||
|
||||
function formatHexId(vendorId, productId) {
|
||||
function formatHexId(vendorId: number, productId: number): string {
|
||||
const hex = `${vendorId.toString(16).padStart(4, '0')}:${productId.toString(16).padStart(4, '0')}`;
|
||||
const name = VENDORS[vendorId] || '';
|
||||
return name ? `${name} [${hex}]` : hex;
|
||||
}
|
||||
|
||||
function readStringDescriptor(device, index) {
|
||||
function readStringDescriptor(device: usb.Device, index: number): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
if (!index) return resolve('');
|
||||
if (!index) { resolve(''); return; }
|
||||
try {
|
||||
device.getStringDescriptor(index, (err, desc) => {
|
||||
resolve(err ? '' : desc);
|
||||
device.getStringDescriptor(index, (err: usb.LibUSBException | undefined, desc?: string) => {
|
||||
resolve(err ? '' : desc ?? '');
|
||||
});
|
||||
} catch {
|
||||
resolve('');
|
||||
@@ -98,12 +94,12 @@ function readStringDescriptor(device, index) {
|
||||
|
||||
// --- USB ---
|
||||
|
||||
ipcMain.handle('usb:listDevices', async () => {
|
||||
ipcMain.handle('usb:listDevices', async (): Promise<DeviceEntry[] | { error: string }> => {
|
||||
try {
|
||||
const devices = usb.getDeviceList();
|
||||
const results = [];
|
||||
const results: DeviceEntry[] = [];
|
||||
for (const d of devices) {
|
||||
const entry = {
|
||||
const entry: DeviceEntry = {
|
||||
vendorId: d.deviceDescriptor.idVendor,
|
||||
productId: d.deviceDescriptor.idProduct,
|
||||
displayName: formatHexId(d.deviceDescriptor.idVendor, d.deviceDescriptor.idProduct),
|
||||
@@ -132,58 +128,61 @@ ipcMain.handle('usb:listDevices', async () => {
|
||||
}
|
||||
return results;
|
||||
} catch (err) {
|
||||
return { error: err.message };
|
||||
return { error: (err as Error).message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('usb:connect', async (_e, { vendorId, productId }) => {
|
||||
ipcMain.handle('usb:connect', (_e: Electron.IpcMainInvokeEvent, { vendorId, productId }: { vendorId: number; productId: number }) => {
|
||||
try {
|
||||
const device = usb.findByIds(vendorId, productId);
|
||||
if (!device) throw new Error('Device not found');
|
||||
device.open();
|
||||
device.open(true);
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
return { error: err.message };
|
||||
return { error: (err as Error).message };
|
||||
}
|
||||
});
|
||||
|
||||
// --- Preferences ---
|
||||
ipcMain.handle('prefs:get', (_e, key) => {
|
||||
|
||||
ipcMain.handle('prefs:get', (_e: Electron.IpcMainInvokeEvent, key?: string) => {
|
||||
if (key) return store.get(key);
|
||||
return store.store;
|
||||
});
|
||||
|
||||
ipcMain.handle('prefs:set', (_e, key, value) => {
|
||||
ipcMain.handle('prefs:set', (_e: Electron.IpcMainInvokeEvent, key: string, value: unknown) => {
|
||||
store.set(key, value);
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
ipcMain.handle('prefs:delete', (_e, key) => {
|
||||
ipcMain.handle('prefs:delete', (_e: Electron.IpcMainInvokeEvent, key: string) => {
|
||||
store.delete(key);
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
// --- File Picker ---
|
||||
ipcMain.handle('dialog:openFile', async (_e, options) => {
|
||||
const result = await dialog.showOpenDialog(mainWindow, {
|
||||
|
||||
ipcMain.handle('dialog:openFile', async (_e: Electron.IpcMainInvokeEvent, options?: Electron.OpenDialogOptions) => {
|
||||
const result = await dialog.showOpenDialog(mainWindow!, {
|
||||
properties: ['openFile'],
|
||||
...options,
|
||||
});
|
||||
return result;
|
||||
});
|
||||
|
||||
ipcMain.handle('dialog:saveFile', async (_e, options) => {
|
||||
const result = await dialog.showSaveDialog(mainWindow, options);
|
||||
ipcMain.handle('dialog:saveFile', async (_e: Electron.IpcMainInvokeEvent, options?: Electron.SaveDialogOptions) => {
|
||||
const result = await dialog.showSaveDialog(mainWindow!, options ?? {});
|
||||
return result;
|
||||
});
|
||||
|
||||
// --- System Info ---
|
||||
|
||||
ipcMain.handle('system:cpuTemp', async () => {
|
||||
try {
|
||||
const data = await si.cpuTemperature();
|
||||
return data;
|
||||
} catch (err) {
|
||||
return { error: err.message };
|
||||
return { error: (err as Error).message };
|
||||
}
|
||||
});
|
||||
|
||||
@@ -197,7 +196,7 @@ ipcMain.handle('system:ram', async () => {
|
||||
usedPercent: ((data.used / data.total) * 100).toFixed(1),
|
||||
};
|
||||
} catch (err) {
|
||||
return { error: err.message };
|
||||
return { error: (err as Error).message };
|
||||
}
|
||||
});
|
||||
|
||||
@@ -213,11 +212,11 @@ ipcMain.handle('system:gpu', async () => {
|
||||
}));
|
||||
return controllers;
|
||||
} catch (err) {
|
||||
return { error: err.message };
|
||||
return { error: (err as Error).message };
|
||||
}
|
||||
});
|
||||
|
||||
app.whenReady().then(() => {
|
||||
void app.whenReady().then(() => {
|
||||
createWindow();
|
||||
createTray();
|
||||
});
|
||||
17
src/preload.ts
Normal file
17
src/preload.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { contextBridge, ipcRenderer } from 'electron';
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
usbListDevices: (): Promise<unknown> => ipcRenderer.invoke('usb:listDevices'),
|
||||
usbConnect: (vendorId: number, productId: number): Promise<unknown> => ipcRenderer.invoke('usb:connect', { vendorId, productId }),
|
||||
|
||||
prefsGet: (key?: string): Promise<unknown> => ipcRenderer.invoke('prefs:get', key),
|
||||
prefsSet: (key: string, value: unknown): Promise<unknown> => ipcRenderer.invoke('prefs:set', key, value),
|
||||
prefsDelete: (key: string): Promise<unknown> => ipcRenderer.invoke('prefs:delete', key),
|
||||
|
||||
dialogOpenFile: (options?: unknown): Promise<unknown> => ipcRenderer.invoke('dialog:openFile', options),
|
||||
dialogSaveFile: (options?: unknown): Promise<unknown> => ipcRenderer.invoke('dialog:saveFile', options),
|
||||
|
||||
systemCpuTemp: (): Promise<unknown> => ipcRenderer.invoke('system:cpuTemp'),
|
||||
systemRam: (): Promise<unknown> => ipcRenderer.invoke('system:ram'),
|
||||
systemGpu: (): Promise<unknown> => ipcRenderer.invoke('system:gpu'),
|
||||
});
|
||||
@@ -1,78 +0,0 @@
|
||||
const api = window.electronAPI;
|
||||
|
||||
// --- System Info ---
|
||||
async function refreshSystemInfo() {
|
||||
const [cpuTemp, ram, gpu] = await Promise.all([
|
||||
api.systemCpuTemp(),
|
||||
api.systemRam(),
|
||||
api.systemGpu(),
|
||||
]);
|
||||
|
||||
document.getElementById('cpu-temp').textContent =
|
||||
cpuTemp.error ? `Error: ${cpuTemp.error}` :
|
||||
cpuTemp.main ? `${cpuTemp.main}°C` : 'N/A';
|
||||
|
||||
document.getElementById('ram-usage').textContent =
|
||||
ram.error ? `Error: ${ram.error}` :
|
||||
`${ram.usedPercent}%`;
|
||||
|
||||
document.getElementById('gpu-temp').textContent =
|
||||
gpu.error ? `Error: ${gpu.error}` :
|
||||
gpu.length > 0 && gpu[0].temperatureGpu != null ? `${gpu[0].temperatureGpu}°C` :
|
||||
'N/A';
|
||||
}
|
||||
|
||||
document.getElementById('refresh-sysinfo').addEventListener('click', refreshSystemInfo);
|
||||
|
||||
// --- USB ---
|
||||
document.getElementById('scan-usb').addEventListener('click', async () => {
|
||||
const result = await api.usbListDevices();
|
||||
const list = document.getElementById('usb-list');
|
||||
list.innerHTML = '';
|
||||
if (result.error) {
|
||||
list.innerHTML = `<li>Error: ${result.error}</li>`;
|
||||
return;
|
||||
}
|
||||
if (result.length === 0) {
|
||||
list.innerHTML = '<li>No USB devices found</li>';
|
||||
return;
|
||||
}
|
||||
for (const dev of result) {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = dev.displayName;
|
||||
list.appendChild(li);
|
||||
}
|
||||
});
|
||||
|
||||
// --- Preferences ---
|
||||
document.getElementById('save-prefs').addEventListener('click', async () => {
|
||||
const interval = document.getElementById('refresh-interval').value;
|
||||
await api.prefsSet('refreshInterval', parseInt(interval, 10));
|
||||
});
|
||||
|
||||
document.getElementById('load-prefs').addEventListener('click', async () => {
|
||||
const interval = await api.prefsGet('refreshInterval');
|
||||
if (interval != null) {
|
||||
document.getElementById('refresh-interval').value = interval;
|
||||
}
|
||||
});
|
||||
|
||||
// --- File Picker ---
|
||||
document.getElementById('open-file').addEventListener('click', async () => {
|
||||
const result = await api.dialogOpenFile({
|
||||
filters: [{ name: 'All Files', extensions: ['*'] }],
|
||||
});
|
||||
document.getElementById('file-result').textContent =
|
||||
result.canceled ? 'Cancelled' : `Selected: ${result.filePaths.join(', ')}`;
|
||||
});
|
||||
|
||||
document.getElementById('save-file').addEventListener('click', async () => {
|
||||
const result = await api.dialogSaveFile({
|
||||
filters: [{ name: 'All Files', extensions: ['*'] }],
|
||||
});
|
||||
document.getElementById('file-result').textContent =
|
||||
result.canceled ? 'Cancelled' : `Save path: ${result.filePath}`;
|
||||
});
|
||||
|
||||
// Auto-refresh on load
|
||||
refreshSystemInfo();
|
||||
148
src/renderer.ts
Normal file
148
src/renderer.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
interface DeviceEntry {
|
||||
vendorId: number;
|
||||
productId: number;
|
||||
displayName: string;
|
||||
manufacturer: string;
|
||||
product: string;
|
||||
serialNumber: string;
|
||||
}
|
||||
|
||||
interface CpuTempResult {
|
||||
main: number;
|
||||
cores: number[];
|
||||
max: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface RamResult {
|
||||
total: number;
|
||||
free: number;
|
||||
used: number;
|
||||
usedPercent: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface GpuController {
|
||||
vendor: string;
|
||||
model: string;
|
||||
temperatureGpu: number | null;
|
||||
memoryUsed: number | null;
|
||||
memoryTotal: number | null;
|
||||
}
|
||||
|
||||
interface ElectronAPI {
|
||||
systemCpuTemp: () => Promise<CpuTempResult>;
|
||||
systemRam: () => Promise<RamResult>;
|
||||
systemGpu: () => Promise<GpuController[] | { error: string }>;
|
||||
usbListDevices: () => Promise<DeviceEntry[] | { error: string }>;
|
||||
usbConnect: (vendorId: number, productId: number) => Promise<{ success: boolean } | { error: string }>;
|
||||
prefsGet: (key?: string) => Promise<unknown>;
|
||||
prefsSet: (key: string, value: unknown) => Promise<unknown>;
|
||||
prefsDelete: (key: string) => Promise<unknown>;
|
||||
dialogOpenFile: (options?: unknown) => Promise<{ canceled: boolean; filePaths: string[] }>;
|
||||
dialogSaveFile: (options?: unknown) => Promise<{ canceled: boolean; filePath: string }>;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
electronAPI: ElectronAPI;
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
|
||||
const api: ElectronAPI = window.electronAPI;
|
||||
|
||||
function isError<T>(result: T | { error: string }): result is { error: string } {
|
||||
return result !== null && typeof result === 'object' && 'error' in result;
|
||||
}
|
||||
|
||||
function formatError(result: { error: string }): string {
|
||||
return `Error: ${result.error}`;
|
||||
}
|
||||
|
||||
// --- System Info ---
|
||||
async function refreshSystemInfo(): Promise<void> {
|
||||
const [cpuTemp, ram, gpu] = await Promise.all([
|
||||
api.systemCpuTemp(),
|
||||
api.systemRam(),
|
||||
api.systemGpu(),
|
||||
]);
|
||||
|
||||
const cpuEl = document.getElementById('cpu-temp');
|
||||
const ramEl = document.getElementById('ram-usage');
|
||||
const gpuEl = document.getElementById('gpu-temp');
|
||||
if (!cpuEl || !ramEl || !gpuEl) return;
|
||||
|
||||
cpuEl.textContent = isError(cpuTemp) ? formatError(cpuTemp) :
|
||||
cpuTemp.main ? `${cpuTemp.main}°C` : 'N/A';
|
||||
|
||||
ramEl.textContent = isError(ram) ? formatError(ram) :
|
||||
`${ram.usedPercent}%`;
|
||||
|
||||
gpuEl.textContent = isError(gpu) ? formatError(gpu) :
|
||||
gpu.length > 0 && gpu[0].temperatureGpu != null ? `${gpu[0].temperatureGpu}°C` :
|
||||
'N/A';
|
||||
}
|
||||
|
||||
document.getElementById('refresh-sysinfo')?.addEventListener('click', refreshSystemInfo);
|
||||
|
||||
// --- USB ---
|
||||
document.getElementById('scan-usb')?.addEventListener('click', async () => {
|
||||
const result = await api.usbListDevices();
|
||||
const list = document.getElementById('usb-list');
|
||||
if (!list) return;
|
||||
list.innerHTML = '';
|
||||
if (isError(result)) {
|
||||
list.innerHTML = `<li>${formatError(result)}</li>`;
|
||||
return;
|
||||
}
|
||||
if (result.length === 0) {
|
||||
list.innerHTML = '<li>No USB devices found</li>';
|
||||
return;
|
||||
}
|
||||
for (const dev of result) {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = dev.displayName;
|
||||
list.appendChild(li);
|
||||
}
|
||||
});
|
||||
|
||||
// --- Preferences ---
|
||||
document.getElementById('save-prefs')?.addEventListener('click', async () => {
|
||||
const intervalEl = document.getElementById('refresh-interval') as HTMLInputElement | null;
|
||||
if (!intervalEl) return;
|
||||
await api.prefsSet('refreshInterval', parseInt(intervalEl.value, 10));
|
||||
});
|
||||
|
||||
document.getElementById('load-prefs')?.addEventListener('click', async () => {
|
||||
const interval = await api.prefsGet('refreshInterval');
|
||||
const intervalEl = document.getElementById('refresh-interval') as HTMLInputElement | null;
|
||||
if (interval != null && intervalEl) {
|
||||
intervalEl.value = typeof interval === 'number' ? String(interval) : '';
|
||||
}
|
||||
});
|
||||
|
||||
// --- File Picker ---
|
||||
document.getElementById('open-file')?.addEventListener('click', async () => {
|
||||
const result = await api.dialogOpenFile({
|
||||
filters: [{ name: 'All Files', extensions: ['*'] }],
|
||||
});
|
||||
const fileEl = document.getElementById('file-result');
|
||||
if (!fileEl) return;
|
||||
fileEl.textContent =
|
||||
result.canceled ? 'Cancelled' : `Selected: ${result.filePaths.join(', ')}`;
|
||||
});
|
||||
|
||||
document.getElementById('save-file')?.addEventListener('click', async () => {
|
||||
const result = await api.dialogSaveFile({
|
||||
filters: [{ name: 'All Files', extensions: ['*'] }],
|
||||
});
|
||||
const fileEl = document.getElementById('file-result');
|
||||
if (!fileEl) return;
|
||||
fileEl.textContent =
|
||||
result.canceled ? 'Cancelled' : `Save path: ${result.filePath}`;
|
||||
});
|
||||
|
||||
// Auto-refresh on load
|
||||
void refreshSystemInfo();
|
||||
22
tsconfig.json
Normal file
22
tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "commonjs",
|
||||
"outDir": "build",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"types": ["node"],
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitReturns": true
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user