Files
Cachy-Predator-Keyboard/plasmoid/org.predatorcontrol.panel/contents/ui/main.qml
T
MrBlakeandClaude Opus 5.5 ea7ce49452 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>
2026-10-04 18:38:14 +02:00

356 lines
15 KiB
QML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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")
}
}
}
}