
セルフホスト型のコントロールデッキとリモートデスクトップ for Linux ワークステーション (Tailscale 経由)。WebRTC ストリーミング、ローカル LLM による音声制御、シーンマクロ、仮想入力、ファイル転送、ライブシステムテレメトリを搭載。
# phone-deck
余っているAndroidスマートフォン(またはブラウザを搭載した任意のデバイス)を、自己ホスト型のコントロールデッキおよびリモートデスクトップに変身させます。プライベートなTailscaleネットワーク上で、サードパーティ製アプリは不要です。
「古いスマホをどうするか」という疑問から始まり、Hyprland環境のコントロールプレーンに発展しました。ウィンドウ/ワークスペース制御、シーンマクロ、ローカルLLMアシスタント付きプッシュトゥトーク音声ルーター、コンテキスト認識型のアプリ内制御、双方向オーディオ+画面ストリーミング、タッチリモートデスクトップ、仮想キーボード/マウス、ファイル転送+スマホ→PC共有、ライブテレメトリー。これらすべてが、リン光ターミナル風のルックを持つインストール済みPWAとして提供されます。
> **移植性に関する注意。** これは特定のセットアップ向けに構築されています: **ArchベースのLinux (Garuda) + Hyprland + PipeWire + NVIDIA + Tailscale** に、[end-4 / illogical-impulse](https://github.com/end-4/dots-hyprland) のドットファイルを使用。アーキテクチャは汎用的ですが、いくつかの統合部分は環境固有です(モニター名、ワークスペースバインディング、matugenテーマパス、`cpupower`/`nvidia-smi`)。これはターンキーパッケージではなく、適応するための実用的なリファレンスとして扱ってください。
---
## Features
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:
- **Send-to-Rig** — PWAはAndroidの**共有ターゲット**として登録されます:スマホの任意のアプリから画像、テキスト、リンクを共有すると、それらがリグに届きます。画像はドロップディレクトリに保存され*かつ*クリップボードに貼り付け可能なPNGとして配置、テキストはクリップボードに、リンクはそのまま開かれます。
- **Ambient Cogitator** — 数分間アイドル状態が続くと、デッキはリン光の計器パネルに変わります:針ダイヤルテレメトリー(ピークホールド:「離れている間に振り切れていたか?」)、時計、再生中。
さらに:画面上のテンキーPINログイン、画面起動ロック、フルスクリーンランドスケープPWA、ログインに対するブルートフォースロックアウト。
---
## Architecture
```
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`](https://github.com/smit-shah-gg/phone-deck/blob/main/deckd/actions.py)で検証されます。たとえウェブアプリが完全に侵害されても、影響範囲は許可リスト化されたアクションのみであり、引数インジェクションはありません。ソケットは`root:<group>`モード`0660`です。
### Security model
- **自分のtailnet内からのみ**アクセス可能 — `tailscale serve`は実際のLet's Encrypt証明書を使用してMagicDNS名で公開します。決して`0.0.0.0`にはバインドしません。
- **JWT**ログイン(PIN → 署名済みCookie)、繰り返し失敗後に指数バックオフでロックアウト。
- 特権アクションは許可リスト化されたヘルパーデーモンで隔離されています。
- アップロードはベースネームがサニタイズされます(ドロップディレクトリから逃れられません)。
> ウェブアプリは構成されたシェルコマンドを実行し、あなたのユーザーとして入力を注入*できます* — これは設計上、あなたのマシンのリモートコントロールです。tailnet内に置き、PINの背後に保ち、公開しないでください。
---
## Requirements
- **Hyprland** (wlroots) と **PipeWire** (`pactl`/PulseAudio互換) を使用したLinux
- **Python ≥ 3.11** と **[uv](https://docs.astral.sh/uv/)**
- **Tailscale** (tailnetでHTTPS証明書が有効になっていること)
- 各種機能で使用されるCLIツール(必要なものをインストール):`hyprctl`、`pactl` / `pw-record` / `pw-play`、`playerctl`、`ddcutil`、`wf-recorder`、`grim`、`cpupower`、`nvidia-smi`、`kitty`(または任意のターミナル)
- ユーザーが**`input`**グループ(`/dev/uinput`用)と**`i2c`**グループ(`ddcutil`用)に所属していること
- オーディオ/ビデオストリーミング用の`aiortc` + PyAV(`uv`でインストール)
---
## Installation
```bash
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. The root helper (`deckd`)
最初に`systemd/deckd.service`を編集し、パスと`DECK_SOCKET_GROUP`をユーザーが属するグループ(通常はプライマリグループ)に設定してください:
```bash
sudo cp systemd/deckd.service /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl enable --now deckd
```
`deckd`はシステムのPythonで動作します(stdlibのみ、venvは不要)。
### 2. The web app
開発時は直接実行:
```bash
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`):
```bash
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 over Tailscale
1. 一度だけ証明書を有効にします:Tailscale管理コンソール → **DNS → Enable HTTPS Certificates**。
2. 実際の証明書でアプリを前面に配置:
```bash
sudo tailscale serve --bg 127.0.0.1:8765
```
3. HTTPSが稼働したら、ウェブアプリの環境変数に`DECK_SECURE_COOKIES=1`を設定します。
### 4. Install on the phone
スマホのChromeで`https://<your-host>.<your-tailnet>.ts.net/`を開き(tailnet内である必要があります)、ログインし、**⋮ → Install app**を選択します。フルスクリーン、横向き固定、画面起動キープの状態で起動します。PWAのサービスワーカー+画面起動ロックにはHTTPSが必要です。
> 任意のtailnetデバイスから動作します — ラップトップも含みます(タッチサーフェスはPointer Eventsを使用するため、マウスでも動作します)。WebRTCストリームはシングルピア(同時に1視聴者)です。
---
## Configuration
すべてのランタイム設定は`~/.config/phone-deck/`にあり、**Config**タブから編集可能です(JSON検証済み、`.bak`が保持されます)。変更は次のアクション時に反映され、再起動は不要です。
### `commands.json` — custom shell actions
```json
[
{ "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 → monitor binding
モードの配置は、ワークスペースがモニターに固定されている場合にのみ決定論的になります。Hyprland設定にバインディングを追加してください(ワークスペース番号とモニター名をあなたの環境に合わせて調整):
```
workspace = 1, monitor:DP-3, default:true
workspace = 2, monitor:DP-3
# … etc
```
### Theme
デッキは**リン光ターミナル**スキンを採用し、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のアイドルタイムアウトとリタニー行もここで設定可能です。
---
## How streaming works
**aiortc + PyAV**を介した双方向WebRTC。サブプロセスパイプでPipeWire/Hyprlandにブリッジ(脆弱なデバイスI/Oバインディングを回避):
- **PC → スマホ音声:** `parec`でデフォルトシンクの`.monitor`をキャプチャ → Opus。
- **スマホ → PCマイク:** スマホのマイクを受信 → PipeWireのnullシンク(そのモニターをアプリが入力として選択:"Monitor of PhoneDeckMic")。
- **スマホのみ出力:** PCの再生を仮想シンクにルーティングし、スマホで聴いている間スピーカーをミュート;停止時に復元。
- **PC → スマホビデオ:** `wf-recorder`がモニターをキャプチャ → aiortc。品質プリセット(540p / 720p / 1080p)により、不安定なリンクでもビットレートを適切に保ちます。
- **タッチリモートデスクトップ:** ストリームされたビデオ上のタップ/ドラッグ → 絶対カーソル位置指定(`hyprctl movecursor`)+`uinput`クリック。タップ=クリック、ドラッグ=移動、長押し=右クリック。
制約事項:レイテンシー約150~300ms。PWAがフォアグラウンドにある場合のみ信頼性が高い(Androidはバックグラウンドタブをサスペンドします)。エコーを避けるためにヘッドフォンを使用。同時に1つのストリーミングピアのみ。ブラウザはスマホ自身のアプリオーディオをキャプチャできません(マイクのみ)。
---
## Tech stack
- **Backend:** FastAPI + Uvicorn, WebSockets, aiortc/PyAV, python-evdev, psutil, PyJWT
- **Frontend:** HTML + Tailwind (CDN) + vanilla JS, PWA (manifest + service worker + wake-lock)
- **Helper daemon:** Python standard library only
- **Transport:** Tailscale (`tailscale serve` for HTTPS/WSS)
## Tests
```bash
uv run pytest
```
`deckd`許可リスト、JWT+ログインロックアウト、PINハッシュ、設定エディタ検証、モード解析、ストリーミング品質/コマンドロジック、入力キーマップをカバー。実際のWebRTC/PipeWire/Hyprlandパスは開発中にループバックで検証しています。
## Project layout
```
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
```
## License
MIT — `LICENSE`を参照(異なるライセンスを希望する場合は公開前に追加してください)。
## Acknowledgements
[Hyprland](https://hyprland.org/)、[PipeWire](https://pipewire.org/)、[Tailscale](https://tailscale.com/)、[aiortc](https://github.com/aiortc/aiortc)、[end-4 dotfiles](https://github.com/end-4/dots-hyprland)向けに構築されました。