Files
MrBlakeandClaude Opus 5.5 cc4fdb9cd0 Add built-in updater, bump to 1.2.0
- predatord checks the Gitea releases every 6 hours, downloads the
  newest pacman package, verifies its SHA-256 from the release notes
  and installs it with `pacman -U` in a separate systemd unit
- Update notice and install button on the System page, in the sidebar
  and in the panel widget; `predatorctl update [--check] [--yes]`
- Optional API token for private repositories
- packaging/release.py builds and publishes releases in the format
  the updater expects

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 17:42:02 +02:00

396 lines
17 KiB
QML
Raw Permalink 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 duration(m) {
if (m === null || m === undefined) return ""
return m >= 60 ? Math.floor(m / 60) + " h " + (m % 60) + " min" : m + " min"
}
function batteryText() {
if (!ok || !st.battery.present) return ""
const b = st.battery
let t = b.capacity + " %"
if (b.charging) t += " · charging" + (b.power_w ? " at " + b.power_w.toFixed(1) + " W" : "")
else if (b.status === "Discharging") t += " · on battery" + (b.power_w ? ", " + b.power_w.toFixed(1) + " W" : "")
else t += b.ac_online ? " · plugged in" : ""
if (b.minutes_left !== null && b.minutes_left !== undefined)
t += " · " + duration(b.minutes_left) + (b.charging ? " until full" : " left")
return t
}
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"
+ "\nBattery " + batteryText()
+ (st.update && st.update.available ? "\nUpdate " + st.update.latest + " available" : "")
: "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.charging ? "battery-charging" : (root.ok && root.st.battery.ac_online ? "battery-full-charging" : "battery")
Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium
Layout.preferredHeight: Kirigami.Units.iconSizes.smallMedium
}
PC3.Label {
Layout.fillWidth: true
text: root.batteryText()
wrapMode: Text.Wrap
color: root.ok && root.st.battery.charging ? Kirigami.Theme.positiveTextColor : Kirigami.Theme.textColor
}
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"))
}
}
Kirigami.InlineMessage {
Layout.fillWidth: true
visible: root.ok && root.st.update !== null && root.st.update !== undefined && root.st.update.available
type: Kirigami.MessageType.Positive
text: root.ok && root.st.update ? (root.st.update_status === "running"
? "Installing update " + root.st.update.latest + "…"
: "Update " + root.st.update.latest + " available") : ""
actions: [
Kirigami.Action {
text: "Install"
icon.name: "update-none"
visible: root.ok && root.st.update && root.st.update.can_install && root.st.update_status !== "running"
onTriggered: root.run("update --yes")
},
Kirigami.Action {
text: "Details"
onTriggered: root.openPage("system")
}
]
}
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")
}
}
}
}