diff --git a/.gitignore b/.gitignore
index 680c727d..853abcf7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,4 +10,5 @@ test/unit/core/state/TMP-v0-root-data-dir
.vagrant
**/__pycache__
**/.venv
-test/integ/.data-root-dir
\ No newline at end of file
+test/integ/.data-root-dir
+ansible/inventories/custom-dev.yml
diff --git a/containers/sunshine/Dockerfile b/containers/sunshine/Dockerfile
index 6987f523..103fec44 100644
--- a/containers/sunshine/Dockerfile
+++ b/containers/sunshine/Dockerfile
@@ -84,6 +84,7 @@ apt install -y \
xterm \
xz-utils \
zenity \
+ jq \
bubblewrap \
software-properties-common \
nano \
@@ -283,6 +284,9 @@ HEALTHCHECK --interval=10s --timeout=5s --start-period=5s --retries=3 \
# Allow user "cloudy" to stop container
RUN echo "cloudy ALL=(ALL) NOPASSWD: ${CLOUDYPAD_BIN_DIR}/stop-supervisord.sh" > /etc/sudoers.d/cloudy-stop-container
+# Allow user "cloudy" to install apps without password (for App Installer)
+RUN echo "cloudy ALL=(ALL) NOPASSWD: /usr/bin/apt-get, /usr/bin/apt, /usr/bin/install, /usr/bin/ln, /usr/bin/mkdir" > /etc/sudoers.d/cloudy-app-install
+
# Contain various Cloudy Pad scripts, add to PATH for easier use
ENV PATH=$PATH:/cloudy/bin
diff --git a/containers/sunshine/overlay/cloudy/bin/cloudy-app-installer.sh b/containers/sunshine/overlay/cloudy/bin/cloudy-app-installer.sh
new file mode 100755
index 00000000..ea80f9ab
--- /dev/null
+++ b/containers/sunshine/overlay/cloudy/bin/cloudy-app-installer.sh
@@ -0,0 +1,95 @@
+#!/usr/bin/env bash
+
+#
+# Cloudy Pad App Installer
+# Install optional applications on-demand
+#
+
+INSTALL_SCRIPTS_DIR="/cloudy/bin"
+
+show_menu() {
+ zenity --list \
+ --title="Cloudy Pad App Installer" \
+ --text="Select an application to install:" \
+ --column="App" --column="Description" --column="Status" \
+ --width=500 --height=400 \
+ --ok-label="Install" \
+ --cancel-label="Close" \
+ "Prism Launcher" "Minecraft launcher" "$(get_status prismlauncher)"
+}
+
+get_status() {
+ case "$1" in
+ prismlauncher)
+ if [ -x "/opt/prismlauncher/PrismLauncher.AppImage" ]; then
+ echo "Installed"
+ else
+ echo "Not installed"
+ fi
+ ;;
+ esac
+}
+
+install_app() {
+ case "$1" in
+ "Prism Launcher")
+ install_prismlauncher
+ ;;
+ esac
+}
+
+install_prismlauncher() {
+ if [ -x "/opt/prismlauncher/PrismLauncher.AppImage" ]; then
+ zenity --question \
+ --title="Prism Launcher" \
+ --text="Prism Launcher is already installed.\n\nDo you want to launch it?" \
+ --ok-label="Launch" \
+ --cancel-label="Back"
+ if [ $? -eq 0 ]; then
+ prismlauncher-start.sh &
+ exit 0
+ fi
+ else
+ "${INSTALL_SCRIPTS_DIR}/install-prismlauncher.sh" | zenity --progress \
+ --title="Installing Prism Launcher" \
+ --text="Starting installation..." \
+ --percentage=0 \
+ --auto-close \
+ --width=400
+
+ if [ -x "/opt/prismlauncher/PrismLauncher.AppImage" ]; then
+ choice=$(zenity --list \
+ --title="Installation Complete" \
+ --text="Prism Launcher installed successfully!" \
+ --column="Action" --column="Description" \
+ --width=500 --height=300 \
+ "Launch now" "Start Prism Launcher" \
+ "Show in Moonlight" "Restart session to update Moonlight app list" \
+ "Close" "Continue using desktop")
+
+ case "$choice" in
+ "Launch now")
+ prismlauncher-start.sh &
+ ;;
+ "Show in Moonlight")
+ pkill -TERM sunshine
+ ;;
+ esac
+ exit 0
+ else
+ zenity --error \
+ --title="Installation Failed" \
+ --text="Installation failed.\n\nCheck /tmp/prismlauncher-install.log for details."
+ fi
+ fi
+}
+
+while true; do
+ choice=$(show_menu)
+
+ if [ -z "$choice" ]; then
+ exit 0
+ fi
+
+ install_app "$choice"
+done
diff --git a/containers/sunshine/overlay/cloudy/bin/install-prismlauncher.sh b/containers/sunshine/overlay/cloudy/bin/install-prismlauncher.sh
new file mode 100755
index 00000000..05ee20bf
--- /dev/null
+++ b/containers/sunshine/overlay/cloudy/bin/install-prismlauncher.sh
@@ -0,0 +1,67 @@
+#!/usr/bin/env bash
+
+#
+# Installation script for Prism Launcher (Minecraft)
+#
+
+set -e
+
+PRISMLAUNCHER_VERSION="${PRISMLAUNCHER_VERSION:-10.0.2}"
+PRISMLAUNCHER_APP_DIR="/opt/prismlauncher"
+PRISMLAUNCHER_APPIMAGE_URL="https://github.com/PrismLauncher/PrismLauncher/releases/download/${PRISMLAUNCHER_VERSION}/PrismLauncher-Linux-x86_64.AppImage"
+
+is_installed() {
+ [ -x "${PRISMLAUNCHER_APP_DIR}/PrismLauncher.AppImage" ]
+}
+
+add_to_sunshine_apps() {
+ APPS_JSON="${XDG_CONFIG_HOME}/sunshine/apps.json"
+
+ [ ! -f "$APPS_JSON" ] && return
+
+ # Check if already added
+ if jq -e '.apps[] | select(.name | contains("Prism Launcher"))' "$APPS_JSON" > /dev/null 2>&1; then
+ return
+ fi
+
+ # Add new app
+ jq '.apps += [{
+ "name": "Prism Launcher (Minecraft)",
+ "image-path": "$(XDG_CONFIG_HOME)/sunshine/assets/prismlauncher.png",
+ "prep-cmd": [{
+ "do": "sh -c \"sunshine-app-startup.sh > /tmp/sunshine-session-start.log 2>&1\"",
+ "undo": "sh -c \"prismlauncher-stop.sh > /tmp/prismlauncher-stop.log 2>&1\""
+ }],
+ "detached": ["sh -c \"prismlauncher-start.sh > /tmp/prismlauncher-start.log 2>&1\""],
+ "exclude-global-prep-cmd": "false",
+ "auto-detach": "true",
+ "wait-all": "true",
+ "exit-timeout": "5",
+ "cmd": ""
+ }]' "$APPS_JSON" > "${APPS_JSON}.tmp" && mv "${APPS_JSON}.tmp" "$APPS_JSON"
+}
+
+if is_installed; then
+ echo "Already installed"
+ exit 0
+fi
+
+echo "10"; echo "# Installing dependencies..."
+sudo apt-get update > /tmp/prismlauncher-install.log 2>&1
+
+echo "30"; echo "# Installing Java and fuse..."
+sudo apt-get install -y fuse3 libfuse2t64 openjdk-21-jre >> /tmp/prismlauncher-install.log 2>&1
+
+echo "50"; echo "# Downloading Prism Launcher..."
+curl -L -o /tmp/PrismLauncher.AppImage "${PRISMLAUNCHER_APPIMAGE_URL}" >> /tmp/prismlauncher-install.log 2>&1
+
+echo "70"; echo "# Installing Prism Launcher..."
+sudo install -d -o "${CLOUDYPAD_USER}" -g "${CLOUDYPAD_USER}" "${PRISMLAUNCHER_APP_DIR}"
+sudo install -m 0755 -o "${CLOUDYPAD_USER}" -g "${CLOUDYPAD_USER}" /tmp/PrismLauncher.AppImage "${PRISMLAUNCHER_APP_DIR}/PrismLauncher.AppImage"
+sudo ln -sf "${PRISMLAUNCHER_APP_DIR}/PrismLauncher.AppImage" /usr/local/bin/prismlauncher
+rm -f /tmp/PrismLauncher.AppImage
+
+echo "90"; echo "# Adding to Sunshine apps..."
+add_to_sunshine_apps
+
+echo "100"; echo "# Done!"
diff --git a/containers/sunshine/overlay/cloudy/bin/prismlauncher-start.sh b/containers/sunshine/overlay/cloudy/bin/prismlauncher-start.sh
new file mode 100755
index 00000000..39161791
--- /dev/null
+++ b/containers/sunshine/overlay/cloudy/bin/prismlauncher-start.sh
@@ -0,0 +1,22 @@
+#!/usr/bin/env bash
+
+wait-x-availability.sh
+
+PRISMLAUNCHER_APPIMAGE_PATH="/opt/prismlauncher/PrismLauncher.AppImage"
+
+if [ ! -x "$PRISMLAUNCHER_APPIMAGE_PATH" ]; then
+ echo "Prism Launcher is not installed."
+ echo "Run 'install-prismlauncher.sh' to install it."
+ zenity --error --text="Prism Launcher is not installed.\n\nOpen a terminal and run:\ninstall-prismlauncher.sh" --title="Prism Launcher" 2>/dev/null || true
+ exit 1
+fi
+
+cd "$CLOUDYPAD_USER_HOME"
+
+"$PRISMLAUNCHER_APPIMAGE_PATH" &
+
+PRISMLAUNCHER_PID=$!
+echo $PRISMLAUNCHER_PID > /tmp/prismlauncher.pid
+echo "Prism Launcher started with PID: $PRISMLAUNCHER_PID"
+
+wait $PRISMLAUNCHER_PID
diff --git a/containers/sunshine/overlay/cloudy/bin/prismlauncher-stop.sh b/containers/sunshine/overlay/cloudy/bin/prismlauncher-stop.sh
new file mode 100755
index 00000000..ee26f97c
--- /dev/null
+++ b/containers/sunshine/overlay/cloudy/bin/prismlauncher-stop.sh
@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+
+echo "Stopping Prism Launcher..."
+
+if [ ! -f /tmp/prismlauncher.pid ]; then
+ echo "No Prism Launcher PID found at /tmp/prismlauncher.pid... Not stopping."
+ exit 0
+fi
+
+PRISMLAUNCHER_PID=$(cat /tmp/prismlauncher.pid)
+
+echo "Killing Prism Launcher with PID: $PRISMLAUNCHER_PID"
+
+kill $PRISMLAUNCHER_PID
+
+echo "Prism Launcher stopped"
diff --git a/containers/sunshine/overlay/cloudy/bin/setup-dirs.sh b/containers/sunshine/overlay/cloudy/bin/setup-dirs.sh
index dfb6ea0c..45949438 100755
--- a/containers/sunshine/overlay/cloudy/bin/setup-dirs.sh
+++ b/containers/sunshine/overlay/cloudy/bin/setup-dirs.sh
@@ -19,7 +19,8 @@ mkdir -p \
$XDG_CACHE_HOME \
$XDG_CONFIG_HOME \
$XDG_DATA_HOME \
- $CLOUDYPAD_USER_HOME
+ $CLOUDYPAD_USER_HOME \
+ $CLOUDYPAD_USER_HOME/Desktop
chown $CLOUDYPAD_USER:$CLOUDYPAD_USER \
$CLOUDYPAD_DATA_DIR \
@@ -38,3 +39,10 @@ chmod 0700 \
$XDG_CONFIG_HOME \
$XDG_DATA_HOME \
$CLOUDYPAD_USER_HOME
+
+# Copy desktop shortcuts to user's Desktop
+if [ -d "/cloudy/conf/desktop-shortcuts" ]; then
+ cp /cloudy/conf/desktop-shortcuts/*.desktop $CLOUDYPAD_USER_HOME/Desktop/ 2>/dev/null || true
+ chown -R $CLOUDYPAD_USER:$CLOUDYPAD_USER $CLOUDYPAD_USER_HOME/Desktop
+ chmod 755 $CLOUDYPAD_USER_HOME/Desktop/*.desktop 2>/dev/null || true
+fi
diff --git a/containers/sunshine/overlay/cloudy/conf/desktop-shortcuts/app-installer.desktop b/containers/sunshine/overlay/cloudy/conf/desktop-shortcuts/app-installer.desktop
new file mode 100644
index 00000000..c0474338
--- /dev/null
+++ b/containers/sunshine/overlay/cloudy/conf/desktop-shortcuts/app-installer.desktop
@@ -0,0 +1,9 @@
+[Desktop Entry]
+Version=1.0
+Type=Application
+Name=App Installer
+Comment=Install optional applications
+Exec=/cloudy/bin/cloudy-app-installer.sh
+Icon=gnome-app-install
+Terminal=false
+Categories=System;
diff --git a/containers/sunshine/overlay/cloudy/conf/sunshine/apps.json b/containers/sunshine/overlay/cloudy/conf/sunshine/apps.json
index b9bc2a92..55fd5ace 100644
--- a/containers/sunshine/overlay/cloudy/conf/sunshine/apps.json
+++ b/containers/sunshine/overlay/cloudy/conf/sunshine/apps.json
@@ -142,6 +142,23 @@
"wait-all": "true",
"exit-timeout": "5",
"cmd": ""
+ },
+ {
+ "name": "App Installer",
+ "image-path": "$(XDG_CONFIG_HOME)/sunshine/assets/app-installer.png",
+ "prep-cmd": [
+ {
+ "do": "sh -c \"sunshine-app-startup.sh > \/tmp\/sunshine-session-start.log 2>&1\""
+ }
+ ],
+ "detached": [
+ "sh -c \"cloudy-app-installer.sh > \/tmp\/app-installer.log 2>&1\""
+ ],
+ "exclude-global-prep-cmd": "false",
+ "auto-detach": "true",
+ "wait-all": "true",
+ "exit-timeout": "5",
+ "cmd": ""
}
]
}
\ No newline at end of file
diff --git a/containers/sunshine/overlay/cloudy/conf/sunshine/assets/app-installer.png b/containers/sunshine/overlay/cloudy/conf/sunshine/assets/app-installer.png
new file mode 100644
index 00000000..f64fd1d4
Binary files /dev/null and b/containers/sunshine/overlay/cloudy/conf/sunshine/assets/app-installer.png differ
diff --git a/containers/sunshine/overlay/cloudy/conf/sunshine/assets/prismlauncher.png b/containers/sunshine/overlay/cloudy/conf/sunshine/assets/prismlauncher.png
new file mode 100644
index 00000000..79781e64
Binary files /dev/null and b/containers/sunshine/overlay/cloudy/conf/sunshine/assets/prismlauncher.png differ
diff --git a/containers/sunshine/overlay/cloudy/conf/xfce4-default/panel/launcher-18/prismlauncher.desktop b/containers/sunshine/overlay/cloudy/conf/xfce4-default/panel/launcher-18/prismlauncher.desktop
new file mode 100644
index 00000000..17f61bb3
--- /dev/null
+++ b/containers/sunshine/overlay/cloudy/conf/xfce4-default/panel/launcher-18/prismlauncher.desktop
@@ -0,0 +1,13 @@
+[Desktop Entry]
+Name=Prism Launcher
+Comment=launcher for Minecraft
+Exec=prismlauncher-start.sh
+Icon=prismlauncher
+Terminal=false
+Type=Application
+Categories=Game;
+PrefersNonDefaultGPU=true
+X-KDE-RunOnDiscreteGpu=true
+X-XFCE-Source=file:///usr/share/applications/prismlauncher.desktop
+Path=
+StartupNotify=false
diff --git a/containers/sunshine/overlay/cloudy/conf/xfce4-default/xfconf/xfce-perchannel-xml/xfce4-panel.xml b/containers/sunshine/overlay/cloudy/conf/xfce4-default/xfconf/xfce-perchannel-xml/xfce4-panel.xml
index 47d64ce2..8d3f964e 100644
--- a/containers/sunshine/overlay/cloudy/conf/xfce4-default/xfconf/xfce-perchannel-xml/xfce4-panel.xml
+++ b/containers/sunshine/overlay/cloudy/conf/xfce4-default/xfconf/xfce-perchannel-xml/xfce4-panel.xml
@@ -45,6 +45,9 @@
+
+
+
@@ -108,6 +111,12 @@
+
+
+
+
+
+