2 Commits

Author SHA1 Message Date
enricobuehler 69fcb6e0b1 docs: restructure host setup by distro, configuration by compositor
apple / swift (push) Successful in 1m8s
apple / screenshots (push) Successful in 5m33s
android / android (push) Successful in 4m43s
arch / build-publish (push) Successful in 5m38s
ci / web (push) Successful in 1m3s
ci / docs-site (push) Successful in 1m17s
ci / rust (push) Successful in 4m48s
ci / bench (push) Successful in 5m7s
decky / build-publish (push) Successful in 14s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 3s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
deb / build-publish (push) Successful in 4m29s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m16s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 10m24s
docker / deploy-docs (push) Successful in 6s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 10m3s
Split the docs' single distro×desktop axis (ubuntu-gnome / ubuntu-kde / fedora-kde) into two,
which deduplicates the shared mechanics and scales to distros that run several desktops (Arch):

- Install the host — per distro/OS (ubuntu, fedora, arch, bazzite, steamos-host, windows-host):
  GPU driver + package + input group, then a canonical "Configure your desktop" funnel.
- Configure your desktop — per compositor (kde, gnome, gamescope, sway): host.env, compositor
  quirks, the headless session, and starting the host.

New shared web-console page (enable · login password · arm pairing) removes the console/password
block that was copy-pasted across all seven host pages. Merged ubuntu-gnome + ubuntu-kde into
ubuntu; renamed fedora-kde to fedora; kept bazzite and steamos-host as dedicated appliance guides
(trimmed of duplication). Moved the KWin headless session, the GNOME EGL/lock traps, and the
gamescope attach/managed model out of the distro pages onto their compositor pages.

Fixed while restructuring: distro-specific paths on kde (kde-desktop-setup.sh is Fedora/Bazzite-only;
the .deb ships host.env.kde under /usr/share/punktfunk-host), the interactive "start the host" step
that was lost in the merge, sway over-claiming Hyprland, and a pre-existing broken anchor in
how-it-works.

Removal of the three old pages was captured by the preceding commit 8ebb614 (a concurrent commit
swept up the staged git-rm); the net docs tree is correct. Fumadocs build + internal link/anchor
check green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 21:04:31 +00:00
enricobuehler 8ebb61400c fix(web): clearer topology/identity copy, capped description width, mobile More-nav, preset spacing
apple / swift (push) Successful in 1m12s
apple / screenshots (push) Successful in 5m18s
windows-host / package (push) Successful in 7m38s
android / android (push) Successful in 12m11s
arch / build-publish (push) Successful in 9m7s
ci / web (push) Successful in 1m7s
ci / docs-site (push) Successful in 1m18s
ci / rust (push) Successful in 4m47s
ci / bench (push) Successful in 5m3s
decky / build-publish (push) Successful in 19s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 6s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 32s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 5s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 6s
deb / build-publish (push) Successful in 4m31s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 57s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 10m48s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 10m28s
docker / deploy-docs (push) Successful in 20s
Console polish on the Virtual displays card + shell:
- Topology help now leads with the streamed display's role (Extend/Primary/Exclusive) instead of
  the confusing physical-monitor-only framing; notes the headless case. Identity help spells out the
  actual behavior (stable per-client identity → the desktop reapplies that client's scaling/resolution
  on reconnect) + what Shared / Per-client / Per-client+resolution each do.
- Cap description/help width at max-w-prose so long help text isn't a full-viewport line on large screens.
- Mobile bottom nav: 8 flat tabs were too cramped → 4 pinned tabs + a "More" tab whose sheet holds the
  rest (Performance/Logs/Pairing/Settings), "More" highlighted when the active route is in the overflow.
- More breathing room under the "Preset" heading.

web tsc + biome + vite build green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 20:40:35 +00:00
27 changed files with 850 additions and 586 deletions
+23 -35
View File
@@ -58,25 +58,30 @@ tuning, and example configs. Updates later are just `sudo pacman -Syu`.
## 4. Configure and run ## 4. Configure and run
The host runs as a systemd **`--user`** service — it needs your session's PipeWire and D-Bus. The host runs as a systemd **`--user`** service — it needs your session's PipeWire and D-Bus. Copy a
Copy a starting config, enable the service, and enable linger so it starts at boot without a login: starting config:
```sh ```sh
mkdir -p ~/.config/punktfunk mkdir -p ~/.config/punktfunk
cp /usr/share/punktfunk/host.env.example ~/.config/punktfunk/host.env # then edit cp /usr/share/punktfunk/host.env.example ~/.config/punktfunk/host.env
```
How the host creates its virtual display and injects input depends on your desktop, not your distro —
edit `host.env` for the desktop you run, following its page for the exact settings and any quirks:
- [KDE Plasma (KWin)](/docs/kde)
- [GNOME (Mutter)](/docs/gnome)
- [Steam / gamescope](/docs/gamescope)
- [Sway / wlroots](/docs/sway)
Then enable the service and turn on linger so it starts at boot without a login:
```sh
systemctl --user daemon-reload systemctl --user daemon-reload
systemctl --user enable --now punktfunk-host systemctl --user enable --now punktfunk-host
sudo loginctl enable-linger "$USER" sudo loginctl enable-linger "$USER"
``` ```
Which compositor the host captures depends on your desktop — it drives a per-client virtual output
via KWin (Plasma), Mutter (GNOME), or wlroots (Sway), or spawns a headless **gamescope** session
per connect. For a headless appliance, the package also ships `punktfunk-kde-session.service`
(a dedicated `kwin --virtual` session, same as the [Fedora KDE](/docs/fedora-kde#3-kwin-streaming-session)
guide — `cp /usr/share/punktfunk/host.env.kde ~/.config/punktfunk/host.env` and enable it alongside
the host). See [Configuration](/docs/configuration) for every knob and
[Running as a Service](/docs/running-as-a-service) for the service model.
Check it came up: Check it came up:
```sh ```sh
@@ -84,27 +89,10 @@ systemctl --user status punktfunk-host # active
journalctl --user -u punktfunk-host -f # watch a client connect journalctl --user -u punktfunk-host -f # watch a client connect
``` ```
### Web console Enable the browser console, find your login password, and arm PIN pairing from
[The Web Console](/docs/web-console). For a headless KWin appliance that streams at boot with no
The console (status, paired devices, arm pairing) ships as `punktfunk-web` — enable it, then open graphical login, see [KDE → Headless session](/docs/kde#headless-session). Full reference:
`http://<host-ip>:47992`: [Configuration](/docs/configuration) · [Running as a Service](/docs/running-as-a-service).
```sh
systemctl --user enable --now punktfunk-web
```
#### Console login password
On first start `punktfunk-web-init` generates a random login password and saves it to
`~/.config/punktfunk/web-password` (as `PUNKTFUNK_UI_PASSWORD=…`). Read it back at any time:
```sh
journalctl --user -u punktfunk-web-init | sed -n 's/.*password generated: //p'
sed -n 's/^PUNKTFUNK_UI_PASSWORD=//p' ~/.config/punktfunk/web-password
```
To set your own, edit that file and `systemctl --user restart punktfunk-web`. Forgot it? See
[Forgot your Password?](/docs/forgot-password).
## 5. Open the firewall (if you have one) ## 5. Open the firewall (if you have one)
@@ -147,9 +135,9 @@ opened. Full port lists (`nftables`, explicit ports) are in
## 6. Connect a client ## 6. Connect a client
From any [client](/docs/clients), `--discover` finds the host on the LAN. On first connect, complete From any [client](/docs/clients), `--discover` finds the host on the LAN. On first connect, complete
the **PIN pairing** arm it from the host's web console, which displays a 4-digit PIN to type into the **PIN pairing**: arm it from [The Web Console](/docs/web-console#arm-pairing), which displays a
the client. (Pairing is required by default; pass `serve --open` only if you deliberately want to 4-digit PIN to type into the client. (Pairing is required by default; pass `serve --open` only if
disable it.) See [Clients](/docs/clients) and [Pairing](/docs/pairing). you deliberately want to disable it.) See [Clients](/docs/clients) for per-platform setup.
## Appendix — build from source (PKGBUILD) ## Appendix — build from source (PKGBUILD)
+18 -32
View File
@@ -16,8 +16,8 @@ mid-stream. You flip between Gaming Mode and Desktop with Bazzite's normal Steam
`host.env` forces a mode. `host.env` forces a mode.
> Ideal for a dedicated game-streaming box that you also occasionally want as a remote desktop. For a > Ideal for a dedicated game-streaming box that you also occasionally want as a remote desktop. For a
> pure desktop machine, [Ubuntu/Fedora KDE](/docs/ubuntu-kde) or [GNOME](/docs/ubuntu-gnome) are > pure desktop machine, install on [Ubuntu](/docs/ubuntu) or [Fedora](/docs/fedora) and configure the
> simpler. > [KDE](/docs/kde) or [GNOME](/docs/gnome) desktop directly — simpler.
> New here? Read [Security & Safe Use](/docs/security) first — a streaming host is remote control of > New here? Read [Security & Safe Use](/docs/security) first — a streaming host is remote control of
> the machine, so keep it on a trusted LAN or VPN and require pairing. > the machine, so keep it on a trusted LAN or VPN and require pairing.
@@ -60,7 +60,7 @@ For a fully baked appliance image there's also a **bootc** Containerfile that in
from the registry at image-build time — see `packaging/bootc/` in the repo. Plain `rpm-ostree` from the registry at image-build time — see `packaging/bootc/` in the repo. Plain `rpm-ostree`
layering from the [RPM registry](https://git.unom.io/unom/-/packages) keeps working too (see layering from the [RPM registry](https://git.unom.io/unom/-/packages) keeps working too (see
`packaging/bazzite/README.md`), but the sysext is the supported default. Building from source `packaging/bazzite/README.md`), but the sysext is the supported default. Building from source
also works (Bazzite is Fedora Atomic underneath — same steps as [Fedora KDE](/docs/fedora-kde)). also works (Bazzite is Fedora Atomic underneath — same steps as [Fedora](/docs/fedora)).
## Allow controller input ## Allow controller input
@@ -99,15 +99,14 @@ PUNKTFUNK_GAMESCOPE_ATTACH=1 # Gaming Mode = attach to the box's own session
For Gaming Mode there are two models (pick one; the shipped default is **attach**): For Gaming Mode there are two models (pick one; the shipped default is **attach**):
- **Attach** (`PUNKTFUNK_GAMESCOPE_ATTACH=1`, the default) — the **box** owns its gamescope session - **Attach** (`PUNKTFUNK_GAMESCOPE_ATTACH=1`, the default) — the **box** owns its gamescope session,
and decides Gaming vs Desktop via the normal Steam UI. The host just attaches to whatever's live the host attaches to whatever's live and never tears it down, and the streamed game-mode resolution
and never tears it down, so switching Desktop ↔ Game is rock-solid and disconnecting leaves the box is the box's own gamescope mode. Switching Desktop ↔ Game is rock-solid.
where it was. The streamed game-mode resolution is the box's gamescope mode - **Managed** (`PUNKTFUNK_GAMESCOPE_MANAGED=1`, and remove the attach line) — the host launches its
(`SCREEN_WIDTH/HEIGHT` in `/etc/gamescope-session-plus/sessions.d/steam`), not the client's. **own** gamescope at the *client's* exact resolution and refresh. Client-mode-following, but there
- **Managed** (`PUNKTFUNK_GAMESCOPE_MANAGED=1`, and remove the attach line) — the host tears the must be no physical gaming session already running.
box's gamescope down on connect and launches its **own** at the *client's* exact resolution and
refresh, restoring on idle. Client-mode-following, but it can't coexist with a box-owned game-mode Full treatment: [Steam / gamescope → Attach vs managed](/docs/gamescope#attach-vs-managed).
session, and there must be **no physical gaming session already running**.
Mid-stream Gaming ↔ Desktop following (`PUNKTFUNK_SESSION_WATCH`) is **on by default** on Mid-stream Gaming ↔ Desktop following (`PUNKTFUNK_SESSION_WATCH`) is **on by default** on
Bazzite/SteamOS. See [Configuration](/docs/configuration) for the full list of knobs. Bazzite/SteamOS. See [Configuration](/docs/configuration) for the full list of knobs.
@@ -116,8 +115,8 @@ Bazzite/SteamOS. See [Configuration](/docs/configuration) for the full list of k
The **virtual output** (video) for the Desktop session needs no config — the host package ships an The **virtual output** (video) for the Desktop session needs no config — the host package ships an
`io.unom.Punktfunk.Host.desktop` file whose `X-KDE-Wayland-Interfaces` grants the host KWin's `io.unom.Punktfunk.Host.desktop` file whose `X-KDE-Wayland-Interfaces` grants the host KWin's
restricted screencast protocol on a normal interactive Plasma session (least-privilege, the same restricted screencast protocol on a normal interactive Plasma session (background:
mechanism krfb/krdp use). After a **fresh host install, log out and back into the Desktop session [KDE Plasma](/docs/kde)). After a **fresh host install, log out and back into the Desktop session
once** so KWin re-reads that grant. once** so KWin re-reads that grant.
The one thing a normal KDE login lacks is the RemoteDesktop grant for headless **input** injection. The one thing a normal KDE login lacks is the RemoteDesktop grant for headless **input** injection.
@@ -138,26 +137,11 @@ Desktop; it follows whichever the box is in.
```sh ```sh
systemctl --user enable --now punktfunk-host systemctl --user enable --now punktfunk-host
# Web console (pairing + status) — enable it and read the auto-generated login password, systemctl --user enable --now punktfunk-web # web console: pairing + status
# then open http://<host-ip>:47992:
systemctl --user enable --now punktfunk-web
journalctl --user -u punktfunk-web-init | sed -n 's/.*password generated: //p'
``` ```
### Console login password Then open [The Web Console](/docs/web-console) for the login password and to
[arm pairing](/docs/web-console#arm-pairing).
The console is password-protected. On first start `punktfunk-web-init` generates a random login
password and saves it to `~/.config/punktfunk/web-password` (as `PUNKTFUNK_UI_PASSWORD=…`). Read it
back at any time — from the init service's journal, or straight from the file:
```sh
journalctl --user -u punktfunk-web-init | sed -n 's/.*password generated: //p'
sed -n 's/^PUNKTFUNK_UI_PASSWORD=//p' ~/.config/punktfunk/web-password
```
To set your own password, edit that file (`PUNKTFUNK_UI_PASSWORD=<your-password>`) and restart the
console: `systemctl --user restart punktfunk-web`. Forgot it? This is the recovery path linked from
the console login screen — see [Forgot your Password?](/docs/forgot-password).
## Good to know ## Good to know
@@ -170,5 +154,7 @@ These apply to the **Gaming Mode (gamescope)** path; the KDE Desktop path is una
- **HDR isn't supported yet** on the gamescope path — gamescope's capture output is 8-bit. SDR streams - **HDR isn't supported yet** on the gamescope path — gamescope's capture output is 8-bit. SDR streams
normally. normally.
Canonical list: [gamescope → Known limits](/docs/gamescope#known-limits).
Then [connect a client](/docs/clients) — Moonlight works great for couch gaming, and the Apple app for Then [connect a client](/docs/clients) — Moonlight works great for couch gaming, and the Apple app for
Apple TV / iPad. Apple TV / iPad.
+4 -2
View File
@@ -48,8 +48,8 @@ let you pick a mode or default to the device's display.)
## gamescope / session following (Linux, Bazzite/SteamOS) ## gamescope / session following (Linux, Bazzite/SteamOS)
Two mutually-exclusive models for a Steam/gamescope box. See [Bazzite](/docs/bazzite) for the full Two mutually-exclusive models for a Steam/gamescope box. See [Steam / gamescope](/docs/gamescope) for
picture. the full picture (and [Bazzite](/docs/bazzite) for that distro's specifics).
| Setting | Values | Meaning | | Setting | Values | Meaning |
|---|---|---| |---|---|---|
@@ -62,6 +62,8 @@ picture.
## Compositor-specific (Linux) ## Compositor-specific (Linux)
See your desktop page ([KDE](/docs/kde), [GNOME](/docs/gnome)) for when to set these.
> **Managing virtual displays** — keep-alive after disconnect, exclusive vs. extend, and (on > **Managing virtual displays** — keep-alive after disconnect, exclusive vs. extend, and (on
> Windows/KDE) persistent per-client scaling — now has its own settings surface in the web console > Windows/KDE) persistent per-client scaling — now has its own settings surface in the web console
> and `display-settings.json`. See [Virtual displays](/docs/virtual-displays). The two > and `display-settings.json`. See [Virtual displays](/docs/virtual-displays). The two
@@ -1,24 +1,25 @@
--- ---
title: Fedora — KDE Plasma title: Fedora
description: Reproducible punktfunk host setup on Fedora KDE (KWin) via the RPM. description: Install the punktfunk host on Fedora from the RPM registry.
--- ---
Set up a punktfunk host on **Fedora KDE** (the KDE Plasma spin). The host runs as an RPM-managed Install a punktfunk host on **Fedora** from the self-hosted RPM registry. The host installs as an
systemd service and uses KWin to create per-client virtual displays, captured zero-copy RPM-managed systemd **`--user`** service and updates with `dnf upgrade` like the rest of your
(dmabuf → CUDA → NVENC) on NVIDIA. system — no building required. It works with either **KDE Plasma** or **GNOME**; the
desktop-specific setup (which compositor captures, headless sessions, quirks) lives on the
> Validated live on **Fedora 44 KDE Plasma** with an RTX 4090: KWin virtual output + full [desktop configure pages](#3-configure-your-desktop). Host encode is **NVENC on NVIDIA** and **VAAPI on
> zero-copy capture. Everything below is the reproducible flow — paste it on a fresh box. AMD/Intel** (`PUNKTFUNK_ENCODER=auto` picks per GPU).
> New here? Read [Security & Safe Use](/docs/security) first — a streaming host is remote control of > New here? Read [Security & Safe Use](/docs/security) first — a streaming host is remote control of
> the machine, so keep it on a trusted LAN or VPN and require pairing. > the machine, so keep it on a trusted LAN or VPN and require pairing.
The setup has three parts: **NVIDIA driver****host RPM****KWin streaming session**. Install is two parts: **GPU driver****host RPM**. Then point the host at your desktop from the
[desktop configure pages](#3-configure-your-desktop).
## 1. NVIDIA driver (RPM Fusion akmod) ## 1. NVIDIA driver (RPM Fusion akmod)
Enable RPM Fusion (free + nonfree), then install the akmod driver + CUDA. RPM Fusion's nonfree Enable RPM Fusion (free + nonfree), then install the akmod driver + CUDA. RPM Fusion's nonfree
NVIDIA repo is sometimes pre-enabled on the KDE spin; the full free/nonfree repos below are still NVIDIA repo is sometimes pre-enabled on some spins; the full free/nonfree repos below are still
needed (they carry the NVENC ffmpeg in the next step). needed (they carry the NVENC ffmpeg in the next step).
```sh ```sh
@@ -55,6 +56,11 @@ ffmpeg -hide_banner -encoders | grep nvenc
(Or disable Secure Boot in firmware to skip the MOK step — fine for a dedicated test box.) (Or disable Secure Boot in firmware to skip the MOK step — fine for a dedicated test box.)
**AMD / Intel (VAAPI).** No akmod needed — the Mesa stack provides the VAAPI encoder. Install the
freeworld VAAPI drivers for full codec support (`mesa-va-drivers-freeworld` for AMD from RPM Fusion,
`intel-media-driver` for Intel); on a desktop these are usually already present. The host auto-picks
VAAPI on these GPUs.
## 2. Install the host (RPM) ## 2. Install the host (RPM)
The host is published to the self-hosted Gitea RPM registry, in a per-Fedora-release group (an RPM The host is published to the self-hosted Gitea RPM registry, in a per-Fedora-release group (an RPM
@@ -85,71 +91,37 @@ udev rule, the UDP socket-buffer sysctl tuning, and example configs.
> `docker build --build-arg FEDORA_VERSION=NN -f ci/fedora-rpm.Dockerfile -t pf-rpm ci` then run > `docker build --build-arg FEDORA_VERSION=NN -f ci/fedora-rpm.Dockerfile -t pf-rpm ci` then run
> `packaging/rpm/build-rpm.sh` inside it — or build from source (appendix below). > `packaging/rpm/build-rpm.sh` inside it — or build from source (appendix below).
## 3. KWin streaming session ## 3. Configure your desktop
KWin's virtual-output capture uses its **privileged** `zkde_screencast` protocol, which an How the host creates its virtual display and injects input depends on your desktop, not your distro.
*interactive* Plasma session will not hand to an external client. So the host streams from a Continue on the page for the desktop you run — it covers your `host.env`, any compositor quirks, and
**dedicated headless KWin session** (`kwin --virtual` launched with starting the host:
`KWIN_WAYLAND_NO_PERMISSION_CHECKS=1`) — shipped as `punktfunk-kde-session.service`. This also
makes the box a self-contained appliance: it streams at boot with no graphical login.
```sh - [KDE Plasma (KWin)](/docs/kde)
# KWin appliance config (ships with the package): - [GNOME (Mutter)](/docs/gnome)
mkdir -p ~/.config/punktfunk - [Steam / gamescope](/docs/gamescope)
cp /usr/share/punktfunk/host.env.kde ~/.config/punktfunk/host.env - [Sway / wlroots](/docs/sway)
# Start the headless KWin session + the host, and start user units at boot without a login: Enable the browser management console (status, paired devices, arm pairing) — see
systemctl --user daemon-reload [Web Console](/docs/web-console).
systemctl --user enable --now punktfunk-kde-session punktfunk-host
sudo loginctl enable-linger "$USER"
```
Check it came up: For a headless KWin appliance that streams at boot with no graphical login, see
[KDE → Headless session](/docs/kde#headless-session).
```sh Full config reference: [Configuration](/docs/configuration). Service model:
systemctl --user status punktfunk-host # active [Running as a Service](/docs/running-as-a-service).
journalctl --user -u punktfunk-host -f # watch a client connect
```
The host now listens on `9777` (native punktfunk/1) + the GameStream ports, and advertises over
mDNS. It requires **PIN pairing** by default (secure on a LAN); pair once from your client.
### Web console
The console (status, paired devices, arm pairing) ships as `punktfunk-web` — enable it, then open
`http://<host-ip>:47992`:
```sh
systemctl --user enable --now punktfunk-web
```
#### Console login password
The console is password-protected. On first start `punktfunk-web-init` generates a random login
password and saves it to `~/.config/punktfunk/web-password` (as `PUNKTFUNK_UI_PASSWORD=…`). Read it
back at any time — from the init service's journal, or straight from the file:
```sh
journalctl --user -u punktfunk-web-init | sed -n 's/.*password generated: //p'
sed -n 's/^PUNKTFUNK_UI_PASSWORD=//p' ~/.config/punktfunk/web-password
```
To set your own password, edit that file (`PUNKTFUNK_UI_PASSWORD=<your-password>`) and restart the
console: `systemctl --user restart punktfunk-web`. Forgot it? This is the recovery path linked from
the console login screen — see [Forgot your Password?](/docs/forgot-password).
## 4. Connect a client ## 4. Connect a client
From any [client](/docs/clients) `punktfunk-client --discover` finds the host on the LAN. On From any [client](/docs/clients), `--discover` finds the host on the LAN. On first connect, complete
first connect, complete the PIN pairing — **arm it from the host's web console / mgmt API**, which the **PIN pairing** — arm it from the host's [web console](/docs/web-console#arm-pairing), which
makes the host display a 4-digit PIN to type into the client. (Pairing is required by default; pass displays a 4-digit PIN to type into the client. See [Clients](/docs/clients) and
`serve --open` only if you deliberately want to disable the requirement.) See [Pairing](/docs/pairing).
[Clients](/docs/clients) and [Running as a Service](/docs/running-as-a-service).
## Appendix — build from source ## Appendix — build from source
If there's no RPM for your Fedora release and you don't want to build one, compile the host If there's no RPM for your Fedora release and you don't want to build one, compile the host directly
directly (no clean updates / no packaged units — you wire those up by hand): (no clean updates / no packaged units — you wire those up by hand):
```sh ```sh
sudo dnf install gcc gcc-c++ make cmake clang clang-devel nasm git \ sudo dnf install gcc gcc-c++ make cmake clang clang-devel nasm git \
@@ -162,4 +134,5 @@ cargo build --release -p punktfunk-host
``` ```
Then write `~/.config/punktfunk/host.env` (as in `/usr/share/punktfunk/host.env.kde`, but the host Then write `~/.config/punktfunk/host.env` (as in `/usr/share/punktfunk/host.env.kde`, but the host
binary is `target/release/punktfunk-host`) and run it inside the KWin session from step 3. binary is `target/release/punktfunk-host`) and run it inside your desktop session — for a headless
KWin appliance see [KDE → Headless session](/docs/kde#headless-session).
+6 -7
View File
@@ -8,21 +8,20 @@ password. That password is generated — or, on Windows, chosen — when the con
it lives on the **host**. So if you can't get past the login screen, you recover or change it on the it lives on the **host**. So if you can't get past the login screen, you recover or change it on the
host machine itself, not from the browser. host machine itself, not from the browser.
New to the console? See [The Web Console](/docs/web-console) to enable it and arm pairing.
> This is **only** the web console login. It is **not** your client/device pairing — if a client > This is **only** the web console login. It is **not** your client/device pairing — if a client
> won't connect, that's [Pairing](/docs/pairing), not this password. > won't connect, that's [Pairing](/docs/pairing), not this password.
## Find your host ## Find your host
Jump to your host platform for exactly where the password lives and how to read or reset it: Find your host platform for exactly where the password lives, then read or reset it below:
| Host | Where the password lives | Section | | Host | Where the password lives | Section |
|------|--------------------------|---------| |------|--------------------------|---------|
| **Ubuntu — GNOME** | `~/.config/punktfunk/web-password` | [Console login password](/docs/ubuntu-gnome#console-login-password) | | **Linux packages (apt / RPM / Bazzite)** | `~/.config/punktfunk/web-password` | [Login password](/docs/web-console#login-password) |
| **Ubuntu — KDE Plasma** | `~/.config/punktfunk/web-password` | [Console login password](/docs/ubuntu-kde#console-login-password) | | **SteamOS (host)** | `~/.config/punktfunk/web.env` | [Login password](/docs/web-console#login-password) |
| **Fedora — KDE Plasma** | `~/.config/punktfunk/web-password` | [Console login password](/docs/fedora-kde#console-login-password) | | **Windows host** | `%ProgramData%\punktfunk\web-password` | [Login password](/docs/web-console#login-password) · [Windows Host](/docs/windows-host) |
| **Bazzite — gamescope** | `~/.config/punktfunk/web-password` | [Console login password](/docs/bazzite#console-login-password) |
| **SteamOS (host)** | `~/.config/punktfunk/web.env` | [Console login password](/docs/steamos-host#console-login-password) |
| **Windows host** | `%ProgramData%\punktfunk\web-password` | [Console login password](/docs/windows-host#console-login-password) |
## The short version ## The short version
+77
View File
@@ -0,0 +1,77 @@
---
title: Steam / gamescope
description: Configure a gamescope/Steam host — attach vs managed, session following, and limits.
---
gamescope is the compositor behind Steam **Gaming Mode** — the couch/handheld game UI on Bazzite,
SteamOS, or any distro running a gamescope session. The host **auto-detects** gamescope from your
live session, so you rarely need to set anything here. It also **follows a Gaming ↔ Desktop switch
mid-stream** — flip between Gaming Mode and the desktop with Steam's normal UI and the host
re-targets whatever's running without a reconnect.
This page covers the gamescope-specific choices. To get a host running on an appliance box, start
from the install guide for your OS: [Bazzite](/docs/bazzite) or [SteamOS (Host)](/docs/steamos-host).
> New here? Read [Security & Safe Use](/docs/security) first — a streaming host is remote control of
> the machine, so keep it on a trusted LAN or VPN and require pairing.
## Attach vs managed
There are two mutually-exclusive models for a gamescope box; pick one. The shipped default is
**attach**.
- **Attach** (`PUNKTFUNK_GAMESCOPE_ATTACH=1`, the default) — the **box** owns its gamescope session
and decides Gaming vs Desktop via the normal Steam UI. The host just attaches to whatever's live
and never tears it down, so switching Desktop ↔ Game is rock-solid and disconnecting leaves the box
where it was. The streamed game-mode resolution is the box's gamescope mode
(`SCREEN_WIDTH/HEIGHT` in `/etc/gamescope-session-plus/sessions.d/steam`), not the client's.
- **Managed** (`PUNKTFUNK_GAMESCOPE_MANAGED=1`, and remove the attach line) — the host tears the
box's gamescope down on connect and launches its **own** at the *client's* exact resolution and
refresh, restoring on idle. Client-mode-following, but it can't coexist with a box-owned game-mode
session, and there must be **no physical gaming session already running**.
## Session following
`PUNKTFUNK_SESSION_WATCH` follows a Gaming ↔ Desktop switch **mid-stream** — the host rebuilds the
backend in place, with no reconnect. It is **on by default** on Bazzite/SteamOS; set `0` to disable.
One host service covers both faces of the box: it streams Gaming Mode over gamescope and the desktop
over its own compositor, and re-targets whichever is live on each switch.
## Start the host
On an appliance box (Bazzite, SteamOS) the install guide already enables the host service for you. On
any other distro running a gamescope session, start it from your session — the default attach model
just latches onto whatever gamescope session is live:
```sh
systemctl --user enable --now punktfunk-host
```
Then bring up [The Web Console](/docs/web-console) to arm pairing.
## gamescope knobs
The gamescope-specific settings in `host.env`. Leave them unset to auto-detect; set one only to force
a model. See the full [Configuration reference](/docs/configuration) for every other knob.
| Setting | Values | Meaning |
|---|---|---|
| `PUNKTFUNK_GAMESCOPE_ATTACH` | `1` | **Attach** model: the box owns its gamescope session; the host captures whatever's live and never tears it down. Streamed resolution is the box's gamescope mode. The default. |
| `PUNKTFUNK_GAMESCOPE_MANAGED` | `1` | **Managed** model: the host tears the box's gamescope down on connect and launches its own at the client's exact mode, restoring on idle. Doesn't coexist with a box-owned game-mode session. |
| `PUNKTFUNK_GAMESCOPE_SESSION` | `steam` | The host owns a `gamescope-session-plus` (Steam) session at the client's mode — a headless appliance with no physical session running. |
| `PUNKTFUNK_GAMESCOPE_NODE` | `auto` · node id | Discover and capture a **running** gamescope's PipeWire node at a fixed mode. Do **not** combine with `SESSION`. |
| `PUNKTFUNK_GAMESCOPE_APP` | command | For an ad-hoc bare-gamescope session, the nested command to run (e.g. `vkcube`). |
| `PUNKTFUNK_SESSION_WATCH` | `1` · `0` | Follow a Gaming ↔ Desktop switch mid-stream (rebuild in place, no reconnect). On by default on Bazzite/SteamOS; set `0` to disable. |
## Known limits
These apply to the **Gaming Mode (gamescope)** path only; the desktop path is unaffected.
- **gamescope 3.16.22 or newer is required.** Older versions can deadlock during capture. Bazzite's
and SteamOS's current gamescope is fine; this only bites if you've pinned an old one.
- **The mouse cursor isn't included in the captured image** — a gamescope limitation for now.
- **HDR isn't supported on the gamescope path** — gamescope's capture output is 8-bit. SDR streams
normally.
To stream the KDE Plasma desktop of a Steam box instead, see [KDE Plasma](/docs/kde). To bring up the
web console and pair a client, see [The Web Console](/docs/web-console).
+96
View File
@@ -0,0 +1,96 @@
---
title: GNOME (Mutter)
description: Configure a punktfunk host for GNOME — host.env, the EGL/lock traps, and a headless session.
---
Configure a host running **GNOME**. The host drives GNOME's Mutter compositor to create a per-client
virtual display over D-Bus (`RecordVirtual`), zero-copy. This page assumes the host is already
installed — see [Ubuntu](/docs/ubuntu), [Fedora](/docs/fedora), or [Arch](/docs/arch).
> New here? Read [Security & Safe Use](/docs/security) first — a streaming host is remote control of
> the machine, so keep it on a trusted LAN or VPN and require pairing.
## host.env
Write `~/.config/punktfunk/host.env` with the GNOME settings. The host auto-detects the compositor
from your session, so the explicit `PUNKTFUNK_COMPOSITOR` is belt-and-braces:
```ini
# ~/.config/punktfunk/host.env
WAYLAND_DISPLAY=wayland-0
XDG_CURRENT_DESKTOP=GNOME
PUNKTFUNK_COMPOSITOR=mutter
PUNKTFUNK_VIDEO_SOURCE=virtual
PUNKTFUNK_ZEROCOPY=1
PUNKTFUNK_INPUT_BACKEND=libei
```
You must be on a **Wayland** session (not X11), and Mutter must be **≥ 48**. See the
[Configuration reference](/docs/configuration) for every option.
## The GL/EGL userspace
On NVIDIA, gnome-shell fails to start — or the host logs **"GPU … not supported by EGL"** — when the
NVIDIA GL/EGL userspace is missing. The base driver package doesn't always pull it in. Install your
distro's NVIDIA GL/EGL userspace package — on **Ubuntu/Debian** it's `libnvidia-gl-<version>` matching
your driver; on **Fedora/Arch** it ships with the RPM Fusion / repo driver — then confirm the glvnd
vendor file exists:
```sh
ls /usr/share/glvnd/egl_vendor.d/10_nvidia.json # must exist
```
Installing the driver itself is covered on your distro's install page
([Ubuntu](/docs/ubuntu), [Fedora](/docs/fedora), [Arch](/docs/arch)).
## Do not lock the session
A **locked** GNOME session blocks screen capture — the host fails with
**"Session creation inhibited"**. On an always-on or headless host there's no one to unlock it, so
disable the lock:
```sh
gsettings set org.gnome.desktop.screensaver lock-enabled false
gsettings set org.gnome.desktop.session idle-delay 0
```
## Start the host
With `host.env` in place, start the host from **inside your GNOME session**:
```sh
systemctl --user enable --now punktfunk-host
journalctl --user -u punktfunk-host -f # watch it come up and print its identity fingerprint
```
Then bring up [The Web Console](/docs/web-console) to arm pairing and connect a
[client](/docs/clients). For an always-on box, see the [headless session](#headless-session) below.
## Headless session
To run with no monitor and no login, keep a GNOME Wayland session up at all times and start the host
without a login. Have GDM auto-login your user:
```ini
# /etc/gdm3/custom.conf (Ubuntu) · /etc/gdm/custom.conf (Fedora)
[daemon]
AutomaticLoginEnable = true
AutomaticLogin = your-user
```
Disable the lock (see [above](#do-not-lock-the-session)), then enable the host user service and let it
linger past logout:
```sh
systemctl --user enable --now punktfunk-host
sudo loginctl enable-linger "$USER"
```
Reboot and the host comes up on the auto-login session. Full walkthrough:
[Running as a Service](/docs/running-as-a-service).
## Troubleshooting
More fixes — black screen, discovery, pairing — in [Troubleshooting](/docs/troubleshooting).
Once the host is up, bring the console up and pair — see [The Web Console](/docs/web-console).
+1 -1
View File
@@ -68,4 +68,4 @@ LAN.
## Multiple devices at once ## Multiple devices at once
A host can stream to several clients simultaneously — your laptop and your TV both viewing (and A host can stream to several clients simultaneously — your laptop and your TV both viewing (and
controlling) the desktop, each at its own resolution. See [Multiple devices](/docs/configuration#multiple-devices). controlling) the desktop, each at its own resolution. See [Multiple devices](/docs/configuration#multiple-devices-at-once).
+2 -2
View File
@@ -29,7 +29,7 @@ It's built for the things that make streaming feel native:
<Cards> <Cards>
<Card title="How It Works" href="/docs/how-it-works" description="The ideas behind punktfunk in a few minutes — virtual displays, the two protocols, pairing." /> <Card title="How It Works" href="/docs/how-it-works" description="The ideas behind punktfunk in a few minutes — virtual displays, the two protocols, pairing." />
<Card title="Quick Start" href="/docs/quickstart" description="From nothing to streaming: set up a host and connect your first client." /> <Card title="Quick Start" href="/docs/quickstart" description="From nothing to streaming: set up a host and connect your first client." />
<Card title="Host Setup" href="/docs/requirements" description="Install the host on Ubuntu (GNOME or KDE), Fedora (KDE), or Bazzite." /> <Card title="Host Setup" href="/docs/requirements" description="Install on Ubuntu, Fedora, Arch, Bazzite, SteamOS, or Windows." />
<Card title="Connect a Client" href="/docs/clients" description="Stream with the native app for your device — macOS, Linux, Windows, Android — or any Moonlight client." /> <Card title="Connect a Client" href="/docs/clients" description="Stream with the native app for your device — macOS, Linux, Windows, Android — or any Moonlight client." />
<Card title="API Reference" href="/api" description="Interactive OpenAPI reference for the host's management REST API — status, devices, pairing, library." /> <Card title="API Reference" href="/api" description="Interactive OpenAPI reference for the host's management REST API — status, devices, pairing, library." />
</Cards> </Cards>
@@ -37,7 +37,7 @@ It's built for the things that make streaming feel native:
## What you need ## What you need
- A **host** with a supported GPU — either a **Linux** machine running one of the - A **host** with a supported GPU — either a **Linux** machine running one of the
[supported setups](/docs/requirements) (**Ubuntu** GNOME or KDE, **Fedora** KDE, or **Bazzite**), or [supported setups](/docs/requirements) (**Ubuntu**, **Fedora**, **Arch**, or **Bazzite**), or
a **[Windows](/docs/windows-host) PC**. a **[Windows](/docs/windows-host) PC**.
- A **client device** to stream to — there are native apps for **macOS, iOS/iPadOS, tvOS, Linux, - A **client device** to stream to — there are native apps for **macOS, iOS/iPadOS, tvOS, Linux,
Windows, and Android**, plus any device that runs **Moonlight**. Windows, and Android**, plus any device that runs **Moonlight**.
+13 -4
View File
@@ -16,9 +16,9 @@ On **Windows**, the host ships as a signed installer instead — see [Windows](#
| Distro | Package manager | One-command happy path | Guide | | Distro | Package manager | One-command happy path | Guide |
|--------|-----------------|------------------------|-------| |--------|-----------------|------------------------|-------|
| **Ubuntu / Debian** | apt | `sudo apt install punktfunk-host` | [Ubuntu — GNOME](/docs/ubuntu-gnome) · [Ubuntu — KDE](/docs/ubuntu-kde) · [packaging/debian](https://git.unom.io/unom/punktfunk/src/branch/main/packaging/debian/README.md) | | **Ubuntu / Debian** | apt | `sudo apt install punktfunk-host` | [Ubuntu / Debian](/docs/ubuntu) · [packaging/debian](https://git.unom.io/unom/punktfunk/src/branch/main/packaging/debian/README.md) |
| **Bazzite / Fedora Atomic** | systemd-sysext | `sudo bash punktfunk-sysext.sh install` (no layering, no reboot) | [Bazzite](/docs/bazzite) · [packaging/bazzite](https://git.unom.io/unom/punktfunk/src/branch/main/packaging/bazzite/README.md) | | **Bazzite / Fedora Atomic** | systemd-sysext | `sudo bash punktfunk-sysext.sh install` (no layering, no reboot) | [Bazzite](/docs/bazzite) · [packaging/bazzite](https://git.unom.io/unom/punktfunk/src/branch/main/packaging/bazzite/README.md) |
| **Fedora (dnf)** | dnf / rpm-ostree | `dnf install punktfunk punktfunk-web` | [Fedora — KDE](/docs/fedora-kde) · [packaging/rpm](https://git.unom.io/unom/punktfunk/src/branch/main/packaging/rpm/README.md) | | **Fedora (dnf)** | dnf / rpm-ostree | `dnf install punktfunk punktfunk-web` | [Fedora](/docs/fedora) · [packaging/rpm](https://git.unom.io/unom/punktfunk/src/branch/main/packaging/rpm/README.md) |
| **Arch** | pacman | `pacman -Sy punktfunk-host` (binary repo) | [Arch Linux](/docs/arch) · [packaging/arch](https://git.unom.io/unom/punktfunk/src/branch/main/packaging/arch/README.md) | | **Arch** | pacman | `pacman -Sy punktfunk-host` (binary repo) | [Arch Linux](/docs/arch) · [packaging/arch](https://git.unom.io/unom/punktfunk/src/branch/main/packaging/arch/README.md) |
| **SteamOS (host)** | on-device script | `bash scripts/steamdeck/install.sh` | [SteamOS (Host)](/docs/steamos-host) | | **SteamOS (host)** | on-device script | `bash scripts/steamdeck/install.sh` | [SteamOS (Host)](/docs/steamos-host) |
@@ -79,13 +79,22 @@ fallback without one. More detail — including the CLI `punktfunk-host service
Bare `serve` is the secure native-only default (native `punktfunk/1` + the web console). On a Bare `serve` is the secure native-only default (native `punktfunk/1` + the web console). On a
trusted LAN, add `--gamestream` to also serve stock [Moonlight](/docs/moonlight) clients. trusted LAN, add `--gamestream` to also serve stock [Moonlight](/docs/moonlight) clients.
3. Enable the web console and read its login password, then open `http://<host-ip>:47992`: 3. Enable the web console:
```sh ```sh
systemctl --user enable --now punktfunk-web systemctl --user enable --now punktfunk-web
journalctl --user -u punktfunk-web-init | sed -n 's/.*password generated: //p'
``` ```
Then open `http://<host-ip>:47992`. Reading its [login password](/docs/web-console#login-password)
and [arming PIN pairing](/docs/web-console#arm-pairing) are covered in
[The Web Console](/docs/web-console).
### Configure your desktop
How the virtual display and input work depends on your desktop — see [KDE](/docs/kde),
[GNOME](/docs/gnome), [Steam / gamescope](/docs/gamescope), or [Sway](/docs/sway) for the
compositor-specific setup.
From there, follow the [Quick Start](/docs/quickstart) to pair your first client. To run the host From there, follow the [Quick Start](/docs/quickstart) to pair your first client. To run the host
automatically at boot, see [Running as a Service](/docs/running-as-a-service). automatically at boot, see [Running as a Service](/docs/running-as-a-service).
+113
View File
@@ -0,0 +1,113 @@
---
title: KDE Plasma (KWin)
description: Configure a punktfunk host for KDE — host.env, quirks, and a headless KWin session.
---
Configure a punktfunk host on **KDE Plasma**. The host uses KDE's KWin compositor to create a
per-client virtual display, captured zero-copy on NVIDIA. This page assumes the package is already
installed — see [Ubuntu](/docs/ubuntu), [Fedora](/docs/fedora), [Arch](/docs/arch), or the
[Bazzite](/docs/bazzite) appliance.
> New here? Read [Security & Safe Use](/docs/security) first — a streaming host is remote control of
> the machine, so keep it on a trusted LAN or VPN and require pairing.
## host.env
A KDE starter `~/.config/punktfunk/host.env`:
```ini
WAYLAND_DISPLAY=wayland-0
XDG_CURRENT_DESKTOP=KDE
PUNKTFUNK_COMPOSITOR=kwin
PUNKTFUNK_VIDEO_SOURCE=virtual
PUNKTFUNK_ZEROCOPY=1
PUNKTFUNK_INPUT_BACKEND=libei
```
The host auto-detects the running compositor on every connect, so most of this is optional — the
values above are just what it resolves to on a KWin session. See the
[Configuration reference](/docs/configuration) for every option.
## Use a Wayland session
KDE must run on **Wayland**, not X11 — pick the Wayland session from the picker on the login screen.
The virtual-display path is Wayland-only and will not come up under X11.
KWin must be **6.5.6 or newer** (virtual outputs land there). Check with:
```sh
kwin_wayland --version
```
## Streaming the interactive desktop
To stream a logged-in Plasma desktop (rather than a headless session, below), KWin has to hand the
host its restricted screencast protocol. The host package ships an `io.unom.Punktfunk.Host.desktop`
file whose `X-KDE-Wayland-Interfaces` grants exactly that on a normal interactive session
(least-privilege, the same mechanism krfb/krdp use). After a **fresh install, log out and back into
the Desktop session once** so KWin re-reads the grant.
A normal KDE login still lacks the RemoteDesktop grant that **input** injection needs — without it the
host pops an "Allow remote control?" dialog no headless box can answer. **Fedora and Bazzite** ship a
one-shot helper that seeds it (run once as the streaming user, no root):
```sh
bash /usr/share/punktfunk/bazzite/kde-desktop-setup.sh # Fedora / Bazzite
```
The `.deb` and Arch packages don't include that wrapper. Seed the grant by hand instead — copy the
shipped `kde-authorized` file into the portal store (the share dir is `/usr/share/punktfunk-host` on
Debian/Ubuntu, `/usr/share/punktfunk` on Arch), then log out and back in:
```sh
mkdir -p ~/.local/share/flatpak/db
cp /usr/share/punktfunk*/headless/kde-authorized ~/.local/share/flatpak/db/kde-authorized
```
A login-less appliance skips all of this — its headless session (below) needs none of these grants.
## Start the host
With `host.env` in place, start the host from **inside your Plasma session**:
```sh
systemctl --user enable --now punktfunk-host
journalctl --user -u punktfunk-host -f # watch it come up and print its identity fingerprint
```
Then bring up [The Web Console](/docs/web-console) to arm pairing and connect a
[client](/docs/clients). To start at boot — including fully headless — see the
[headless session](#headless-session) below or [Running as a Service](/docs/running-as-a-service).
## Persistent per-client scaling
KWin round-trips per-client display scale: it names each session's virtual output per client, so a
scale you set for one client (150 %, 125 %, …) is reapplied on that client's next connect. See
[Virtual displays](/docs/virtual-displays).
## Headless session
For a login-less appliance — a box that streams at boot with no graphical login — the host brings up a
**dedicated headless KWin session** rather than relying on an interactive one. It runs its own
`kwin --virtual` session (shipped as the `punktfunk-kde-session.service` unit) with permission checks
relaxed, so it needs none of the interactive grants above.
```sh
mkdir -p ~/.config/punktfunk
cp /usr/share/punktfunk/host.env.kde ~/.config/punktfunk/host.env # Debian/Ubuntu: /usr/share/punktfunk-host/host.env.kde
systemctl --user daemon-reload
systemctl --user enable --now punktfunk-kde-session punktfunk-host
sudo loginctl enable-linger "$USER"
```
The session unit brings up headless KWin; the host unit follows it and starts listening. See
[Running as a Service](/docs/running-as-a-service) for the full headless setup.
## Troubleshooting
- **KWin too old:** virtual outputs need KWin **≥ 6.5.6**. Check with `kwin_wayland --version`.
- **Black screen / no picture:** confirm you're on a Wayland session (not X11) and the NVIDIA GL
userspace is installed. More in [Troubleshooting](/docs/troubleshooting).
To bring the console up and pair, see [The Web Console](/docs/web-console).
+13 -9
View File
@@ -5,27 +5,31 @@
"how-it-works", "how-it-works",
"security", "security",
"quickstart", "quickstart",
"install", "---Install the host---",
"---Host Setup---",
"requirements", "requirements",
"ubuntu-gnome", "install",
"ubuntu-kde", "ubuntu",
"fedora-kde", "fedora",
"arch", "arch",
"bazzite", "bazzite",
"steamos-host", "steamos-host",
"windows-host", "windows-host",
"web-console",
"---Configure your desktop---",
"configuration",
"kde",
"gnome",
"gamescope",
"sway",
"running-as-a-service", "running-as-a-service",
"virtual-displays",
"host-cli",
"---Connecting---", "---Connecting---",
"clients", "clients",
"install-client", "install-client",
"steam-deck", "steam-deck",
"moonlight", "moonlight",
"pairing", "pairing",
"---Configuration---",
"configuration",
"virtual-displays",
"host-cli",
"---Troubleshooting---", "---Troubleshooting---",
"troubleshooting", "troubleshooting",
"stats", "stats",
+14 -10
View File
@@ -11,15 +11,19 @@ This is the shortest path to a working stream. Each step links to the details.
## 1. Set up the host ## 1. Set up the host
On your Linux gaming machine (NVIDIA, AMD, or Intel GPU), follow the guide for your system: On your gaming machine (NVIDIA, AMD, or Intel GPU), follow the install guide for your system:
- [Ubuntu — GNOME](/docs/ubuntu-gnome) - [Ubuntu / Debian](/docs/ubuntu)
- [Ubuntu — KDE Plasma](/docs/ubuntu-kde) - [Fedora](/docs/fedora)
- [Fedora — KDE Plasma](/docs/fedora-kde) - [Arch](/docs/arch)
- [Bazzite — gamescope / Steam](/docs/bazzite) - [Bazzite](/docs/bazzite)
- [SteamOS](/docs/steamos-host)
- [Windows host](/docs/windows-host)
Each one covers the GPU driver, the dependencies, and how to build and run the host. Check the Each one covers the GPU driver, the dependencies, and how to install and run the host. After
[Requirements](/docs/requirements) first if you're not sure your machine is a fit. installing, configure for your desktop ([KDE](/docs/kde) / [GNOME](/docs/gnome) /
[gamescope](/docs/gamescope) / [Sway](/docs/sway)). Check the [Requirements](/docs/requirements)
first if you're not sure your machine is a fit.
## 2. Start the host ## 2. Start the host
@@ -44,9 +48,9 @@ latency, or any Moonlight client:
the list of hosts found on your network. Select it, and when prompted, **pair**. the list of hosts found on your network. Select it, and when prompted, **pair**.
- **Anything with Moonlight:** add the host (it should be discovered automatically), then pair. - **Anything with Moonlight:** add the host (it should be discovered automatically), then pair.
To pair, the host needs to show a PIN. Arm pairing from the host's web console — the host displays a To pair, the host needs to show a PIN. [Arm pairing](/docs/web-console#arm-pairing) from the host's
4-digit PIN, you type it into the client, and they trust each other from then on. Pairing is required web console — the host displays a 4-digit PIN, you type it into the client, and they trust each other
by default. Full details: [Pairing & Trust](/docs/pairing). from then on. Pairing is required by default. Full details: [Pairing & Trust](/docs/pairing).
## 4. Stream ## 4. Stream
+28 -20
View File
@@ -6,19 +6,31 @@ description: What you need to run a punktfunk host — GPU, driver, desktop, and
## Supported setups ## Supported setups
A punktfunk host runs primarily on a Linux machine with a dedicated GPU — NVIDIA (NVENC) is the A punktfunk host runs primarily on a Linux machine with a dedicated GPU — NVIDIA (NVENC) is the
most-exercised path, and AMD/Intel GPUs work via VAAPI (a native most-exercised path, and AMD/Intel GPUs work via VAAPI. A native [Windows host](/docs/windows-host)
[Windows host](/docs/windows-host) is also available — see below). These are the Linux desktop is also available. Setup splits along two axes: you **install** the package per distro, then
environments it supports today, each with its own guide: **configure** the host — and learn its quirks — per desktop/compositor.
| Setup | Desktop / compositor | Guide | > New here? Read [Security & Safe Use](/docs/security) first — a streaming host is remote control of
|---|---|---| > the machine, so keep it on a trusted LAN or VPN and require pairing.
| **Ubuntu** (Desktop or Server) | GNOME (Mutter) | [Ubuntu — GNOME](/docs/ubuntu-gnome) |
| **Ubuntu** (Desktop or Server) | KDE Plasma (KWin) | [Ubuntu — KDE](/docs/ubuntu-kde) |
| **Fedora** | KDE Plasma (KWin) | [Fedora — KDE](/docs/fedora-kde) |
| **Bazzite** | gamescope (Steam) | [Bazzite](/docs/bazzite) |
Other wlroots compositors (Sway/Hyprland) also work but aren't a primary target. If your desktop isn't **Distros — install the package:**
listed, the host still needs one of these compositor backends to create a virtual display.
- [Ubuntu / Debian](/docs/ubuntu)
- [Fedora](/docs/fedora)
- [Arch](/docs/arch)
- [Bazzite](/docs/bazzite)
- [SteamOS](/docs/steamos-host)
**Desktops — configure and quirks:**
- [KDE Plasma (KWin)](/docs/kde)
- [GNOME (Mutter)](/docs/gnome)
- [Steam / gamescope](/docs/gamescope)
- [Sway / wlroots](/docs/sway)
Pick your distro to install, then your desktop to configure — the two are independent. Other
wlroots compositors (Hyprland) work but aren't a primary target; the host still needs one of these
compositor backends to create a virtual display.
> **Windows host:** punktfunk also runs as a native host on **Windows 11 22H2 or newer (x64)** — a > **Windows host:** punktfunk also runs as a native host on **Windows 11 22H2 or newer (x64)** — a
> signed installer that registers a service and bundles a virtual-display driver (whose driver- > signed installer that registers a service and bundles a virtual-display driver (whose driver-
@@ -32,8 +44,8 @@ listed, the host still needs one of these compositor backends to create a virtua
encodes the video in hardware. encodes the video in hardware.
- **NVIDIA driver 535 or newer** (550+ recommended). The driver must include the **GL/EGL userspace**, - **NVIDIA driver 535 or newer** (550+ recommended). The driver must include the **GL/EGL userspace**,
not just `nvidia-utils` — without it the compositor can't initialise the GPU and capture fails. Each not just `nvidia-utils` — without it the compositor can't initialise the GPU and capture fails. Each
setup guide installs the right package (e.g. `libnvidia-gl-<version>` on Ubuntu). install guide installs the right package (e.g. `libnvidia-gl-<version>` on Ubuntu).
- **`nvidia-drm modeset=1`** must be enabled (Wayland on NVIDIA needs it). The setup guides cover this. - **`nvidia-drm modeset=1`** must be enabled (Wayland on NVIDIA needs it). The install guides cover this.
- **AMD / Intel GPUs** encode via **VAAPI** instead (install `mesa-va-drivers` or - **AMD / Intel GPUs** encode via **VAAPI** instead (install `mesa-va-drivers` or
`intel-media-driver`; validated live on AMD RDNA3). The NVIDIA-specific notes above don't apply `intel-media-driver`; validated live on AMD RDNA3). The NVIDIA-specific notes above don't apply
there. On modern Intel (Gen12/Tiger Lake and newer, including Arc) the driver only offers the there. On modern Intel (Gen12/Tiger Lake and newer, including Arc) the driver only offers the
@@ -57,9 +69,9 @@ needs to be running for the user the host runs as. This can be:
Minimum compositor versions (newer is fine): Minimum compositor versions (newer is fine):
- **KWin ≥ 6.5.6** (KDE Plasma) — headless virtual outputs. - **KWin ≥ 6.5.6** ([KDE Plasma](/docs/kde)) — headless virtual outputs.
- **GNOME ≥ 48** (Mutter) — virtual-monitor screen-cast. - **GNOME ≥ 48** ([Mutter](/docs/gnome)) — virtual-monitor screen-cast.
- **gamescope ≥ 3.16.22** (Bazzite/Steam) — older versions deadlock during capture. - **gamescope ≥ 3.16.22** ([Bazzite/Steam](/docs/gamescope)) — older versions deadlock during capture.
## Network ## Network
@@ -70,10 +82,6 @@ Minimum compositor versions (newer is fine):
- For best results, a wired or fast Wi-Fi link. The host can run a built-in **speed test** to pick a - For best results, a wired or fast Wi-Fi link. The host can run a built-in **speed test** to pick a
bitrate for your link (see [Configuration](/docs/configuration)). bitrate for your link (see [Configuration](/docs/configuration)).
> **Before you set up a host, read [Security & Safe Use](/docs/security).** A streaming host is
> remote control of the machine — it's important to understand what that exposes, why to keep it on a
> trusted network, and how pairing protects you.
## A client ## A client
You also need something to stream *to* — see [Connect a Client](/docs/clients). There are native You also need something to stream *to* — see [Connect a Client](/docs/clients). There are native
+9 -38
View File
@@ -37,50 +37,21 @@ Start by making the host service start at boot even when nobody logs in:
sudo loginctl enable-linger "$USER" sudo loginctl enable-linger "$USER"
``` ```
Then bring up a session automatically, depending on your desktop: Then bring up a session automatically. How you do that is desktop-specific — auto-login, lock
disable, and the session unit differ per compositor, so each is documented on its own page:
### Headless GNOME - GNOME: [GNOME → Headless session](/docs/gnome#headless-session).
- KDE Plasma: [KDE → Headless session](/docs/kde#headless-session).
- Steam / gamescope: [gamescope](/docs/gamescope) — the host launches its own session per client, so
there's no separate session unit.
Have GDM auto-login your user, so a GNOME Wayland session is always running: Once a session comes up at boot, enable the host user service (section A) and reboot. The host comes up
on that session.
```ini
# /etc/gdm3/custom.conf (Ubuntu) · /etc/gdm/custom.conf (Fedora)
[daemon]
AutomaticLoginEnable = true
AutomaticLogin = your-user
```
Then **disable the screen lock** — a locked GNOME session blocks screen capture, and there's no one to
unlock a headless box:
```sh
gsettings set org.gnome.desktop.screensaver lock-enabled false
gsettings set org.gnome.desktop.session idle-delay 0
```
Enable the host user service (section A) and reboot. The host comes up on the auto-login session.
### Headless KDE
punktfunk ships a unit that brings up a headless KWin/Plasma session with no display manager, so the
host has a desktop to stream even with no monitor attached:
```sh
cp scripts/punktfunk-kde-session.service scripts/punktfunk-host.service ~/.config/systemd/user/
# host.env: PUNKTFUNK_COMPOSITOR=kwin, WAYLAND_DISPLAY=wayland-kde
systemctl --user daemon-reload
systemctl --user enable punktfunk-kde-session punktfunk-host
sudo loginctl enable-linger "$USER"
reboot
```
The session unit starts headless KWin; the host unit follows it and starts listening. (KWin only needs
to be up by the time a client connects, so the ordering is soft.)
### Headless Bazzite ### Headless Bazzite
On Bazzite, the host launches its own gamescope/Steam session per client, so you don't need a separate On Bazzite, the host launches its own gamescope/Steam session per client, so you don't need a separate
session unit — see [Bazzite](/docs/bazzite). session unit — see [Bazzite](/docs/bazzite) and [gamescope](/docs/gamescope).
## Windows ## Windows
+7 -15
View File
@@ -86,9 +86,8 @@ When it finishes it prints the web-console URL and how to pair.
By default the host **requires PIN pairing** (secure). Two ways to pair: By default the host **requires PIN pairing** (secure). Two ways to pair:
- **Web console** (printed at the end of step 2): open `http://<device-ip>:47992`, log in with the - **Web console** (printed at the end of step 2): open `http://<device-ip>:47992`,
generated password (in `~/.config/punktfunk/web.env`), go to **Devices → arm pairing**, and enter [arm pairing](/docs/web-console#arm-pairing), and enter the PIN on your client.
the PIN on your client.
- **From the client directly**: pick this host (it advertises over mDNS as `_punktfunk._udp`) and - **From the client directly**: pick this host (it advertises over mDNS as `_punktfunk._udp`) and
enter the PIN the host shows. enter the PIN the host shows.
@@ -96,17 +95,9 @@ On a trusted home LAN you can instead install with `--open` and skip pairing ent
### Console login password ### Console login password
The installer generates a random console login password and writes it to The installer generates a random console login password (printed at the end of step 2) and writes it
`~/.config/punktfunk/web.env` (as `PUNKTFUNK_UI_PASSWORD=…`); it's also printed at the end of the to `~/.config/punktfunk/web.env`. To read it back or set your own, see
install run (step 2). Read it back with: [The Web Console](/docs/web-console#login-password).
```sh
sed -n 's/^PUNKTFUNK_UI_PASSWORD=//p' ~/.config/punktfunk/web.env
```
To set your own password, edit that file and restart the console:
`systemctl --user restart punktfunk-web`. Forgot it? This is the recovery path linked from the
console login screen — see [Forgot your Password?](/docs/forgot-password).
## 4. Verify ## 4. Verify
@@ -118,7 +109,8 @@ journalctl --user -u punktfunk-host -f # watch a client connect
Connect from a [native client](/docs/clients), or from [Moonlight](/docs/moonlight) (unless you Connect from a [native client](/docs/clients), or from [Moonlight](/docs/moonlight) (unless you
installed with `--no-gamestream`). In Game Mode the host attaches to the running gamescope session and installed with `--no-gamestream`). In Game Mode the host attaches to the running gamescope session and
streams it at your client's resolution; in Desktop Mode it streams the KDE desktop. The host streams it at your client's resolution; in Desktop Mode it streams the KDE desktop. The host
auto-detects which session is live per connection. auto-detects which session is live per connection. See [Steam / gamescope](/docs/gamescope) for the
attach-vs-managed detail and known limits.
## Updating ## Updating
+68
View File
@@ -0,0 +1,68 @@
---
title: Sway / wlroots
description: Configure a punktfunk host on a wlroots compositor (Sway, Hyprland).
---
The wlroots family can host — but **Sway is the only validated path.** The host adds a per-client
headless output at the client's exact mode and captures it through the xdg-desktop-portal-wlr (xdpw)
ScreenCast portal, injecting input via the wlroots virtual pointer/keyboard protocols. Hyprland and
other wlroots compositors are best-effort (see [How it works](#how-it-works) for the caveat).
This is **not a primary target.** It works and is validated live on **sway 1.11** (zero-copy), but it
sees far less testing than the KDE and GNOME paths — expect rougher edges. If you have a choice,
[KDE](/docs/kde) or [GNOME](/docs/gnome) are the better-exercised desktops.
This page assumes the package is already installed — see [Arch](/docs/arch), [Ubuntu](/docs/ubuntu),
or [Fedora](/docs/fedora).
> New here? Read [Security & Safe Use](/docs/security) first — a streaming host is remote control of
> the machine, so keep it on a trusted LAN or VPN and require pairing.
## host.env
The host auto-detects a wlroots session, so you usually need nothing here. To force the backend, set
these in `~/.config/punktfunk/host.env`:
```ini
PUNKTFUNK_COMPOSITOR=wlroots # aliases: sway, hyprland
PUNKTFUNK_INPUT_BACKEND=wlr
PUNKTFUNK_VIDEO_SOURCE=virtual
PUNKTFUNK_ZEROCOPY=1 # GPU zero-copy capture→encode; auto-falls back to CPU
```
See [Configuration](/docs/configuration) for the full reference.
## How it works
- **Video** — the host adds a headless output at the client's exact mode with `swaymsg create_output`.
This uses Sway's IPC specifically; other wlroots compositors (Hyprland, …) don't expose an
equivalent, so virtual-output creation isn't wired up for them yet — Sway is the supported wlroots
path today.
- **Capture** — it captures that output through the **xdg-desktop-portal-wlr (xdpw)** ScreenCast
portal. The host writes a managed chooser config so the output pick is automatic — no interactive
picker dialog to answer.
- **Input** — mouse and keyboard are injected via the wlroots **virtual pointer** and **virtual
keyboard** protocols.
For how long the virtual output lives, and extend-vs-exclusive topology, see
[Virtual displays](/docs/virtual-displays).
## Requirements
- A running wlroots session (Sway, Hyprland, …).
- **xdg-desktop-portal-wlr (xdpw)** installed and running — the host captures through its ScreenCast
portal. Without it there is no video.
## Start the host
With the backend selected, start the host from **inside your Sway session**:
```sh
systemctl --user enable --now punktfunk-host
journalctl --user -u punktfunk-host -f
```
## Bring up the console and pair
Enable the web console, read its login password, and arm PIN pairing — see
[The Web Console](/docs/web-console). Then [connect a client](/docs/clients).
+9 -3
View File
@@ -71,10 +71,14 @@ The NVIDIA **GL/EGL userspace** is missing — the base driver package doesn't a
- **Ubuntu:** `sudo apt install libnvidia-gl-<version>` (matching your driver). - **Ubuntu:** `sudo apt install libnvidia-gl-<version>` (matching your driver).
- Confirm `/usr/share/glvnd/egl_vendor.d/10_nvidia.json` exists and `nvidia-drm modeset` is `Y`. - Confirm `/usr/share/glvnd/egl_vendor.d/10_nvidia.json` exists and `nvidia-drm modeset` is `Y`.
See [GNOME](/docs/gnome) for the GL/EGL userspace details.
## Black screen / no picture, but the client connects ## Black screen / no picture, but the client connects
- You must be on a **Wayland** session, not X11 (check the login-screen session picker). - You must be on a **Wayland** session, not X11 (check the login-screen session picker).
- KWin must be **≥ 6.5.6** (`kwin_wayland --version`); GNOME **≥ 48**; gamescope **≥ 3.16.22**. - KWin must be **≥ 6.5.6** (`kwin_wayland --version`); GNOME **≥ 48**; gamescope **≥ 3.16.22**. See
[KDE](/docs/kde) for the KWin/Wayland requirement and [gamescope](/docs/gamescope) for the
gamescope one.
- Confirm `PUNKTFUNK_COMPOSITOR` in [`host.env`](/docs/configuration) matches your desktop. - Confirm `PUNKTFUNK_COMPOSITOR` in [`host.env`](/docs/configuration) matches your desktop.
## Capture fails: "Session creation inhibited" (GNOME) ## Capture fails: "Session creation inhibited" (GNOME)
@@ -86,7 +90,8 @@ gsettings set org.gnome.desktop.screensaver lock-enabled false
gsettings set org.gnome.desktop.session idle-delay 0 gsettings set org.gnome.desktop.session idle-delay 0
``` ```
See [Running as a Service](/docs/running-as-a-service). See [GNOME → Headless session](/docs/gnome#headless-session) and
[Running as a Service](/docs/running-as-a-service).
## A controller is detected but does nothing (Bazzite) ## A controller is detected but does nothing (Bazzite)
@@ -96,7 +101,8 @@ The host user needs to be in the `input` group. On Bazzite:
ujust add-user-to-input-group ujust add-user-to-input-group
``` ```
Then log out and back in. On other distros this is `sudo usermod -aG input $USER` + re-login. Then log out and back in. On other distros this is `sudo usermod -aG input $USER` + re-login. See
[Bazzite](/docs/bazzite).
## Pairing is rejected / the client can't connect ## Pairing is rejected / the client can't connect
-172
View File
@@ -1,172 +0,0 @@
---
title: Ubuntu — GNOME
description: Set up a punktfunk host on Ubuntu with the GNOME desktop (Mutter).
---
Set up a punktfunk host on **Ubuntu** (Desktop or Server) running **GNOME**. The host uses GNOME's
Mutter compositor to create a per-client virtual display. Tested on Ubuntu 24.04+ and GNOME 48+.
> New to this? Skim [Requirements](/docs/requirements) first, and read
> [Security & Safe Use](/docs/security) — a streaming host is remote control of the machine, so keep it
> on a trusted LAN or VPN and require pairing.
## 1. NVIDIA driver
Install the recommended NVIDIA driver:
```sh
sudo ubuntu-drivers install # or: sudo apt install nvidia-driver-<version>
```
Then make sure the **GL/EGL userspace** is present — GNOME on NVIDIA needs it, and the base driver
package doesn't always pull it in. Install the `libnvidia-gl` package matching your driver version:
```sh
sudo apt install libnvidia-gl-<version> # e.g. libnvidia-gl-550
```
Reboot, then confirm the driver and KMS modeset:
```sh
nvidia-smi
cat /sys/module/nvidia_drm/parameters/modeset # should print Y
```
If modeset is not `Y`:
```sh
echo 'options nvidia-drm modeset=1' | sudo tee /etc/modprobe.d/nvidia-drm.conf
sudo update-initramfs -u && sudo reboot
```
> **Secure Boot:** on a machine with Secure Boot **enabled**, the NVIDIA kernel module won't load
> until you enrol its signing key. If `nvidia-smi` reports it can't talk to the driver, run
> `sudo mokutil --import /var/lib/shim-signed/mok/MOK.der` (set a one-time password), reboot, and
> choose **Enrol MOK** at the blue screen. Or disable Secure Boot in firmware.
## 2. Install the host (apt)
`punktfunk-host` is published as a `.deb` to the public Gitea apt registry, so the box installs and
updates with plain `apt`. The registry is public — no auth needed, just trust its signing key:
```sh
sudo install -d -m 0755 /etc/apt/keyrings
curl -fsSL https://git.unom.io/api/packages/unom/debian/repository.key \
| sudo tee /etc/apt/keyrings/punktfunk.asc >/dev/null
echo "deb [signed-by=/etc/apt/keyrings/punktfunk.asc] https://git.unom.io/api/packages/unom/debian stable main" \
| sudo tee /etc/apt/sources.list.d/punktfunk.list
sudo apt update
sudo apt install punktfunk-host
```
`punktfunk-host` `Recommends` the browser console (`punktfunk-web`), so apt pulls it in by default.
The desktop *client* (`punktfunk-client`) is a separate package for the machine you stream *to* — not
installed on a host. The NVIDIA driver is **not** a dependency — you installed it out of band in
step 1. Later updates are just `sudo apt update && sudo apt upgrade`.
## 3. Configure
The package ships the systemd **user** unit, the `/dev/uinput` udev rule, the socket-buffer sysctl
tuning, and an example config. As the desktop user, grant gamepad access and write the GNOME config:
```sh
sudo usermod -aG input "$USER" # /dev/uinput for virtual gamepads (re-login to apply)
mkdir -p ~/.config/punktfunk
cat > ~/.config/punktfunk/host.env <<'ENV'
WAYLAND_DISPLAY=wayland-0
XDG_CURRENT_DESKTOP=GNOME
PUNKTFUNK_COMPOSITOR=mutter
PUNKTFUNK_VIDEO_SOURCE=virtual
PUNKTFUNK_ZEROCOPY=1
PUNKTFUNK_INPUT_BACKEND=libei
ENV
```
See the [Configuration reference](/docs/configuration) for every option.
## 4. Run
Start the host as a user service from **inside your GNOME session** (so it can reach Mutter):
```sh
systemctl --user enable --now punktfunk-host
journalctl --user -u punktfunk-host -f # watch it come up + print its fingerprint
```
The host listens on UDP `9777` (native punktfunk/1) plus the GameStream ports, and advertises itself
over mDNS. It requires **PIN pairing** by default (secure on a LAN) — arm pairing from the web
console (next step) and pair once from your [client](/docs/clients).
### Web console
The console (status, paired devices, arm pairing) ships as `punktfunk-web`:
```sh
systemctl --user enable --now punktfunk-web
# read the auto-generated login password, then open http://<host-ip>:47992
journalctl --user -u punktfunk-web-init | sed -n 's/.*password generated: //p'
```
#### Console login password
The console is password-protected. On first start `punktfunk-web-init` generates a random login
password and saves it to `~/.config/punktfunk/web-password` (as `PUNKTFUNK_UI_PASSWORD=…`). Read it
back at any time — from the init service's journal, or straight from the file:
```sh
journalctl --user -u punktfunk-web-init | sed -n 's/.*password generated: //p'
sed -n 's/^PUNKTFUNK_UI_PASSWORD=//p' ~/.config/punktfunk/web-password
```
To set your own password, edit that file (`PUNKTFUNK_UI_PASSWORD=<your-password>`) and restart the
console: `systemctl --user restart punktfunk-web`. Forgot it? This is the recovery path linked from
the console login screen — see [Forgot your Password?](/docs/forgot-password).
To run the host automatically at boot — including on a **headless** machine with no monitor — see
[Running as a Service](/docs/running-as-a-service).
## Troubleshooting
- **gnome-shell fails to start / "GPU … not supported by EGL":** the NVIDIA GL/EGL userspace is
missing. Install `libnvidia-gl-<version>` (step 1) and confirm
`/usr/share/glvnd/egl_vendor.d/10_nvidia.json` exists.
- **Capture fails with "Session creation inhibited":** a **locked** GNOME session blocks screen
capture. On a headless/always-on host, disable the lock — see
[Running as a Service](/docs/running-as-a-service).
- More in [Troubleshooting](/docs/troubleshooting).
## Appendix — build from source
If the apt registry doesn't have a build for your release, or you want to track `main` directly,
compile the host yourself (no clean updates / no packaged units — you wire those up by hand).
Install the build toolchain and runtime libraries:
```sh
sudo apt install build-essential pkg-config cmake clang libclang-dev nasm git curl \
pipewire pipewire-pulse wireplumber libpipewire-0.3-dev libspa-0.2-dev \
libwayland-dev wayland-protocols libxkbcommon-dev libopus-dev \
libdrm-dev libgbm-dev libegl-dev libgles-dev mesa-common-dev libva-dev \
ffmpeg libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libavfilter-dev libavdevice-dev \
libnvidia-egl-wayland1 libnvidia-egl-gbm1 libei-dev
```
Install Rust if you don't have it, then build:
```sh
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
git clone https://git.unom.io/unom/punktfunk.git && cd punktfunk
cargo build --release -p punktfunk-host
```
The host binary lands at `target/release/punktfunk-host`. Write `~/.config/punktfunk/host.env` as in
step 3, then run it inside your GNOME session:
```sh
cargo run --release -p punktfunk-host -- serve --gamestream
```
(The native plane is always on; `--gamestream` adds the Moonlight-compat surface this guide's
GameStream ports refer to — trusted LAN only. Drop it for a secure native-only host.)
-128
View File
@@ -1,128 +0,0 @@
---
title: Ubuntu — KDE Plasma
description: Set up a punktfunk host on Ubuntu with KDE Plasma (KWin).
---
Set up a punktfunk host on **Ubuntu** running **KDE Plasma**. The host uses KDE's KWin compositor to
create a per-client virtual display. Needs **KWin 6.5.6 or newer**.
> New to this? Skim [Requirements](/docs/requirements) first, and read
> [Security & Safe Use](/docs/security) — a streaming host is remote control of the machine, so keep it
> on a trusted LAN or VPN and require pairing.
## 1. NVIDIA driver
Identical to the GNOME guide — follow **step 1** of
[Ubuntu — GNOME](/docs/ubuntu-gnome#1-nvidia-driver): install the NVIDIA driver **and** the
`libnvidia-gl-<version>` userspace, enable `nvidia-drm modeset=1`, reboot, and verify with
`nvidia-smi`.
## 2. Install the host (apt)
The host is published as a `.deb` to the public Gitea apt registry — install and update with plain
`apt`. Trust the repo's signing key, add the repo, and install:
```sh
sudo install -d -m 0755 /etc/apt/keyrings
curl -fsSL https://git.unom.io/api/packages/unom/debian/repository.key \
| sudo tee /etc/apt/keyrings/punktfunk.asc >/dev/null
echo "deb [signed-by=/etc/apt/keyrings/punktfunk.asc] https://git.unom.io/api/packages/unom/debian stable main" \
| sudo tee /etc/apt/sources.list.d/punktfunk.list
sudo apt update
sudo apt install punktfunk-host
```
This also pulls the web console (`punktfunk-web`) via `Recommends` (the pairing/status UI). The
desktop *client*`punktfunk-client`, for the machine you stream *to* — is a separate package, not
needed on a host. The NVIDIA driver stays out of band (step 1). Updates later are just
`sudo apt update && sudo apt upgrade`.
## 3. Configure
The package ships the systemd **user** unit, the udev rule, and the sysctl tuning. As the desktop
user, grant gamepad access and write the KDE config:
```sh
sudo usermod -aG input "$USER" # /dev/uinput for virtual gamepads (re-login to apply)
mkdir -p ~/.config/punktfunk
cat > ~/.config/punktfunk/host.env <<'ENV'
WAYLAND_DISPLAY=wayland-0
XDG_CURRENT_DESKTOP=KDE
PUNKTFUNK_COMPOSITOR=kwin
PUNKTFUNK_VIDEO_SOURCE=virtual
PUNKTFUNK_ZEROCOPY=1
PUNKTFUNK_INPUT_BACKEND=libei
ENV
```
> Make sure you're on a **KDE Wayland** session (not X11) — the picker on the login screen. The
> virtual-display path is Wayland-only. See the [Configuration reference](/docs/configuration) for
> every option.
## 4. Run
Start the host as a user service from **inside your Plasma session**:
```sh
systemctl --user enable --now punktfunk-host
journalctl --user -u punktfunk-host -f # watch it come up + print its fingerprint
```
The host listens on UDP `9777` (native punktfunk/1) plus the GameStream ports and advertises over
mDNS. It requires **PIN pairing** by default — arm pairing from the web console and pair once from
your [client](/docs/clients).
### Web console
```sh
systemctl --user enable --now punktfunk-web
# read the auto-generated login password, then open http://<host-ip>:47992
journalctl --user -u punktfunk-web-init | sed -n 's/.*password generated: //p'
```
#### Console login password
The console is password-protected. On first start `punktfunk-web-init` generates a random login
password and saves it to `~/.config/punktfunk/web-password` (as `PUNKTFUNK_UI_PASSWORD=…`). Read it
back at any time — from the init service's journal, or straight from the file:
```sh
journalctl --user -u punktfunk-web-init | sed -n 's/.*password generated: //p'
sed -n 's/^PUNKTFUNK_UI_PASSWORD=//p' ~/.config/punktfunk/web-password
```
To set your own password, edit that file (`PUNKTFUNK_UI_PASSWORD=<your-password>`) and restart the
console: `systemctl --user restart punktfunk-web`. Forgot it? This is the recovery path linked from
the console login screen — see [Forgot your Password?](/docs/forgot-password).
To run it at boot — including fully **headless**, with KWin brought up automatically and no login —
see [Running as a Service](/docs/running-as-a-service); the headless appliance is built around KDE.
## Troubleshooting
- **KWin too old:** virtual outputs need KWin **≥ 6.5.6**. Check with `kwin_wayland --version`.
- **No picture / capture fails:** confirm you're on a Wayland session and the NVIDIA GL userspace is
installed (`libnvidia-gl-<version>`). More in [Troubleshooting](/docs/troubleshooting).
## Appendix — build from source
If the apt registry has no build for your release, compile the host yourself (no clean updates / no
packaged units). Install the build toolchain and runtime libraries — the same `apt` line as the
[GNOME build-from-source appendix](/docs/ubuntu-gnome#appendix--build-from-source) — then:
```sh
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
git clone https://git.unom.io/unom/punktfunk.git && cd punktfunk
cargo build --release -p punktfunk-host
```
Write `~/.config/punktfunk/host.env` as in step 3, then run it inside your Plasma session:
```sh
cargo run --release -p punktfunk-host -- serve --gamestream
```
(The native plane is always on; `--gamestream` adds the Moonlight-compat surface this guide's
GameStream ports refer to — trusted LAN only. Drop it for a secure native-only host.)
+133
View File
@@ -0,0 +1,133 @@
---
title: Ubuntu / Debian
description: Install the punktfunk host on Ubuntu or Debian with apt.
---
Install a punktfunk host on **Ubuntu** (Desktop or Server) or **Debian** from the apt registry. This
page covers the distro-level setup — GPU driver, package, gamepad access. It works with either GNOME
or KDE; how the host creates its virtual display and injects input is desktop-specific, so pick your
desktop on the [configure pages](#configure-your-desktop) afterward rather than here.
> New here? Read [Security & Safe Use](/docs/security) first — a streaming host is remote control of
> the machine, so keep it on a trusted LAN or VPN and require pairing.
## 1. GPU driver
On **NVIDIA**, install the recommended driver:
```sh
sudo ubuntu-drivers install # or: sudo apt install nvidia-driver-<version>
```
Then make sure the **GL/EGL userspace** is present — Wayland compositors on NVIDIA need it, and the
base driver package doesn't always pull it in. Install the `libnvidia-gl` package matching your
driver version:
```sh
sudo apt install libnvidia-gl-<version> # e.g. libnvidia-gl-550
```
Reboot, then confirm the driver and KMS modeset:
```sh
nvidia-smi
cat /sys/module/nvidia_drm/parameters/modeset # should print Y
```
If modeset is not `Y`:
```sh
echo 'options nvidia-drm modeset=1' | sudo tee /etc/modprobe.d/nvidia-drm.conf
sudo update-initramfs -u && sudo reboot
```
> **Secure Boot:** on a machine with Secure Boot **enabled**, the NVIDIA kernel module won't load
> until you enrol its signing key. If `nvidia-smi` reports it can't talk to the driver, run
> `sudo mokutil --import /var/lib/shim-signed/mok/MOK.der` (set a one-time password), reboot, and
> choose **Enrol MOK** at the blue screen. Or disable Secure Boot in firmware.
On **AMD/Intel** none of the NVIDIA steps apply. Encode runs through VAAPI on the Mesa stack —
`mesa-va-drivers` on AMD, `intel-media-driver` on Intel — which your desktop install already provides.
## 2. Install the host (apt)
`punktfunk-host` is published as a `.deb` to the public Gitea apt registry, so the box installs and
updates with plain `apt`. The registry is public — no auth needed, just trust its signing key:
```sh
sudo install -d -m 0755 /etc/apt/keyrings
curl -fsSL https://git.unom.io/api/packages/unom/debian/repository.key \
| sudo tee /etc/apt/keyrings/punktfunk.asc >/dev/null
echo "deb [signed-by=/etc/apt/keyrings/punktfunk.asc] https://git.unom.io/api/packages/unom/debian stable main" \
| sudo tee /etc/apt/sources.list.d/punktfunk.list
sudo apt update
sudo apt install punktfunk-host
```
`punktfunk-host` `Recommends` the browser console (`punktfunk-web`), so apt pulls it in by default.
The desktop *client* (`punktfunk-client`) is a separate package for the machine you stream *to* — not
installed on a host. The NVIDIA driver is **not** a dependency — you installed it out of band in
step 1. Later updates are just `sudo apt update && sudo apt upgrade`.
The `stable` component above is the stable channel. To track pre-release builds instead, see
[Release Channels](/docs/channels).
## 3. Grant gamepad access
Virtual gamepads inject through `/dev/uinput`, which is gated by the `input` group. Add yourself and
re-login so the new group membership takes effect:
```sh
sudo usermod -aG input "$USER" # re-login to apply
```
## Configure your desktop
How the host creates its virtual display and injects input depends on your desktop, not your distro.
Continue on the page for the desktop you run — it covers your `host.env`, any compositor quirks, and
starting the host:
- [KDE Plasma (KWin)](/docs/kde)
- [GNOME (Mutter)](/docs/gnome)
- [Steam / gamescope](/docs/gamescope)
- [Sway / wlroots](/docs/sway)
Then bring up [The Web Console](/docs/web-console) to arm pairing and connect your first
[client](/docs/clients). To run the host at boot — including fully **headless** — see
[Running as a Service](/docs/running-as-a-service).
## Appendix — build from source
If the apt registry doesn't have a build for your release, or you want to track `main` directly,
compile the host yourself (no clean updates / no packaged units — you wire those up by hand).
Install the build toolchain and runtime libraries:
```sh
sudo apt install build-essential pkg-config cmake clang libclang-dev nasm git curl \
pipewire pipewire-pulse wireplumber libpipewire-0.3-dev libspa-0.2-dev \
libwayland-dev wayland-protocols libxkbcommon-dev libopus-dev \
libdrm-dev libgbm-dev libegl-dev libgles-dev mesa-common-dev libva-dev \
ffmpeg libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libavfilter-dev libavdevice-dev \
libnvidia-egl-wayland1 libnvidia-egl-gbm1 libei-dev
```
Install Rust if you don't have it, then build:
```sh
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
git clone https://git.unom.io/unom/punktfunk.git && cd punktfunk
cargo build --release -p punktfunk-host
```
The host binary lands at `target/release/punktfunk-host`. Configure your desktop as above, then run
it from inside your session:
```sh
cargo run --release -p punktfunk-host -- serve --gamestream
```
(The native plane is always on; `--gamestream` adds the Moonlight-compat surface — trusted LAN only.
Drop it for a secure native-only host.)
+73
View File
@@ -0,0 +1,73 @@
---
title: The Web Console
description: Enable the punktfunk browser console, read or change its login password, and arm PIN pairing.
---
The web console is the browser UI for a punktfunk host — live status, paired devices, and the PIN
pairing flow. It ships as the **`punktfunk-web`** systemd user unit on Linux and the **`PunktfunkWeb`**
task on Windows, and serves on **`http://<host-ip>:47992`**. The host's own management API stays
loopback-only behind it, so the console is the one surface you expose on the LAN.
> New here? Read [Security & Safe Use](/docs/security) first — a streaming host is remote control of
> the machine, so keep it on a trusted LAN or VPN and require pairing.
## Enable the console
- **Linux packages (apt / RPM / Bazzite):** `punktfunk-host` recommends `punktfunk-web`, so your
package manager pulls it in. Enable and start it as your desktop user, then open the URL:
```sh
systemctl --user enable --now punktfunk-web
# then browse to http://<host-ip>:47992
```
- **Windows host:** the installer sets up the console, its runtime, and the `PunktfunkWeb` task and
starts it at boot. There is nothing to enable — open `http://<this-PC>:47992`.
- **SteamOS host:** the install script builds and starts the console as a user service for you. It
prints the URL when it finishes.
## Login password
The console is password-protected. Where that password lives and how you change it depends on the
host platform.
**Linux packages (apt / RPM / Bazzite).** On first start `punktfunk-web-init` generates a random
password and saves it to `~/.config/punktfunk/web-password` (as `PUNKTFUNK_UI_PASSWORD=…`). Read it
back from the init service's journal or straight from the file:
```sh
journalctl --user -u punktfunk-web-init | sed -n 's/.*password generated: //p'
sed -n 's/^PUNKTFUNK_UI_PASSWORD=//p' ~/.config/punktfunk/web-password
```
To set your own, edit that file (`PUNKTFUNK_UI_PASSWORD=<your-password>`) and restart the console:
`systemctl --user restart punktfunk-web`.
**SteamOS host.** Same idea, but the install script writes the generated password to
`~/.config/punktfunk/web.env` and prints it at the end of the install run:
```sh
sed -n 's/^PUNKTFUNK_UI_PASSWORD=//p' ~/.config/punktfunk/web.env
```
Edit that file and `systemctl --user restart punktfunk-web` to change it.
**Windows host.** You choose the password during install — a secure random default is pre-filled and
shown again on the installer's final page. It's stored in `%ProgramData%\punktfunk\web-password` (as
`PUNKTFUNK_UI_PASSWORD=…`), readable only by Administrators and SYSTEM. To change it, edit the file
and restart the task in an **elevated** PowerShell:
```powershell
notepad "$env:ProgramData\punktfunk\web-password" # set PUNKTFUNK_UI_PASSWORD=<your-password>
schtasks /End /TN PunktfunkWeb; schtasks /Run /TN PunktfunkWeb
```
Forgot it? See [Forgot your Password?](/docs/forgot-password).
## Arm pairing
The host **requires PIN pairing** by default (secure on a LAN). To connect the first time, open the
console, log in, and go to **Devices → arm pairing**. The host displays a 4-digit PIN — enter it on
your [client](/docs/clients) to pair. See [Pairing & Trust](/docs/pairing) for the full trust model
and how to approve or remove devices later.
+9 -14
View File
@@ -59,29 +59,24 @@ Packaging internals live in
### Web console & pairing ### Web console & pairing
See [The Web Console](/docs/web-console) for the console + pairing model shared with the Linux host;
the Windows specifics follow.
The installer also sets up the **web management console** (status, paired devices, the PIN pairing The installer also sets up the **web management console** (status, paired devices, the PIN pairing
flow): it bundles the console plus its own runtime and runs it as the **`PunktfunkWeb`** task on flow): it bundles the console plus its own runtime and runs it as the **`PunktfunkWeb`** task on
**`http://<this-PC>:47992`**, starting at boot. **`http://<this-PC>:47992`**, starting at boot.
#### Console login password #### Console login password
During setup you choose the console **login password** — it's pre-filled with a secure random default You choose the console **login password** during setup — a secure random default is pre-filled and
and shown again on the installer's final page. It's stored in `%ProgramData%\punktfunk\web-password` shown on the installer's final page. It's stored in `%ProgramData%\punktfunk\web-password`, readable
(as `PUNKTFUNK_UI_PASSWORD=…`), readable only by Administrators and SYSTEM. only by Administrators and SYSTEM. To read or change it (with the `schtasks` restart), see
[The Web Console → Login password](/docs/web-console#login-password); forgot it entirely?
To change it, edit that file and restart the console task. In an **elevated** PowerShell:
```powershell
notepad "$env:ProgramData\punktfunk\web-password" # set PUNKTFUNK_UI_PASSWORD=<your-password>
schtasks /End /TN PunktfunkWeb; schtasks /Run /TN PunktfunkWeb
```
Forgot it? This is the recovery path linked from the console login screen — see
[Forgot your Password?](/docs/forgot-password). [Forgot your Password?](/docs/forgot-password).
The host **requires PIN pairing** by default (secure on a LAN). To connect the first time, open the The host **requires PIN pairing** by default (secure on a LAN). To connect the first time, open the
console from any browser on the LAN, log in, go to **Devices → arm pairing**, and enter the PIN on console from any browser on the LAN, log in, go to **Devices → [arm pairing](/docs/web-console#arm-pairing)**,
your [client](/docs/clients). The host's own management API stays loopback-only behind the console. and enter the PIN on your [client](/docs/clients). The host's own management API stays loopback-only behind the console.
### Configure ### Configure
+3 -2
View File
@@ -9,6 +9,7 @@
"nav_pairing": "Kopplung", "nav_pairing": "Kopplung",
"nav_library": "Bibliothek", "nav_library": "Bibliothek",
"nav_settings": "Einstellungen", "nav_settings": "Einstellungen",
"nav_more": "Mehr",
"status_title": "Live-Status", "status_title": "Live-Status",
"status_video": "Video", "status_video": "Video",
"status_audio": "Audio", "status_audio": "Audio",
@@ -87,7 +88,7 @@
"display_preset_current": "Aktiv", "display_preset_current": "Aktiv",
"display_preset_soon": "in Kürze", "display_preset_soon": "in Kürze",
"display_keep_alive_help": "„Aus“ baut die Anzeige sofort beim Trennen ab. Halte sie (und bei gamescope ihr Spiel) am Leben, damit ein schnelles Wiederverbinden sofort fortsetzt, statt neu aufzubauen.", "display_keep_alive_help": "„Aus“ baut die Anzeige sofort beim Trennen ab. Halte sie (und bei gamescope ihr Spiel) am Leben, damit ein schnelles Wiederverbinden sofort fortsetzt, statt neu aufzubauen.",
"display_topology_help": "Was mit den physischen Monitoren des Hosts während des Streamings geschieht.", "display_topology_help": "Wie sich die gestreamte Anzeige in den Desktop des Hosts einfügt. Erweitern: ein zusätzlicher Bildschirm neben deinen Monitoren. Primär: die gestreamte Anzeige wird der primäre Ausgang, deine Monitore bleiben. Exklusiv: die gestreamte Anzeige ist der einzige Ausgang — physische Monitore werden beim Streamen deaktiviert und danach wiederhergestellt. Auf einem Host ohne Monitor legt es nur fest, ob die gestreamte Anzeige primär ist.",
"display_conflict": "Wenn ein weiterer Client verbindet", "display_conflict": "Wenn ein weiterer Client verbindet",
"display_conflict_help": "Was passiert, wenn ein zweiter Client verbindet, während bereits gestreamt wird, und eine andere Auflösung anfragt.", "display_conflict_help": "Was passiert, wenn ein zweiter Client verbindet, während bereits gestreamt wird, und eine andere Auflösung anfragt.",
"display_conflict_separate": "Eigene Anzeige", "display_conflict_separate": "Eigene Anzeige",
@@ -95,7 +96,7 @@
"display_conflict_join": "Ansicht teilen", "display_conflict_join": "Ansicht teilen",
"display_conflict_reject": "Besetzt — ablehnen", "display_conflict_reject": "Besetzt — ablehnen",
"display_identity": "Client-Identität", "display_identity": "Client-Identität",
"display_identity_help": "Jedem Client eine stabile Anzeige geben, damit der Desktop seine Monitor-Einstellungen merkt (z. B. Skalierung).", "display_identity_help": "Ob die gestreamte Anzeige eine stabile Client-Identität trägt, sodass der Desktop des Hosts die Monitor-Einstellungen dieses Clients (Skalierung, Auflösung) merkt und beim erneuten Verbinden wieder anwendet. Geteilt: eine Identität für alle. Pro Client: jedes Gerät eigene. Pro Client + Auflösung: separate Einstellungen je Gerät und Auflösung.",
"display_identity_shared": "Geteilt", "display_identity_shared": "Geteilt",
"display_identity_per_client": "Pro Client", "display_identity_per_client": "Pro Client",
"display_identity_per_client_mode": "Pro Client + Auflösung", "display_identity_per_client_mode": "Pro Client + Auflösung",
+3 -2
View File
@@ -9,6 +9,7 @@
"nav_pairing": "Pairing", "nav_pairing": "Pairing",
"nav_library": "Library", "nav_library": "Library",
"nav_settings": "Settings", "nav_settings": "Settings",
"nav_more": "More",
"status_title": "Live status", "status_title": "Live status",
"status_video": "Video", "status_video": "Video",
"status_audio": "Audio", "status_audio": "Audio",
@@ -87,7 +88,7 @@
"display_preset_current": "Active", "display_preset_current": "Active",
"display_preset_soon": "coming soon", "display_preset_soon": "coming soon",
"display_keep_alive_help": "Off tears the display down as soon as the client disconnects. Keep it alive (and, on gamescope, its game) so a quick reconnect resumes instantly instead of rebuilding.", "display_keep_alive_help": "Off tears the display down as soon as the client disconnects. Keep it alive (and, on gamescope, its game) so a quick reconnect resumes instantly instead of rebuilding.",
"display_topology_help": "What happens to the host's physical monitors while streaming.", "display_topology_help": "How the streamed display fits into the host's desktop. Extend: an extra screen alongside your monitors. Primary: the streamed display becomes the primary output, your monitors kept. Exclusive: the streamed display is the sole output — physical monitors are disabled while streaming and restored after. On a headless host it just sets whether the streamed display is the primary.",
"display_conflict": "When another client connects", "display_conflict": "When another client connects",
"display_conflict_help": "What happens if a second client connects while one is already streaming and asks for a different resolution.", "display_conflict_help": "What happens if a second client connects while one is already streaming and asks for a different resolution.",
"display_conflict_separate": "Own display", "display_conflict_separate": "Own display",
@@ -95,7 +96,7 @@
"display_conflict_join": "Share view", "display_conflict_join": "Share view",
"display_conflict_reject": "Busy — reject", "display_conflict_reject": "Busy — reject",
"display_identity": "Per-client identity", "display_identity": "Per-client identity",
"display_identity_help": "Give each client a stable display so the desktop remembers its per-monitor settings (e.g. scaling).", "display_identity_help": "Whether the streamed display carries a stable per-client identity, so the host's desktop remembers that client's per-monitor settings (scaling, resolution) and reapplies them when it reconnects. Shared: one identity for everyone. Per client: each device keeps its own. Per client + resolution: a device keeps separate settings per resolution it connects at.",
"display_identity_shared": "Shared", "display_identity_shared": "Shared",
"display_identity_per_client": "Per client", "display_identity_per_client": "Per client",
"display_identity_per_client_mode": "Per client + resolution", "display_identity_per_client_mode": "Per client + resolution",
+85 -20
View File
@@ -1,16 +1,17 @@
import { Link } from "@tanstack/react-router"; import { Link, useRouterState } from "@tanstack/react-router";
import { import {
Activity, Activity,
GaugeCircle, GaugeCircle,
KeyRound, KeyRound,
LibraryBig, LibraryBig,
MonitorPlay, MonitorPlay,
MoreHorizontal,
ScrollText, ScrollText,
Server, Server,
Settings, Settings,
} from "lucide-react"; } from "lucide-react";
import { motion, stagger } from "motion/react"; import { motion, stagger } from "motion/react";
import type { ReactNode } from "react"; import { type ReactNode, useState } from "react";
import { BrandMark } from "@/components/brand-mark"; import { BrandMark } from "@/components/brand-mark";
import { Wordmark } from "@/components/wordmark"; import { Wordmark } from "@/components/wordmark";
import { changeLocale, type Locale, locales, useLocale } from "@/lib/i18n"; import { changeLocale, type Locale, locales, useLocale } from "@/lib/i18n";
@@ -30,6 +31,11 @@ const NAV = [
{ to: "/settings", icon: Settings, label: () => m.nav_settings() }, { to: "/settings", icon: Settings, label: () => m.nav_settings() },
] as const; ] as const;
// On phones a flat 8-tab bar is too cramped, so the first four are pinned and the rest live behind a
// "More" tab that opens a sheet above the bar. Keep it ≤ 5 slots including "More".
const MOBILE_PRIMARY = NAV.slice(0, 4);
const MOBILE_OVERFLOW = NAV.slice(4);
export function AppShell({ children }: { children: ReactNode }) { export function AppShell({ children }: { children: ReactNode }) {
// Read the locale so the whole shell re-renders on a language switch. // Read the locale so the whole shell re-renders on a language switch.
useLocale(); useLocale();
@@ -108,29 +114,88 @@ export function AppShell({ children }: { children: ReactNode }) {
</main> </main>
</div> </div>
{/* Mobile bottom tab bar (< sm): the primary navigation on phones. */} <MobileNav />
</div>
);
}
/** Mobile bottom navigation (< sm): four pinned tabs + a "More" tab whose sheet holds the rest. */
function MobileNav() {
const [moreOpen, setMoreOpen] = useState(false);
const pathname = useRouterState({ select: (s) => s.location.pathname });
// Highlight "More" when the current route lives in the overflow.
const overflowActive = MOBILE_OVERFLOW.some(
(n) => pathname === n.to || pathname.startsWith(`${n.to}/`),
);
// Fixed two-line-tall label box so a 1- or 2-line label (labels vary by locale) keeps every icon
// at the same height.
const tab =
"flex flex-1 flex-col items-center justify-center gap-1 px-0.5 py-2 text-muted-foreground transition-colors";
const lbl =
"flex h-7 w-full items-center justify-center text-center text-[10px] leading-tight";
return (
<>
{/* Tap-outside backdrop, under the bar (z-50) but over the page. */}
{moreOpen && (
<button
type="button"
aria-label="Close menu"
className="fixed inset-0 z-40 bg-black/40 sm:hidden"
onClick={() => setMoreOpen(false)}
/>
)}
<nav <nav
className="fixed inset-x-0 bottom-0 z-40 flex border-t bg-card/95 backdrop-blur sm:hidden" className="fixed inset-x-0 bottom-0 z-50 sm:hidden"
style={{ paddingBottom: "env(safe-area-inset-bottom)" }} style={{ paddingBottom: "env(safe-area-inset-bottom)" }}
> >
{NAV.map(({ to, icon: Icon, label }) => ( {/* The "More" sheet sits directly above the bar (bottom-full of the fixed nav). */}
<Link {moreOpen && (
key={to} <div className="absolute inset-x-0 bottom-full border-t bg-card/95 backdrop-blur">
to={to} <div className="grid grid-cols-4 gap-1 p-2">
activeOptions={{ exact: to === "/" }} {MOBILE_OVERFLOW.map(({ to, icon: Icon, label }) => (
className="flex flex-1 flex-col items-center justify-center gap-1 px-0.5 py-2 text-muted-foreground transition-colors" <Link
activeProps={{ className: "text-[var(--brand-light)]" }} key={to}
to={to}
onClick={() => setMoreOpen(false)}
className={cn(tab, "rounded-md")}
activeProps={{ className: "text-[var(--brand-light)]" }}
>
<Icon className="size-5 shrink-0" />
<span className={lbl}>{label()}</span>
</Link>
))}
</div>
</div>
)}
<div className="flex border-t bg-card/95 backdrop-blur">
{MOBILE_PRIMARY.map(({ to, icon: Icon, label }) => (
<Link
key={to}
to={to}
onClick={() => setMoreOpen(false)}
activeOptions={{ exact: to === "/" }}
className={tab}
activeProps={{ className: "text-[var(--brand-light)]" }}
>
<Icon className="size-5 shrink-0" />
<span className={lbl}>{label()}</span>
</Link>
))}
<button
type="button"
onClick={() => setMoreOpen((o) => !o)}
aria-expanded={moreOpen}
className={cn(
tab,
(moreOpen || overflowActive) && "text-[var(--brand-light)]",
)}
> >
<Icon className="size-5 shrink-0" /> <MoreHorizontal className="size-5 shrink-0" />
{/* Fixed two-line-tall box so a 1- or 2-line label keeps every icon <span className={lbl}>{m.nav_more()}</span>
at the same height (the labels vary by locale). */} </button>
<span className="flex h-7 w-full items-center justify-center text-center text-[10px] leading-tight"> </div>
{label()}
</span>
</Link>
))}
</nav> </nav>
</div> </>
); );
} }
+5 -5
View File
@@ -67,7 +67,7 @@ export const DisplaySection: FC = () => {
<CardTitle>{m.display_config_title()}</CardTitle> <CardTitle>{m.display_config_title()}</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<p className="text-sm text-muted-foreground">{m.host_displays_help()}</p> <p className="max-w-prose text-sm text-muted-foreground">{m.host_displays_help()}</p>
<QueryState isLoading={q.isLoading} error={q.error} refetch={q.refetch}> <QueryState isLoading={q.isLoading} error={q.error} refetch={q.refetch}>
{q.data && draft && ( {q.data && draft && (
<DisplayForm <DisplayForm
@@ -154,8 +154,8 @@ const DisplayForm: FC<{
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* One-click presets — a 2-up grid so each has room to breathe */} {/* One-click presets — a 2-up grid so each has room to breathe */}
<div className="space-y-3"> <div className="space-y-4">
<Label className="text-base font-semibold">{m.display_preset()}</Label> <Label className="mb-1 block text-base font-semibold">{m.display_preset()}</Label>
<div className="grid gap-3 sm:grid-cols-2"> <div className="grid gap-3 sm:grid-cols-2">
{PRESET_ORDER.map((id) => { {PRESET_ORDER.map((id) => {
const p = presets.find((x) => x.id === id); const p = presets.find((x) => x.id === id);
@@ -341,7 +341,7 @@ const DisplayForm: FC<{
<Badge variant="outline">{`${effective.max_displays}×`}</Badge> <Badge variant="outline">{`${effective.max_displays}×`}</Badge>
</div> </div>
<p className="text-xs text-muted-foreground">{m.display_pending_note()}</p> <p className="max-w-prose text-xs text-muted-foreground">{m.display_pending_note()}</p>
{error && <p className="text-sm text-amber-600 dark:text-amber-500">{error}</p>} {error && <p className="text-sm text-amber-600 dark:text-amber-500">{error}</p>}
</div> </div>
); );
@@ -357,7 +357,7 @@ const Field: FC<{ label: string; help?: string; children: ReactNode }> = ({
<div className="space-y-3"> <div className="space-y-3">
<Label className="block">{label}</Label> <Label className="block">{label}</Label>
{children} {children}
{help && <p className="text-xs text-muted-foreground">{help}</p>} {help && <p className="max-w-prose text-xs text-muted-foreground">{help}</p>}
</div> </div>
); );