Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
65f1eb0
implement base frontend changes necessary for plugin disabling
jessebofill Oct 6, 2025
1ae6519
implement frontend diisable functions/ modal
jessebofill Oct 6, 2025
c3c0e87
plugin disable boilerplate / untested
marios8543 Oct 6, 2025
6fba202
Feat disable plugins (#810)
marios8543 Oct 6, 2025
d2b6999
fix mistakes
jessebofill Oct 6, 2025
6640279
add frontend
marios8543 Oct 6, 2025
6fac721
Merge branch 'feat-disable-plugins' into marios8543/plugin-disable
marios8543 Oct 6, 2025
968c8b1
working plugin disable, not tested extensively
marios8543 Oct 6, 2025
ffab38d
fix uninstalled hidden plugins remaining in list
jessebofill Oct 7, 2025
a8880fa
hide plugin irrelevant plugin setting menu option when disabled
jessebofill Oct 7, 2025
dd81d25
fix hidden plugin issues
jessebofill Oct 7, 2025
7741d74
reset disabled plugin on uninstall
jessebofill Oct 7, 2025
8f52407
fix plugin load on reenable
jessebofill Oct 7, 2025
9701d2d
move disable settings uninstall cleanup
jessebofill Oct 7, 2025
b8d2832
add engilsh tranlsations for enable/ disable elements
jessebofill Oct 7, 2025
bf7d326
fix bug where wrong loadType can get passed to importPlugin
jessebofill Oct 7, 2025
16248f9
show correct number of hidden plugins if plugin is both hidden and di…
jessebofill Oct 7, 2025
504bbd1
fix: get fresh list of plugin updates when changed in settings plugin…
jessebofill Oct 9, 2025
ab596c5
fix: fix invalid semver plugin version from preventing latest updates
jessebofill Oct 9, 2025
962a01d
retain x position when changing focus in list items that have multip…
jessebofill Oct 9, 2025
0bff2d4
correction to pluging version checking validation
jessebofill Oct 9, 2025
d43907e
make sure disabled plugins get checked for updates
jessebofill Oct 10, 2025
9725950
show number of disabled plugins at bottom of plugin view
jessebofill Oct 10, 2025
55978eb
add notice to update modals that disabled plugins will be enabled upo…
jessebofill Oct 10, 2025
aff7129
Merge remote-tracking branch 'origin/main' into feat-disable-plugins
jessebofill Nov 23, 2025
0d8e7d9
run formatter
jessebofill Nov 23, 2025
3935758
Update backend/decky_loader/locales/en-US.json
jessebofill Nov 28, 2025
ff850cd
chore: correct filename typo
jessebofill Nov 28, 2025
d2ccf7c
chore: change disabled icon
jessebofill Nov 28, 2025
188c4f9
chore: revert accidental defsettings changes
jessebofill Nov 28, 2025
744047c
Merge branch 'feat-disable-plugins' of https://github.com/jessebofill…
jessebofill Nov 28, 2025
93d09a7
format
jessebofill Dec 9, 2025
a5b6bd4
Merge branch 'minor-fixes' into feat-disable-plugins
jessebofill Dec 9, 2025
b396625
add timeout to frontend importPlugin
jessebofill Dec 9, 2025
9682a5a
fix plugin update checking loop
jessebofill Dec 24, 2025
9269206
Merge remote-tracking branch 'origin/main' into feat-disable-plugins
jessebofill Dec 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions backend/decky_loader/browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ async def uninstall_plugin(self, name: str):
# plugins_snapshot = self.plugins.copy()
# snapshot_string = pformat(plugins_snapshot)
# logger.debug("current plugins: %s", snapshot_string)

if name in self.plugins:
logger.debug("Plugin %s was found", name)
await self.plugins[name].stop(uninstall=True)
Expand Down Expand Up @@ -345,5 +346,10 @@ def cleanup_plugin_settings(self, name: str):
if name in plugin_order:
plugin_order.remove(name)
self.settings.setSetting("pluginOrder", plugin_order)

disabled_plugins: List[str] = self.settings.getSetting("disabled_plugins", [])
if name in disabled_plugins:
disabled_plugins.remove(name)
self.settings.setSetting("disabled_plugins", disabled_plugins)

logger.debug("Removed any settings for plugin %s", name)
9 changes: 7 additions & 2 deletions backend/decky_loader/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ def __init__(self, server_instance: PluginManager, ws: WSRouter, plugin_path: st
self.live_reload = live_reload
self.reload_queue: ReloadQueue = Queue()
self.loop.create_task(self.handle_reloads())
self.context: PluginManager = server_instance

if live_reload:
self.observer = Observer()
Expand Down Expand Up @@ -130,7 +131,7 @@ async def handle_frontend_locales(self, request: web.Request):

async def get_plugins(self):
plugins = list(self.plugins.values())
return [{"name": str(i), "version": i.version, "load_type": i.load_type} for i in plugins]
return [{"name": str(i), "version": i.version, "load_type": i.load_type, "disabled": i.disabled} for i in plugins]

async def handle_plugin_dist(self, request: web.Request):
plugin = self.plugins[request.match_info["plugin_name"]]
Expand Down Expand Up @@ -164,6 +165,10 @@ async def plugin_emitted_event(event: str, args: Any):
await self.ws.emit(f"loader/plugin_event", {"plugin": plugin.name, "event": event, "args": args})

plugin = PluginWrapper(file, plugin_directory, self.plugin_path, plugin_emitted_event)
if hasattr(self.context, "utilities") and plugin.name in await self.context.utilities.get_setting("disabled_plugins",[]):
plugin.disabled = True
self.plugins[plugin.name] = plugin
return
if plugin.name in self.plugins:
if not "debug" in plugin.flags and refresh:
self.logger.info(f"Plugin {plugin.name} is already loaded and has requested to not be re-loaded")
Expand All @@ -183,7 +188,7 @@ async def plugin_emitted_event(event: str, args: Any):
print_exc()

async def dispatch_plugin(self, name: str, version: str | None, load_type: int = PluginLoadType.ESMODULE_V1.value):
await self.ws.emit("loader/import_plugin", name, version, load_type)
await self.ws.emit("loader/import_plugin", name, version, load_type, True, 15000)

async def import_plugins(self):
self.logger.info(f"import plugins from {self.plugin_path}")
Expand Down
21 changes: 18 additions & 3 deletions backend/decky_loader/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
},
"no_hash": "This plugin does not have a hash, you are installing it at your own risk.",
"not_installed": "(not installed)",
"disabled": "The plugin will be re-enabled after installation",
"overwrite": {
"button_idle": "Overwrite",
"button_processing": "Overwriting",
Expand Down Expand Up @@ -133,10 +134,13 @@
"uninstall": "Uninstall",
"update_all_one": "Update 1 plugin",
"update_all_other": "Update {{count}} plugins",
"update_to": "Update to {{name}}"
"update_to": "Update to {{name}}",
"disable": "Disable",
"enable": "Enable"
},
"PluginListLabel": {
"hidden": "Hidden from the quick access menu"
"hidden": "Hidden from the quick access menu",
"disabled": "Plugin disabled"
},
"PluginLoader": {
"decky_title": "Decky",
Expand All @@ -152,12 +156,23 @@
"desc": "Are you sure you want to uninstall {{name}}?",
"title": "Uninstall {{name}}"
},
"plugin_disable": {
"button": "Disable",
"desc": "Are you sure you want to disable {{name}}?",
"title": "Disable {{name}}",
"error": "Error disabling {{name}}"
},
"plugin_enable": {
"error": "Error enabling {{name}}"
},
"plugin_update_one": "Updates available for 1 plugin!",
"plugin_update_other": "Updates available for {{count}} plugins!"
},
"PluginView": {
"hidden_one": "1 plugin is hidden from this list",
"hidden_other": "{{count}} plugins are hidden from this list"
"hidden_other": "{{count}} plugins are hidden from this list",
"disabled_one": "1 plugin is disabled",
"disabled_other": "{{count}} plugins are disabled"
},
"RemoteDebugging": {
"remote_cef": {
Expand Down
1 change: 1 addition & 0 deletions backend/decky_loader/plugin/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def __init__(self, file: str, plugin_directory: str, plugin_path: str, emit_call
self.author = json["author"]
self.flags = json["flags"]
self.api_version = json["api_version"] if "api_version" in json else 0
self.disabled = False

self.passive = not path.isfile(self.file)

Expand Down
38 changes: 34 additions & 4 deletions backend/decky_loader/utilities.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from __future__ import annotations
from os import stat_result
from os import path, stat_result
import uuid
from urllib.parse import unquote
from json.decoder import JSONDecodeError
Expand All @@ -8,7 +8,7 @@
from traceback import format_exc
from stat import FILE_ATTRIBUTE_HIDDEN # pyright: ignore [reportAttributeAccessIssue, reportUnknownVariableType]

from asyncio import StreamReader, StreamWriter, start_server, gather, open_connection
from asyncio import StreamReader, StreamWriter, sleep, start_server, gather, open_connection
from aiohttp import ClientSession, hdrs
from aiohttp.web import Request, StreamResponse, Response, json_response, post
from typing import TYPE_CHECKING, Callable, Coroutine, Dict, Any, List, TypedDict
Expand Down Expand Up @@ -80,6 +80,8 @@ def __init__(self, context: PluginManager) -> None:
context.ws.add_route("utilities/restart_webhelper", self.restart_webhelper)
context.ws.add_route("utilities/close_cef_socket", self.close_cef_socket)
context.ws.add_route("utilities/_call_legacy_utility", self._call_legacy_utility)
context.ws.add_route("utilities/enable_plugin", self.enable_plugin)
context.ws.add_route("utilities/disable_plugin", self.disable_plugin)

context.web_app.add_routes([
post("/methods/{method_name}", self._handle_legacy_server_method_call)
Expand Down Expand Up @@ -214,7 +216,7 @@ async def http_request(self, req: Request) -> StreamResponse:

async def http_request_legacy(self, method: str, url: str, extra_opts: Any = {}, timeout: int | None = None):
async with ClientSession() as web:
res = await web.request(method, url, ssl=helpers.get_ssl_context(), timeout=timeout, **extra_opts)
res = await web.request(method, url, ssl=helpers.get_ssl_context(), timeout=timeout, **extra_opts) # type: ignore
text = await res.text()
return {
"status": res.status,
Expand Down Expand Up @@ -390,7 +392,6 @@ async def filepicker_ls(self,
"total": len(all),
}


# Based on https://stackoverflow.com/a/46422554/13174603
def start_rdt_proxy(self, ip: str, port: int):
async def pipe(reader: StreamReader, writer: StreamWriter):
Expand Down Expand Up @@ -474,3 +475,32 @@ async def get_user_info(self) -> Dict[str, str]:

async def get_tab_id(self, name: str):
return (await get_tab(name)).id

async def disable_plugin(self, name: str):
disabled_plugins: List[str] = await self.get_setting("disabled_plugins", [])
if name not in disabled_plugins:
disabled_plugins.append(name)
await self.set_setting("disabled_plugins", disabled_plugins)

await self.context.plugin_loader.plugins[name].stop()
await self.context.ws.emit("loader/disable_plugin", name)

async def enable_plugin(self, name: str):
plugin_folder = self.context.plugin_browser.find_plugin_folder(name)
assert plugin_folder is not None
plugin_dir = path.join(self.context.plugin_browser.plugin_path, plugin_folder)

if name in self.context.plugin_loader.plugins:
plugin = self.context.plugin_loader.plugins[name]
if plugin.proc and plugin.proc.is_alive():
await plugin.stop()
self.context.plugin_loader.plugins.pop(name, None)
await sleep(1)

disabled_plugins: List[str] = await self.get_setting("disabled_plugins", [])

if name in disabled_plugins:
disabled_plugins.remove(name)
await self.set_setting("disabled_plugins", disabled_plugins)

await self.context.plugin_loader.import_plugin(path.join(plugin_dir, "main.py"), plugin_folder)
18 changes: 17 additions & 1 deletion frontend/src/components/DeckyState.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { FC, ReactNode, createContext, useContext, useEffect, useState } from 'react';

import { DEFAULT_NOTIFICATION_SETTINGS, NotificationSettings } from '../notification-service';
import { Plugin } from '../plugin';
import { DisabledPlugin, Plugin } from '../plugin';
import { PluginUpdateMapping } from '../store';
import { VerInfo } from '../updater';

interface PublicDeckyState {
plugins: Plugin[];
disabledPlugins: DisabledPlugin[];
installedPlugins: (Plugin | DisabledPlugin)[];
pluginOrder: string[];
frozenPlugins: string[];
hiddenPlugins: string[];
Expand All @@ -26,6 +28,8 @@ export interface UserInfo {

export class DeckyState {
private _plugins: Plugin[] = [];
private _disabledPlugins: DisabledPlugin[] = [];
private _installedPlugins: (Plugin | DisabledPlugin)[] = [];
private _pluginOrder: string[] = [];
private _frozenPlugins: string[] = [];
private _hiddenPlugins: string[] = [];
Expand All @@ -42,6 +46,8 @@ export class DeckyState {
publicState(): PublicDeckyState {
return {
plugins: this._plugins,
disabledPlugins: this._disabledPlugins,
installedPlugins: this._installedPlugins,
pluginOrder: this._pluginOrder,
frozenPlugins: this._frozenPlugins,
hiddenPlugins: this._hiddenPlugins,
Expand All @@ -62,6 +68,13 @@ export class DeckyState {

setPlugins(plugins: Plugin[]) {
this._plugins = plugins;
this._installedPlugins = [...plugins, ...this._disabledPlugins];
this.notifyUpdate();
}

setDisabledPlugins(disabledPlugins: DisabledPlugin[]) {
this._disabledPlugins = disabledPlugins;
this._installedPlugins = [...this._plugins, ...disabledPlugins];
this.notifyUpdate();
}

Expand Down Expand Up @@ -125,6 +138,7 @@ interface DeckyStateContext extends PublicDeckyState {
setIsLoaderUpdating(hasUpdate: boolean): void;
setActivePlugin(name: string): void;
setPluginOrder(pluginOrder: string[]): void;
setDisabledPlugins(disabled: DisabledPlugin[]): void;
closeActivePlugin(): void;
}

Expand Down Expand Up @@ -163,6 +177,7 @@ export const DeckyStateContextProvider: FC<Props> = ({ children, deckyState }) =
const setActivePlugin = deckyState.setActivePlugin.bind(deckyState);
const closeActivePlugin = deckyState.closeActivePlugin.bind(deckyState);
const setPluginOrder = deckyState.setPluginOrder.bind(deckyState);
const setDisabledPlugins = deckyState.setDisabledPlugins.bind(deckyState);

return (
<DeckyStateContext.Provider
Expand All @@ -173,6 +188,7 @@ export const DeckyStateContextProvider: FC<Props> = ({ children, deckyState }) =
setActivePlugin,
closeActivePlugin,
setPluginOrder,
setDisabledPlugins,
}}
>
{children}
Expand Down
46 changes: 36 additions & 10 deletions frontend/src/components/PluginView.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
import { ButtonItem, ErrorBoundary, Focusable, PanelSection, PanelSectionRow } from '@decky/ui';
import { FC, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { FaEyeSlash } from 'react-icons/fa';
import { FaBan, FaEyeSlash } from 'react-icons/fa';

import { useDeckyState } from './DeckyState';
import NotificationBadge from './NotificationBadge';
import { useQuickAccessVisible } from './QuickAccessVisibleState';
import TitleView from './TitleView';

const PluginView: FC = () => {
const { plugins, hiddenPlugins, updates, activePlugin, pluginOrder, setActivePlugin, closeActivePlugin } =
useDeckyState();
const {
plugins,
disabledPlugins,
hiddenPlugins,
updates,
activePlugin,
pluginOrder,
setActivePlugin,
closeActivePlugin,
} = useDeckyState();
const visible = useQuickAccessVisible();
const { t } = useTranslation();

Expand All @@ -21,7 +29,9 @@ const PluginView: FC = () => {
.sort((a, b) => pluginOrder.indexOf(a.name) - pluginOrder.indexOf(b.name))
.filter((p) => p.content)
.filter(({ name }) => !hiddenPlugins.includes(name));
}, [plugins, pluginOrder]);
}, [plugins, pluginOrder, hiddenPlugins]);

const numberOfHidden = hiddenPlugins.filter((name) => !!plugins.find((p) => p.name === name)).length;

if (activePlugin) {
return (
Expand Down Expand Up @@ -53,12 +63,28 @@ const PluginView: FC = () => {
</ButtonItem>
</PanelSectionRow>
))}
{hiddenPlugins.length > 0 && (
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', fontSize: '0.8rem', marginTop: '10px' }}>
<FaEyeSlash />
<div>{t('PluginView.hidden', { count: hiddenPlugins.length })}</div>
</div>
)}
<div
style={{
display: 'flex',
flexDirection: 'column',
position: 'absolute',
justifyContent: 'center',
padding: '5px 0px',
}}
>
{numberOfHidden > 0 && (
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', fontSize: '0.8rem' }}>
<FaEyeSlash />
<div>{t('PluginView.hidden', { count: numberOfHidden })}</div>
</div>
)}
{disabledPlugins.length > 0 && (
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', fontSize: '0.8rem' }}>
<FaBan />
<div>{t('PluginView.disabled', { count: disabledPlugins.length })}</div>
</div>
)}
</div>
</PanelSection>
</div>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { FC, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FaCheck, FaDownload } from 'react-icons/fa';

import { InstallType, InstallTypeTranslationMapping } from '../../plugin';
import { DisabledPlugin, InstallType, InstallTypeTranslationMapping } from '../../plugin';

interface MultiplePluginsInstallModalProps {
requests: { name: string; version: string; hash: string; install_type: InstallType }[];
disabledPlugins: DisabledPlugin[];
onOK(): void | Promise<void>;
onCancel(): void | Promise<void>;
closeModal?(): void;
Expand All @@ -17,6 +18,7 @@ type TitleTranslationMapping = 'mixed' | (typeof InstallTypeTranslationMapping)[

const MultiplePluginsInstallModal: FC<MultiplePluginsInstallModalProps> = ({
requests,
disabledPlugins,
onOK,
onCancel,
closeModal,
Expand Down Expand Up @@ -116,10 +118,11 @@ const MultiplePluginsInstallModal: FC<MultiplePluginsInstallModalProps> = ({
version,
});

const disabled = disabledPlugins.some((p) => p.name === name);
return (
<li key={i} style={{ display: 'flex', flexDirection: 'column' }}>
<span>
{description}{' '}
{disabled ? `${description} - ${t('PluginInstallModal.disabled')}` : description}{' '}
{(pluginsCompleted.includes(name) && <FaCheck />) || (name === pluginInProgress && <FaDownload />)}
</span>
{hash === 'False' && (
Expand Down
Loading