diff --git a/discord-plugin/com.discord.streamdeck.sdPlugin/bin/plugin.cjs b/discord-plugin/com.discord.streamdeck.sdPlugin/bin/plugin.cjs index 9727fa1..a7093a5 100644 --- a/discord-plugin/com.discord.streamdeck.sdPlugin/bin/plugin.cjs +++ b/discord-plugin/com.discord.streamdeck.sdPlugin/bin/plugin.cjs @@ -34,27 +34,89 @@ try { console.warn(`[${pluginUUID}] WARNING: xdotool not found. Install it with: sudo apt install xdotool`) } +/** + * Find Discord's X11 window ID. + * Returns the window ID string, or null if Discord is not running / not found. + */ +function getDiscordWindowId() { + // Try by window class first (most reliable) + const byClass = spawnSync('xdotool', ['search', '--class', 'discord'], { stdio: 'pipe' }) + if (byClass.status === 0) { + const ids = byClass.stdout.toString().trim().split('\n').filter(Boolean) + if (ids.length > 0) return ids[ids.length - 1] + } + // Fallback: search by window title + const byName = spawnSync('xdotool', ['search', '--name', 'Discord'], { stdio: 'pipe' }) + if (byName.status === 0) { + const ids = byName.stdout.toString().trim().split('\n').filter(Boolean) + if (ids.length > 0) return ids[ids.length - 1] + } + console.warn(`[${pluginUUID}] Could not find Discord window — sending key to focused window instead`) + return null +} + +/** + * Focus `winId`, run `action()`, then restore focus to the previously active + * window. Uses XTestFakeKeyEvent (not XSendEvent) so Electron/Chromium treats + * the event as a trusted hardware input (isTrusted=true in JavaScript). + * xdotool key --window uses XSendEvent which Discord ignores. + */ +function withDiscordFocus(winId, action) { + const prevResult = spawnSync('xdotool', ['getactivewindow'], { stdio: 'pipe' }) + const prevWinId = prevResult.status === 0 ? prevResult.stdout.toString().trim() : null + + spawnSync('xdotool', ['windowfocus', '--sync', winId], { stdio: 'pipe' }) + action() + if (prevWinId && prevWinId !== winId) { + spawnSync('xdotool', ['windowfocus', '--sync', prevWinId], { stdio: 'pipe' }) + } +} + function xdotoolKey(hotkey) { if (!xdotoolAvailable) return - const result = spawnSync('xdotool', ['key', hotkey], { stdio: 'pipe' }) - if (result.status !== 0) { - console.warn(`[${pluginUUID}] xdotool key failed for "${hotkey}":`, result.stderr?.toString().trim()) + const winId = getDiscordWindowId() + const args = ['key', '--clearmodifiers', hotkey] + console.log(`[${pluginUUID}] xdotool${winId ? ' (via Discord focus)' : ''} ${args.join(' ')}`) + if (winId) { + withDiscordFocus(winId, () => { + const r = spawnSync('xdotool', args, { stdio: 'pipe' }) + if (r.status !== 0) console.warn(`[${pluginUUID}] xdotool key failed:`, r.stderr?.toString().trim()) + }) + } else { + const r = spawnSync('xdotool', args, { stdio: 'pipe' }) + if (r.status !== 0) console.warn(`[${pluginUUID}] xdotool key failed:`, r.stderr?.toString().trim()) } } function xdotoolKeyDown(hotkey) { if (!xdotoolAvailable) return - const result = spawnSync('xdotool', ['keydown', hotkey], { stdio: 'pipe' }) - if (result.status !== 0) { - console.warn(`[${pluginUUID}] xdotool keydown failed for "${hotkey}":`, result.stderr?.toString().trim()) + const winId = getDiscordWindowId() + const args = ['keydown', '--clearmodifiers', hotkey] + console.log(`[${pluginUUID}] xdotool${winId ? ' (via Discord focus)' : ''} ${args.join(' ')}`) + if (winId) { + withDiscordFocus(winId, () => { + const r = spawnSync('xdotool', args, { stdio: 'pipe' }) + if (r.status !== 0) console.warn(`[${pluginUUID}] xdotool keydown failed:`, r.stderr?.toString().trim()) + }) + } else { + const r = spawnSync('xdotool', args, { stdio: 'pipe' }) + if (r.status !== 0) console.warn(`[${pluginUUID}] xdotool keydown failed:`, r.stderr?.toString().trim()) } } function xdotoolKeyUp(hotkey) { if (!xdotoolAvailable) return - const result = spawnSync('xdotool', ['keyup', hotkey], { stdio: 'pipe' }) - if (result.status !== 0) { - console.warn(`[${pluginUUID}] xdotool keyup failed for "${hotkey}":`, result.stderr?.toString().trim()) + const winId = getDiscordWindowId() + const args = ['keyup', '--clearmodifiers', hotkey] + console.log(`[${pluginUUID}] xdotool${winId ? ' (via Discord focus)' : ''} ${args.join(' ')}`) + if (winId) { + withDiscordFocus(winId, () => { + const r = spawnSync('xdotool', args, { stdio: 'pipe' }) + if (r.status !== 0) console.warn(`[${pluginUUID}] xdotool keyup failed:`, r.stderr?.toString().trim()) + }) + } else { + const r = spawnSync('xdotool', args, { stdio: 'pipe' }) + if (r.status !== 0) console.warn(`[${pluginUUID}] xdotool keyup failed:`, r.stderr?.toString().trim()) } } diff --git a/package.json b/package.json index 67aa124..396866f 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,12 @@ "electron/**", "package.json" ], + "extraResources": [ + { + "from": "public/bridge/sdpi-bridge.js", + "to": "bridge/sdpi-bridge.js" + } + ], "linux": { "target": [ { diff --git a/src/App.jsx b/src/App.jsx index 90f32ad..c941d1e 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -2479,9 +2479,9 @@ export default function App() { setPressedKey(p => p === index ? null : p) const config = buttonConfigsRef.current[index] // Forward keyUp to plugin processes (needed for Push-to-Talk / Push-to-Mute) - if (config?.type?.includes('.')) { - const context = JSON.stringify({ index, actionUUID: config.type, pluginUUID: config.pluginUUID }) - window.streamDeck?.sendToPlugin?.(config.pluginUUID, config.type, 'keyUp', { ...config }, context) + if (config?.action?.type?.includes('.')) { + const context = JSON.stringify({ index, actionUUID: config.action.type, pluginUUID: config.action.pluginUUID }) + window.streamDeck?.sendToPlugin?.(config.action.pluginUUID, config.action.type, 'keyUp', { ...config.action }, context) } if (config?.pressedIconDataUrl) {