diff --git a/README.md b/README.md index 64f3598..49d5dd0 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ Includes: * Latest FEX * CachyOS Proton 11 * Desktop mode (KDE) +* Nested Desktop inside Gaming Mode * Bazaar App Store * Over-the-air updates * Install to internal storage (alongside Android) @@ -174,6 +175,23 @@ desktop. The **Bazaar** app store and the **Armada Installer** ([Install to internal storage](#install-to-internal-storage)) live here. Use the **Return to Gaming Mode** shortcut on the desktop to switch back. +To add a desktop application, executable, AppImage, script, or Windows program +to the Gaming Mode library, right-click its file in Dolphin and choose **Add to +Steam**. + +### Nested Desktop + +Armada includes a **Nested Desktop** launcher for running KDE Plasma without +leaving Gaming Mode. In Desktop Mode, find **Nested Desktop** in the application +menu, right-click it, and choose **Add to Steam**. Back in Gaming Mode, open its +properties, select **Armada Native** under **Compatibility**, and set **Game +Resolution** to **Native**. Then launch it from the **Non-Steam** library. + +The nested session uses the same fullscreen KWin path as SteamOS: Gamescope +controls its surface and input mapping, while the launcher normalizes Armada's +rotated panel to a 150% landscape Plasma workspace. It also shares the parent +session's PipeWire audio. + ### Power button and sleep Pressing the power button does a "fake suspend" (inspired by ROCKNIX) rather than diff --git a/build_files/10-base-packages.sh b/build_files/10-base-packages.sh index 466b97d..396a286 100755 --- a/build_files/10-base-packages.sh +++ b/build_files/10-base-packages.sh @@ -78,6 +78,7 @@ dnf5 -y install --setopt=install_weak_deps=False \ plasma-pa \ plasma-nm \ bluedevil \ + upower \ maliit-keyboard \ libappindicator-gtk3 \ libdbusmenu-gtk3 \ diff --git a/build_files/50-create-user.sh b/build_files/50-create-user.sh index 9f03fc0..45c50cd 100755 --- a/build_files/50-create-user.sh +++ b/build_files/50-create-user.sh @@ -16,6 +16,7 @@ done install -d -m 0700 -o armada -g armada /var/home/armada chown -R armada:armada /var/home/armada chmod 0700 /var/home/armada +install -d -m 0755 -o armada -g armada /var/home/armada/Desktop install -Dpm 0755 -o armada -g armada \ /usr/share/applications/armada-return-to-gamemode.desktop \ /var/home/armada/Desktop/armada-return-to-gamemode.desktop diff --git a/system_files/etc/xdg/autostart/armada-desktop-steam.desktop b/system_files/etc/xdg/autostart/armada-desktop-steam.desktop new file mode 100644 index 0000000..da0a76a --- /dev/null +++ b/system_files/etc/xdg/autostart/armada-desktop-steam.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Type=Application +Name=Steam Desktop Controls +Comment=Start Steam to provide gamepad controls in Desktop Mode +Exec=/usr/libexec/armada/desktop-steam-autostart +Terminal=false +OnlyShowIn=KDE; +X-KDE-autostart-after=panel +X-KDE-StartupNotify=false diff --git a/system_files/usr/bin/steamos-add-to-steam b/system_files/usr/bin/steamos-add-to-steam new file mode 100755 index 0000000..f0fad4b --- /dev/null +++ b/system_files/usr/bin/steamos-add-to-steam @@ -0,0 +1,70 @@ +#!/usr/bin/bash +# SPDX-License-Identifier: GPL-2.0-or-later +# Adapted from Valve's steamdeck-kde-presets helper for Armada's ARM client. +set -euo pipefail + +show_dialog=0 + +usage() { + echo "Usage: steamos-add-to-steam [-ui] " >&2 +} + +show_error() { + if (( show_dialog )) && command -v kdialog >/dev/null 2>&1 && + [[ -n ${DISPLAY:-}${WAYLAND_DISPLAY:-} ]]; then + kdialog --title "Add to Steam" --error "$1" + else + echo "steamos-add-to-steam: $1" >&2 + fi +} + +if [[ ${1:-} == -ui ]]; then + show_dialog=1 + shift +fi + +if (( EUID == 0 )); then + show_error "This command cannot be run as root." + exit 1 +fi +if (( $# != 1 )); then + usage + exit 2 +fi + +if ! file=$(realpath -e -- "$1") || [[ ! -f ${file} ]]; then + show_error "File not found: $1" + exit 1 +fi + +mime=$(kmimetypefinder "${file}") +case ${mime} in + application/x-desktop|application/x-ms-dos-executable|application/x-msdownload|application/vnd.microsoft.portable-executable) + ;; + application/x-executable|application/vnd.appimage|application/x-shellscript|application/x-sh|text/x-shellscript) + if [[ ! -x ${file} ]]; then + show_error "The selected file is not executable." + exit 1 + fi + ;; + *) + show_error "Unsupported file type: ${mime}" + exit 1 + ;; +esac + +if [[ ${XDG_SESSION_TYPE:-tty} == tty ]] && + ! pgrep -u "$(id -u)" -f '/steamrtarm64/steam([[:space:]]|$)' >/dev/null 2>&1; then + show_error "Steam must be running when this command is used from a terminal session." + exit 1 +fi + +encoded_url=$(python3 -c \ + 'import sys, urllib.parse; print("steam://addnonsteamgame/" + urllib.parse.quote(sys.argv[1], safe=""))' \ + "${file}") + +# Steam watches this marker while processing the addnonsteamgame URI. The +# launcher forwards the URI to an existing client or starts the ARM client. +touch /tmp/addnonsteamgamefile +nohup /usr/libexec/armada/launch-steam --desktop "${encoded_url}" \ + /dev/null 2>&1 & diff --git a/system_files/usr/bin/steamos-nested-desktop b/system_files/usr/bin/steamos-nested-desktop new file mode 100755 index 0000000..e1763fc --- /dev/null +++ b/system_files/usr/bin/steamos-nested-desktop @@ -0,0 +1,119 @@ +#!/usr/bin/bash +# SPDX-License-Identifier: GPL-2.0-or-later +set -euo pipefail + +if [[ ${XDG_SESSION_DESKTOP:-} == KDE || ${KDE_FULL_SESSION:-} == true ]]; then + echo "steamos-nested-desktop: refusing to launch inside KDE" >&2 + if [[ ! -t 1 ]] && command -v kdialog >/dev/null 2>&1; then + kdialog --title "Nested Desktop" --sorry \ + "Nested Desktop should be launched from Gaming Mode." + fi + exit 0 +fi + +state_dir="${XDG_STATE_HOME:-${HOME}/.local/state}/armada" +install -d -m 0700 "${state_dir}" +exec >>"${state_dir}/nested-desktop.log" 2>&1 +echo "=== Nested Desktop $(date --iso-8601=seconds) ===" + +# Gamescope's overlay and session-specific portals must not leak into Plasma. +unset LD_PRELOAD XDG_DESKTOP_PORTAL_DIR XDG_SEAT_PATH XDG_SESSION_PATH +cd "${HOME}" + +parent_runtime=${XDG_RUNTIME_DIR:?XDG_RUNTIME_DIR is not set} +nested_runtime=$(mktemp -d "${parent_runtime}/nested-desktop.XXXXXXXX") + +cleanup() { + umount --recursive "${nested_runtime}" >/dev/null 2>&1 || true + rm -rf --one-file-system -- "${nested_runtime}" || true +} +trap cleanup EXIT + +# Keep audio connected to the parent Gaming Mode PipeWire session. +if [[ -S "${parent_runtime}/pulse/native" ]]; then + install -d -m 0700 "${nested_runtime}/pulse" + ln -s "${parent_runtime}/pulse/native" "${nested_runtime}/pulse/native" +fi +shopt -s nullglob +for socket in "${parent_runtime}"/pipewire-*; do + ln -s "${socket}" "${nested_runtime}/${socket##*/}" +done +shopt -u nullglob + +# Match SteamOS: let Gamescope define the surface, input region, orientation, +# and pixel geometry instead of imposing a second resolution on nested KWin. +install -d -m 0700 "${nested_runtime}/bin" +cat > "${nested_runtime}/bin/kwin_wayland_wrapper" <<'EOF' +#!/usr/bin/bash +exec /usr/bin/kwin_wayland_wrapper --fullscreen true --no-lockscreen "$@" +EOF +chmod 0755 "${nested_runtime}/bin/kwin_wayland_wrapper" + +# Armada changes between a rotated handheld panel and Gamescope's landscape +# surface. Once KScreen has attached the nested output, use 150% logical scaling +# on Gamescope's native surface, then remap Plasma Shell so the panel is created +# on the final screen geometry. +cat > "${nested_runtime}/bin/start-nested-plasma" </dev/null 2>&1; then + shell_pid=\$(pgrep -n -x plasmashell) + for _ in {1..40}; do + if env -u DISPLAY QT_QPA_PLATFORM=wayland \ + kscreen-doctor \ + output.X11-0.rotation.normal \ + output.X11-0.position.0,0 \ + output.X11-0.scale.1.5 \ + >>"${state_dir}/nested-desktop.log" 2>&1; then + break + fi + sleep 0.25 + done + gdbus call --session --dest org.kde.plasmashell \ + --object-path /PlasmaShell \ + --method org.kde.PlasmaShell.evaluateScript \ + 'panels().forEach(function (panel) { var geometry = screenGeometry(panel.screen); panel.lengthMode = "fill"; panel.minimumLength = geometry.width; panel.maximumLength = geometry.width; });' \ + >>"${state_dir}/nested-desktop.log" 2>&1 || true + sleep 0.5 + ( + while IFS= read -r -d '' entry; do + [[ \${entry} == BASH_FUNC_* ]] && continue + export "\${entry}" + done < "/proc/\${shell_pid}/environ" + unset DISPLAY + export QT_QPA_PLATFORM=wayland + exec /usr/bin/plasmashell --replace + ) >>"${state_dir}/nested-desktop.log" 2>&1 & + exit 0 + fi + sleep 0.25 + done +) & +bootstrap_pid=\$! + +wait "\${session_pid}" +status=\$? +kill "\${bootstrap_pid}" >/dev/null 2>&1 || true +wait "\${bootstrap_pid}" 2>/dev/null || true +exit "\${status}" +EOF +chmod 0755 "${nested_runtime}/bin/start-nested-plasma" + +export PATH="${nested_runtime}/bin:${PATH}" +export XDG_RUNTIME_DIR="${nested_runtime}" +export DESKTOP_SESSION=plasma +export XDG_CURRENT_DESKTOP=KDE +export XDG_SESSION_DESKTOP=KDE +export XDG_SESSION_TYPE=wayland +export ARMADA_NESTED_DESKTOP=1 + +dbus-run-session "${nested_runtime}/bin/start-nested-plasma" diff --git a/system_files/usr/libexec/armada/desktop-bootstrap b/system_files/usr/libexec/armada/desktop-bootstrap index 1bd9420..ff947b2 100755 --- a/system_files/usr/libexec/armada/desktop-bootstrap +++ b/system_files/usr/libexec/armada/desktop-bootstrap @@ -2,6 +2,7 @@ set -euo pipefail [[ ${XDG_SESSION_TYPE:-} == wayland ]] || exit 0 +[[ ${ARMADA_NESTED_DESKTOP:-0} != 1 ]] || exit 0 activation_env=( DESKTOP_SESSION @@ -24,7 +25,6 @@ config_dir="${XDG_CONFIG_HOME:-${HOME}/.config}/armada" rotation_done="${config_dir}/desktop-rotation.done" scale_done="${config_dir}/desktop-scale.done" -[[ ! -e "${rotation_done}" || ! -e "${scale_done}" ]] || exit 0 mkdir -p "${config_dir}" if command -v /usr/libexec/armada/device-env >/dev/null 2>&1; then @@ -41,18 +41,20 @@ for _ in {1..5}; do sleep 2 done -if [[ ! -e "${rotation_done}" ]]; then - case "${panel_orientation}" in - left|right|inverted) - if kscreen-doctor "output.${display_connector}.rotation.${panel_orientation}" >/dev/null 2>&1; then - touch "${rotation_done}" - fi - ;; - *) +# KScreen profiles can be rewritten by Gamescope or a nested session. Reapply +# the physical panel's orientation on every full desktop login. +case "${panel_orientation}" in + left|right|inverted) + if kscreen-doctor "output.${display_connector}.rotation.${panel_orientation}" >/dev/null 2>&1; then touch "${rotation_done}" - ;; - esac -fi + fi + ;; + *) + if kscreen-doctor "output.${display_connector}.rotation.normal" >/dev/null 2>&1; then + touch "${rotation_done}" + fi + ;; +esac if [[ ! -e "${scale_done}" ]] && kscreen-doctor "output.${display_connector}.scale.1.5" >/dev/null 2>&1; then touch "${scale_done}" diff --git a/system_files/usr/libexec/armada/desktop-steam-autostart b/system_files/usr/libexec/armada/desktop-steam-autostart new file mode 100755 index 0000000..2dd3e8a --- /dev/null +++ b/system_files/usr/libexec/armada/desktop-steam-autostart @@ -0,0 +1,47 @@ +#!/usr/bin/bash +set -euo pipefail + +# Gaming Mode already owns its Steam process while Nested Desktop is running. +[[ ${ARMADA_NESTED_DESKTOP:-0} != 1 ]] || exit 0 + +# Plasma's systemd autostart generator can run applications before plasmashell +# and Xwayland have attached to the display. Steam's ARM client crashes if it +# reaches its legacy UI initialization during that window. +shell_pid="" +for _ in {1..60}; do + shell_pid=$(pgrep -n -u "$(id -u)" -x plasmashell 2>/dev/null || true) + [[ -n "${shell_pid}" ]] && break + sleep 0.25 +done + +# Use the exact display credentials from Plasma rather than the user manager's +# possibly incomplete early-login environment. +if [[ -n "${shell_pid}" && -r "/proc/${shell_pid}/environ" ]]; then + while IFS= read -r -d '' entry; do + [[ ${entry} == BASH_FUNC_* ]] && continue + export "${entry}" + done < "/proc/${shell_pid}/environ" +fi + +if command -v xprop >/dev/null 2>&1; then + for _ in {1..240}; do + xprop -root >/dev/null 2>&1 && break + sleep 0.25 + done +else + sleep 10 +fi +sleep 2 + +# Avoid opening a second client if Steam was started manually during login. +# Confirm it survives startup before yielding; a competing early autostart may +# briefly create the process and then crash. +if pgrep -u "$(id -u)" -x steam >/dev/null 2>&1; then + sleep 5 + pgrep -u "$(id -u)" -x steam >/dev/null 2>&1 && exit 0 +fi + +state_dir="${XDG_STATE_HOME:-${HOME}/.local/state}/armada" +install -d -m 0700 "${state_dir}" +exec /usr/libexec/armada/launch-steam --desktop \ + >>"${state_dir}/desktop-steam.log" 2>&1 diff --git a/system_files/usr/share/applications/steam/steamos-nested-desktop/steamos-nested-desktop.desktop b/system_files/usr/share/applications/steam/steamos-nested-desktop/steamos-nested-desktop.desktop new file mode 100644 index 0000000..93fd6d6 --- /dev/null +++ b/system_files/usr/share/applications/steam/steamos-nested-desktop/steamos-nested-desktop.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Type=Application +Name=Nested Desktop +Comment=Run the KDE Plasma desktop inside Gaming Mode +Exec=/usr/bin/bash /usr/bin/steamos-nested-desktop +Icon=preferences-desktop-display +Terminal=false +Categories=System; +X-Steam-Controller-Template=Desktop diff --git a/system_files/usr/share/kio/servicemenus/steam.desktop b/system_files/usr/share/kio/servicemenus/steam.desktop new file mode 100644 index 0000000..834bc5f --- /dev/null +++ b/system_files/usr/share/kio/servicemenus/steam.desktop @@ -0,0 +1,11 @@ +[Desktop Entry] +Type=Service +X-KDE-ServiceTypes=KonqPopupMenu/Plugin +MimeType=application/x-desktop;application/x-executable;application/vnd.appimage;application/x-shellscript;application/x-sh;application/x-ms-dos-executable;application/x-msdownload;application/vnd.microsoft.portable-executable;text/x-shellscript; +Actions=addToSteam; + +[Desktop Action addToSteam] +Name=Add to Steam +Icon=steam +TryExec=steamos-add-to-steam +Exec=steamos-add-to-steam -ui %f diff --git a/system_files/usr/share/plasma/kickeractions/steam.desktop b/system_files/usr/share/plasma/kickeractions/steam.desktop new file mode 100644 index 0000000..6f60c57 --- /dev/null +++ b/system_files/usr/share/plasma/kickeractions/steam.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Type=Service +Actions=addToSteam; + +[Desktop Action addToSteam] +Name=Add to Steam +Icon=steam +TryExec=steamos-add-to-steam +Exec=steamos-add-to-steam -ui %u diff --git a/system_files/usr/share/steam/compatibilitytools.d/armada-native/armada-native b/system_files/usr/share/steam/compatibilitytools.d/armada-native/armada-native new file mode 100755 index 0000000..c5d4626 --- /dev/null +++ b/system_files/usr/share/steam/compatibilitytools.d/armada-native/armada-native @@ -0,0 +1,19 @@ +#!/bin/bash +set -euo pipefail + +verb=${1:-} +[[ -n "$verb" ]] || exit 2 +shift + +case "$verb" in + run|waitforexitandrun) + exec "$@" + ;; + destroyprefix) + exit 0 + ;; + *) + echo "Unsupported Armada Native verb: $verb" >&2 + exit 2 + ;; +esac diff --git a/system_files/usr/share/steam/compatibilitytools.d/armada-native/compatibilitytool.vdf b/system_files/usr/share/steam/compatibilitytools.d/armada-native/compatibilitytool.vdf new file mode 100644 index 0000000..6bf3224 --- /dev/null +++ b/system_files/usr/share/steam/compatibilitytools.d/armada-native/compatibilitytool.vdf @@ -0,0 +1,13 @@ +"compatibilitytools" +{ + "compat_tools" + { + "armada-native" + { + "install_path" "." + "display_name" "Armada Native" + "from_oslist" "windows" + "to_oslist" "linux" + } + } +} diff --git a/system_files/usr/share/steam/compatibilitytools.d/armada-native/toolmanifest.vdf b/system_files/usr/share/steam/compatibilitytools.d/armada-native/toolmanifest.vdf new file mode 100644 index 0000000..cc143a9 --- /dev/null +++ b/system_files/usr/share/steam/compatibilitytools.d/armada-native/toolmanifest.vdf @@ -0,0 +1,7 @@ +"manifest" +{ + "version" "2" + "commandline" "/armada-native %verb%" + "use_sessions" "0" + "compatmanager_layer_name" "native" +}