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:
@@ -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"
|
||||
}
|
||||
Reference in New Issue
Block a user