Files
Cachy-Predator-Keyboard/plasmoid/org.predatorcontrol.panel/contents/ui/main.qml
T
MrBlakeandClaude Opus 5.5 2ef62e85cd Add charging indicator and pacman package, bump to 1.1.0
- Show charge/discharge power in watts, a battery graphic with charge
  bolt, a power history graph and the estimated time until full or
  remaining (respecting the 80 % charge limit) in the app, the panel
  widget, its tooltip and `predatorctl status`
- Add packaging/arch/PKGBUILD that builds a pacman package; the kernel
  module ships as DKMS source and is rebuilt for every kernel
- Bump version to 1.1.0

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

374 lines
16 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 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()
: "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"))
}
}
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")
}
}
}
}