Convert to use typescript, implement eslint
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,4 +1,5 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
|
build/
|
||||||
dist/
|
dist/
|
||||||
out/
|
out/
|
||||||
release/
|
release/
|
||||||
|
|||||||
33
README.md
33
README.md
@@ -18,7 +18,7 @@ cd corsair-lcd-control
|
|||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
`postinstall` automatically rebuilds native modules for Electron using `@electron/rebuild`.
|
`postinstall` rebuilds native USB modules for Electron.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
@@ -26,13 +26,15 @@ npm install
|
|||||||
npm start
|
npm start
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Compiles TypeScript to `build/`, then launches Electron with the compiled output.
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
Produces platform-specific packages:
|
Compiles TypeScript, then produces platform-specific packages via electron-builder:
|
||||||
|
|
||||||
| Platform | Format |
|
| Platform | Format |
|
||||||
|----------|--------|
|
|----------|--------|
|
||||||
@@ -40,6 +42,14 @@ Produces platform-specific packages:
|
|||||||
| macOS | `.dmg` |
|
| macOS | `.dmg` |
|
||||||
| Windows | `.exe` (NSIS) |
|
| Windows | `.exe` (NSIS) |
|
||||||
|
|
||||||
|
## Lint
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run lint
|
||||||
|
```
|
||||||
|
|
||||||
|
Runs ESLint with `typescript-eslint` (`recommendedTypeChecked`) across all `src/` TypeScript files.
|
||||||
|
|
||||||
## Rebuild Native Modules
|
## Rebuild Native Modules
|
||||||
|
|
||||||
If native modules fail to load after a Node.js or Electron upgrade:
|
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
|
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
|
## 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)
|
- **System Tray** — background process with context menu (Show/Hide/Quit)
|
||||||
- **Preferences** — persisted via `electron-store` (JSON)
|
- **Preferences** — persisted via `electron-store` (JSON)
|
||||||
- **File Pickers** — native OS dialogs via Electron's `dialog` API
|
- **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>",
|
"author": "xerotacovix <xerotacovix@proton.me>",
|
||||||
"homepage": "https://github.com/xerotacovix/corsair-lcd-control",
|
"homepage": "https://github.com/xerotacovix/corsair-lcd-control",
|
||||||
"description": "Electron app to control Corsair LCD displays",
|
"description": "Electron app to control Corsair LCD displays",
|
||||||
"main": "main.js",
|
"main": "build/main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "electron .",
|
"build:ts": "tsc && cp src/index.html src/styles.css build/",
|
||||||
"build": "electron-builder",
|
"start": "npm run build:ts && electron .",
|
||||||
|
"build": "npm run build:ts && electron-builder",
|
||||||
"postinstall": "CXXFLAGS='-std=c++17' npx @electron/rebuild",
|
"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": {
|
"build": {
|
||||||
"appId": "com.corsair-lcd-control.app",
|
"appId": "com.corsair-lcd-control.app",
|
||||||
"productName": "Corsair LCD Control",
|
"productName": "Corsair LCD Control",
|
||||||
"files": [
|
"files": [
|
||||||
"main.js",
|
"build/**/*",
|
||||||
"preload.js",
|
"assets/**/*"
|
||||||
"src/**/*"
|
|
||||||
],
|
],
|
||||||
"linux": {
|
"linux": {
|
||||||
"target": [
|
"target": [
|
||||||
@@ -42,8 +43,16 @@
|
|||||||
"usb": "^2.9.0"
|
"usb": "^2.9.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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": "^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": {
|
"allowScripts": {
|
||||||
"usb@2.18.0": true,
|
"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">
|
<section id="preferences-section">
|
||||||
<h2>Preferences</h2>
|
<h2>Preferences</h2>
|
||||||
<div>
|
<div>
|
||||||
<label>Refresh Interval (ms):</label>
|
<label for="refresh-interval">Refresh Interval (ms):</label>
|
||||||
<input type="number" id="refresh-interval" value="5000" />
|
<input type="number" id="refresh-interval" value="5000" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -1,38 +1,34 @@
|
|||||||
const { app, BrowserWindow, ipcMain, Tray, Menu, nativeImage, dialog } = require('electron');
|
import { app, BrowserWindow, ipcMain, Tray, Menu, nativeImage, dialog } from 'electron';
|
||||||
const path = require('path');
|
import path = require('path');
|
||||||
const Store = require('electron-store');
|
import Store from 'electron-store';
|
||||||
const si = require('systeminformation');
|
import si = require('systeminformation');
|
||||||
const usb = require('usb');
|
import usb = require('usb');
|
||||||
|
|
||||||
const store = new Store({ defaults: { preferences: {} } });
|
interface DeviceEntry {
|
||||||
|
vendorId: number;
|
||||||
let mainWindow = null;
|
productId: number;
|
||||||
let tray = null;
|
displayName: string;
|
||||||
|
manufacturer: string;
|
||||||
function createTrayIcon() {
|
product: string;
|
||||||
const size = 16;
|
serialNumber: string;
|
||||||
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 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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({
|
mainWindow = new BrowserWindow({
|
||||||
width: 900,
|
width: 900,
|
||||||
height: 600,
|
height: 600,
|
||||||
@@ -42,29 +38,29 @@ function createWindow() {
|
|||||||
nodeIntegration: false,
|
nodeIntegration: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
mainWindow.loadFile(path.join(__dirname, 'src', 'index.html'));
|
void mainWindow.loadFile(path.join(__dirname, 'index.html'));
|
||||||
mainWindow.on('close', (e) => {
|
mainWindow.on('close', (e: Electron.Event) => {
|
||||||
if (!app.isQuitting) {
|
if (!isQuitting) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
mainWindow.hide();
|
mainWindow!.hide();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function createTray() {
|
function createTray(): void {
|
||||||
tray = new Tray(createTrayIcon());
|
tray = new Tray(createTrayIcon());
|
||||||
tray.setToolTip('Corsair LCD Control');
|
tray.setToolTip('Corsair LCD Control');
|
||||||
const contextMenu = Menu.buildFromTemplate([
|
const contextMenu = Menu.buildFromTemplate([
|
||||||
{ label: 'Show', click: () => mainWindow.show() },
|
{ label: 'Show', click: () => { mainWindow!.show(); } },
|
||||||
{ label: 'Hide', click: () => mainWindow.hide() },
|
{ label: 'Hide', click: () => { mainWindow!.hide(); } },
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{ label: 'Quit', click: () => { app.isQuitting = true; app.quit(); } },
|
{ label: 'Quit', click: () => { isQuitting = true; app.quit(); } },
|
||||||
]);
|
]);
|
||||||
tray.setContextMenu(contextMenu);
|
tray.setContextMenu(contextMenu);
|
||||||
tray.on('double-click', () => mainWindow.show());
|
tray.on('double-click', () => { mainWindow!.show(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
const VENDORS = {
|
const VENDORS: VendorMap = {
|
||||||
0x1B1C: 'Corsair',
|
0x1B1C: 'Corsair',
|
||||||
0x046D: 'Logitech',
|
0x046D: 'Logitech',
|
||||||
0x0A5C: 'Broadcom',
|
0x0A5C: 'Broadcom',
|
||||||
@@ -77,18 +73,18 @@ const VENDORS = {
|
|||||||
0x5964: 'Asmedia',
|
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 hex = `${vendorId.toString(16).padStart(4, '0')}:${productId.toString(16).padStart(4, '0')}`;
|
||||||
const name = VENDORS[vendorId] || '';
|
const name = VENDORS[vendorId] || '';
|
||||||
return name ? `${name} [${hex}]` : hex;
|
return name ? `${name} [${hex}]` : hex;
|
||||||
}
|
}
|
||||||
|
|
||||||
function readStringDescriptor(device, index) {
|
function readStringDescriptor(device: usb.Device, index: number): Promise<string> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (!index) return resolve('');
|
if (!index) { resolve(''); return; }
|
||||||
try {
|
try {
|
||||||
device.getStringDescriptor(index, (err, desc) => {
|
device.getStringDescriptor(index, (err: usb.LibUSBException | undefined, desc?: string) => {
|
||||||
resolve(err ? '' : desc);
|
resolve(err ? '' : desc ?? '');
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
resolve('');
|
resolve('');
|
||||||
@@ -98,12 +94,12 @@ function readStringDescriptor(device, index) {
|
|||||||
|
|
||||||
// --- USB ---
|
// --- USB ---
|
||||||
|
|
||||||
ipcMain.handle('usb:listDevices', async () => {
|
ipcMain.handle('usb:listDevices', async (): Promise<DeviceEntry[] | { error: string }> => {
|
||||||
try {
|
try {
|
||||||
const devices = usb.getDeviceList();
|
const devices = usb.getDeviceList();
|
||||||
const results = [];
|
const results: DeviceEntry[] = [];
|
||||||
for (const d of devices) {
|
for (const d of devices) {
|
||||||
const entry = {
|
const entry: DeviceEntry = {
|
||||||
vendorId: d.deviceDescriptor.idVendor,
|
vendorId: d.deviceDescriptor.idVendor,
|
||||||
productId: d.deviceDescriptor.idProduct,
|
productId: d.deviceDescriptor.idProduct,
|
||||||
displayName: formatHexId(d.deviceDescriptor.idVendor, d.deviceDescriptor.idProduct),
|
displayName: formatHexId(d.deviceDescriptor.idVendor, d.deviceDescriptor.idProduct),
|
||||||
@@ -132,58 +128,61 @@ ipcMain.handle('usb:listDevices', async () => {
|
|||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
} catch (err) {
|
} 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 {
|
try {
|
||||||
const device = usb.findByIds(vendorId, productId);
|
const device = usb.findByIds(vendorId, productId);
|
||||||
if (!device) throw new Error('Device not found');
|
if (!device) throw new Error('Device not found');
|
||||||
device.open();
|
device.open(true);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return { error: err.message };
|
return { error: (err as Error).message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Preferences ---
|
// --- Preferences ---
|
||||||
ipcMain.handle('prefs:get', (_e, key) => {
|
|
||||||
|
ipcMain.handle('prefs:get', (_e: Electron.IpcMainInvokeEvent, key?: string) => {
|
||||||
if (key) return store.get(key);
|
if (key) return store.get(key);
|
||||||
return store.store;
|
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);
|
store.set(key, value);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('prefs:delete', (_e, key) => {
|
ipcMain.handle('prefs:delete', (_e: Electron.IpcMainInvokeEvent, key: string) => {
|
||||||
store.delete(key);
|
store.delete(key);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- File Picker ---
|
// --- 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'],
|
properties: ['openFile'],
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('dialog:saveFile', async (_e, options) => {
|
ipcMain.handle('dialog:saveFile', async (_e: Electron.IpcMainInvokeEvent, options?: Electron.SaveDialogOptions) => {
|
||||||
const result = await dialog.showSaveDialog(mainWindow, options);
|
const result = await dialog.showSaveDialog(mainWindow!, options ?? {});
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- System Info ---
|
// --- System Info ---
|
||||||
|
|
||||||
ipcMain.handle('system:cpuTemp', async () => {
|
ipcMain.handle('system:cpuTemp', async () => {
|
||||||
try {
|
try {
|
||||||
const data = await si.cpuTemperature();
|
const data = await si.cpuTemperature();
|
||||||
return data;
|
return data;
|
||||||
} catch (err) {
|
} 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),
|
usedPercent: ((data.used / data.total) * 100).toFixed(1),
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return { error: err.message };
|
return { error: (err as Error).message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -213,11 +212,11 @@ ipcMain.handle('system:gpu', async () => {
|
|||||||
}));
|
}));
|
||||||
return controllers;
|
return controllers;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return { error: err.message };
|
return { error: (err as Error).message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.whenReady().then(() => {
|
void app.whenReady().then(() => {
|
||||||
createWindow();
|
createWindow();
|
||||||
createTray();
|
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