Initial commit: StreamDeck Electron app
This commit is contained in:
@@ -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('');
|
||||
});
|
||||
Reference in New Issue
Block a user