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(''); });