
Console de contrôle auto-hébergée et bureau à distance pour postes de travail Linux via Tailscale, avec streaming WebRTC, contrôle vocal avec LLM local, macros de scène, entrée virtuelle, transfert de fichiers et télémétrie système en direct.
Transformez un téléphone Android de rechange (ou tout appareil avec un navigateur) en un panneau de contrôle auto-hébergé et en bureau à distance pour votre station de travail Linux — via votre réseau privé Tailscale, sans applications tierces.
Tout a commencé par « que faire d'un vieux téléphone ? » et est devenu un plan de contrôle pour une configuration Hyprland : contrôle des fenêtres/espaces de travail, macros de scène, routeur vocal push-to-talk avec un assistant LLM local, contrôles contextuels dans l'application, streaming audio + écran bidirectionnel, un bureau à distance tactile, un clavier/souris virtuel, transfert de fichiers + partage téléphone→PC, et télémétrie en direct — le tout servi comme une PWA installée avec un aspect terminal phosphor.
Note sur la portabilité. Ceci a été construit pour une configuration spécifique : Linux basé sur Arch (Garuda) + Hyprland + PipeWire + NVIDIA + Tailscale, avec les dotfiles end-4 / illogical-impulse. L'architecture est générique, mais plusieurs intégrations sont spécifiques à l'environnement (noms de moniteurs, liaisons d'espaces de travail, chemin du thème matugen,
cpupower/nvidia-smi). Considérez-le comme une référence de travail à adapter, pas un package clé en main.
L'UI est une application web sombre, paysage, à onglets (synchronisée avec le thème de votre bureau) :
| Onglet | Ce qu'il fait |
|---|---|
| Espaces | Grille d'espaces de travail en direct par écran (taper pour changer), liste des fenêtres en direct (taper pour focaliser), bascule DPMS par écran + luminosité ddcutil |
| Distant | Pavé tactile à l'écran (glisser/taper/deux doigts) + clavier virtuel avec modificateurs et combinaisons, via un périphérique uinput du noyau |
| Modes | Macros de scène en un tap — lancer/fermer/agencer des dispositions d'applications complètes sur plusieurs écrans (ex. « Travail » / « Loisirs ») |
| Voix | Routeur vocal push-to-talk (faster-whisper, CPU) : voies de mot-clé — macro (exécuter un mode/une commande), type (dicter), input (phrase → combinaison de touches) — avec confirmation avant exécution, plus « friday », un répondeur LLM local en lecture seule (Ollama + Qwen) avec recherche web auto-hébergée et connaissance de l'état système en direct |
| Audio | Sourdine micro/haut-parleur, volume, sélecteurs de périphériques de sortie et d'entrée, transport playerctl + couverture d'album (suit le lecteur MPRIS actif) |
| Stream | Audio WebRTC bidirectionnel (PC↔téléphone, avec micro du téléphone et sortie téléphone uniquement) + vidéo d'écran, et taper pour contrôler l'écran streamé = un véritable bureau à distance |
| Système | Bascule du mode de performance, changement de thème en direct, statut Tailscale, principaux processus (taper pour tuer), verrouillage, redémarrage de NetworkManager, mise en veille/redémarrage/extinction |
| Fichiers | Capture d'écran d'un moniteur → visualisation/téléchargement sur le téléphone ; déposer un fichier téléphone→PC |
| Config | Modifier la configuration commands.json / modes.json (et voix/contexte) depuis le téléphone (validé JSON) |
Toujours présent, au-dessus des onglets — une bande contextuelle qui affiche ce qui se passe en ce moment : un appel actif (sourdine / aller à), média en cours de lecture + transport, et contrôles de clavier dans l'application pour l'application focalisée (YouTube / Brave / Teams), fournis sans voler le focus.
Deux autres, au-delà des onglets :
Plus : connexion par code PIN numérique, verrouillage de l'écran, PWA plein écran paysage, et verrouillage par force brute sur la connexion.
téléphone / ordinateur portable (PWA navigateur)
│ HTTPS + WSS (Tailscale uniquement)
▼
tailscale serve ──► Application web FastAPI ── Socket Unix ──► deckd
(certificat TLS réel) (exécutée en tant que votre utilisateur) (noms d'actions) (exécutée en tant que root)
│ │
hyprctl · pactl · ddcutil · grim · liste d'autorisation fixe
wf-recorder · uinput · WebRTC de commandes privilégiées
(cpupower, nvidia-smi,
systemctl, …)
Deux processus :
app/ — l'application web s'exécute en tant que votre utilisateur normal. Elle fait tout ce qui ne nécessite pas root : contrôle Hyprland, audio, luminosité, capture d'écran, entrée virtuelle, streaming WebRTC, transfert de fichiers.deckd/ — un petit assistant root (stdlib uniquement, aucune dépendance) pour les quelques actions privilégiées. L'application web ne lui envoie jamais de chaînes shell — seulement des noms d'actions d'une liste d'autorisation fixe (governor_performance, gpu_power_limit, suspend, …), validée dans deckd/actions.py avant toute exécution. Même si l'application web était complètement compromise, le rayon d'impact est exactement les actions autorisées, sans injection d'arguments. Le socket est root:<groupe> mode 0660.tailscale serve l'expose sur votre nom MagicDNS avec un vrai certificat Let's Encrypt ; il ne se lie jamais à 0.0.0.0.L'application web peut exécuter vos commandes shell configurées et injecter des entrées en tant que votre utilisateur — c'est, par conception, une télécommande pour votre machine. Gardez-la sur votre tailnet, derrière le PIN, et ne l'exposez pas publiquement.
pactl/PulseAudio)hyprctl, pactl / pw-record / pw-play, playerctl, ddcutil,
wf-recorder, grim, cpupower, nvidia-smi, kitty (ou votre terminal)input (pour ) et le groupe (pour )git clone <votre-url-de-repo> phone-deck
cd phone-deck
uv sync # crée .venv et installe les dépendances
uv run python -m app.set_pin # définissez votre PIN de déverrouillage
deckd)Modifiez systemd/deckd.service d'abord — définissez les chemins et DECK_SOCKET_GROUP à un groupe auquel votre utilisateur appartient (généralement votre groupe principal) :
sudo cp systemd/deckd.service /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl enable --now deckd
deckd utilise le Python système (stdlib uniquement — pas besoin de venv).
Exécutez-la directement pour le développement :
uv run uvicorn app.main:app --host 127.0.0.1 --port 8765
…ou comme service utilisateur. Modifiez d'abord les chemins dans systemd/phone-deck.service, puis installez-le.
Important : sur une configuration où Hyprland n'est pas lancé via uwsm, le graphical-session.target de systemd ne s'active jamais, donc l'unité utilisateur ne démarrera pas automatiquement à la connexion. La solution fiable est de le lancer depuis Hyprland avec l'environnement de session importé — ajoutez à votre démarrage automatique Hyprland (par exemple, le ~/.config/hypr/custom/execs.conf de end-4) :
exec-once = systemctl --user import-environment WAYLAND_DISPLAY HYPRLAND_INSTANCE_SIGNATURE XDG_RUNTIME_DIR DBUS_SESSION_BUS_ADDRESS XDG_CURRENT_DESKTOP && systemctl --user start phone-deck
Activez les certificats une fois : console d'administration Tailscale → DNS → Enable HTTPS Certificates.
Faites front-end de l'application avec un vrai certificat :
sudo tailscale serve --bg 127.0.0.1:8765
Définissez DECK_SECURE_COOKIES=1 dans l'environnement de l'application web une fois HTTPS en place.
Ouvrez https://<votre-hôte>.<votre-tailnet>.ts.net/ dans Chrome sur le téléphone (il doit être sur votre tailnet), connectez-vous, puis ⋮ → Installer l'application. Elle se lance en plein écran, verrouillé en paysage, avec l'écran maintenu éveillé. HTTPS est requis pour le service worker PWA + wake-lock.
Fonctionne depuis n'importe quel appareil du tailnet — ordinateurs portables inclus (les surfaces tactiles utilisent les Pointer Events, donc une souris fonctionne aussi). Le flux WebRTC est à un seul pair (un seul spectateur à la fois).
Toute la configuration d'exécution se trouve dans ~/.config/phone-deck/ et est modifiable depuis l'onglet Config (validé JSON, conserve un .bak). Les changements prennent effet à la prochaine action — pas de redémarrage.
commands.json — actions shell personnalisées[
{ "id": "reload_wm", "label": "Recharger WM", "run": "hyprctl reload" },
{ "id": "deploy", "label": "Déployer le site", "run": "cd /srv/app && ./deploy.sh",
"confirm": true, "timeout": 120 }
]
run peut être une chaîne (exécutée via bash -lc) ou une liste (argv, sans shell). Ces commandes s'exécutent en tant que votre utilisateur — c'est un fichier local de confiance.
modes.json — macros de scèneChaque mode est une liste ordonnée d'étapes exécutées via hyprctl :
launch {cmd, workspace, match?, once?} — ouvrir une application sur un espace de travail. Avec match (classe de fenêtre) + once, elle ne dupliquera pas, et elle replace la fenêtre si elle atterrit sur le mauvais espace de travail (nécessaire pour les applications comme les PWA de navigateur dont la fenêtre provient d'un processus existant).close {match, workspaces?} — fermer les fenêtres d'une classe, optionnellement seulement sur les espaces de travail listés.move {match, workspace} — déplacer les fenêtres correspondantes vers un espace de travail.focus {workspace} — amener un espace de travail à l'affichage (par exemple, réinitialiser chaque moniteur à son espace de travail d'origine).Le placement des modes est déterministe uniquement si vos espaces de travail sont épinglés aux moniteurs. Ajoutez des liaisons à votre configuration Hyprland (ajustez les numéros d'espace de travail et les noms de moniteurs aux vôtres) :
workspace = 1, monitor:DP-3, default:true
workspace = 2, monitor:DP-3
# … etc
Le panneau arbore un skin phosphor-terminal avec quatre profils de couleur commutables en direct — vert (par défaut), ambre, glace, et auto (dérive la teinte phosphor de votre palette de fond d'écran matugen, ~/.local/state/quickshell/user/generated/colors.json). Basculez dans l'onglet Système ; le choix persiste dans ~/.config/phone-deck/theme.json. app/theme.py le sert comme /theme.css. Le délai d'inactivité du Cogitateur Ambiant et sa ligne de liturgie sont également configurables ici.
WebRTC bidirectionnel via aiortc + PyAV, ponté vers PipeWire/Hyprland avec des pipes de sous-processus (évite les liaisons fragiles d'E/S de périphériques) :
.monitor du sink par défaut avec parec → Opus.wf-recorder capture un moniteur → aiortc, avec des préréglages de qualité (540p / 720p / 1080p) pour maintenir un débit binaire raisonnable sur des liaisons instables.hyprctl movecursor) + clics uinput. Taper = clic, glisser = déplacer, appui long = clic droit.Contraintes : latence d'environ 150–300 ms ; fiable uniquement tant que la PWA est au premier plan (Android suspend les onglets en arrière-plan) ; utilisez un casque pour éviter l'écho ; un seul pair de streaming à la fois ; un navigateur ne peut pas capturer l'audio de sa propre application (micro uniquement).
tailscale serve pour HTTPS/WSS)uv run pytest
Couvre la liste d'autorisation de deckd, JWT + verrouillage de connexion, hachage du PIN, validation de l'éditeur de configuration, analyse des modes, logique de qualité/commande du streaming, et la carte des touches d'entrée. Les chemins WebRTC/PipeWire/Hyprland en direct sont validés par boucle de retour pendant le développement.
app/ Application web FastAPI
main.py routes + WebSockets
hypr.py Hyprland (instantané, dispatch, socket d'événements, curseur)
audio.py Contrôle des périphériques PipeWire + playerctl
audio_rtc.py Pont audio WebRTC + vidéo d'écran
hid.py Clavier/souris virtuel via uinput
modes.py Moteur de macros de scène
brightness.py ddcutil
grab.py Capture d'écran + téléchargement de fichier
theme.py matugen → CSS
...
deckd/ Assistant root (stdlib uniquement) + liste d'autorisation d'actions
systemd/ Unités de service
tests/ Suite pytest
MIT — voir LICENSE (ajoutez-en une avant de publier si vous souhaitez une licence différente).
Construit pour Hyprland, PipeWire, Tailscale, aiortc, et les dotfiles end-4.
/dev/uinputi2cddcutilaiortc + PyAV (installés via uv) pour le streaming audio/vidéo