Feat : STRCHA Stream status
This commit is contained in:
188
packages/agent/src/hotkey.ts
Normal file
188
packages/agent/src/hotkey.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import { execFile } from 'node:child_process';
|
||||
import { promisify } from 'node:util';
|
||||
|
||||
const run = promisify(execFile);
|
||||
|
||||
export interface HotkeyRequest {
|
||||
/** Touche à envoyer : « f », « F11 », « space »… */
|
||||
key: string;
|
||||
/** Fragment du titre de la fenêtre cible (insensible à la casse). */
|
||||
windowMatch: string;
|
||||
}
|
||||
|
||||
export interface HotkeyResult {
|
||||
method: string;
|
||||
window?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Jeu de touches autorisé. Ces valeurs viennent de la configuration serveur et
|
||||
* finissent dans une ligne de commande : on refuse tout ce qui sort du lot
|
||||
* plutôt que d'échapper au cas par cas.
|
||||
*/
|
||||
const KEY_PATTERN = /^(?:[A-Za-z0-9]|F[1-9]|F1[0-2]|space|Return|Escape|Tab)$/;
|
||||
|
||||
function assertKey(key: string): string {
|
||||
if (!KEY_PATTERN.test(key)) {
|
||||
throw new Error(
|
||||
`Touche « ${key} » non autorisée (lettres, chiffres, F1-F12, space, Return, Escape, Tab)`,
|
||||
);
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Envoie une touche au lecteur vidéo, par exemple pour rebasculer en plein
|
||||
* écran après un show privé.
|
||||
*
|
||||
* Limite assumée : sous X11 comme sous Windows, la fenêtre cible doit passer au
|
||||
* premier plan. Les navigateurs ignorent les évènements clavier synthétiques
|
||||
* envoyés par XSendEvent (`xdotool key --window`), donc on active la fenêtre et
|
||||
* on injecte la touche via XTEST. Sur une VM d'enregistrement dédiée c'est sans
|
||||
* conséquence, mais ça vole le focus si quelqu'un est en train de s'en servir.
|
||||
*/
|
||||
export async function sendHotkey(request: HotkeyRequest): Promise<HotkeyResult> {
|
||||
const key = assertKey(request.key);
|
||||
const match = request.windowMatch.trim();
|
||||
if (!match) throw new Error('Aucun titre de fenêtre à cibler (windowMatch vide)');
|
||||
|
||||
switch (process.platform) {
|
||||
case 'linux':
|
||||
return sendLinux(key, match);
|
||||
case 'win32':
|
||||
return sendWindows(key, match);
|
||||
case 'darwin':
|
||||
return sendDarwin(key, match);
|
||||
default:
|
||||
throw new Error(`Envoi de touche non supporté sur ${process.platform}`);
|
||||
}
|
||||
}
|
||||
|
||||
// --- X11 --------------------------------------------------------------------
|
||||
|
||||
async function sendLinux(key: string, match: string): Promise<HotkeyResult> {
|
||||
const env = { ...process.env, DISPLAY: process.env.DISPLAY ?? ':0' };
|
||||
|
||||
let ids: string[];
|
||||
try {
|
||||
const { stdout } = await run(
|
||||
'xdotool',
|
||||
['search', '--onlyvisible', '--name', match],
|
||||
{ env, timeout: 5000 },
|
||||
);
|
||||
ids = stdout.split('\n').map((line) => line.trim()).filter(Boolean);
|
||||
} catch (err) {
|
||||
const code = (err as NodeJS.ErrnoException).code;
|
||||
if (code === 'ENOENT') {
|
||||
throw new Error('xdotool est absent — installe-le : apt install xdotool');
|
||||
}
|
||||
// xdotool sort en code 1 quand rien ne correspond.
|
||||
throw new Error(`Aucune fenêtre visible ne correspond à « ${match} »`);
|
||||
}
|
||||
if (ids.length === 0) throw new Error(`Aucune fenêtre visible ne correspond à « ${match} »`);
|
||||
|
||||
// La dernière fenêtre listée est en général la plus récemment mappée.
|
||||
const windowId = ids[ids.length - 1]!;
|
||||
|
||||
let title = windowId;
|
||||
try {
|
||||
const { stdout } = await run('xdotool', ['getwindowname', windowId], { env, timeout: 5000 });
|
||||
title = stdout.trim() || windowId;
|
||||
} catch {
|
||||
/* le titre n'est qu'informatif */
|
||||
}
|
||||
|
||||
await run('xdotool', ['windowactivate', '--sync', windowId], { env, timeout: 5000 });
|
||||
await run('xdotool', ['key', '--clearmodifiers', key], { env, timeout: 5000 });
|
||||
|
||||
return { method: 'xdotool', window: title };
|
||||
}
|
||||
|
||||
// --- Windows ----------------------------------------------------------------
|
||||
|
||||
/** Échappe une valeur pour une chaîne littérale PowerShell entre apostrophes. */
|
||||
function psLiteral(value: string): string {
|
||||
return `'${value.replace(/'/g, "''")}'`;
|
||||
}
|
||||
|
||||
async function sendWindows(key: string, match: string): Promise<HotkeyResult> {
|
||||
// SendKeys interprète certains caractères ; les touches nommées se notent {F11}.
|
||||
const sendKeysArg = key.length === 1 ? key.toLowerCase() : `{${key.toUpperCase()}}`;
|
||||
|
||||
const script = `
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Add-Type @"
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
public class Win32 {
|
||||
[DllImport("user32.dll")] public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
|
||||
[DllImport("user32.dll")] public static extern int GetWindowTextLength(IntPtr hWnd);
|
||||
[DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count);
|
||||
[DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr hWnd);
|
||||
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd);
|
||||
[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
|
||||
public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
|
||||
}
|
||||
"@
|
||||
$needle = ${psLiteral(match)}
|
||||
$found = [IntPtr]::Zero
|
||||
$foundTitle = ''
|
||||
$callback = [Win32+EnumWindowsProc]{
|
||||
param($hWnd, $lParam)
|
||||
if (-not [Win32]::IsWindowVisible($hWnd)) { return $true }
|
||||
$len = [Win32]::GetWindowTextLength($hWnd)
|
||||
if ($len -eq 0) { return $true }
|
||||
$sb = New-Object System.Text.StringBuilder($len + 1)
|
||||
[void][Win32]::GetWindowText($hWnd, $sb, $sb.Capacity)
|
||||
$title = $sb.ToString()
|
||||
if ($title -like ('*' + $needle + '*')) { $script:found = $hWnd; $script:foundTitle = $title; return $false }
|
||||
return $true
|
||||
}
|
||||
[void][Win32]::EnumWindows($callback, [IntPtr]::Zero)
|
||||
if ($script:found -eq [IntPtr]::Zero) { Write-Error ('Aucune fenetre ne correspond a ' + $needle); exit 1 }
|
||||
[void][Win32]::ShowWindow($script:found, 9)
|
||||
[void][Win32]::SetForegroundWindow($script:found)
|
||||
Start-Sleep -Milliseconds 300
|
||||
Add-Type -AssemblyName System.Windows.Forms
|
||||
[System.Windows.Forms.SendKeys]::SendWait(${psLiteral(sendKeysArg)})
|
||||
Write-Output $script:foundTitle
|
||||
`;
|
||||
|
||||
// -EncodedCommand évite tout problème de guillemets entre cmd.exe et PowerShell.
|
||||
const encoded = Buffer.from(script, 'utf16le').toString('base64');
|
||||
|
||||
try {
|
||||
const { stdout } = await run(
|
||||
'powershell.exe',
|
||||
['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-EncodedCommand', encoded],
|
||||
{ timeout: 20_000, windowsHide: true },
|
||||
);
|
||||
return { method: 'SendKeys', window: stdout.trim() || undefined };
|
||||
} catch (err) {
|
||||
const stderr = (err as { stderr?: string }).stderr?.trim();
|
||||
throw new Error(stderr || `Envoi de « ${key} » impossible vers « ${match} »`);
|
||||
}
|
||||
}
|
||||
|
||||
// --- macOS (confort de développement) ---------------------------------------
|
||||
|
||||
async function sendDarwin(key: string, match: string): Promise<HotkeyResult> {
|
||||
const script = `
|
||||
tell application "System Events"
|
||||
set matches to (every process whose name contains "${match.replace(/["\\]/g, '')}")
|
||||
if (count of matches) = 0 then error "Aucun processus ne correspond"
|
||||
set target to item 1 of matches
|
||||
set frontmost of target to true
|
||||
delay 0.3
|
||||
keystroke "${key.toLowerCase()}"
|
||||
end tell`;
|
||||
|
||||
try {
|
||||
await run('osascript', ['-e', script], { timeout: 15_000 });
|
||||
return { method: 'osascript', window: match };
|
||||
} catch (err) {
|
||||
const stderr = (err as { stderr?: string }).stderr?.trim();
|
||||
throw new Error(stderr || `Envoi de « ${key} » impossible vers « ${match} »`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user