# A compositor that exists only so Sunshine has something to capture. # mainframe has no desktop: the only connected output (HDMI-A-2) hangs off the # Intel iGPU, while the RTX 3060 that does the encoding drives no monitor at # all. Capturing the real output would mean grabbing on Intel and encoding on # NVIDIA — a cross-GPU copy every frame. A virtual output on the 3060 keeps # capture and NVENC on one card. output HEADLESS-1 mode 1920x1080@60Hz position 0,0 output HEADLESS-1 background #101920 solid_color # Start Xwayland eagerly rather than on first X client. Steam is an X app, and # with the default lazy start DISPLAY does not exist yet when the line below # imports the environment — so Steam launched by Sunshine got # "Unable to open a connection to X" and showed a blank stream. xwayland force # Sunshine is a systemd user unit and inherits nothing from this session, so # hand it the display variables before starting it. # # ONE exec with &&, not two. sway runs exec lines asynchronously and does not # order them, so as two separate lines Sunshine won every time: it started in # the same second as the compositor, before DISPLAY was imported, and passed no # DISPLAY to the apps it launched. Steam then failed with "Unable to open a # connection to X" even though Xwayland was up and the manager environment was # correct. exec systemctl --user import-environment WAYLAND_DISPLAY DISPLAY SWAYSOCK XDG_CURRENT_DESKTOP && systemctl --user start sunshine.service # Input arrives from the Moonlight client, so keybinds are worth having even # though nobody is sitting at this machine. Without them the Desktop app # streams an empty screen with no way to open anything. # # Mod1 (Alt), not Mod4 (Super). The client machines run Hyprland with SUPER as # their own modifier, and a compositor grabs its own modifier before Moonlight # ever sees the key, so every Super bind here was unreachable from the stream — # it acted on the client instead. Alt is not grabbed there, so it arrives. The # cost is that an Alt bind here shadows the focused app's menu mnemonics # (Alt+F, Alt+D), which is acceptable on a session that shows one fullscreen # app at a time. set $mod Mod1 # Tiling, sway's default. This used to float every window, which left a # single-app stream at whatever size the app opened at instead of filling the # output. Each Sunshine app switches to its own workspace first (apps.json), so # its window is alone there and fills the screen. Dialogs sway recognizes as # dialogs still float. # Title bars are the handle you grab with the mouse, so floating windows keep # a real border rather than the thin default. default_floating_border normal 2 # Alt+drag moves a window, Alt+right-drag resizes it. Without this the # only way to move a window is the title bar, which a fullscreen app has none of. floating_modifier $mod normal # Hyprland takes $browser from hosts//hypr/local-env.conf; mainframe links # no hypr config, so it is named here. qutebrowser, to match the laptops. set $browser qutebrowser # These mirror common/hypr/hyprland.conf, so the muscle memory from the laptops # carries over the stream. Only binds whose target exists on this host are # ported: mainframe has no grim/grimblast, no udiskie, no # networkmanager_dmenu, no ydotool, no julia, no ncmpcpp and no swayimg, so # Hyprland's $mainMod+P, Shift+P, M, W, A, S and V are deliberately absent # rather than bound to a missing binary — sway reports nothing when an exec # target does not exist, so a ported bind would look correct and silently do # nothing. The brightness keys are out for the same reason twice over: # brightnessctl is absent and /sys/class/backlight is empty. # # $mainMod+N and Shift+N are out too, less obviously. The daemon here is mako, # not swaync, and mako's do-not-disturb is not built in — it needs a # [mode=do-not-disturb] invisible=1 section, and this host has no mako config at # all. `makoctl mode -t do-not-disturb` would therefore succeed and suppress # nothing. Write that config first if these binds are wanted. # # $mainMod+Ctrl+L is not ported: there is no lock screen here on purpose, and # locking the session Sunshine captures would blank the stream. bindsym $mod+Return exec kitty bindsym $mod+Shift+Return exec wofi --show drun bindsym $mod+b exec $browser bindsym $mod+q kill bindsym $mod+Shift+q exit bindsym $mod+f fullscreen bindsym $mod+t floating toggle # Hyprland moves focus with both vim keys and arrows; so does this. bindsym $mod+h focus left bindsym $mod+j focus down bindsym $mod+k focus up bindsym $mod+l focus right bindsym $mod+Left focus left bindsym $mod+Down focus down bindsym $mod+Up focus up bindsym $mod+Right focus right # Not a Hyprland bind, kept because a Moonlight client arrives with a pointer # and little else: this is the only way to reach a window the pointer cannot # see. Spelled literally rather than as $mod so it stays Alt+Tab if $mod moves. bindsym Alt+Tab focus next bindsym Alt+Shift+Tab focus prev # Workspaces, and $mod+scroll to cycle them, as on the laptops. Sway has no # master layout, so Hyprland's $mainMod+Space, Y, U, O and I have no # counterpart; nor do its # movecurrentworkspacetomonitor binds, with one virtual output. bindsym $mod+1 workspace number 1 bindsym $mod+2 workspace number 2 bindsym $mod+3 workspace number 3 bindsym $mod+4 workspace number 4 bindsym $mod+5 workspace number 5 bindsym $mod+6 workspace number 6 bindsym $mod+7 workspace number 7 bindsym $mod+8 workspace number 8 bindsym $mod+9 workspace number 9 bindsym $mod+0 workspace number 10 bindsym $mod+Shift+1 move container to workspace number 1 bindsym $mod+Shift+2 move container to workspace number 2 bindsym $mod+Shift+3 move container to workspace number 3 bindsym $mod+Shift+4 move container to workspace number 4 bindsym $mod+Shift+5 move container to workspace number 5 bindsym $mod+Shift+6 move container to workspace number 6 bindsym $mod+Shift+7 move container to workspace number 7 bindsym $mod+Shift+8 move container to workspace number 8 bindsym $mod+Shift+9 move container to workspace number 9 bindsym $mod+Shift+0 move container to workspace number 10 bindsym --whole-window $mod+button4 workspace prev_on_output bindsym --whole-window $mod+button5 workspace next_on_output # Volume reaches the client: the default sink is sink-sunshine-stereo, which is # the one Sunshine captures. --locked because there is no lock screen to respect. # XF86AudioMicMute is not ported — this host has no audio source at all. bindsym --locked XF86AudioRaiseVolume exec wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 5%+ bindsym --locked XF86AudioLowerVolume exec wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%- bindsym --locked XF86AudioMute exec wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle bindsym --locked XF86AudioNext exec playerctl next bindsym --locked XF86AudioPause exec playerctl play-pause bindsym --locked XF86AudioPlay exec playerctl play-pause bindsym --locked XF86AudioPrev exec playerctl previous # Something visible on login, so a working capture is obviously working rather # than indistinguishable from a broken one. exec kitty # No bar, no idle daemon, no lock screen — there is no one sitting here.