├── .gitattributes ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .vscode ├── build.sh ├── config.sh ├── defsettings.json ├── setup.sh └── tasks.json ├── LICENSE ├── README.md ├── deck.json ├── decky_plugin.pyi ├── docs ├── about.md ├── getting_started.md ├── manual_install.md └── platform_streaming.md ├── main.py ├── package.json ├── plugin.json ├── pnpm-lock.yaml ├── py_modules └── sunshine.py ├── rollup.config.js ├── src ├── components │ ├── PINInput.tsx │ └── PasswordInput.tsx ├── index.tsx ├── types.d.ts └── util │ ├── backend.tsx │ └── util.tsx ├── tests └── test_sunshine.py └── tsconfig.json /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build and Release 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | permissions: 8 | contents: write 9 | 10 | jobs: 11 | bundle: 12 | runs-on: ubuntu-latest 13 | defaults: 14 | run: 15 | working-directory: ./ 16 | steps: 17 | - name: Check out the repository to the runner 18 | uses: actions/checkout@v4 19 | - name: Run setup 20 | run: ./.vscode/setup.sh 21 | - name: Run build 22 | id: build 23 | run: ./.vscode/build.sh 24 | - name: Get current date 25 | id: date 26 | uses: Kaven-Universe/github-action-current-date-time@v1 27 | with: 28 | format: "YYYYMMDD-HHmmss" 29 | - name: Check Tag 30 | env: 31 | NAME: "nightly-${{ steps.date.outputs.time }}" 32 | run: echo "$NAME" 33 | - name: Nightly Release 34 | if: steps.build.outcome == 'success' 35 | env: 36 | NAME: "nightly-${{ steps.date.outputs.time }}" 37 | uses: softprops/action-gh-release@v2 38 | with: 39 | prerelease: true 40 | generate_release_notes: true 41 | name: ${{ env.NAME }} 42 | tag_name: ${{ env.NAME }} 43 | files: "./out/Decky\ Sunshine.zip" 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | *.swp 10 | 11 | pids 12 | logs 13 | results 14 | tmp 15 | 16 | # Coverage reports 17 | coverage 18 | 19 | # API keys and secrets 20 | .env 21 | 22 | # Dependency directory 23 | node_modules 24 | bower_components 25 | .pnpm-store 26 | 27 | # Editors 28 | .idea 29 | *.iml 30 | 31 | # OS metadata 32 | .DS_Store 33 | Thumbs.db 34 | 35 | # Ignore built ts files 36 | dist/ 37 | 38 | __pycache__/ 39 | 40 | /.yalc 41 | yalc.lock 42 | 43 | .vscode/settings.json 44 | 45 | # Ignore output folder 46 | 47 | backend/out 48 | 49 | # Make sure to ignore any instance of the loader's decky_plugin.py 50 | decky_plugin.py 51 | 52 | # Ignore decky CLI for building plugins 53 | out 54 | out/* 55 | cli/ 56 | cli/* 57 | cli/decky 58 | 59 | dev/ -------------------------------------------------------------------------------- /.vscode/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | CLI_LOCATION="$(pwd)/cli" 3 | echo "Building plugin in $(pwd)" 4 | printf "Please input sudo password to proceed.\n" 5 | 6 | # read -s sudopass 7 | # printf "\n" 8 | 9 | echo $sudopass | sudo $CLI_LOCATION/decky plugin build $(pwd) 10 | -------------------------------------------------------------------------------- /.vscode/config.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]:-$0}"; )" &> /dev/null && pwd 2> /dev/null; )"; 3 | # printf "${SCRIPT_DIR}\n" 4 | # printf "$(dirname $0)\n" 5 | if ! [[ -e "${SCRIPT_DIR}/settings.json" ]]; then 6 | printf '.vscode/settings.json does not exist. Creating it with default settings. Exiting afterwards. Run your task again.\n\n' 7 | cp "${SCRIPT_DIR}/defsettings.json" "${SCRIPT_DIR}/settings.json" 8 | exit 1 9 | else 10 | printf '.vscode/settings.json does exist. Congrats.\n' 11 | printf 'Make sure to change settings.json to match your deck.\n' 12 | fi -------------------------------------------------------------------------------- /.vscode/defsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deckip" : "steamdeck.local", 3 | "deckport" : "22", 4 | "deckuser" : "deck", 5 | "deckpass" : "ssap", 6 | "deckkey" : "-i ${env:HOME}/.ssh/id_rsa", 7 | "deckdir" : "/home/deck", 8 | "pluginname": "Example Plugin", 9 | "python.analysis.extraPaths": [ 10 | "./py_modules" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | PNPM_INSTALLED="$(which pnpm)" 3 | DOCKER_INSTALLED="$(which docker)" 4 | CLI_INSTALLED="$(pwd)/cli/decky" 5 | 6 | # echo "$PNPM_INSTALLED" 7 | # echo "$DOCKER_INSTALLED" 8 | # echo "$CLI_INSTALLED" 9 | 10 | echo "If you are using alpine linux, do not expect any support." 11 | if [[ "$PNPM_INSTALLED" =~ "which" ]]; then 12 | echo "pnpm is not currently installed, you can install it via your distro's package managment system or via a script that will attempt to do a manual install based on your system. If you wish to proceed with installing via the script then answer "no" (capitals do not matter) and proceed with the rest of the script. Otherwise, just hit enter to proceed and use the script." 13 | read run_pnpm_script 14 | if [[ "$run_pnpm_script" =~ "n" ]]; then 15 | echo "You have chose to install pnpm via npm or your distros package manager. Please make sure to do so before attempting to build your plugin." 16 | else 17 | CURL_INSTALLED="$(which curl)" 18 | WGET_INSTALLED="$(which wget)" 19 | if [[ "$CURL_INSTALLED" =~ "which" ]]; then 20 | printf "curl not found, attempting with wget.\n" 21 | if [[ "$WGET_INSTALLED" =~ "which" ]]; then 22 | printf "wget not found, please install wget or curl.\n" 23 | printf "Could not install pnpm as curl and wget were not found.\n" 24 | else 25 | wget -qO- https://get.pnpm.io/install.sh | sh - 26 | fi 27 | else 28 | curl -fsSL https://get.pnpm.io/install.sh | sh - 29 | fi 30 | fi 31 | fi 32 | 33 | if [[ "$DOCKER_INSTALLED" =~ "which" ]]; then 34 | echo "Docker is not currently installed, in order build plugins with a backend you will need to have Docker installed. Please install Docker via the preferred method for your distribution." 35 | fi 36 | 37 | if ! test -f "$CLI_INSTALLED"; then 38 | echo "The Decky CLI tool (binary file is just called "decky") is used to build your plugin as a zip file which you can then install on your Steam Deck to perform testing. We highly recommend you install it. Hitting enter now will run the script to install Decky CLI and extract it to a folder called cli in the current plugin directory. You can also type 'no' and hit enter to skip this but keep in mind you will not have a usable plugin without building it." 39 | read run_cli_script 40 | if [[ "$run_cli_script" =~ "n" ]]; then 41 | echo "You have chosen to not install the Decky CLI tool to build your plugins. Please install this tool to build and test your plugin before submitting it to the Plugin Database." 42 | else 43 | 44 | SYSTEM_ARCH="$(uname -a)" 45 | 46 | mkdir "$(pwd)"/cli 47 | if [[ "$SYSTEM_ARCH" =~ "x86_64" ]]; then 48 | 49 | if [[ "$SYSTEM_ARCH" =~ "Linux" ]]; then 50 | curl -L -o "$(pwd)"/cli/decky "https://github.com/SteamDeckHomebrew/cli/releases/latest/download/decky-linux-x86_64" 51 | fi 52 | 53 | if [[ "$SYSTEM_ARCH" =~ "Darwin" ]]; then 54 | curl -L -o "$(pwd)"/cli/decky "https://github.com/SteamDeckHomebrew/cli/releases/latest/download/decky-macOS-x86_64" 55 | fi 56 | 57 | else 58 | echo "System Arch not found! The only supported systems are Linux x86_64 and Apple x86_64/ARM64, not $SYSTEM_ARCH" 59 | fi 60 | 61 | if [[ "$SYSTEM_ARCH" =~ "arm64" ]]; then 62 | curl -L -o "$(pwd)"/cli/decky "https://github.com/SteamDeckHomebrew/cli/releases/latest/download/decky-macOS-aarch64" 63 | fi 64 | 65 | chmod +x "$(pwd)"/cli/decky 66 | echo "Decky CLI tool is now installed and you can build plugins into easy zip files using the "Build Zip" Task in vscodium." 67 | fi 68 | fi 69 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | //PRELIMINARY SETUP TASKS 5 | //Dependency setup task 6 | { 7 | "label": "depsetup", 8 | "type": "shell", 9 | "group": "none", 10 | "detail": "Install depedencies for basic setup", 11 | "linux": { 12 | "command": "${workspaceFolder}/.vscode/setup.sh", 13 | }, 14 | // // placeholder for windows scripts, not currently planned 15 | // "windows": { 16 | // "command": "call -c ${workspaceFolder}\\.vscode\\setup.bat", 17 | // }, 18 | "problemMatcher": [] 19 | }, 20 | //pnpm setup task to grab all needed modules 21 | { 22 | "label": "pnpmsetup", 23 | "type": "shell", 24 | "group": "none", 25 | "detail": "Setup pnpm", 26 | "command": "which pnpm && pnpm i", 27 | "problemMatcher": [] 28 | }, 29 | //Preliminary "All-in-one" setup task 30 | { 31 | "label": "setup", 32 | "detail": "Set up depedencies, pnpm and update Decky Frontend Library.", 33 | "dependsOrder": "sequence", 34 | "dependsOn": [ 35 | "depsetup", 36 | "pnpmsetup", 37 | "updatefrontendlib" 38 | ], 39 | "problemMatcher": [] 40 | }, 41 | //Preliminary Deploy Config Setup 42 | { 43 | "label": "settingscheck", 44 | "type": "shell", 45 | "group": "none", 46 | "detail": "Check that settings.json has been created", 47 | "linux": { 48 | "command": "${workspaceFolder}/.vscode/config.sh", 49 | }, 50 | // // placeholder for windows scripts, not currently planned 51 | // "windows": { 52 | // "command": "call ${workspaceFolder}\\.vscode\\config.bat", 53 | // }, 54 | "problemMatcher": [] 55 | }, 56 | //BUILD TASKS 57 | { 58 | "label": "cli-build", 59 | "group": "build", 60 | "detail": "Build plugin with CLI", 61 | "linux": { 62 | "command": "${workspaceFolder}/.vscode/build.sh", 63 | }, 64 | // // placeholder for windows logic, not currently planned 65 | // "windows": { 66 | // "command": "call ${workspaceFolder}\\.vscode\\build.bat", 67 | // }, 68 | "problemMatcher": [] 69 | }, 70 | //"All-in-one" build task 71 | { 72 | "label": "build", 73 | "group": "build", 74 | "detail": "Build decky-plugin-template", 75 | "dependsOrder": "sequence", 76 | "dependsOn": [ 77 | "setup", 78 | "settingscheck", 79 | "cli-build", 80 | ], 81 | "problemMatcher": [] 82 | }, 83 | //DEPLOY TASKS 84 | //Copies the zip file of the built plugin to the plugins folder 85 | { 86 | "label": "copyzip", 87 | "detail": "Deploy plugin zip to deck", 88 | "type": "shell", 89 | "group": "none", 90 | "dependsOn": [ 91 | "chmodplugins" 92 | ], 93 | "command": "rsync -azp --chmod=D0755,F0755 --rsh='ssh -p ${config:deckport} ${config:deckkey}' out/ ${config:deckuser}@${config:deckip}:${config:deckdir}/homebrew/plugins", 94 | "problemMatcher": [] 95 | }, 96 | // 97 | { 98 | "label": "extractzip", 99 | "detail": "", 100 | "type": "shell", 101 | "group": "none", 102 | "command": "echo '${config:deckdir}/homebrew/plugins/${config:pluginname}.zip' && ssh ${config:deckuser}@${config:deckip} -p ${config:deckport} ${config:deckkey} 'echo ${config:deckpass} | sudo -S mkdir 755 -p \"$(echo \"${config:deckdir}/homebrew/plugins/${config:pluginname}\" | sed \"s| |-|\")\" && echo ${config:deckpass} | sudo -S chown ${config:deckuser}:${config:deckuser} \"$(echo \"${config:deckdir}/homebrew/plugins/${config:pluginname}\" | sed \"s| |-|\")\" && echo ${config:deckpass} | sudo -S bsdtar -xzpf \"${config:deckdir}/homebrew/plugins/${config:pluginname}.zip\" -C \"$(echo \"${config:deckdir}/homebrew/plugins/${config:pluginname}\" | sed \"s| |-|g\")\" --strip-components=1 --fflags '", 103 | "problemMatcher": [] 104 | }, 105 | //"All-in-one" deploy task 106 | { 107 | "label": "deploy", 108 | "dependsOrder": "sequence", 109 | "group": "none", 110 | "dependsOn": [ 111 | "copyzip", 112 | "extractzip" 113 | ], 114 | "problemMatcher": [] 115 | }, 116 | //"All-in-on" build & deploy task 117 | { 118 | "label": "builddeploy", 119 | "detail": "Builds plugin and deploys to deck", 120 | "dependsOrder": "sequence", 121 | "group": "none", 122 | "dependsOn": [ 123 | "build", 124 | "deploy" 125 | ], 126 | "problemMatcher": [] 127 | }, 128 | //GENERAL TASKS 129 | //Update Decky Frontend Library, aka DFL 130 | { 131 | "label": "updatefrontendlib", 132 | "type": "shell", 133 | "group": "build", 134 | "detail": "Update deck-frontend-lib aka DFL", 135 | "command": "pnpm update decky-frontend-lib --latest", 136 | "problemMatcher": [] 137 | }, 138 | //Used chmod plugins folder to allow copy-over of files 139 | { 140 | "label": "chmodplugins", 141 | "detail": "chmods plugins folder to prevent perms issues", 142 | "type": "shell", 143 | "group": "none", 144 | "command": "ssh ${config:deckuser}@${config:deckip} -p ${config:deckport} ${config:deckkey} 'echo '${config:deckpass}' | sudo -S chmod -R ug+rw ${config:deckdir}/homebrew/plugins/'", 145 | "problemMatcher": [] 146 | }, 147 | ] 148 | } 149 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2024, s0t7x 4 | Original Copyright (c) 2022-2024, Steam Deck Homebrew 5 | 6 | All rights reserved. 7 | 8 | Redistribution and use in source and binary forms, with or without 9 | modification, are permitted provided that the following conditions are met: 10 | 11 | 1. Redistributions of source code must retain the above copyright notice, this 12 | list of conditions and the following disclaimer. 13 | 14 | 2. Redistributions in binary form must reproduce the above copyright notice, 15 | this list of conditions and the following disclaimer in the documentation 16 | and/or other materials provided with the distribution. 17 | 18 | 3. Neither the name of the copyright holder nor the names of its 19 | contributors may be used to endorse or promote products derived from 20 | this software without specific prior written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 23 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 24 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 26 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 27 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 28 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 30 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | Decky Sunshine Logo 3 |

Decky Sunshine Plugin

4 |

5 | 6 | Release 7 | 8 | 9 | Issues 10 | 11 | 12 | [![Build](https://github.com/s0t7x/decky-sunshine/actions/workflows/main.yml/badge.svg)](https://github.com/s0t7x/decky-sunshine/actions/workflows/main.yml) 13 | 14 |

15 |
16 | 17 | Decky Sunshine is the perfect solution for capturing your Steam Deck gameplay. With this plugin, you can easily toggle the Sunshine game streaming server and pair new devices, allowing you to share your gaming experiences with the world seamlessly. 18 | 19 | [Getting Started with Decky Sunshine](https://github.com/s0t7x/decky-sunshine/blob/main/docs/getting_started.md) 20 | 21 | [What exactly does all of this mean?](https://github.com/s0t7x/decky-sunshine/blob/main/docs/about.md) 22 | 23 | [How do I stream my SteamDeck to Twitch using my PC?](https://github.com/s0t7x/decky-sunshine/blob/main/docs/platform_streaming.md) 24 | 25 | [How to manually install Sunshine?](https://github.com/s0t7x/decky-sunshine/blob/main/docs/manual_install.md) 26 | 27 | ## Features 28 | 29 | - Toggle the Sunshine game streaming server on/off directly from Decky's interface. 30 | - Easily pair new devices with Sunshine. 31 | - Automatic installation and setup if Sunshine is not already installed. 32 | - Resume last state on reboot 33 | - Stream games from your Steam Deck to other devices over a local network. 34 | 35 | ## Installation 36 | 37 | 1. Make sure [Decky Plugin loader](https://decky.xyz) is installed 38 | 2. Open the Decky Plugin Store 39 | 2. Search for "Decky Sunshine" and install it 40 | 41 | ## Usage 42 | 43 | 1. Use the toggle button to enable or disable the Sunshine server as needed. 44 | 2. Enter the PIN to pair new devices when prompted. 45 | 3. Enjoy streaming games seamlessly with Sunshine during your gaming sessions. 46 | 47 | When sunshine was already installed a sunshine user may already have been set. 48 | Decky Sunshine will ask for the credentials once it can't authenticate. 49 | 50 | ## Contributing 51 | 52 | Contributions to the Decky Sunshine project are welcome! If you have any ideas for improvements, bug fixes, or new features, feel free to open an issue or submit a pull request. 53 | 54 | ## Acknowledgements 55 | 56 | Thanks to [Decky Plugin Loader](https://decky.xyz/) for being amazing. 57 | 58 | Thanks to [CameronRedmore/memory-deck](https://github.com/CameronRedmore/memory-deck) for the work on `NumpadInput` which was adapted for `PINInput.tsx`. 59 | 60 | Special thanks to the developers of Sunshine for creating the game streaming server. 61 | -------------------------------------------------------------------------------- /deck.json: -------------------------------------------------------------------------------- 1 | { 2 | "deckip": "0.0.0.0", 3 | "deckport": "22", 4 | "deckpass": "ssap", 5 | "deckkey": "-i $HOME/.ssh/id_rsa", 6 | "deckdir": "/home/deck" 7 | } -------------------------------------------------------------------------------- /decky_plugin.pyi: -------------------------------------------------------------------------------- 1 | """ 2 | This module exposes various constants and helpers useful for decky plugins. 3 | 4 | * Plugin's settings and configurations should be stored under `DECKY_PLUGIN_SETTINGS_DIR`. 5 | * Plugin's runtime data should be stored under `DECKY_PLUGIN_RUNTIME_DIR`. 6 | * Plugin's persistent log files should be stored under `DECKY_PLUGIN_LOG_DIR`. 7 | 8 | Avoid writing outside of `DECKY_HOME`, storing under the suggested paths is strongly recommended. 9 | 10 | Some basic migration helpers are available: `migrate_any`, `migrate_settings`, `migrate_runtime`, `migrate_logs`. 11 | 12 | A logging facility `logger` is available which writes to the recommended location. 13 | """ 14 | 15 | __version__ = '0.5.0' 16 | 17 | import logging 18 | 19 | """ 20 | Constants 21 | """ 22 | 23 | HOME: str 24 | """ 25 | The home directory of the effective user running the process. 26 | Environment variable: `HOME`. 27 | If `root` was specified in the plugin's flags it will be `/root` otherwise the user whose home decky resides in. 28 | e.g.: `/home/deck` 29 | """ 30 | 31 | USER: str 32 | """ 33 | The effective username running the process. 34 | Environment variable: `USER`. 35 | It would be `root` if `root` was specified in the plugin's flags otherwise the user whose home decky resides in. 36 | e.g.: `deck` 37 | """ 38 | 39 | DECKY_VERSION: str 40 | """ 41 | The version of the decky loader. 42 | Environment variable: `DECKY_VERSION`. 43 | e.g.: `v2.5.0-pre1` 44 | """ 45 | 46 | DECKY_USER: str 47 | """ 48 | The user whose home decky resides in. 49 | Environment variable: `DECKY_USER`. 50 | e.g.: `deck` 51 | """ 52 | 53 | 54 | DECKY_USER_HOME: str 55 | """ 56 | The home of the user where decky resides in. 57 | Environment variable: `DECKY_USER_HOME`. 58 | e.g.: `/home/deck` 59 | """ 60 | 61 | DECKY_HOME: str 62 | """ 63 | The root of the decky folder. 64 | Environment variable: `DECKY_HOME`. 65 | e.g.: `/home/deck/homebrew` 66 | """ 67 | 68 | DECKY_PLUGIN_SETTINGS_DIR: str 69 | """ 70 | The recommended path in which to store configuration files (created automatically). 71 | Environment variable: `DECKY_PLUGIN_SETTINGS_DIR`. 72 | e.g.: `/home/deck/homebrew/settings/decky-plugin-template` 73 | """ 74 | 75 | DECKY_PLUGIN_RUNTIME_DIR: str 76 | """ 77 | The recommended path in which to store runtime data (created automatically). 78 | Environment variable: `DECKY_PLUGIN_RUNTIME_DIR`. 79 | e.g.: `/home/deck/homebrew/data/decky-plugin-template` 80 | """ 81 | 82 | DECKY_PLUGIN_LOG_DIR: str 83 | """ 84 | The recommended path in which to store persistent logs (created automatically). 85 | Environment variable: `DECKY_PLUGIN_LOG_DIR`. 86 | e.g.: `/home/deck/homebrew/logs/decky-plugin-template` 87 | """ 88 | 89 | DECKY_PLUGIN_DIR: str 90 | """ 91 | The root of the plugin's directory. 92 | Environment variable: `DECKY_PLUGIN_DIR`. 93 | e.g.: `/home/deck/homebrew/plugins/decky-plugin-template` 94 | """ 95 | 96 | DECKY_PLUGIN_NAME: str 97 | """ 98 | The name of the plugin as specified in the 'plugin.json'. 99 | Environment variable: `DECKY_PLUGIN_NAME`. 100 | e.g.: `Example Plugin` 101 | """ 102 | 103 | DECKY_PLUGIN_VERSION: str 104 | """ 105 | The version of the plugin as specified in the 'package.json'. 106 | Environment variable: `DECKY_PLUGIN_VERSION`. 107 | e.g.: `0.0.1` 108 | """ 109 | 110 | DECKY_PLUGIN_AUTHOR: str 111 | """ 112 | The author of the plugin as specified in the 'plugin.json'. 113 | Environment variable: `DECKY_PLUGIN_AUTHOR`. 114 | e.g.: `John Doe` 115 | """ 116 | 117 | DECKY_PLUGIN_LOG: str 118 | """ 119 | The path to the plugin's main logfile. 120 | Environment variable: `DECKY_PLUGIN_LOG`. 121 | e.g.: `/home/deck/homebrew/logs/decky-plugin-template/plugin.log` 122 | """ 123 | 124 | """ 125 | Migration helpers 126 | """ 127 | 128 | 129 | def migrate_any(target_dir: str, *files_or_directories: str) -> dict[str, str]: 130 | """ 131 | Migrate files and directories to a new location and remove old locations. 132 | Specified files will be migrated to `target_dir`. 133 | Specified directories will have their contents recursively migrated to `target_dir`. 134 | 135 | Returns the mapping of old -> new location. 136 | """ 137 | 138 | 139 | def migrate_settings(*files_or_directories: str) -> dict[str, str]: 140 | """ 141 | Migrate files and directories relating to plugin settings to the recommended location and remove old locations. 142 | Specified files will be migrated to `DECKY_PLUGIN_SETTINGS_DIR`. 143 | Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_SETTINGS_DIR`. 144 | 145 | Returns the mapping of old -> new location. 146 | """ 147 | 148 | 149 | def migrate_runtime(*files_or_directories: str) -> dict[str, str]: 150 | """ 151 | Migrate files and directories relating to plugin runtime data to the recommended location and remove old locations 152 | Specified files will be migrated to `DECKY_PLUGIN_RUNTIME_DIR`. 153 | Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_RUNTIME_DIR`. 154 | 155 | Returns the mapping of old -> new location. 156 | """ 157 | 158 | 159 | def migrate_logs(*files_or_directories: str) -> dict[str, str]: 160 | """ 161 | Migrate files and directories relating to plugin logs to the recommended location and remove old locations. 162 | Specified files will be migrated to `DECKY_PLUGIN_LOG_DIR`. 163 | Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_LOG_DIR`. 164 | 165 | Returns the mapping of old -> new location. 166 | """ 167 | 168 | 169 | """ 170 | Logging 171 | """ 172 | 173 | logger: logging.Logger 174 | """The main plugin logger writing to `DECKY_PLUGIN_LOG`.""" 175 | -------------------------------------------------------------------------------- /docs/about.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | This document provides an overview of the different components involved in the Decky Sunshine Plugin and how they work together. 4 | 5 | ## Decky Plugin Loader (Decky Loader) 6 | 7 | Decky Plugin Loader is an open-source project written in Python with the goal of adding plugin-loading capabilities to the Steam Deck. After installing, plugins can be accessed using a plugins tab in the quick access menu. It serves as a platform for developers to create and distribute plugins that extend the functionality of the Steam Deck. 8 | 9 | ## A Decky Plugin 10 | 11 | The Decky Sunshine Plugin is a plugin specifically designed to work with the Decky Plugin Loader. It provides a user-friendly interface for controlling the Sunshine game streaming server directly from the Steam Deck's interface. 12 | 13 | ## Sunshine (Open Source Moonlight Game Streaming Server) 14 | 15 | Sunshine is an open-source implementation of the Moonlight game streaming server, which allows you to stream games from your Steam Deck to other devices over a local network or the internet. It is based on the GameStream protocol developed by NVIDIA. 16 | 17 | ## Moonlight (Game Streaming Protocol by NVIDIA) 18 | 19 | Moonlight is a game streaming protocol developed by NVIDIA that enables low-latency, high-performance game streaming from compatible devices to other devices on the same network or over the internet. It is designed to provide a seamless gaming experience by efficiently encoding and transmitting video and audio data. 20 | 21 | ## Moonlight Desktop Client 22 | 23 | The Moonlight Desktop Client is a software application that allows you to connect to and stream games from a Moonlight-compatible server, such as Sunshine. It runs on various platforms, including Windows, macOS, and Linux, and provides a user interface for connecting to the streaming server, managing game streaming sessions, and configuring settings. 24 | 25 | ## How Sunshine (Server) and Desktop Client Work Together 26 | 27 | Sunshine and the Moonlight Desktop Client work together to enable game streaming from the Steam Deck to other devices. Here's how the process works: 28 | 29 | 1. **Sunshine Server**: The Sunshine server runs on your Steam Deck and captures the game's video and audio output in real-time. 30 | 31 | 2. **Encoding and Streaming**: Sunshine encodes the captured data using the Moonlight game streaming protocol and streams it over the network. 32 | 33 | 3. **Moonlight Desktop Client**: The Moonlight Desktop Client, running on another device (e.g., a desktop computer, laptop, or mobile device), connects to the Sunshine server and receives the encoded video and audio stream. 34 | 35 | 4. **Decoding and Rendering**: The Moonlight Desktop Client decodes the received stream and renders the game's video and audio on the target device, allowing you to play the game remotely as if it were running locally. 36 | 37 | 5. **Input Handling**: The Moonlight Desktop Client also handles input from the target device (e.g., keyboard, mouse, or gamepad) and sends it back to the Sunshine server, which then injects the input into the game running on the Steam Deck. 38 | 39 | By leveraging the Decky Sunshine Plugin, you can easily control the Sunshine server, enabling or disabling game streaming as needed, without having to navigate through complex menus or command-line interfaces. The plugin provides a convenient way to manage your game streaming sessions directly from the Steam Deck's interface. 40 | -------------------------------------------------------------------------------- /docs/getting_started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 🚀 2 | ## Streaming your SteamDeck to your PC 3 | 4 | This guide will walk you through the steps to stream your Steam Deck gameplay to your PC. 5 | 6 | ## Installing Decky 7 | 8 | 1. Access the [Decky website](https://decky.xyz) from your Steam Deck and download the installer. 9 | 2. If your browser cannot run files, open the downloaded installer from the Dolphin file manager. 10 | 11 | ## Installing Decky Sunshine Plugin 12 | 13 | 1. Open the Decky Plugin Store 14 | 2. Search for "Decky Sunshine" 15 | 3. Press install 16 | 17 | ## Starting the Sunshine Server 18 | 19 | 1. In the Decky tab of the Quick Access Menu, locate the Decky Sunshine Plugin and click on the toggle button to enable the Sunshine server. 20 | 21 | ## Installing Moonlight Client on a Windows PC 22 | 23 | 1. Download the Moonlight Desktop Client for Windows from the official website (https://moonlight-stream.org/). 24 | 2. Run the installer and follow the on-screen instructions to complete the installation. 25 | 26 | ## Pairing the Moonlight Client with the Sunshine Server 27 | 28 | 1. Launch the Moonlight Desktop Client on your Windows PC. 29 | 2. You may wait till your Sunshine Server is located or add its IP manually. 30 | 3. Select your Steam Deck. 31 | 4. On your Steam Deck navigate to Decky Sunshine in the Quick Access Menu. 32 | 5. Enter the PIN displayed on your PC and click on "Pair". 33 | 6. Once paired, the Moonlight Client will connect to the Sunshine server and stream your Steam Deck. 34 | 35 | With these steps, you should now be able to stream your Steam Deck gameplay to your PC. 36 | [You may also want to pass what you see to platforms like Twitch.](https://github.com/s0t7x/decky-sunshine/blob/main/docs/platform_streaming.md) 37 | -------------------------------------------------------------------------------- /docs/manual_install.md: -------------------------------------------------------------------------------- 1 | # Manual installation 2 | 3 | Sometimes you want to install sunshine manually. This usually is the case when the automatic installation fails. 4 | 5 | 1. Change your SteamDeck to Desktop Mode 6 | 2. Open a terminal window 7 | 3. Enter `flatpak install --system -y dev.lizardbyte.app.Sunshine` to install sunshine 8 | 4. In the same terminal run `flatpak run dev.lizardbyte.app.Sunshine` after installation 9 | 5. Open up your web-browser and navigate to `https://localhost:47990` 10 | 6. Follow the instructions to setup a user 11 | 7. Close the terminal (stops sunshine) and go back to Game Mode 12 | 8. Open the Decky Sunshine Plugin and enable sunshine 13 | 9. Enter the given user credentials 14 | -------------------------------------------------------------------------------- /docs/platform_streaming.md: -------------------------------------------------------------------------------- 1 | # Streaming Gameplay to Platforms like Twitch 2 | 3 | This guide will shortly summarize [Getting Started with Decky Sunshine](https://github.com/s0t7x/decky-sunshine/blob/main/docs/getting_started.md) and explain further steps to stream your Steam Deck gameplay to platforms like Twitch using Open Broadcaster Software (OBS) on your PC. 4 | 5 | ## Prepare Streaming 6 | 7 | On your Steam Deck: 8 | 9 | 1. Make sure [Decky Plugin Loader](https://decky.xyz) is installed 10 | 2. Install Decky Sunshine 11 | 3. Enable the Sunshine Server 12 | 13 | On your PC: 14 | 15 | 1. Download and install the Moonlight Desktop Client 16 | 2. In the Moonlight Client Select your Steam Deck. 17 | 3. On your Steam Deck navigate to Decky Sunshine in the Quick Access Menu. 18 | 4. Enter the PIN displayed on your PC and click on "Pair". 19 | 5. Once paired, the Moonlight Client will connect to the Sunshine server and stream your Steam Deck. 20 | 21 | For a detailed guide on how to install Decky Sunshine take a look at [Getting Started with Decky Sunshine](https://github.com/s0t7x/decky-sunshine/blob/main/docs/getting_started.md). 22 | 23 | ## Setting up OBS for Streaming 24 | 25 | 1. Download and install OBS Studio from the [official website](https://obsproject.com/). 26 | 2. Launch OBS and create a new scene. 27 | 3. In the "Sources" panel, click on the "+" button and select "Game Capture". 28 | 4. In the "Create/Select Source" window, choose the Moonlight Desktop Client from the list of running applications. 29 | 5. Configure the settings as desired and click "OK" to add the game capture source to your scene. 30 | 6. In the "Audio Mixer" panel, ensure that the Moonlight Desktop Client is selected as an audio source. 31 | 32 | ## Streaming to Twitch 33 | 34 | 1. In OBS, navigate to the "Settings" menu and select the "Stream" option. 35 | 2. Choose "Twitch" as the streaming service and enter your Twitch stream key. 36 | 3. Configure any additional settings as desired. 37 | 4. When ready, click the "Start Streaming" button in OBS to begin streaming your Steam Deck gameplay to Twitch. 38 | 39 | With these steps, you should now be able to stream your Steam Deck gameplay to platforms like Twitch using the Decky Sunshine Plugin, Moonlight Desktop Client, and OBS. Remember to adjust settings and configurations as needed for optimal streaming performance. 40 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import decky_plugin 2 | 3 | from pathlib import Path 4 | import os 5 | import time 6 | 7 | from settings import SettingsManager 8 | from sunshine import SunshineController 9 | 10 | class Plugin: 11 | sunshineController = None 12 | settingManager = None 13 | 14 | async def set_setting(self, key, value): 15 | return self.settingManager.setSetting(key, value) 16 | 17 | async def get_setting(self, key, default): 18 | return self.settingManager.getSetting(key, default) 19 | 20 | async def sunshineIsRunning(self): 21 | isRunning = self.sunshineController.isRunning() 22 | decky_plugin.logger.info("Is Sunshine running: " + str(isRunning)) 23 | return isRunning 24 | 25 | async def sunshineIsAuthorized(self): 26 | isAuthorized = self.sunshineController.isAuthorized() 27 | decky_plugin.logger.info("Is Decky Sunshine authorized: " + str(isAuthorized)) 28 | return isAuthorized 29 | 30 | async def sunshineStart(self): 31 | decky_plugin.logger.info("Starting sunshine...") 32 | res = self.sunshineController.start() 33 | if res: 34 | decky_plugin.logger.info("Sunshine started") 35 | self.settingManager.setSetting("lastRunState", "start") 36 | else: 37 | decky_plugin.logger.info("Couldn't start Sunshine") 38 | self.settingManager.setSetting("lastRunState", "stop") 39 | return res 40 | 41 | async def sunshineStop(self): 42 | decky_plugin.logger.info("Stopping sunshine...") 43 | self.sunshineController.stop() 44 | self.settingManager.setSetting("lastRunState", "stop") 45 | return True 46 | 47 | async def sunshineSetUser(self, newUsername, newPassword, confirmNewPassword, currentUsername = None, currentPassword = None): 48 | if len(newUsername) + len(newPassword) < 1: 49 | decky_plugin.logger.info("No User to set") 50 | return 51 | decky_plugin.logger.info("Set Sunshine User...") 52 | result = self.sunshineController.setUser(newUsername, newPassword, confirmNewPassword, currentUsername, currentPassword) 53 | self.settingManager.setSetting("lastAuthHeader", str(result)) 54 | wasUserChanged = result is None 55 | decky_plugin.logger.info("User changed: " + str(wasUserChanged)) 56 | return wasUserChanged 57 | 58 | async def sendPin(self, pin): 59 | decky_plugin.logger.info("Sending PIN..." + pin) 60 | send = self.sunshineController.sendPin(pin) 61 | decky_plugin.logger.info("PIN send " + str(send)) 62 | return send 63 | 64 | async def setAuthHeader(self, username, password): 65 | if len(username) + len(password) < 1: 66 | decky_plugin.logger.info("No AuthHeader to set") 67 | return 68 | decky_plugin.logger.info("Set AuthHeader...") 69 | res = self.sunshineController.setAuthHeader(username, password) 70 | self.settingManager.setSetting("lastAuthHeader", str(res)) 71 | decky_plugin.logger.info("AuthHeader set") 72 | 73 | async def _main(self): 74 | decky_plugin.logger.info("Decky Sunshine version: " + decky_plugin.__version__) 75 | if self.sunshineController is None: 76 | self.sunshineController = SunshineController(decky_plugin.logger) 77 | 78 | if self.settingManager is None: 79 | decky_plugin.logger.info("Reading settings...") 80 | self.settingManager = SettingsManager(name = "decky-sunshine", settings_directory=os.environ["DECKY_PLUGIN_SETTINGS_DIR"]) 81 | self.settingManager.read() 82 | decky_plugin.logger.info(f"Read settings") 83 | 84 | if not self.sunshineController.ensureDependencies(): 85 | return 86 | 87 | if self.sunshineController.isFreshInstallation: 88 | self.sunshineController.start() 89 | triesLeft = 5 90 | time.sleep(2) 91 | while not self.sunshineController.isRunning() or triesLeft < 1: 92 | triesLeft -= 1 93 | time.sleep(1) 94 | res = self.sunshineController.setUser("decky_sunshine", "decky_sunshine", "decky_sunshine") 95 | self.settingManager.setSetting("lastAuthHeader", str(res)) 96 | decky_plugin.logger.info("AuthHeader set " + res) 97 | else: 98 | lastAuthHeader = self.settingManager.getSetting("lastAuthHeader", "") 99 | if(len(lastAuthHeader) > 0): 100 | self.sunshineController.setAuthHeaderRaw(lastAuthHeader) 101 | 102 | lastRunState = self.settingManager.getSetting("lastRunState", "") 103 | if(lastRunState == "start"): 104 | time.sleep(20) 105 | self.sunshineController.start() 106 | decky_plugin.logger.info("Decky Sunshine loaded") 107 | 108 | async def _unload(self): 109 | decky_plugin.logger.info("Decky Sunshine unloaded") 110 | 111 | async def _migration(self): 112 | decky_plugin.migrate_settings(str(Path(decky_plugin.DECKY_HOME) / "settings" / "decky-sunshine.json")) 113 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "decky-sunshine", 3 | "version": "0.5.0", 4 | "description": "Decky plugin to easily use Sunshine streaming in Game Mode", 5 | "scripts": { 6 | "build": "shx rm -rf dist && rollup -c", 7 | "watch": "rollup -c -w", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "s0t7x", 11 | "devDependencies": { 12 | "@rollup/plugin-commonjs": "^21.1.0", 13 | "@rollup/plugin-json": "^4.1.0", 14 | "@rollup/plugin-node-resolve": "^13.3.0", 15 | "@rollup/plugin-replace": "^4.0.0", 16 | "@rollup/plugin-typescript": "^8.3.3", 17 | "@types/react": "16.14.0", 18 | "@types/webpack": "^5.28.0", 19 | "rollup": "^2.77.1", 20 | "rollup-plugin-import-assets": "^1.1.1", 21 | "shx": "^0.3.4", 22 | "tslib": "^2.4.0", 23 | "typescript": "^4.7.4" 24 | }, 25 | "dependencies": { 26 | "decky-frontend-lib": "^3.24.5", 27 | "react-icons": "^4.4.0" 28 | }, 29 | "pnpm": { 30 | "peerDependencyRules": { 31 | "ignoreMissing": [ 32 | "react", 33 | "react-dom" 34 | ] 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Decky Sunshine", 3 | "author": "s0t7x", 4 | "flags": ["root"], 5 | "publish": { 6 | "tags": ["root", "streaming", "moonlight", "sunshine", "server"], 7 | "description": "Decky plugin to integrate Sunshine server into game mode.", 8 | "image": "https://opengraph.githubassets.com/1/s0t7x/decky-sunshine" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | dependencies: 11 | decky-frontend-lib: 12 | specifier: ^3.24.5 13 | version: 3.24.5 14 | react-icons: 15 | specifier: ^4.4.0 16 | version: 4.9.0(react@18.3.1) 17 | devDependencies: 18 | '@rollup/plugin-commonjs': 19 | specifier: ^21.1.0 20 | version: 21.1.0(rollup@2.79.1) 21 | '@rollup/plugin-json': 22 | specifier: ^4.1.0 23 | version: 4.1.0(rollup@2.79.1) 24 | '@rollup/plugin-node-resolve': 25 | specifier: ^13.3.0 26 | version: 13.3.0(rollup@2.79.1) 27 | '@rollup/plugin-replace': 28 | specifier: ^4.0.0 29 | version: 4.0.0(rollup@2.79.1) 30 | '@rollup/plugin-typescript': 31 | specifier: ^8.3.3 32 | version: 8.5.0(rollup@2.79.1)(tslib@2.5.2)(typescript@4.9.5) 33 | '@types/react': 34 | specifier: 16.14.0 35 | version: 16.14.0 36 | '@types/webpack': 37 | specifier: ^5.28.0 38 | version: 5.28.1 39 | rollup: 40 | specifier: ^2.77.1 41 | version: 2.79.1 42 | rollup-plugin-import-assets: 43 | specifier: ^1.1.1 44 | version: 1.1.1(rollup@2.79.1) 45 | shx: 46 | specifier: ^0.3.4 47 | version: 0.3.4 48 | tslib: 49 | specifier: ^2.4.0 50 | version: 2.5.2 51 | typescript: 52 | specifier: ^4.7.4 53 | version: 4.9.5 54 | 55 | packages: 56 | 57 | '@jridgewell/gen-mapping@0.3.3': 58 | resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} 59 | engines: {node: '>=6.0.0'} 60 | 61 | '@jridgewell/resolve-uri@3.1.0': 62 | resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} 63 | engines: {node: '>=6.0.0'} 64 | 65 | '@jridgewell/set-array@1.1.2': 66 | resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} 67 | engines: {node: '>=6.0.0'} 68 | 69 | '@jridgewell/source-map@0.3.3': 70 | resolution: {integrity: sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==} 71 | 72 | '@jridgewell/sourcemap-codec@1.4.15': 73 | resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} 74 | 75 | '@jridgewell/trace-mapping@0.3.25': 76 | resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} 77 | 78 | '@rollup/plugin-commonjs@21.1.0': 79 | resolution: {integrity: sha512-6ZtHx3VHIp2ReNNDxHjuUml6ur+WcQ28N1yHgCQwsbNkQg2suhxGMDQGJOn/KuDxKtd1xuZP5xSTwBA4GQ8hbA==} 80 | engines: {node: '>= 8.0.0'} 81 | peerDependencies: 82 | rollup: ^2.38.3 83 | 84 | '@rollup/plugin-json@4.1.0': 85 | resolution: {integrity: sha512-yfLbTdNS6amI/2OpmbiBoW12vngr5NW2jCJVZSBEz+H5KfUJZ2M7sDjk0U6GOOdCWFVScShte29o9NezJ53TPw==} 86 | peerDependencies: 87 | rollup: ^1.20.0 || ^2.0.0 88 | 89 | '@rollup/plugin-node-resolve@13.3.0': 90 | resolution: {integrity: sha512-Lus8rbUo1eEcnS4yTFKLZrVumLPY+YayBdWXgFSHYhTT2iJbMhoaaBL3xl5NCdeRytErGr8tZ0L71BMRmnlwSw==} 91 | engines: {node: '>= 10.0.0'} 92 | peerDependencies: 93 | rollup: ^2.42.0 94 | 95 | '@rollup/plugin-replace@4.0.0': 96 | resolution: {integrity: sha512-+rumQFiaNac9y64OHtkHGmdjm7us9bo1PlbgQfdihQtuNxzjpaB064HbRnewUOggLQxVCCyINfStkgmBeQpv1g==} 97 | peerDependencies: 98 | rollup: ^1.20.0 || ^2.0.0 99 | 100 | '@rollup/plugin-typescript@8.5.0': 101 | resolution: {integrity: sha512-wMv1/scv0m/rXx21wD2IsBbJFba8wGF3ErJIr6IKRfRj49S85Lszbxb4DCo8iILpluTjk2GAAu9CoZt4G3ppgQ==} 102 | engines: {node: '>=8.0.0'} 103 | peerDependencies: 104 | rollup: ^2.14.0 105 | tslib: '*' 106 | typescript: '>=3.7.0' 107 | peerDependenciesMeta: 108 | tslib: 109 | optional: true 110 | 111 | '@rollup/pluginutils@3.1.0': 112 | resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==} 113 | engines: {node: '>= 8.0.0'} 114 | peerDependencies: 115 | rollup: ^1.20.0||^2.0.0 116 | 117 | '@types/estree@0.0.39': 118 | resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==} 119 | 120 | '@types/estree@1.0.1': 121 | resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==} 122 | 123 | '@types/estree@1.0.5': 124 | resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} 125 | 126 | '@types/json-schema@7.0.12': 127 | resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==} 128 | 129 | '@types/node@20.2.5': 130 | resolution: {integrity: sha512-JJulVEQXmiY9Px5axXHeYGLSjhkZEnD+MDPDGbCbIAbMslkKwmygtZFy1X6s/075Yo94sf8GuSlFfPzysQrWZQ==} 131 | 132 | '@types/prop-types@15.7.5': 133 | resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} 134 | 135 | '@types/react@16.14.0': 136 | resolution: {integrity: sha512-jJjHo1uOe+NENRIBvF46tJimUvPnmbQ41Ax0pEm7pRvhPg+wuj8VMOHHiMvaGmZRzRrCtm7KnL5OOE/6kHPK8w==} 137 | 138 | '@types/resolve@1.17.1': 139 | resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==} 140 | 141 | '@types/webpack@5.28.1': 142 | resolution: {integrity: sha512-qw1MqGZclCoBrpiSe/hokSgQM/su8Ocpl3L/YHE0L6moyaypg4+5F7Uzq7NgaPKPxUxUbQ4fLPLpDWdR27bCZw==} 143 | 144 | '@webassemblyjs/ast@1.12.1': 145 | resolution: {integrity: sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==} 146 | 147 | '@webassemblyjs/floating-point-hex-parser@1.11.6': 148 | resolution: {integrity: sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==} 149 | 150 | '@webassemblyjs/helper-api-error@1.11.6': 151 | resolution: {integrity: sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==} 152 | 153 | '@webassemblyjs/helper-buffer@1.12.1': 154 | resolution: {integrity: sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==} 155 | 156 | '@webassemblyjs/helper-numbers@1.11.6': 157 | resolution: {integrity: sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==} 158 | 159 | '@webassemblyjs/helper-wasm-bytecode@1.11.6': 160 | resolution: {integrity: sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==} 161 | 162 | '@webassemblyjs/helper-wasm-section@1.12.1': 163 | resolution: {integrity: sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==} 164 | 165 | '@webassemblyjs/ieee754@1.11.6': 166 | resolution: {integrity: sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==} 167 | 168 | '@webassemblyjs/leb128@1.11.6': 169 | resolution: {integrity: sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==} 170 | 171 | '@webassemblyjs/utf8@1.11.6': 172 | resolution: {integrity: sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==} 173 | 174 | '@webassemblyjs/wasm-edit@1.12.1': 175 | resolution: {integrity: sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==} 176 | 177 | '@webassemblyjs/wasm-gen@1.12.1': 178 | resolution: {integrity: sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==} 179 | 180 | '@webassemblyjs/wasm-opt@1.12.1': 181 | resolution: {integrity: sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==} 182 | 183 | '@webassemblyjs/wasm-parser@1.12.1': 184 | resolution: {integrity: sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==} 185 | 186 | '@webassemblyjs/wast-printer@1.12.1': 187 | resolution: {integrity: sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==} 188 | 189 | '@xtuc/ieee754@1.2.0': 190 | resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} 191 | 192 | '@xtuc/long@4.2.2': 193 | resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} 194 | 195 | acorn-import-attributes@1.9.5: 196 | resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} 197 | peerDependencies: 198 | acorn: ^8 199 | 200 | acorn@8.8.2: 201 | resolution: {integrity: sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==} 202 | engines: {node: '>=0.4.0'} 203 | hasBin: true 204 | 205 | ajv-keywords@3.5.2: 206 | resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} 207 | peerDependencies: 208 | ajv: ^6.9.1 209 | 210 | ajv@6.12.6: 211 | resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} 212 | 213 | balanced-match@1.0.2: 214 | resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 215 | 216 | brace-expansion@1.1.11: 217 | resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} 218 | 219 | browserslist@4.23.3: 220 | resolution: {integrity: sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==} 221 | engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} 222 | hasBin: true 223 | 224 | buffer-from@1.1.2: 225 | resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} 226 | 227 | builtin-modules@3.3.0: 228 | resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} 229 | engines: {node: '>=6'} 230 | 231 | caniuse-lite@1.0.30001655: 232 | resolution: {integrity: sha512-jRGVy3iSGO5Uutn2owlb5gR6qsGngTw9ZTb4ali9f3glshcNmJ2noam4Mo9zia5P9Dk3jNNydy7vQjuE5dQmfg==} 233 | 234 | chrome-trace-event@1.0.3: 235 | resolution: {integrity: sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==} 236 | engines: {node: '>=6.0'} 237 | 238 | commander@2.20.3: 239 | resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} 240 | 241 | commondir@1.0.1: 242 | resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} 243 | 244 | concat-map@0.0.1: 245 | resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} 246 | 247 | csstype@3.1.2: 248 | resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} 249 | 250 | decky-frontend-lib@3.24.5: 251 | resolution: {integrity: sha512-eYlbKDOOcIBPI0b76Rqvlryq2ym/QNiry4xf2pFrXmBa1f95dflqbQAb2gTq9uHEa5gFmeV4lUcMPGJ3M14Xqw==} 252 | 253 | deepmerge@4.3.1: 254 | resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} 255 | engines: {node: '>=0.10.0'} 256 | 257 | electron-to-chromium@1.5.13: 258 | resolution: {integrity: sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q==} 259 | 260 | enhanced-resolve@5.17.1: 261 | resolution: {integrity: sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==} 262 | engines: {node: '>=10.13.0'} 263 | 264 | es-module-lexer@1.2.1: 265 | resolution: {integrity: sha512-9978wrXM50Y4rTMmW5kXIC09ZdXQZqkE4mxhwkd8VbzsGkXGPgV4zWuqQJgCEzYngdo2dYDa0l8xhX4fkSwJSg==} 266 | 267 | escalade@3.2.0: 268 | resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} 269 | engines: {node: '>=6'} 270 | 271 | eslint-scope@5.1.1: 272 | resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} 273 | engines: {node: '>=8.0.0'} 274 | 275 | esrecurse@4.3.0: 276 | resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} 277 | engines: {node: '>=4.0'} 278 | 279 | estraverse@4.3.0: 280 | resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} 281 | engines: {node: '>=4.0'} 282 | 283 | estraverse@5.3.0: 284 | resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} 285 | engines: {node: '>=4.0'} 286 | 287 | estree-walker@0.6.1: 288 | resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==} 289 | 290 | estree-walker@1.0.1: 291 | resolution: {integrity: sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==} 292 | 293 | estree-walker@2.0.2: 294 | resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} 295 | 296 | events@3.3.0: 297 | resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} 298 | engines: {node: '>=0.8.x'} 299 | 300 | fast-deep-equal@3.1.3: 301 | resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} 302 | 303 | fast-json-stable-stringify@2.1.0: 304 | resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} 305 | 306 | fs.realpath@1.0.0: 307 | resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} 308 | 309 | fsevents@2.3.3: 310 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 311 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 312 | os: [darwin] 313 | 314 | function-bind@1.1.1: 315 | resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} 316 | 317 | glob-to-regexp@0.4.1: 318 | resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} 319 | 320 | glob@7.2.3: 321 | resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} 322 | deprecated: Glob versions prior to v9 are no longer supported 323 | 324 | graceful-fs@4.2.11: 325 | resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} 326 | 327 | has-flag@4.0.0: 328 | resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} 329 | engines: {node: '>=8'} 330 | 331 | has@1.0.3: 332 | resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} 333 | engines: {node: '>= 0.4.0'} 334 | 335 | inflight@1.0.6: 336 | resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} 337 | deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. 338 | 339 | inherits@2.0.4: 340 | resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} 341 | 342 | interpret@1.4.0: 343 | resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} 344 | engines: {node: '>= 0.10'} 345 | 346 | is-builtin-module@3.2.1: 347 | resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} 348 | engines: {node: '>=6'} 349 | 350 | is-core-module@2.12.1: 351 | resolution: {integrity: sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==} 352 | 353 | is-module@1.0.0: 354 | resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} 355 | 356 | is-reference@1.2.1: 357 | resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} 358 | 359 | jest-worker@27.5.1: 360 | resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} 361 | engines: {node: '>= 10.13.0'} 362 | 363 | js-tokens@4.0.0: 364 | resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} 365 | 366 | json-parse-even-better-errors@2.3.1: 367 | resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} 368 | 369 | json-schema-traverse@0.4.1: 370 | resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} 371 | 372 | loader-runner@4.3.0: 373 | resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} 374 | engines: {node: '>=6.11.5'} 375 | 376 | loose-envify@1.4.0: 377 | resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} 378 | hasBin: true 379 | 380 | magic-string@0.25.9: 381 | resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} 382 | 383 | merge-stream@2.0.0: 384 | resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} 385 | 386 | mime-db@1.52.0: 387 | resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} 388 | engines: {node: '>= 0.6'} 389 | 390 | mime-types@2.1.35: 391 | resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} 392 | engines: {node: '>= 0.6'} 393 | 394 | minimatch@3.1.2: 395 | resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} 396 | 397 | minimist@1.2.8: 398 | resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} 399 | 400 | neo-async@2.6.2: 401 | resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} 402 | 403 | node-releases@2.0.18: 404 | resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} 405 | 406 | once@1.4.0: 407 | resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} 408 | 409 | path-is-absolute@1.0.1: 410 | resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} 411 | engines: {node: '>=0.10.0'} 412 | 413 | path-parse@1.0.7: 414 | resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} 415 | 416 | picocolors@1.0.1: 417 | resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} 418 | 419 | picomatch@2.3.1: 420 | resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} 421 | engines: {node: '>=8.6'} 422 | 423 | punycode@2.3.0: 424 | resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} 425 | engines: {node: '>=6'} 426 | 427 | randombytes@2.1.0: 428 | resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} 429 | 430 | react-icons@4.9.0: 431 | resolution: {integrity: sha512-ijUnFr//ycebOqujtqtV9PFS7JjhWg0QU6ykURVHuL4cbofvRCf3f6GMn9+fBktEFQOIVZnuAYLZdiyadRQRFg==} 432 | peerDependencies: 433 | react: '*' 434 | 435 | react@18.3.1: 436 | resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} 437 | engines: {node: '>=0.10.0'} 438 | 439 | rechoir@0.6.2: 440 | resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} 441 | engines: {node: '>= 0.10'} 442 | 443 | resolve@1.22.2: 444 | resolution: {integrity: sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==} 445 | hasBin: true 446 | 447 | rollup-plugin-import-assets@1.1.1: 448 | resolution: {integrity: sha512-u5zJwOjguTf2N+wETq2weNKGvNkuVc1UX/fPgg215p5xPvGOaI6/BTc024E9brvFjSQTfIYqgvwogQdipknu1g==} 449 | peerDependencies: 450 | rollup: '>=1.9.0' 451 | 452 | rollup-pluginutils@2.8.2: 453 | resolution: {integrity: sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==} 454 | 455 | rollup@2.79.1: 456 | resolution: {integrity: sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==} 457 | engines: {node: '>=10.0.0'} 458 | hasBin: true 459 | 460 | safe-buffer@5.2.1: 461 | resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} 462 | 463 | schema-utils@3.3.0: 464 | resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} 465 | engines: {node: '>= 10.13.0'} 466 | 467 | serialize-javascript@6.0.1: 468 | resolution: {integrity: sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==} 469 | 470 | shelljs@0.8.5: 471 | resolution: {integrity: sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==} 472 | engines: {node: '>=4'} 473 | hasBin: true 474 | 475 | shx@0.3.4: 476 | resolution: {integrity: sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g==} 477 | engines: {node: '>=6'} 478 | hasBin: true 479 | 480 | source-map-support@0.5.21: 481 | resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} 482 | 483 | source-map@0.6.1: 484 | resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} 485 | engines: {node: '>=0.10.0'} 486 | 487 | sourcemap-codec@1.4.8: 488 | resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} 489 | deprecated: Please use @jridgewell/sourcemap-codec instead 490 | 491 | supports-color@8.1.1: 492 | resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} 493 | engines: {node: '>=10'} 494 | 495 | supports-preserve-symlinks-flag@1.0.0: 496 | resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} 497 | engines: {node: '>= 0.4'} 498 | 499 | tapable@2.2.1: 500 | resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} 501 | engines: {node: '>=6'} 502 | 503 | terser-webpack-plugin@5.3.10: 504 | resolution: {integrity: sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==} 505 | engines: {node: '>= 10.13.0'} 506 | peerDependencies: 507 | '@swc/core': '*' 508 | esbuild: '*' 509 | uglify-js: '*' 510 | webpack: ^5.1.0 511 | peerDependenciesMeta: 512 | '@swc/core': 513 | optional: true 514 | esbuild: 515 | optional: true 516 | uglify-js: 517 | optional: true 518 | 519 | terser@5.31.6: 520 | resolution: {integrity: sha512-PQ4DAriWzKj+qgehQ7LK5bQqCFNMmlhjR2PFFLuqGCpuCAauxemVBWwWOxo3UIwWQx8+Pr61Df++r76wDmkQBg==} 521 | engines: {node: '>=10'} 522 | hasBin: true 523 | 524 | tslib@2.5.2: 525 | resolution: {integrity: sha512-5svOrSA2w3iGFDs1HibEVBGbDrAY82bFQ3HZ3ixB+88nsbsWQoKqDRb5UBYAUPEzbBn6dAp5gRNXglySbx1MlA==} 526 | 527 | typescript@4.9.5: 528 | resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} 529 | engines: {node: '>=4.2.0'} 530 | hasBin: true 531 | 532 | update-browserslist-db@1.1.0: 533 | resolution: {integrity: sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==} 534 | hasBin: true 535 | peerDependencies: 536 | browserslist: '>= 4.21.0' 537 | 538 | uri-js@4.4.1: 539 | resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} 540 | 541 | url-join@4.0.1: 542 | resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==} 543 | 544 | watchpack@2.4.2: 545 | resolution: {integrity: sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==} 546 | engines: {node: '>=10.13.0'} 547 | 548 | webpack-sources@3.2.3: 549 | resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} 550 | engines: {node: '>=10.13.0'} 551 | 552 | webpack@5.94.0: 553 | resolution: {integrity: sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==} 554 | engines: {node: '>=10.13.0'} 555 | hasBin: true 556 | peerDependencies: 557 | webpack-cli: '*' 558 | peerDependenciesMeta: 559 | webpack-cli: 560 | optional: true 561 | 562 | wrappy@1.0.2: 563 | resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} 564 | 565 | snapshots: 566 | 567 | '@jridgewell/gen-mapping@0.3.3': 568 | dependencies: 569 | '@jridgewell/set-array': 1.1.2 570 | '@jridgewell/sourcemap-codec': 1.4.15 571 | '@jridgewell/trace-mapping': 0.3.25 572 | 573 | '@jridgewell/resolve-uri@3.1.0': {} 574 | 575 | '@jridgewell/set-array@1.1.2': {} 576 | 577 | '@jridgewell/source-map@0.3.3': 578 | dependencies: 579 | '@jridgewell/gen-mapping': 0.3.3 580 | '@jridgewell/trace-mapping': 0.3.25 581 | 582 | '@jridgewell/sourcemap-codec@1.4.15': {} 583 | 584 | '@jridgewell/trace-mapping@0.3.25': 585 | dependencies: 586 | '@jridgewell/resolve-uri': 3.1.0 587 | '@jridgewell/sourcemap-codec': 1.4.15 588 | 589 | '@rollup/plugin-commonjs@21.1.0(rollup@2.79.1)': 590 | dependencies: 591 | '@rollup/pluginutils': 3.1.0(rollup@2.79.1) 592 | commondir: 1.0.1 593 | estree-walker: 2.0.2 594 | glob: 7.2.3 595 | is-reference: 1.2.1 596 | magic-string: 0.25.9 597 | resolve: 1.22.2 598 | rollup: 2.79.1 599 | 600 | '@rollup/plugin-json@4.1.0(rollup@2.79.1)': 601 | dependencies: 602 | '@rollup/pluginutils': 3.1.0(rollup@2.79.1) 603 | rollup: 2.79.1 604 | 605 | '@rollup/plugin-node-resolve@13.3.0(rollup@2.79.1)': 606 | dependencies: 607 | '@rollup/pluginutils': 3.1.0(rollup@2.79.1) 608 | '@types/resolve': 1.17.1 609 | deepmerge: 4.3.1 610 | is-builtin-module: 3.2.1 611 | is-module: 1.0.0 612 | resolve: 1.22.2 613 | rollup: 2.79.1 614 | 615 | '@rollup/plugin-replace@4.0.0(rollup@2.79.1)': 616 | dependencies: 617 | '@rollup/pluginutils': 3.1.0(rollup@2.79.1) 618 | magic-string: 0.25.9 619 | rollup: 2.79.1 620 | 621 | '@rollup/plugin-typescript@8.5.0(rollup@2.79.1)(tslib@2.5.2)(typescript@4.9.5)': 622 | dependencies: 623 | '@rollup/pluginutils': 3.1.0(rollup@2.79.1) 624 | resolve: 1.22.2 625 | rollup: 2.79.1 626 | typescript: 4.9.5 627 | optionalDependencies: 628 | tslib: 2.5.2 629 | 630 | '@rollup/pluginutils@3.1.0(rollup@2.79.1)': 631 | dependencies: 632 | '@types/estree': 0.0.39 633 | estree-walker: 1.0.1 634 | picomatch: 2.3.1 635 | rollup: 2.79.1 636 | 637 | '@types/estree@0.0.39': {} 638 | 639 | '@types/estree@1.0.1': {} 640 | 641 | '@types/estree@1.0.5': {} 642 | 643 | '@types/json-schema@7.0.12': {} 644 | 645 | '@types/node@20.2.5': {} 646 | 647 | '@types/prop-types@15.7.5': {} 648 | 649 | '@types/react@16.14.0': 650 | dependencies: 651 | '@types/prop-types': 15.7.5 652 | csstype: 3.1.2 653 | 654 | '@types/resolve@1.17.1': 655 | dependencies: 656 | '@types/node': 20.2.5 657 | 658 | '@types/webpack@5.28.1': 659 | dependencies: 660 | '@types/node': 20.2.5 661 | tapable: 2.2.1 662 | webpack: 5.94.0 663 | transitivePeerDependencies: 664 | - '@swc/core' 665 | - esbuild 666 | - uglify-js 667 | - webpack-cli 668 | 669 | '@webassemblyjs/ast@1.12.1': 670 | dependencies: 671 | '@webassemblyjs/helper-numbers': 1.11.6 672 | '@webassemblyjs/helper-wasm-bytecode': 1.11.6 673 | 674 | '@webassemblyjs/floating-point-hex-parser@1.11.6': {} 675 | 676 | '@webassemblyjs/helper-api-error@1.11.6': {} 677 | 678 | '@webassemblyjs/helper-buffer@1.12.1': {} 679 | 680 | '@webassemblyjs/helper-numbers@1.11.6': 681 | dependencies: 682 | '@webassemblyjs/floating-point-hex-parser': 1.11.6 683 | '@webassemblyjs/helper-api-error': 1.11.6 684 | '@xtuc/long': 4.2.2 685 | 686 | '@webassemblyjs/helper-wasm-bytecode@1.11.6': {} 687 | 688 | '@webassemblyjs/helper-wasm-section@1.12.1': 689 | dependencies: 690 | '@webassemblyjs/ast': 1.12.1 691 | '@webassemblyjs/helper-buffer': 1.12.1 692 | '@webassemblyjs/helper-wasm-bytecode': 1.11.6 693 | '@webassemblyjs/wasm-gen': 1.12.1 694 | 695 | '@webassemblyjs/ieee754@1.11.6': 696 | dependencies: 697 | '@xtuc/ieee754': 1.2.0 698 | 699 | '@webassemblyjs/leb128@1.11.6': 700 | dependencies: 701 | '@xtuc/long': 4.2.2 702 | 703 | '@webassemblyjs/utf8@1.11.6': {} 704 | 705 | '@webassemblyjs/wasm-edit@1.12.1': 706 | dependencies: 707 | '@webassemblyjs/ast': 1.12.1 708 | '@webassemblyjs/helper-buffer': 1.12.1 709 | '@webassemblyjs/helper-wasm-bytecode': 1.11.6 710 | '@webassemblyjs/helper-wasm-section': 1.12.1 711 | '@webassemblyjs/wasm-gen': 1.12.1 712 | '@webassemblyjs/wasm-opt': 1.12.1 713 | '@webassemblyjs/wasm-parser': 1.12.1 714 | '@webassemblyjs/wast-printer': 1.12.1 715 | 716 | '@webassemblyjs/wasm-gen@1.12.1': 717 | dependencies: 718 | '@webassemblyjs/ast': 1.12.1 719 | '@webassemblyjs/helper-wasm-bytecode': 1.11.6 720 | '@webassemblyjs/ieee754': 1.11.6 721 | '@webassemblyjs/leb128': 1.11.6 722 | '@webassemblyjs/utf8': 1.11.6 723 | 724 | '@webassemblyjs/wasm-opt@1.12.1': 725 | dependencies: 726 | '@webassemblyjs/ast': 1.12.1 727 | '@webassemblyjs/helper-buffer': 1.12.1 728 | '@webassemblyjs/wasm-gen': 1.12.1 729 | '@webassemblyjs/wasm-parser': 1.12.1 730 | 731 | '@webassemblyjs/wasm-parser@1.12.1': 732 | dependencies: 733 | '@webassemblyjs/ast': 1.12.1 734 | '@webassemblyjs/helper-api-error': 1.11.6 735 | '@webassemblyjs/helper-wasm-bytecode': 1.11.6 736 | '@webassemblyjs/ieee754': 1.11.6 737 | '@webassemblyjs/leb128': 1.11.6 738 | '@webassemblyjs/utf8': 1.11.6 739 | 740 | '@webassemblyjs/wast-printer@1.12.1': 741 | dependencies: 742 | '@webassemblyjs/ast': 1.12.1 743 | '@xtuc/long': 4.2.2 744 | 745 | '@xtuc/ieee754@1.2.0': {} 746 | 747 | '@xtuc/long@4.2.2': {} 748 | 749 | acorn-import-attributes@1.9.5(acorn@8.8.2): 750 | dependencies: 751 | acorn: 8.8.2 752 | 753 | acorn@8.8.2: {} 754 | 755 | ajv-keywords@3.5.2(ajv@6.12.6): 756 | dependencies: 757 | ajv: 6.12.6 758 | 759 | ajv@6.12.6: 760 | dependencies: 761 | fast-deep-equal: 3.1.3 762 | fast-json-stable-stringify: 2.1.0 763 | json-schema-traverse: 0.4.1 764 | uri-js: 4.4.1 765 | 766 | balanced-match@1.0.2: {} 767 | 768 | brace-expansion@1.1.11: 769 | dependencies: 770 | balanced-match: 1.0.2 771 | concat-map: 0.0.1 772 | 773 | browserslist@4.23.3: 774 | dependencies: 775 | caniuse-lite: 1.0.30001655 776 | electron-to-chromium: 1.5.13 777 | node-releases: 2.0.18 778 | update-browserslist-db: 1.1.0(browserslist@4.23.3) 779 | 780 | buffer-from@1.1.2: {} 781 | 782 | builtin-modules@3.3.0: {} 783 | 784 | caniuse-lite@1.0.30001655: {} 785 | 786 | chrome-trace-event@1.0.3: {} 787 | 788 | commander@2.20.3: {} 789 | 790 | commondir@1.0.1: {} 791 | 792 | concat-map@0.0.1: {} 793 | 794 | csstype@3.1.2: {} 795 | 796 | decky-frontend-lib@3.24.5: {} 797 | 798 | deepmerge@4.3.1: {} 799 | 800 | electron-to-chromium@1.5.13: {} 801 | 802 | enhanced-resolve@5.17.1: 803 | dependencies: 804 | graceful-fs: 4.2.11 805 | tapable: 2.2.1 806 | 807 | es-module-lexer@1.2.1: {} 808 | 809 | escalade@3.2.0: {} 810 | 811 | eslint-scope@5.1.1: 812 | dependencies: 813 | esrecurse: 4.3.0 814 | estraverse: 4.3.0 815 | 816 | esrecurse@4.3.0: 817 | dependencies: 818 | estraverse: 5.3.0 819 | 820 | estraverse@4.3.0: {} 821 | 822 | estraverse@5.3.0: {} 823 | 824 | estree-walker@0.6.1: {} 825 | 826 | estree-walker@1.0.1: {} 827 | 828 | estree-walker@2.0.2: {} 829 | 830 | events@3.3.0: {} 831 | 832 | fast-deep-equal@3.1.3: {} 833 | 834 | fast-json-stable-stringify@2.1.0: {} 835 | 836 | fs.realpath@1.0.0: {} 837 | 838 | fsevents@2.3.3: 839 | optional: true 840 | 841 | function-bind@1.1.1: {} 842 | 843 | glob-to-regexp@0.4.1: {} 844 | 845 | glob@7.2.3: 846 | dependencies: 847 | fs.realpath: 1.0.0 848 | inflight: 1.0.6 849 | inherits: 2.0.4 850 | minimatch: 3.1.2 851 | once: 1.4.0 852 | path-is-absolute: 1.0.1 853 | 854 | graceful-fs@4.2.11: {} 855 | 856 | has-flag@4.0.0: {} 857 | 858 | has@1.0.3: 859 | dependencies: 860 | function-bind: 1.1.1 861 | 862 | inflight@1.0.6: 863 | dependencies: 864 | once: 1.4.0 865 | wrappy: 1.0.2 866 | 867 | inherits@2.0.4: {} 868 | 869 | interpret@1.4.0: {} 870 | 871 | is-builtin-module@3.2.1: 872 | dependencies: 873 | builtin-modules: 3.3.0 874 | 875 | is-core-module@2.12.1: 876 | dependencies: 877 | has: 1.0.3 878 | 879 | is-module@1.0.0: {} 880 | 881 | is-reference@1.2.1: 882 | dependencies: 883 | '@types/estree': 1.0.1 884 | 885 | jest-worker@27.5.1: 886 | dependencies: 887 | '@types/node': 20.2.5 888 | merge-stream: 2.0.0 889 | supports-color: 8.1.1 890 | 891 | js-tokens@4.0.0: {} 892 | 893 | json-parse-even-better-errors@2.3.1: {} 894 | 895 | json-schema-traverse@0.4.1: {} 896 | 897 | loader-runner@4.3.0: {} 898 | 899 | loose-envify@1.4.0: 900 | dependencies: 901 | js-tokens: 4.0.0 902 | 903 | magic-string@0.25.9: 904 | dependencies: 905 | sourcemap-codec: 1.4.8 906 | 907 | merge-stream@2.0.0: {} 908 | 909 | mime-db@1.52.0: {} 910 | 911 | mime-types@2.1.35: 912 | dependencies: 913 | mime-db: 1.52.0 914 | 915 | minimatch@3.1.2: 916 | dependencies: 917 | brace-expansion: 1.1.11 918 | 919 | minimist@1.2.8: {} 920 | 921 | neo-async@2.6.2: {} 922 | 923 | node-releases@2.0.18: {} 924 | 925 | once@1.4.0: 926 | dependencies: 927 | wrappy: 1.0.2 928 | 929 | path-is-absolute@1.0.1: {} 930 | 931 | path-parse@1.0.7: {} 932 | 933 | picocolors@1.0.1: {} 934 | 935 | picomatch@2.3.1: {} 936 | 937 | punycode@2.3.0: {} 938 | 939 | randombytes@2.1.0: 940 | dependencies: 941 | safe-buffer: 5.2.1 942 | 943 | react-icons@4.9.0(react@18.3.1): 944 | dependencies: 945 | react: 18.3.1 946 | 947 | react@18.3.1: 948 | dependencies: 949 | loose-envify: 1.4.0 950 | 951 | rechoir@0.6.2: 952 | dependencies: 953 | resolve: 1.22.2 954 | 955 | resolve@1.22.2: 956 | dependencies: 957 | is-core-module: 2.12.1 958 | path-parse: 1.0.7 959 | supports-preserve-symlinks-flag: 1.0.0 960 | 961 | rollup-plugin-import-assets@1.1.1(rollup@2.79.1): 962 | dependencies: 963 | rollup: 2.79.1 964 | rollup-pluginutils: 2.8.2 965 | url-join: 4.0.1 966 | 967 | rollup-pluginutils@2.8.2: 968 | dependencies: 969 | estree-walker: 0.6.1 970 | 971 | rollup@2.79.1: 972 | optionalDependencies: 973 | fsevents: 2.3.3 974 | 975 | safe-buffer@5.2.1: {} 976 | 977 | schema-utils@3.3.0: 978 | dependencies: 979 | '@types/json-schema': 7.0.12 980 | ajv: 6.12.6 981 | ajv-keywords: 3.5.2(ajv@6.12.6) 982 | 983 | serialize-javascript@6.0.1: 984 | dependencies: 985 | randombytes: 2.1.0 986 | 987 | shelljs@0.8.5: 988 | dependencies: 989 | glob: 7.2.3 990 | interpret: 1.4.0 991 | rechoir: 0.6.2 992 | 993 | shx@0.3.4: 994 | dependencies: 995 | minimist: 1.2.8 996 | shelljs: 0.8.5 997 | 998 | source-map-support@0.5.21: 999 | dependencies: 1000 | buffer-from: 1.1.2 1001 | source-map: 0.6.1 1002 | 1003 | source-map@0.6.1: {} 1004 | 1005 | sourcemap-codec@1.4.8: {} 1006 | 1007 | supports-color@8.1.1: 1008 | dependencies: 1009 | has-flag: 4.0.0 1010 | 1011 | supports-preserve-symlinks-flag@1.0.0: {} 1012 | 1013 | tapable@2.2.1: {} 1014 | 1015 | terser-webpack-plugin@5.3.10(webpack@5.94.0): 1016 | dependencies: 1017 | '@jridgewell/trace-mapping': 0.3.25 1018 | jest-worker: 27.5.1 1019 | schema-utils: 3.3.0 1020 | serialize-javascript: 6.0.1 1021 | terser: 5.31.6 1022 | webpack: 5.94.0 1023 | 1024 | terser@5.31.6: 1025 | dependencies: 1026 | '@jridgewell/source-map': 0.3.3 1027 | acorn: 8.8.2 1028 | commander: 2.20.3 1029 | source-map-support: 0.5.21 1030 | 1031 | tslib@2.5.2: {} 1032 | 1033 | typescript@4.9.5: {} 1034 | 1035 | update-browserslist-db@1.1.0(browserslist@4.23.3): 1036 | dependencies: 1037 | browserslist: 4.23.3 1038 | escalade: 3.2.0 1039 | picocolors: 1.0.1 1040 | 1041 | uri-js@4.4.1: 1042 | dependencies: 1043 | punycode: 2.3.0 1044 | 1045 | url-join@4.0.1: {} 1046 | 1047 | watchpack@2.4.2: 1048 | dependencies: 1049 | glob-to-regexp: 0.4.1 1050 | graceful-fs: 4.2.11 1051 | 1052 | webpack-sources@3.2.3: {} 1053 | 1054 | webpack@5.94.0: 1055 | dependencies: 1056 | '@types/estree': 1.0.5 1057 | '@webassemblyjs/ast': 1.12.1 1058 | '@webassemblyjs/wasm-edit': 1.12.1 1059 | '@webassemblyjs/wasm-parser': 1.12.1 1060 | acorn: 8.8.2 1061 | acorn-import-attributes: 1.9.5(acorn@8.8.2) 1062 | browserslist: 4.23.3 1063 | chrome-trace-event: 1.0.3 1064 | enhanced-resolve: 5.17.1 1065 | es-module-lexer: 1.2.1 1066 | eslint-scope: 5.1.1 1067 | events: 3.3.0 1068 | glob-to-regexp: 0.4.1 1069 | graceful-fs: 4.2.11 1070 | json-parse-even-better-errors: 2.3.1 1071 | loader-runner: 4.3.0 1072 | mime-types: 2.1.35 1073 | neo-async: 2.6.2 1074 | schema-utils: 3.3.0 1075 | tapable: 2.2.1 1076 | terser-webpack-plugin: 5.3.10(webpack@5.94.0) 1077 | watchpack: 2.4.2 1078 | webpack-sources: 3.2.3 1079 | transitivePeerDependencies: 1080 | - '@swc/core' 1081 | - esbuild 1082 | - uglify-js 1083 | 1084 | wrappy@1.0.2: {} 1085 | -------------------------------------------------------------------------------- /py_modules/sunshine.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import os 3 | import signal 4 | import base64 5 | import json 6 | import ssl 7 | 8 | from urllib.request import Request, HTTPError, HTTPRedirectHandler, build_opener, HTTPSHandler 9 | from http.client import OK, UNAUTHORIZED 10 | 11 | def killpg(group) -> None: 12 | """ 13 | Kill a process group by sending a SIGTERM signal. 14 | :param group: The process group ID 15 | """ 16 | try: 17 | os.killpg(group, signal.SIGTERM) 18 | except: 19 | return 20 | 21 | def kill(pid) -> None: 22 | """ 23 | Kill a process by sending a SIGTERM signal. 24 | :param pid: The process ID 25 | """ 26 | try: 27 | os.kill(pid, signal.SIGTERM) 28 | except: 29 | return 30 | 31 | def createRequest(path, authHeader, data=None) -> Request: 32 | """ 33 | Create a Request with necessary headers and set the data accordingly. 34 | :param path: The path of the request 35 | :param authHeader: The authorization header data for the request 36 | :param data: The data to send to the server 37 | """ 38 | sunshineBaseUrl = "https://127.0.0.1:47990" 39 | url = sunshineBaseUrl + path 40 | request = Request(url) 41 | request.add_header("User-Agent", "decky-sunshine") 42 | request.add_header("Connection", "keep-alive") 43 | request.add_header("Accept", "application/json, */*; q=0.01") 44 | request.add_header("Authorization", authHeader) 45 | if data: 46 | request.add_header("Content-Type", "application/json") 47 | request.data = json.dumps(data).encode('utf-8') 48 | return request 49 | 50 | class NoRedirect(HTTPRedirectHandler): 51 | def redirect_request(self, req, fp, code, msg, headers, newurl): 52 | return None 53 | 54 | class SunshineController: 55 | shellHandle = None 56 | controllerStore = None 57 | isFreshInstallation = False 58 | logger = None 59 | 60 | authHeader = "" 61 | 62 | def __init__(self, logger) -> None: 63 | """ 64 | Initialize the SunshineController instance. 65 | """ 66 | assert logger is not None 67 | self.logger = logger 68 | 69 | sslContext = ssl.create_default_context() 70 | sslContext.check_hostname = False 71 | sslContext.verify_mode = ssl.CERT_NONE 72 | 73 | self.opener = build_opener(NoRedirect(), HTTPSHandler(context=sslContext)) 74 | 75 | self.environment_variables = os.environ.copy() 76 | self.environment_variables["PULSE_SERVER"] = "unix:/run/user/1000/pulse/native" 77 | self.environment_variables["DISPLAY"] = ":0" 78 | self.environment_variables["FLATPAK_BWRAP"] = self.environment_variables["DECKY_PLUGIN_RUNTIME_DIR"] + "/bwrap" 79 | self.environment_variables["LD_LIBRARY_PATH"] = "/usr/lib/:" + self.environment_variables["LD_LIBRARY_PATH"] 80 | 81 | def killShell(self) -> None: 82 | """ 83 | Kill the shell process if it exists. 84 | """ 85 | if self.shellHandle is not None: 86 | killpg(os.getpgid(self.shellHandle.pid)) 87 | self.shellHandle = None 88 | 89 | def killSunshine(self) -> None: 90 | """ 91 | Kill the Sunshine process if it exists. 92 | """ 93 | pid = self.getSunshinePID() 94 | if pid: 95 | kill(pid) 96 | 97 | def getSunshinePID(self) -> int: 98 | """ 99 | Get the process ID of the Sunshine process. 100 | :return: The process ID or None if not found 101 | """ 102 | result = subprocess.run(["pgrep", "-x", "sunshine"], capture_output=True, text=True) 103 | if result.returncode == 0: 104 | sunshinePids = [int(pid) for pid in result.stdout.split()] 105 | return sunshinePids[0] 106 | return None 107 | 108 | def setAuthHeader(self, username, password) -> str: 109 | """ 110 | Set the authentication header for the controller. 111 | :param username: The username for authentication 112 | :param password: The password for authentication 113 | """ 114 | if (len(username) + len(password) < 1): 115 | return "" 116 | credentials = f"{username}:{password}" 117 | base64_credentials = base64.b64encode(credentials.encode('utf-8')) 118 | auth_header = f"Basic {base64_credentials.decode('utf-8')}" 119 | return self.setAuthHeaderRaw(auth_header) 120 | 121 | def setAuthHeaderRaw(self, authHeader) -> str: 122 | self.authHeader = str(authHeader) 123 | return self.authHeader 124 | 125 | def request(self, path, data=None) -> str | None: 126 | """ 127 | Make an HTTP request to the Sunshine server. 128 | :param path: The path of the request 129 | :param data: The request data (optional) 130 | :return: The response data as a string 131 | """ 132 | try: 133 | request = createRequest(path, self.authHeader, data) 134 | with self.opener.open(request) as response: 135 | if response.getcode() != OK: 136 | return None 137 | encoding = response.headers.get_content_charset() or "utf-8" 138 | content = response.read().decode(encoding) 139 | return content 140 | except Exception as e: 141 | self.logger.info(f"Exception in request to path '{path}' with data '{data}', exception: {e}") 142 | return None 143 | 144 | def isRunning(self) -> bool: 145 | """ 146 | Check if the Sunshine process is running. 147 | :return: True if the process is running, False otherwise 148 | """ 149 | return self.getSunshinePID() is not None 150 | 151 | def isAuthorized(self) -> bool: 152 | """ 153 | Check if the controller is authorized to access the Sunshine server. 154 | :return: True if authorized, False otherwise 155 | """ 156 | if not self.isRunning: 157 | return False 158 | try: 159 | request = createRequest("/api/apps", self.authHeader) 160 | with self.opener.open(request) as response: 161 | return response.getcode() == OK 162 | except Exception as e: 163 | if not (isinstance(e, HTTPError) and e.code == UNAUTHORIZED): 164 | self.logger.info(f"Exception when checking authorization status, exception: {e}") 165 | return False 166 | 167 | def start(self) -> bool: 168 | """ 169 | Start the Sunshine process. 170 | """ 171 | if self.isRunning(): 172 | return True 173 | 174 | # Set the permissions for our bwrap 175 | try: 176 | self.shellHandle = subprocess.Popen(['chown', '0:0', self.environment_variables["FLATPAK_BWRAP"]], env=self.environment_variables, user=0, stdin=subprocess.PIPE, stdout=subprocess.PIPE, preexec_fn=os.setsid) 177 | except Exception as e: 178 | self.logger.info(f"An error occurred wwith bwrap chown: {e}") 179 | self.shellHandle = None 180 | return False 181 | try: 182 | _ = subprocess.Popen(['chmod', 'u+s', self.environment_variables["FLATPAK_BWRAP"]], env=self.environment_variables, user=0, stdin=subprocess.PIPE, stdout=subprocess.PIPE, preexec_fn=os.setsid) 183 | except Exception as e: 184 | self.logger.info(f"An error occurred with bwrap chmod: {e}") 185 | self.shellHandle = None 186 | return False 187 | 188 | # Run Sunshine 189 | try: 190 | _ = subprocess.Popen("sh -c 'flatpak run --socket=wayland dev.lizardbyte.app.Sunshine'", env=self.environment_variables, user=0, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, preexec_fn=os.setsid) 191 | except subprocess.TimeoutExpired: 192 | self.logger.error("Sunshine start took too long") 193 | return False 194 | except Exception as e: 195 | self.logger.info(f"An error occurred while starting Sunshine: {e}") 196 | self.shellHandle = None 197 | return False 198 | return True 199 | 200 | def stop(self) -> None: 201 | """ 202 | Stop the Sunshine process and shell process. 203 | """ 204 | self.killShell() 205 | self.killSunshine() 206 | 207 | def sendPin(self, pin) -> bool: 208 | """ 209 | Send a PIN to the Sunshine server. 210 | :param pin: The PIN to send 211 | :return: True if the PIN was accepted, False otherwise 212 | """ 213 | res = self.request("/api/pin", { "pin": pin, "name": "Deck Sunshine Friendly Client Name" }) 214 | if not res: 215 | return False 216 | try: 217 | data = json.loads(res) 218 | return data["status"] == "true" 219 | except Exception as e: 220 | self.logger.info(f"Exception when sending pin, exception: {e}") 221 | return False 222 | 223 | def ensureDependencies(self) -> bool: 224 | """ 225 | Ensure that Sunshine and the environment are set up as expected, and 226 | """ 227 | if self._isBwrapInstalled(): 228 | self.logger.info("Decky Sunshine's copy of bwrap was already obtained.") 229 | else: 230 | self.logger.info("Decky Sunshine's copy of bwrap is missing. Obtaining now...'") 231 | installed = self._installBwrap() 232 | if not installed: 233 | self.logger.info("Decky Sunshine's copy of bwrap could not be obtained.") 234 | return False 235 | self.logger.info("Decky Sunshine's copy of bwrap obtained successfully.") 236 | 237 | if self._isSunshineInstalled(): 238 | self.logger.info("Sunshine already installed.") 239 | else: 240 | self.logger.info("Sunshine not installed. Installing...") 241 | installed = self._installSunshine() 242 | if not installed: 243 | self.logger.info("Sunshine could not be installed.") 244 | return False 245 | self.logger.info("Sunshine was installed successfully.") 246 | self.isFreshInstallation = True 247 | 248 | return True 249 | 250 | 251 | def _isSunshineInstalled(self) -> bool: 252 | # flatpak list --system | grep Sunshine 253 | try: 254 | child = subprocess.Popen(["flatpak", "list", "--system"], env=self.environment_variables, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 255 | response, _ = child.communicate() 256 | response = response.decode("utf-8") # Decode the bytes output to a string 257 | for app in response.split("\n"): 258 | if "Sunshine" in app: 259 | return True 260 | return False 261 | except: 262 | return False 263 | 264 | def _isBwrapInstalled(self) -> bool: 265 | # Look for our own copy of bwrap 266 | try: 267 | return os.path.isfile(self.environment_variables["FLATPAK_BWRAP"]) 268 | except Exception as e: 269 | return False 270 | 271 | def _installSunshine(self) -> bool: 272 | try: 273 | child = subprocess.Popen(["flatpak", "install", "--system", "-y", "dev.lizardbyte.app.Sunshine"], env=self.environment_variables, 274 | stdout=subprocess.PIPE, stderr=subprocess.PIPE) 275 | _, _ = child.communicate() 276 | 277 | return child.returncode == 0 278 | except Exception as e: 279 | self.logger.info(f"An exception occurred while installing Sunshine: {e}") 280 | return False 281 | 282 | def _installBwrap(self) -> bool: 283 | try: 284 | child = subprocess.Popen(["cp", "/usr/bin/bwrap", self.environment_variables["FLATPAK_BWRAP"]], env=self.environment_variables, 285 | stdout=subprocess.PIPE, stderr=subprocess.PIPE) 286 | _, _ = child.communicate() 287 | 288 | return child.returncode == 0 289 | except Exception as e: 290 | self.logger.info(f"An exception occurred while obtaining bwrap: {e}") 291 | return False 292 | 293 | def setUser(self, newUsername, newPassword, confirmNewPassword, currentUsername = None, currentPassword = None) -> str: 294 | data = { "newUsername": newUsername, "newPassword": newPassword, "confirmNewPassword": confirmNewPassword } 295 | 296 | if(currentUsername or currentPassword): 297 | data += { "currentUsername": currentUsername, "currentPassword": currentPassword } 298 | 299 | res = self.request("/api/password", data) 300 | 301 | if len(res) <= 0: 302 | return None 303 | 304 | try: 305 | data = json.loads(res) 306 | wasUserChanged = data["status"] == "true" 307 | except: 308 | return None 309 | 310 | if not wasUserChanged: 311 | return None 312 | 313 | return self.setAuthHeader(newUsername, newPassword) 314 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from '@rollup/plugin-commonjs'; 2 | import json from '@rollup/plugin-json'; 3 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 4 | import replace from '@rollup/plugin-replace'; 5 | import typescript from '@rollup/plugin-typescript'; 6 | import { defineConfig } from 'rollup'; 7 | import importAssets from 'rollup-plugin-import-assets'; 8 | 9 | import { name } from "./plugin.json"; 10 | 11 | export default defineConfig({ 12 | input: './src/index.tsx', 13 | plugins: [ 14 | commonjs(), 15 | nodeResolve(), 16 | typescript(), 17 | json(), 18 | replace({ 19 | preventAssignment: false, 20 | 'process.env.NODE_ENV': JSON.stringify('production'), 21 | }), 22 | importAssets({ 23 | publicPath: `http://127.0.0.1:1337/plugins/${name}/` 24 | }) 25 | ], 26 | context: 'window', 27 | external: ["react", "react-dom", "decky-frontend-lib"], 28 | output: { 29 | file: "dist/index.js", 30 | globals: { 31 | react: "SP_REACT", 32 | "react-dom": "SP_REACTDOM", 33 | "decky-frontend-lib": "DFL" 34 | }, 35 | format: 'iife', 36 | exports: 'default', 37 | }, 38 | }); 39 | -------------------------------------------------------------------------------- /src/components/PINInput.tsx: -------------------------------------------------------------------------------- 1 | // Basic React component that renders a numeric input field 2 | 3 | import React from "react"; 4 | import { PanelSectionRow, gamepadDialogClasses, joinClassNames, DialogButton, Focusable } from "decky-frontend-lib"; 5 | 6 | import { playSound } from "../util/util"; 7 | 8 | const FieldWithSeparator = joinClassNames(gamepadDialogClasses.Field, gamepadDialogClasses.WithBottomSeparatorStandard); 9 | 10 | // NumericInputProps interface with value and onChange properties 11 | interface PINInputProps { 12 | label: string; 13 | value: string; 14 | onChange: (value: string) => void; 15 | onSend: (value: string) => void; 16 | sendLabel: string; 17 | } 18 | 19 | export const PINInput = (props: PINInputProps): JSX.Element => { 20 | const { label, value, onChange, onSend, sendLabel } = props; 21 | 22 | const enterDigit = (digit: string) => { 23 | //Concat the digit to the current value 24 | if (value.length == 4) { 25 | playSound("https://steamloopback.host/sounds/deck_ui_default_activation.wav"); 26 | return 27 | } 28 | let newValue = value + digit; 29 | 30 | // setvalue(newValue); 31 | onChange(newValue); 32 | 33 | playSound("https://steamloopback.host/sounds/deck_ui_misc_10.wav"); 34 | } 35 | 36 | const backspace = () => { 37 | playSound("https://steamloopback.host/sounds/deck_ui_misc_10.wav"); 38 | if (value.length > 1) { 39 | //Remove the last digit from the current value 40 | const newValue = value.slice(0, -1); 41 | // setvalue(newValue); 42 | onChange(newValue); 43 | } 44 | else { 45 | //Clear the current value 46 | // setvalue(""); 47 | onChange(""); 48 | } 49 | } 50 | 51 | const sendPin = () => { 52 | playSound("https://steamloopback.host/sounds/deck_ui_side_menu_fly_in.wav"); 53 | onSend(value) 54 | } 55 | 56 | return ( 57 | 58 | 59 |
60 |
63 |
67 | {label} 68 |
69 |
73 | {value} 74 |
75 |
76 |
77 |
78 | 79 | 80 | 81 | {/* Override min-width for DialogButtons */} 82 | 87 | 88 | {/* 3x4 Digit Grid */} 89 | 90 | enterDigit("7")}>7 91 | enterDigit("8")}>8 92 | enterDigit("9")}>9 93 | 94 | enterDigit("4")}>4 95 | enterDigit("5")}>5 96 | enterDigit("6")}>6 97 | 98 | enterDigit("1")}>1 99 | enterDigit("2")}>2 100 | enterDigit("3")}>3 101 | 102 | backspace()}>← 103 | enterDigit("0")}>0 104 | sendPin()} style={{ backgroundColor: "green" }}>{sendLabel} 105 | 106 | 107 |
108 | ); 109 | } -------------------------------------------------------------------------------- /src/components/PasswordInput.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { TextField } from "decky-frontend-lib"; 3 | 4 | // NumericInputProps interface with value and onChange properties 5 | interface PasswordInputProps { 6 | label: string; 7 | value: string; 8 | onChange: (e: string) => void; 9 | } 10 | 11 | export const PasswordInput = (props: PasswordInputProps): JSX.Element => { 12 | const { label, value, onChange } = props; 13 | 14 | const tfId = Math.trunc(Math.random() * 999_999) 15 | 16 | const doOnChange = (e: any) => { 17 | e.target.type = "password" 18 | onChange(e.target.value) 19 | } 20 | 21 | return ( 22 | 23 | 0} value={value} onChange={doOnChange}> 24 | 25 | ); 26 | } -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, VFC } from "react"; 2 | import { 3 | ToggleField, 4 | definePlugin, 5 | PanelSection, 6 | PanelSectionRow, 7 | ButtonItem, 8 | Spinner, 9 | Navigation, 10 | TextField, 11 | QuickAccessTab, 12 | } from "decky-frontend-lib"; 13 | import { FaSun } from "react-icons/fa"; 14 | import backend from "./util/backend"; 15 | 16 | import { PINInput } from "./components/PINInput"; 17 | import { PasswordInput } from "./components/PasswordInput"; 18 | 19 | const Content: VFC<{ serverAPI: any }> = ({ serverAPI }) => { 20 | // State variables 21 | const [sunshineIsRunning, setSunshineIsRunning] = useState(false); 22 | const [sunshineIsAuthorized, setSunshineIsAuthorized] = useState(false); 23 | const [wantToggleSunshine, setWantToggleSunshine] = useState(false); 24 | const [localPin, setLocalPin] = useState(""); 25 | 26 | // Function to fetch Sunshine state from the backend 27 | const updateSunshineState = async () => { 28 | const authed = await backend.sunshineIsAuthorized(); 29 | setSunshineIsAuthorized(authed); 30 | 31 | const running = await backend.sunshineIsRunning(); 32 | setSunshineIsRunning(running); 33 | 34 | setWantToggleSunshine(running) 35 | }; 36 | 37 | useEffect(() => { 38 | // Update Sunshine state when the component mounts 39 | updateSunshineState() 40 | }, []); 41 | 42 | useEffect(() => { 43 | // Start or stop Sunshine process based on wantToggleSunshine state 44 | if (wantToggleSunshine !== sunshineIsRunning) { 45 | if (wantToggleSunshine) { 46 | backend.sunshineStart(); 47 | } else { 48 | backend.sunshineStop(); 49 | } 50 | // Update state each 2 seconds till loading is done 51 | const interval = setInterval(() => { 52 | if (wantToggleSunshine !== sunshineIsRunning) { 53 | updateSunshineState(); 54 | } else { 55 | clearInterval(interval) 56 | } 57 | }, 2000); 58 | // Cleanup interval to avoid memory leaks 59 | return () => clearInterval(interval); 60 | } 61 | return () => { } 62 | }, [wantToggleSunshine]); 63 | 64 | // Show spinner while Sunshine state is being updated 65 | if (wantToggleSunshine !== sunshineIsRunning) { 66 | return 67 | } 68 | 69 | return ( 70 | 71 | 72 | {/* Toggle field to enable/disable Sunshine */} 73 | 78 | 79 | {/* Render PIN input if Sunshine is authorized */} 80 | {sunshineIsAuthorized ? ( 81 | sunshineIsRunning && ( 82 | { 88 | backend.sunshineSendPin(localPin); 89 | setLocalPin(""); 90 | }} 91 | sendLabel="Pair" 92 | /> 93 | ) 94 | ) : ( 95 | // Render login button if Sunshine is not authorized 96 | sunshineIsRunning && ( 97 | 98 |

You need to log into Sunshine

99 | { 101 | Navigation.CloseSideMenus(); 102 | Navigation.Navigate("/sunshine-login"); 103 | }} 104 | > 105 | Login 106 | 107 |
108 | ) 109 | )} 110 |
111 | ); 112 | }; 113 | 114 | const DeckySunshineLogin: VFC = () => { 115 | // State variables for local username and password 116 | const [localUsername, setLocalUsername] = useState(localStorage.getItem("decky_sunshine:localUsername") || ""); 117 | const [localPassword, setLocalPassword] = useState(""); 118 | 119 | return ( 120 |
121 | {/* Input fields for username and password */} 122 | setLocalUsername(e.target.value)} 126 | /> 127 | setLocalPassword(value)} 131 | /> 132 | {/* Button to not accidentally being forced to overwrite existing credentials */} 133 | { 135 | Navigation.NavigateBack(); 136 | Navigation.OpenQuickAccessMenu(QuickAccessTab.Decky); 137 | }}> 138 | Cancel 139 | 140 | {/* Button to login with entered credentials */} 141 | { 143 | // Store username in localStorage 144 | localStorage.setItem("decky_sunshine:localUsername", localUsername); 145 | Navigation.NavigateBack(); 146 | // Set authorization header and open Quick Access menu 147 | backend.sunshineSetAuthHeader(localUsername, localPassword); 148 | Navigation.OpenQuickAccessMenu(QuickAccessTab.Decky); 149 | }} 150 | disabled={localUsername.length < 1 || localPassword.length < 1} 151 | > 152 | Login 153 | 154 |
155 | ); 156 | }; 157 | 158 | // Define plugin 159 | export default definePlugin((serverApi: any) => { 160 | // Add route for Sunshine login 161 | serverApi.routerHook.addRoute("/sunshine-login", DeckySunshineLogin, { 162 | exact: true, 163 | }); 164 | 165 | // Set server API for backend 166 | backend.serverAPI = serverApi; 167 | 168 | return { 169 | title:
Decky Sunshine
, 170 | content: , 171 | icon: , 172 | // Remove route on dismount to avoid memory leaks 173 | onDismount() { 174 | serverApi.routerHook.removeRoute("/sunshine-login"); 175 | }, 176 | }; 177 | }); 178 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.svg" { 2 | const content: string; 3 | export default content; 4 | } 5 | 6 | declare module "*.png" { 7 | const content: string; 8 | export default content; 9 | } 10 | 11 | declare module "*.jpg" { 12 | const content: string; 13 | export default content; 14 | } 15 | -------------------------------------------------------------------------------- /src/util/backend.tsx: -------------------------------------------------------------------------------- 1 | import { ServerAPI } from "decky-frontend-lib"; 2 | 3 | class Backend { 4 | public serverAPI: ServerAPI | undefined 5 | 6 | public sunshineSendPin = async (pin: string): Promise => { 7 | const result = await this.serverAPI?.callPluginMethod<{ pin: string }, boolean>( 8 | "sendPin", 9 | { pin } 10 | ); 11 | console.log("[SUN]", "sendPin result", result) 12 | return Boolean(result?.result || false) 13 | } 14 | 15 | public sunshineIsRunning = async () => { 16 | const result = await this.serverAPI?.callPluginMethod( 17 | "sunshineIsRunning", 18 | {} 19 | ); 20 | console.log("[SUN]", "sunshineCheckRunning result", result) 21 | if (result?.result) { 22 | return true 23 | } else { 24 | return false 25 | } 26 | }; 27 | 28 | public sunshineSetAuthHeader = async (username: string, password: string): Promise => { 29 | const result = await this.serverAPI?.callPluginMethod( 30 | "setAuthHeader", 31 | { 32 | username, 33 | password 34 | } 35 | ); 36 | console.log("[SUN]", "setAuthHeader result", result) 37 | return Boolean(result?.result || false) 38 | } 39 | 40 | public sunshineIsAuthorized = async (): Promise => { 41 | if (!this.sunshineIsRunning()) return false 42 | const result = await this.serverAPI?.callPluginMethod( 43 | "sunshineIsAuthorized", 44 | {} 45 | ); 46 | console.log("[SUN]", "sunshineCheckAuthorized result", result) 47 | if (result?.success) { 48 | return Boolean(result?.result || false); 49 | } else { 50 | return false; 51 | } 52 | }; 53 | 54 | public sunshineStart = async () => { 55 | console.log("[SUN]", "should start") 56 | let result = await this.serverAPI?.callPluginMethod( 57 | "sunshineStart", 58 | {} 59 | ) 60 | console.log("[SUN] start res: ", result) 61 | return Boolean(result?.result || false) 62 | } 63 | 64 | public sunshineStop = async () => { 65 | console.log("[SUN]", "should stop") 66 | let result = await this.serverAPI?.callPluginMethod( 67 | "sunshineStop", 68 | {} 69 | ) 70 | console.log("[SUN] stop res: ", result) 71 | return Boolean(result?.result || false) 72 | } 73 | 74 | public sunshineSetUser = async ( 75 | newUsername: string, 76 | newPassword: string, 77 | confirmNewPassword: string, 78 | currentUsername: string | undefined, 79 | currentPassword: string | undefined, 80 | ) => { 81 | console.log("[SUN]", "Set User") 82 | let namedParams = { 83 | newUsername, 84 | newPassword, 85 | confirmNewPassword 86 | } as any 87 | if(currentUsername || currentPassword) { 88 | namedParams.currentUsername = currentUsername 89 | namedParams.currentPassword = currentPassword 90 | } 91 | let result = await this.serverAPI?.callPluginMethod( 92 | "sunshineSetUser", 93 | namedParams 94 | ) 95 | console.log("[SUN] setUser res: ", result) 96 | return Boolean(result?.result || false) 97 | } 98 | } 99 | const backend = new Backend() 100 | export default backend -------------------------------------------------------------------------------- /src/util/util.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, SetStateAction, Dispatch } from 'react'; 2 | 3 | export const playSound = (sound: string) => { 4 | const audio = new Audio(sound); 5 | audio.play(); 6 | } 7 | 8 | //Function to use storage, and automatically store it in local storage 9 | export function useLocalStorageState (key: string, defaultValue: S | (() => S)): [S, Dispatch>] { 10 | const [state, setState] = useState(() => { 11 | const valueInLocalStorage = localStorage.getItem(key); 12 | if (valueInLocalStorage) { 13 | try 14 | { 15 | let value = JSON.parse(valueInLocalStorage); 16 | console.log(key, valueInLocalStorage); 17 | return value; 18 | } 19 | catch(ex) 20 | { 21 | return null; 22 | } 23 | } 24 | 25 | return typeof defaultValue === 'function' ? (defaultValue as Function)() : defaultValue; 26 | }); 27 | 28 | useEffect(() => { 29 | localStorage.setItem(key, JSON.stringify(state)); 30 | }, [key, state]); 31 | 32 | return [state, setState]; 33 | } -------------------------------------------------------------------------------- /tests/test_sunshine.py: -------------------------------------------------------------------------------- 1 | from py_modules.sunshine import SunshineController 2 | import urllib.request 3 | 4 | sun = SunshineController() 5 | 6 | isRunning = sun.isRunning() 7 | print("Running: " + str(isRunning)) 8 | 9 | if(not isRunning): 10 | sun.start() 11 | 12 | sun.setAuthHeader("sunshine", "lol2") 13 | 14 | isAuthorized = sun.isAuthorized() 15 | print("Authorized: " + str(isAuthorized)) 16 | 17 | sendPin = sun.sendPin("6141") 18 | print("sendPin: " + str(sendPin)) 19 | 20 | setUser = sun.setUser("sunshine", "lol2", "sunshine", "lol", "lol") 21 | print("setUser: " + str(setUser)) 22 | 23 | sun.stop() -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "module": "ESNext", 5 | "target": "ES2020", 6 | "jsx": "react", 7 | "jsxFactory": "window.SP_REACT.createElement", 8 | "jsxFragmentFactory": "window.SP_REACT.Fragment", 9 | "declaration": false, 10 | "moduleResolution": "node", 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "esModuleInterop": true, 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | "noImplicitAny": true, 17 | "strict": true, 18 | "suppressImplicitAnyIndexErrors": true, 19 | "allowSyntheticDefaultImports": true, 20 | "skipLibCheck": true 21 | }, 22 | "include": ["src"], 23 | "exclude": ["node_modules"] 24 | } 25 | --------------------------------------------------------------------------------