Files
StreamDeck/server.js
T
2026-06-06 13:52:05 +02:00

239 lines
9.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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('');
});