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>
356 lines
15 KiB
QML
356 lines
15 KiB
QML
/*
|
||
* 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")
|
||
}
|
||
}
|
||
}
|
||
}
|