diff --git a/docs/reference.md b/docs/reference.md index 2e49ae0..49b3866 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -194,7 +194,7 @@ remote-displays/install.sh # its menu entry On the PC, `host/windows/Setup Frametop host.cmd` sets it up for Frametop: Vibepollo 2.0.0 (installed if missing), Frametop's build of its `sunshine.exe`, the settings Frametop needs, the Web UI login you sign in with from the Frame, and a firewall check (`-Check` to see what it would change, `-Undo` to put things back). -On a Mac, `host/macos/frametop-host-setup.sh` (or `Setup Frametop host.command`) installs Sunshine and runs one instance per real display, each on its own port (47989 + 100·N), started at login. You allow Sunshine Screen Recording and Accessibility once in System Settings. Without Accessibility the Frame's pointer moves on the Mac but clicks, scrolling and typing don't arrive; the setup checks both, and offers to ask again if Accessibility is missing. Remote Displays finds the Mac as one computer; signing in with the Web UI login pairs the displays you pick, and the password isn't kept, so adding more later signs in again. Macs have no virtual displays (`--check`, `--undo` as on the PC). +On a Mac, `host/macos/frametop-host-setup.sh` (or `Setup Frametop host.command`) installs Sunshine and runs one instance per real display, each on its own port (47989 + 100·N), started at login. You allow Sunshine Screen Recording and Accessibility once in System Settings. Without Accessibility the Frame's pointer moves on the Mac but clicks, scrolling and typing don't arrive; the setup checks both, and offers to ask again if Accessibility is missing. Remote Displays finds the Mac as one computer; signing in with the Web UI login pairs the displays you pick, and the password isn't kept, so adding more later signs in again. Macs have no virtual displays (`--check`, `--undo` as on the PC). Frametop Streams in the menu bar turns each display on or off with a click, or all of them, and `ft-sunshine off|on|status` does the same in Terminal. Off lasts through logins until on. [host/macos/README.md](../host/macos/README.md) has the steps for setting up a Mac. ## Floating windows diff --git a/docs/remote-displays.md b/docs/remote-displays.md index 26fb867..6206358 100644 --- a/docs/remote-displays.md +++ b/docs/remote-displays.md @@ -462,6 +462,7 @@ The user decided remote displays are Frametop displays, with the same controls a **The Mac: one Sunshine instance per display (2026-10-10, branch `mac-host`):** - `host/macos/frametop-host-setup.sh` (or `Setup Frametop host.command`): installs the pinned Sunshine pre-release (SHA-256 checked, notarized by LizardByte), lists the displays (`displays.js`, JavaScript for Automation, no compiler), and writes an instance per display in `~/Library/Application Support/Frametop/sunshine/instance-N`, the main display's first, each started at login by the LaunchAgent `frametop.sunshine.N`. Its settings: `port`, `sunshine_name` "COMPUTER: DISPLAY (WxH)", its own state and apps files, `minimum_fps_target = 1`, and the menu bar icon on the first only. The rest of each `sunshine.conf` (Web UI changes) is kept on a rerun. `--check`, `--undo` (removes the LaunchAgents; settings, pairings and Sunshine stay), `--set-login`, `--skip-login`. +- Turning them off and on (2026-10-12): Frametop Streams, a menu bar icon, lists the displays with a check by each one that's on. A click turns one on or off, Turn All On and Turn All Off do every one, and the icon dims while they're all off. It's `Frametop Streams.applescript`, a stay-open AppleScriptObjC app (`NSStatusItem`, so no compiler) that the setup compiles into `~/Applications`, marks `LSUIElement` (no Dock icon), signs again ad hoc after that change, and starts at login with the LaunchAgent `frametop.streams`. Quit from its menu, it stays quit until the next login. It runs `ft-sunshine off|on|status [--tsv] [N...]`, in the background for a change, with the output in `~/Library/Logs/Frametop/streams.log`. `ft-sunshine` is installed next to `ft-sunshine-instance` and linked into `~/.local/bin` when there is one. Off disables each instance's LaunchAgent (`launchctl disable`) and stops it, so it stays off through logins. On enables it and starts it, or starts one quit from Sunshine's own menu bar icon, which launchd leaves stopped after a clean exit. Rerunning the setup turns them all on. The Frame's streams of the Mac keep trying while it's off and reconnect after on. Tested with throwaway LaunchAgents serving fake instances, through the menu (UI scripting) and `ft-sunshine`: on, on again (the running one kept its pid), off, on after off, one display, a stopped one, unknown numbers, a display that isn't connected, and the app started by its LaunchAgent and quit from its menu. On the M3 Max: the setup from the kit zip, unpacked with a download's quarantine (the installed scripts keep it, and run anyway: launchd and the app run them through bash and sh), which turned the three on; then Turn All Off, Turn All On and Turn All Off again from the menu, each done within 3 s. - An instance is tied to its monitor by vendor, model and serial, not to a display id: `ft-sunshine-instance` looks the id up at every start and passes `output_name=ID`. While the monitor is unplugged it doesn't start Sunshine, which would fall back to the main display, and launchd tries again. - One certificate for all the instances, so Remote Displays pins one key per Mac; one Web UI login file for all, kept from an earlier Sunshine install if there was one. An earlier Homebrew Sunshine's LaunchAgent (the same ports) is turned off, and back on with `--undo`. - Screen Recording and Accessibility are granted by hand, once for all the instances: macOS lists them as Sunshine (the LaunchAgent runs a bash script that execs it; `AssociatedBundleIdentifiers` makes Login Items say Sunshine too). Sunshine started from a terminal gets that terminal's grants instead. diff --git a/host/macos/Frametop Streams.applescript b/host/macos/Frametop Streams.applescript new file mode 100644 index 0000000..448c8fd --- /dev/null +++ b/host/macos/Frametop Streams.applescript @@ -0,0 +1,139 @@ +-- Frametop Streams: a menu bar icon for this Mac's Frametop displays (its Sunshine instances, one +-- per display). Click a display to turn it on or off, or turn them all on or off. It runs +-- ft-sunshine. frametop-host-setup.sh compiles it into ~/Applications as a stay-open app with no +-- Dock icon and starts it at login (the LaunchAgent frametop.streams). +use AppleScript version "2.4" +use framework "Foundation" +use framework "AppKit" +use scripting additions + +property statusItem : missing value +property theMenu : missing value +property theTimer : missing value +property tool : "" +property logFile : "" +property busyPid : 0 -- the ft-sunshine started from the menu, while it runs +property busyText : "" +property ticks : 0 + +on run + set tool to quoted form of ((POSIX path of (path to application support folder from user domain)) & "Frametop/sunshine/bin/ft-sunshine") + set logFile to (POSIX path of (path to library folder from user domain)) & "Logs/Frametop/streams.log" + set statusItem to current application's NSStatusBar's systemStatusBar()'s statusItemWithLength:(current application's NSVariableStatusItemLength) + set icon to current application's NSImage's imageWithSystemSymbolName:"display.2" accessibilityDescription:"Frametop Streams" + icon's setTemplate:true + statusItem's button()'s setImage:icon + statusItem's button()'s setToolTip:"Frametop Streams" + set theMenu to current application's NSMenu's alloc()'s initWithTitle:"Frametop Streams" + theMenu's setAutoenablesItems:false + theMenu's setDelegate:me + statusItem's setMenu:theMenu + refresh() + set theTimer to current application's NSTimer's scheduledTimerWithTimeInterval:2 target:me selector:"tick:" userInfo:(missing value) repeats:true +end run + +-- Every 2 s while a change runs, otherwise every 10 s: the icon dims while every display is +-- off, whatever turned them off. +on tick:aTimer + set ticks to ticks + 1 + if busyPid is not 0 or ticks mod 5 = 0 then refresh() +end tick: + +on menuNeedsUpdate:aMenu + refresh() +end menuNeedsUpdate: + +on refresh() + if busyPid is not 0 then + try + do shell script "kill -0 " & busyPid + on error + set busyPid to 0 + end try + end if + theMenu's removeAllItems() + try + set rows to paragraphs of (do shell script tool & " status --tsv") + on error msg + addItem(msg, missing value, missing value, false) + addItem("Run frametop-host-setup.sh to set this Mac up.", missing value, missing value, false) + theMenu's addItem:(current application's NSMenuItem's separatorItem()) + addItem("Quit Frametop Streams", "quitApp:", missing value, true) + statusItem's button()'s setAppearsDisabled:true + return + end try + set ready to busyPid is 0 + set anyOn to false + set saved to AppleScript's text item delimiters + set AppleScript's text item delimiters to tab + repeat with r in rows + set f to text items of r + set n to (item 1 of f) as integer + set instState to item 2 of f + set label to item 3 of f + if instState is "waiting" then + set label to label & " (waiting for it to be connected)" + else if instState is "starting" then + set label to label & " (starting)" + else if instState is "stopped" then + set label to label & " (stopped)" + end if + set mi to addItem(label, "toggle:", n, ready) + -- A check while it's on; a dash while it's on but not streaming yet. + if instState is "on" then + mi's setState:(current application's NSControlStateValueOn) + set anyOn to true + else if instState is "waiting" or instState is "starting" then + mi's setState:(current application's NSControlStateValueMixed) + set anyOn to true + end if + end repeat + set AppleScript's text item delimiters to saved + theMenu's addItem:(current application's NSMenuItem's separatorItem()) + if not ready then addItem(busyText, missing value, missing value, false) + addItem("Turn All On", "allOn:", missing value, ready) + addItem("Turn All Off", "allOff:", missing value, ready) + theMenu's addItem:(current application's NSMenuItem's separatorItem()) + addItem("Quit Frametop Streams", "quitApp:", missing value, true) + statusItem's button()'s setAppearsDisabled:(not anyOn) +end refresh + +on addItem(itemTitle, itemAction, itemTag, itemEnabled) + set mi to current application's NSMenuItem's alloc()'s initWithTitle:itemTitle action:itemAction keyEquivalent:"" + if itemAction is not missing value then mi's setTarget:me + if itemTag is not missing value then mi's setTag:itemTag + mi's setEnabled:itemEnabled + theMenu's addItem:mi + return mi +end addItem + +-- A display that's off or stopped turns on; one that's on, waiting or starting turns off. +on toggle:sender + set n to (sender's tag()) as integer + if ((sender's state()) as integer) = 0 then + change("on " & n, "Turning a display on…") + else + change("off " & n, "Turning a display off…") + end if +end toggle: + +on allOn:sender + change("on", "Turning every display on…") +end allOn: + +on allOff:sender + change("off", "Turning every display off…") +end allOff: + +-- In the background: on waits up to 20 s for the instances to answer. Its output goes to the log. +on change(args, what) + set busyText to what + set logDir to do shell script "dirname " & quoted form of logFile + set busyPid to (do shell script "mkdir -p " & quoted form of logDir & "; { date; " & tool & " " & args & "; } >> " & quoted form of logFile & " 2>&1 & echo $!") as integer + refresh() +end change + +on quitApp:sender + theTimer's invalidate() + tell me to quit +end quitApp: diff --git a/host/macos/README.md b/host/macos/README.md new file mode 100644 index 0000000..01dff9e --- /dev/null +++ b/host/macos/README.md @@ -0,0 +1,64 @@ +# Frametop host for macOS + +This kit makes a Mac a host for Frametop's remote displays: the Mac's monitors appear as screens on a Steam Frame. Vibepollo has no macOS build, so the Mac uses LizardByte's Sunshine, one copy (an instance) per display. Frametop Streams, a menu bar icon, turns them on and off. + +## Requirements + +- A Mac with Apple silicon, on macOS 14.2 or later. +- An admin account, to install Sunshine in /Applications. +- The Mac and the Steam Frame on the same network. +- Frametop on the Steam Frame. + +## Set up + +1. Download `frametop-macos-host-1.zip` and open it. You get a folder named `frametop-macos-host`. +2. Open Terminal and run the setup: + + ``` + bash ~/Downloads/frametop-macos-host/frametop-host-setup.sh + ``` + + You can also double-click `Setup Frametop host.command`. The kit isn't signed, so macOS blocks it the first time. To allow it, open System Settings > Privacy & Security, click Open Anyway next to the message about it, then open it again. +3. The setup does the following: + - It installs Sunshine 2026.1010.141104 in /Applications after checking the download's SHA-256. This is a pre-release, and it's the first build where the mouse reaches displays other than the main one. + - It creates one Sunshine instance per display, on ports 47989, 48089, 48189 and so on, each started at login. + - It asks for the Web UI user name and password that the Steam Frame signs in with. The password is typed in Terminal and is never shown or saved. If an earlier run of this setup or an earlier Sunshine install already has a login, the setup uses that one instead. + - It adds Frametop Streams to the menu bar. +4. When macOS asks, allow Sunshine both Screen Recording and Accessibility in System Settings > Privacy & Security. Then go back to Terminal and press Enter. Without Accessibility, the pointer moves but clicks, scrolling and typing don't reach the Mac. The setup checks both permissions and offers to ask again if Accessibility is missing. +5. On the Steam Frame, open Frametop Remote Displays and choose Add computer. Pick this Mac, sign in with the Web UI login, and choose the displays to show. + +Run the setup again after you connect a new monitor. + +## Turn the streams on and off + +Click the display icon in the menu bar to open Frametop Streams. It lists the Mac's displays and puts a check next to each one that's on. Click a display to turn it on or off, or choose Turn All On or Turn All Off. The icon is dimmed while every display is off. A display you turn off stays off after a restart, until you turn it on again. + +You can do the same in Terminal with `ft-sunshine`: + +``` +ft-sunshine status # each display and whether it's on +ft-sunshine off # turn every display off +ft-sunshine on # turn every display on +ft-sunshine on 1 # turn on one display, by the number status shows +``` + +If you have a `~/.local/bin` folder, the setup puts `ft-sunshine` there. Otherwise, run it as `~/Library/Application\ Support/Frametop/sunshine/bin/ft-sunshine`. + +While a display is off, the Steam Frame keeps trying to reach it, and it reconnects once the display is on again. + +If you quit Frametop Streams, it starts again at your next login. You can also open it from Spotlight. + +## Good to know + +- The Steam Frame can't reach a Mac that's asleep. To keep the Mac awake on power, turn on System Settings > Battery > Options > Prevent automatic sleeping on power adapter when the display is off. The setup tells you if the Mac sleeps when idle. +- Sunshine's own menu bar icon belongs to the main display's instance. Quitting Sunshine from that icon stops only the main display. Frametop Streams or `ft-sunshine on` starts it again. +- Logs are in `~/Library/Logs/Frametop`. + +## Check and remove + +- `bash frametop-host-setup.sh --check` shows what the setup would change, without changing anything. +- `bash frametop-host-setup.sh --undo` removes the instances' LaunchAgents, Frametop Streams and `ft-sunshine`. Sunshine and its settings and pairings stay, and the setup says how to remove those too. + +## Source + +This kit is `host/macos` in [Frametop](https://github.com/Frametop/frametop), under the MIT license. Sunshine is [LizardByte's](https://github.com/LizardByte/Sunshine), under GPL-3.0. The setup downloads it from Sunshine's GitHub release. diff --git a/host/macos/frametop-host-setup.sh b/host/macos/frametop-host-setup.sh index ce0a672..40a023c 100755 --- a/host/macos/frametop-host-setup.sh +++ b/host/macos/frametop-host-setup.sh @@ -15,6 +15,9 @@ # LaunchAgent frametop.sunshine.N and started again if it stops. An instance is tied to its # monitor, not to a port: plug the monitor in elsewhere and it keeps its instance, and while # it's unplugged its instance waits for it. Run this again after connecting a new monitor. +# Frametop Streams, a menu bar icon started at login, turns each one on or off with a +# click, or all of them; ft-sunshine off|on|status does the same in Terminal. Off lasts +# through logins until on, or until this runs again. # 3. The Web UI login you sign in with once from the Steam Frame, the same for every instance: # keep the one there is, take the one an earlier Sunshine install had, or set one. The # password is typed here and never shown or saved. @@ -27,7 +30,8 @@ # --set-login set a new Web UI login even if there is one # --skip-login leave the Web UI login alone # --check only say what would change -# --undo remove the instances' LaunchAgents (settings, pairings and Sunshine stay) +# --undo remove the instances' LaunchAgents, Frametop Streams and ft-sunshine +# (settings, pairings and Sunshine stay) # A log goes to ~/Library/Logs/Frametop/host-setup.log. set -euo pipefail @@ -45,6 +49,9 @@ BASE_PORT=47989 GUI="gui/$(id -u)" OLD_STATE="$HOME/.config/sunshine/sunshine_state.json" # a stock Sunshine install's login BREW_LABEL=homebrew.mxcl.sunshine +STREAMS_APP="$HOME/Applications/Frametop Streams.app" +STREAMS_LABEL=frametop.streams # its LaunchAgent and bundle id +LINK="$HOME/.local/bin/ft-sunshine" # only when there's a ~/.local/bin HERE=$(cd "$(dirname "$0")" && pwd) check=0 undo=0 login=auto @@ -69,6 +76,8 @@ our_agents() { ls "$AGENTS" 2>/dev/null | sed -n "s/^\($LABEL\.[0-9][0-9]*\)\.pl # bootout returns before the job is gone, and bootstrap fails until it is. start_agent() { local i + # ft-sunshine off disables the agents, and bootstrap fails while they are. + launchctl enable "$GUI/$1" for i in 1 2 3 4 5; do launchctl bootstrap "$GUI" "$AGENTS/$1.plist" 2>/dev/null && return 0 agent_loaded "$1" && return 0 @@ -84,6 +93,50 @@ stop_agents() { done } +# Frametop Streams: a menu bar icon that turns each instance on or off through ft-sunshine, +# started at login. It's a stay-open AppleScript app, so nothing needs compiling. LSUIElement +# keeps it out of the Dock, and changing Info.plist means signing it again (ad hoc, as osacompile +# does). +install_streams_app() { + local where="$DATA/bin/ft-sunshine" info="$STREAMS_APP/Contents/Info.plist" + if [ -d "$HOME/.local/bin" ] && { [ ! -e "$LINK" ] || [ -L "$LINK" ]; }; then + ln -sf "$DATA/bin/ft-sunshine" "$LINK" + where=ft-sunshine + fi + launchctl bootout "$GUI/$STREAMS_LABEL" 2>/dev/null || true + pkill -f "$STREAMS_APP/Contents/MacOS/applet" 2>/dev/null || true + mkdir -p "$(dirname "$STREAMS_APP")" + rm -rf "$STREAMS_APP" + if ! osacompile -s -o "$STREAMS_APP" "$HERE/Frametop Streams.applescript" 2>/dev/null || + ! plutil -replace LSUIElement -bool true "$info" || + ! plutil -replace CFBundleIdentifier -string "$STREAMS_LABEL" "$info" || + ! codesign -f -s - "$STREAMS_APP" 2>/dev/null; then + say "Couldn't make $STREAMS_APP. To turn the instances off or on: $where off|on." + return 0 + fi + cat > "$AGENTS/$STREAMS_LABEL.plist" < + + + + Label$STREAMS_LABEL + ProgramArguments + + $STREAMS_APP/Contents/MacOS/applet + + AssociatedBundleIdentifiers$STREAMS_LABEL + RunAtLoad + + KeepAliveSuccessfulExit + ProcessTypeInteractive + LimitLoadToSessionTypeAqua + + +EOF + start_agent "$STREAMS_LABEL" || true + say "Frametop Streams is in the menu bar: click a display there to turn it on or off. In Terminal: $where off|on|status." +} + # Answers when the instance on this port is up (its HTTP serverinfo, port N). answers() { curl -s -m 2 -o /dev/null "http://127.0.0.1:$1/serverinfo"; } wait_answers() { @@ -204,6 +257,18 @@ undo() { rm -f "$AGENTS/$label.plist" say "Removed the LaunchAgent $label." done + if [ -e "$AGENTS/$STREAMS_LABEL.plist" ] || [ -e "$STREAMS_APP" ] || [ -L "$LINK" ]; then + if [ $check = 1 ]; then + say "Would remove Frametop Streams and ft-sunshine." + else + launchctl bootout "$GUI/$STREAMS_LABEL" 2>/dev/null || true + pkill -f "$STREAMS_APP/Contents/MacOS/applet" 2>/dev/null || true + rm -f "$AGENTS/$STREAMS_LABEL.plist" + rm -rf "$STREAMS_APP" + [ "$(readlink "$LINK" 2>/dev/null)" = "$DATA/bin/ft-sunshine" ] && rm -f "$LINK" + say "Removed Frametop Streams and ft-sunshine." + fi + fi if [ -e "$DATA/.disabled-$BREW_LABEL" ]; then if [ $check = 1 ]; then say "Would turn Homebrew's Sunshine LaunchAgent back on." @@ -272,11 +337,13 @@ $n" [ -n "$n" ] || continue say "Would $([ -e "$DATA/instance-$n/display" ] && echo keep || echo add) instance $n: $name ($w x $h), port $((BASE_PORT + 100 * n))." done <<< "$lines" + say "Would install Frametop Streams ($STREAMS_APP), a menu bar icon that turns each instance on or off, and ft-sunshine, the same in Terminal." else stop_agents mkdir -p "$DATA/bin" "$DATA/credentials" "$AGENTS" - cp "$HERE/ft-sunshine-instance" "$HERE/displays.js" "$DATA/bin/" - chmod 755 "$DATA/bin/ft-sunshine-instance" + cp "$HERE/ft-sunshine-instance" "$HERE/ft-sunshine" "$HERE/displays.js" "$DATA/bin/" + chmod 755 "$DATA/bin/ft-sunshine-instance" "$DATA/bin/ft-sunshine" + install_streams_app # One certificate for every instance, so Frametop pins one key for the Mac. if [ ! -s "$DATA/credentials/cacert.pem" ] || [ ! -s "$DATA/credentials/cakey.pem" ]; then (umask 077 && openssl req -x509 -newkey rsa:2048 -nodes -days 7300 -subj "/CN=Sunshine Gamestream Host" \ diff --git a/host/macos/ft-sunshine b/host/macos/ft-sunshine new file mode 100755 index 0000000..a3f7175 --- /dev/null +++ b/host/macos/ft-sunshine @@ -0,0 +1,155 @@ +#!/bin/bash +# Turns this Mac's Frametop displays (its Sunshine instances, one per display) on or off. The +# setup installs it, and the Frametop Streams menu bar app runs it. +# Usage: ft-sunshine on|off|status [--tsv] [N...] +# +# on start the instances, and start them at login again +# off stop them, and leave them stopped at login until "on" +# status each instance's display, port and state (the default). With --tsv, a line per +# instance: N, state (on, off, waiting, starting, stopped), display, port, tab-separated +# +# Without N it's every instance. Settings and pairings stay as they are. The Frame's streams of +# the Mac keep trying while it's off and come back by themselves after "on". +set -u + +DATA="$HOME/Library/Application Support/Frametop/sunshine" +AGENTS="$HOME/Library/LaunchAgents" +LABEL=frametop.sunshine # instance N's agent is frametop.sunshine.N +BASE_PORT=47989 +GUI="gui/$(id -u)" + +usage() { sed -n '2,/^set -u/p' "$0" | sed '$d; s/^# \{0,1\}//'; } + +cmd=${1:-status} +[ $# -gt 0 ] && shift +case $cmd in + on|off|status) ;; + -h|--help|help) usage; exit 0 ;; + *) usage >&2; exit 2 ;; +esac +tsv=0 +nums=() +for arg in "$@"; do + case $arg in + --tsv) tsv=1 ;; + *) nums+=("$arg") ;; + esac +done +set -- ${nums[@]+"${nums[@]}"} + +loaded() { launchctl print "$GUI/$LABEL.$1" >/dev/null 2>&1; } +answers() { curl -s -m 2 -o /dev/null "http://127.0.0.1:$((BASE_PORT + 100 * $1))/serverinfo"; } + +state() { + if ! loaded "$1"; then + echo off + elif answers "$1"; then + echo on + elif [ -e "$DATA/instance-$1/.waiting" ]; then + echo waiting + elif launchctl print "$GUI/$LABEL.$1" | grep -q 'state = running'; then + echo starting + else + # Quit from Sunshine's menu bar icon: launchd doesn't start it again after a clean exit. + echo stopped + fi +} + +status() { + local n st name port + for n in "${instances[@]}"; do + st=$(state "$n") + name=$(cut -f2 "$DATA/instance-$n/display") + port=$((BASE_PORT + 100 * n)) + if [ $tsv = 1 ]; then + printf '%s\t%s\t%s\t%s\n' "$n" "$st" "$name" "$port" + continue + fi + case $st in + waiting) st="on, waiting for the display to be connected" ;; + stopped) st="stopped (quit from Sunshine's menu bar icon)" ;; + esac + echo "Instance $n, $name (port $port): $st" + done +} + +start() { + local i + # off disables the agent so it stays off at login; bootstrap fails while it's disabled. + launchctl enable "$GUI/$LABEL.$1" + if loaded "$1"; then + # Loaded but stopped (quit from the menu bar): start it. One that's running is left alone. + launchctl kickstart "$GUI/$LABEL.$1" >/dev/null 2>&1 + return 0 + fi + # bootout returns before the job is gone, and bootstrap fails until it is. + for i in 1 2 3 4 5; do + launchctl bootstrap "$GUI" "$AGENTS/$LABEL.$1.plist" 2>/dev/null && return 0 + loaded "$1" && return 0 + sleep 1 + done + echo "Couldn't start instance $1's LaunchAgent $LABEL.$1." >&2 + return 1 +} + +stop() { + launchctl disable "$GUI/$LABEL.$1" + launchctl bootout "$GUI/$LABEL.$1" 2>/dev/null || true +} + +# Every instance the setup made, or the ones named. +instances=() +if [ $# -gt 0 ]; then + for n in "$@"; do + if ! [[ $n =~ ^[0-9]+$ ]] || [ ! -e "$DATA/instance-$n/display" ] || [ ! -e "$AGENTS/$LABEL.$n.plist" ]; then + echo "There's no instance $n; ft-sunshine status lists them." >&2 + exit 2 + fi + instances+=("$n") + done +else + for d in "$DATA"/instance-*/display; do + [ -e "$d" ] || continue + n=$(basename "$(dirname "$d")" | sed 's/^instance-//') + [ -e "$AGENTS/$LABEL.$n.plist" ] && instances+=("$n") + done + if [ ${#instances[@]} -eq 0 ]; then + echo "No Frametop Sunshine instances here; run frametop-host-setup.sh first." >&2 + exit 1 + fi + IFS=$'\n' read -r -d '' -a instances < <(printf '%s\n' "${instances[@]}" | sort -n) || true +fi + +failed=0 +case $cmd in + on) + for n in "${instances[@]}"; do + start "$n" || failed=1 + done + # Wait for each to answer (or to wait for its display), so the status means something. + for i in $(seq 1 20); do + pending=0 + for n in "${instances[@]}"; do + loaded "$n" || continue + answers "$n" || [ -e "$DATA/instance-$n/.waiting" ] || pending=1 + done + [ $pending = 0 ] && break + sleep 1 + done + ;; + off) + for n in "${instances[@]}"; do + stop "$n" + done + for i in $(seq 1 10); do + pending=0 + for n in "${instances[@]}"; do + loaded "$n" && pending=1 + done + [ $pending = 0 ] && break + sleep 0.5 + done + ;; +esac +status +exit $failed