#!/usr/bin/env bash # omarchy-moonlight installer # Sets up Sunshine (host) + Moonlight (client) on Omarchy/Hyprland/Wayland. # Idempotent: re-run safely. Same script works on NVIDIA and AMD machines. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=lib/common.sh source "$SCRIPT_DIR/lib/common.sh" # shellcheck source=lib/distro.sh source "$SCRIPT_DIR/lib/distro.sh" # shellcheck source=lib/detect.sh source "$SCRIPT_DIR/lib/detect.sh" # shellcheck source=lib/preflight.sh source "$SCRIPT_DIR/lib/preflight.sh" # shellcheck source=lib/packages.sh source "$SCRIPT_DIR/lib/packages.sh" # shellcheck source=lib/permissions.sh source "$SCRIPT_DIR/lib/permissions.sh" # shellcheck source=lib/config.sh source "$SCRIPT_DIR/lib/config.sh" # shellcheck source=lib/firewall.sh source "$SCRIPT_DIR/lib/firewall.sh" # shellcheck source=lib/service.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" # shellcheck source=lib/certs.sh source "$SCRIPT_DIR/lib/certs.sh" usage() { cat <5 MiB. setup_install_log() { local log="$SCRIPT_DIR/install.log" if [[ -f "$log" ]] && [[ $(stat -c %s "$log" 2>/dev/null || echo 0) -gt 5242880 ]]; then mv -f "$log" "$log.1" fi { echo echo "=== $(date -Iseconds) host=$(hostname) user=$(id -un) ===" echo " args: ${ARGV_FOR_LOG:-(none)}" } >> "$log" # stdout is about to become a pipe, so [[ -t 1 ]] in lib/common.sh would # disable colors. FORCE_COLOR keeps the terminal output colored; sed strips # ANSI from the log copy. export FORCE_COLOR=1 exec > >(tee >(sed -u 's/\x1b\[[0-9;]*m//g' >> "$log")) 2>&1 trap 'rc=$?; printf "=== exit=%s @ %s ===\n" "$rc" "$(date -Iseconds)" >> "'"$log"'"' EXIT } main() { setup_install_log require_not_root require_supported_distro step "Detecting system" detect_all compute_stream_mode export STREAM_MODE info "Distro: $DISTRO ($DISTRO_ID ${DISTRO_VERSION:-}) Host: $HOSTNAME_SHORT" info "GPU: $GPU_VENDOR Session: $SESSION_TYPE Compositor: $COMPOSITOR" info "Mode: $STREAM_MODE" if [[ $DOCTOR_ONLY -eq 1 ]]; then verify_install exit $(( VERIFY_FAILURES > 0 ? 1 : 0 )) fi step "Preflight checks" preflight_all # On headless mode, make sure we have a compositor we can drive. On Arch # this is typically Hyprland (already on Omarchy); on Ubuntu we install Sway # and switch COMPOSITOR before installing hooks. if [[ "$STREAM_MODE" == "headless" && "$COMPOSITOR" == "none" ]]; then step "Installing headless compositor" install_headless_compositor COMPOSITOR="$(detect_compositor)" export COMPOSITOR info "Compositor (after install): $COMPOSITOR" fi if [[ $INSTALL_SUNSHINE -eq 1 ]]; then step "Installing Sunshine and GPU encoder support" install_sunshine install_gpu_encoder_packages step "Configuring permissions for KMS capture and virtual input" ensure_input_group ensure_uinput_udev_rule set_sunshine_capabilities if [[ "$STREAM_MODE" == "headless" ]]; then step "Installing headless prep-cmd hooks" install_headless_hooks fi # X11/NVENC headless backend (capture = x11): the headless Xorg on :0 has # no compositor of its own, so it needs a window manager rendering on it or # Sunshine captures a black screen. Detected from the existing sunshine.conf # (this backend is hand-configured; see FOLLOWUPS.md P3). Harmless no-op on # the wlr/kms backends, whose capture source renders for itself. if capture_backend_is_x11; then step "Installing headless desktop (Openbox on :0 for the X11 capture path)" install_headless_desktop fi # NOTE: the headless prestart drop-in needs the sunshine unit to already # exist; install it after service-unit detection in enable_sunshine_service. if [[ $WRITE_CONFIG -eq 1 ]]; then step "Writing tuned sunshine.conf" write_sunshine_config "$STREAM_MODE" else info "Skipping sunshine.conf (--no-config)" fi if [[ $INSTALL_CERTS -eq 1 ]]; then step "Installing CA-signed Sunshine cert from 1Password" if fetch_and_install_certs; then CERTS_REPLACED=1 else warn "Cert install failed — falling back to Sunshine's self-signed cert." warn "Run scripts/cert-bootstrap.sh to create the CA item, then re-run install.sh." fi else info "Skipping cert step (--no-certs)" fi if [[ $FIREWALL -eq 1 ]]; then step "Configuring firewall for Sunshine ports" open_sunshine_ports else info "Skipping firewall (--no-firewall)" fi if [[ $AUTOSTART -eq 1 ]]; then step "Enabling Sunshine user service" enable_sunshine_service else info "Skipping autostart (--no-autostart). Start manually with: systemctl --user start sunshine" fi else info "Skipping Sunshine install (--no-sunshine)" fi if [[ $INSTALL_MOONLIGHT -eq 1 ]]; then step "Installing Moonlight client" install_moonlight else info "Skipping Moonlight install (--no-moonlight)" fi if [[ $INSTALL_SUNSHINE -eq 1 ]]; then verify_install fi step "Done" print_next_steps } print_next_steps() { local ip ip="$(ip -4 -o addr show scope global | awk '{print $4}' | cut -d/ -f1 | head -n1)" cat <}${RESET} - Enter the 4-digit PIN that Sunshine's UI shows during pairing. 4. To check status: ${DIM}systemctl --user status sunshine${RESET} To view logs: ${DIM}journalctl --user -u sunshine -f${RESET} To uninstall: ${DIM}$SCRIPT_DIR/uninstall.sh${RESET} EOF } main "$@"