
phone-deck v3.0.1
Pannello di controllo self-hosted e desktop remoto per workstation Linux tramite Tailscale, con streaming WebRTC, controllo vocale con LLM locale, macro di scena, input virtuale, trasferimento file e telemetria di sistema in tempo reale.
phone-deck
Trasforma un vecchio telefono Android (o qualsiasi dispositivo con un browser) in un pannello di controllo self-hosted e desktop remoto per la tua workstation Linux — sulla tua rete Tailscale privata, senza app di terze parti.
È iniziato come "cosa faccio con un vecchio telefono?" ed è diventato un piano di controllo per un setup Hyprland: controllo di finestre/workspace, macro di scena, un router vocale push-to-talk con un assistente LLM locale, controlli contestuali nell'app, streaming audio+video bidirezionale, un desktop remoto tattile, una tastiera/mouse virtuali, trasferimento file + condivisione telefono→PC, e telemetria live — tutto servito come una PWA installata con un aspetto da terminale fosforico.
Nota sulla portabilità. Questo è stato costruito per una configurazione specifica: Arch-based Linux (Garuda) + Hyprland + PipeWire + NVIDIA + Tailscale, con i dotfiles end-4 / illogical-impulse. L'architettura è generica, ma diverse integrazioni sono specifiche dell'ambiente (nomi dei monitor, binding dei workspace, il percorso del tema matugen,
cpupower/nvidia-smi). Trattalo come un riferimento funzionante da adattare, non un pacchetto già pronto.
Caratteristiche
L'interfaccia è un'app web scura, a schede, in orizzontale (sincronizzata con il tema del desktop):
| Scheda | Cosa fa |
|---|---|
| Workspace | Griglia di workspace live per monitor (tocca per cambiare), elenco finestre live (tocca per mettere a fuoco), attivazione/disattivazione DPMS per monitor + luminosità con ddcutil |
| Remoto | Trackpad sullo schermo (trascina/tocca/due dita) + tastiera virtuale con modificatori e combinazioni, tramite un dispositivo kernel uinput |
| Modalità | Macro di scena con un tocco — avvia/chiudi/disponi interi layout di app su più monitor (es. "Lavoro" / "Libero") |
| Voce | Router vocale push-to-talk (faster-whisper, CPU): corsie di parole chiave — macro (esegui una modalità/comando), type (dettatura), input (frase → combinazione di tasti) — con conferma prima dell'esecuzione, più "friday", un risponditore LLM locale in sola lettura (Ollama + Qwen) con ricerca web self-hosted e consapevolezza dello stato live del sistema |
| Audio | Disattivazione microfono/altoparlante, volume, selettori dispositivi di uscita e ingresso, trasporto playerctl + copertina album (traccia il player MPRIS attivo) |
| Stream | Audio WebRTC bidirezionale (PC↔telefono, con telefono come microfono e uscita solo telefono) + video dello schermo, e tocca per controllare lo schermo in streaming = un vero desktop remoto |
| Sistema | Attivazione/disattivazione modalità prestazioni, cambio tema live, stato Tailscale, processi principali (tocca per uccidere), blocco, riavvio NetworkManager, sospensione/riavvio/spegnimento |
| File | Screenshot di un monitor → visualizza/scarica sul telefono; trascina un file telefono→PC |
| Configurazione | Modifica la configurazione commands.json / modes.json (e contesto/voce) dal telefono (convalidata JSON) |
Sempre visibile, sopra le schede — una barra contestuale che mostra cosa sta succedendo in tempo reale: una chiamata attiva (silenzia / vai ad essa), media in riproduzione + trasporto, e controlli tastiera in-app per l'app focalizzata (YouTube / Brave / Teams), forniti senza rubare il focus.
Due altre, oltre le schede:
- Send-to-Rig — la PWA si registra come destinazione di condivisione Android: condividi un'immagine, del testo o un link da qualsiasi app sul tuo telefono e atterra sul rig — le immagini vengono salvate in una directory di drop e poste negli appunti come PNG pronto per l'incolla, il testo negli appunti, un link nudo aperto.
- Ambient Cogitator — dopo alcuni minuti di inattività, il deck diventa un pannello strumenti fosforico: telemetria con lancette e tenuta del picco ("è stato al massimo mentre ero via?"), orologio, ora in riproduzione.
In più: login PIN con tastierino numerico sullo schermo, wake-lock dello schermo, PWA a schermo intero orizzontale, e blocco anti-forza bruta sul login.
Architettura
phone / laptop (browser PWA)
│ HTTPS + WSS (Tailscale-only)
▼
tailscale serve ──► FastAPI web app ── Unix socket ──► deckd
(real TLS cert) (runs as your user) (action names) (runs as root)
│ │
hyprctl · pactl · ddcutil · grim · fixed allowlist of
wf-recorder · uinput · WebRTC privileged commands
(cpupower, nvidia-smi,
systemctl, …)
Due processi:
app/— l'app web viene eseguita come utente normale. Fa tutto ciò che non necessita di root: controllo Hyprland, audio, luminosità, cattura schermo, input virtuale, streaming WebRTC, trasferimento file.deckd/— un piccolo helper root (solo stdlib, nessuna dipendenza) per le poche azioni privilegiate. L'app web non gli invia mai stringhe di shell — solo nomi di azioni da una allowlist fissa (governor_performance,gpu_power_limit,suspend, …), convalidata indeckd/actions.pyprima che qualsiasi cosa venga eseguita. Anche se l'app web fosse completamente compromessa, il raggio di esplosione è esattamente le azioni consentite, senza iniezione di argomenti. Il socket ha modalitàroot:<gruppo>0660.
Modello di sicurezza
- Raggiungibile solo all'interno della tua tailnet —
tailscale servelo espone sul tuo nome MagicDNS con un certificato Let's Encrypt reale; non si lega mai a0.0.0.0. - Login JWT (PIN → cookie firmato), con blocco a backoff esponenziale dopo fallimenti ripetuti.
- Le azioni privilegiate sono isolate dietro il demone helper con allowlist.
- I caricamenti sono sanificati per basename (non possono uscire dalla directory di drop).
L'app web può eseguire i tuoi comandi shell configurati e iniettare input come tuo utente — è, per progettazione, un controllo remoto per la tua macchina. Tienilo nella tua tailnet, dietro il PIN, e non esporlo pubblicamente.
Requisiti
- Linux con Hyprland (wlroots), PipeWire (con compatibilità
pactl/PulseAudio) - Python ≥ 3.11 e uv
- Tailscale (con certificati HTTPS abilitati per la tua tailnet)
- Strumenti CLI usati da varie funzionalità (installa quelli che vuoi usare):
hyprctl,pactl/pw-record/pw-play,playerctl,ddcutil,wf-recorder,grim,cpupower,nvidia-smi,kitty(o il tuo terminale) - Il tuo utente nel gruppo
input(per/dev/uinput) e nel gruppoi2c(perddcutil) aiortc+ PyAV (installati tramiteuv) per lo streaming audio/video
Installazione
git clone <your-repo-url> phone-deck
cd phone-deck
uv sync # creates .venv and installs dependencies
uv run python -m app.set_pin # set your unlock PIN
1. L'helper root (deckd)
Modifica prima systemd/deckd.service — imposta i percorsi e DECK_SOCKET_GROUP su un gruppo a cui appartiene il tuo utente (di solito il tuo gruppo principale):
sudo cp systemd/deckd.service /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl enable --now deckd
deckd utilizza il Python di sistema (solo stdlib — nessun venv necessario).
2. L'app web
Esegui direttamente per sviluppo:
uv run uvicorn app.main:app --host 127.0.0.1 --port 8765
...o come servizio utente. Modifica prima i percorsi in systemd/phone-deck.service, poi installalo. Importante: su un setup in cui Hyprland non viene avviato tramite uwsm, il target systemd graphical-session.target non si attiva mai, quindi l'unità utente non si avvia automaticamente al login. La soluzione affidabile è avviarlo da Hyprland con l'ambiente di sessione importato — aggiungi al tuo autostart di Hyprland (es. ~/.config/hypr/custom/execs.conf di 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
3. HTTPS su Tailscale
-
Abilita i certificati una volta: Console amministrativa Tailscale → DNS → Abilita certificati HTTPS.
-
Metti l'app dietro un certificato reale:
sudo tailscale serve --bg 127.0.0.1:8765 -
Imposta
DECK_SECURE_COOKIES=1nell'ambiente dell'app web una volta che HTTPS è attivo.
4. Installa sul telefono
Apri https://<your-host>.<your-tailnet>.ts.net/ in Chrome sul telefono (deve essere sulla tua tailnet), effettua il login, quindi ⋮ → Installa app. Si avvia a schermo intero, bloccato in orizzontale, con lo schermo mantenuto acceso. HTTPS è richiesto per il service worker PWA + wake-lock.
Funziona da qualsiasi dispositivo nella tailnet — inclusi i laptop (le superfici touch usano Pointer Events, quindi anche un mouse funziona). Lo stream WebRTC è single-peer (un solo visualizzatore alla volta).
Configurazione
Tutta la configurazione runtime risiede in ~/.config/phone-deck/ ed è modificabile dalla scheda Configurazione (convalidata JSON, mantiene un .bak). Le modifiche hanno effetto alla prossima azione — nessun riavvio.
commands.json — azioni shell personalizzate
[
{ "id": "reload_wm", "label": "Reload WM", "run": "hyprctl reload" },
{ "id": "deploy", "label": "Deploy site", "run": "cd /srv/app && ./deploy.sh",
"confirm": true, "timeout": 120 }
]
run può essere una stringa (eseguita tramite bash -lc) o una lista (argv, nessuna shell). Queste vengono eseguite come tuo utente — è un file locale e fidato.
modes.json — macro di scena
Ogni modalità è una lista ordinata di passi eseguiti tramite hyprctl:
launch {cmd, workspace, match?, once?}— apre un'app su un workspace. Conmatch(classe finestra) +once, non duplica, e riposiziona la finestra se atterra sul workspace sbagliato (necessario per app come le PWA del browser la cui finestra proviene da un processo esistente).close {match, workspaces?}— chiude finestre di una classe, opzionalmente solo sui workspace elencati.move {match, workspace}— sposta le finestre corrispondenti su un workspace.focus {workspace}— porta un workspace in vista (es. resetta ogni monitor al suo ws di partenza).
Binding workspace → monitor di Hyprland
Il posizionamento delle modalità è deterministico solo se i tuoi workspace sono fissati ai monitor. Aggiungi i binding alla tua configurazione Hyprland (adatta i numeri dei workspace e i nomi dei monitor ai tuoi):
workspace = 1, monitor:DP-3, default:true
workspace = 2, monitor:DP-3
# … etc
Tema
Il deck indossa una skin phosphor-terminal con quattro profili colore commutabili live — verde (predefinito), ambra, ghiaccio e auto (deriva la tonalità fosforica dalla palette dello sfondo matugen, ~/.local/state/quickshell/user/generated/colors.json). Cambialo nella scheda Sistema; la scelta persiste in ~/.config/phone-deck/theme.json. app/theme.py lo serve come /theme.css. Il timeout di inattività e la linea del liturgia dell'Ambient Cogitator sono configurabili qui.
Come funziona lo streaming
WebRTC bidirezionale tramite aiortc + PyAV, collegato a PipeWire/Hyprland con pipe di subprocess (evita legami fragili con I/O dei dispositivi):
- Audio PC → telefono: cattura il
.monitordel sink predefinito conparec→ Opus. - Microfono telefono → PC: ricevi il microfono del telefono → un sink nullo PipeWire che le app selezionano come input ("Monitor of PhoneDeckMic").
- Uscita solo telefono: instrada la riproduzione del PC in un sink virtuale in modo che gli altoparlanti siano silenziosi mentre ascolti sul telefono; ripristinato allo stop.
- Video PC → telefono:
wf-recordercattura un monitor → aiortc, con preset di qualità (540p / 720p / 1080p) per mantenere il bitrate sensato su collegamenti instabili. - Desktop remoto tattile: tocca/trascina sul video in streaming → posizionamento assoluto del cursore (
hyprctl movecursor) + clicuinput. Tocca = clic, trascina = muovi, pressione lunga = clic destro.
Vincoli: latenza ~150–300 ms; affidabile solo mentre la PWA è in primo piano (Android sospende le schede in background); usa cuffie per evitare eco; un solo peer di streaming alla volta; un browser non può catturare l'audio delle proprie app del telefono (solo microfono).
Stack tecnologico
- Backend: FastAPI + Uvicorn, WebSockets, aiortc/PyAV, python-evdev, psutil, PyJWT
- Frontend: HTML + Tailwind (CDN) + vanilla JS, PWA (manifest + service worker + wake-lock)
- Demone helper: solo libreria standard Python
- Trasporto: Tailscale (
tailscale serveper HTTPS/WSS)
Test
uv run pytest
Copre la allowlist di deckd, JWT + blocco login, hashing PIN, validazione editor configurazione, parsing delle modalità, logica di qualità/comando streaming e mappa tastiera. I percorsi live WebRTC/PipeWire/Hyprland sono validati da loopback durante lo sviluppo.
Struttura del progetto
app/ FastAPI web app
main.py routes + WebSockets
hypr.py Hyprland (snapshot, dispatch, event socket, cursor)
audio.py PipeWire device control + playerctl
audio_rtc.py WebRTC audio + screen video bridge
hid.py virtual keyboard/mouse via uinput
modes.py scene-macro engine
brightness.py ddcutil
grab.py screenshot + file upload
theme.py matugen → CSS
...
deckd/ root helper (stdlib only) + action allowlist
systemd/ service units
tests/ pytest suite
Licenza
MIT — vedi LICENSE (aggiungine uno prima di pubblicare se vuoi una licenza diversa).
Riconoscimenti
Costruito per Hyprland, PipeWire, Tailscale, aiortc, e i dotfiles di end-4.