Add headless streaming mode + Mac client + full docs
Headless mode (new) — for KVM-attached hosts streaming to disconnected clients - --headless / --mirror flags; default headless on hostname JARVIS, mirror elsewhere - New lib/headless.sh installs prep-cmd hooks to ~/.local/share/omarchy-moonlight/bin - bin/sunshine-stream-do.sh creates/resizes a Hyprland HEADLESS-1 output to the connecting client's resolution and migrates the active workspace onto it - bin/sunshine-stream-undo.sh tears down the headless output on disconnect and returns the workspace to a non-headless monitor when one is available - lib/config.sh writes capture=wlr, output_name=HEADLESS-1, and the JSON global_prep_cmd entry referencing the installed hook paths - lib/preflight.sh adds a preflight_headless step that checks hyprctl, jq, and a running Hyprland session (warn-only, install can proceed) - lib/verify.sh adds checks for the hook scripts and the wlr/global_prep_cmd config lines Mac client - client/install-macos.sh: Darwin guard, Homebrew presence check, brew cask install of Moonlight, idempotent - client/README.md: per-platform install (macOS / Android / iOS / Apple TV / Linux + Steam Deck) and the five-step first-pair walkthrough Other - jq added to the helper install set in lib/packages.sh (hooks parse Hyprland JSON output) - README.md rewritten to cover both modes, the new flags, the tuned defaults per mode + per vendor, the headless internals, and the client pointer
This commit is contained in:
214
README.md
214
README.md
@@ -1,14 +1,8 @@
|
||||
# omarchy-moonlight
|
||||
|
||||
Idempotent install scripts that set up [Sunshine](https://github.com/LizardByte/Sunshine) (host) and [Moonlight](https://moonlight-stream.org/) (client) on Omarchy / Arch Linux / Hyprland / Wayland machines.
|
||||
Idempotent bash installer that sets up [Sunshine](https://github.com/LizardByte/Sunshine) (host) and [Moonlight](https://moonlight-stream.org/) (client) on Omarchy / Arch Linux / Hyprland / Wayland machines. Works across NVIDIA, AMD, and Intel hosts. A companion script handles the Mac client; iOS / Android / Apple TV clients are App Store installs.
|
||||
|
||||
Designed so the same script runs on:
|
||||
|
||||
- a primary NVIDIA desktop (host + client)
|
||||
- a Framework AMD laptop (host + client)
|
||||
- any other Omarchy box
|
||||
|
||||
After install, the machine can both stream out (via Sunshine) and view streams (via Moonlight). A Macbook can install Moonlight separately and connect to either Linux host.
|
||||
The same script can leave a machine acting as host, client, or both. Re-running it is safe: every step is "check, then act."
|
||||
|
||||
## Quick start
|
||||
|
||||
@@ -20,69 +14,119 @@ cd ~/omarchy-moonlight
|
||||
|
||||
Then:
|
||||
|
||||
1. **Log out and back in** if you weren't already in the `input` group (the installer adds you; the group only takes effect on a fresh login).
|
||||
2. Open <https://localhost:47990> and set a Sunshine username + password.
|
||||
3. On a Moonlight client (Mac / phone / the other Linux box), add this host by LAN IP and enter the PIN that Sunshine's web UI shows during pairing.
|
||||
1. **Log out and back in** if you weren't already in the `input` group (the installer adds you; the group only takes effect on a fresh login). `newgrp input` works as a one-shell shortcut.
|
||||
2. Open <https://localhost:47990> and set a Sunshine username + password. The cert is self-signed; accept the warning.
|
||||
3. On a Moonlight client (Mac / phone / the other Linux box), add this host by LAN IP and enter the 4-digit PIN that Sunshine's web UI shows during pairing.
|
||||
|
||||
## What it does
|
||||
## Streaming modes
|
||||
|
||||
In order, each step is "check, then act" — re-running is safe:
|
||||
The installer picks between two capture strategies. The choice gets baked into `~/.config/sunshine/sunshine.conf` and (for headless) wires in two prep-cmd hook scripts.
|
||||
|
||||
1. **Preflight** — confirms Wayland session, GPU driver is loaded, `nvidia-drm.modeset=1` isn't explicitly disabled (would silently break KMS capture), pipewire-pulse is present for audio.
|
||||
2. **Packages** — installs `sunshine-bin` (precompiled, fast) and `moonlight-qt` from the AUR via `yay`. Plus runtime helpers: `pipewire-pulse`, `vulkan-tools`, `libva-utils`. Use `--from-source` to build Sunshine from source instead.
|
||||
| Mode | Capture backend | Stream resolution | Needs physical monitor? | Default on |
|
||||
|---|---|---|---|---|
|
||||
| **Mirror** | `capture=kms` (DRM) | Whatever the host's monitor is set to | Yes (or a connected dummy plug) | All hosts except `JARVIS` |
|
||||
| **Headless** | `capture=wlr`, `output_name=HEADLESS-1` | Adapts per-connection to each client's native resolution | No | Hostname `JARVIS` |
|
||||
|
||||
Override with `--headless` or `--mirror` on any host.
|
||||
|
||||
### Mirror mode
|
||||
|
||||
Sunshine captures the real DRM display. Lowest latency, simplest mental model. The client sees exactly what's on the host monitor. Pick this when the host has a working display attached and you want a "remote screen" experience.
|
||||
|
||||
### Headless mode
|
||||
|
||||
Sunshine captures a Hyprland headless output. On each client connect, a `global_prep_cmd` runs `sunshine-stream-do.sh`, which:
|
||||
|
||||
1. Reads `SUNSHINE_CLIENT_WIDTH`, `SUNSHINE_CLIENT_HEIGHT`, `SUNSHINE_CLIENT_FPS` from Sunshine's environment.
|
||||
2. Ensures `HEADLESS-1` exists, creating it via `hyprctl output create headless` if missing.
|
||||
3. Resizes it to the client's native resolution / framerate via `hyprctl keyword monitor`.
|
||||
4. Moves the currently active workspace onto `HEADLESS-1` and focuses it, so existing windows appear in the stream.
|
||||
|
||||
On disconnect, `sunshine-stream-undo.sh` moves the workspace back to a real monitor (if one exists) and removes `HEADLESS-1`.
|
||||
|
||||
The result: different clients can connect at different resolutions and the host adapts per-connection. The host's physical monitor is optional — perfect for a KVM-attached machine whose monitor is often switched to another input. The hostname-`JARVIS` default exists because that's the canonical KVM-attached target; a laptop with its own screen is better off in mirror mode.
|
||||
|
||||
## What the installer does
|
||||
|
||||
Every step is idempotent. In order:
|
||||
|
||||
1. **Preflight** — confirms Wayland session, GPU driver is loaded, `nvidia-drm.modeset=1` is not explicitly disabled (would silently break KMS capture), `pipewire-pulse` is present for audio.
|
||||
2. **Packages** — installs `sunshine-bin` (precompiled) and `moonlight-qt` from the AUR via `yay`, plus runtime helpers: `pipewire-pulse`, `vulkan-tools`, `libva-utils`. `--from-source` (or `SUNSHINE_PKG=sunshine`) switches to the source build.
|
||||
3. **GPU encoder support**:
|
||||
- NVIDIA: `nvidia-utils`, `libva-nvidia-driver`
|
||||
- AMD: `libva-mesa-driver`, `mesa-vdpau`, `vulkan-radeon`
|
||||
- Intel: `intel-media-driver`, `vulkan-intel`
|
||||
4. **Permissions**:
|
||||
- Adds you to the `input` group
|
||||
- Drops `/etc/udev/rules.d/60-uinput.rules` if no equivalent rule exists (lets Sunshine use `/dev/uinput` for virtual gamepad/keyboard/mouse)
|
||||
- `setcap cap_sys_admin+p` on the `sunshine` binary so KMS screen capture works without root
|
||||
5. **Tuned config** — writes `~/.config/sunshine/sunshine.conf` for low-latency LAN streaming. Per-vendor encoder settings (NVENC P1+`ll`, VAAPI ultralowlatency, QuickSync veryfast). Marked with `# managed-by: omarchy-moonlight`; remove that marker to take ownership and the installer will never touch it again.
|
||||
6. **Firewall** — opens Sunshine's ports on `firewalld` / `ufw` if either is active. Skips silently otherwise.
|
||||
7. **Service** — enables `sunshine.service` under `systemd --user` and turns on `loginctl enable-linger` so the host is reachable without an active graphical login.
|
||||
8. **Verify** — runs the same checks as `--doctor` to confirm everything's actually wired up (cap_sys_admin set, group resolved, web UI listening on :47990, encoder reachable).
|
||||
- Adds you to the `input` group.
|
||||
- Drops `/etc/udev/rules.d/60-uinput.rules` if no equivalent rule exists (so Sunshine can use `/dev/uinput` for virtual gamepad / keyboard / mouse).
|
||||
- `setcap cap_sys_admin+p` on the `sunshine` binary so KMS screen capture works without root.
|
||||
5. **Headless hooks** (headless mode only) — installs `sunshine-stream-do.sh` and `sunshine-stream-undo.sh` to `~/.local/share/omarchy-moonlight/bin/` and references them as the Sunshine `global_prep_cmd`.
|
||||
6. **Tuned config** — writes `~/.config/sunshine/sunshine.conf` for low-latency LAN streaming, with per-vendor encoder picks. Marked with `# managed-by: omarchy-moonlight`; remove that marker to take ownership and the installer will never touch it again.
|
||||
7. **Firewall** — opens Sunshine's ports on `firewalld` / `ufw` if either is active. Skips silently otherwise.
|
||||
8. **Service** — enables `sunshine.service` under `systemctl --user` and turns on `loginctl enable-linger` so the host is reachable without an active graphical login.
|
||||
9. **Verify** — runs the same checks as `--doctor` to confirm everything's actually wired up (cap_sys_admin set, group resolved, web UI listening on `:47990`, encoder reachable, hooks present where expected).
|
||||
|
||||
## Flags
|
||||
|
||||
```text
|
||||
./install.sh --no-autostart # install but don't enable the user service
|
||||
./install.sh --no-firewall # skip firewall rules
|
||||
./install.sh --no-moonlight # host-only (no client)
|
||||
./install.sh --no-sunshine # client-only (no host)
|
||||
./install.sh --no-config # leave Sunshine to generate its own default config
|
||||
./install.sh --from-source # build Sunshine from source (slower; uses 'sunshine' AUR pkg)
|
||||
./install.sh --doctor # run only the verification checks (no install)
|
||||
./install.sh # auto-detect mode (headless on JARVIS, mirror elsewhere)
|
||||
./install.sh --headless # force headless mode
|
||||
./install.sh --mirror # force mirror mode
|
||||
./install.sh --no-autostart # install but don't enable systemctl --user sunshine
|
||||
./install.sh --no-firewall # skip firewall configuration
|
||||
./install.sh --no-config # don't write a tuned sunshine.conf
|
||||
./install.sh --no-sunshine # client-only (install Moonlight only)
|
||||
./install.sh --no-moonlight # host-only
|
||||
./install.sh --from-source # build sunshine from source (default uses sunshine-bin)
|
||||
./install.sh --doctor # verification only (no install)
|
||||
|
||||
./uninstall.sh # remove packages and udev rule, keep user data
|
||||
./uninstall.sh --purge # also delete ~/.config/sunshine
|
||||
./uninstall.sh --keep-moonlight
|
||||
```
|
||||
|
||||
The doctor flag is the fastest way to debug a degraded install — it'll tell you exactly which piece (group, cap, udev, encoder, service, port) is broken.
|
||||
Environment overrides:
|
||||
|
||||
### Tuned defaults written to sunshine.conf
|
||||
| Variable | Effect |
|
||||
|---|---|
|
||||
| `SUNSHINE_PKG=sunshine` | Build from source (equivalent to `--from-source`). |
|
||||
| `SUNSHINE_PKG=sunshine-bin` | Use the prebuilt AUR package (default). |
|
||||
|
||||
| Setting | Value | Why |
|
||||
|---|---|---|
|
||||
| `capture` | `kms` | Correct backend for Wayland; uses DRM directly. |
|
||||
| `encoder` (NVIDIA) | `nvenc` + `nvenc_preset=p1`, `nvenc_tune=ll`, `nvenc_rc=cbr` | P1 minimizes encode latency; low-latency tune disables look-ahead; CBR keeps bitrate predictable over LAN. |
|
||||
| `encoder` (AMD) | `vaapi` + `amd_usage=ultralowlatency`, `amd_rc=cbr` | Mirrors the NVIDIA choices on AMD's encoder. |
|
||||
| `min_threads` | `4` | Helps keep up at high bitrates / 4K. |
|
||||
| `audio_sink` | `pulse` | Captures from PipeWire's Pulse compat layer. |
|
||||
The `--doctor` flag is the fastest way to debug a degraded install — it reports exactly which piece (group, cap, udev, encoder, service, port, hooks) is broken.
|
||||
|
||||
Anything else (resolution, bitrate, paired clients, app launchers) is set via the web UI.
|
||||
## Tuned defaults
|
||||
|
||||
## Uninstall
|
||||
Written to `~/.config/sunshine/sunshine.conf` with a `# managed-by: omarchy-moonlight` marker on the first line. The installer only overwrites the file while that marker is present; delete the marker (or the line) to lock the file against future runs.
|
||||
|
||||
```bash
|
||||
./uninstall.sh # remove packages + udev rule, keep user data
|
||||
./uninstall.sh --purge # also delete ~/.config/sunshine
|
||||
```
|
||||
| Setting | Mirror value | Headless value | Why |
|
||||
|---|---|---|---|
|
||||
| `capture` | `kms` | `wlr` | KMS for real DRM displays; `wlr` for Hyprland headless outputs. |
|
||||
| `output_name` | (unset) | `HEADLESS-1` | Pin wlr capture to the headless output the hooks manage. |
|
||||
| `global_prep_cmd` | (unset) | `do`/`undo` pair | Runs the headless hook scripts on client connect / disconnect. |
|
||||
| `min_threads` | `4` | `4` | Helps keep up at high bitrates / 4K. |
|
||||
| `audio_sink` | `pulse` | `pulse` | Captures from PipeWire's Pulse compat layer. |
|
||||
|
||||
## How streaming works once it's set up
|
||||
Per-vendor encoder picks:
|
||||
|
||||
- **Host (Sunshine) ports** (auto-opened if a firewall is active):
|
||||
- TCP: `47984 47989 47990 48010`
|
||||
- UDP: `47998 47999 48000 48010`
|
||||
- **Pairing**: on first connect, Moonlight shows a PIN. Type it into Sunshine's web UI (<https://localhost:47990> → PIN tab) within a few seconds.
|
||||
- **Capture mode**: this script configures KMS capture, which streams whatever is on the host's real monitor. A virtual-display mode (so streaming doesn't take over the desk) is a future addition — see `remote/` notes when it lands.
|
||||
| Vendor | Encoder | Settings | Why |
|
||||
|---|---|---|---|
|
||||
| NVIDIA | `nvenc` | `nvenc_preset=p1`, `nvenc_tune=ll`, `nvenc_rc=cbr` | P1 minimizes encode latency; `ll` disables look-ahead; CBR keeps bitrate predictable over LAN. |
|
||||
| AMD | `vaapi` | `amd_usage=ultralowlatency`, `amd_rc=cbr` | Mirrors the NVIDIA latency-first picks on AMD's encoder. |
|
||||
| Intel | `quicksync` | `qsv_preset=veryfast` | Lowest-latency QuickSync preset. |
|
||||
|
||||
Everything else (bitrate, paired clients, app launchers) is set via the web UI.
|
||||
|
||||
## Clients
|
||||
|
||||
The host-side installer handles Linux clients via `moonlight-qt`. For everything else, see `client/README.md` for per-platform install plus the first-pair walkthrough.
|
||||
|
||||
| Platform | Install path |
|
||||
|---|---|
|
||||
| Linux (Arch / Omarchy) | Bundled — `./install.sh` installs `moonlight-qt` unless `--no-moonlight`. |
|
||||
| macOS | `client/install-macos.sh` (Moonlight via Homebrew cask). |
|
||||
| iOS / iPadOS | App Store: Moonlight Game Streaming. |
|
||||
| Android | Play Store: Moonlight Game Streaming. |
|
||||
| Apple TV (tvOS) | App Store: Moonlight Game Streaming. |
|
||||
|
||||
## Diagnostics
|
||||
|
||||
@@ -94,16 +138,47 @@ getcap "$(readlink -f "$(command -v sunshine)")" # should include cap_sys_a
|
||||
id -nG | tr ' ' '\n' | grep -x input # confirm group membership
|
||||
```
|
||||
|
||||
If Moonlight pairs but the stream is black:
|
||||
Useful Sunshine ports (auto-opened if a firewall is active):
|
||||
|
||||
- Confirm you're in the `input` group **in a freshly logged-in session** (not just listed in `/etc/group`).
|
||||
- Confirm `getcap` shows `cap_sys_admin` on the sunshine binary.
|
||||
- Check `journalctl --user -u sunshine` for `KMS` / `DRM` errors.
|
||||
- On NVIDIA: confirm the proprietary driver is active (`nvidia-smi`) and that `nvidia-drm.modeset=1` is in effect. If you have an older driver (≤555) and modeset isn't on, add `nvidia-drm.modeset=1` to your kernel cmdline and reboot.
|
||||
- TCP: `47984 47989 47990 48010`
|
||||
- UDP: `47998 47999 48000 48010`
|
||||
|
||||
## Headless mode internals
|
||||
|
||||
Hooks live at `~/.local/share/omarchy-moonlight/bin/` and are referenced from `sunshine.conf` as a `global_prep_cmd` pair.
|
||||
|
||||
| Script | Trigger | Responsibilities |
|
||||
|---|---|---|
|
||||
| `sunshine-stream-do.sh` | Client connects | Ensure `HEADLESS-1` exists; resize it to `${SUNSHINE_CLIENT_WIDTH}x${SUNSHINE_CLIENT_HEIGHT}@${SUNSHINE_CLIENT_FPS}`; move active workspace onto it; focus it. |
|
||||
| `sunshine-stream-undo.sh` | Client disconnects | Move workspace back to a real monitor (if any); remove `HEADLESS-1`. |
|
||||
|
||||
Environment variables Sunshine sets on each connect and the hooks consume:
|
||||
|
||||
- `SUNSHINE_CLIENT_WIDTH` — client viewport width in pixels.
|
||||
- `SUNSHINE_CLIENT_HEIGHT` — client viewport height in pixels.
|
||||
- `SUNSHINE_CLIENT_FPS` — client target framerate.
|
||||
|
||||
The hooks defensively recover `HYPRLAND_INSTANCE_SIGNATURE` by scanning `$XDG_RUNTIME_DIR/hypr/` if Sunshine's environment doesn't inherit it (rare, but possible under stripped-down user services). They also snapshot prior monitor state under `$XDG_RUNTIME_DIR/sunshine-headless/` so undo can restore the previous workspace assignment. If Hyprland isn't running, the hooks log a message and exit cleanly — Sunshine still streams, just without per-client resizing.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Stream pairs but is black.** Confirm three things:
|
||||
|
||||
1. You're in the `input` group in a freshly logged-in session (not just listed in `/etc/group` — group membership is set at login). `id -nG` is the truth.
|
||||
2. `getcap` shows `cap_sys_admin+p` on the resolved `sunshine` binary.
|
||||
3. `journalctl --user -u sunshine` doesn't show `KMS` / `DRM` / `wlr` errors at stream start.
|
||||
|
||||
**NVIDIA: capture starts then dies.** Older proprietary drivers (≤555) need `nvidia-drm.modeset=1` on the kernel command line. `cat /sys/module/nvidia_drm/parameters/modeset` should print `Y`. If not, add `nvidia-drm.modeset=1` to your bootloader cmdline and reboot.
|
||||
|
||||
**KVM / headless: client sees an old resolution.** The headless hooks resize `HEADLESS-1` per connect. If you see stale geometry, check `~/.local/share/omarchy-moonlight/bin/sunshine-stream-do.sh` is referenced in `sunshine.conf`'s `global_prep_cmd` and that `hyprctl monitors all` lists `HEADLESS-1` during a stream.
|
||||
|
||||
**Headless host, no monitor at all.** Hyprland needs to be running for the hooks to do anything. On a truly headless box, run Hyprland under `uwsm` from a tty (or via `systemctl --user start hyprland-session.target`) so its IPC socket exists when Sunshine fires the prep command.
|
||||
|
||||
**Moonlight can't find the host on the LAN.** Confirm the firewall ports above are open. mDNS discovery is best-effort; adding the host manually by LAN IP is the reliable path.
|
||||
|
||||
## Remote access (planned)
|
||||
|
||||
LAN-only for now. Remote access will be added later via one of: Tailscale, WireGuard, or Cloudflare. See `remote/` (stub) when implemented.
|
||||
LAN-only for now. Remote access is planned via one of Tailscale, WireGuard, or Cloudflare Tunnel, depending on which gives the best UDP latency to mobile clients. Notes will land under `remote/` when implemented.
|
||||
|
||||
## Layout
|
||||
|
||||
@@ -112,15 +187,22 @@ omarchy-moonlight/
|
||||
├── install.sh
|
||||
├── uninstall.sh
|
||||
├── README.md
|
||||
├── bin/
|
||||
│ ├── sunshine-stream-do.sh # prep-cmd hook: create/resize headless on connect
|
||||
│ └── sunshine-stream-undo.sh # prep-cmd hook: tear down headless on disconnect
|
||||
├── client/
|
||||
│ ├── install-macos.sh # Moonlight via brew cask
|
||||
│ └── README.md # per-platform install + first-pair flow
|
||||
├── lib/
|
||||
│ ├── common.sh # logging, sudo, idempotency helpers
|
||||
│ ├── detect.sh # GPU vendor, session type, hostname
|
||||
│ ├── preflight.sh # pre-install sanity checks (driver, modeset, audio, session)
|
||||
│ ├── packages.sh # yay -S sunshine-bin moonlight-qt + GPU encoders
|
||||
│ ├── permissions.sh # input group, uinput udev, setcap cap_sys_admin
|
||||
│ ├── config.sh # writes tuned sunshine.conf (managed-by marker)
|
||||
│ ├── firewall.sh # ufw/firewalld detection + port opening
|
||||
│ ├── service.sh # systemctl --user enable + loginctl enable-linger
|
||||
│ └── verify.sh # post-install checks (also reused by --doctor)
|
||||
└── files/ # (reserved — drop-in config files if needed later)
|
||||
│ ├── common.sh # logging, sudo, idempotency helpers
|
||||
│ ├── detect.sh # GPU vendor, session type, hostname
|
||||
│ ├── preflight.sh # pre-install sanity checks
|
||||
│ ├── packages.sh # yay -S sunshine-bin moonlight-qt + GPU encoders
|
||||
│ ├── permissions.sh # input group, uinput udev, setcap cap_sys_admin
|
||||
│ ├── config.sh # writes tuned sunshine.conf (managed-by marker)
|
||||
│ ├── headless.sh # installs prep-cmd hooks to ~/.local/share/
|
||||
│ ├── firewall.sh # ufw/firewalld detection + port opening
|
||||
│ ├── service.sh # systemctl --user enable + enable-linger
|
||||
│ └── verify.sh # post-install checks (also reused by --doctor)
|
||||
└── files/ # drop-in config files referenced by lib/
|
||||
```
|
||||
|
||||
66
bin/sunshine-stream-do.sh
Normal file
66
bin/sunshine-stream-do.sh
Normal file
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env bash
|
||||
# Invoked by Sunshine as a stream-start hook (global_prep_cmd `do`).
|
||||
# Creates/resizes a Hyprland headless output to match the connecting
|
||||
# Moonlight client's resolution, and moves the active workspace onto it
|
||||
# so the user's existing windows are visible on the stream.
|
||||
#
|
||||
# Sunshine env vars set on connect:
|
||||
# SUNSHINE_CLIENT_WIDTH, SUNSHINE_CLIENT_HEIGHT, SUNSHINE_CLIENT_FPS
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
log() { printf '[sunshine-do] %s\n' "$*" >&2; }
|
||||
|
||||
WIDTH="${SUNSHINE_CLIENT_WIDTH:-1920}"
|
||||
HEIGHT="${SUNSHINE_CLIENT_HEIGHT:-1080}"
|
||||
FPS="${SUNSHINE_CLIENT_FPS:-60}"
|
||||
MON="HEADLESS-1"
|
||||
STATE_DIR="${XDG_RUNTIME_DIR:-/tmp}/sunshine-headless"
|
||||
mkdir -p "$STATE_DIR"
|
||||
|
||||
if ! command -v hyprctl >/dev/null 2>&1; then
|
||||
log "hyprctl not found; cannot configure headless. Stream will use whatever output Sunshine selects."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Recover Hyprland signature if it wasn't inherited (defensive — UWSM exports it,
|
||||
# but a stray service environment could miss it).
|
||||
if [[ -z "${HYPRLAND_INSTANCE_SIGNATURE:-}" ]]; then
|
||||
for sig_dir in "${XDG_RUNTIME_DIR:-/tmp}"/hypr/*/; do
|
||||
[[ -d "$sig_dir" ]] || continue
|
||||
export HYPRLAND_INSTANCE_SIGNATURE="$(basename "$sig_dir")"
|
||||
log "Discovered HYPRLAND_INSTANCE_SIGNATURE=$HYPRLAND_INSTANCE_SIGNATURE"
|
||||
break
|
||||
done
|
||||
if [[ -z "${HYPRLAND_INSTANCE_SIGNATURE:-}" ]]; then
|
||||
log "Hyprland not running; nothing to configure."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Snapshot prior state so undo can restore.
|
||||
hyprctl monitors -j > "$STATE_DIR/prev-monitors.json" 2>/dev/null || true
|
||||
PREV_WS="$(hyprctl activeworkspace -j 2>/dev/null | jq -r '.id // 1' || echo 1)"
|
||||
echo "$PREV_WS" > "$STATE_DIR/prev-workspace-id"
|
||||
|
||||
# Ensure headless exists.
|
||||
if ! hyprctl monitors all -j 2>/dev/null | jq -e --arg m "$MON" '.[] | select(.name == $m)' >/dev/null; then
|
||||
log "Creating headless output $MON"
|
||||
hyprctl output create headless >/dev/null
|
||||
# Brief settle so Hyprland registers the new output before we configure it.
|
||||
for _ in 1 2 3 4 5; do
|
||||
hyprctl monitors all -j | jq -e --arg m "$MON" '.[] | select(.name == $m)' >/dev/null 2>&1 && break
|
||||
sleep 0.1
|
||||
done
|
||||
fi
|
||||
|
||||
# Resize headless to the client's resolution / framerate.
|
||||
log "Sizing $MON → ${WIDTH}x${HEIGHT}@${FPS}"
|
||||
hyprctl keyword monitor "$MON,${WIDTH}x${HEIGHT}@${FPS},auto,1" >/dev/null
|
||||
|
||||
# Move the active workspace onto the headless so existing windows appear in the stream.
|
||||
log "Moving workspace $PREV_WS → $MON, focusing it"
|
||||
hyprctl dispatch moveworkspacetomonitor "$PREV_WS $MON" >/dev/null || true
|
||||
hyprctl dispatch focusmonitor "$MON" >/dev/null || true
|
||||
|
||||
log "Stream ready: ${WIDTH}x${HEIGHT}@${FPS} on $MON"
|
||||
51
bin/sunshine-stream-undo.sh
Normal file
51
bin/sunshine-stream-undo.sh
Normal file
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env bash
|
||||
# Invoked by Sunshine as a stream-stop hook (global_prep_cmd `undo`).
|
||||
# Moves the previously-active workspace back to a real monitor (if any
|
||||
# exist) and tears down the headless output created by sunshine-stream-do.sh.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
log() { printf '[sunshine-undo] %s\n' "$*" >&2; }
|
||||
|
||||
MON="HEADLESS-1"
|
||||
STATE_DIR="${XDG_RUNTIME_DIR:-/tmp}/sunshine-headless"
|
||||
|
||||
if ! command -v hyprctl >/dev/null 2>&1; then
|
||||
log "hyprctl not found; nothing to undo."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ -z "${HYPRLAND_INSTANCE_SIGNATURE:-}" ]]; then
|
||||
for sig_dir in "${XDG_RUNTIME_DIR:-/tmp}"/hypr/*/; do
|
||||
[[ -d "$sig_dir" ]] || continue
|
||||
export HYPRLAND_INSTANCE_SIGNATURE="$(basename "$sig_dir")"
|
||||
break
|
||||
done
|
||||
if [[ -z "${HYPRLAND_INSTANCE_SIGNATURE:-}" ]]; then
|
||||
log "Hyprland not running; nothing to undo."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
PREV_WS="$(cat "$STATE_DIR/prev-workspace-id" 2>/dev/null || echo 1)"
|
||||
|
||||
# Find a non-headless monitor to move the workspace back to. If there isn't one
|
||||
# (truly headless host with KVM detached), the workspace just lives on whatever
|
||||
# Hyprland reassigns it to when we remove the output.
|
||||
REAL_MON="$(hyprctl monitors -j 2>/dev/null | jq -r '.[] | select(.name | test("^HEADLESS") | not) | .name' | head -n1)"
|
||||
if [[ -n "$REAL_MON" ]]; then
|
||||
log "Returning workspace $PREV_WS → $REAL_MON"
|
||||
hyprctl dispatch moveworkspacetomonitor "$PREV_WS $REAL_MON" >/dev/null || true
|
||||
hyprctl dispatch focusmonitor "$REAL_MON" >/dev/null || true
|
||||
else
|
||||
log "No real monitor connected; leaving workspace assignment to Hyprland defaults."
|
||||
fi
|
||||
|
||||
if hyprctl monitors all -j 2>/dev/null | jq -e --arg m "$MON" '.[] | select(.name == $m)' >/dev/null; then
|
||||
log "Removing $MON"
|
||||
hyprctl output remove "$MON" >/dev/null || true
|
||||
fi
|
||||
|
||||
# Clean state files but keep the directory for the next run.
|
||||
rm -f "$STATE_DIR/prev-monitors.json" "$STATE_DIR/prev-workspace-id"
|
||||
log "Stream teardown complete"
|
||||
125
client/README.md
Normal file
125
client/README.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# Moonlight clients
|
||||
|
||||
This directory covers installing Moonlight on the devices that connect to your
|
||||
Sunshine host. The host side (Sunshine on Linux) is documented in the
|
||||
[top-level README](../README.md).
|
||||
|
||||
## Install
|
||||
|
||||
### macOS
|
||||
|
||||
From a Mac that has this repo checked out:
|
||||
|
||||
```sh
|
||||
./client/install-macos.sh
|
||||
```
|
||||
|
||||
The script verifies Homebrew is present and runs `brew install --cask moonlight`.
|
||||
It will not install Homebrew for you.
|
||||
|
||||
Manual equivalent:
|
||||
|
||||
```sh
|
||||
brew install --cask moonlight
|
||||
```
|
||||
|
||||
Fallback (no Homebrew): download a signed DMG from the official releases page
|
||||
and drag `Moonlight.app` into `/Applications`:
|
||||
|
||||
- https://github.com/moonlight-stream/moonlight-qt/releases
|
||||
|
||||
### Android
|
||||
|
||||
Install "Moonlight Game Streaming" from the Play Store:
|
||||
|
||||
- https://play.google.com/store/apps/details?id=com.limelight
|
||||
|
||||
### iOS / iPadOS
|
||||
|
||||
Install "Moonlight Game Streaming" from the App Store:
|
||||
|
||||
- https://apps.apple.com/us/app/moonlight-game-streaming/id1000551566
|
||||
|
||||
### Apple TV
|
||||
|
||||
Same listing on the tvOS App Store. Search "Moonlight Game Streaming" on the
|
||||
Apple TV itself, or use the App Store link above from an iOS device signed in
|
||||
to the same Apple ID.
|
||||
|
||||
### Steam Deck / Linux
|
||||
|
||||
Use `moonlight-qt` from your distro's package manager (Flatpak on Steam Deck,
|
||||
`pacman`/`apt` elsewhere). It is the same Qt-based client as macOS and Windows.
|
||||
|
||||
## First pair
|
||||
|
||||
The pairing flow is the same on every client.
|
||||
|
||||
1. Confirm the host is healthy. On the host machine:
|
||||
|
||||
```sh
|
||||
./install.sh --doctor
|
||||
```
|
||||
|
||||
Sunshine must be running and reachable on the LAN.
|
||||
|
||||
2. On the client, open Moonlight. The host should appear automatically via
|
||||
mDNS as long as the client is on the same LAN. If it does not appear, use
|
||||
"Add Host Manually" and enter the host's LAN IP address.
|
||||
|
||||
3. Click or tap the host. Moonlight displays a 4-digit PIN.
|
||||
|
||||
4. On the host machine, open the Sunshine web UI:
|
||||
|
||||
```
|
||||
https://localhost:47990
|
||||
```
|
||||
|
||||
Accept the self-signed certificate. Log in (credentials are set the first
|
||||
time you visit). Go to the PIN tab, enter the 4-digit PIN from the client,
|
||||
and submit. Pairing typically completes within about 5 seconds.
|
||||
|
||||
5. Back in Moonlight, the host now shows as "Paired". Click it to see the
|
||||
available apps. The default app is "Desktop", which is either a full-screen
|
||||
mirror of the host's display or a headless virtual display, depending on
|
||||
how the host is configured.
|
||||
|
||||
## Streaming notes
|
||||
|
||||
- **Resolution.** Moonlight lets you pick a target stream resolution per host.
|
||||
On a headless-configured host, that resolution is what the host actually
|
||||
renders at. On a mirror-configured host, the host renders at its native
|
||||
resolution and Moonlight downscales on the client.
|
||||
|
||||
- **Bitrate.** Moonlight's defaults are conservative for general internet use.
|
||||
For LAN streaming, bump it to 50-100 Mbps.
|
||||
|
||||
- **Audio.** By default, Moonlight captures audio from the host and plays it
|
||||
on the client. If you want the host's speakers/monitor to keep playing
|
||||
audio while you stream, enable "Play audio on host" in Moonlight's stream
|
||||
settings.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **"Host not found" / host does not appear automatically.** Confirm both
|
||||
machines are on the same LAN/VLAN. mDNS does not traverse VLANs without an
|
||||
mDNS reflector. Fall back to "Add Host Manually" with the host's IP.
|
||||
|
||||
- **Pairing PIN does not work.** Two common causes: the Sunshine web UI
|
||||
session has timed out (log in again and re-enter the PIN), or the PIN is
|
||||
being entered into the wrong host (multiple Sunshine instances on the LAN).
|
||||
Cancel and re-trigger pair from the client to get a fresh PIN.
|
||||
|
||||
- **Black screen after pairing.** This is a host-side problem. On the host:
|
||||
|
||||
```sh
|
||||
./install.sh --doctor
|
||||
```
|
||||
|
||||
- **Mac cannot reach `brew`.** Homebrew is not installed (or not on `PATH`).
|
||||
Install it per https://brew.sh, open a fresh shell, then re-run
|
||||
`./client/install-macos.sh`.
|
||||
|
||||
- **Android sees the host but cannot connect.** The Android device is
|
||||
probably on a guest WiFi SSID that is isolated from the main LAN. Move it
|
||||
to the main LAN, or use "Add Host Manually" with the host IP.
|
||||
61
client/install-macos.sh
Executable file
61
client/install-macos.sh
Executable file
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env bash
|
||||
# Install Moonlight on macOS via Homebrew cask.
|
||||
# Standalone: does not source lib/common.sh (intended to run on a Mac that
|
||||
# may not have the full repo checked out yet).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [[ -t 1 ]]; then
|
||||
BOLD=$'\033[1m'
|
||||
RED=$'\033[31m'
|
||||
GREEN=$'\033[32m'
|
||||
YELLOW=$'\033[33m'
|
||||
BLUE=$'\033[34m'
|
||||
RESET=$'\033[0m'
|
||||
else
|
||||
BOLD="" RED="" GREEN="" YELLOW="" BLUE="" RESET=""
|
||||
fi
|
||||
|
||||
step() { printf '\n%s==>%s %s%s%s\n' "$BLUE" "$RESET" "$BOLD" "$*" "$RESET"; }
|
||||
info() { printf ' %s\n' "$*"; }
|
||||
ok() { printf ' %s✓%s %s\n' "$GREEN" "$RESET" "$*"; }
|
||||
warn() { printf ' %s!%s %s\n' "$YELLOW" "$RESET" "$*" >&2; }
|
||||
err() { printf ' %s✗%s %s\n' "$RED" "$RESET" "$*" >&2; }
|
||||
|
||||
# Refuse to run anywhere but macOS.
|
||||
if [[ "$(uname -s)" != "Darwin" ]]; then
|
||||
err "This script only runs on macOS (Darwin). Detected: $(uname -s)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
step "Checking for Homebrew"
|
||||
if ! command -v brew >/dev/null 2>&1; then
|
||||
err "Homebrew is not installed."
|
||||
info "Install it with the official one-liner, then re-run this script:"
|
||||
info ""
|
||||
info ' /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"'
|
||||
info ""
|
||||
info "See https://brew.sh for details."
|
||||
exit 1
|
||||
fi
|
||||
ok "Homebrew found: $(command -v brew)"
|
||||
|
||||
step "Installing Moonlight (brew cask)"
|
||||
# brew install --cask is idempotent: re-running on an already-installed cask
|
||||
# is a no-op and exits 0.
|
||||
if brew install --cask moonlight; then
|
||||
ok "Moonlight installed (or already present)."
|
||||
else
|
||||
err "brew install --cask moonlight failed."
|
||||
warn "Try: brew update && brew install --cask moonlight"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
step "Next steps"
|
||||
info "App location: /Applications/Moonlight.app"
|
||||
info "Launch: open -a Moonlight"
|
||||
info ""
|
||||
info "Pair this Mac with your Sunshine host by following the walkthrough in:"
|
||||
info " client/README.md"
|
||||
info ""
|
||||
ok "Done."
|
||||
32
install.sh
32
install.sh
@@ -24,6 +24,8 @@ source "$SCRIPT_DIR/lib/firewall.sh"
|
||||
source "$SCRIPT_DIR/lib/service.sh"
|
||||
# shellcheck source=lib/verify.sh
|
||||
source "$SCRIPT_DIR/lib/verify.sh"
|
||||
# shellcheck source=lib/headless.sh
|
||||
source "$SCRIPT_DIR/lib/headless.sh"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
@@ -38,6 +40,8 @@ Options:
|
||||
--no-sunshine Don't install sunshine (client-only setup)
|
||||
--no-config Don't write a tuned sunshine.conf
|
||||
--from-source Build Sunshine from source (equivalent to SUNSHINE_PKG=sunshine)
|
||||
--headless Force headless streaming mode (wlr capture of HEADLESS-1)
|
||||
--mirror Force mirror mode (KMS capture of the real display)
|
||||
--doctor Run only the post-install verification checks
|
||||
-h, --help Show this help
|
||||
|
||||
@@ -53,6 +57,7 @@ INSTALL_SUNSHINE=1
|
||||
INSTALL_MOONLIGHT=1
|
||||
WRITE_CONFIG=1
|
||||
DOCTOR_ONLY=0
|
||||
MODE_OVERRIDE=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
@@ -62,6 +67,8 @@ while [[ $# -gt 0 ]]; do
|
||||
--no-sunshine) INSTALL_SUNSHINE=0 ;;
|
||||
--no-config) WRITE_CONFIG=0 ;;
|
||||
--from-source) export SUNSHINE_PKG=sunshine ;;
|
||||
--headless) MODE_OVERRIDE="headless" ;;
|
||||
--mirror) MODE_OVERRIDE="mirror" ;;
|
||||
--doctor) DOCTOR_ONLY=1 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) err "Unknown option: $1"; usage; exit 2 ;;
|
||||
@@ -69,6 +76,21 @@ while [[ $# -gt 0 ]]; do
|
||||
shift
|
||||
done
|
||||
|
||||
# Pick streaming mode: explicit flag wins; otherwise default to headless on
|
||||
# JARVIS (the KVM-attached primary target) and mirror everywhere else.
|
||||
compute_stream_mode() {
|
||||
if [[ -n "$MODE_OVERRIDE" ]]; then
|
||||
STREAM_MODE="$MODE_OVERRIDE"
|
||||
return 0
|
||||
fi
|
||||
local host_lc="${HOSTNAME_SHORT,,}"
|
||||
if [[ "$host_lc" == "jarvis" ]]; then
|
||||
STREAM_MODE="headless"
|
||||
else
|
||||
STREAM_MODE="mirror"
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
require_not_root
|
||||
require_arch
|
||||
@@ -76,7 +98,10 @@ main() {
|
||||
|
||||
step "Detecting system"
|
||||
detect_all
|
||||
compute_stream_mode
|
||||
export STREAM_MODE
|
||||
info "Host: $HOSTNAME_SHORT GPU: $GPU_VENDOR Session: $SESSION_TYPE"
|
||||
info "Mode: $STREAM_MODE"
|
||||
|
||||
if [[ $DOCTOR_ONLY -eq 1 ]]; then
|
||||
verify_install
|
||||
@@ -96,9 +121,14 @@ main() {
|
||||
ensure_uinput_udev_rule
|
||||
set_sunshine_capabilities
|
||||
|
||||
if [[ "$STREAM_MODE" == "headless" ]]; then
|
||||
step "Installing headless prep-cmd hooks"
|
||||
install_headless_hooks
|
||||
fi
|
||||
|
||||
if [[ $WRITE_CONFIG -eq 1 ]]; then
|
||||
step "Writing tuned sunshine.conf"
|
||||
write_sunshine_config
|
||||
write_sunshine_config "$STREAM_MODE"
|
||||
else
|
||||
info "Skipping sunshine.conf (--no-config)"
|
||||
fi
|
||||
|
||||
@@ -7,6 +7,7 @@ SUNSHINE_CONF="$SUNSHINE_CONF_DIR/sunshine.conf"
|
||||
MANAGED_MARKER="# managed-by: omarchy-moonlight"
|
||||
|
||||
write_sunshine_config() {
|
||||
local mode="${1:-mirror}"
|
||||
mkdir -p "$SUNSHINE_CONF_DIR"
|
||||
|
||||
if [[ -f "$SUNSHINE_CONF" ]] && ! grep -qF "$MANAGED_MARKER" "$SUNSHINE_CONF"; then
|
||||
@@ -31,19 +32,36 @@ write_sunshine_config() {
|
||||
;;
|
||||
esac
|
||||
|
||||
info "Writing tuned $SUNSHINE_CONF (GPU: $GPU_VENDOR)"
|
||||
local capture_block
|
||||
case "$mode" in
|
||||
headless)
|
||||
# wlr capture of the Hyprland headless output that prep-cmd hooks create.
|
||||
# global_prep_cmd is parsed as a JSON array by Sunshine, so keep it on one line.
|
||||
capture_block="# Capture: wlr — captures a Hyprland headless output created on stream start.
|
||||
capture = wlr
|
||||
output_name = HEADLESS-1
|
||||
|
||||
# Hooks that create/resize HEADLESS-1 on stream start and remove it on stop.
|
||||
global_prep_cmd = [{\"do\":\"${DO_SCRIPT}\",\"undo\":\"${UNDO_SCRIPT}\"}]"
|
||||
;;
|
||||
mirror|*)
|
||||
capture_block="# Capture: KMS (DRM) — the right choice on Wayland.
|
||||
capture = kms"
|
||||
;;
|
||||
esac
|
||||
|
||||
info "Writing tuned $SUNSHINE_CONF (GPU: $GPU_VENDOR, mode: $mode)"
|
||||
cat >"$SUNSHINE_CONF" <<EOF
|
||||
$MANAGED_MARKER
|
||||
# Generated by omarchy-moonlight install.sh on $(date -Iseconds)
|
||||
# Delete this marker line to take ownership; the installer will then leave the file alone.
|
||||
#
|
||||
# Tuned for low-latency LAN streaming on Hyprland/Wayland with KMS capture.
|
||||
# Tuned for low-latency LAN streaming on Hyprland/Wayland.
|
||||
# Tune further via the web UI at https://localhost:47990
|
||||
|
||||
sunshine_name = ${HOSTNAME_SHORT}
|
||||
|
||||
# Capture: KMS (DRM) — the right choice on Wayland.
|
||||
capture = kms
|
||||
$capture_block
|
||||
|
||||
# Encoder tuned for $GPU_VENDOR
|
||||
$encoder_block
|
||||
|
||||
20
lib/headless.sh
Normal file
20
lib/headless.sh
Normal file
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
# Set up headless streaming mode: install prep-cmd hook scripts into a stable
|
||||
# location and ensure they're executable. The actual sunshine.conf entries
|
||||
# (capture=wlr, output_name=HEADLESS-1, global_prep_cmd=[...]) are written
|
||||
# by lib/config.sh.
|
||||
|
||||
HEADLESS_BIN_DIR="$HOME/.local/share/omarchy-moonlight/bin"
|
||||
|
||||
DO_SCRIPT="$HEADLESS_BIN_DIR/sunshine-stream-do.sh"
|
||||
UNDO_SCRIPT="$HEADLESS_BIN_DIR/sunshine-stream-undo.sh"
|
||||
export DO_SCRIPT UNDO_SCRIPT
|
||||
|
||||
install_headless_hooks() {
|
||||
# Install hook scripts to ~/.local/share so they don't disappear if the
|
||||
# repo gets moved or deleted. Sunshine's config will reference these stable paths.
|
||||
mkdir -p "$HEADLESS_BIN_DIR"
|
||||
install -m 0755 "$SCRIPT_DIR/bin/sunshine-stream-do.sh" "$DO_SCRIPT"
|
||||
install -m 0755 "$SCRIPT_DIR/bin/sunshine-stream-undo.sh" "$UNDO_SCRIPT"
|
||||
ok "Installed prep-cmd hooks to $HEADLESS_BIN_DIR"
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
install_sunshine() {
|
||||
# Ensure runtime deps useful for capture/diagnostics across vendors.
|
||||
yay_install pipewire-pulse vulkan-tools libva-utils
|
||||
yay_install pipewire-pulse vulkan-tools libva-utils jq
|
||||
yay_install "$SUNSHINE_PKG"
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ preflight_all() {
|
||||
preflight_gpu
|
||||
preflight_kms_modeset
|
||||
preflight_audio
|
||||
preflight_headless
|
||||
}
|
||||
|
||||
preflight_session() {
|
||||
@@ -96,3 +97,33 @@ preflight_audio() {
|
||||
warn "pipewire-pulse is not installed. Audio capture may not work until it is."
|
||||
fi
|
||||
}
|
||||
|
||||
preflight_headless() {
|
||||
# Only relevant in headless mode. Checks are non-fatal: install can proceed
|
||||
# even if Hyprland isn't reachable right now (hooks just won't function until
|
||||
# the user logs into Hyprland on the host).
|
||||
[[ "${STREAM_MODE:-}" == "headless" ]] || return 0
|
||||
|
||||
if command -v hyprctl >/dev/null 2>&1; then
|
||||
ok "hyprctl on PATH"
|
||||
else
|
||||
warn "hyprctl not found. Headless prep-cmd hooks will fail until Hyprland is installed and reachable."
|
||||
fi
|
||||
|
||||
if pkg_installed jq; then
|
||||
ok "jq installed (prep-cmd hooks have their parser)"
|
||||
else
|
||||
info "jq not installed yet — will be installed in the packages step."
|
||||
fi
|
||||
|
||||
if [[ -n "${HYPRLAND_INSTANCE_SIGNATURE:-}" ]]; then
|
||||
ok "Hyprland instance signature present in environment"
|
||||
else
|
||||
local rt="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
|
||||
if compgen -G "$rt/hypr/*/" >/dev/null 2>&1; then
|
||||
ok "Hyprland runtime directory found under $rt/hypr/"
|
||||
else
|
||||
warn "Hyprland does not appear to be running. Install will proceed; hooks will only work once you log into Hyprland on the host."
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -67,6 +67,40 @@ verify_install() {
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ "${STREAM_MODE:-}" == "headless" ]]; then
|
||||
local do_script="$HOME/.local/share/omarchy-moonlight/bin/sunshine-stream-do.sh"
|
||||
local undo_script="$HOME/.local/share/omarchy-moonlight/bin/sunshine-stream-undo.sh"
|
||||
local conf="$HOME/.config/sunshine/sunshine.conf"
|
||||
|
||||
if [[ -x "$do_script" ]]; then
|
||||
ok "headless do-hook present and executable"
|
||||
else
|
||||
err "headless do-hook missing or not executable: $do_script"
|
||||
VERIFY_FAILURES=$((VERIFY_FAILURES + 1))
|
||||
fi
|
||||
|
||||
if [[ -x "$undo_script" ]]; then
|
||||
ok "headless undo-hook present and executable"
|
||||
else
|
||||
err "headless undo-hook missing or not executable: $undo_script"
|
||||
VERIFY_FAILURES=$((VERIFY_FAILURES + 1))
|
||||
fi
|
||||
|
||||
if [[ -f "$conf" ]] && grep -q '^capture = wlr' "$conf"; then
|
||||
ok "sunshine.conf has capture = wlr"
|
||||
else
|
||||
err "sunshine.conf missing 'capture = wlr'"
|
||||
VERIFY_FAILURES=$((VERIFY_FAILURES + 1))
|
||||
fi
|
||||
|
||||
if [[ -f "$conf" ]] && grep -q '^global_prep_cmd' "$conf"; then
|
||||
ok "sunshine.conf has global_prep_cmd"
|
||||
else
|
||||
err "sunshine.conf missing 'global_prep_cmd'"
|
||||
VERIFY_FAILURES=$((VERIFY_FAILURES + 1))
|
||||
fi
|
||||
fi
|
||||
|
||||
if systemctl --user is-active --quiet sunshine.service; then
|
||||
ok "sunshine.service is active"
|
||||
if ss -ltn 2>/dev/null | grep -q ':47990 '; then
|
||||
|
||||
Reference in New Issue
Block a user