
セルフホスト型のコントロールデッキとリモートデスクトップ for Linux ワークステーション (Tailscale 経由)。WebRTC ストリーミング、ローカル LLM による音声制御、シーンマクロ、仮想入力、ファイル転送、ライブシステムテレメトリを搭載。
余っているAndroidスマートフォン(またはブラウザを搭載した任意のデバイス)を、自己ホスト型のコントロールデッキおよびリモートデスクトップに変身させます。プライベートなTailscaleネットワーク上で、サードパーティ製アプリは不要です。
「古いスマホをどうするか」という疑問から始まり、Hyprland環境のコントロールプレーンに発展しました。ウィンドウ/ワークスペース制御、シーンマクロ、ローカルLLMアシスタント付きプッシュトゥトーク音声ルーター、コンテキスト認識型のアプリ内制御、双方向オーディオ+画面ストリーミング、タッチリモートデスクトップ、仮想キーボード/マウス、ファイル転送+スマホ→PC共有、ライブテレメトリー。これらすべてが、リン光ターミナル風のルックを持つインストール済みPWAとして提供されます。
移植性に関する注意。 これは特定のセットアップ向けに構築されています: ArchベースのLinux (Garuda) + Hyprland + PipeWire + NVIDIA + Tailscale に、end-4 / illogical-impulse のドットファイルを使用。アーキテクチャは汎用的ですが、いくつかの統合部分は環境固有です(モニター名、ワークスペースバインディング、matugenテーマパス、
cpupower/nvidia-smi)。これはターンキーパッケージではなく、適応するための実用的なリファレンスとして扱ってください。
UIはタブ式でダークなランドスケープのウェブアプリです(デスクトップとテーマ同期)。
| Tab | What it does |
|---|---|
| Workspaces | 各モニターのライブワークスペースグリッド(タップで切り替え)、ライブウィンドウ一覧(タップでフォーカス)、モニターごとのDPMSトグル + ddcutil 輝度 |
| Remote | 画面上のトラックパッド(ドラッグ/タップ/2本指)+修飾キーとコンボ対応の仮想キーボード(カーネルuinputデバイス経由) |
| Modes | ワンタップシーンマクロ — アプリレイアウト全体をモニター間で起動/閉じる/配置(例:「Work」/「Free」) |
| Voice | プッシュトゥトーク音声ルーター(faster-whisper、CPU):リードワードレーン — macro(モード/コマンド実行)、type(ディクテーション)、input(フレーズ→キーコンボ)— 実行前確認機能付き。さらに 「friday」 は読み取り専用のローカルLLM回答者(Ollama + Qwen)で、セルフホスト型ウェブ検索とライブシステム状態認識を備える。 |
| Audio | マイク/スピーカーミュート、音量、出力および入力デバイスピッカー、playerctlトランスポート+カバーアート(アクティブなMPRISプレイヤーを追跡) |
| Stream | 双方向WebRTCオーディオ(PC↔スマホ、スマホをマイクとして使用、スマホのみ出力)+画面ビデオ、さらにストリーミング画面をタップで制御=本格的なリモートデスクトップ |
| System | パフォーマンスモード切り替え、ライブテーマ切り替え、Tailscaleステータス、上位プロセス(タップで強制終了)、ロック、NetworkManager再起動、サスペンド/再起動/電源オフ |
| Files | モニターのスクリーンショットを撮影→スマホで表示/ダウンロード;スマホからPCにファイルをドロップ |
| Config | スマホからcommands.json/modes.json(およびコンテキスト/音声)設定を編集(JSON検証済み) |
タブの上に常に表示されるコンテキストストリップが、現在の状況を表示します:アクティブな通話(ミュート/ジャンプ)、メディアの再生中+トランスポート、フォーカス中のアプリ(YouTube / Brave / Teams)のアプリ内キーボードコントロール。フォーカスを奪わずに提供されます。
Two more, beyond the tabs:
さらに:画面上のテンキーPINログイン、画面起動ロック、フルスクリーンランドスケープPWA、ログインに対するブルートフォースロックアウト。
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, …)
2つのプロセス:
app/ — ウェブアプリは通常のユーザーとして実行されます。root権限を必要としないすべての処理を行います:Hyprland制御、オーディオ、輝度、画面キャプチャ、仮想入力、WebRTCストリーミング、ファイル転送。deckd/ — 小さなrootヘルパー(stdlibのみ、依存関係なし)で、特権アクションを処理します。ウェブアプリは決してシェル文字列を送信せず、固定の許可リストからのアクション名のみ(governor_performance、gpu_power_limit、suspend、…)であり、実行前にdeckd/actions.pyで検証されます。たとえウェブアプリが完全に侵害されても、影響範囲は許可リスト化されたアクションのみであり、引数インジェクションはありません。ソケットはroot:<group>モード0660です。tailscale serveは実際のLet's Encrypt証明書を使用してMagicDNS名で公開します。決して0.0.0.0にはバインドしません。ウェブアプリは構成されたシェルコマンドを実行し、あなたのユーザーとして入力を注入できます — これは設計上、あなたのマシンのリモートコントロールです。tailnet内に置き、PINの背後に保ち、公開しないでください。
pactl/PulseAudio互換) を使用したLinuxhyprctl、pactl / pw-record / pw-play、playerctl、ddcutil、wf-recorder、grim、cpupower、nvidia-smi、kitty(または任意のターミナル)inputグループ(/dev/uinput用)と**グループ(用)に所属していること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
deckd)最初にsystemd/deckd.serviceを編集し、パスとDECK_SOCKET_GROUPをユーザーが属するグループ(通常はプライマリグループ)に設定してください:
sudo cp systemd/deckd.service /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl enable --now deckd
deckdはシステムのPythonで動作します(stdlibのみ、venvは不要)。
開発時は直接実行:
uv run uvicorn app.main:app --host 127.0.0.1 --port 8765
…またはユーザーサービスとして。最初にsystemd/phone-deck.serviceのパスを編集し、インストールしてください。重要: Hyprlandがuwsm経由で起動されていないセットアップでは、systemdのgraphical-session.targetが有効にならず、ユーザーユニットはログイン時に自動起動しません。信頼できる修正方法は、セッション環境をインポートした状態でHyprlandから起動することです。Hyprlandの自動起動に追加してください(例:end-4の~/.config/hypr/custom/execs.conf):
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
一度だけ証明書を有効にします:Tailscale管理コンソール → DNS → Enable HTTPS Certificates。
実際の証明書でアプリを前面に配置:
sudo tailscale serve --bg 127.0.0.1:8765
HTTPSが稼働したら、ウェブアプリの環境変数にDECK_SECURE_COOKIES=1を設定します。
スマホのChromeでhttps://<your-host>.<your-tailnet>.ts.net/を開き(tailnet内である必要があります)、ログインし、⋮ → Install appを選択します。フルスクリーン、横向き固定、画面起動キープの状態で起動します。PWAのサービスワーカー+画面起動ロックにはHTTPSが必要です。
任意のtailnetデバイスから動作します — ラップトップも含みます(タッチサーフェスはPointer Eventsを使用するため、マウスでも動作します)。WebRTCストリームはシングルピア(同時に1視聴者)です。
すべてのランタイム設定は~/.config/phone-deck/にあり、Configタブから編集可能です(JSON検証済み、.bakが保持されます)。変更は次のアクション時に反映され、再起動は不要です。
commands.json — custom shell actions[
{ "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は文字列(bash -lcで実行)またはリスト(argv、シェルなし)のいずれかです。これらはユーザーとして実行されます — ローカルの信頼されたファイルです。
modes.json — scene macros各モードは、hyprctlを介して実行されるステップの順序付きリストです:
launch {cmd, workspace, match?, once?} — ワークスペース上でアプリを開きます。match(ウィンドウクラス)とonceを指定すると重複せず、間違ったワークスペースに配置された場合はウィンドウを移動します(ブラウザPWAのように既存のプロセスからウィンドウが生成されるアプリに必要)。close {match, workspaces?} — 指定されたクラスのウィンドウを閉じます。必要に応じて指定したワークスペース上でのみ閉じます。move {match, workspace} — 一致するウィンドウを指定のワークスペースに移動します。focus {workspace} — ワークスペースを表示に切り替えます(例:各モニターをホームワークスペースにリセット)。モードの配置は、ワークスペースがモニターに固定されている場合にのみ決定論的になります。Hyprland設定にバインディングを追加してください(ワークスペース番号とモニター名をあなたの環境に合わせて調整):
workspace = 1, monitor:DP-3, default:true
workspace = 2, monitor:DP-3
# … etc
デッキはリン光ターミナルスキンを採用し、4つのライブ切り替え可能なカラープロファイルを備えています — green(デフォルト)、amber、ice、auto(matugenの壁紙パレットからリン光色を派生、~/.local/state/quickshell/user/generated/colors.json)。切り替えはSystemタブで行います。選択は~/.config/phone-deck/theme.jsonに永続化されます。app/theme.pyが/theme.cssとして提供します。Ambient Cogitatorのアイドルタイムアウトとリタニー行もここで設定可能です。
aiortc + PyAVを介した双方向WebRTC。サブプロセスパイプでPipeWire/Hyprlandにブリッジ(脆弱なデバイスI/Oバインディングを回避):
parecでデフォルトシンクの.monitorをキャプチャ → Opus。wf-recorderがモニターをキャプチャ → aiortc。品質プリセット(540p / 720p / 1080p)により、不安定なリンクでもビットレートを適切に保ちます。hyprctl movecursor)+uinputクリック。タップ=クリック、ドラッグ=移動、長押し=右クリック。制約事項:レイテンシー約150~300ms。PWAがフォアグラウンドにある場合のみ信頼性が高い(Androidはバックグラウンドタブをサスペンドします)。エコーを避けるためにヘッドフォンを使用。同時に1つのストリーミングピアのみ。ブラウザはスマホ自身のアプリオーディオをキャプチャできません(マイクのみ)。
tailscale serve for HTTPS/WSS)uv run pytest
deckd許可リスト、JWT+ログインロックアウト、PINハッシュ、設定エディタ検証、モード解析、ストリーミング品質/コマンドロジック、入力キーマップをカバー。実際のWebRTC/PipeWire/Hyprlandパスは開発中にループバックで検証しています。
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
MIT — LICENSEを参照(異なるライセンスを希望する場合は公開前に追加してください)。
Hyprland、PipeWire、Tailscale、aiortc、end-4 dotfiles向けに構築されました。
i2cddcutilaiortc + PyAV(uvでインストール)