Initial code commit for project

This commit is contained in:
2026-06-09 02:29:12 -06:00
parent 78fb9ad2ac
commit d39e202de2
14 changed files with 4583 additions and 0 deletions

14
.gitignore vendored Normal file
View File

@@ -0,0 +1,14 @@
node_modules/
dist/
out/
release/
.DS_Store
Thumbs.db
*.log
.env
.env.local
npm-debug.log*
yarn-error.log*
yarn-debug.log*

10
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,10 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Ignored default folder with query files
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

8
.idea/corsair-lcd-control.iml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/corsair-lcd-control.iml" filepath="$PROJECT_DIR$/.idea/corsair-lcd-control.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@@ -1,2 +1,59 @@
# corsair-lcd-control # corsair-lcd-control
Electron app to control Corsair LCD displays. Monitors system stats (CPU temp, RAM, GPU temp), detects USB devices, and manages preferences.
## Prerequisites
- [Node.js](https://nodejs.org/) >= 18
- npm >= 9
- **Linux**: `libusb-1.0-0-dev`, `build-essential`, `python3`
- **macOS**: Xcode Command Line Tools (`xcode-select --install`)
- **Windows**: Build Tools for Visual Studio
## Setup
```bash
git clone <repo-url>
cd corsair-lcd-control
npm install
```
`postinstall` automatically rebuilds native modules for Electron using `@electron/rebuild`.
## Development
```bash
npm start
```
## Build
```bash
npm run build
```
Produces platform-specific packages:
| Platform | Format |
|----------|--------|
| Linux | `.AppImage`, `.deb` |
| macOS | `.dmg` |
| Windows | `.exe` (NSIS) |
## Rebuild Native Modules
If native modules fail to load after a Node.js or Electron upgrade:
```bash
npm run rebuild
```
## Capabilities
- **USB** — `usb` module enumerates and connects to devices
- **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
- **CPU Temperature** — `systeminformation.cpuTemperature()`
- **RAM Usage** — `systeminformation.mem()`
- **GPU Temperature** — `systeminformation.graphics()`

BIN
cat.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

231
main.js Normal file
View File

@@ -0,0 +1,231 @@
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');
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 });
}
function createWindow() {
mainWindow = new BrowserWindow({
width: 900,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
},
});
mainWindow.loadFile(path.join(__dirname, 'src', 'index.html'));
mainWindow.on('close', (e) => {
if (!app.isQuitting) {
e.preventDefault();
mainWindow.hide();
}
});
}
function createTray() {
tray = new Tray(createTrayIcon());
tray.setToolTip('Corsair LCD Control');
const contextMenu = Menu.buildFromTemplate([
{ label: 'Show', click: () => mainWindow.show() },
{ label: 'Hide', click: () => mainWindow.hide() },
{ type: 'separator' },
{ label: 'Quit', click: () => { app.isQuitting = true; app.quit(); } },
]);
tray.setContextMenu(contextMenu);
tray.on('double-click', () => mainWindow.show());
}
const VENDORS = {
0x1B1C: 'Corsair',
0x046D: 'Logitech',
0x0A5C: 'Broadcom',
0x8087: 'Intel',
0x05E3: 'Genesys Logic',
0x04F2: 'Chicony',
0x0BDA: 'Realtek',
0x1D6B: 'Linux Foundation',
0x3034: 'Realtek',
0x5964: 'Asmedia',
};
function formatHexId(vendorId, productId) {
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) {
return new Promise((resolve) => {
if (!index) return resolve('');
try {
device.getStringDescriptor(index, (err, desc) => {
resolve(err ? '' : desc);
});
} catch {
resolve('');
}
});
}
// --- USB ---
ipcMain.handle('usb:listDevices', async () => {
try {
const devices = usb.getDeviceList();
const results = [];
for (const d of devices) {
const entry = {
vendorId: d.deviceDescriptor.idVendor,
productId: d.deviceDescriptor.idProduct,
displayName: formatHexId(d.deviceDescriptor.idVendor, d.deviceDescriptor.idProduct),
manufacturer: '',
product: '',
serialNumber: '',
};
try {
d.open(true);
const [mfg, prod, sn] = await Promise.all([
readStringDescriptor(d, d.deviceDescriptor.iManufacturer),
readStringDescriptor(d, d.deviceDescriptor.iProduct),
readStringDescriptor(d, d.deviceDescriptor.iSerialNumber),
]);
if (mfg || prod) {
entry.manufacturer = mfg;
entry.product = prod;
entry.displayName = [mfg, prod].filter(Boolean).join(' ');
}
entry.serialNumber = sn;
d.close();
} catch {
// no permission — keep the hex-based displayName
}
results.push(entry);
}
return results;
} catch (err) {
return { error: err.message };
}
});
ipcMain.handle('usb:connect', async (_e, { vendorId, productId }) => {
try {
const device = usb.findByIds(vendorId, productId);
if (!device) throw new Error('Device not found');
device.open();
return { success: true };
} catch (err) {
return { error: err.message };
}
});
// --- Preferences ---
ipcMain.handle('prefs:get', (_e, key) => {
if (key) return store.get(key);
return store.store;
});
ipcMain.handle('prefs:set', (_e, key, value) => {
store.set(key, value);
return { success: true };
});
ipcMain.handle('prefs:delete', (_e, key) => {
store.delete(key);
return { success: true };
});
// --- File Picker ---
ipcMain.handle('dialog:openFile', async (_e, options) => {
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);
return result;
});
// --- System Info ---
ipcMain.handle('system:cpuTemp', async () => {
try {
const data = await si.cpuTemperature();
return data;
} catch (err) {
return { error: err.message };
}
});
ipcMain.handle('system:ram', async () => {
try {
const data = await si.mem();
return {
total: data.total,
free: data.free,
used: data.used,
usedPercent: ((data.used / data.total) * 100).toFixed(1),
};
} catch (err) {
return { error: err.message };
}
});
ipcMain.handle('system:gpu', async () => {
try {
const data = await si.graphics();
const controllers = data.controllers.map((c) => ({
vendor: c.vendor,
model: c.model,
temperatureGpu: c.temperatureGpu,
memoryUsed: c.memoryUsed,
memoryTotal: c.memoryTotal,
}));
return controllers;
} catch (err) {
return { error: err.message };
}
});
app.whenReady().then(() => {
createWindow();
createTray();
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});

3922
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

52
package.json Normal file
View File

@@ -0,0 +1,52 @@
{
"name": "corsair-lcd-control",
"version": "0.1.0",
"author": "xerotacovix <xerotacovix@proton.me>",
"homepage": "https://github.com/xerotacovix/corsair-lcd-control",
"description": "Electron app to control Corsair LCD displays",
"main": "main.js",
"scripts": {
"start": "electron .",
"build": "electron-builder",
"postinstall": "CXXFLAGS='-std=c++17' npx @electron/rebuild",
"rebuild": "CXXFLAGS='-std=c++17' npx @electron/rebuild"
},
"build": {
"appId": "com.corsair-lcd-control.app",
"productName": "Corsair LCD Control",
"files": [
"main.js",
"preload.js",
"src/**/*"
],
"linux": {
"target": [
"AppImage",
"deb"
]
},
"mac": {
"target": [
"dmg"
]
},
"win": {
"target": [
"nsis"
]
}
},
"dependencies": {
"electron-store": "^8.1.0",
"systeminformation": "^5.21.0",
"usb": "^2.9.0"
},
"devDependencies": {
"electron": "^42.3.3",
"electron-builder": "^24.9.0"
},
"allowScripts": {
"usb@2.18.0": true,
"electron@42.3.3": true
}
}

21
preload.js Normal file
View File

@@ -0,0 +1,21 @@
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'),
});

63
src/index.html Normal file
View File

@@ -0,0 +1,63 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Corsair LCD Control</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<div class="app">
<header>
<h1>Corsair LCD Control</h1>
</header>
<main>
<section id="system-info">
<h2>System Monitoring</h2>
<div class="info-grid">
<div class="card">
<h3>CPU Temperature</h3>
<p id="cpu-temp">--</p>
</div>
<div class="card">
<h3>RAM Usage</h3>
<p id="ram-usage">--</p>
</div>
<div class="card">
<h3>GPU Temperature</h3>
<p id="gpu-temp">--</p>
</div>
</div>
<button id="refresh-sysinfo">Refresh</button>
</section>
<section id="usb-section">
<h2>USB Devices</h2>
<button id="scan-usb">Scan USB Devices</button>
<ul id="usb-list"></ul>
</section>
<section id="preferences-section">
<h2>Preferences</h2>
<div>
<label>Refresh Interval (ms):</label>
<input type="number" id="refresh-interval" value="5000" />
</div>
<div>
<button id="save-prefs">Save Preferences</button>
<button id="load-prefs">Load Preferences</button>
</div>
</section>
<section id="file-section">
<h2>File Picker</h2>
<button id="open-file">Open File</button>
<button id="save-file">Save File</button>
<p id="file-result"></p>
</section>
</main>
</div>
<script src="renderer.js"></script>
</body>
</html>

78
src/renderer.js Normal file
View File

@@ -0,0 +1,78 @@
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();

113
src/styles.css Normal file
View File

@@ -0,0 +1,113 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #1a1a2e;
color: #e0e0e0;
padding: 20px;
}
.app {
max-width: 800px;
margin: 0 auto;
}
header {
margin-bottom: 30px;
}
header h1 {
color: #0078ff;
font-size: 1.5rem;
}
section {
background: #16213e;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
section h2 {
font-size: 1.1rem;
margin-bottom: 15px;
color: #a0a0c0;
}
.info-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
margin-bottom: 15px;
}
.card {
background: #0f3460;
border-radius: 6px;
padding: 15px;
text-align: center;
}
.card h3 {
font-size: 0.85rem;
color: #8888aa;
margin-bottom: 8px;
}
.card p {
font-size: 1.4rem;
font-weight: bold;
color: #0078ff;
}
button {
background: #0078ff;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
margin-right: 8px;
}
button:hover {
background: #005fcc;
}
#usb-list {
list-style: none;
margin-top: 10px;
}
#usb-list li {
padding: 8px;
background: #0f3460;
border-radius: 4px;
margin-bottom: 5px;
font-size: 0.85rem;
}
#file-result {
margin-top: 10px;
font-size: 0.85rem;
color: #8888aa;
word-break: break-all;
}
label {
margin-right: 10px;
}
input[type="number"] {
background: #0f3460;
border: 1px solid #333;
color: #e0e0e0;
padding: 6px 10px;
border-radius: 4px;
margin-bottom: 10px;
}