Compare commits

..

1 Commits

Author SHA1 Message Date
TrainDoctor bfd09520f9 Update aiohttp and others 2025-07-01 12:53:26 -07:00
29 changed files with 991 additions and 947 deletions
+1 -1
View File
@@ -41,7 +41,7 @@ jobs:
working-directory: ./frontend
run: |
npm i -g pnpm
pnpm i --frozen-lockfile --dangerously-allow-all-builds
pnpm i --frozen-lockfile
- name: Build JS Frontend 🛠️
working-directory: ./frontend
+1 -1
View File
@@ -43,7 +43,7 @@ jobs:
working-directory: ./frontend
run: |
npm i -g pnpm
pnpm i --frozen-lockfile --dangerously-allow-all-builds
pnpm i --frozen-lockfile
- name: Build JS Frontend 🛠️
working-directory: ./frontend
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
working-directory: frontend
run: |
npm i -g pnpm
pnpm i --frozen-lockfile --dangerously-allow-all-builds
pnpm i --frozen-lockfile
- name: Run prettier (TypeScript)
working-directory: frontend
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
working-directory: frontend
run: |
npm i -g pnpm
pnpm i --frozen-lockfile --dangerously-allow-all-builds
pnpm i --frozen-lockfile
- name: Run pyright (Python)
uses: jakebailey/pyright-action@v1
+1 -1
View File
@@ -95,7 +95,7 @@ Please consult [the wiki page regarding development](https://wiki.deckbrew.xyz/e
1. In your clone of the repository, run these commands.
```bash
cd frontend
pnpm i # NOTE: you may need to approve esbuild's build script with pnpm approve-builds
pnpm i
pnpm run build
```
1. If you are modifying the UI, these commands will need to be run before deploying the changes to your Steam Deck.
+10 -25
View File
@@ -18,10 +18,9 @@ from enum import IntEnum
from typing import Dict, List, TypedDict
# Local modules
from .localplatform.localplatform import chown, chmod, get_chown_plugin_path
from .localplatform.localplatform import chown, chmod
from .loader import Loader, Plugins
from .helpers import get_ssl_context, download_remote_binary_to_path
from .enums import UserType
from .settings import SettingsManager
logger = getLogger("Browser")
@@ -61,6 +60,13 @@ class PluginBrowser:
return False
zip_file = ZipFile(zip)
zip_file.extractall(self.plugin_path)
plugin_folder = self.find_plugin_folder(name)
assert plugin_folder is not None
plugin_dir = path.join(self.plugin_path, plugin_folder)
if not chown(plugin_dir) or not chmod(plugin_dir, 555):
logger.error(f"chown/chmod exited with a non-zero exit code")
return False
return True
async def _download_remote_binaries_for_plugin_with_name(self, pluginBasePath: str):
@@ -95,6 +101,8 @@ class PluginBrowser:
rv = False
raise Exception(f"Error Downloading Remote Binary {binName}@{binURL} with hash {binHash} to {path.join(pluginBinPath, binName)}")
chown(self.plugin_path)
chmod(pluginBasePath, 555)
else:
rv = True
logger.info(f"No Remote Binaries to Download")
@@ -116,25 +124,6 @@ class PluginBrowser:
return folder
except:
logger.debug(f"skipping {folder}")
def set_plugin_dir_permissions(self, plugin_dir: str) -> bool:
plugin_json_path = path.join(plugin_dir, 'plugin.json')
logger.debug(f"Checking plugin.json at {plugin_json_path}")
root_plugin = False
if access(plugin_json_path, R_OK):
with open(plugin_json_path, "r", encoding="utf-8") as f:
plugin_json = json.load(f)
if "flags" in plugin_json and "root" in plugin_json["flags"]:
root_plugin = True
logger.debug("root_plugin %d, dir %s", root_plugin, plugin_dir)
if get_chown_plugin_path():
return chown(plugin_dir, UserType.EFFECTIVE_USER if root_plugin else UserType.HOST_USER, True) and chown(plugin_dir, UserType.EFFECTIVE_USER, False) and chmod(plugin_dir, 755) and chown(plugin_json_path, UserType.EFFECTIVE_USER, False) and chmod(plugin_json_path, 755)
else:
logger.debug("chown disabled by environment")
return True
async def uninstall_plugin(self, name: str):
if self.loader.watcher:
@@ -277,7 +266,6 @@ class PluginBrowser:
plugin_dir = path.join(self.plugin_path, plugin_folder)
await self.loader.ws.emit("loader/plugin_download_info", 95, "Store.download_progress_info.download_remote")
ret = await self._download_remote_binaries_for_plugin_with_name(plugin_dir)
chown_ret = self.set_plugin_dir_permissions(plugin_dir)
if ret:
logger.info(f"Installed {name} (Version: {version})")
if name in self.loader.plugins:
@@ -290,9 +278,6 @@ class PluginBrowser:
self.settings.setSetting("pluginOrder", current_plugin_order)
logger.debug("Plugin %s was added to the pluginOrder setting", name)
await self.loader.import_plugin(path.join(plugin_dir, "main.py"), plugin_folder)
elif not chown_ret:
logger.error("Could not chown plugin")
return
else:
logger.error("Could not download remote binaries")
return
+3 -2
View File
@@ -1,8 +1,9 @@
from enum import IntEnum
class UserType(IntEnum):
HOST_USER = 1 # usually deck
EFFECTIVE_USER = 2 # usually root
HOST_USER = 1
EFFECTIVE_USER = 2
ROOT = 3
class PluginLoadType(IntEnum):
LEGACY_EVAL_IIFE = 0 # legacy, uses legacy serverAPI
+1 -2
View File
@@ -181,8 +181,7 @@ def get_user_group_id() -> int:
# Get the default home path unless a user is specified
def get_home_path(username: str | None = None) -> str:
# TODO hardcoded root is kinda a hack
return localplatform.get_home_path(UserType.EFFECTIVE_USER if username == "root" else UserType.HOST_USER)
return localplatform.get_home_path(UserType.ROOT if username == "root" else UserType.HOST_USER)
async def is_systemd_unit_active(unit_name: str) -> bool:
return await localplatform.service_active(unit_name)
@@ -59,6 +59,8 @@ def chown(path : str, user : UserType = UserType.HOST_USER, recursive : bool =
user_str = _get_user()+":"+_get_user_group()
elif user == UserType.EFFECTIVE_USER:
user_str = _get_effective_user()+":"+_get_effective_user_group()
elif user == UserType.ROOT:
user_str = "root:root"
else:
raise Exception("Unknown User Type")
@@ -85,7 +87,7 @@ def chmod(path : str, permissions : int, recursive : bool = True) -> bool:
return True
def file_owner(path : str) -> UserType|None:
def folder_owner(path : str) -> UserType|None:
user_owner = _get_user_owner(path)
if (user_owner == _get_user()):
@@ -104,14 +106,13 @@ def get_home_path(user : UserType = UserType.HOST_USER) -> str:
user_name = _get_user()
elif user == UserType.EFFECTIVE_USER:
user_name = _get_effective_user()
elif user == UserType.ROOT:
pass
else:
raise Exception("Unknown User Type")
return pwd.getpwnam(user_name).pw_dir
def get_effective_username() -> str:
return _get_effective_user()
def get_username() -> str:
return _get_user()
@@ -120,8 +121,8 @@ def setgid(user : UserType = UserType.HOST_USER):
if user == UserType.HOST_USER:
user_id = _get_user_group_id()
elif user == UserType.EFFECTIVE_USER:
pass # we already are
elif user == UserType.ROOT:
pass
else:
raise Exception("Unknown user type")
@@ -132,8 +133,8 @@ def setuid(user : UserType = UserType.HOST_USER):
if user == UserType.HOST_USER:
user_id = _get_user_id()
elif user == UserType.EFFECTIVE_USER:
pass # we already are
elif user == UserType.ROOT:
pass
else:
raise Exception("Unknown user type")
@@ -7,7 +7,7 @@ def chown(path : str, user : UserType = UserType.HOST_USER, recursive : bool =
def chmod(path : str, permissions : int, recursive : bool = True) -> bool:
return True # Stubbed
def file_owner(path : str) -> UserType|None:
def folder_owner(path : str) -> UserType|None:
return UserType.HOST_USER # Stubbed
def get_home_path(user : UserType = UserType.HOST_USER) -> str:
@@ -34,9 +34,6 @@ async def service_restart(service_name : str, block : bool = True) -> bool:
return True # Stubbed
def get_effective_username() -> str:
return os.getlogin()
def get_username() -> str:
return os.getlogin()
+1 -1
View File
@@ -50,7 +50,7 @@ def chown_plugin_dir():
if not path.exists(plugin_path): # For safety, create the folder before attempting to do anything with it
mkdir_as_user(plugin_path)
if not chown(plugin_path, UserType.EFFECTIVE_USER, False) or not chmod(plugin_path, 755, False):
if not chown(plugin_path, UserType.HOST_USER) or not chmod(plugin_path, 555):
logger.error(f"chown/chmod exited with a non-zero exit code")
if get_chown_plugin_path() == True:
+4 -19
View File
@@ -8,8 +8,7 @@ from traceback import format_exc
from .sandboxed_plugin import SandboxedPlugin
from .messages import MethodCallRequest, SocketMessageType
from ..enums import PluginLoadType, UserType
from ..localplatform.localplatform import file_owner, chown, chmod, get_chown_plugin_path
from ..enums import PluginLoadType
from ..localplatform.localsocket import LocalSocket
from ..helpers import get_homebrew_path, mkdir_as_user
@@ -27,12 +26,9 @@ class PluginWrapper:
self.load_type = PluginLoadType.LEGACY_EVAL_IIFE.value
plugin_dir_path = path.join(plugin_path, plugin_directory)
plugin_json_path = path.join(plugin_dir_path, "plugin.json")
json = load(open(plugin_json_path, "r", encoding="utf-8"))
if path.isfile(path.join(plugin_dir_path, "package.json")):
package_json = load(open(path.join(plugin_dir_path, "package.json"), "r", encoding="utf-8"))
json = load(open(path.join(plugin_path, plugin_directory, "plugin.json"), "r", encoding="utf-8"))
if path.isfile(path.join(plugin_path, plugin_directory, "package.json")):
package_json = load(open(path.join(plugin_path, plugin_directory, "package.json"), "r", encoding="utf-8"))
self.version = package_json["version"]
if ("type" in package_json and package_json["type"] == "module"):
self.load_type = PluginLoadType.ESMODULE_V1.value
@@ -46,17 +42,6 @@ class PluginWrapper:
self.log = getLogger("plugin")
if get_chown_plugin_path():
# ensure plugin folder ownership
if file_owner(plugin_dir_path) != UserType.EFFECTIVE_USER:
chown(plugin_dir_path, UserType.EFFECTIVE_USER if "root" in self.flags else UserType.HOST_USER, True)
chown(plugin_dir_path, UserType.EFFECTIVE_USER, False)
chmod(plugin_dir_path, 755, True)
# fix plugin.json permissions
if file_owner(plugin_json_path) != UserType.EFFECTIVE_USER:
chown(plugin_json_path, UserType.EFFECTIVE_USER, False)
chmod(plugin_json_path, 755, False)
self.sandboxed_plugin = SandboxedPlugin(self.name, self.passive, self.flags, self.file, self.plugin_directory, self.plugin_path, self.version, self.author, self.api_version)
self.proc: Process | None = None
self._socket = LocalSocket()
@@ -13,7 +13,7 @@ from .messages import SocketResponseDict, SocketMessageType
from ..localplatform.localsocket import LocalSocket
from ..localplatform.localplatform import setgid, setuid, get_username, get_home_path, ON_LINUX
from ..enums import UserType
from .. import helpers
from .. import helpers, settings, injector # pyright: ignore [reportUnusedImport]
from typing import List, TypeVar, Any
@@ -61,10 +61,10 @@ class SandboxedPlugin:
if self.passive:
return
setgid(UserType.EFFECTIVE_USER if "root" in self.flags else UserType.HOST_USER)
setuid(UserType.EFFECTIVE_USER if "root" in self.flags else UserType.HOST_USER)
setgid(UserType.ROOT if "root" in self.flags else UserType.HOST_USER)
setuid(UserType.ROOT if "root" in self.flags else UserType.HOST_USER)
# export a bunch of environment variables to help plugin developers
environ["HOME"] = get_home_path(UserType.EFFECTIVE_USER if "root" in self.flags else UserType.HOST_USER)
environ["HOME"] = get_home_path(UserType.ROOT if "root" in self.flags else UserType.HOST_USER)
environ["USER"] = "root" if "root" in self.flags else get_username()
environ["DECKY_VERSION"] = helpers.get_loader_version()
environ["DECKY_USER"] = get_username()
+3 -3
View File
@@ -1,7 +1,7 @@
from json import dump, load
from os import mkdir, path, listdir, rename
from typing import Any, Dict
from .localplatform.localplatform import chown, file_owner, get_chown_plugin_path
from .localplatform.localplatform import chown, folder_owner, get_chown_plugin_path
from .enums import UserType
from .helpers import get_homebrew_path
@@ -28,8 +28,8 @@ class SettingsManager:
#If the owner of the settings directory is not the user, then set it as the user:
expected_user = UserType.HOST_USER if get_chown_plugin_path() else UserType.EFFECTIVE_USER
if file_owner(settings_directory) != expected_user:
expected_user = UserType.HOST_USER if get_chown_plugin_path() else UserType.ROOT
if folder_owner(settings_directory) != expected_user:
chown(settings_directory, expected_user, False)
self.settings: Dict[str, Any] = {}
+3 -1
View File
@@ -7,7 +7,7 @@ from aiohttp.web import Application, WebSocketResponse, Request, Response, get
from enum import IntEnum
from typing import Callable, Coroutine, Dict, Any, cast
from typing import Callable, Coroutine, Dict, Any, cast, TypeVar
from traceback import format_exc
@@ -29,6 +29,8 @@ class WSMessageExtra(WSMessage):
# see wsrouter.ts for typings
DataType = TypeVar("DataType")
Route = Callable[..., Coroutine[Any, Any, Any]]
class WSRouter:
+816 -733
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -14,7 +14,7 @@ include = [
]
[tool.poetry.dependencies]
python = ">=3.10,<3.14"
python = ">=3.10,<=3.13.5"
aiohttp = "^3.10.11"
aiohttp-jinja2 = "^1.5.1"
+8 -8
View File
@@ -20,8 +20,8 @@
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-replace": "^5.0.7",
"@rollup/plugin-typescript": "^11.1.6",
"@types/react": "19.1.1",
"@types/react-dom": "19.1.1",
"@types/react": "18.3.3",
"@types/react-dom": "18.3.0",
"@types/react-file-icon": "^1.0.4",
"@types/react-router": "5.1.20",
"husky": "^9.0.11",
@@ -30,8 +30,8 @@
"inquirer": "^9.2.23",
"prettier": "^3.3.2",
"prettier-plugin-import-sort": "^0.0.7",
"react": "19.1.1",
"react-dom": "19.1.1",
"react": "18.3.1",
"react-dom": "18.3.1",
"rollup": "^4.22.4",
"rollup-plugin-delete": "^2.0.0",
"rollup-plugin-external-globals": "^0.10.0",
@@ -47,13 +47,13 @@
}
},
"dependencies": {
"@decky/ui": "^4.11.0",
"@decky/ui": "^4.10.2",
"compare-versions": "^6.1.1",
"filesize": "^10.1.2",
"i18next": "^25.6.0",
"i18next": "^23.11.5",
"i18next-http-backend": "^2.5.2",
"react-file-icon": "^1.6.0",
"react-i18next": "^16.0.1",
"react-file-icon": "^1.5.0",
"react-i18next": "^14.1.2",
"react-icons": "^5.2.1",
"react-markdown": "^9.0.1",
"remark-gfm": "^4.0.0"
+77 -92
View File
@@ -9,8 +9,8 @@ importers:
.:
dependencies:
'@decky/ui':
specifier: ^4.11.0
version: 4.11.0
specifier: ^4.10.2
version: 4.10.2
compare-versions:
specifier: ^6.1.1
version: 6.1.1
@@ -18,23 +18,23 @@ importers:
specifier: ^10.1.2
version: 10.1.2
i18next:
specifier: ^25.6.0
version: 25.6.0(typescript@5.4.5)
specifier: ^23.11.5
version: 23.11.5
i18next-http-backend:
specifier: ^2.5.2
version: 2.5.2
react-file-icon:
specifier: ^1.6.0
version: 1.6.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
specifier: ^1.5.0
version: 1.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-i18next:
specifier: ^16.0.1
version: 16.0.1(i18next@25.6.0(typescript@5.4.5))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.4.5)
specifier: ^14.1.2
version: 14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-icons:
specifier: ^5.2.1
version: 5.2.1(react@19.1.1)
version: 5.2.1(react@18.3.1)
react-markdown:
specifier: ^9.0.1
version: 9.0.1(@types/react@19.1.1)(react@19.1.1)
version: 9.0.1(@types/react@18.3.3)(react@18.3.1)
remark-gfm:
specifier: ^4.0.0
version: 4.0.0
@@ -61,11 +61,11 @@ importers:
specifier: ^11.1.6
version: 11.1.6(rollup@4.22.4)(tslib@2.6.3)(typescript@5.4.5)
'@types/react':
specifier: 19.1.1
version: 19.1.1
specifier: 18.3.3
version: 18.3.3
'@types/react-dom':
specifier: 19.1.1
version: 19.1.1(@types/react@19.1.1)
specifier: 18.3.0
version: 18.3.0
'@types/react-file-icon':
specifier: ^1.0.4
version: 1.0.4
@@ -91,11 +91,11 @@ importers:
specifier: ^0.0.7
version: 0.0.7(prettier@3.3.2)
react:
specifier: 19.1.1
version: 19.1.1
specifier: 18.3.1
version: 18.3.1
react-dom:
specifier: 19.1.1
version: 19.1.1(react@19.1.1)
specifier: 18.3.1
version: 18.3.1(react@18.3.1)
rollup:
specifier: ^4.22.4
version: 4.22.4
@@ -203,10 +203,6 @@ packages:
resolution: {integrity: sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==}
engines: {node: '>=6.9.0'}
'@babel/runtime@7.28.4':
resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==}
engines: {node: '>=6.9.0'}
'@babel/template@7.24.7':
resolution: {integrity: sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==}
engines: {node: '>=6.9.0'}
@@ -222,8 +218,8 @@ packages:
'@decky/api@1.1.1':
resolution: {integrity: sha512-R5fkBRHBt5QIQY7Q0AlbVIhlIZ/nTzwBOoi8Rt4Go2fjFnoMKPInCJl6cPjXzimGwl2pyqKJgY6VnH6ar0XrHQ==}
'@decky/ui@4.11.0':
resolution: {integrity: sha512-l9PstFC+S8FE8M2kIM78L8cYW4vzJ/ZD30II0huarHLcCsKM4Q+rbmEnbWjlJ1/KLmGXVRXBdAbyD4X/FzfxnQ==}
'@decky/ui@4.10.2':
resolution: {integrity: sha512-dfY/OEI/rhG4d3Tvx4Y3TLmBrJ+nAm2RTbkoOJ3VAglql3Lu7RY2ixeDFbs21ZWWzWh/C+9dAQKjQ4lx4d1f2g==}
'@esbuild/aix-ppc64@0.20.2':
resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==}
@@ -602,10 +598,11 @@ packages:
'@types/node@20.14.2':
resolution: {integrity: sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==}
'@types/react-dom@19.1.1':
resolution: {integrity: sha512-jFf/woGTVTjUJsl2O7hcopJ1r0upqoq/vIOoCj0yLh3RIXxWcljlpuZ+vEBRXsymD1jhfeJrlyTy/S1UW+4y1w==}
peerDependencies:
'@types/react': ^19.0.0
'@types/prop-types@15.7.12':
resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==}
'@types/react-dom@18.3.0':
resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==}
'@types/react-file-icon@1.0.4':
resolution: {integrity: sha512-c1mIklUDaxm9odxf8RTiy/EAxsblZliJ86EKIOAyuafP9eK3iudyn4ATv53DX6ZvgGymc7IttVNm97LTGnTiYA==}
@@ -613,8 +610,8 @@ packages:
'@types/react-router@5.1.20':
resolution: {integrity: sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==}
'@types/react@19.1.1':
resolution: {integrity: sha512-ePapxDL7qrgqSF67s0h9m412d9DbXyC1n59O2st+9rjuuamWsZuD2w55rqY12CbzsZ7uVXb5Nw0gEp9Z8MMutQ==}
'@types/react@18.3.3':
resolution: {integrity: sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==}
'@types/resolve@1.20.2':
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
@@ -1190,16 +1187,8 @@ packages:
engines: {node: '>=18.0.0 || >=20.0.0 || >=22.0.0', npm: '>=6', yarn: '>=1'}
hasBin: true
i18next@23.16.8:
resolution: {integrity: sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==}
i18next@25.6.0:
resolution: {integrity: sha512-tTn8fLrwBYtnclpL5aPXK/tAYBLWVvoHM1zdfXoRNLcI+RvtMsoZRV98ePlaW3khHYKuNh/Q65W/+NVFUeIwVw==}
peerDependencies:
typescript: ^5
peerDependenciesMeta:
typescript:
optional: true
i18next@23.11.5:
resolution: {integrity: sha512-41pvpVbW9rhZPk5xjCX2TPJi2861LEig/YRhUkY+1FQ2IQPS0bKUDYnEqY8XPPbB48h1uIwLnP9iiEfuSl20CA==}
iconv-lite@0.4.24:
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
@@ -1720,32 +1709,29 @@ packages:
quick-temp@0.1.8:
resolution: {integrity: sha512-YsmIFfD9j2zaFwJkzI6eMG7y0lQP7YeWzgtFgNl38pGWZBSXJooZbOWwkcRot7Vt0Fg9L23pX0tqWU3VvLDsiA==}
react-dom@19.1.1:
resolution: {integrity: sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==}
react-dom@18.3.1:
resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
peerDependencies:
react: ^19.1.1
react: ^18.3.1
react-file-icon@1.6.0:
resolution: {integrity: sha512-Ba4Qa2ya/kvhcCd4LJja77sV7JD7u1ZXcI1DUz+TII3nGmglG6QY+NZeHizThokgct3qI0glwb9eV8NqRGs5lw==}
react-file-icon@1.5.0:
resolution: {integrity: sha512-6K2/nAI69CS838HOS+4S95MLXwf1neWywek1FgqcTFPTYjnM8XT7aBLz4gkjoqQKY9qPhu3A2tu+lvxhmZYY9w==}
peerDependencies:
react: ^19.0.0 || ^18.0.0 || ^17.0.0 || ^16.2.0
react-dom: ^19.0.0 || ^18.0.0 || ^17.0.0 || ^16.2.0
react: ^18.0.0 || ^17.0.0 || ^16.2.0
react-dom: ^18.0.0 || ^17.0.0 || ^16.2.0
react-i18next@16.0.1:
resolution: {integrity: sha512-0S//bpYEkCPjzuVmxDf9Z6+Y+ArNvpAUk7eDL4qNCZXjDh6Z9j6MZ+NThU7kMCOsmYmDCun3GYEwkiOjjZo9Ug==}
react-i18next@14.1.2:
resolution: {integrity: sha512-FSIcJy6oauJbGEXfhUgVeLzvWBhIBIS+/9c6Lj4niwKZyGaGb4V4vUbATXSlsHJDXXB+ociNxqFNiFuV1gmoqg==}
peerDependencies:
i18next: '>= 25.5.2'
i18next: '>= 23.2.3'
react: '>= 16.8.0'
react-dom: '*'
react-native: '*'
typescript: ^5
peerDependenciesMeta:
react-dom:
optional: true
react-native:
optional: true
typescript:
optional: true
react-icons@5.2.1:
resolution: {integrity: sha512-zdbW5GstTzXaVKvGSyTaBalt7HSfuK5ovrzlpyiWHAFXndXTdd/1hdDHI4xBM1Mn7YriT6aqESucFl9kEXzrdw==}
@@ -1761,8 +1747,8 @@ packages:
'@types/react': '>=18'
react: '>=18'
react@19.1.1:
resolution: {integrity: sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==}
react@18.3.1:
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
engines: {node: '>=0.10.0'}
readable-stream@2.3.8:
@@ -1887,8 +1873,8 @@ packages:
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
scheduler@0.26.0:
resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}
scheduler@0.23.2:
resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
semver@6.3.1:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
@@ -2280,8 +2266,6 @@ snapshots:
dependencies:
regenerator-runtime: 0.14.1
'@babel/runtime@7.28.4': {}
'@babel/template@7.24.7':
dependencies:
'@babel/code-frame': 7.24.7
@@ -2311,7 +2295,7 @@ snapshots:
'@decky/api@1.1.1': {}
'@decky/ui@4.11.0': {}
'@decky/ui@4.10.2': {}
'@esbuild/aix-ppc64@0.20.2':
optional: true
@@ -2583,21 +2567,24 @@ snapshots:
dependencies:
undici-types: 5.26.5
'@types/react-dom@19.1.1(@types/react@19.1.1)':
'@types/prop-types@15.7.12': {}
'@types/react-dom@18.3.0':
dependencies:
'@types/react': 19.1.1
'@types/react': 18.3.3
'@types/react-file-icon@1.0.4':
dependencies:
'@types/react': 19.1.1
'@types/react': 18.3.3
'@types/react-router@5.1.20':
dependencies:
'@types/history': 4.7.11
'@types/react': 19.1.1
'@types/react': 18.3.3
'@types/react@19.1.1':
'@types/react@18.3.3':
dependencies:
'@types/prop-types': 15.7.12
csstype: 3.1.3
'@types/resolve@1.20.2': {}
@@ -3242,7 +3229,7 @@ snapshots:
esbuild: 0.20.2
fs-extra: 11.2.0
gulp-sort: 2.0.0
i18next: 23.16.8
i18next: 23.11.5
js-yaml: 4.1.0
lilconfig: 3.1.2
rsvp: 4.8.5
@@ -3253,16 +3240,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
i18next@23.16.8:
i18next@23.11.5:
dependencies:
'@babel/runtime': 7.24.7
i18next@25.6.0(typescript@5.4.5):
dependencies:
'@babel/runtime': 7.28.4
optionalDependencies:
typescript: 5.4.5
iconv-lite@0.4.24:
dependencies:
safer-buffer: 2.1.2
@@ -3979,43 +3960,43 @@ snapshots:
rimraf: 2.7.1
underscore.string: 3.3.6
react-dom@19.1.1(react@19.1.1):
react-dom@18.3.1(react@18.3.1):
dependencies:
react: 19.1.1
scheduler: 0.26.0
loose-envify: 1.4.0
react: 18.3.1
scheduler: 0.23.2
react-file-icon@1.6.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
react-file-icon@1.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
colord: 2.9.3
prop-types: 15.8.1
react: 19.1.1
react-dom: 19.1.1(react@19.1.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-i18next@16.0.1(i18next@25.6.0(typescript@5.4.5))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.4.5):
react-i18next@14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@babel/runtime': 7.28.4
'@babel/runtime': 7.24.7
html-parse-stringify: 3.0.1
i18next: 25.6.0(typescript@5.4.5)
react: 19.1.1
i18next: 23.11.5
react: 18.3.1
optionalDependencies:
react-dom: 19.1.1(react@19.1.1)
typescript: 5.4.5
react-dom: 18.3.1(react@18.3.1)
react-icons@5.2.1(react@19.1.1):
react-icons@5.2.1(react@18.3.1):
dependencies:
react: 19.1.1
react: 18.3.1
react-is@16.13.1: {}
react-markdown@9.0.1(@types/react@19.1.1)(react@19.1.1):
react-markdown@9.0.1(@types/react@18.3.3)(react@18.3.1):
dependencies:
'@types/hast': 3.0.4
'@types/react': 19.1.1
'@types/react': 18.3.3
devlop: 1.1.0
hast-util-to-jsx-runtime: 2.3.0
html-url-attributes: 3.0.0
mdast-util-to-hast: 13.2.0
react: 19.1.1
react: 18.3.1
remark-parse: 11.0.0
remark-rehype: 11.1.0
unified: 11.0.4
@@ -4024,7 +4005,9 @@ snapshots:
transitivePeerDependencies:
- supports-color
react@19.1.1: {}
react@18.3.1:
dependencies:
loose-envify: 1.4.0
readable-stream@2.3.8:
dependencies:
@@ -4181,7 +4164,9 @@ snapshots:
safer-buffer@2.1.2: {}
scheduler@0.26.0: {}
scheduler@0.23.2:
dependencies:
loose-envify: 1.4.0
semver@6.3.1: {}
-1
View File
@@ -23,7 +23,6 @@ export default defineConfig([
}),
externalGlobals({
react: 'SP_REACT',
'react/jsx-runtime': 'SP_JSX',
'react-dom': 'SP_REACTDOM',
// hack to shut up react-markdown
process: '{cwd: () => {}}',
@@ -1,10 +1,10 @@
import { FC, PropsWithChildren, createContext, useContext, useState } from 'react';
import { FC, ReactNode, createContext, useContext, useState } from 'react';
const QuickAccessVisibleState = createContext<boolean>(false);
export const useQuickAccessVisible = () => useContext(QuickAccessVisibleState);
export const QuickAccessVisibleStateProvider: FC<PropsWithChildren<{ tab: any }>> = ({ children, tab }) => {
export const QuickAccessVisibleStateProvider: FC<{ tab: any; children: ReactNode }> = ({ children, tab }) => {
const initial = tab.initialVisibility;
const [visible, setVisible] = useState<boolean>(initial);
// HACK but i can't think of a better way to do this
+1 -1
View File
@@ -10,7 +10,7 @@ interface WithSuspenseProps {
const WithSuspense: FunctionComponent<WithSuspenseProps> = (props) => {
const propsCopy = { ...props };
delete propsCopy.children;
(props.children as ReactElement<any>)?.props && Object.assign((props.children as ReactElement<any>).props, propsCopy); // There is probably a better way to do this but valve does it this way so ¯\_(ツ)_/¯
(props.children as ReactElement)?.props && Object.assign((props.children as ReactElement).props, propsCopy); // There is probably a better way to do this but valve does it this way so ¯\_(ツ)_/¯
return (
<Suspense
fallback={
@@ -1,4 +1,4 @@
import { FC, JSX, useEffect, useState } from 'react';
import { FC, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { IconContext } from 'react-icons';
import { FaExclamationTriangle, FaQuestionCircle, FaUserSlash } from 'react-icons/fa';
+5 -13
View File
@@ -1,4 +1,8 @@
// Sets up DFL, then loads start.ts which starts up the loader
interface Window {
// Shut up TS
SP_REACTDOM: any;
}
(async () => {
console.debug('[Decky:Boot] Frontend init');
@@ -17,19 +21,7 @@
// deliberate partial import
const DFLWebpack = await import('@decky/ui/dist/webpack');
window.SP_REACT = DFLWebpack.findModule((m) => m.Component && m.PureComponent && m.useLayoutEffect);
window.SP_REACTDOM =
DFLWebpack.findModule((m) => m.createPortal && m.createRoot) ||
DFLWebpack.findModule((m) => m.createPortal && m.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE);
console.debug('[Decky:Boot] Setting up JSX internals...');
const jsx = DFLWebpack.findModule((m) => m.jsx && Object.keys(m).length == 1)?.jsx;
if (jsx) {
window.SP_JSX = {
jsx,
jsxs: jsx,
Fragment: window.SP_REACT.Fragment,
};
}
window.SP_REACTDOM = DFLWebpack.findModule((m) => m.createPortal && m.createRoot);
}
console.debug('[Decky:Boot] Setting up @decky/ui...');
window.DFL = await import('@decky/ui');
+22
View File
@@ -120,6 +120,28 @@ class PluginLoader extends Logger {
<DeckyStateContextProvider deckyState={this.deckyState}>
<FaPlug />
<TabBadge />
<style>
{`
/* fixes random overscrolling in QAM */
.${quickAccessMenuClasses?.TabContentColumn} {
flex-grow: 1 !important;
margin-top: 0 !important;
margin-bottom: 0 !important;
justify-content: center !important;
}
.${quickAccessMenuClasses?.Tab} {
flex-grow: 1 !important;
height: unset !important;
--decky-qam-tab-max-height: 64px; /* make things a little easier for themers */
max-height: var(--decky-qam-tab-max-height) !important;
}
/* they broke the footer a while ago and forgot to update the styles LOL */
.${quickAccessMenuClasses?.Tabs}.${quickAccessMenuClasses.TabsWithFooter} {
margin-bottom: 0 !important;
padding-bottom: 0 !important;
}
`}
</style>
</DeckyStateContextProvider>
),
});
-1
View File
@@ -1,4 +1,3 @@
import type { JSX } from 'react';
export enum PluginLoadType {
LEGACY_EVAL_IIFE = 0, // legacy, uses legacy serverAPI
ESMODULE_V1 = 1, // esmodule loading with modern @decky/backend apis
+5 -5
View File
@@ -9,7 +9,7 @@ import {
getReactRoot,
sleep,
} from '@decky/ui';
import { FC, JSX, ReactElement, ReactNode, cloneElement, createElement } from 'react';
import { FC, ReactElement, ReactNode, cloneElement, createElement } from 'react';
import type { Route } from 'react-router';
import {
@@ -37,7 +37,7 @@ const isPatched = Symbol('is patched');
class RouterHook extends Logger {
private routerState: DeckyRouterState = new DeckyRouterState();
private globalComponentsState: DeckyGlobalComponentsState = new DeckyGlobalComponentsState();
private renderedComponents: ReactElement<any>[] = [];
private renderedComponents: ReactElement[] = [];
private Route: any;
private DeckyGamepadRouterWrapper = this.gamepadRouterWrapper.bind(this);
private DeckyDesktopRouterWrapper = this.desktopRouterWrapper.bind(this);
@@ -233,7 +233,7 @@ class RouterHook extends Logger {
return <>{this.renderedComponents}</>;
}
private gamepadRouterWrapper({ children }: { children: ReactElement<any> }) {
private gamepadRouterWrapper({ children }: { children: ReactElement }) {
// Used to store the new replicated routes we create to allow routes to be unpatched.
const { routes, routePatches } = useDeckyRouterState();
@@ -251,7 +251,7 @@ class RouterHook extends Logger {
return children;
}
private desktopRouterWrapper({ children }: { children: ReactElement<any> }) {
private desktopRouterWrapper({ children }: { children: ReactElement }) {
// Used to store the new replicated routes we create to allow routes to be unpatched.
this.debug('desktop router wrapper render', children);
const { routes, routePatches } = useDeckyRouterState();
@@ -287,7 +287,7 @@ class RouterHook extends Logger {
if (routes) {
if (!routeList[routerIndex - 1]?.length || routeList[routerIndex - 1]?.length !== routes.size) {
if (routeList[routerIndex - 1]?.length && routeList[routerIndex - 1].length !== routes.size) routerIndex--;
const newRouterArray: (ReactElement<any> | JSX.Element)[] = [];
const newRouterArray: (ReactElement | JSX.Element)[] = [];
routes.forEach(({ component, props }, path) => {
newRouterArray.push(
<Route path={path} {...props}>
+4 -3
View File
@@ -2,7 +2,9 @@
"compilerOptions": {
"module": "ESNext",
"target": "ES2021",
"jsx": "react-jsx",
"jsx": "react",
"jsxFactory": "window.SP_REACT.createElement",
"jsxFragmentFactory": "window.SP_REACT.Fragment",
"declaration": false,
"moduleResolution": "node",
"noUnusedLocals": true,
@@ -13,8 +15,7 @@
"noImplicitAny": true,
"strict": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"skipLibCheck": true
"resolveJsonModule": true
},
"include": ["src", "index.d.ts"],
"exclude": ["node_modules"]
+5 -12
View File
@@ -20,10 +20,7 @@ if [ -z "$INSIDE_NIX_RANDOMSTRING" ] && command -v nix &> /dev/null; then
exit $?
fi
[[ -f "$HOME/.config/deckdebug/config.sh" ]] && source "$HOME/.config/deckdebug/config.sh"
CHROMIUM="${CHROMIUM:-chromium}"
required_dependencies=(websocat jq curl $CHROMIUM)
required_dependencies=(websocat jq curl chromium)
# Check if the dependencies are installed
for cmd in "${required_dependencies[@]}"; do
@@ -33,7 +30,7 @@ for cmd in "${required_dependencies[@]}"; do
fi
done
$CHROMIUM --remote-debugging-port=9222 &
chromium --remote-debugging-port=9222 &
sleep 2
ADDR=$1
@@ -52,14 +49,10 @@ while :; do
TARGET=$NEWTARGET
TARGETURL="http://$ADDR/devtools/inspector.html?ws=$ADDR/devtools/page/$TARGET"
echo '{"id": 1, "method": "Target.createTarget", "params": {"background": true, "url": "'$TARGETURL'"}}
LOCALTARGET=$(echo '{"id": 1, "method": "Target.createTarget", "params": {"background": true, "url": "'$TARGETURL'"}}
{"id": 2, "method": "Target.closeTarget", "params": {"targetId": "'$LOCALTARGET'"}}' \
| websocat -t ws://$LOCAL/devtools/page/$LOCALTARGET
sleep 2
LOCALTARGETS=$(curl -s http://$LOCAL/json/list)
LOCALTARGET=$(jq -r '.[] | select(.title | startswith("DevTools")) | .id' <<< "$LOCALTARGETS")
| websocat ws://$LOCAL/devtools/page/$LOCALTARGET \
| jq -r '.result.targetId')
echo started devtools at $LOCALTARGET
fi