Initial release of Predator Control 1.0.0

PredatorSense replacement for the Acer Predator Helios 16 (PH16-71) on Linux:

- predatord: root service for profiles, fans (incl. temperature curves),
  keyboard RGB, lightbar and battery health; restores settings at boot
  and after resume, switches profiles on AC/battery changes
- Qt desktop app with dashboard, fan curve editor and per-key RGB studio
- Plasma 6 panel widget with quick controls
- predatorctl command line tool
- predator_wmi kernel module (DKMS) as a minimal WMI method bridge

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-04 18:38:14 +02:00
co-authored by Claude Opus 5.5
commit ea7ce49452
34 changed files with 3774 additions and 0 deletions
@@ -0,0 +1,355 @@
/*
* Predator Control – Plasma panel widget.
*
* Reads the state snapshot written by predatord (/run/predator-control/state.json)
* and sends commands through the `predatorctl` CLI.
*
* SPDX-License-Identifier: GPL-2.0-only
*/
import QtQuick
import QtQuick.Layouts
import org.kde.plasma.plasmoid
import org.kde.plasma.core as PlasmaCore
import org.kde.plasma.components as PC3
import org.kde.plasma.extras as PlasmaExtras
import org.kde.plasma.plasma5support as P5Support
import org.kde.kirigami as Kirigami
PlasmoidItem {
id: root
property var st: null
property string lastError: ""
readonly property bool ok: st !== null
readonly property string stateCmd: "cat /run/predator-control/state.json"
readonly property var profiles: [
{ key: "low-power", label: "Eco" },
{ key: "quiet", label: "Quiet" },
{ key: "balanced", label: "Balanced" },
{ key: "balanced-performance", label: "Perf." },
{ key: "performance", label: "Turbo" }
]
readonly property var swatches: ["#00d4ff", "#0057ff", "#7a00ff", "#ff00c8",
"#ff0033", "#ff6a00", "#ffd000", "#3cff00", "#ffffff"]
function profileLabel(key) {
const names = { "low-power": "Eco", "quiet": "Quiet", "balanced": "Balanced",
"balanced-performance": "Performance", "performance": "Turbo" }
return names[key] || key || "–"
}
function temp(v) { return v ? Math.round(v) + " °C" : "off" }
function run(args) {
// unique suffix so the executable engine runs identical commands again
runner.connectSource("predatorctl " + args + " #" + Date.now())
}
function openPage(page) {
run("open " + page)
root.expanded = false
}
Plasmoid.icon: "predator-control"
Plasmoid.status: PlasmaCore.Types.ActiveStatus
toolTipMainText: "Predator Control"
toolTipSubText: ok
? profileLabel(st.profile) + " · CPU " + temp(st.sensors.cpu_temp) + " · GPU " + temp(st.sensors.gpu_temp)
+ "\nFans " + (st.sensors.cpu_fan || 0) + " / " + (st.sensors.gpu_fan || 0) + " RPM"
: "Service predatord not running"
P5Support.DataSource {
id: reader
engine: "executable"
connectedSources: [root.stateCmd]
interval: root.expanded ? 1500 : 5000
onNewData: (source, data) => {
if (data["exit code"] !== 0) { root.st = null; return }
try { root.st = JSON.parse(data["stdout"]) } catch (e) { }
}
}
P5Support.DataSource {
id: runner
engine: "executable"
onNewData: (source, data) => {
disconnectSource(source)
root.lastError = data["exit code"] !== 0 ? (data["stderr"] || "").trim() : ""
refresh.restart()
}
}
Timer {
id: refresh
interval: 300
onTriggered: {
reader.disconnectSource(root.stateCmd)
reader.connectSource(root.stateCmd)
}
}
compactRepresentation: MouseArea {
Layout.minimumWidth: Kirigami.Units.iconSizes.small
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.MiddleButton
onClicked: mouse => {
if (mouse.button === Qt.MiddleButton) root.run("profile-cycle")
else root.expanded = !root.expanded
}
Kirigami.Icon {
anchors.fill: parent
source: "predator-control"
active: parent.containsMouse
}
}
fullRepresentation: PlasmaExtras.Representation {
id: full
Layout.preferredWidth: Kirigami.Units.gridUnit * 24
Layout.preferredHeight: content.implicitHeight + header.implicitHeight + Kirigami.Units.largeSpacing * 2
Layout.minimumWidth: Kirigami.Units.gridUnit * 20
header: PlasmaExtras.PlasmoidHeading {
RowLayout {
anchors.fill: parent
Kirigami.Icon {
source: "predator-control"
Layout.preferredWidth: Kirigami.Units.iconSizes.medium
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
}
ColumnLayout {
spacing: 0
Layout.fillWidth: true
Kirigami.Heading { level: 3; text: "Predator Control" }
PC3.Label {
opacity: 0.7
text: root.ok ? root.profileLabel(root.st.profile) + " mode" : "Service not running"
}
}
PC3.ToolButton {
icon.name: "configure"
text: "Open"
display: PC3.AbstractButton.IconOnly
PC3.ToolTip.text: "Open Predator Control"
PC3.ToolTip.visible: hovered
onClicked: root.openPage("home")
}
}
}
PlasmaExtras.PlaceholderMessage {
anchors.centerIn: parent
width: parent.width - Kirigami.Units.gridUnit * 2
visible: !root.ok
iconName: "dialog-warning"
text: "predatord is not running"
explanation: "Start it with: sudo systemctl enable --now predatord"
}
ColumnLayout {
id: content
visible: root.ok
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Kirigami.Units.smallSpacing
spacing: Kirigami.Units.smallSpacing
// ---------------------------------------------------------- sensors
GridLayout {
columns: 4
Layout.fillWidth: true
columnSpacing: Kirigami.Units.smallSpacing
Repeater {
model: root.ok ? [
{ t: "CPU", v: root.temp(root.st.sensors.cpu_temp) },
{ t: "GPU", v: root.temp(root.st.sensors.gpu_temp) },
{ t: "CPU fan", v: (root.st.sensors.cpu_fan || 0) + " rpm" },
{ t: "GPU fan", v: (root.st.sensors.gpu_fan || 0) + " rpm" }
] : []
delegate: ColumnLayout {
Layout.fillWidth: true
spacing: 0
PC3.Label {
Layout.alignment: Qt.AlignHCenter
text: modelData.v
font.bold: true
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 1.25
}
PC3.Label {
Layout.alignment: Qt.AlignHCenter
text: modelData.t
opacity: 0.7
font: Kirigami.Theme.smallFont
}
}
}
}
// ---------------------------------------------------------- performance
Kirigami.Heading { level: 5; text: "Performance mode"; opacity: 0.8; Layout.topMargin: Kirigami.Units.smallSpacing }
RowLayout {
Layout.fillWidth: true
spacing: 2
Repeater {
model: root.profiles
delegate: PC3.Button {
Layout.fillWidth: true
text: modelData.label
checkable: true
checked: root.ok && root.st.profile === modelData.key
enabled: root.ok && root.st.profile_choices.indexOf(modelData.key) >= 0
onClicked: root.run("profile " + modelData.key)
}
}
}
// ---------------------------------------------------------- fans
Kirigami.Heading { level: 5; text: "Fans"; opacity: 0.8; Layout.topMargin: Kirigami.Units.smallSpacing }
RowLayout {
Layout.fillWidth: true
spacing: 2
Repeater {
model: [{ k: "auto", l: "Auto" }, { k: "max", l: "Max" }, { k: "curve", l: "Curve" }, { k: "custom", l: "Manual" }]
delegate: PC3.Button {
Layout.fillWidth: true
text: modelData.l
checkable: true
checked: root.ok && root.st.config.fans.mode === modelData.k
onClicked: {
root.run("fans " + modelData.k)
if (modelData.k === "custom" || modelData.k === "curve") root.openPage("fans")
}
}
}
}
// ---------------------------------------------------------- keyboard
RowLayout {
Layout.fillWidth: true
Layout.topMargin: Kirigami.Units.smallSpacing
Kirigami.Heading { level: 5; text: "Keyboard lighting"; opacity: 0.8; Layout.fillWidth: true }
PC3.Switch {
checked: root.ok && root.st.config.keyboard.effect !== "off"
onToggled: root.run("kbd " + (checked ? "on" : "off"))
}
}
RowLayout {
Layout.fillWidth: true
PC3.ComboBox {
id: effectBox
Layout.fillWidth: true
textRole: "label"
valueRole: "key"
model: {
const out = []
if (!root.ok) return out
const fx = root.st.keyboard_effects
for (const k in fx) if (k !== "off") out.push({ key: k, label: fx[k][0] })
return out
}
currentIndex: root.ok ? Math.max(0, indexOfValue(root.st.config.keyboard.effect)) : 0
onActivated: root.run("kbd " + currentValue)
}
PC3.Button {
icon.name: "color-management"
text: "RGB Studio…"
onClicked: root.openPage("rgb")
}
}
RowLayout {
Layout.fillWidth: true
spacing: Kirigami.Units.smallSpacing
Repeater {
model: root.swatches
delegate: Rectangle {
Layout.preferredWidth: Kirigami.Units.gridUnit * 1.3
Layout.preferredHeight: Kirigami.Units.gridUnit * 1.3
radius: width / 2
color: modelData
border.width: root.ok && root.st.config.keyboard.color.toLowerCase() === modelData ? 3 : 1
border.color: Kirigami.Theme.textColor
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
let fx = root.st.config.keyboard.effect
const usesColor = root.st.keyboard_effects[fx] && root.st.keyboard_effects[fx][1]
if (!usesColor) fx = "static"
root.run("kbd " + fx + " " + modelData.substring(1))
}
}
}
}
Item { Layout.fillWidth: true }
}
RowLayout {
Layout.fillWidth: true
Kirigami.Icon { source: "brightness-low"; Layout.preferredWidth: Kirigami.Units.iconSizes.small; Layout.preferredHeight: Kirigami.Units.iconSizes.small }
PC3.Slider {
id: kbdBright
Layout.fillWidth: true
from: 0; to: 255; stepSize: 5
value: root.ok ? root.st.config.keyboard.brightness : 0
onPressedChanged: if (!pressed) root.run("kbd-brightness " + Math.round(value))
}
Kirigami.Icon { source: "brightness-high"; Layout.preferredWidth: Kirigami.Units.iconSizes.small; Layout.preferredHeight: Kirigami.Units.iconSizes.small }
}
// ---------------------------------------------------------- lightbar
RowLayout {
Layout.fillWidth: true
Layout.topMargin: Kirigami.Units.smallSpacing
Kirigami.Heading { level: 5; text: "Lightbar"; opacity: 0.8; Layout.fillWidth: true }
PC3.CheckBox {
text: "Follow keyboard"
checked: root.ok && root.st.config.lightbar.sync_keyboard
onToggled: root.run("lightbar --sync " + (checked ? "on" : "off"))
}
PC3.Switch {
checked: root.ok && root.st.lightbar_effective.mode !== "off"
enabled: root.ok && !root.st.config.lightbar.sync_keyboard
onToggled: root.run("lightbar " + (checked ? "static" : "off"))
}
}
// ---------------------------------------------------------- battery
RowLayout {
Layout.fillWidth: true
Layout.topMargin: Kirigami.Units.smallSpacing
Kirigami.Icon {
source: root.ok && root.st.battery.ac_online ? "battery-charging" : "battery"
Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium
Layout.preferredHeight: Kirigami.Units.iconSizes.smallMedium
}
PC3.Label {
Layout.fillWidth: true
text: root.ok ? root.st.battery.capacity + " % · " + (root.st.battery.ac_online ? "plugged in" : "on battery") : ""
}
PC3.CheckBox {
text: "Limit to 80 %"
enabled: root.ok && root.st.battery_limit !== null
checked: root.ok && root.st.battery_limit === true
onToggled: root.run("battery-limit " + (checked ? "on" : "off"))
}
}
PC3.Label {
Layout.fillWidth: true
visible: root.lastError !== ""
text: root.lastError
color: Kirigami.Theme.negativeTextColor
wrapMode: Text.Wrap
font: Kirigami.Theme.smallFont
}
PC3.Button {
Layout.fillWidth: true
Layout.topMargin: Kirigami.Units.smallSpacing
icon.name: "preferences-system"
text: "Open Predator Control"
onClicked: root.openPage("home")
}
}
}
}
@@ -0,0 +1,21 @@
{
"KPackageStructure": "Plasma/Applet",
"KPlugin": {
"Authors": [
{
"Name": "Predator Control contributors"
}
],
"Category": "System Information",
"Description": "Quick controls for Acer Predator laptops: performance mode, fans, keyboard RGB, lightbar and battery",
"Icon": "predator-control",
"Id": "org.predatorcontrol.panel",
"License": "GPL-2.0-only",
"Name": "Predator Control",
"Version": "1.0.0",
"Website": "https://git.mrblake.cc/MrBlake/Cachy-Predator-Keyboard"
},
"X-Plasma-API-Minimum-Version": "6.0",
"X-Plasma-NotificationArea": "true",
"X-Plasma-NotificationAreaCategory": "Hardware"
}