diff --git a/.gitignore b/.gitignore
index e33609d..d6fed4e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,41 @@
-*.png
+lib-cov
+*.seed
+*.log
+*.csv
+*.dat
+*.out
+*.pid
+*.gz
+*.swp
+
+pids
+logs
+results
+tmp
+
+# Coverage reports
+coverage
+
+# API keys and secrets
+.env
+
+# Dependency directory
+node_modules
+bower_components
+package-lock.json
+
+# Editors
+.idea
+*.iml
+
+# OS metadata
+.DS_Store
+Thumbs.db
+
+# Ignore built ts files
+dist/
+
+__pycache__/
+
+/.yalc
+yalc.lock
diff --git a/README.md b/README.md
index 3d18495..647063c 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# PowerTools
-![plugin_demo](./extras/ui.png)
+![plugin_demo](./assets/ui.png)
Steam Deck power tweaks for power users.
diff --git a/extras/icon.svg b/assets/icon.svg
similarity index 100%
rename from extras/icon.svg
rename to assets/icon.svg
diff --git a/assets/logo.png b/assets/logo.png
new file mode 100644
index 0000000..94c719c
Binary files /dev/null and b/assets/logo.png differ
diff --git a/assets/thumbnail.png b/assets/thumbnail.png
new file mode 100644
index 0000000..b22ecbe
Binary files /dev/null and b/assets/thumbnail.png differ
diff --git a/assets/ui.png b/assets/ui.png
new file mode 100644
index 0000000..5adb07f
Binary files /dev/null and b/assets/ui.png differ
diff --git a/extras/ui.png b/extras/ui.png
deleted file mode 100644
index d319477..0000000
Binary files a/extras/ui.png and /dev/null differ
diff --git a/main.py b/main.py
index 7d97797..598c6ab 100644
--- a/main.py
+++ b/main.py
@@ -5,8 +5,8 @@ import asyncio
import pathlib
import subprocess
-VERSION = "0.7.0"
-HOME_DIR = str(pathlib.Path(os.getcwd()).parent.parent.resolve())
+VERSION = "0.7.0-alpha-react"
+HOME_DIR = "/home/deck"
DEFAULT_SETTINGS_LOCATION = HOME_DIR + "/.config/powertools/default_settings.json"
LOG_LOCATION = "/tmp/powertools.log"
FANTASTIC_INSTALL_DIR = HOME_DIR + "/homebrew/plugins/Fantastic"
@@ -127,10 +127,13 @@ class Plugin:
CPU_COUNT = 8
FAN_SPEEDS = [0, 1000, 2000, 3000, 4000, 5000, 6000]
+ gpu_power_values = [[-1, -1, -1], [1000000, 15000000, 29000000], [0, 15000000, 30000000]]
+
auto_fan = True
persistent = True
modified_settings = False
current_gameid = None
+ ready = False
async def get_version(self) -> str:
return VERSION
@@ -139,6 +142,7 @@ class Plugin:
# call from main_view.html with setCPUs(count, smt)
async def set_cpus(self, count, smt=True):
+ logging.info(f"set_cpus({count}, {smt})")
self.modified_settings = True
cpu_count = len(self.cpus)
self.smt = smt
@@ -165,9 +169,11 @@ class Plugin:
for cpu in self.cpus:
if(cpu.status()):
online_count += 1
+ logging.info(f"get_cpus() -> {online_count}")
return online_count
async def get_smt(self) -> bool:
+ logging.info(f"get_smt() -> {self.smt}")
return self.smt
async def set_boost(self, enabled: bool) -> bool:
@@ -203,6 +209,24 @@ class Plugin:
async def get_gpu_power(self, power_number: int) -> int:
return read_gpu_ppt(power_number)
+ async def set_gpu_power_index(self, index: int, power_number: int) -> bool:
+ if index < 3 and index >= 0:
+ self.modified_settings = True
+ old_value = read_gpu_ppt(power_number)
+ if old_value not in self.gpu_power_values[power_number]:
+ self.gpu_power_values[power_number][1] = old_value
+ write_gpu_ppt(power_number, self.gpu_power_values[power_number][index])
+ return True
+ return False
+
+ async def get_gpu_power_index(self, power_number: int) -> int:
+ value = read_gpu_ppt(power_number)
+ if value not in self.gpu_power_values[power_number]:
+ #self.gpu_power_values[power_number][1] = value
+ return 1
+ else:
+ return self.gpu_power_values[power_number].index(value)
+
# Fan stuff
async def set_fan_tick(self, tick: int):
@@ -331,6 +355,9 @@ class Plugin:
# called from main_view::onViewReady
async def on_ready(self):
delta = time.time() - startup_time
+ if self.ready:
+ logging.info(f"Front-end init called again {delta}s after startup")
+ return
logging.info(f"Front-end initialised {delta}s after startup")
# persistence
@@ -439,7 +466,15 @@ class Plugin:
self.current_gameid = None
async def get_per_game_profile(self) -> bool:
- return self.current_gameid is not None
+ return self.current_gameid is not None and current_game.has_settings()
+
+ async def on_game_start(self, game_id: int, data) -> bool:
+ pt_server.http_server.set_game(game_id, data)
+ return True
+
+ async def on_game_stop(self, game_id: int) -> bool:
+ pt_server.http_server.unset_game(game_id)
+ return True
diff --git a/main_view.html b/main_view.html
deleted file mode 100644
index 9b23608..0000000
--- a/main_view.html
+++ /dev/null
@@ -1,746 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..2e5dfcf
--- /dev/null
+++ b/package.json
@@ -0,0 +1,45 @@
+{
+ "name": "PowerTools",
+ "version": "0.7.0",
+ "description": "Power tweaks for power users",
+ "scripts": {
+ "build": "shx rm -rf dist && rollup -c",
+ "watch": "rollup -c -w",
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/NGnius/PowerTools.git"
+ },
+ "keywords": [
+ "plugin",
+ "utility",
+ "power-management",
+ "steam-deck",
+ "deck"
+ ],
+ "author": "NGnius (Graham) ",
+ "license": "GPL-3.0",
+ "bugs": {
+ "url": "https://github.com/NGnius/PowerTools/issues"
+ },
+ "homepage": "https://github.com/NGnius/PowerTools#readme",
+ "devDependencies": {
+ "@rollup/plugin-commonjs": "^21.1.0",
+ "@rollup/plugin-json": "^4.1.0",
+ "@rollup/plugin-node-resolve": "^13.2.1",
+ "@rollup/plugin-replace": "^4.0.0",
+ "@rollup/plugin-typescript": "^8.3.2",
+ "@types/react": "16.14.0",
+ "@types/webpack": "^5.28.0",
+ "rollup": "^2.70.2",
+ "rollup-plugin-import-assets": "^1.1.1",
+ "shx": "^0.3.4",
+ "tslib": "^2.4.0",
+ "typescript": "^4.6.4"
+ },
+ "dependencies": {
+ "decky-frontend-lib": "*",
+ "react-icons": "^4.3.1"
+ }
+}
diff --git a/plugin.json b/plugin.json
index 88badf1..ee4f4ee 100644
--- a/plugin.json
+++ b/plugin.json
@@ -1,8 +1,6 @@
{
- "name": "Power Tools",
+ "name": "PowerTools",
"author": "NGnius",
- "main_view_html": "main_view.html",
- "tile_view_html": "",
"flags": ["root", "debug"],
"publish": {
"discord_id": "106537989684887552",
diff --git a/rollup.config.js b/rollup.config.js
new file mode 100644
index 0000000..8717908
--- /dev/null
+++ b/rollup.config.js
@@ -0,0 +1,37 @@
+import commonjs from '@rollup/plugin-commonjs';
+import json from '@rollup/plugin-json';
+import { nodeResolve } from '@rollup/plugin-node-resolve';
+import replace from '@rollup/plugin-replace';
+import typescript from '@rollup/plugin-typescript';
+import { defineConfig } from 'rollup';
+import importAssets from 'rollup-plugin-import-assets';
+
+import { name } from "./plugin.json";
+
+export default defineConfig({
+ input: './src/index.tsx',
+ plugins: [
+ commonjs(),
+ nodeResolve(),
+ typescript(),
+ json(),
+ replace({
+ preventAssignment: false,
+ 'process.env.NODE_ENV': JSON.stringify('production'),
+ }),
+ importAssets({
+ publicPath: `http://127.0.0.1:1337/plugins/${name}/`
+ })
+ ],
+ context: 'window',
+ external: ['react', 'react-dom'],
+ output: {
+ file: 'dist/index.js',
+ globals: {
+ react: 'SP_REACT',
+ 'react-dom': 'SP_REACTDOM',
+ },
+ format: 'iife',
+ exports: 'default',
+ },
+});
diff --git a/server.py b/server.py
index 3e2e799..fec3162 100644
--- a/server.py
+++ b/server.py
@@ -7,7 +7,7 @@ import pathlib
from aiohttp import web
import aiohttp
-HOME_DIR = str(pathlib.Path(os.getcwd()).parent.parent.resolve())
+HOME_DIR = "/home/deck"
SETTINGS_DIR = HOME_DIR + "/.config/powertools"
if not os.path.exists(SETTINGS_DIR):
@@ -60,6 +60,15 @@ class Server(web.Application):
def game(self) -> GameInfo:
return self.current_game
+ def set_game(self, game_id, data):
+ self.current_game = GameInfo(game_id, data)
+
+ def unset_game(self, game_id):
+ if self.current_game is None:
+ return
+ if game_id is None or self.current_game.gameid == game_id:
+ self.current_game = None
+
async def index(self, request):
logging.debug("Debug index page accessed")
current_game = None if self.current_game is None else self.current_game.gameid
@@ -97,16 +106,13 @@ class Server(web.Application):
except ValueError:
return web.Response(text="WTF", status=400)
if self.current_game.gameid == game_id:
- pass
- #self.current_game = None
- # TODO change settings to default
+ self.current_game = None
return web.Response(status=204, headers={"Access-Control-Allow-Origin": "*"})
async def on_game_exit_null(self, request):
# ignored for now
logging.info(f"on_game_exit_null")
- #self.current_game = None
- # TODO change settings to default
+ self.current_game = None
return web.Response(status=204, headers={"Access-Control-Allow-Origin": "*"})
async def self_destruct(self, request):
diff --git a/src/index.tsx b/src/index.tsx
new file mode 100755
index 0000000..034a5fc
--- /dev/null
+++ b/src/index.tsx
@@ -0,0 +1,368 @@
+import {
+ //ButtonItem,
+ definePlugin,
+ DialogButton,
+ //Menu,
+ //MenuItem,
+ PanelSection,
+ PanelSectionRow,
+ //Router,
+ ServerAPI,
+ //showContextMenu,
+ staticClasses,
+ Slider,
+ Toggle,
+ //NotchLabel
+ gamepadDialogClasses,
+ joinClassNames,
+} from "decky-frontend-lib";
+import { VFC, useState } from "react";
+import { FaShip } from "react-icons/fa";
+
+import * as python from "./python";
+
+//import logo from "../assets/logo.png";
+
+// interface AddMethodArgs {
+// left: number;
+// right: number;
+// }
+
+var firstTime: boolean = true;
+var versionGlobal: string = "0.0.0-jank";
+var periodicHook: NodeJS.Timer | null = null;
+var lastGame: string = "";
+var lifetimeHook: any = null;
+var startHook: any = null;
+
+var reload = function(){};
+
+const Content: VFC<{ serverAPI: ServerAPI }> = ({serverAPI}) => {
+ // const [result, setResult] = useState();
+
+ // const onClick = async () => {
+ // const result = await serverAPI.callPluginMethod(
+ // "add",
+ // {
+ // left: 2,
+ // right: 2,
+ // }
+ // );
+ // if (result.success) {
+ // setResult(result.result);
+ // }
+ // };
+
+ python.setServer(serverAPI);
+
+ const [smtGlobal, setSMT] = useState(true);
+ const [cpusGlobal, setCPUs] = useState(8);
+ const [boostGlobal, setBoost] = useState(true);
+
+ const [freqGlobal, setFreq] = useState(8);
+
+ const [slowPPTGlobal, setSlowPPT] = useState(1);
+ const [fastPPTGlobal, setFastPPT] = useState(1);
+
+ const [chargeNowGlobal, setChargeNow] = useState(40);
+ const [chargeFullGlobal, setChargeFull] = useState(40);
+ const [chargeDesignGlobal, setChargeDesign] = useState(40);
+
+ const [persistGlobal, setPersist] = useState(false);
+ const [perGameProfileGlobal, setPerGameProfile] = useState(false);
+ const [gameGlobal, setGame] = useState("with your mom");
+
+ reload = function () {
+ python.execute(python.onViewReady());
+
+ python.resolve(python.getSMT(), setSMT);
+ python.resolve(python.getCPUs(), setCPUs);
+ python.resolve(python.getCPUBoost(), setBoost);
+ python.resolve(python.getMaxBoost(), setFreq);
+
+ python.resolve(python.getGPUPowerI(1), setSlowPPT);
+ python.resolve(python.getGPUPowerI(2), setFastPPT);
+
+ python.resolve(python.getPersistent(), setPersist);
+ python.resolve(python.getPerGameProfile(), setPerGameProfile);
+ };
+
+
+ if (firstTime) {
+ firstTime = false;
+
+ reload(); // technically it's just load, not reload ;)
+
+ python.resolve(python.getChargeNow(), setChargeNow);
+ python.resolve(python.getChargeFull(), setChargeFull);
+ python.resolve(python.getChargeDesign(), setChargeDesign);
+
+ python.resolve(python.getCurrentGame(), setGame);
+
+ periodicHook = setInterval(function() {
+ python.resolve(python.getChargeNow(), setChargeNow);
+ python.resolve(python.getChargeFull(), setChargeFull);
+ python.resolve(python.getCurrentGame(), (game: string) => {
+ if (lastGame != game) {
+ setGame(game);
+ lastGame = game;
+ reload();
+ }
+ });
+ }, 1000);
+
+ python.resolve(python.getVersion(), (v: string) => {versionGlobal = v;});
+
+ //@ts-ignore
+ lifetimeHook = SteamClient.GameSessions.RegisterForAppLifetimeNotifications((update) => {
+ if (update.bRunning) {
+ console.log("AppID " + update.unAppID.toString() + " is now running");
+ } else {
+ console.log("AppID " + update.unAppID.toString() + " is no longer running");
+ python.execute(python.onGameStop(null));
+ }
+ });
+ //@ts-ignore
+ SteamClient.Apps.RegisterForGameActionStart((actionType, id) => {
+ //@ts-ignore
+ let gameInfo: any = appStore.GetAppOverviewByGameID(id);
+ python.execute(python.onGameStart(id, gameInfo));
+ });
+ }
+
+ const FieldWithSeparator = joinClassNames(gamepadDialogClasses.Field, gamepadDialogClasses.WithBottomSeparatorStandard);
+
+ return (
+
+ {/* CPU */}
+
+ CPU
+
+
+ {
+ console.log("SMT is now " + smt.toString());
+ python.execute(python.setCPUs(cpusGlobal, smt));
+ python.resolve(python.getCPUs(), setCPUs);
+ python.resolve(python.getSMT(), setSMT);
+ }}
+ />
+
+
+ {
+ console.log("CPU slider is now " + cpus.toString());
+ if (cpus != cpusGlobal) {
+ python.execute(python.setCPUs(cpus, smtGlobal));
+ python.resolve(python.getCPUs(), setCPUs);
+ }
+ }}
+ />
+
+
+ {
+ console.log("Boost is now " + boost.toString());
+ python.execute(python.setCPUBoost(boost));
+ python.resolve(python.getCPUBoost(), setBoost);
+ }}
+ />
+
+
+ {
+ console.log("CPU slider is now " + freq.toString());
+ if (freq != freqGlobal) {
+ python.execute(python.setMaxBoost(freq));
+ python.resolve(python.getMaxBoost(), setFreq);
+ }
+ }}
+ />
+
+ {/* GPU */}
+
+ GPU
+
+
+ {/* index: 1 */}
+ {
+ console.log("SlowPPT is now " + ppt.toString());
+ if (ppt != slowPPTGlobal) {
+ python.execute(python.setGPUPowerI(ppt, 1));
+ python.resolve(python.getGPUPowerI(1), setSlowPPT);
+ }
+ }}
+ />
+
+
+ {/* index: 2 */}
+ {
+ console.log("FastPPT is now " + ppt.toString());
+ if (ppt != fastPPTGlobal) {
+ python.execute(python.setGPUPowerI(ppt, 2));
+ python.resolve(python.getGPUPowerI(2), setFastPPT);
+ }
+ }}
+ />
+
+ {/* Battery */}
+
+ Battery
+
+
+
+
+
+ Now (Charge)
+
+
+ {(7.7 * chargeNowGlobal / 1000000).toFixed(1).toString() + " Wh (" + (100 * chargeNowGlobal / chargeFullGlobal).toFixed(1).toString() + "%)"}
+
+
+
+
+
+
+
+
+ Max (Design)
+
+
+ {(7.7 * chargeFullGlobal / 1000000).toFixed(1).toString() + " Wh (" + (100 * chargeFullGlobal / chargeDesignGlobal).toFixed(1).toString() + "%)"}
+
+
+
+
+ {/* Persistence */}
+
+ {
+ console.log("Persist is now " + persist.toString());
+ python.execute(python.setPersistent(persist));
+ python.resolve(python.getPersistent(), setPersist);
+ }}
+ />
+
+
+ {
+ console.log("Per game profile is now " + p.toString());
+ python.execute(python.setPerGameProfile(p));
+ python.resolve(python.getPerGameProfile(), setPerGameProfile);
+ reload();
+ }}
+ />
+
+
+
+
+
+ Now Playing
+
+
+ {gameGlobal}
+
+
+
+
+ {/* Version */}
+
+ Debug
+
+
+
+
+
+ PowerTools
+
+
+ v{versionGlobal}
+
+
+
+
+
+ );
+};
+
+const DeckyPluginRouterTest: VFC = () => {
+ return (
+
+ Hello World!
+ {}}>
+ Go to Store
+
+
+ );
+};
+
+export default definePlugin((serverApi: ServerAPI) => {
+ serverApi.routerHook.addRoute("/decky-plugin-test", DeckyPluginRouterTest, {
+ exact: true,
+ });
+
+ return {
+ title: PowerTools
,
+ content: ,
+ icon: ,
+ onDismount() {
+ console.log("PowerTools shutting down");
+ clearInterval(periodicHook!);
+ lifetimeHook.unregister();
+ startHook.unregister();
+ serverApi.routerHook.removeRoute("/decky-plugin-test");
+ },
+ };
+});
diff --git a/src/python.ts b/src/python.ts
new file mode 100644
index 0000000..9912f65
--- /dev/null
+++ b/src/python.ts
@@ -0,0 +1,133 @@
+import { ServerAPI } from "decky-frontend-lib";
+
+var server: ServerAPI | undefined = undefined;
+
+//import { useEffect } from "react";
+
+export function resolve(promise: Promise, setter: any) {
+ (async function () {
+ let data = await promise;
+ console.log("Got resolved", data);
+ if (data.success) {
+ setter(data.result);
+ } else {
+ console.log("Resolve failed:", data);
+ }
+ })();
+}
+
+export function execute(promise: Promise) {
+ (async function () {
+ let data = await promise;
+ console.log("Got executed", data);
+ })();
+}
+
+export function setServer(s: ServerAPI) {
+ server = s;
+}
+
+// Python functions
+export function getVersion(): Promise {
+ return server!.callPluginMethod("get_version", {});
+}
+
+export function onViewReady(): Promise {
+ return server!.callPluginMethod("on_ready", {});
+}
+
+export function setCPUs(value: number, smt: boolean): Promise {
+ return server!.callPluginMethod("set_cpus", {"count":value, "smt": smt});
+}
+
+export function getCPUs(): Promise {
+ return server!.callPluginMethod("get_cpus", {});
+}
+
+export function getSMT(): Promise {
+ return server!.callPluginMethod("get_smt", {});
+}
+
+export function setCPUBoost(value: boolean): Promise {
+ return server!.callPluginMethod("set_boost", {"enabled": value});
+}
+
+export function getCPUBoost(): Promise {
+ return server!.callPluginMethod("get_boost", {});
+}
+
+export function setMaxBoost(index: number): Promise {
+ return server!.callPluginMethod("set_max_boost", {"index": index});
+}
+
+export function getMaxBoost(): Promise {
+ return server!.callPluginMethod("get_max_boost", {});
+}
+
+export function setGPUPower(value: number, index: number): Promise {
+ return server!.callPluginMethod("set_gpu_power", {"value": value, "power_number": index});
+}
+
+export function getGPUPower(index: number): Promise {
+ return server!.callPluginMethod("get_gpu_power", {"power_number": index});
+}
+
+export function setGPUPowerI(value: number, index: number): Promise {
+ return server!.callPluginMethod("set_gpu_power_index", {"index": value, "power_number": index});
+}
+
+export function getGPUPowerI(index: number): Promise {
+ return server!.callPluginMethod("get_gpu_power_index", {"power_number": index});
+}
+
+export function setFanTick(tick: number): Promise {
+ return server!.callPluginMethod("set_fan_tick", {"tick": tick});
+}
+
+export function getFanTick(): Promise {
+ return server!.callPluginMethod("get_fan_tick", {});
+}
+
+export function getFantastic(): Promise {
+ return server!.callPluginMethod("fantastic_installed", {});
+}
+
+export function getChargeNow(): Promise {
+ return server!.callPluginMethod("get_charge_now", {});
+}
+
+export function getChargeFull(): Promise {
+ return server!.callPluginMethod("get_charge_full", {});
+}
+
+export function getChargeDesign(): Promise {
+ return server!.callPluginMethod("get_charge_design", {});
+}
+
+export function setPersistent(value: boolean): Promise {
+ return server!.callPluginMethod("set_persistent", {"enabled": value});
+}
+
+export function getPersistent(): Promise {
+ return server!.callPluginMethod("get_persistent", {});
+}
+
+export function setPerGameProfile(value: boolean): Promise {
+ return server!.callPluginMethod("set_per_game_profile", {"enabled": value});
+}
+
+export function getPerGameProfile(): Promise {
+ return server!.callPluginMethod("get_per_game_profile", {});
+}
+
+export function getCurrentGame(): Promise {
+ return server!.callPluginMethod("get_current_game", {});
+}
+
+export function onGameStart(gameId: number, data: any): Promise {
+ return server!.callPluginMethod("on_game_start", {"game_id": gameId, "data":data});
+}
+
+export function onGameStop(gameId: number | null): Promise {
+ return server!.callPluginMethod("on_game_stop", {"game_id": gameId});
+}
diff --git a/src/types.d.ts b/src/types.d.ts
new file mode 100644
index 0000000..dfc0472
--- /dev/null
+++ b/src/types.d.ts
@@ -0,0 +1,14 @@
+declare module "*.svg" {
+ const content: string;
+ export default content;
+}
+
+declare module "*.png" {
+ const content: string;
+ export default content;
+}
+
+declare module "*.jpg" {
+ const content: string;
+ export default content;
+}
diff --git a/tile_view.html b/tile_view.html
deleted file mode 100644
index e69de29..0000000
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..13b0c35
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ "outDir": "dist",
+ "module": "ESNext",
+ "target": "ES2020",
+ "jsx": "react",
+ "jsxFactory": "window.SP_REACT.createElement",
+ "declaration": false,
+ "moduleResolution": "node",
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "esModuleInterop": true,
+ "noImplicitReturns": true,
+ "noImplicitThis": true,
+ "noImplicitAny": true,
+ "strict": true,
+ "suppressImplicitAnyIndexErrors": true,
+ "allowSyntheticDefaultImports": true,
+ "skipLibCheck": true
+ },
+ "include": ["src"],
+ "exclude": ["node_modules"]
+}