├── .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 |

3 |
Decky Sunshine Plugin
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | [](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 |
--------------------------------------------------------------------------------