HydraIssues

Run hydraheadflatscreen heads on omarchy (Arch + Hyprland)
closed feature Project: hydraheadflatscreen Reporter: claude 17 Aug 2026 12:01

Description

Goal

Run hydraheadflatscreen heads on omarchy (https://github.com/basecamp/omarchy). Omarchy is an Arch-based distribution. It runs Hyprland on Wayland, with a Quickshell shell, SDDM plus uwsm for login, and pacman for packages. This issue records the full investigation and the work plan.

What already works

  • CI cross-compiles linux/amd64 on every tag and publishes hydrahead-linux-amd64 to releases.experiencenet.com (.github/workflows/release.yml:46-51). The self-updater resolves that exact name at runtime and can download, verify, and install on Linux today. The release server needs no changes.
  • The agent core is portable: config fetch, heartbeat, local API on :9740, the pure-Go GameStream pairing protocol, and the stream argument set in pkg/client/moonlight.go.
  • pkg/client/moonlight_linux.go launches moonlight-qt from PATH. Omarchy preinstalls moonlight-qt and ships a window rule (default/hypr/apps/moonlight.lua) that forces the app-id com.moonlight_stream.Moonlight fullscreen with idle-inhibit. Our Qt fork keeps that app-id on Linux (app/main.cpp:880-882), so the rule matches the fork as well.
  • The Qt fork (hydra-experiencenet) builds on Linux. The qmake unix branch is complete and names the binary hydra-experiencenet (app/app.pro:4-5). Upstream moonlight-qt supports Wayland as a first-class target (VAAPI with libva-wayland, Vulkan via libplacebo, a Wayland vsync source). The custom kiosk code (kiosk CLI, KioskView.qml, LocalServer on :9741) is cross-platform Qt. The only macOS-specific custom file, platform/macos_permissions.mm, is correctly guarded, so a Linux build compiles today.
  • Omarchy is kiosk-friendly: updates are manual only, unattended installs work from a cidata drive (with authorized_keys or a Tailscale key), grim gives scriptable screenshots with no portal or TCC ceremony, and systemd user services are the native autostart pattern.

Decision needed first: stock moonlight-qt or our fork

The two options are incompatible and gate most other work:

  • Stock moonlight-qt (pacman, preinstalled on omarchy). Pairing fails. The agent's subprocess pairing runs moonlight-qt --pin <PIN> pair --headless, and --headless is a fork-only flag. Stock moonlight-qt calls handleUnknownOptions() and exits (commandlineparser.cpp:53-57). Without native Linux cert storage (#278) there is no pairing path at all. Kiosk mode and the exit overlay also do not exist. Stream-only heads, and only after #278.
  • Our fork (hydra-experiencenet binary). Headless pairing and kiosk mode work, but CI must build a Linux artifact (only DMGs today), and the agent hardcodes the name moonlight-qt in three places in moonlight_linux.go: exec.LookPath (launch), pkill -x (stop), and pgrep -x (running detection). Deploy the fork and all three silently break. Fix the name handling or install a moonlight-qt symlink.

Recommendation: use the fork. It keeps parity with the macOS heads (kiosk grid, headless pairing, exit overlay) and the omarchy window rule already matches its app-id.

Work items: hydraheadflatscreen (Go agent)

  1. systemd user unit install. internal/cli/install_linux.go:7-9 is a stub that returns "install is only supported on Windows" (the message is also stale, darwin is supported). Write a systemd --user unit named exactly hydraheadflatscreen (the name service_name_unix.go and the updater expect), ExecStart=<exe> run --config ~/.hydraheadflatscreen/config.yaml, Restart=always, WantedBy=graphical-session.target. Do not use linger: linger conflicts with a graphical-session binding, and the agent is useless without the Hyprland session anyway. SDDM autologin provides the session at boot. Mirror in uninstall_linux.go.
  2. Fix restart after self-update. hydrarelease pkg/updater/restart_unix.go:11 runs system-scope systemctl restart hydraheadflatscreen. That fails for a user unit, so every auto-update would leave the old binary running. Add a systemctl --user variant (or detection) in hydrarelease, release it, and bump the dependency.
  3. Moonlight binary name. Make the launch/stop/detect name configurable or symlink moonlight-qt to the fork binary (see decision above).
  4. Native cert storage (moonlight_cert_linux.go). Read and write moonlight-qt's QSettings ini (~/.config/Moonlight Game Streaming Project/Moonlight.conf) so native pairing works without the subprocess fallback. This closes #278 for Linux.
  5. Screenshot producer (EYES). GET /api/v1/screenshot serves /tmp/hydra-live-screenshot.jpg (localapi.go:412-445). On macOS a Terminal screencapture loop under TCC produces it. On omarchy a grim -t jpeg loop in the user session does the same with no permission ceremony. Note: grim bakes software-composited cursors into frames, and capture fails while the shell lock screen is up.
  6. Idle and screensaver. screensaver_linux.go is an empty no-op. Important: omarchy v4 removed hypridle/hyprlock. Its Quickshell idle service honors ONLY the Wayland idle-inhibit protocol and omarchy's own flag files. systemd-inhibit and the org.freedesktop.ScreenSaver D-Bus path do nothing. During streams the shipped window rule (idle_inhibit = "fullscreen") covers us. Between streams, set the timeouts in ~/.config/omarchy/shell.json or set the flag files (omarchy toggle idle stay-awake, omarchy toggle screensaver) at provision time.
  7. WireGuard tunnel. tunnel_linux.go ensureWireGuardTunnel() is empty. Implement a check plus bring-up (hydraguard binary or wg-quick@hydraguard-air), and extend diagnostics.go:96 beyond darwin. Open question: hydranode and hydraguard are unverified on Arch, and the whole enrollment recipe (macOS TCC steps) needs a Linux equivalent. The Linux recipe is simpler: no TCC means no physical-access step.
  8. Autologin check. autologin_linux.go hardcodes true. Omarchy uses SDDM. Check /etc/sddm.conf.d/autologin.conf. Note: unencrypted omarchy installs get NO autologin by default (encrypted installs get it, the LUKS prompt is the auth boundary). Kiosk provisioning must write the file itself.
  9. Mic relay (optional). micrelay_linux.go reports unsupported, streams work without it. If voice experiences are wanted, mirror the Windows shape: ffmpeg with a PipeWire/Pulse source, Opus RTP to bodyIP:47995.
  10. Stream process tracking (optional). Track the subprocess handle like darwin does instead of pkill/pgrep -x, so a manually started client is not mistaken for the agent's stream.

Work items: hydra-experiencenet (Qt fork)

  1. Linux CI build. CI builds and releases only DMGs. scripts/build-appimage.sh exists upstream but is not wired in. Add a Linux job (AppImage or Arch tarball) and publish it. Arch build deps: qt6-base, qt6-declarative, qt6-svg, qt6-wayland, sdl2-compat, sdl2_ttf, ffmpeg, libva, libvdpau, opus, openssl, libplacebo, libx11, libdrm. Build with qmake6 after git submodule update --init --recursive.
  2. Run under XWayland first. Known moonlight-qt Hyprland issues: overexposed image (upstream #1875, fixed by QT_QPA_PLATFORM=xcb), intermittent stream-window open failure (#1201), Wayland crashes with a Force-XWayland workaround (#1721). XWayland is present and configured on omarchy, so xcb is the safe default. Validate Wayland-native later.
  3. Exit overlay. StreamOverlay.qml:39 relies on Qt.WindowStaysOnTopHint, which Wayland ignores, and the follow-all-Spaces trick no-ops off macOS. Use a Hyprland window rule (float plus pin) or a layer-shell surface to keep the overlay above the stream.
  4. Window hide/show cycle. showFullScreen()+raise()+requestActivate() (localserver.cpp:140-161) depends on compositor activation policy. Verify the kiosk-hide, stream, kiosk-show cycle under Hyprland and add window rules as needed.
  5. Agent integration. qtapp_linux.go must install and launch the Linux binary (tarball or AppImage, not DMG), and startKiosk() must stop returning "kiosk mode not supported on Linux".

Omarchy provisioning notes

  • Disk encryption. LUKS is the install default and blocks unattended reboot. Kiosk installs must take the no-encryption path, then write /etc/sddm.conf.d/autologin.conf themselves.
  • Hyprland config is Lua. Omarchy v4 configures Hyprland through hl.*/o.* Lua in ~/.config/hypr/*.lua, not hyprland.conf. Any tooling that writes monitor rotation, window rules, or autostart must emit Lua, or use hyprctl eval / hyprctl keyword at runtime.
  • Kiosk lockdown. Omarchy is a full desktop. Turn the bar off (omarchy toggle bar), silence notifications, and override the Super-key bindings. Caution: "Remove > Preinstalls" also removes moonlight-qt.
  • Display scale. Defaults assume 2x retina-class panels. A 1080p kiosk panel needs scale 1. Portrait rotation is a shipped example: hl.monitor({ transform = 1 }).
  • Config persistence. Omarchy updates can restore omarchy-owned configs, and omarchy reinstall resets everything. The agent must re-assert its autostart, window rules, shell.json, and autologin state idempotently on boot.
  • Fleet install. Unattended cidata installs with authorized_keys (enables sshd plus a firewall hole) or a Tailscale key fit the fleet model. The ufw firewall default-denies inbound; the local API binds 127.0.0.1 only, so define the cluster access path explicitly. x86_64 only, Secure Boot and TPM off; no Apple Silicon, Intel Macs are supported.
  • Updates. Manual only, omarchy update (plain pacman -Syu is guarded). Stable channel lags Arch by a month, snapper snapshots give rollback. Verify it runs non-interactively before the agent drives it; otherwise updates stay an operator action.

Open questions

  • Audio path: SDL audio through PipeWire, default sink selection on headless boot, and volume provisioning are unverified.
  • Hardware: no candidate mini-PC is named. Verify the target iGPU sustains HEVC 1080p60 hardware decode at 150 Mbps, and check the decode fallback behavior under the xcb path.
  • hydranode/hydraguard on Arch: unbuilt and untested; this defines head enrollment.
  • Touch input: touchscreen rotation must follow the monitor transform for portrait heads; untested.
  • Arch coverage: CI builds linux/amd64 only; add arm64 only if ARM hardware enters scope (omarchy itself is x86_64 only).

Validation

Write an omarchy E2E testbook per the runbook-plus-testbook rule: unattended install, enroll, pair, stream, kiosk hide/show cycle, self-update with user-unit restart, reboot survival. Also fix the runbook: docs/runbooks/runbook.md:26 has a wrong manual-download URL (missing /production/vX.Y.Z/, and uname -m gives x86_64 while artifacts say amd64), and the Linux rows reference a systemd service that item 1 must first create.

Decision (2026-08-17): use the fork

We use the hydra-experiencenet fork on Linux heads, for headless autopairing and kiosk parity with macOS. Stock moonlight-qt is out.

Updater consequence: extend the existing QtAppUpdater (pkg/client/qtapp.go), do not add a new mechanism.

  • CI on hydra-experiencenet gains a Linux job (wire scripts/build-appimage.sh) and uploads HydraExperienceNet-v-linux-x86_64.AppImage into the same hydraexperiencenet/production/v/ directory as the DMG. One tag, one shared latest.json, both platforms move in lockstep.
  • qtapp_linux.go implements qtAppPlatformInstall: download, chmod +x, atomic rename to ~/.hydraheadflatscreen/bin/hydra-experiencenet. qtAppPlatformRestartKiosk: pkill -f "hydra-experiencenet kiosk".
  • moonlight_linux.go getMoonlightExe() returns that managed path. This routes both the stream launch and the pairing subprocess (pairing.go:92) to the fork, so --headless autopairing works as on macOS. Switch pkill/pgrep stop/detect off the hardcoded "moonlight-qt" name at the same time. No symlinks.
  • The agent binary keeps the hydrarelease updater (hydrahead project); only the systemctl --user restart fix (item 2) changes there.
  • Provisioning: add fuse2 to the package set for AppImage, or launch with --appimage-extract-and-run.

This supersedes work item 3 (binary name handling: solved via getMoonlightExe) and narrows item 4 (#278 native cert storage becomes optional hardening, not a blocker, since fork headless pairing covers enrollment).

Test hardware validated (2026-08-17)

Node node-5e0ea2c8 (cranky-toaster-86), a 2013 MacBook Air (i5-4250U, Haswell-ULT) running omarchy, is enrolled and validated end to end:

  • Exec channel works (hydranode runs as root from /usr/local/bin).
  • WireGuard: provisioned as air peer 10.10.100.20 via hydraguard air add on the hub plus a wg-quick@hydraguard-air systemd unit on the head. The Linux WG story is plain wg-quick; ensureWireGuardTunnel() can check that unit.
  • Pairing: stock moonlight paired remotely with moonlight --pin <PIN> pair <host> plus a POST of the PIN to Sunshine /api/pin. No fork needed for enrollment-time pairing when driven this way (a Qt window opens on the display during pairing, acceptable at enrollment).
  • Streaming: rupelmonde-castle-viewer streamed from cosmic-pretzel-98 (10.10.100.12) at 1080p with H.264 VAAPI hardware decode confirmed in the FFmpeg log. Verified visually with a grim screenshot fetched over exec, which also proves the EYES replacement.
  • hydrarelease v1.19.0 (SetUserService for systemctl --user restart) is released and hydraheadflatscreen main is bumped to it.

New constraints found:

  1. NO HEVC on this hardware: Haswell VAAPI decodes H.264 only (vainfo verified). Vulkan is also absent, so the libplacebo renderer is out; VAAPI-on-x11 (QT_QPA_PLATFORM=xcb) is the working path. The agent hardcodes --video-codec HEVC in pkg/client/moonlight.go; Linux heads need H.264 (per-head codec config, or capability detection).
  2. The Arch moonlight-qt package installs the binary as /usr/bin/moonlight, not moonlight-qt, so the agent's hardcoded name is wrong even for stock. The getMoonlightExe() managed-path fix covers this.
  3. Audio: PipeWire analog stereo sink exists; end-to-end audio not yet verified (needs ears on site).
  4. Provisioning additions validated: pacman moonlight-qt libva-utils libva-intel-driver fuse2 wireguard-tools.

Linux parity shipped (2026-08-17, hydraheadflatscreen v2.2.0)

Commit 8866468, tag v2.2.0. Native GameStream pairing now runs in-process on Linux: moonlight_cert_linux.go reads and writes moonlight-qt's QSettings ini, generates the client identity on fresh heads, and writes host certs UUID-first. Validated on cranky-toaster-86: fresh pair against cosmic-pretzel-98 completed in about one second with no display, and stock /usr/bin/moonlight streamed with the agent-written identity (grim screenshot proof). This resolves work items 1, 3, 4, 5, 6, 7 (monitor side), 8, 9, 10 and the #495 race class for the primary path (subprocess pairing remains the fallback).

Also shipped: systemd --user install/uninstall, AppImage installer for the managed fork, built-in grim EYES loop, omarchy idle flag files, SDDM autologin check, PipeWire mic relay, WireGuard heartbeat diag, and a cross-platform 'pair ' CLI command. Runbook has a new "Linux heads (omarchy)" section.

Two findings from validation that stay open:

  1. Codec selection: the agent hardcodes HEVC stream args; pre-Skylake heads (the 2013 MacBook Air test head) decode H.264 only. Needs per-head codec config or capability detection.
  2. The fork's Linux AppImage CI job is still pending (hydra-experiencenet builds DMG only); the agent's qtapp updater 404s gracefully until the artifact exists.

Remaining work: fork AppImage CI job, codec selection, provisioning recipe (hydracluster recipe for Arch/omarchy), E2E testbook.

Status 2026-08-18: port complete, head in production

  • Agent v2.2.3: --video-codec auto on Linux (Haswell streams H.264), per-head kiosk_disabled toggle (hydracluster v2.0.105, POST /api/v1/nodes/{id}/kiosk-mode), GET /api/v1/version (#498).
  • Fork: Linux AppImage CI live (appimage.yml, linuxdeploy), v6.1.37 backfilled, libva exclusion fix from #500 applied and republished. Three real portability bugs fixed along the way: SDL_syswm.h include order, kioskbridge.h moc, desktop Exec name.
  • cranky-toaster-86 moved to district bxl1 venue cloud-seven, kiosk_disabled set (machine doubles as a workstation).
  • E2E testbook added: docs/testbooks/omarchy-head-e2e.md. Runbooks updated in hydraheadflatscreen, hydra-experiencenet, and hydracluster.

Remaining, tracked separately: #500 final on-device verification, omarchy stay-awake flag behavior, HandleLidSwitch for laptop heads, #499 naming decision.