Compare commits

...

20 Commits

Author SHA1 Message Date
TrainDoctor faf46ba533 Update edit-check.yml 2023-03-09 16:32:54 -08:00
TrainDoctor 94ec434eae Update edit-check.yml 2023-03-09 16:31:43 -08:00
TrainDoctor a223efd6f5 Update edit-check.yml 2023-03-09 10:24:01 -08:00
suchmememanyskill 395e45167d Shared Ctx tab rename to SharedJSContext (#395) 2023-03-09 18:58:19 +01:00
TrainDoctor 0dd0d9f4bd Add CI to automatically update plugin stub in template 2023-03-05 16:23:17 -08:00
geeksville 3e5404abdd fix #390 the plugin_directory argument to import_plugin was incorrect (#391) 2023-03-05 14:30:26 -08:00
Beebles 46abc5a266 Fix QAM And Toaster Injection for Mar 02 Beta (#388) 2023-03-01 20:20:31 -08:00
TrainDoctor 88e1e9b869 Update README.md 2023-02-25 10:30:20 -08:00
TrainDoctor fc0089f7a5 Update bug_report.yml 2023-02-25 07:26:58 -08:00
AAGaming d335562328 update NavigateToExternalWeb in Markdown to use Navigation 2023-02-22 22:17:28 -05:00
AAGaming f9624a0859 how did this ever happen 2023-02-22 22:03:19 -05:00
AAGaming 97bb3fa4c8 Fix loader on feb 22 2023 beta 2023-02-22 22:00:30 -05:00
TrainDoctor 611245aec9 Update bug_report.yml 2023-02-22 17:38:46 -08:00
suchmememanyskill e1807e8c75 General Backend Fixes (#373)
* General Backend Fixes

* Ajust helpers.get_loader_version() to never throw an exception
2023-02-19 16:37:26 -08:00
TrainDoctor b94cfe32d9 Update README.md 2023-02-19 16:22:26 -08:00
Philipp Richter f1e679c3fb Expose a 'decky_plugin' module to decky plugins (#353)
* Expose a 'decky_plugin' module to decky plugins

* expose decky user home path
* support 'py_modules' python modules in plugins
* allow for a '_migration' method in plugins to have an explicit file
  moving step

* Expose the plugin python module as .pyi stub interface

* Expose system and user python paths to plugins
2023-02-19 14:42:55 -08:00
Beebles e1b138bcbd Fix fullscreen route inject issues caused by Feb. 17th beta. (#372)
* remove gamepad ui

* Refactor
2023-02-17 17:27:20 -08:00
Kevin Hester c6be8f6c14 Minor README fix for build instructions. (#370) 2023-02-17 14:10:03 -08:00
TrainDoctor ac086cf59e Update README.md 2023-02-08 18:43:33 -08:00
Marco Rodolfi 3e120ea312 Fix class name shenanigans for toast notification (#366)
* Fix class name shenanigans for  toast notification

* Corrected number of iterations
2023-02-06 17:30:44 -08:00
29 changed files with 534 additions and 103 deletions
+2 -1
View File
@@ -12,6 +12,7 @@ body:
- label: I have searched existing issues
- label: This issue is not a duplicate of an existing one
- label: I have checked the [common issues section in the readme file](https://github.com/SteamDeckHomebrew/decky-loader#-common-issues)
- label: I have attached logs to this bug report (failure to include logs will mean your issue will not be responded too).
- type: textarea
attributes:
@@ -70,4 +71,4 @@ body:
description: Please reboot your deck (if possible) when attempting to recreate the issue, then run ``cd ~ && journalctl -b0 -u plugin_loader.service > deckylog.txt``. This will save the log file to ``~`` aka ``/home/deck``. Please upload the file here
placeholder: deckylog.txt
validations:
required: false
required: true
+1 -1
View File
@@ -69,7 +69,7 @@ jobs:
run: pnpm run build
- name: Build Python Backend 🛠️
run: pyinstaller --noconfirm --onefile --name "PluginLoader" --add-data ./backend/static:/static --add-data ./backend/legacy:/legacy ./backend/*.py
run: pyinstaller --noconfirm --onefile --name "PluginLoader" --add-data ./backend/static:/static --add-data ./backend/legacy:/legacy --add-data ./plugin:/plugin ./backend/*.py
- name: Upload package artifact ⬆️
if: ${{ !env.ACT }}
+55
View File
@@ -0,0 +1,55 @@
name: Push Updated Plugin Stub to Template
on: push
jobs:
copy-stub:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@8230315d06ad95c617244d2f265d237a1682d445
with:
ref: ${{ github.sha }}
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Checkout
uses: actions/checkout@v2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v35.6.3
with:
separator: ","
files: |
plugin/*
- name: Is stub changed
id: changed-stub
run: |
STUB_CHANGED="false"
PATHS=(plugin plugin/decky_plugin.pyi)
SHA=${{ github.sha }}
SHA_PREV=HEAD^
FILES=$(git diff $SHA_PREV..$SHA --name-only -- ${PATHS[@]} | jq -Rsc 'split("\n")[:-1] | join (",")')
if [[ "$FILES" == *"plugin/decky_plugin.pyi"* ]]; then
$STUB_CHANGED="true"
echo "Stub has changed, pushing updated stub"
else
echo "Stub has not changed, exiting."
echo "has_changed=$STUB_CHANGED" >> $GITHUB_OUTPUT
exit 0
fi
echo "has_changed=$STUB_CHANGED" >> $GITHUB_OUTPUT
- name: Push updated stub
if: steps.changed-stub.outputs.has_changed == true
uses: dmnemec/copy_file_to_another_repo_action@bbebd3da22e4a37d04dca5f782edd5201cb97083
env:
API_TOKEN_GITHUB: ${{ secrets.GITHUB_TOKEN }}
with:
source_file: 'plugin/decky_plugin.pyi'
destination_repo: 'SteamDeckHomebrew/decky-plugin-template'
user_email: '11465594+TrainDoctor@users.noreply.github.com'
user_name: 'TrainDoctor'
commit_message: 'Updated template with latest plugin stub changes'
+5 -4
View File
@@ -11,7 +11,7 @@
<a href="https://github.com/SteamDeckHomebrew/decky-loader/stargazers"><img src="https://img.shields.io/github/stars/SteamDeckHomebrew/decky-loader" /></a>
<a href="https://github.com/SteamDeckHomebrew/decky-loader/commits/main"><img src="https://img.shields.io/github/last-commit/SteamDeckHomebrew/decky-loader.svg" /></a>
<a href="https://github.com/SteamDeckHomebrew/decky-loader/blob/main/LICENSE"><img src="https://img.shields.io/github/license/SteamDeckHomebrew/decky-loader" /></a>
<a href="https://discord.gg/ZU74G2NJzk"><img src="https://img.shields.io/discord/960281551428522045?color=%235865F2&label=discord" /></a>
<a href="https://deckbrew.xyz/discord"><img src="https://img.shields.io/discord/960281551428522045?color=%235865F2&label=discord" /></a>
<br>
<br>
<img src="https://media.discordapp.net/attachments/966017112244125756/1012466063893610506/main.jpg" alt="Decky screenshot" width="80%">
@@ -62,7 +62,7 @@ For more information about Decky Loader as well as documentation and development
### 👋 Uninstallation
We are sorry to see you go! If you are considering uninstalling because you are having issues, please consider [opening an issue](https://github.com/SteamDeckHomebrew/decky-loader/issues) or [joining our Discord](https://discord.gg/ZU74G2NJzk) so we can help you and other users.
We are sorry to see you go! If you are considering uninstalling because you are having issues, please consider [opening an issue](https://github.com/SteamDeckHomebrew/decky-loader/issues) or [joining our Discord](https://deckbrew.xyz/discord) so we can help you and other users.
1. Press the <img src="./docs/images/light/steam.svg#gh-dark-mode-only" height=16><img src="./docs/images/dark/steam.svg#gh-light-mode-only" height=16> button and open the Power menu.
1. Select "Switch to Desktop".
@@ -84,15 +84,16 @@ Now that you have Decky Loader installed, you can start using plugins. Each plug
### 🛠️ Plugin Development
There is no complete plugin development documentation yet. However a good starting point is the [plugin template repository](https://github.com/SteamDeckHomebrew/decky-plugin-template). Consider [joining our Discord](https://discord.gg/ZU74G2NJzk) if you have any questions.
There is no complete plugin development documentation yet. However a good starting point is the [plugin template repository](https://github.com/SteamDeckHomebrew/decky-plugin-template). Consider [joining our Discord](https://deckbrew.xyz/discord) if you have any questions.
### 🤝 Contributing
Please consult [the wiki page regarding development](https://deckbrew.xyz/en/loader-dev/development) for more information on installing development versions of Decky Loader. You can also install the Steam Deck UI on a Windows or Linux computer for testing by following [this YouTube guide](https://youtu.be/1IAbZte8e7E?t=112).
Please consult [the wiki page regarding development](https://wiki.deckbrew.xyz/en/loader-dev/development) for more information on installing development versions of Decky Loader. You can also install the Steam Deck UI on a Windows or Linux computer for testing by following [this YouTube guide](https://youtu.be/1IAbZte8e7E?t=112).
1. Clone the repository using the latest commit to main before starting your PR.
1. In your clone of the repository, run these commands.
```bash
cd frontend
pnpm i
pnpm run build
```
+10 -7
View File
@@ -42,7 +42,7 @@ class PluginBrowser:
return False
zip_file = ZipFile(zip)
zip_file.extractall(self.plugin_path)
plugin_dir = self.find_plugin_folder(name)
plugin_dir = path.join(self.plugin_path, self.find_plugin_folder(name))
code_chown = call(["chown", "-R", get_user()+":"+get_user_group(), plugin_dir])
code_chmod = call(["chmod", "-R", "555", plugin_dir])
if code_chown != 0 or code_chmod != 0:
@@ -92,6 +92,7 @@ class PluginBrowser:
return rv
"""Return the filename (only) for the specified plugin"""
def find_plugin_folder(self, name):
for folder in listdir(self.plugin_path):
try:
@@ -99,7 +100,7 @@ class PluginBrowser:
plugin = json.load(f)
if plugin['name'] == name:
return str(path.join(self.plugin_path, folder))
return folder
except:
logger.debug(f"skipping {folder}")
@@ -107,9 +108,10 @@ class PluginBrowser:
if self.loader.watcher:
self.loader.watcher.disabled = True
tab = await get_gamepadui_tab()
plugin_dir = path.join(self.plugin_path, self.find_plugin_folder(name))
try:
logger.info("uninstalling " + name)
logger.info(" at dir " + self.find_plugin_folder(name))
logger.info(" at dir " + plugin_dir)
logger.debug("calling frontend unload for %s" % str(name))
res = await tab.evaluate_js(f"DeckyPluginLoader.unloadPlugin('{name}')")
logger.debug("result of unload from UI: %s", res)
@@ -123,11 +125,11 @@ class PluginBrowser:
del self.plugins[name]
logger.debug("Plugin %s was removed from the dictionary", name)
logger.debug("removing files %s" % str(name))
rmtree(self.find_plugin_folder(name))
rmtree(plugin_dir)
except FileNotFoundError:
logger.warning(f"Plugin {name} not installed, skipping uninstallation")
except Exception as e:
logger.error(f"Plugin {name} in {self.find_plugin_folder(name)} was not uninstalled")
logger.error(f"Plugin {name} in {plugin_dir} was not uninstalled")
logger.error(f"Error at %s", exc_info=e)
if self.loader.watcher:
self.loader.watcher.disabled = False
@@ -160,7 +162,8 @@ class PluginBrowser:
logger.debug("Unzipping...")
ret = self._unzip_to_plugin_dir(res_zip, name, hash)
if ret:
plugin_dir = self.find_plugin_folder(name)
plugin_folder = self.find_plugin_folder(name)
plugin_dir = path.join(self.plugin_path, plugin_folder)
ret = await self._download_remote_binaries_for_plugin_with_name(plugin_dir)
if ret:
logger.info(f"Installed {name} (Version: {version})")
@@ -168,7 +171,7 @@ class PluginBrowser:
self.loader.plugins[name].stop()
self.loader.plugins.pop(name, None)
await sleep(1)
self.loader.import_plugin(path.join(plugin_dir, "main.py"), plugin_dir)
self.loader.import_plugin(path.join(plugin_dir, "main.py"), plugin_folder)
else:
logger.fatal(f"Failed Downloading Remote Binaries")
else:
+12 -2
View File
@@ -115,8 +115,18 @@ def mkdir_as_user(path):
# Fetches the version of loader
def get_loader_version() -> str:
with open(os.path.join(os.path.dirname(sys.argv[0]), ".loader.version"), "r", encoding="utf-8") as version_file:
return version_file.readline().replace("\n", "")
try:
with open(os.path.join(os.getcwd(), ".loader.version"), "r", encoding="utf-8") as version_file:
return version_file.readline().strip()
except:
return "unknown"
# returns the appropriate system python paths
def get_system_pythonpaths() -> list[str]:
# run as normal normal user to also include user python paths
proc = subprocess.run(["python3", "-c", "import sys; print(':'.join(x for x in sys.path if x))"],
user=get_user_id(), env={}, capture_output=True)
return proc.stdout.decode().strip().split(":")
# Download Remote Binaries to local Plugin
async def download_remote_binary_to_path(url, binHash, path) -> bool:
+5 -3
View File
@@ -394,8 +394,10 @@ async def get_tab_lambda(test) -> Tab:
raise ValueError(f"Tab not found by lambda")
return tab
SHARED_CTX_NAMES = ["SharedJSContext", "Steam Shared Context presented by Valve™", "Steam", "SP"]
def tab_is_gamepadui(t: Tab) -> bool:
return "https://steamloopback.host/routes/" in t.url and (t.title == "Steam Shared Context presented by Valve™" or t.title == "Steam" or t.title == "SP")
return "https://steamloopback.host/routes/" in t.url and t.title in SHARED_CTX_NAMES
async def get_gamepadui_tab() -> Tab:
tabs = await get_tabs()
@@ -412,7 +414,7 @@ async def inject_to_tab(tab_name, js, run_async=False):
async def close_old_tabs():
tabs = await get_tabs()
for t in tabs:
if not t.title or (t.title != "Steam Shared Context presented by Valve™" and t.title != "Steam" and t.title != "SP"):
if not t.title or t.title not in SHARED_CTX_NAMES:
logger.debug("Closing tab: " + getattr(t, "title", "Untitled"))
await t.close()
await sleep(0.5)
await sleep(0.5)
+2 -7
View File
@@ -6,14 +6,9 @@ from pathlib import Path
from traceback import print_exc
from aiohttp import web
from genericpath import exists
from os.path import exists
from watchdog.events import RegexMatchingEventHandler
from watchdog.utils import UnsupportedLibc
try:
from watchdog.observers.inotify import InotifyObserver as Observer
except UnsupportedLibc:
from watchdog.observers.fsevents import FSEventsObserver as Observer
from watchdog.observers import Observer
from injector import get_tab, get_gamepadui_tab
from plugin import PluginWrapper
+10 -1
View File
@@ -9,7 +9,7 @@ from logging import getLogger
from traceback import format_exc
from os import path, setgid, setuid, environ
from signal import SIGINT, signal
from sys import exit
from sys import exit, path as syspath
from time import time
import helpers
from updater import Updater
@@ -66,6 +66,7 @@ class PluginWrapper:
environ["USER"] = "root" if "root" in self.flags else helpers.get_user()
environ["DECKY_VERSION"] = helpers.get_loader_version()
environ["DECKY_USER"] = helpers.get_user()
environ["DECKY_USER_HOME"] = helpers.get_home_path()
environ["DECKY_HOME"] = helpers.get_homebrew_path()
environ["DECKY_PLUGIN_SETTINGS_DIR"] = path.join(environ["DECKY_HOME"], "settings", self.plugin_directory)
helpers.mkdir_as_user(environ["DECKY_PLUGIN_SETTINGS_DIR"])
@@ -77,11 +78,19 @@ class PluginWrapper:
environ["DECKY_PLUGIN_NAME"] = self.name
environ["DECKY_PLUGIN_VERSION"] = self.version
environ["DECKY_PLUGIN_AUTHOR"] = self.author
# append the loader's plugin path to the recognized python paths
syspath.append(path.realpath(path.join(path.dirname(__file__), "plugin")))
# append the plugin's `py_modules` to the recognized python paths
syspath.append(path.join(environ["DECKY_PLUGIN_DIR"], "py_modules"))
# append the system and user python paths
syspath.extend(helpers.get_system_pythonpaths())
spec = spec_from_file_location("_", self.file)
module = module_from_spec(spec)
spec.loader.exec_module(module)
self.Plugin = module.Plugin
if hasattr(self.Plugin, "_migration"):
get_event_loop().run_until_complete(self.Plugin._migration(self.Plugin))
if hasattr(self.Plugin, "_main"):
get_event_loop().create_task(self.Plugin._main(self.Plugin))
get_event_loop().create_task(self._setup_socket())
+10 -16
View File
@@ -30,10 +30,7 @@ class Updater:
}
self.remoteVer = None
self.allRemoteVers = None
try:
self.localVer = helpers.get_loader_version()
except:
self.localVer = False
self.localVer = helpers.get_loader_version()
try:
self.currentBranch = self.get_branch(self.context.settings)
@@ -68,7 +65,7 @@ class Updater:
logger.debug("current branch: %i" % ver)
if ver == -1:
logger.info("Current branch is not set, determining branch from version...")
if self.localVer.startswith("v") and self.localVer.find("-pre"):
if self.localVer.startswith("v") and "-pre" in self.localVer:
logger.info("Current version determined to be pre-release")
return 1
else:
@@ -94,15 +91,12 @@ class Updater:
return str(url)
async def get_version(self):
if self.localVer:
return {
"current": self.localVer,
"remote": self.remoteVer,
"all": self.allRemoteVers,
"updatable": self.localVer != None
}
else:
return {"current": "unknown", "remote": self.remoteVer, "all": self.allRemoteVers, "updatable": False}
return {
"current": self.localVer,
"remote": self.remoteVer,
"all": self.allRemoteVers,
"updatable": self.localVer != "unknown"
}
async def check_for_updates(self):
logger.debug("checking for updates")
@@ -114,10 +108,10 @@ class Updater:
logger.debug("determining release type to find, branch is %i" % selectedBranch)
if selectedBranch == 0:
logger.debug("release type: release")
self.remoteVer = next(filter(lambda ver: ver["tag_name"].startswith("v") and not ver["prerelease"] and ver["tag_name"], remoteVersions), None)
self.remoteVer = next(filter(lambda ver: ver["tag_name"].startswith("v") and not ver["prerelease"] and not ver["tag_name"].find("-pre") > 0 and ver["tag_name"], remoteVersions), None)
elif selectedBranch == 1:
logger.debug("release type: pre-release")
self.remoteVer = next(filter(lambda ver: ver["prerelease"] and ver["tag_name"].startswith("v") and ver["tag_name"].find("-pre"), remoteVersions), None)
self.remoteVer = next(filter(lambda ver:ver["tag_name"].startswith("v"), remoteVersions), None)
else:
logger.error("release type: NOT FOUND")
raise ValueError("no valid branch found")
+1 -1
View File
@@ -251,7 +251,7 @@ class Utilities:
enumerable: true,
configurable: true,
get: function() {
return FocusNavController?.m_ActiveContext?.ActiveWindow || window;
return (GamepadNavTree?.m_context?.m_controller || FocusNavController)?.m_ActiveContext?.ActiveWindow || window;
}
});
""" + await res.text() + "\n}"
+1 -1
View File
@@ -42,7 +42,7 @@
}
},
"dependencies": {
"decky-frontend-lib": "^3.18.10",
"decky-frontend-lib": "^3.19.1",
"react-file-icon": "^1.2.0",
"react-icons": "^4.4.0",
"react-markdown": "^8.0.3",
+4 -4
View File
@@ -11,7 +11,7 @@ specifiers:
'@types/react-file-icon': ^1.0.1
'@types/react-router': 5.1.18
'@types/webpack': ^5.28.0
decky-frontend-lib: ^3.18.10
decky-frontend-lib: ^3.19.1
husky: ^8.0.1
import-sort-style-module: ^6.0.0
inquirer: ^8.2.4
@@ -31,7 +31,7 @@ specifiers:
typescript: ^4.7.4
dependencies:
decky-frontend-lib: 3.18.10
decky-frontend-lib: 3.19.1
react-file-icon: 1.2.0_wcqkhtmu7mswc6yz4uyexck3ty
react-icons: 4.4.0_react@16.14.0
react-markdown: 8.0.3_vshvapmxg47tngu7tvrsqpq55u
@@ -975,8 +975,8 @@ packages:
dependencies:
ms: 2.1.2
/decky-frontend-lib/3.18.10:
resolution: {integrity: sha512-2mgbA3sSkuwQR/FnmhXVrcW6LyTS95IuL6muJAmQCruhBvXapDtjk1TcgxqMZxFZwGD1IPnemPYxHZll6IgnZw==}
/decky-frontend-lib/3.19.1:
resolution: {integrity: sha512-hU4+EFs74MGzUCv8l1AO2+EBj9RRbnpU19Crm4u+3lbLu6d63U2GsUeQ9ssmNRcOMY1OuVZkRoZBE58soOBJ3A==}
dev: false
/decode-named-character-reference/1.0.2:
+2 -2
View File
@@ -1,4 +1,4 @@
import { Focusable, Router } from 'decky-frontend-lib';
import { Focusable, Navigation } from 'decky-frontend-lib';
import { FunctionComponent, useRef } from 'react';
import ReactMarkdown, { Options as ReactMarkdownOptions } from 'react-markdown';
import remarkGfm from 'remark-gfm';
@@ -22,7 +22,7 @@ const Markdown: FunctionComponent<MarkdownProps> = (props) => {
onActivate={() => {}}
onOKButton={() => {
props.onDismiss?.();
Router.NavigateToExternalWeb(aRef.current!.href);
Navigation.NavigateToExternalWeb(aRef.current!.href);
}}
style={{ display: 'inline' }}
>
@@ -4,15 +4,11 @@ const QuickAccessVisibleState = createContext<boolean>(true);
export const useQuickAccessVisible = () => useContext(QuickAccessVisibleState);
export const QuickAccessVisibleStateProvider: FC<{ initial: boolean; setter: ((val: boolean) => {}[]) | never[] }> = ({
children,
initial,
setter,
}) => {
export const QuickAccessVisibleStateProvider: FC<{ initial: boolean; tab: any }> = ({ children, initial, tab }) => {
const [visible, setVisible] = useState<boolean>(initial);
const [prev, setPrev] = useState<boolean>(initial);
// hack to use an array as a "pointer" to pass the setter up the tree
setter[0] = setVisible;
// HACK but i can't think of a better way to do this
tab.qAMVisibilitySetter = setVisible;
if (initial != prev) {
setPrev(initial);
setVisible(initial);
@@ -4,13 +4,6 @@ import Logger from '../../../../logger';
const logger = new Logger('LibraryPatch');
declare global {
interface Window {
SteamClient: any;
appDetailsStore: any;
}
}
let patch: Patch;
function rePatch() {
@@ -20,7 +13,9 @@ function rePatch() {
const details = window.appDetailsStore.GetAppDetails(appid);
logger.debug('game details', details);
// strShortcutStartDir
const file = await window.DeckyPluginLoader.openFilePicker(details.strShortcutStartDir.replaceAll('"', ''));
const file = await window.DeckyPluginLoader.openFilePicker(
details?.strShortcutStartDir.replaceAll('"', '') || '/',
);
logger.debug('user selected', file);
window.SteamClient.Apps.SetShortcutExe(appid, JSON.stringify(file.path));
const pathArr = file.path.split('/');
@@ -52,7 +52,7 @@ export default function DeveloperSettings() {
>
<Toggle
value={reactDevtoolsEnabled}
disabled={reactDevtoolsIP == ''}
// disabled={reactDevtoolsIP == ''}
onChange={(toggleValue) => {
setReactDevtoolsEnabled(toggleValue);
setShouldConnectToReactDevTools(toggleValue);
@@ -6,6 +6,7 @@ import {
Focusable,
ProgressBarWithInfo,
Spinner,
findSP,
showModal,
} from 'decky-frontend-lib';
import { useCallback } from 'react';
@@ -14,7 +15,6 @@ import { useEffect, useState } from 'react';
import { FaExclamation } from 'react-icons/fa';
import { VerInfo, callUpdaterMethod, finishUpdate } from '../../../../updater';
import { findSP } from '../../../../utils/windows';
import { useDeckyState } from '../../../DeckyState';
import InlinePatchNotes from '../../../patchnotes/InlinePatchNotes';
import WithSuspense from '../../../WithSuspense';
+1 -1
View File
@@ -15,7 +15,7 @@ import Logger from '../../logger';
import { StorePlugin, getPluginList } from '../../store';
import PluginCard from './PluginCard';
const logger = new Logger('FilePicker');
const logger = new Logger('Store');
const StorePage: FC<{}> = () => {
const [currentTabRoute, setCurrentTabRoute] = useState<string>('browse');
+3 -5
View File
@@ -123,11 +123,9 @@ class RouterHook extends Logger {
this.wrapperPatch = afterPatch(this.gamepadWrapper, 'render', (_: any, ret: any) => {
if (ret?.props?.children?.props?.children?.length == 5 || ret?.props?.children?.props?.children?.length == 4) {
const idx = ret?.props?.children?.props?.children?.length == 4 ? 1 : 2;
if (
ret.props.children.props.children[idx]?.props?.children?.[0]?.type?.type
?.toString()
?.includes('GamepadUI.Settings.Root()')
) {
const potentialSettingsRootString =
ret.props.children.props.children[idx]?.props?.children?.[0]?.type?.type?.toString() || '';
if (potentialSettingsRootString?.includes('Settings.Root()')) {
if (!this.router) {
this.router = ret.props.children.props.children[idx]?.props?.children?.[0]?.type;
this.routerPatch = afterPatch(this.router, 'type', (_: any, ret: any) => {
+1 -1
View File
@@ -7,6 +7,6 @@ export function deinitSteamFixes() {
}
export async function initSteamFixes() {
fixes.push(reloadFix());
fixes.push(await reloadFix());
fixes.push(await restartFix());
}
+9 -2
View File
@@ -1,10 +1,17 @@
import { getFocusNavController, sleep } from 'decky-frontend-lib';
import Logger from '../logger';
const logger = new Logger('ReloadSteamFix');
export default function reloadFix() {
declare global {
var GamepadNavTree: any;
}
export default async function reloadFix() {
// Hack to unbreak the ui when reloading it
if (window.FocusNavController?.m_rgAllContexts?.length == 0) {
await sleep(4000);
if (getFocusNavController()?.m_rgAllContexts?.length == 0) {
SteamClient.URL.ExecuteSteamURL('steam://open/settings');
logger.log('Applied UI reload fix.');
}
-7
View File
@@ -4,13 +4,6 @@ import Logger from '../logger';
const logger = new Logger('RestartSteamFix');
declare global {
interface Window {
SteamClient: any;
appDetailsStore: any;
}
}
let patch: Patch;
function rePatch() {
+10 -9
View File
@@ -35,7 +35,7 @@ class TabsHook extends Logger {
const tree = (document.getElementById('root') as any)._reactRootContainer._internalRoot.current;
let qAMRoot: any;
const findQAMRoot = (currentNode: any, iters: number): any => {
if (iters >= 55) {
if (iters >= 65) {
// currently 45
return null;
}
@@ -128,22 +128,23 @@ class TabsHook extends Logger {
let deckyTabAmount = existingTabs.reduce((prev: any, cur: any) => (cur.decky ? prev + 1 : prev), 0);
if (deckyTabAmount == this.tabs.length) {
for (let tab of existingTabs) {
if (tab?.decky) tab.panel.props.setter[0](visible);
if (tab?.decky && tab?.qAMVisibilitySetter) tab?.qAMVisibilitySetter(visible);
}
return;
}
for (const { title, icon, content, id } of this.tabs) {
existingTabs.push({
const tab: any = {
key: id,
title,
tab: icon,
decky: true,
panel: (
<QuickAccessVisibleStateProvider initial={visible} setter={[]}>
{content}
</QuickAccessVisibleStateProvider>
),
});
};
tab.panel = (
<QuickAccessVisibleStateProvider initial={visible} tab={tab}>
{content}
</QuickAccessVisibleStateProvider>
);
existingTabs.push(tab);
}
}
}
+7 -3
View File
@@ -40,11 +40,15 @@ class Toaster extends Logger {
let instance: any;
const tree = (document.getElementById('root') as any)._reactRootContainer._internalRoot.current;
const findToasterRoot = (currentNode: any, iters: number): any => {
if (iters >= 50) {
// currently 40
if (iters >= 65) {
// currently 65
return null;
}
if (currentNode?.memoizedProps?.className?.startsWith?.('toastmanager_ToastPlaceholder')) {
if (
currentNode?.memoizedProps?.className?.startsWith?.('gamepadtoasts_GamepadToastPlaceholder') ||
currentNode?.memoizedProps?.className?.startsWith?.('toastmanager_ToastPlaceholder') ||
currentNode?.memoizedProps?.className?.startsWith?.('toastmanager_ToastPopup')
) {
this.log(`Toaster root was found in ${iters} recursion cycles`);
return currentNode;
}
-7
View File
@@ -1,7 +0,0 @@
export function findSP(): Window {
// old (SP as host)
if (document.title == 'SP') return window;
// new (SP as popup)
return FocusNavController.m_ActiveContext.m_rgGamepadNavigationTrees.find((x: any) => x.m_ID == 'root_1_').Root
.Element.ownerDocument.defaultView;
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"module": "ESNext",
"target": "ES2020",
"target": "ES2021",
"jsx": "react",
"jsxFactory": "window.SP_REACT.createElement",
"jsxFragmentFactory": "window.SP_REACT.Fragment",
+201
View File
@@ -0,0 +1,201 @@
"""
This module exposes various constants and helpers useful for decky plugins.
* Plugin's settings and configurations should be stored under `DECKY_PLUGIN_SETTINGS_DIR`.
* Plugin's runtime data should be stored under `DECKY_PLUGIN_RUNTIME_DIR`.
* Plugin's persistent log files should be stored under `DECKY_PLUGIN_LOG_DIR`.
Avoid writing outside of `DECKY_HOME`, storing under the suggested paths is strongly recommended.
Some basic migration helpers are available: `migrate_any`, `migrate_settings`, `migrate_runtime`, `migrate_logs`.
A logging facility `logger` is available which writes to the recommended location.
"""
__version__ = '0.1.0'
import os
import subprocess
import logging
"""
Constants
"""
HOME: str = os.getenv("HOME", default="")
"""
The home directory of the effective user running the process.
Environment variable: `HOME`.
If `root` was specified in the plugin's flags it will be `/root` otherwise the user whose home decky resides in.
e.g.: `/home/deck`
"""
USER: str = os.getenv("USER", default="")
"""
The effective username running the process.
Environment variable: `USER`.
It would be `root` if `root` was specified in the plugin's flags otherwise the user whose home decky resides in.
e.g.: `deck`
"""
DECKY_VERSION: str = os.getenv("DECKY_VERSION", default="")
"""
The version of the decky loader.
Environment variable: `DECKY_VERSION`.
e.g.: `v2.5.0-pre1`
"""
DECKY_USER: str = os.getenv("DECKY_USER", default="")
"""
The user whose home decky resides in.
Environment variable: `DECKY_USER`.
e.g.: `deck`
"""
DECKY_USER_HOME: str = os.getenv("DECKY_USER_HOME", default="")
"""
The home of the user where decky resides in.
Environment variable: `DECKY_USER_HOME`.
e.g.: `/home/deck`
"""
DECKY_HOME: str = os.getenv("DECKY_HOME", default="")
"""
The root of the decky folder.
Environment variable: `DECKY_HOME`.
e.g.: `/home/deck/homebrew`
"""
DECKY_PLUGIN_SETTINGS_DIR: str = os.getenv(
"DECKY_PLUGIN_SETTINGS_DIR", default="")
"""
The recommended path in which to store configuration files (created automatically).
Environment variable: `DECKY_PLUGIN_SETTINGS_DIR`.
e.g.: `/home/deck/homebrew/settings/decky-plugin-template`
"""
DECKY_PLUGIN_RUNTIME_DIR: str = os.getenv(
"DECKY_PLUGIN_RUNTIME_DIR", default="")
"""
The recommended path in which to store runtime data (created automatically).
Environment variable: `DECKY_PLUGIN_RUNTIME_DIR`.
e.g.: `/home/deck/homebrew/data/decky-plugin-template`
"""
DECKY_PLUGIN_LOG_DIR: str = os.getenv("DECKY_PLUGIN_LOG_DIR", default="")
"""
The recommended path in which to store persistent logs (created automatically).
Environment variable: `DECKY_PLUGIN_LOG_DIR`.
e.g.: `/home/deck/homebrew/logs/decky-plugin-template`
"""
DECKY_PLUGIN_DIR: str = os.getenv("DECKY_PLUGIN_DIR", default="")
"""
The root of the plugin's directory.
Environment variable: `DECKY_PLUGIN_DIR`.
e.g.: `/home/deck/homebrew/plugins/decky-plugin-template`
"""
DECKY_PLUGIN_NAME: str = os.getenv("DECKY_PLUGIN_NAME", default="")
"""
The name of the plugin as specified in the 'plugin.json'.
Environment variable: `DECKY_PLUGIN_NAME`.
e.g.: `Example Plugin`
"""
DECKY_PLUGIN_VERSION: str = os.getenv("DECKY_PLUGIN_VERSION", default="")
"""
The version of the plugin as specified in the 'package.json'.
Environment variable: `DECKY_PLUGIN_VERSION`.
e.g.: `0.0.1`
"""
DECKY_PLUGIN_AUTHOR: str = os.getenv("DECKY_PLUGIN_AUTHOR", default="")
"""
The author of the plugin as specified in the 'plugin.json'.
Environment variable: `DECKY_PLUGIN_AUTHOR`.
e.g.: `John Doe`
"""
DECKY_PLUGIN_LOG: str = os.path.join(DECKY_PLUGIN_LOG_DIR, "plugin.log")
"""
The path to the plugin's main logfile.
Environment variable: `DECKY_PLUGIN_LOG`.
e.g.: `/home/deck/homebrew/logs/decky-plugin-template/plugin.log`
"""
"""
Migration helpers
"""
def migrate_any(target_dir: str, *files_or_directories: str) -> dict[str, str]:
"""
Migrate files and directories to a new location and remove old locations.
Specified files will be migrated to `target_dir`.
Specified directories will have their contents recursively migrated to `target_dir`.
Returns the mapping of old -> new location.
"""
file_map: dict[str, str] = {}
for f in files_or_directories:
if not os.path.exists(f):
file_map[f] = ""
continue
if os.path.isdir(f):
src_dir = f
src_file = "."
file_map[f] = target_dir
else:
src_dir = os.path.dirname(f)
src_file = os.path.basename(f)
file_map[f] = os.path.join(target_dir, src_file)
subprocess.run(["sh", "-c", "mkdir -p \"$3\"; tar -cf - -C \"$1\" \"$2\" | tar -xf - -C \"$3\" && rm -rf \"$4\"",
"_", src_dir, src_file, target_dir, f])
return file_map
def migrate_settings(*files_or_directories: str) -> dict[str, str]:
"""
Migrate files and directories relating to plugin settings to the recommended location and remove old locations.
Specified files will be migrated to `DECKY_PLUGIN_SETTINGS_DIR`.
Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_SETTINGS_DIR`.
Returns the mapping of old -> new location.
"""
return migrate_any(DECKY_PLUGIN_SETTINGS_DIR, *files_or_directories)
def migrate_runtime(*files_or_directories: str) -> dict[str, str]:
"""
Migrate files and directories relating to plugin runtime data to the recommended location and remove old locations
Specified files will be migrated to `DECKY_PLUGIN_RUNTIME_DIR`.
Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_RUNTIME_DIR`.
Returns the mapping of old -> new location.
"""
return migrate_any(DECKY_PLUGIN_RUNTIME_DIR, *files_or_directories)
def migrate_logs(*files_or_directories: str) -> dict[str, str]:
"""
Migrate files and directories relating to plugin logs to the recommended location and remove old locations.
Specified files will be migrated to `DECKY_PLUGIN_LOG_DIR`.
Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_LOG_DIR`.
Returns the mapping of old -> new location.
"""
return migrate_any(DECKY_PLUGIN_LOG_DIR, *files_or_directories)
"""
Logging
"""
logging.basicConfig(filename=DECKY_PLUGIN_LOG,
format='[%(asctime)s][%(levelname)s]: %(message)s',
force=True)
logger: logging.Logger = logging.getLogger()
"""The main plugin logger writing to `DECKY_PLUGIN_LOG`."""
logger.setLevel(logging.INFO)
+173
View File
@@ -0,0 +1,173 @@
"""
This module exposes various constants and helpers useful for decky plugins.
* Plugin's settings and configurations should be stored under `DECKY_PLUGIN_SETTINGS_DIR`.
* Plugin's runtime data should be stored under `DECKY_PLUGIN_RUNTIME_DIR`.
* Plugin's persistent log files should be stored under `DECKY_PLUGIN_LOG_DIR`.
Avoid writing outside of `DECKY_HOME`, storing under the suggested paths is strongly recommended.
Some basic migration helpers are available: `migrate_any`, `migrate_settings`, `migrate_runtime`, `migrate_logs`.
A logging facility `logger` is available which writes to the recommended location.
"""
__version__ = '0.1.0'
import logging
"""
Constants
"""
HOME: str
"""
The home directory of the effective user running the process.
Environment variable: `HOME`.
If `root` was specified in the plugin's flags it will be `/root` otherwise the user whose home decky resides in.
e.g.: `/home/deck`
"""
USER: str
"""
The effective username running the process.
Environment variable: `USER`.
It would be `root` if `root` was specified in the plugin's flags otherwise the user whose home decky resides in.
e.g.: `deck`
"""
DECKY_VERSION: str
"""
The version of the decky loader.
Environment variable: `DECKY_VERSION`.
e.g.: `v2.5.0-pre1`
"""
DECKY_USER: str
"""
The user whose home decky resides in.
Environment variable: `DECKY_USER`.
e.g.: `deck`
"""
DECKY_USER_HOME: str
"""
The home of the user where decky resides in.
Environment variable: `DECKY_USER_HOME`.
e.g.: `/home/deck`
"""
DECKY_HOME: str
"""
The root of the decky folder.
Environment variable: `DECKY_HOME`.
e.g.: `/home/deck/homebrew`
"""
DECKY_PLUGIN_SETTINGS_DIR: str
"""
The recommended path in which to store configuration files (created automatically).
Environment variable: `DECKY_PLUGIN_SETTINGS_DIR`.
e.g.: `/home/deck/homebrew/settings/decky-plugin-template`
"""
DECKY_PLUGIN_RUNTIME_DIR: str
"""
The recommended path in which to store runtime data (created automatically).
Environment variable: `DECKY_PLUGIN_RUNTIME_DIR`.
e.g.: `/home/deck/homebrew/data/decky-plugin-template`
"""
DECKY_PLUGIN_LOG_DIR: str
"""
The recommended path in which to store persistent logs (created automatically).
Environment variable: `DECKY_PLUGIN_LOG_DIR`.
e.g.: `/home/deck/homebrew/logs/decky-plugin-template`
"""
DECKY_PLUGIN_DIR: str
"""
The root of the plugin's directory.
Environment variable: `DECKY_PLUGIN_DIR`.
e.g.: `/home/deck/homebrew/plugins/decky-plugin-template`
"""
DECKY_PLUGIN_NAME: str
"""
The name of the plugin as specified in the 'plugin.json'.
Environment variable: `DECKY_PLUGIN_NAME`.
e.g.: `Example Plugin`
"""
DECKY_PLUGIN_VERSION: str
"""
The version of the plugin as specified in the 'package.json'.
Environment variable: `DECKY_PLUGIN_VERSION`.
e.g.: `0.0.1`
"""
DECKY_PLUGIN_AUTHOR: str
"""
The author of the plugin as specified in the 'plugin.json'.
Environment variable: `DECKY_PLUGIN_AUTHOR`.
e.g.: `John Doe`
"""
DECKY_PLUGIN_LOG: str
"""
The path to the plugin's main logfile.
Environment variable: `DECKY_PLUGIN_LOG`.
e.g.: `/home/deck/homebrew/logs/decky-plugin-template/plugin.log`
"""
"""
Migration helpers
"""
def migrate_any(target_dir: str, *files_or_directories: str) -> dict[str, str]:
"""
Migrate files and directories to a new location and remove old locations.
Specified files will be migrated to `target_dir`.
Specified directories will have their contents recursively migrated to `target_dir`.
Returns the mapping of old -> new location.
"""
def migrate_settings(*files_or_directories: str) -> dict[str, str]:
"""
Migrate files and directories relating to plugin settings to the recommended location and remove old locations.
Specified files will be migrated to `DECKY_PLUGIN_SETTINGS_DIR`.
Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_SETTINGS_DIR`.
Returns the mapping of old -> new location.
"""
def migrate_runtime(*files_or_directories: str) -> dict[str, str]:
"""
Migrate files and directories relating to plugin runtime data to the recommended location and remove old locations
Specified files will be migrated to `DECKY_PLUGIN_RUNTIME_DIR`.
Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_RUNTIME_DIR`.
Returns the mapping of old -> new location.
"""
def migrate_logs(*files_or_directories: str) -> dict[str, str]:
"""
Migrate files and directories relating to plugin logs to the recommended location and remove old locations.
Specified files will be migrated to `DECKY_PLUGIN_LOG_DIR`.
Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_LOG_DIR`.
Returns the mapping of old -> new location.
"""
"""
Logging
"""
logger: logging.Logger
"""The main plugin logger writing to `DECKY_PLUGIN_LOG`."""