/* * 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") } } } }