diff --git a/.gitignore b/.gitignore
index 42a7f05..de5c299 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
#Folders
/node_modules/
+/.venv/
#Files
-/config.json
\ No newline at end of file
+/config.json
diff --git a/arduino_thread.js b/arduino_thread.js
deleted file mode 100644
index b406ed6..0000000
--- a/arduino_thread.js
+++ /dev/null
@@ -1,170 +0,0 @@
-const { SerialPort } = require('serialport');
-const fs = require('fs');
-const EventEmitter = require('events');
-// add parser import
-const { ReadlineParser } = require('@serialport/parser-readline');
-
-class ArduinoThread extends EventEmitter {
- constructor(comPort, baudRate = 9600, sliderCount = 3, yamlPath = 'config.json', verbose = false) {
- super();
-
- if (typeof comPort !== 'string') throw new Error('comPort must be a string');
- if (typeof baudRate !== 'number') throw new Error('baudRate must be a number');
- if (typeof sliderCount !== 'number') throw new Error('sliderCount must be a number');
-
- this.port = new SerialPort({ path: comPort, baudRate, autoOpen: true });
- this.sliderCount = sliderCount;
- this.yamlPath = yamlPath; // now points to config.json by default
- this.verbose = !!verbose;
-
- this.sendBuffer = [];
- this.updateList = Array(sliderCount).fill(0);
- this.isRunning = true;
- this.pendingMessage = null;
-
- // pipe through a newline-based parser so we always get full lines
- try {
- this.parser = this.port.pipe(new ReadlineParser({ delimiter: '\n' }));
- this.parser.on('data', (line) => this._handleData(line));
- } catch (err) {
- // fallback to raw data event if parser unavailable
- this.port.on('data', (data) => this._handleData(data));
- }
-
- // keep error/close handlers on the port
- this.port.on('error', (err) => {
- this.emit('error', err);
- if (this.verbose) console.error('Serial error:', err);
- });
- this.port.on('close', () => {
- this.isRunning = false;
- if (this.verbose) console.log('Serial port closed');
- });
- }
-
- _handleData(data) {
- // accept string or Buffer; trim newline/whitespace
- const input = (typeof data === 'string') ? data.trim() : data.toString().trim();
-
- // acknowledgement handling
- if (input === 'OK' && this.pendingMessage) {
- if (this.verbose) console.log(`Message confirmed: ${this.pendingMessage}`);
- this.pendingMessage = null;
- // drain buffer if present
- if (this.sendBuffer.length > 0) {
- const next = this.sendBuffer.shift();
- this._writeMessage(next);
- }
- return;
- }
-
- const numbers = input.split('|');
- if (numbers.length === this.sliderCount) {
- try {
- const values = numbers.map((n) => parseInt(n, 10));
- values.forEach((val, i) => {
- if (!Number.isFinite(val)) throw new Error('parse error');
- if (val !== this.updateList[i]) {
- this._setVolume(val, i);
- }
- });
- this.updateList = values;
- this.emit('update', values); // notify listeners (GUI)
- if (this.verbose) console.log('Slider update ->', values);
- } catch (err) {
- this.emit('warn', { msg: 'Invalid slider data', raw: input });
- if (this.verbose) console.warn('Invalid slider data:', input);
- }
- } else {
- // Not an update; emit for consumers
- this.emit('raw', input);
- if (this.verbose) console.warn('Unexpected input:', input);
- }
- }
-
- send(message) {
- if (!message || typeof message !== 'string') return;
- if (this.pendingMessage) {
- // queue if another message is awaiting ACK
- this.sendBuffer.push(message);
- return;
- }
- this._writeMessage(message);
- }
-
- _writeMessage(message) {
- if (!this.port || !this.port.writable) {
- const err = new Error('Serial port not writable');
- this.emit('error', err);
- if (this.verbose) console.error(err);
- return;
- }
-
- this.pendingMessage = message;
- this.port.write(message + '\n', (err) => {
- if (err) {
- this.emit('error', err);
- if (this.verbose) console.error('Error writing:', err.message);
- this.pendingMessage = null;
- } else if (this.verbose) {
- console.log('Wrote message:', message);
- }
- });
-
- // fallback timeout if device never replies OK
- setTimeout(() => {
- if (this.pendingMessage) {
- this.emit('warn', { msg: "Did not receive OK from device", message: this.pendingMessage });
- if (this.verbose) console.warn('Did not receive OK from device for', this.pendingMessage);
- this.pendingMessage = null;
- // try to drain next queued message if any
- if (this.sendBuffer.length > 0) {
- const next = this.sendBuffer.shift();
- this._writeMessage(next);
- }
- }
- }, 100); // adjustable timeout
- }
-
- _setVolume(value, sliderNumber) {
- const normalized = value / 1023;
-
- if (!fs.existsSync(this.yamlPath)) {
- this.emit('warn', { msg: 'Config file missing', path: this.yamlPath });
- if (this.verbose) console.warn('Config file missing:', this.yamlPath);
- return;
- }
-
- try {
- const file = fs.readFileSync(this.yamlPath, 'utf8');
- const config = JSON.parse(file || '{}'); // parse JSON config.json
- const processes = config[sliderNumber] || [];
-
- processes.forEach((proc) => {
- // Emit an event so the application can perform the platform-specific change.
- this.emit('set-volume', { process: proc, value: normalized, slider: sliderNumber });
- if (this.verbose) {
- console.log(`Would set volume for "${proc}" to ${normalized.toFixed(2)} (slider ${sliderNumber})`);
- }
- });
- } catch (err) {
- this.emit('error', err);
- if (this.verbose) console.error('Config JSON error:', err);
- }
- }
-
- stop() {
- this.isRunning = false;
- try {
- if (this.port && this.port.isOpen) this.port.close();
- } catch (e) {
- if (this.verbose) console.warn('Error closing port', e);
- }
- }
-
- getValues() {
- return this.updateList.slice();
- }
-}
-
-module.exports = ArduinoThread;
diff --git a/index.html b/index.html
deleted file mode 100644
index 89ec91a..0000000
--- a/index.html
+++ /dev/null
@@ -1,543 +0,0 @@
-
-
-
-
-
- Dreckshub
-
-
-
-
-
-
-
-
-
-
-
-
-
Makropad — Main
-
Content for Makropad on the first tab.
-
-
-
-
-
Makropad Ghostwire — Main
-
Alternate content for Makropad Ghostwire on the first tab.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Settings
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Placeholder B
- Content for placeholder B.
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/main.js b/main.js
deleted file mode 100644
index 4bb34cc..0000000
--- a/main.js
+++ /dev/null
@@ -1,708 +0,0 @@
-const { app, BrowserWindow, ipcMain, Tray, Menu, nativeImage } = require("electron");
-const fs = require("fs");
-const path = require("path");
-const { exec } = require("child_process");
-const { execFile } = require("child_process");
-const { spawn } = require("child_process");
-
-// add ArduinoThread module and instance holder
-const ArduinoThread = require("./arduino_thread");
-let arduinoInstance = null;
-
-// NEW: persistent python server helpers (globals)
-let pythonServer = null;
-let pythonServerStdoutBuffer = "";
-let pythonServerReady = false;
-
-let mainWindow;
-let tray = null;
-let isQuiting = false;
-const tagsFilePath = path.join(__dirname, "config.json");
-
-// load a single app/tray icon to reuse (tray.png expected next to index.html/main.js)
-const iconPath = path.join(__dirname, "tray.png");
-let appIcon;
-try {
- if (fs.existsSync(iconPath)) {
- const img = nativeImage.createFromPath(iconPath);
- // prefer a multi-size icon where possible; keep as-is if empty
- appIcon = img && !img.isEmpty() ? img : undefined;
- }
-} catch (e) {
- appIcon = undefined;
-}
-
-function readConfig() {
- try {
- if (fs.existsSync(tagsFilePath)) {
- return JSON.parse(fs.readFileSync(tagsFilePath, "utf8"));
- }
- } catch (err) {
- console.error("readConfig error:", err);
- }
- return {};
-}
-
-function writeConfig(obj) {
- try {
- fs.writeFileSync(tagsFilePath, JSON.stringify(obj, null, 2));
- } catch (err) {
- console.error("writeConfig error:", err);
- }
-}
-
-// helper to run a command and return stdout
-function execPromise(cmd) {
- return new Promise((resolve, reject) => {
- exec(cmd, { encoding: "utf8", windowsHide: true }, (err, stdout, stderr) => {
- if (err) return reject(err);
- resolve(stdout || "");
- });
- });
-}
-
-// --- NEW: tray helpers ---
-function createTray() {
- try {
- if (tray) return;
- // use the shared appIcon if available
- tray = new Tray(appIcon || undefined);
- const contextMenu = Menu.buildFromTemplate([
- {
- label: "Show",
- click: () => {
- if (mainWindow) {
- mainWindow.show();
- mainWindow.focus();
- }
- }
- },
- {
- label: "Quit",
- click: () => {
- // user explicitly wants to quit
- isQuiting = true;
- app.quit();
- }
- }
- ]);
- tray.setToolTip("Dreckshub");
- tray.setContextMenu(contextMenu);
- tray.on("double-click", () => {
- if (mainWindow) {
- mainWindow.show();
- mainWindow.focus();
- }
- });
- } catch (err) {
- console.error("createTray failed:", err);
- }
-}
-
-function destroyTray() {
- try {
- if (tray) {
- tray.destroy();
- tray = null;
- }
- } catch (err) {
- console.error("destroyTray failed:", err);
- }
-}
-
-/* Ensure tray state matches config.closeToTray */
-function updateTrayState(enable) {
- if (enable) {
- createTray();
- } else {
- destroyTray();
- }
-}
-
-app.whenReady().then(() => {
- // set macOS dock icon early so the app uses tray.png instead of default electron icon
- if (process.platform === "darwin" && appIcon) {
- try { app.dock && app.dock.setIcon && app.dock.setIcon(appIcon); } catch (err) { /* ignore */ }
- }
-
- // restore saved window bounds if present
- const cfg = readConfig();
- const wb = cfg.windowBounds || null;
- const winOptions = {
- // start with the typical fixed size used previously
- width: 1466,
- height: 642,
- // prevent user resizing
- resizable: false,
- // set window/taskbar icon (Windows/Linux)
- icon: appIcon || undefined,
- webPreferences: {
- preload: path.join(__dirname, "preload.js"),
- },
- autoHideMenuBar: true,
- };
-
- if (wb && typeof wb.width === "number" && typeof wb.height === "number") {
- winOptions.width = wb.width;
- winOptions.height = wb.height;
- if (typeof wb.x === "number" && typeof wb.y === "number") {
- winOptions.x = wb.x;
- winOptions.y = wb.y;
- }
- }
-
- mainWindow = new BrowserWindow(winOptions);
- // window is non-resizable; no minimum size call needed
-
- mainWindow.loadFile("index.html");
-
- // after content loads, apply maximized state if saved
- mainWindow.webContents.once("did-finish-load", () => {
- try {
- if (wb && wb.isMaximized) {
- mainWindow.maximize();
- }
- } catch (err) {
- console.error("apply maximize failed:", err);
- }
- });
-
- // create tray if setting enabled in config
- updateTrayState(Boolean(cfg && cfg.closeToTray));
-
- // save bounds when window is closing
- mainWindow.on("close", (e) => {
- try {
- // If close-to-tray is enabled and user didn't explicitly choose to quit, hide instead of closing
- const cfgNow = readConfig();
- const useTray = Boolean(cfgNow && cfgNow.closeToTray);
- if (useTray && !isQuiting) {
- e.preventDefault();
- if (mainWindow) mainWindow.hide();
- return;
- }
-
- // allow closing — save bounds as before
- // getBounds returns the un-maximized bounds; good for restoring windowed state
- const bounds = mainWindow.getBounds();
- const isMax = mainWindow.isMaximized();
- const curCfg = readConfig();
- curCfg.windowBounds = {
- x: bounds.x,
- y: bounds.y,
- width: bounds.width,
- height: bounds.height,
- isMaximized: isMax,
- };
- writeConfig(curCfg);
- } catch (err) {
- console.error("save window bounds failed:", err);
- }
- });
-
- mainWindow.on("closed", () => {
- mainWindow = null;
- });
-});
-
-ipcMain.on("save-tags", (event, data) => {
- // merge incoming tag data with existing config so windowBounds (and future fields) are preserved
- try {
- const cfg = readConfig() || {};
- // If data is an array => legacy behavior for slider0
- if (Array.isArray(data)) {
- cfg.slider0 = data;
- } else if (data && typeof data === "object") {
- // copy all keys provided (slider0, slider1, slider2, sliderAssignments, etc.)
- Object.keys(data).forEach((k) => {
- cfg[k] = data[k];
- });
- }
- writeConfig(cfg);
-
- // react to closeToTray changes immediately
- try {
- updateTrayState(Boolean(cfg.closeToTray));
- } catch (err) {
- console.error("updateTrayState failed after save-tags:", err);
- }
- } catch (err) {
- console.error("save-tags merge failed:", err);
- }
-});
-
-ipcMain.handle("load-tags", () => {
- const cfg = readConfig() || {};
- // ensure defaults for all three sliders for backward compatibility
- if (!Array.isArray(cfg.slider0)) cfg.slider0 = [];
- if (!Array.isArray(cfg.slider1)) cfg.slider1 = [];
- if (!Array.isArray(cfg.slider2)) cfg.slider2 = [];
- return cfg;
-});
-
-ipcMain.handle("list-serial-ports", async () => {
- try {
- // Windows: prefer .NET SerialPort.GetPortNames() via PowerShell (reliable, returns COM names)
- if (process.platform === "win32") {
- try {
- const psCmd = `powershell -NoProfile -Command "[System.IO.Ports.SerialPort]::GetPortNames() | ForEach-Object { $_ }"`;
- const out = await execPromise(psCmd);
- const lines = out.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
- if (lines.length > 0) {
- // Ensure unique, return objects with label=value
- const uniq = Array.from(new Set(lines));
- return uniq.map(p => ({ value: p, label: p }));
- }
-
- // Fallback 1: Get-PnpDevice friendly names containing (COMx)
- const psFallback = `powershell -NoProfile -Command "Get-PnpDevice -Status OK | Where-Object { $_.FriendlyName -match '\\(COM\\d+\\)' -or $_.Name -match '\\(COM\\d+\\)' } | ForEach-Object { ($_.FriendlyName -or $_.Name) }"`;
- const out2 = await execPromise(psFallback);
- const lines2 = out2.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
- const ports = [];
- const re = /(COM\d+)/i;
- for (const l of lines2) {
- const m = l.match(re);
- if (m) ports.push({ value: m[1], label: l });
- }
- if (ports.length > 0) {
- // dedupe by value
- const seen = new Set();
- return ports.filter(p => {
- if (seen.has(p.value)) return false;
- seen.add(p.value);
- return true;
- });
- }
-
- // Fallback 2: 'mode' command parsing
- const out3 = await execPromise("mode");
- const lines3 = out3.split(/\r?\n/);
- const reMode = /(COM\d+)/gi;
- const portsMode = [];
- for (const l of lines3) {
- const m = l.match(reMode);
- if (m) {
- for (const token of m) portsMode.push({ value: token, label: token });
- }
- }
- return portsMode;
- } catch (err) {
- console.error("list-serial-ports (win) primary/fallback failed:", err);
- return [];
- }
- } else {
- // macOS / Linux: scan common device files
- try {
- const out = await execPromise("ls /dev/tty.* /dev/ttyUSB* /dev/ttyACM* 2>/dev/null || true");
- const lines = out.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
- return lines.map(p => ({ value: p, label: p }));
- } catch (err) {
- console.error("list-serial-ports (unix) failed:", err);
- return [];
- }
- }
- } catch (err) {
- console.error("list-serial-ports failed:", err);
- return [];
- }
-});
-
-// --- new IPC handlers to start/stop and forward serial updates ---
-// Ensure we don't double-register handlers if the file is reloaded in dev reloads:
-if (ipcMain.removeHandler) {
- try { ipcMain.removeHandler("connect-com"); } catch (e) { /* ignore */ }
-}
-ipcMain.handle("connect-com", async (event, { port, baudRate = 9600 }) => {
- try {
- // if already connected, return info
- if (arduinoInstance) {
- return { ok: false, message: "Already connected" };
- }
- if (!port) return { ok: false, message: "No port specified" };
-
- // create and wire instance (verbose disabled to avoid noisy slider logs)
- arduinoInstance = new ArduinoThread(port, Number(baudRate) || 9600, 3, "config.json", false);
-
- // Keep last normalized value per slider so we only act when the slider actually changed.
- // Index = slider number (0..2). null means "unknown / not initialized".
- let _lastSliderNormalized = [null, null, null];
- const _minSliderDeltaForChange = 0.01; // 1% change required to be considered different
-
- arduinoInstance.on("update", (values) => {
- // forward to renderer
- try {
- if (mainWindow && mainWindow.webContents) {
- mainWindow.webContents.send("serial-update", values);
- }
- } catch (e) { /* ignore */ }
-
- // Also ensure the Python helper is invoked for each slider value.
- // This is a fallback in case the ArduinoThread did not emit 'set-volume'.
- try {
- const cfg = readConfig() || {};
- // values expected as integers (0..1023)
- if (Array.isArray(values)) {
- // ensure we have a session cache and refresher running
- startSessionRefresher();
- values.forEach((raw, idx) => {
- const num = Number(raw);
- if (!Number.isFinite(num)) return;
- let normalized = Math.max(0, Math.min(1, num / 1023));
- // snap near-zero to exact 0 to avoid tiny audible residuals
- if (normalized < 0.02) normalized = 0;
-
- // only continue if the slider value changed enough since last time
- const prev = _lastSliderNormalized[idx];
- const changed = prev === null || Math.abs(prev - normalized) > _minSliderDeltaForChange;
- if (!changed) return; // skip unchanged slider
- _lastSliderNormalized[idx] = normalized;
-
- const key = `slider${idx}`;
- const procs = Array.isArray(cfg[key]) ? cfg[key] : [];
- if (!procs || procs.length === 0) return;
-
- // match configured process names against running sessions (case-insensitive)
- procs.forEach((proc) => {
- if (!proc) return;
- const procLow = String(proc).toLowerCase();
- const matched = _runningSessions.some(s => {
- return s === procLow || s.includes(procLow) || procLow.includes(s);
- });
- if (matched) {
- // enqueue only when matched; DO NOT produce per-update verbose logs
- runPythonSetVolume(proc, normalized, idx);
- }
- });
- });
- }
- } catch (err) {
- console.error("update handler fallback failed:", err);
- sendVolumeLog(`update handler fallback failed: ${String(err)}`, true);
- }
- });
- arduinoInstance.on("raw", (raw) => {
- try { if (mainWindow && mainWindow.webContents) mainWindow.webContents.send("serial-raw", raw); } catch (e) {}
- });
- arduinoInstance.on("error", (err) => {
- try { if (mainWindow && mainWindow.webContents) mainWindow.webContents.send("serial-error", String(err)); } catch (e) {}
- });
- arduinoInstance.on("warn", (w) => {
- try { if (mainWindow && mainWindow.webContents) mainWindow.webContents.send("serial-warn", w); } catch (e) {}
- });
-
- // --- new helper to call the python volume setter and emit logs to renderer ---
- // send only concise important logs to renderer to avoid flooding DevTools
- const _verboseVolumeLogs = false;
- function sendVolumeLog(msg, force = false) {
- try {
- // keep main-process console for troubleshooting but avoid massive logs
- if (_verboseVolumeLogs || force || /\b(error|failed|succeed|succeeded)\b/i.test(String(msg))) {
- console.log(msg);
- try { if (mainWindow && mainWindow.webContents) mainWindow.webContents.send("volume-log", String(msg)); } catch (e) {}
- }
- } catch (e) { /* ignore */ }
- }
-
- // cached running audio sessions (lowercase strings)
- let _runningSessions = [];
- let _sessionRefreshTimer = null;
- const _sessionRefreshIntervalMs = 5000; // less frequent to avoid many spawns
-
- // ask set_volume.py to list sessions (tries several invocation patterns)
- async function refreshRunningSessions() {
- const script = path.join(__dirname, "set_volume.py");
- // Try invocation patterns; prefer "python