Initial commit: StreamDeck Electron app

This commit is contained in:
Dávid Ádám
2026-06-06 13:52:05 +02:00
commit 96ef83c904
12 changed files with 3462 additions and 0 deletions
+238
View File
@@ -0,0 +1,238 @@
const express = require('express');
const WebSocket = require('ws');
const http = require('http');
const path = require('path');
const { exec, spawn } = require('child_process');
const fs = require('fs');
// ─── Config ───────────────────────────────────────────────────────────────────
const PORT = 8090;
const CONFIG_PATH = path.join(__dirname, 'config.json');
let config = { pages: [], gridCols: 5, gridRows: 3 };
function loadConfig() {
try {
config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
console.log(` ✓ Config loaded: ${config.pages.length} page(s), ${config.gridCols}×${config.gridRows} grid`);
} catch (e) {
console.log(' ! No config.json found, using defaults');
}
}
loadConfig();
// ─── Express server ────────────────────────────────────────────────────────────
const app = express();
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.json());
// API endpoint to get config
app.get('/api/config', (req, res) => res.json(config));
// API endpoint to reload config
app.post('/api/reload', (req, res) => {
loadConfig();
res.json({ ok: true, pages: config.pages.length });
});
// API endpoint to run AppleScript
app.post('/api/osascript', (req, res) => {
const { script } = req.body;
if (!script) return res.status(400).json({ error: 'missing script' });
exec(`osascript -e '${script.replace(/'/g, "'\\''")}'`, (err, stdout, stderr) => {
if (err && stderr) console.error(' ✗ osascript error:', stderr.trim());
res.json({ ok: !err, stdout: stdout?.trim(), stderr: stderr?.trim() });
});
});
// API endpoint to run shell commands
app.post('/api/exec', (req, res) => {
const { command } = req.body;
if (!command) return res.status(400).json({ error: 'missing command' });
exec(command, { timeout: 5000 }, (err, stdout, stderr) => {
if (err && stderr) console.error(' ✗ exec error:', stderr.trim());
res.json({ ok: !err, stdout: stdout?.trim(), stderr: stderr?.trim() });
});
});
// API endpoint to open an app
app.post('/api/open', (req, res) => {
const { app, url } = req.body;
if (url) {
exec(`open '${url.replace(/'/g, "'\\''")}'`, (err) => {
res.json({ ok: !err });
});
} else if (app) {
exec(`open -a '${app.replace(/'/g, "'\\''")}'`, (err) => {
res.json({ ok: !err });
});
} else {
res.status(400).json({ error: 'missing app or url' });
}
});
// ─── WebSocket server ──────────────────────────────────────────────────────────
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });
function runAction(action, ws) {
if (!action) return;
const { type, value } = action;
console.log(`${type}: ${value?.substring(0, 60)}`);
switch (type) {
case 'keystroke': {
// Simulate keyboard shortcut via AppleScript
// value: "cmd+c", "cmd+shift+4", "cmd+space", etc.
const parts = value.split('+').map(s => s.trim().toLowerCase());
const key = parts.pop();
const specialKeys = {
'cmd': 'command down', 'ctrl': 'control down', 'alt': 'option down',
'shift': 'shift down', 'option': 'option down', 'command': 'command down',
'control': 'control down', 'fn': 'fn down',
};
// Map key names to AppleScript key codes or names
const keyMap = {
'enter': 'return', 'return': 'return', 'tab': 'tab', 'space': 'space',
'escape': 'escape', 'esc': 'escape', 'backspace': 'backspace',
'delete': 'backspace', 'up': 'up', 'down': 'down', 'left': 'left',
'right': 'right', 'home': 'home', 'end': 'end', 'pageup': 'page up',
'pagedown': 'page down',
};
const appleKey = keyMap[key] || key;
const pressedKeys = parts.map(k => specialKeys[k] || k).join(', ');
const script = pressedKeys
? `tell application "System Events" to keystroke "${appleKey}" using {${pressedKeys}}`
: `tell application "System Events" to keystroke "${appleKey}"`;
exec(`osascript -e '${script.replace(/'/g, "'\\''")}'`, (err) => {
if (err) console.error(` ✗ keystroke error: ${err.message}`);
});
break;
}
case 'app': {
exec(`open -a '${(value || '').replace(/'/g, "'\\''")}'`, (err) => {
if (err) console.error(` ✗ open app error: ${err.message}`);
});
break;
}
case 'url': {
exec(`open '${(value || '').replace(/'/g, "'\\''")}'`, (err) => {
if (err) console.error(` ✗ open url error: ${err.message}`);
});
break;
}
case 'shell': {
exec(value, { timeout: 5000 }, (err, stdout, stderr) => {
if (err) console.error(` ✗ shell error: ${err.message}`);
else if (stdout?.trim()) console.log(`${stdout.trim().substring(0, 80)}`);
});
break;
}
case 'osascript': {
exec(`osascript -e '${(value || '').replace(/'/g, "'\\''")}'`, (err, stdout, stderr) => {
if (err && stderr) console.error(` ✗ osascript error: ${stderr.trim()}`);
});
break;
}
case 'volume': {
const vol = parseInt(value);
if (!isNaN(vol)) {
exec(`osascript -e 'set volume output volume ${vol}'`, (err) => {
if (err) console.error(` ✗ volume error: ${err.message}`);
});
}
break;
}
case 'brightness': {
// Requires external tool or AppleScript
exec(`osascript -e 'tell application "System Events"' -e 'repeat 20 times' -e 'key code 107' -e 'end repeat' -e 'end tell'`, () => {});
break;
}
case 'sleep': {
exec('pmset displaysleepnow', (err) => {
if (err) console.error(` ✗ sleep error: ${err.message}`);
});
break;
}
case 'screenshot': {
const script = value === 'window'
? 'tell application "System Events" to keystroke "3" using {command down, shift down}' // full screen
: value === 'area'
? 'tell application "System Events" to keystroke "4" using {command down, shift down}' // area
: 'tell application "System Events" to keystroke "5" using {command down, shift down}'; // screen record
exec(`osascript -e '${script}'`, (err) => {
if (err) console.error(` ✗ screenshot error: ${err.message}`);
});
break;
}
default: {
if (ws) ws.send(JSON.stringify({ type: 'error', message: `Unknown action type: ${type}` }));
}
}
}
wss.on('connection', (ws) => {
console.log(' ◉ Browser connected via WebSocket');
ws.send(JSON.stringify({ type: 'connected', message: 'Stream Deck ready' }));
ws.on('message', (raw) => {
try {
const msg = JSON.parse(raw.toString());
console.log(`${msg.type}${msg.id ? ` [${msg.id}]` : ''}`);
switch (msg.type) {
case 'press':
if (msg.actions) {
// Execute multiple actions sequentially
const executeNext = (index) => {
if (index >= msg.actions.length) return;
const action = msg.actions[index];
const delay = action.delay || 0;
if (delay > 0) {
setTimeout(() => { runAction(action, ws); executeNext(index + 1); }, delay);
} else {
runAction(action, ws);
executeNext(index + 1);
}
};
executeNext(0);
}
break;
case 'ping':
ws.send(JSON.stringify({ type: 'pong' }));
break;
default:
ws.send(JSON.stringify({ type: 'error', message: `Unknown message type: ${msg.type}` }));
}
} catch (e) {
console.error(' ✗ Invalid message:', e.message);
}
});
ws.on('close', () => console.log(' ◉ Browser disconnected'));
});
// ─── Start ─────────────────────────────────────────────────────────────────────
server.listen(PORT, '0.0.0.0', () => {
console.log('');
console.log('╔══════════════════════════════════════════════╗');
console.log('║ 🎛 Custom Stream Deck Server ║');
console.log('╠══════════════════════════════════════════════╣');
console.log(`║ Local: http://localhost:${PORT}`);
console.log(`║ Network: http://192.168.1.231:${PORT}`);
console.log('╠══════════════════════════════════════════════╣');
console.log(`║ Pages: ${config.pages.length} Grid: ${config.gridCols}×${config.gridRows}`);
console.log('╚══════════════════════════════════════════════╝');
console.log('');
});