Merge branch 'dev'

This commit is contained in:
NGnius (Graham) 2022-07-10 14:30:29 -04:00
commit 1182ab11df
18 changed files with 852 additions and 869 deletions

42
.gitignore vendored
View file

@ -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

View file

@ -1,10 +1,10 @@
# PowerTools
![plugin_demo](./extras/ui.png)
![plugin_demo](./assets/ui.png)
Steam Deck power tweaks for power users.
This is generated from the template plugin for the [SteamOS Plugin Loader](https://github.com/SteamDeckHomebrew/PluginLoader).
This is generated from the template plugin for the [Decky Plugin Loader](https://github.com/SteamDeckHomebrew/decky-loader).
You will need that installed for this plugin to work.
## What does it do?
@ -12,7 +12,6 @@ You will need that installed for this plugin to work.
- Enable & disable CPU threads & SMT
- Set CPU max frequency and toggle boost
- Set some GPU power parameters (fastPPT & slowPPT)
- Set the fan RPM (unsupported on SteamOS beta)
- Display supplementary battery info
- Keep settings between restarts (stored in `~/.config/powertools/<appid>.json`)
@ -52,6 +51,8 @@ Get the entry limits for those two commands with `cat /sys/class/hwmon/hwmon4/po
### Set Fan speed
NOTE: PowerTools no longer supports this, since [Fantastic](https://github.com/NGnius/Fantastic) does it much better.
Enable automatic control: `echo 0 > /sys/class/hwmon/hwmon5/recalculate` enables automatic fan control.
Disable automatic control: `echo 1 > /sys/class/hwmon/hwmon5/recalculate` disables automatic (temperature-based) fan control and starts using the set fan target instead.
@ -73,6 +74,8 @@ Get the design battery capacity: `cat /sys/class/hwmon/hwmon2/device/charge_full
Get whether the deck is plugged in: `cat /sys/class/hwmon/hwmon5/curr1_input` gives the charger current in mA.
NOTE: 7.7 is the voltage of the battery -- it's not just a magic number.
### Steam Deck kernel patches
This is how I figured out how the fan stuff works.
@ -81,22 +84,26 @@ https://lkml.org/lkml/2022/2/5/391
### Game launch detection
The biggest limitation right now is it can't detect a game closing -- only opening -- and only after PowerTools is looked at at least once (per SteamOS restart).
From a plugin, this can be accomplished by running some front-end Javascript.
```javascript
await execute_in_tab("SP", false,
`SteamClient.Apps.RegisterForGameActionStart((actionType, data) => {
console.log("start game", appStore.GetAppOverviewByGameID(data));
});`
);
```typescript
//@ts-ignore
let 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");
// game exit code here
// NOTE: custom games always have 0 as AppID, so AppID is bad to use as ID
}
});
//@ts-ignore
let startHook = SteamClient.Apps.RegisterForGameActionStart((actionType, id) => {
//@ts-ignore
let gameInfo: any = appStore.GetAppOverviewByGameID(id);
// game start code here
// NOTE: GameID (variable: id) is always unique, even for custom games, so it's better to use than AppID
});
```
In PowerTools, the callback (the part surrounded by `{` and `}`, containing `console.log(...)`) sends a message to a local HTTP server to notify the PowerTools back-end that a game has been launched.
If you go to `http://127.0.0.1:5030` on your Steam Deck with PowerTools >=0.6.0, you can see some info about the last game you launched.
## License
This is licensed under GNU GPLv3.

View file

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
assets/thumbnail.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

BIN
assets/ui.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 KiB

89
main.py
View file

@ -5,8 +5,8 @@ import asyncio
import pathlib
import subprocess
VERSION = "0.6.0"
HOME_DIR = str(pathlib.Path(os.getcwd()).parent.parent.resolve())
VERSION = "0.7.0"
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,14 @@ 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
old_gameid = None
ready = False
async def get_version(self) -> str:
return VERSION
@ -139,6 +143,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 +170,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 +210,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):
@ -304,26 +329,7 @@ class Plugin:
if self.modified_settings and self.persistent:
self.save_settings(self)
self.modified_settings = False
if self.persistent:
# per-game profiles
current_game = pt_server.http_server.game()
old_gameid = self.current_gameid
if current_game is not None and current_game.has_settings():
self.current_gameid = current_game.gameid
if old_gameid != self.current_gameid:
logging.info(f"Applying custom settings for {current_game.name()} {current_game.appid()}")
# new game; apply settings
settings = current_game.load_settings()
if settings is not None:
self.apply_settings(self, settings)
else:
self.current_gameid = None
if old_gameid != self.current_gameid:
logging.info("Reapplying default settings; game without custom settings found")
# game without custom settings; apply defaults
settings = read_json(DEFAULT_SETTINGS_LOCATION)
self.apply_settings(self, settings)
logging.debug(f"gameid update: {old_gameid} -> {self.current_gameid}")
#self.reload_current_settings(self)
await asyncio.sleep(1)
await pt_server.shutdown()
@ -331,6 +337,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
@ -373,6 +382,29 @@ class Plugin:
settings["auto"] = self.auto_fan
return settings
def reload_current_settings(self):
logging.debug(f"gameid update: {self.old_gameid} -> {self.current_gameid}")
if self.persistent:
# per-game profiles
current_game = pt_server.http_server.game()
self.old_gameid = self.current_gameid
if current_game is not None and current_game.has_settings():
self.current_gameid = current_game.gameid
if self.old_gameid != self.current_gameid:
logging.info(f"Applying custom settings for {current_game.name()} {current_game.appid()}")
# new game; apply settings
settings = current_game.load_settings()
if settings is not None:
self.apply_settings(self, settings)
else:
self.current_gameid = None
if self.old_gameid != None:
logging.info("Reapplying default settings; game without custom settings found")
self.old_gameid = None
# game without custom settings; apply defaults
settings = read_json(DEFAULT_SETTINGS_LOCATION)
self.apply_settings(self, settings)
def save_settings(self):
settings = self.current_settings(self)
logging.debug(f"Saving settings to file: {settings}")
@ -439,7 +471,18 @@ class Plugin:
self.current_gameid = None
async def get_per_game_profile(self) -> bool:
return self.current_gameid is not None
current_game = pt_server.http_server.game()
return current_game 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)
self.reload_current_settings(self)
return True
async def on_game_stop(self, game_id: int) -> bool:
pt_server.http_server.unset_game(game_id)
self.reload_current_settings(self)
return True

View file

@ -1,746 +0,0 @@
<html>
<head>
<link rel="stylesheet" href="/steam_resource/css/2.css">
<link rel="stylesheet" href="/steam_resource/css/39.css">
<link rel="stylesheet" href="/steam_resource/css/library.css">
<script src="/static/library.js"></script>
<script>
// Python functions
function getVersion() {
return call_plugin_method("get_version", {});
}
function onViewReady() {
return call_plugin_method("on_ready", {});
}
function setCPUs(value, smt) {
return call_plugin_method("set_cpus", {"count":value, "smt": smt});
}
function getCPUs() {
return call_plugin_method("get_cpus", {});
}
function getSMT() {
return call_plugin_method("get_smt", {});
}
function setCPUBoost(value) {
return call_plugin_method("set_boost", {"enabled": value});
}
function getCPUBoost() {
return call_plugin_method("get_boost", {});
}
function setMaxBoost(index) {
return call_plugin_method("set_max_boost", {"index": index});
}
function getMaxBoost() {
return call_plugin_method("get_max_boost", {});
}
function setGPUPower(value, index) {
return call_plugin_method("set_gpu_power", {"value": value, "power_number": index});
}
function getGPUPower(index) {
return call_plugin_method("get_gpu_power", {"power_number": index});
}
function setFanTick(tick) {
return call_plugin_method("set_fan_tick", {"tick": tick});
}
function getFanTick() {
return call_plugin_method("get_fan_tick", {});
}
function getFantastic() {
return call_plugin_method("fantastic_installed", {});
}
function getChargeNow() {
return call_plugin_method("get_charge_now", {});
}
function getChargeFull() {
return call_plugin_method("get_charge_full", {});
}
function getChargeDesign() {
return call_plugin_method("get_charge_design", {});
}
function setPersistent(value) {
return call_plugin_method("set_persistent", {"enabled": value});
}
function getPersistent() {
return call_plugin_method("get_persistent", {});
}
function setPerGameProfile(value) {
return call_plugin_method("set_per_game_profile", {"enabled": value});
}
function getPerGameProfile() {
return call_plugin_method("get_per_game_profile", {});
}
function getCurrentGame() {
return call_plugin_method("get_current_game", {});
}
// other logic
async function onReady() {
await onViewReady();
// detect game starts and exits
console.log("Injecting game detection code into main window (SP)");
await execute_in_tab("SP", false,
`console.log("Hey PowerTools is over here now too!");
SteamClient.Apps.RegisterForGameActionStart((actionType, data) => {
console.log("start game", appStore.GetAppOverviewByGameID(data));
fetch("http://127.0.0.1:5030/on_game_start/" + data.toString(), {method: "POST", body: JSON.stringify(appStore.GetAppOverviewByGameID(data))}).then((_) => {});
});
// this seems to not run when I thought (runs right after ^^^, not when game exits)
/*SteamClient.Apps.RegisterForGameActionEnd((actionType, data) => {
if (data != null && data != undefined) {
console.log("stop game", appStore.GetAppOverviewByGameID(data));
fetch("http://127.0.0.1:5030/on_game_exit/" + data.toString(), {method: "POST", body: JSON.stringify(appStore.GetAppOverviewByGameID(data))}).then((_) => {});
} else {
console.log("stop game null");
fetch("http://127.0.0.1:5030/on_game_exit_null", {method: "POST", body:data}).then((_) => {});
}
});*/`
);
await updateCurrentGame();
/*let boostToggle = document.getElementById("boostToggle");
setToggleState(boostToggle, await getCPUBoost());
setToggleState(document.getElementById("smtToggle"), await getSMT());
selectNotch("cpuThreadsNotch", await getCPUs() - 1, 8);
selectNotch("frequencyNotch", await getMaxBoost(), 3);
await onReadyGPU();
let isFantasticInstalled = await getFantastic();
if (isFantasticInstalled) {
// Don't fight with Fantastic
let fanRoot = document.getElementById("fanRoot");
fanRoot.style.visibility = "hidden";
fanRoot.style.height = "0px";
} else {
selectNotch("fanNotch", await getFanTick(), 8);
}
await updateBatteryStats();
setToggleState(document.getElementById("persistToggle"), await getPersistent());
setToggleState(document.getElementById("gameProfileToggle"), await getPerGameProfile());
await updateCurrentGame();*/
// this is unimportant; always do it last
await updateVersion();
window.setInterval(function() {
updateBatteryStats().then(_ => {});
updateCurrentGame().then(_ => {});
}, 1000);
}
async function reloadSettings() {
let boostToggle = document.getElementById("boostToggle");
setToggleState(boostToggle, await getCPUBoost());
setToggleState(document.getElementById("smtToggle"), await getSMT());
selectNotch("cpuThreadsNotch", await getCPUs() - 1, 8);
selectNotch("frequencyNotch", await getMaxBoost(), 3);
await onReadyGPU();
let isFantasticInstalled = await getFantastic();
if (isFantasticInstalled) {
// Don't fight with Fantastic
let fanRoot = document.getElementById("fanRoot");
fanRoot.style.visibility = "hidden";
fanRoot.style.height = "0px";
} else {
selectNotch("fanNotch", await getFanTick(), 8);
}
await updateBatteryStats();
setToggleState(document.getElementById("persistToggle"), await getPersistent());
setToggleState(document.getElementById("gameProfileToggle"), await getPerGameProfile());
}
async function setCPUNotch(index) {
const ROOT_ID = "cpuThreadsNotch";
await setCPUs(index, getToggleState(document.getElementById("smtToggle")));
selectNotch(ROOT_ID, await getCPUs() - 1, 8);
}
async function onSlideCPUNotch(e) {
const ROOT_ID = "cpuThreadsNotch";
let closest = closestNotch(e, ROOT_ID, 8);
await setCPUNotch(closest);
}
const TOGGLE_ON_CLASS = "gamepaddialog_On_3ld7T";
function setToggleState(toggle, state) {
if (state && !toggle.classList.contains(TOGGLE_ON_CLASS)) {
toggle.classList.add(TOGGLE_ON_CLASS);
}
if (!state && toggle.classList.contains(TOGGLE_ON_CLASS)) {
toggle.classList.remove(TOGGLE_ON_CLASS);
}
}
function getToggleState(toggle) {
return toggle.classList.contains(TOGGLE_ON_CLASS);
}
async function toggleCPUBoost() {
let toggle = document.getElementById("boostToggle");
let isActive = getToggleState(toggle);
await setCPUBoost(!isActive);
setToggleState(toggle, !isActive);
}
async function toggleCPUSMT() {
let toggle = document.getElementById("smtToggle");
let isActive = getToggleState(toggle);
let currentCPUs = await getCPUs();
if (currentCPUs == 4 && !isActive) {
// if all cores are running, enable all the threads as well
await setCPUs(8, !isActive);
} else {
await setCPUs(currentCPUs, !isActive);
}
setToggleState(toggle, !isActive);
selectNotch("cpuThreadsNotch", await getCPUs() - 1, 8);
}
async function setBoostNotch(index) {
const ROOT_ID = "frequencyNotch";
await setMaxBoost(index);
selectNotch(ROOT_ID, await getMaxBoost(), 3);
}
async function onSlideBoostNotch(e) {
const ROOT_ID = "frequencyNotch";
let closest = closestNotch(e, ROOT_ID, 3);
await setBoostNotch(closest);
}
async function onSetFanNotch(index) {
const ROOT_ID = "fanNotch";
await setFanTick(index);
selectNotch(ROOT_ID, index, 8);
}
async function onSlideFanNotch(e) {
const ROOT_ID = "fanNotch";
let closest = closestNotch(e, ROOT_ID, 8);
await onSetFanNotch(closest);
}
async function onReadyGPU() {
let power1_cap = await getGPUPower(1);
let power2_cap = await getGPUPower(2);
if (power1_cap <= 0) {
selectNotch("slowPPTNotch", 0, 3);
document.getElementById("slowPPTAutoDefault").innerText = "Default";
} else if (power1_cap > 15000000) {
selectNotch("slowPPTNotch", 2, 3);
document.getElementById("slowPPTAutoDefault").innerText = "Default";
} else {
selectNotch("slowPPTNotch", 1, 3);
}
if (power2_cap <= 0) {
selectNotch("fastPPTNotch", 0, 3);
document.getElementById("fastPPTAutoDefault").innerText = "Default";
} else if (power2_cap > 15000000) {
selectNotch("fastPPTNotch", 2, 3);
document.getElementById("fastPPTAutoDefault").innerText = "Default";
} else {
selectNotch("fastPPTNotch", 1, 3);
}
}
async function onSetSlowPPTNotch(index) {
const ROOT_ID = "slowPPTNotch";
document.getElementById("slowPPTAutoDefault").innerText = "Default";
if (index == 0) {
await setGPUPower(0, 1);
} else if (index == 1) {
await setGPUPower(15000000, 1);
} else {
await setGPUPower(29000000, 1);
}
selectNotch(ROOT_ID, index, 3);
}
async function onSlideSlowPPTNotch(e) {
const ROOT_ID = "slowPPTNotch";
let closest = closestNotch(e, ROOT_ID, 3);
onSetSlowPPTNotch(closest);
}
async function onSetFastPPTNotch(index) {
const ROOT_ID = "fastPPTNotch";
document.getElementById("fastPPTAutoDefault").innerText = "Default";
if (index == 0) {
await setGPUPower(0, 2);
} else if (index == 1) {
await setGPUPower(15000000, 2);
} else {
await setGPUPower(30000000, 2);
}
selectNotch(ROOT_ID, index, 3);
}
async function onSlideFastPPTNotch(e) {
const ROOT_ID = "fastPPTNotch";
let closest = closestNotch(e, ROOT_ID, 3);
await onSetFastPPTNotch(closest);
}
function selectNotch(rootId, index, elements) {
// WARNING: this yeets any style in div of slider
const ENABLED_CLASS = "gamepadslider_TickActive_1gnUV";
//console.log("Selecting notches up to " + index);
let root = document.getElementById(rootId);
root.style = "--normalized-slider-value:" + index/(elements-1) + ";";
for (let i = 0; i < elements; i++) {
let notch = document.getElementById(rootId + i);
if (notch.classList.contains(ENABLED_CLASS) && i > index) {
notch.classList.remove(ENABLED_CLASS);
} else if (!notch.classList.contains(ENABLED_CLASS) && i <= index) {
notch.classList.add(ENABLED_CLASS);
}
}
}
function closestNotch(e, rootId, elements) {
let root = document.getElementById(rootId);
let val = e.x / root.scrollWidth;
let closest_notch = Math.round(val * elements);
if (closest_notch > elements) {
closest_notch = elements;
} else if (closest_notch < 0) {
closest_notch = 0;
}
return closest_notch
//selectNotch(closest_notch);
}
async function updateBatteryStats() {
//console.log("Updating battery stats");
let batCapacityNow = document.getElementById("batCapacityNow");
let batCapacityFull = document.getElementById("batCapacityFull");
let chargeNow = await getChargeNow();
let chargeFull = await getChargeFull();
let chargeDesign = await getChargeDesign();
batCapacityNow.innerText = (7.7 * chargeNow / 1000000).toFixed(1).toString() + " Wh (" + (100 * chargeNow / chargeFull).toFixed(1).toString() + "%)";
batCapacityFull.innerText = (7.7 * chargeFull / 1000000).toFixed(1).toString() + " Wh (" + (100 * chargeFull / chargeDesign).toFixed(1).toString() + "%)";
}
async function togglePersist() {
let toggle = document.getElementById("persistToggle");
let isActive = getToggleState(toggle);
await setPersistent(!isActive);
setToggleState(toggle, !isActive);
}
async function toggleGameProfile() {
let toggle = document.getElementById("gameProfileToggle");
let isActive = getToggleState(toggle);
await setPerGameProfile(!isActive);
setToggleState(toggle, await getPerGameProfile());
}
let lastGameName = "";
async function updateCurrentGame() {
let gameNow = document.getElementById("gameNow");
let gameNameNow = await getCurrentGame();
if (lastGameName != gameNameNow) {
setToggleState(document.getElementById("gameProfileToggle"), await getPerGameProfile());
await reloadSettings();
}
lastGameName = gameNameNow
gameNow.innerText = gameNameNow;
}
let versionCount = -1;
async function updateVersion() {
let version = await getVersion();
let target = document.getElementById("versionStr");
target.innerText = "v" + version;
if (versionCount >= 9) {
target.innerText += " by NGnius ;) ";
versionCount = 0;
} else {
versionCount += 1;
}
}
</script>
<style type="text/css" media="screen"></style>
</head>
<body onload="onReady()" style="/*margin:0px;padding:0px;*/overflow-x:hidden;margin:0px;">
<!-- Spacer (moves top out of shadow above it) -->
<div class="quickaccessmenu_TabGroupPanel_1QO7b">
<div class="quickaccesscontrols_PanelSection_2C0g0" style="margin-bottom:6px;">
<!--<div class="quickaccesscontrols_PanelSectionRow_2VQ88">
</div>-->
</div>
</div>
<!-- CPU -->
<!-- SMT toggle switch, roughly copied from https://github.com/SteamDeckHomebrew/ExtraSettingsPlugin/blob/main/main_view.html -->
<!-- Due to a bug in MangoHud, this has a warning for now -->
<div class="quickaccessmenu_TabGroupPanel_1QO7b Panel Focusable">
<div class="quickaccesscontrols_PanelSection_2C0g0" style="">
<div class="quickaccesscontrols_PanelSectionRow_2VQ88">
<div class="gamepaddialog_Field_S-_La gamepaddialog_WithFirstRow_qFXi6 gamepaddialog_VerticalAlignCenter_3XNvA gamepaddialog_WithDescription_3bMIS gamepaddialog_ExtraPaddingOnChildrenBelow_5UO-_ gamepaddialog_StandardPadding_XRBFu gamepaddialog_HighlightOnFocus_wE4V6 Panel Focusable" style="--indent-level:0;" onclick="toggleCPUSMT()">
<div class="gamepaddialog_FieldLabelRow_H9WOq">
<div class="gamepaddialog_FieldLabel_3b0U-">
CPU SMT
</div>
<div class="gamepaddialog_FieldChildren_14_HB">
<div id="smtToggle" tabindex="0" class="gamepaddialog_Toggle_24G4g Focusable" >
<div class="gamepaddialog_ToggleRail_2JtC3"></div>
<div class="gamepaddialog_ToggleSwitch_3__OD"></div>
</div>
</div>
</div>
<div class="gamepaddialog_FieldDescription_2OJfk">Enables odd-numbered CPUs</div>
</div>
</div>
<!-- CPUs selector -->
<div class="quickaccesscontrols_PanelSectionRow_2VQ88">
<div class="gamepaddialog_Field_S-_La gamepaddialog_WithFirstRow_qFXi6 gamepaddialog_WithChildrenBelow_1u5FT gamepaddialog_VerticalAlignCenter_3XNvA gamepaddialog_InlineWrapShiftsChildrenBelow_pHUb6 gamepaddialog_WithBottomSeparatorStandard_3s1Rk gamepaddialog_ChildrenWidthFixed_1ugIU gamepaddialog_ExtraPaddingOnChildrenBelow_5UO-_ gamepaddialog_StandardPadding_XRBFu gamepaddialog_HighlightOnFocus_wE4V6 Panel Focusable">
<div class="gamepaddialog_FieldLabelRow_H9WOq">
<div class="gamepaddialog_FieldLabel_3b0U-">Threads</div>
</div>
<div id="cpuThreadsNotch" class="gamepadslider_SliderControlAndNotches_1Cccx Focusable" tabindex="0" style="--normalized-slider-value:0.5;" onmousemove="onSlideCPUNotch(event)">
<div class="gamepadslider_SliderControl_3o137">
<div class="gamepadslider_SliderTrack_Mq25N gamepadslider_SliderHasNotches_2XiAy "></div>
<div class="gamepadslider_SliderHandleContainer_1pQZi">
<div class="gamepadslider_SliderHandle_2yVKj"></div>
</div>
</div>
<div class="gamepadslider_SliderNotchContainer_2N-a5 Panel Focusable">
<div class="gamepadslider_SliderNotch_3x6ve">
<div id="cpuThreadsNotch0" class="gamepadslider_SliderNotchTick_Fv1Ht" onclick='setCPUNotch(1)'></div>
<div class="gamepadslider_SliderNotchLabel_u_sH1">1</div>
</div>
<div class="gamepadslider_SliderNotch_3x6ve">
<div id="cpuThreadsNotch1" class="gamepadslider_SliderNotchTick_Fv1Ht" onclick='setCPUNotch(2)'></div>
<div class="gamepadslider_SliderNotchLabel_u_sH1">2</div>
</div>
<div class="gamepadslider_SliderNotch_3x6ve">
<div id="cpuThreadsNotch2" class="gamepadslider_SliderNotchTick_Fv1Ht" onclick='setCPUNotch(3)'></div>
<div class="gamepadslider_SliderNotchLabel_u_sH1">3</div>
</div>
<div class="gamepadslider_SliderNotch_3x6ve">
<div id="cpuThreadsNotch3" class="gamepadslider_SliderNotchTick_Fv1Ht" onclick='setCPUNotch(4)'></div>
<div class="gamepadslider_SliderNotchLabel_u_sH1">4</div>
</div>
<div class="gamepadslider_SliderNotch_3x6ve">
<div id="cpuThreadsNotch4" class="gamepadslider_SliderNotchTick_Fv1Ht" onclick='setCPUNotch(5)'></div>
<div class="gamepadslider_SliderNotchLabel_u_sH1">5</div>
</div>
<div class="gamepadslider_SliderNotch_3x6ve">
<div id="cpuThreadsNotch5" class="gamepadslider_SliderNotchTick_Fv1Ht" onclick='setCPUNotch(6)'></div>
<div class="gamepadslider_SliderNotchLabel_u_sH1">6</div>
</div>
<div class="gamepadslider_SliderNotch_3x6ve">
<div id="cpuThreadsNotch6" class="gamepadslider_SliderNotchTick_Fv1Ht" onclick='setCPUNotch(7)'></div>
<div class="gamepadslider_SliderNotchLabel_u_sH1">7</div>
</div>
<div class="gamepadslider_SliderNotch_3x6ve">
<div id="cpuThreadsNotch7" class="gamepadslider_SliderNotchTick_Fv1Ht" onclick='setCPUNotch(8)'></div>
<div class="gamepadslider_SliderNotchLabel_u_sH1">8</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- CPU Boost toggle switch, roughly copied from https://github.com/SteamDeckHomebrew/ExtraSettingsPlugin/blob/main/main_view.html -->
<div class="quickaccesscontrols_PanelSection_2C0g0" style="">
<div class="quickaccesscontrols_PanelSectionRow_2VQ88">
<div class="gamepaddialog_Field_S-_La gamepaddialog_WithFirstRow_qFXi6 gamepaddialog_VerticalAlignCenter_3XNvA gamepaddialog_WithDescription_3bMIS gamepaddialog_ExtraPaddingOnChildrenBelow_5UO-_ gamepaddialog_StandardPadding_XRBFu gamepaddialog_HighlightOnFocus_wE4V6 Panel Focusable" style="--indent-level:0;" onclick="toggleCPUBoost()">
<div class="gamepaddialog_FieldLabelRow_H9WOq">
<div class="gamepaddialog_FieldLabel_3b0U-">
CPU Boost
</div>
<div class="gamepaddialog_FieldChildren_14_HB">
<div id="boostToggle" tabindex="0" class="gamepaddialog_Toggle_24G4g Focusable">
<div class="gamepaddialog_ToggleRail_2JtC3"></div>
<div class="gamepaddialog_ToggleSwitch_3__OD"></div>
</div>
</div>
</div>
<div class="gamepaddialog_FieldDescription_2OJfk">Allows the CPU to go above max frequency</div>
</div>
</div>
<!-- Frequency selector -->
<div class="quickaccesscontrols_PanelSectionRow_2VQ88">
<div class="gamepaddialog_Field_S-_La gamepaddialog_WithFirstRow_qFXi6 gamepaddialog_WithChildrenBelow_1u5FT gamepaddialog_VerticalAlignCenter_3XNvA gamepaddialog_InlineWrapShiftsChildrenBelow_pHUb6 gamepaddialog_WithBottomSeparatorStandard_3s1Rk gamepaddialog_ChildrenWidthFixed_1ugIU gamepaddialog_ExtraPaddingOnChildrenBelow_5UO-_ gamepaddialog_StandardPadding_XRBFu gamepaddialog_HighlightOnFocus_wE4V6 Panel Focusable">
<div class="gamepaddialog_FieldLabelRow_H9WOq">
<div class="gamepaddialog_FieldLabel_3b0U-">Max Frequency</div>
</div>
<div class="gamepaddialog_FieldChildren_14_HB">
<div id="frequencyNotch" class="gamepadslider_SliderControlAndNotches_1Cccx Focusable" tabindex="0" style="--normalized-slider-value:0.5;" onmousemove="onSlideBoostNotch(event)">
<div class="gamepadslider_SliderControl_3o137">
<div class="gamepadslider_SliderTrack_Mq25N gamepadslider_SliderHasNotches_2XiAy "></div>
<div class="gamepadslider_SliderHandleContainer_1pQZi">
<div class="gamepadslider_SliderHandle_2yVKj"></div>
</div>
</div>
<div class="gamepadslider_SliderNotchContainer_2N-a5 Panel Focusable">
<div class="gamepadslider_SliderNotch_3x6ve">
<div id="frequencyNotch0" class="gamepadslider_SliderNotchTick_Fv1Ht gamepadslider_TickActive_j418S" onclick='setBoostNotch(0)'></div>
<div class="gamepadslider_SliderNotchLabel_u_sH1" style="margin-left:2em;">1.7GHz</div>
</div>
<div class="gamepadslider_SliderNotch_3x6ve">
<div id="frequencyNotch1" class="gamepadslider_SliderNotchTick_Fv1Ht gamepadslider_TickActive_j418S" onclick='setBoostNotch(1)'></div>
<div class="gamepadslider_SliderNotchLabel_u_sH1">2.4GHz</div>
</div>
<div class="gamepadslider_SliderNotch_3x6ve">
<div id="frequencyNotch2" class="gamepadslider_SliderNotchTick_Fv1Ht" onclick='setBoostNotch(2)'></div>
<div class="gamepadslider_SliderNotchLabel_u_sH1" style="margin-right:2em;">2.8GHz</div>
</div>
</div>
</div>
</div>
<div style="font-size:x-small;">
WARNING: This will change the CPU governor.
</div>
</div>
</div>
</div>
<!-- GPU -->
<div class="quickaccesscontrols_PanelSection_2C0g0" style="">
<!-- SlowPPT power limit (number 1) -->
<div class="quickaccesscontrols_PanelSectionRow_2VQ88">
<div class="gamepaddialog_Field_S-_La gamepaddialog_WithFirstRow_qFXi6 gamepaddialog_WithChildrenBelow_1u5FT gamepaddialog_VerticalAlignCenter_3XNvA gamepaddialog_InlineWrapShiftsChildrenBelow_pHUb6 gamepaddialog_ChildrenWidthFixed_1ugIU gamepaddialog_ExtraPaddingOnChildrenBelow_5UO-_ gamepaddialog_StandardPadding_XRBFu gamepaddialog_HighlightOnFocus_wE4V6 Panel Focusable">
<div class="gamepaddialog_FieldLabelRow_H9WOq">
<div class="gamepaddialog_FieldLabel_3b0U-">GPU SlowPPT Power</div>
</div>
<div class="gamepaddialog_FieldChildren_14_HB">
<div id="slowPPTNotch" class="gamepadslider_SliderControlAndNotches_1Cccx Focusable" tabindex="0" style="--normalized-slider-value:0.33;" onmousemove="onSlideSlowPPTNotch(event)">
<div class="gamepadslider_SliderControl_3o137">
<div class="gamepadslider_SliderTrack_Mq25N gamepadslider_SliderHasNotches_2XiAy "></div>
<div class="gamepadslider_SliderHandleContainer_1pQZi">
<div class="gamepadslider_SliderHandle_2yVKj"></div>
</div>
</div>
<div class="gamepadslider_SliderNotchContainer_2N-a5 Panel Focusable">
<div class="gamepadslider_SliderNotch_3x6ve">
<div id="slowPPTNotch0" class="gamepadslider_SliderNotchTick_Fv1Ht gamepadslider_TickActive_j418S" onclick='onSetSlowPPTNotch(0)'></div>
<div class="gamepadslider_SliderNotchLabel_u_sH1">0</div>
</div>
<div class="gamepadslider_SliderNotch_3x6ve">
<div id="slowPPTNotch1" class="gamepadslider_SliderNotchTick_Fv1Ht gamepadslider_TickActive_j418S" onclick='onSetSlowPPTNotch(1)'></div>
<div class="gamepadslider_SliderNotchLabel_u_sH1" id="slowPPTAutoDefault">Auto</div>
</div>
<div class="gamepadslider_SliderNotch_3x6ve">
<div id="slowPPTNotch2" class="gamepadslider_SliderNotchTick_Fv1Ht gamepadslider_TickActive_j418S" onclick='onSetSlowPPTNotch(2)'></div>
<div class="gamepadslider_SliderNotchLabel_u_sH1">Max</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- FastPPT power limit (number 2) -->
<div class="quickaccesscontrols_PanelSectionRow_2VQ88">
<div class="gamepaddialog_Field_S-_La gamepaddialog_WithFirstRow_qFXi6 gamepaddialog_WithChildrenBelow_1u5FT gamepaddialog_VerticalAlignCenter_3XNvA gamepaddialog_InlineWrapShiftsChildrenBelow_pHUb6 gamepaddialog_WithBottomSeparatorStandard_3s1Rk gamepaddialog_ChildrenWidthFixed_1ugIU gamepaddialog_ExtraPaddingOnChildrenBelow_5UO-_ gamepaddialog_StandardPadding_XRBFu gamepaddialog_HighlightOnFocus_wE4V6 Panel Focusable">
<div class="gamepaddialog_FieldLabelRow_H9WOq">
<div class="gamepaddialog_FieldLabel_3b0U-">GPU FastPPT Power</div>
</div>
<div class="gamepaddialog_FieldChildren_14_HB">
<div id="fastPPTNotch" class="gamepadslider_SliderControlAndNotches_1Cccx Focusable" tabindex="0" style="--normalized-slider-value:0.33;" onmousemove="onSlideFastPPTNotch(event)">
<div class="gamepadslider_SliderControl_3o137">
<div class="gamepadslider_SliderTrack_Mq25N gamepadslider_SliderHasNotches_2XiAy "></div>
<div class="gamepadslider_SliderHandleContainer_1pQZi">
<div class="gamepadslider_SliderHandle_2yVKj"></div>
</div>
</div>
<div class="gamepadslider_SliderNotchContainer_2N-a5 Panel Focusable">
<div class="gamepadslider_SliderNotch_3x6ve">
<div id="fastPPTNotch0" class="gamepadslider_SliderNotchTick_Fv1Ht gamepadslider_TickActive_j418S" onclick='onSetFastPPTNotch(0)'></div>
<div class="gamepadslider_SliderNotchLabel_u_sH1">0</div>
</div>
<div class="gamepadslider_SliderNotch_3x6ve">
<div id="fastPPTNotch1" class="gamepadslider_SliderNotchTick_Fv1Ht gamepadslider_TickActive_j418S" onclick='onSetFastPPTNotch(1)'></div>
<div class="gamepadslider_SliderNotchLabel_u_sH1" id="fastPPTAutoDefault">Auto</div>
</div>
<div class="gamepadslider_SliderNotch_3x6ve">
<div id="fastPPTNotch2" class="gamepadslider_SliderNotchTick_Fv1Ht gamepadslider_TickActive_j418S" onclick='onSetFastPPTNotch(2)'></div>
<div class="gamepadslider_SliderNotchLabel_u_sH1">Max</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Fan RPM selector -->
<div class="quickaccesscontrols_PanelSection_2C0g0" style="" id="fanRoot">
<div class="quickaccesscontrols_PanelSectionRow_2VQ88">
<!-- TODO: Make this non-notched slider when PluginLoader PR#41 is merged -->
<div class="gamepaddialog_Field_S-_La gamepaddialog_WithFirstRow_qFXi6 gamepaddialog_WithChildrenBelow_1u5FT gamepaddialog_VerticalAlignCenter_3XNvA gamepaddialog_InlineWrapShiftsChildrenBelow_pHUb6 gamepaddialog_WithBottomSeparatorStandard_3s1Rk gamepaddialog_ChildrenWidthFixed_1ugIU gamepaddialog_ExtraPaddingOnChildrenBelow_5UO-_ gamepaddialog_StandardPadding_XRBFu gamepaddialog_HighlightOnFocus_wE4V6 Panel Focusable">
<div class="gamepaddialog_FieldLabelRow_H9WOq">
<div class="gamepaddialog_FieldLabel_3b0U-">Fan RPM</div>
</div>
<div class="gamepaddialog_FieldDescription_2OJfk" style="display:none;">Requires disabling updated fan control</div>
<div class="gamepaddialog_FieldChildren_14_HB">
<div id="fanNotch" class="gamepadslider_SliderControlAndNotches_1Cccx Focusable" tabindex="0" style="--normalized-slider-value:0.33;" onmousemove="onSlideFanNotch(event)">
<div class="gamepadslider_SliderControl_3o137">
<div class="gamepadslider_SliderTrack_Mq25N gamepadslider_SliderHasNotches_2XiAy "></div>
<div class="gamepadslider_SliderHandleContainer_1pQZi">
<div class="gamepadslider_SliderHandle_2yVKj"></div>
</div>
</div>
<div class="gamepadslider_SliderNotchContainer_2N-a5 Panel Focusable">
<div class="gamepadslider_SliderNotch_3x6ve">
<div id="fanNotch0" class="gamepadslider_SliderNotchTick_Fv1Ht gamepadslider_TickActive_j418S" onclick='onSetFanNotch(0)'></div>
<div class="gamepadslider_SliderNotchLabel_u_sH1">0</div>
</div>
<div class="gamepadslider_SliderNotch_3x6ve">
<div id="fanNotch1" class="gamepadslider_SliderNotchTick_Fv1Ht gamepadslider_TickActive_j418S" onclick='onSetFanNotch(1)'></div>
<div class="gamepadslider_SliderNotchLabel_u_sH1">1K</div>
</div>
<div class="gamepadslider_SliderNotch_3x6ve">
<div id="fanNotch2" class="gamepadslider_SliderNotchTick_Fv1Ht gamepadslider_TickActive_j418S" onclick='onSetFanNotch(2)'></div>
<div class="gamepadslider_SliderNotchLabel_u_sH1">2K</div>
</div>
<div class="gamepadslider_SliderNotch_3x6ve">
<div id="fanNotch3" class="gamepadslider_SliderNotchTick_Fv1Ht gamepadslider_TickActive_j418S" onclick='onSetFanNotch(3)'></div>
<div class="gamepadslider_SliderNotchLabel_u_sH1">3K</div>
</div>
<div class="gamepadslider_SliderNotch_3x6ve">
<div id="fanNotch4" class="gamepadslider_SliderNotchTick_Fv1Ht gamepadslider_TickActive_j418S" onclick='onSetFanNotch(4)'></div>
<div class="gamepadslider_SliderNotchLabel_u_sH1">4K</div>
</div>
<div class="gamepadslider_SliderNotch_3x6ve">
<div id="fanNotch5" class="gamepadslider_SliderNotchTick_Fv1Ht gamepadslider_TickActive_j418S" onclick='onSetFanNotch(5)'></div>
<div class="gamepadslider_SliderNotchLabel_u_sH1">5K</div>
</div>
<div class="gamepadslider_SliderNotch_3x6ve">
<div id="fanNotch6" class="gamepadslider_SliderNotchTick_Fv1Ht gamepadslider_TickActive_j418S" onclick='onSetFanNotch(6)'></div>
<div class="gamepadslider_SliderNotchLabel_u_sH1">6K</div>
</div>
<div class="gamepadslider_SliderNotch_3x6ve">
<div id="fanNotch7" class="gamepadslider_SliderNotchTick_Fv1Ht gamepadslider_TickActive_j418S" onclick='onSetFanNotch(7)'></div>
<div class="gamepadslider_SliderNotchLabel_u_sH1">Auto</div>
</div>
</div>
</div>
</div>
<div style="font-size:x-small;">
WARNING: This can cause component overheating.
</div>
</div>
</div>
</div>
<!-- Battery Info -->
<div class="quickaccesscontrols_PanelSection_2C0g0" style="" onclick="updateBatteryStats()" style="margin-bottom:0px;">
<div class="quickaccesscontrols_PanelSectionTitle_2iFf9">
<div class="quickaccesscontrols_Text_1hJkB">Battery</div>
</div>
<div class="Panel Focusable" tabindex="0">
<div class="quickaccesscontrols_PanelSectionRow_2VQ88">
<div class="gamepaddialog_Field_S-_La gamepaddialog_WithFirstRow_qFXi6 gamepaddialog_VerticalAlignCenter_3XNvA gamepaddialog_InlineWrapShiftsChildrenBelow_pHUb6 gamepaddialog_WithBottomSeparatorStandard_3s1Rk gamepaddialog_StandardPadding_XRBFu gamepaddialog_HighlightOnFocus_wE4V6 Panel Focusable" style="--indent-level:0;">
<div class="gamepaddialog_FieldLabelRow_H9WOq">
<div class="gamepaddialog_FieldLabel_3b0U-">Now (Charge)</div>
<div class="gamepaddialog_FieldChildren_14_HB">
<div class="gamepaddialog_LabelFieldValue_5Mylh" id="batCapacityNow"> :'( (|-_-|) </div>
</div>
</div>
</div>
</div>
<div class="quickaccesscontrols_PanelSectionRow_2VQ88">
<div class="gamepaddialog_Field_S-_La gamepaddialog_WithFirstRow_qFXi6 gamepaddialog_VerticalAlignCenter_3XNvA gamepaddialog_InlineWrapShiftsChildrenBelow_pHUb6 gamepaddialog_WithBottomSeparatorStandard_3s1Rk gamepaddialog_StandardPadding_XRBFu gamepaddialog_HighlightOnFocus_wE4V6 Panel Focusable" style="--indent-level:0;">
<div class="gamepaddialog_FieldLabelRow_H9WOq">
<div class="gamepaddialog_FieldLabel_3b0U-">Max (Design)</div>
<div class="gamepaddialog_FieldChildren_14_HB">
<div class="gamepaddialog_LabelFieldValue_5Mylh" id="batCapacityFull"> 9000+ (420%) </div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="quickaccesscontrols_PanelSection_2C0g0" style="">
<div class="quickaccesscontrols_PanelSectionRow_2VQ88">
<div class="gamepaddialog_Field_S-_La gamepaddialog_WithFirstRow_qFXi6 gamepaddialog_VerticalAlignCenter_3XNvA gamepaddialog_WithDescription_3bMIS gamepaddialog_ExtraPaddingOnChildrenBelow_5UO-_ gamepaddialog_StandardPadding_XRBFu gamepaddialog_HighlightOnFocus_wE4V6 Panel Focusable" style="--indent-level:0;" onclick="togglePersist()">
<div class="gamepaddialog_FieldLabelRow_H9WOq">
<div class="gamepaddialog_FieldLabel_3b0U-">
Persistent
</div>
<div class="gamepaddialog_FieldChildren_14_HB">
<div id="persistToggle" tabindex="0" class="gamepaddialog_Toggle_24G4g Focusable">
<div class="gamepaddialog_ToggleRail_2JtC3"></div>
<div class="gamepaddialog_ToggleSwitch_3__OD"></div>
</div>
</div>
</div>
<div class="gamepaddialog_FieldDescription_2OJfk">Restores settings after an app or OS restart</div>
</div>
</div>
<div class="quickaccesscontrols_PanelSectionRow_2VQ88">
<div class="gamepaddialog_Field_S-_La gamepaddialog_WithFirstRow_qFXi6 gamepaddialog_VerticalAlignCenter_3XNvA gamepaddialog_WithDescription_3bMIS gamepaddialog_ExtraPaddingOnChildrenBelow_5UO-_ gamepaddialog_StandardPadding_XRBFu gamepaddialog_HighlightOnFocus_wE4V6 Panel Focusable" style="--indent-level:0;" onclick="toggleGameProfile()">
<div class="gamepaddialog_FieldLabelRow_H9WOq">
<div class="gamepaddialog_FieldLabel_3b0U-">
Use per-game profile
</div>
<div class="gamepaddialog_FieldChildren_14_HB">
<div id="gameProfileToggle" tabindex="0" class="gamepaddialog_Toggle_24G4g Focusable">
<div class="gamepaddialog_ToggleRail_2JtC3"></div>
<div class="gamepaddialog_ToggleSwitch_3__OD"></div>
</div>
</div>
</div>
</div>
</div>
<div class="quickaccesscontrols_PanelSectionRow_2VQ88">
<div class="gamepaddialog_Field_S-_La gamepaddialog_WithFirstRow_qFXi6 gamepaddialog_VerticalAlignCenter_3XNvA gamepaddialog_InlineWrapShiftsChildrenBelow_pHUb6 gamepaddialog_WithBottomSeparatorStandard_3s1Rk gamepaddialog_StandardPadding_XRBFu gamepaddialog_HighlightOnFocus_wE4V6 Panel Focusable" style="--indent-level:0;">
<div class="gamepaddialog_FieldLabelRow_H9WOq">
<div class="gamepaddialog_FieldLabel_3b0U-">Now Playing</div>
<div class="gamepaddialog_FieldChildren_14_HB">
<div class="gamepaddialog_LabelFieldValue_5Mylh" id="gameNow"> the bongos </div>
</div>
</div>
</div>
</div>
<div class="quickaccesscontrols_PanelSectionRow_2VQ88" onclick="updateVersion()">
<div class="gamepaddialog_Field_S-_La gamepaddialog_WithFirstRow_qFXi6 gamepaddialog_VerticalAlignCenter_3XNvA gamepaddialog_InlineWrapShiftsChildrenBelow_pHUb6 gamepaddialog_WithBottomSeparatorStandard_3s1Rk gamepaddialog_StandardPadding_XRBFu gamepaddialog_HighlightOnFocus_wE4V6 Panel Focusable" style="--indent-level:0;">
<div class="gamepaddialog_FieldLabelRow_H9WOq">
<div class="gamepaddialog_FieldLabel_3b0U-">PowerTools</div>
<div class="gamepaddialog_FieldChildren_14_HB">
<div class="gamepaddialog_LabelFieldValue_5Mylh" id="versionStr"> v0.42.0 </div>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

45
package.json Normal file
View file

@ -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) <ngniusness@gmail.com>",
"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.4.0"
}
}

View file

@ -1,8 +1,6 @@
{
"name": "PowerTools",
"author": "NGnius",
"main_view_html": "main_view.html",
"tile_view_html": "",
"flags": ["root", "_debug"],
"publish": {
"discord_id": "106537989684887552",

37
rollup.config.js Normal file
View file

@ -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',
},
});

View file

@ -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):
@ -42,99 +42,30 @@ class GameInfo:
return os.path.exists(self.settings_path())
class Server(web.Application):
class Server:
def __init__(self, version):
super().__init__()
self.version = version
self.current_game = None
self.add_routes([
web.get("/", lambda req: self.index(req)),
web.post("/on_game_start/{game_id}", lambda req: self.on_game_start(req)),
web.post("/on_game_exit/{game_id}", lambda req: self.on_game_exit(req)),
web.post("/on_game_exit_null", lambda req: self.on_game_exit_null(req)),
web.get("/self_destruct", lambda req: self.self_destruct(req))
])
logging.debug("Server init complete")
def game(self) -> GameInfo:
return self.current_game
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
game_info = None if self.current_game is None else self.current_game.game_info
settings_info = None if self.current_game is None else self.current_game.load_settings()
return web.json_response({
"name": "PowerTools",
"version": self.version,
"latest_game_id": current_game,
"game_info": game_info,
"settings": settings_info
}, headers={"Access-Control-Allow-Origin": "*"})
async def on_game_start(self, request):
game_id = request.match_info["game_id"]
data = await request.text()
logging.debug(f"on_game_start {game_id} body:\n{data}")
try:
game_id = int(game_id)
data = json.loads(data)
except:
return web.Response(text="WTF", status=400)
def set_game(self, game_id, data):
self.current_game = GameInfo(game_id, data)
if self.current_game.has_settings():
self.last_recognised_game = self.current_game
return web.Response(status=204, headers={"Access-Control-Allow-Origin": "*"})
async def on_game_exit(self, request):
# ignored for now
game_id = request.match_info["game_id"]
data = await request.text()
logging.debug(f"on_game_exit {game_id}")
try:
game_id = int(game_id)
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
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
return web.Response(status=204, headers={"Access-Control-Allow-Origin": "*"})
async def self_destruct(self, request):
logging.warning("Geodude self-destructed")
await shutdown()
# unreachable \/ \/ \/
return web.Response(status=204, headers={"Access-Control-Allow-Origin": "*"})
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 start(version):
global http_runner, http_server, http_site
# make sure old server has shutdown
try:
async with aiohttp.ClientSession() as session:
async with session.get('http://127.0.0.1:5030/self_destruct') as response:
await response.text()
except:
pass
global http_server
http_server = Server(version)
http_runner = web.AppRunner(http_server)
await http_runner.setup()
site = web.TCPSite(http_runner, '127.0.0.1', 5030)
await site.start()
async def shutdown(): # never really called
global http_runner, http_server, http_site
if http_runner is not None:
await http_runner.cleanup()
http_runner = None
http_site.stop()
http_site = None
global http_server
http_server = None

452
src/index.tsx Executable file
View file

@ -0,0 +1,452 @@
import {
//ButtonItem,
definePlugin,
DialogButton,
//Menu,
//MenuItem,
PanelSection,
PanelSectionRow,
//Router,
ServerAPI,
//showContextMenu,
staticClasses,
SliderField,
ToggleField,
//NotchLabel
gamepadDialogClasses,
joinClassNames,
} from "decky-frontend-lib";
import { VFC, useState } from "react";
import { GiDrill } from "react-icons/gi";
import * as python from "./python";
//import logo from "../assets/logo.png";
// interface AddMethodArgs {
// left: number;
// right: number;
// }
var firstTime: boolean = true;
var versionGlobalHolder: string = "0.0.0-jank";
var periodicHook: NodeJS.Timer | null = null;
var lastGame: string = "";
var lifetimeHook: any = null;
var startHook: any = null;
var smt_backup: boolean = true;
var cpus_backup: number = 8;
var boost_backup: boolean = true;
var freq_backup: number = 8;
var slowPPT_backup: number = 1;
var fastPPT_backup: number = 1;
var chargeNow_backup: number = 5200000;
var chargeFull_backup: number = 5200000;
var chargeDesign_backup: number = 5200000;
var persistent_backup: boolean = false;
var perGameProfile_backup: boolean = false;
var reload = function(){};
const Content: VFC<{ serverAPI: ServerAPI }> = ({serverAPI}) => {
// const [result, setResult] = useState<number | undefined>();
// const onClick = async () => {
// const result = await serverAPI.callPluginMethod<AddMethodArgs, number>(
// "add",
// {
// left: 2,
// right: 2,
// }
// );
// if (result.success) {
// setResult(result.result);
// }
// };
python.setServer(serverAPI);
const [smtGlobal, setSMT_internal] = useState<boolean>(smt_backup);
const setSMT = (value: boolean) => {
smt_backup = value;
setSMT_internal(value);
};
const [cpusGlobal, setCPUs_internal] = useState<number>(cpus_backup);
const setCPUs = (value: number) => {
cpus_backup = value;
setCPUs_internal(value);
};
const [boostGlobal, setBoost_internal] = useState<boolean>(boost_backup);
const setBoost = (value: boolean) => {
boost_backup = value;
setBoost_internal(value);
};
const [freqGlobal, setFreq_internal] = useState<number>(freq_backup);
const setFreq = (value: number) => {
freq_backup = value;
setFreq_internal(value);
};
const [slowPPTGlobal, setSlowPPT_internal] = useState<number>(slowPPT_backup);
const setSlowPPT = (value: number) => {
slowPPT_backup = value;
setSlowPPT_internal(value);
};
const [fastPPTGlobal, setFastPPT_internal] = useState<number>(fastPPT_backup);
const setFastPPT = (value: number) => {
fastPPT_backup = value;
setFastPPT_internal(value);
};
const [chargeNowGlobal, setChargeNow_internal] = useState<number>(chargeNow_backup);
const setChargeNow = (value: number) => {
chargeNow_backup = value;
setChargeNow_internal(value);
};
const [chargeFullGlobal, setChargeFull_internal] = useState<number>(chargeFull_backup);
const setChargeFull = (value: number) => {
chargeFull_backup = value;
setChargeFull_internal(value);
};
const [chargeDesignGlobal, setChargeDesign_internal] = useState<number>(chargeDesign_backup);
const setChargeDesign = (value: number) => {
chargeDesign_backup = value;
setChargeDesign_internal(value);
};
const [persistGlobal, setPersist_internal] = useState<boolean>(persistent_backup);
const setPersist = (value: boolean) => {
persistent_backup = value;
setPersist_internal(value);
};
const [perGameProfileGlobal, setPerGameProfile_internal] = useState<boolean>(perGameProfile_backup);
const setPerGameProfile = (value: boolean) => {
perGameProfile_backup = value;
setPerGameProfile_internal(value);
};
const [gameGlobal, setGame_internal] = useState<string>(lastGame);
const setGame = (value: string) => {
lastGame = value;
setGame_internal(value);
};
const [versionGlobal, setVersion_internal] = useState<string>(versionGlobalHolder);
const setVersion = (value: string) => {
versionGlobalHolder = value;
setVersion_internal(value);
};
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);
python.resolve(python.getVersion(), setVersion);
}
if (periodicHook != null) {
clearInterval(periodicHook);
periodicHook = null;
}
periodicHook = setInterval(function() {
python.resolve(python.getChargeFull(), setChargeFull);
python.resolve(python.getChargeNow(), setChargeNow);
python.resolve(python.getCurrentGame(), (game: string) => {
if (lastGame != game) {
setGame(game);
lastGame = game;
reload();
}
});
}, 1000);
const FieldWithSeparator = joinClassNames(gamepadDialogClasses.Field, gamepadDialogClasses.WithBottomSeparatorStandard);
return (
<PanelSection>
{/* CPU */}
<div className={staticClasses.PanelSectionTitle}>
CPU
</div>
<PanelSectionRow>
<ToggleField
checked={smtGlobal}
label="SMT"
description="Enables odd-numbered CPUs"
onChange={(smt: boolean) => {
console.log("SMT is now " + smt.toString());
python.execute(python.setCPUs(cpusGlobal, smt));
python.resolve(python.getCPUs(), setCPUs);
python.resolve(python.getSMT(), setSMT);
}}
/>
</PanelSectionRow>
<PanelSectionRow>
<SliderField
label="Threads"
value={cpusGlobal}
step={1}
max={smtGlobal? 8 : 4}
min={1}
showValue={true}
onChange={(cpus: number) => {
console.log("CPU slider is now " + cpus.toString());
if (cpus != cpusGlobal) {
python.execute(python.setCPUs(cpus, smtGlobal));
python.resolve(python.getCPUs(), setCPUs);
}
}}
/>
</PanelSectionRow>
<PanelSectionRow>
<ToggleField
checked={boostGlobal}
label="Boost"
description="Allows the CPU to go above max frequency"
onChange={(boost: boolean) => {
console.log("Boost is now " + boost.toString());
python.execute(python.setCPUBoost(boost));
python.resolve(python.getCPUBoost(), setBoost);
}}
/>
</PanelSectionRow>
<PanelSectionRow>
<SliderField
label="Max Frequency"
value={freqGlobal}
max={2}
min={0}
notchCount={3}
notchLabels={[
{notchIndex: 0, label: "1.7GHz"},
{notchIndex: 1, label: "2.4GHz"},
{notchIndex: 2, label: "2.8GHz"},
]}
notchTicksVisible={true}
onChange={(freq: number) => {
console.log("CPU slider is now " + freq.toString());
if (freq != freqGlobal) {
python.execute(python.setMaxBoost(freq));
python.resolve(python.getMaxBoost(), setFreq);
}
}}
/>
</PanelSectionRow>
{/* GPU */}
<div className={staticClasses.PanelSectionTitle}>
GPU
</div>
<PanelSectionRow>
{/* index: 1 */}
<SliderField
label="SlowPPT Power"
value={slowPPTGlobal}
max={2}
min={0}
notchCount={3}
notchLabels={[
{notchIndex: 0, label: "Min"},
{notchIndex: 1, label: "Auto"},
{notchIndex: 2, label: "Max"},
]}
notchTicksVisible={true}
onChange={(ppt: number) => {
console.log("SlowPPT is now " + ppt.toString());
if (ppt != slowPPTGlobal) {
python.execute(python.setGPUPowerI(ppt, 1));
python.resolve(python.getGPUPowerI(1), setSlowPPT);
}
}}
/>
</PanelSectionRow>
<PanelSectionRow>
{/* index: 2 */}
<SliderField
label="FastPPT Power"
value={fastPPTGlobal}
max={2}
min={0}
notchCount={3}
notchLabels={[
{notchIndex: 0, label: "Min"},
{notchIndex: 1, label: "Auto"},
{notchIndex: 2, label: "Max"},
]}
notchTicksVisible={true}
onChange={(ppt: number) => {
console.log("FastPPT is now " + ppt.toString());
if (ppt != fastPPTGlobal) {
python.execute(python.setGPUPowerI(ppt, 2));
python.resolve(python.getGPUPowerI(2), setFastPPT);
}
}}
/>
</PanelSectionRow>
{/* Battery */}
<div className={staticClasses.PanelSectionTitle}>
Battery
</div>
<PanelSectionRow>
<div className={FieldWithSeparator}>
<div className={gamepadDialogClasses.FieldLabelRow}>
<div className={gamepadDialogClasses.FieldLabel}>
Now (Charge)
</div>
<div className={gamepadDialogClasses.FieldChildren}>
{(7.7 * chargeNowGlobal / 1000000).toFixed(1).toString() + " Wh (" + (100 * chargeNowGlobal / chargeFullGlobal).toFixed(1).toString() + "%)"}
</div>
</div>
</div>
</PanelSectionRow>
<PanelSectionRow>
<div className={FieldWithSeparator}>
<div className={gamepadDialogClasses.FieldLabelRow}>
<div className={gamepadDialogClasses.FieldLabel}>
Max (Design)
</div>
<div className={gamepadDialogClasses.FieldChildren}>
{(7.7 * chargeFullGlobal / 1000000).toFixed(1).toString() + " Wh (" + (100 * chargeFullGlobal / chargeDesignGlobal).toFixed(1).toString() + "%)"}
</div>
</div>
</div>
</PanelSectionRow>
{/* Persistence */}
<PanelSectionRow>
<ToggleField
checked={persistGlobal}
label="Persistent"
description="Restores settings after an app or OS restart"
onChange={(persist: boolean) => {
console.log("Persist is now " + persist.toString());
python.execute(python.setPersistent(persist));
python.resolve(python.getPersistent(), setPersist);
}}
/>
</PanelSectionRow>
<PanelSectionRow>
<ToggleField
checked={perGameProfileGlobal}
label="Use per-game profile"
onChange={(p: boolean) => {
console.log("Per game profile is now " + p.toString());
python.execute(python.setPerGameProfile(p));
python.resolve(python.getPerGameProfile(), setPerGameProfile);
reload();
}}
/>
</PanelSectionRow>
<PanelSectionRow>
<div className={FieldWithSeparator}>
<div className={gamepadDialogClasses.FieldLabelRow}>
<div className={gamepadDialogClasses.FieldLabel}>
Now Playing
</div>
<div className={gamepadDialogClasses.FieldChildren}>
{gameGlobal}
</div>
</div>
</div>
</PanelSectionRow>
{/* Version */}
<div className={staticClasses.PanelSectionTitle}>
Debug
</div>
<PanelSectionRow>
<div className={FieldWithSeparator}>
<div className={gamepadDialogClasses.FieldLabelRow}>
<div className={gamepadDialogClasses.FieldLabel}>
PowerTools
</div>
<div className={gamepadDialogClasses.FieldChildren}>
v{versionGlobal}
</div>
</div>
</div>
</PanelSectionRow>
</PanelSection>
);
};
const DeckyPluginRouterTest: VFC = () => {
return (
<div style={{ marginTop: "50px", color: "white" }}>
Hello World!
<DialogButton onClick={() => {}}>
Go to Store
</DialogButton>
</div>
);
};
export default definePlugin((serverApi: ServerAPI) => {
serverApi.routerHook.addRoute("/decky-plugin-test", DeckyPluginRouterTest, {
exact: true,
});
//@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
startHook = SteamClient.Apps.RegisterForGameActionStart((actionType, id) => {
//@ts-ignore
let gameInfo: any = appStore.GetAppOverviewByGameID(id);
python.execute(python.onGameStart(id, gameInfo));
});
console.log("Registered PowerTools callbacks, hello!");
return {
title: <div className={staticClasses.Title}>PowerTools</div>,
content: <Content serverAPI={serverApi} />,
icon: <GiDrill />,
onDismount() {
console.log("PowerTools shutting down");
clearInterval(periodicHook!);
periodicHook = null;
lifetimeHook!.unregister();
startHook!.unregister();
serverApi.routerHook.removeRoute("/decky-plugin-test");
firstTime = true;
lastGame = "";
console.log("Unregistered PowerTools callbacks, goodbye.");
},
};
});

139
src/python.ts Normal file
View file

@ -0,0 +1,139 @@
import { ServerAPI } from "decky-frontend-lib";
var server: ServerAPI | undefined = undefined;
//import { useEffect } from "react";
export function resolve(promise: Promise<any>, setter: any) {
(async function () {
let data = await promise;
if (data.success) {
console.debug("Got resolved", data, "promise", promise);
setter(data.result);
} else {
console.warn("Resolve failed:", data, "promise", promise);
}
})();
}
export function execute(promise: Promise<any>) {
(async function () {
let data = await promise;
if (data.success) {
console.debug("Got executed", data, "promise", promise);
} else {
console.warn("Execute failed:", data, "promise", promise);
}
})();
}
export function setServer(s: ServerAPI) {
server = s;
}
// Python functions
export function getVersion(): Promise<any> {
return server!.callPluginMethod("get_version", {});
}
export function onViewReady(): Promise<any> {
return server!.callPluginMethod("on_ready", {});
}
export function setCPUs(value: number, smt: boolean): Promise<any> {
return server!.callPluginMethod("set_cpus", {"count":value, "smt": smt});
}
export function getCPUs(): Promise<any> {
return server!.callPluginMethod("get_cpus", {});
}
export function getSMT(): Promise<any> {
return server!.callPluginMethod("get_smt", {});
}
export function setCPUBoost(value: boolean): Promise<any> {
return server!.callPluginMethod("set_boost", {"enabled": value});
}
export function getCPUBoost(): Promise<any> {
return server!.callPluginMethod("get_boost", {});
}
export function setMaxBoost(index: number): Promise<any> {
return server!.callPluginMethod("set_max_boost", {"index": index});
}
export function getMaxBoost(): Promise<any> {
return server!.callPluginMethod("get_max_boost", {});
}
export function setGPUPower(value: number, index: number): Promise<any> {
return server!.callPluginMethod("set_gpu_power", {"value": value, "power_number": index});
}
export function getGPUPower(index: number): Promise<any> {
return server!.callPluginMethod("get_gpu_power", {"power_number": index});
}
export function setGPUPowerI(value: number, index: number): Promise<any> {
return server!.callPluginMethod("set_gpu_power_index", {"index": value, "power_number": index});
}
export function getGPUPowerI(index: number): Promise<any> {
return server!.callPluginMethod("get_gpu_power_index", {"power_number": index});
}
export function setFanTick(tick: number): Promise<any> {
return server!.callPluginMethod("set_fan_tick", {"tick": tick});
}
export function getFanTick(): Promise<any> {
return server!.callPluginMethod("get_fan_tick", {});
}
export function getFantastic(): Promise<any> {
return server!.callPluginMethod("fantastic_installed", {});
}
export function getChargeNow(): Promise<any> {
return server!.callPluginMethod("get_charge_now", {});
}
export function getChargeFull(): Promise<any> {
return server!.callPluginMethod("get_charge_full", {});
}
export function getChargeDesign(): Promise<any> {
return server!.callPluginMethod("get_charge_design", {});
}
export function setPersistent(value: boolean): Promise<any> {
return server!.callPluginMethod("set_persistent", {"enabled": value});
}
export function getPersistent(): Promise<any> {
return server!.callPluginMethod("get_persistent", {});
}
export function setPerGameProfile(value: boolean): Promise<any> {
return server!.callPluginMethod("set_per_game_profile", {"enabled": value});
}
export function getPerGameProfile(): Promise<any> {
return server!.callPluginMethod("get_per_game_profile", {});
}
export function getCurrentGame(): Promise<any> {
return server!.callPluginMethod("get_current_game", {});
}
export function onGameStart(gameId: number, data: any): Promise<any> {
const data2 = {appid: data.appid, display_name: data.display_name, gameid: gameId}; // Issue #17
return server!.callPluginMethod("on_game_start", {"game_id": gameId, "data":data2});
}
export function onGameStop(gameId: number | null): Promise<any> {
return server!.callPluginMethod("on_game_stop", {"game_id": gameId});
}

14
src/types.d.ts vendored Normal file
View file

@ -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;
}

View file

23
tsconfig.json Normal file
View file

@ -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"]
}