diff --git a/auto-cpufreq/BarWidget.qml b/auto-cpufreq/BarWidget.qml new file mode 100644 index 000000000..ce536435e --- /dev/null +++ b/auto-cpufreq/BarWidget.qml @@ -0,0 +1,311 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Widgets +import qs.Services.UI +import qs.Services.System + +Item { + id: root + + property var pluginApi: null + property ShellScreen screen: null + property string widgetId: "" + property string section: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + property var cfg: pluginApi?.pluginSettings || ({}) + property var defaults: pluginApi?.manifest?.metadata?.defaultSettings || ({}) + + // ── Per-screen bar properties ───────────────────────────────────────────── + readonly property string screenName: screen?.name ?? "" + readonly property string barPosition: Settings.getBarPositionForScreen(screenName) + readonly property bool isBarVertical: barPosition === "left" || barPosition === "right" + readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName) + readonly property real barFontSize: Style.getBarFontSizeForScreen(screenName) + readonly property string fixedFont: Settings.data?.ui?.fontFixed ?? "monospace" + readonly property bool compact: cfg.compactMode ?? defaults.compactMode ?? false + + // ── State ───────────────────────────────────────────────────────────────── + property string governor: "—" + property string turboState: "—" + property bool daemonRunning: false + property string forceOverride: "default" + property string turboOverride: "auto" // "never" | "always" | "auto" + property bool pkexecFailed: false + + // Battery state (sysfs — more reliable than UPower on this system) + property int batCapacity: -1 // -1 = no battery + property string batStatus: "" // Charging / Discharging / Full + property real batWatts: 0.0 + property string batDevice: "" // BAT0, BAT1, etc. + + readonly property color govColor: { + if (governor === "performance") return Color.mTertiary + if (governor === "powersave") return Color.mPrimary + return Color.mOnSurface + } + + // ── Widget dimensions ───────────────────────────────────────────────────── + readonly property real contentWidth: compact + ? capsuleHeight + : (content.implicitWidth + Style.marginM * 2) + readonly property real contentHeight: capsuleHeight + + implicitWidth: contentWidth + implicitHeight: contentHeight + + Component.onCompleted: { + if (pluginApi) pluginApi.mainInstance = root + } + + // ── Sysfs: governor ─────────────────────────────────────────────────────── + FileView { + id: governorFile + path: "/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor" + printErrors: false + onLoaded: root.governor = text().trim() || "—" + } + + FileView { + id: noTurboFile + path: "/sys/devices/system/cpu/intel_pstate/no_turbo" + printErrors: false + onLoaded: { + let v = text().trim() + if (v !== "") root.turboState = (v === "0") ? "on" : "off" + } + } + + FileView { + id: boostFile + path: "/sys/devices/system/cpu/cpufreq/boost" + printErrors: false + onLoaded: { + if (noTurboFile.text().trim() !== "") return + let v = text().trim() + if (v !== "") root.turboState = (v === "1") ? "on" : "off" + } + } + + // ── Battery — sysfs ────────────────────────────────────────────────────── + Process { + id: batFinder + command: ["sh", "-c", "for d in /sys/class/power_supply/*/type; do [ \"$(cat $d 2>/dev/null)\" = 'Battery' ] && basename $(dirname $d) && exit 0; done"] + running: false + stdout: StdioCollector { + onStreamFinished: { + let dev = text.trim() + if (dev !== "") { root.batDevice = dev; batUevent.reload() } + } + } + } + + FileView { + id: batUevent + path: root.batDevice !== "" ? "/sys/class/power_supply/" + root.batDevice + "/uevent" : "/dev/null" + printErrors: false + onLoaded: { + let cap = -1, status = "", currentUa = 0, voltageUv = 0 + for (let line of text().split("\n")) { + if (line.startsWith("POWER_SUPPLY_CAPACITY=")) cap = parseInt(line.split("=")[1]) || -1 + else if (line.startsWith("POWER_SUPPLY_STATUS=")) status = line.split("=")[1]?.trim() ?? "" + else if (line.startsWith("POWER_SUPPLY_CURRENT_NOW=")) currentUa = parseInt(line.split("=")[1]) || 0 + else if (line.startsWith("POWER_SUPPLY_VOLTAGE_NOW=")) voltageUv = parseInt(line.split("=")[1]) || 0 + } + root.batCapacity = cap + root.batStatus = status + root.batWatts = (voltageUv > 0 && currentUa > 0) + ? Math.round((voltageUv / 1e6) * (currentUa / 1e6) * 10) / 10 + : 0.0 + } + } + + // ── Override state — read pickle files ─────────────────────────────────── + // Pickle is binary but contains the string value — grep works fine + Process { + id: overrideReader + // Try common paths: NixOS uses /run/, classic installs use /opt/auto-cpufreq/ + command: ["sh", "-c", + "for f in /run/override.pickle /opt/auto-cpufreq/override.pickle; do " + + " [ -f \"$f\" ] && strings \"$f\" | grep -oE '(powersave|performance)' | head -1 && exit 0; " + + "done; echo 'default'"] + running: false + stdout: StdioCollector { + onStreamFinished: { + let v = text.trim() + if (v === "powersave" || v === "performance") root.forceOverride = v + else root.forceOverride = "default" + } + } + } + + Process { + id: turboOverrideReader + command: ["sh", "-c", + "for f in /run/turbo-override.pickle /opt/auto-cpufreq/turbo-override.pickle; do " + + " [ -f \"$f\" ] && strings \"$f\" | grep -oE '(never|always)' | head -1 && exit 0; " + + "done; echo 'auto'"] + running: false + stdout: StdioCollector { + onStreamFinished: { + let v = text.trim() + root.turboOverride = (v === "never" || v === "always") ? v : "auto" + } + } + } + + // ── Daemon check ────────────────────────────────────────────────────────── + Process { + id: daemonChecker + command: ["systemctl", "is-active", "--quiet", "auto-cpufreq"] + running: false + onExited: (code) => { root.daemonRunning = (code === 0) } + } + + // ── Actions ─────────────────────────────────────────────────────────────── + Process { + id: forceProc + running: false + onExited: (code) => { + if (code === 0) { root.pkexecFailed = false; root.refreshAll() } + else if (code === 126 || code === 127) root.pkexecFailed = true + } + } + + Process { + id: turboProc + running: false + onExited: (code) => { + if (code === 0) { root.pkexecFailed = false; root.refreshAll() } + else if (code === 126 || code === 127) root.pkexecFailed = true + } + } + + function setForce(mode) { + forceProc.command = ["pkexec", "auto-cpufreq", "--force=" + mode] + forceProc.running = false + forceProc.running = true + // don't set locally — refreshAll() will read real state from pickle + } + + function setTurbo(mode) { + turboProc.command = ["pkexec", "auto-cpufreq", "--turbo=" + mode] + turboProc.running = false + turboProc.running = true + } + + function refreshAll() { + governorFile.reload() + noTurboFile.reload() + boostFile.reload() + daemonChecker.running = false + daemonChecker.running = true + overrideReader.running = false + overrideReader.running = true + turboOverrideReader.running = false + turboOverrideReader.running = true + if (root.batDevice === "") { batFinder.running = false; batFinder.running = true } + else batUevent.reload() + } + + // ── Poll timer ──────────────────────────────────────────────────────────── + Timer { + interval: root.cfg.refreshInterval ?? root.defaults.refreshInterval ?? 3000 + running: true + repeat: true + triggeredOnStart: true + onTriggered: root.refreshAll() + } + + // ── Visual capsule (correct noctalia pattern) ───────────────────────────── + Rectangle { + id: visualCapsule + x: Style.pixelAlignCenter(parent.width, width) + y: Style.pixelAlignCenter(parent.height, height) + width: root.contentWidth + height: root.contentHeight + radius: Style.radiusL + color: mouseArea.containsMouse ? Color.mHover : Style.capsuleColor + border.color: Style.capsuleBorderColor + border.width: Style.capsuleBorderWidth + + RowLayout { + id: content + anchors.centerIn: parent + spacing: Style.marginS + + NIcon { + icon: "cpu" + color: mouseArea.containsMouse ? Color.mOnHover : root.govColor + pointSize: Style.fontSizeM + applyUiScale: false + } + + ColumnLayout { + visible: !root.compact + spacing: Style.marginXXXS + Layout.rightMargin: Style.marginS + + NText { + text: root.governor + pointSize: root.barFontSize + font.family: root.fixedFont + font.weight: Font.Bold + color: mouseArea.containsMouse ? Color.mOnHover : root.govColor + } + + NText { + visible: root.turboState !== "—" + text: pluginApi?.tr("widget.turbo-label", { state: root.turboState }) + pointSize: root.barFontSize * 0.85 + color: mouseArea.containsMouse ? Color.mOnHover : Color.mOnSurfaceVariant + } + } + } + } + + // ── Context menu ────────────────────────────────────────────────────────── + NPopupContextMenu { + id: contextMenu + model: [ + { "label": pluginApi?.tr("menu.force-performance"), "action": "force-performance", "icon": "gauge", "enabled": root.daemonRunning }, + { "label": pluginApi?.tr("menu.force-powersave"), "action": "force-powersave", "icon": "leaf", "enabled": root.daemonRunning }, + { "label": pluginApi?.tr("menu.force-reset"), "action": "force-reset", "icon": "refresh", "enabled": root.forceOverride !== "default" }, + { "label": pluginApi?.tr("menu.turbo-on"), "action": "turbo-on", "icon": "bolt", "enabled": root.daemonRunning }, + { "label": pluginApi?.tr("menu.turbo-off"), "action": "turbo-off", "icon": "bolt", "enabled": root.daemonRunning }, + { "label": pluginApi?.tr("menu.turbo-auto"), "action": "turbo-auto", "icon": "cpu", "enabled": root.daemonRunning }, + { "label": pluginApi?.tr("actions.widget-settings"), "action": "widget-settings", "icon": "settings" } + ] + onTriggered: action => { + contextMenu.close() + PanelService.closeContextMenu(screen) + if (action === "widget-settings") BarService.openPluginSettings(screen, pluginApi.manifest) + else if (action === "force-performance") root.setForce("performance") + else if (action === "force-powersave") root.setForce("powersave") + else if (action === "force-reset") root.setForce("reset") + else if (action === "turbo-on") root.setTurbo("always") + else if (action === "turbo-off") root.setTurbo("never") + else if (action === "turbo-auto") root.setTurbo("auto") + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton + onClicked: (mouse) => { + if (mouse.button === Qt.LeftButton) + pluginApi?.togglePanel(root.screen, root) + else + PanelService.showContextMenu(contextMenu, root, screen) + } + onEntered: TooltipService.show(root, root.governor + " · turbo: " + root.turboState, BarService.getTooltipDirection()) + onExited: TooltipService.hide() + } +} diff --git a/auto-cpufreq/Panel.qml b/auto-cpufreq/Panel.qml new file mode 100644 index 000000000..2ea637a5e --- /dev/null +++ b/auto-cpufreq/Panel.qml @@ -0,0 +1,467 @@ +import QtQuick +import QtQuick.Layouts +import qs.Commons +import qs.Services.UI +import qs.Services.System +import qs.Widgets +// BatteryService import kept for potential future use + +Item { + id: root + property var pluginApi: null + + readonly property var main: pluginApi?.mainInstance + readonly property var geometryPlaceholder: panelContainer + readonly property bool allowAttach: true + + property real contentPreferredWidth: 360 * Style.uiScaleRatio + property real contentPreferredHeight: mainCol.implicitHeight + Style.marginL * 4 + + anchors.fill: parent + + // Register with SystemStatService so it runs while panel is open + Component.onCompleted: SystemStatService.registerComponent("auto-cpufreq-panel") + Component.onDestruction: SystemStatService.unregisterComponent("auto-cpufreq-panel") + + Rectangle { + id: panelContainer + anchors.fill: parent + color: "transparent" + + ColumnLayout { + id: mainCol + anchors { fill: parent; margins: Style.marginL } + spacing: Style.marginM + + // ── Header ──────────────────────────────────────────────────────── + Rectangle { + Layout.fillWidth: true + implicitHeight: headerRow.implicitHeight + Style.marginM * 2 + color: Color.mSurfaceVariant + radius: Style.radiusM + + RowLayout { + id: headerRow + anchors { fill: parent; margins: Style.marginM } + spacing: Style.marginM + + NIcon { + icon: "cpu" + pointSize: Style.fontSizeXL + color: root.main?.daemonRunning ? Color.mPrimary : Color.mError + } + + ColumnLayout { + spacing: Style.marginXXS + Layout.fillWidth: true + NText { + text: pluginApi?.tr("panel.title") + pointSize: Style.fontSizeM + font.weight: Font.Bold + color: Color.mOnSurface + } + NText { + text: root.main?.daemonRunning + ? pluginApi?.tr("panel.daemon-running") + : pluginApi?.tr("panel.daemon-stopped") + pointSize: Style.fontSizeXS + color: root.main?.daemonRunning ? Color.mOnSurfaceVariant : Color.mError + } + } + + NIconButton { + icon: "refresh" + onClicked: root.main?.refreshAll() + } + } + } + + // ── CPU stats (SystemStatService) ───────────────────────────────── + Rectangle { + Layout.fillWidth: true + implicitHeight: statsRow.implicitHeight + Style.marginM * 2 + color: Color.mSurfaceVariant + radius: Style.radiusM + + RowLayout { + id: statsRow + anchors { fill: parent; margins: Style.marginM } + spacing: Style.marginL + + StatCell { + icon: "activity" + label: pluginApi?.tr("panel.cpu-usage") + value: Math.round(SystemStatService.cpuUsage) + "%" + valueColor: { + let u = SystemStatService.cpuUsage + if (u >= 90) return Color.mError + if (u >= 70) return Color.mTertiary + return Color.mOnSurface + } + } + + StatCell { + icon: "cpu" + label: pluginApi?.tr("panel.cpu-freq") + value: SystemStatService.cpuFreq || "—" + } + + StatCell { + icon: "thermometer" + label: pluginApi?.tr("panel.cpu-temp") + value: SystemStatService.cpuTemp > 0 + ? Math.round(SystemStatService.cpuTemp) + "°C" + : "—" + valueColor: { + let t = SystemStatService.cpuTemp + if (t >= 90) return Color.mError + if (t >= 75) return Color.mTertiary + return Color.mOnSurface + } + } + } + } + + // ── Battery (sysfs via BarWidget mainInstance) ──────────────────── + Rectangle { + Layout.fillWidth: true + implicitHeight: batRow.implicitHeight + Style.marginM * 2 + color: Color.mSurfaceVariant + radius: Style.radiusM + visible: (root.main?.batCapacity ?? -1) >= 0 + + RowLayout { + id: batRow + anchors { fill: parent; margins: Style.marginM } + spacing: Style.marginM + + NIcon { + icon: { + let s = root.main?.batStatus ?? "" + let c = root.main?.batCapacity ?? 0 + if (s === "Charging") return "battery-charging" + if (c >= 80) return "battery-4" + if (c >= 60) return "battery-3" + if (c >= 40) return "battery-2" + if (c >= 20) return "battery-1" + return "battery" + } + color: { + let s = root.main?.batStatus ?? "" + let c = root.main?.batCapacity ?? 100 + if (s === "Charging") return Color.mTertiary + if (c <= 20) return Color.mError + return Color.mOnSurface + } + pointSize: Style.fontSizeXL + } + + ColumnLayout { + spacing: Style.marginXXS + Layout.fillWidth: true + + NText { + text: (root.main?.batCapacity ?? 0) + "% · " + (root.main?.batStatus ?? "—") + pointSize: Style.fontSizeM + font.weight: Font.Bold + color: Color.mOnSurface + } + + NText { + visible: (root.main?.batWatts ?? 0) > 0 + text: { + let s = root.main?.batStatus ?? "" + let w = root.main?.batWatts ?? 0 + return (s === "Charging" ? "+" : "−") + w.toFixed(1) + " W" + } + pointSize: Style.fontSizeXS + color: Color.mOnSurfaceVariant + } + } + } + } + + // ── Governor & turbo ────────────────────────────────────────────── + Rectangle { + Layout.fillWidth: true + implicitHeight: govCol.implicitHeight + Style.marginM * 2 + color: Color.mSurfaceVariant + radius: Style.radiusM + + ColumnLayout { + id: govCol + anchors { fill: parent; margins: Style.marginM } + spacing: Style.marginS + + NText { + text: pluginApi?.tr("panel.section-governor") + pointSize: Style.fontSizeXS + color: Color.mOnSurfaceVariant + font.weight: Font.Medium + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + + NIcon { + icon: { + let g = root.main?.governor ?? "" + if (g === "performance") return "gauge" + if (g === "powersave") return "leaf" + return "cpu" + } + color: { + let g = root.main?.governor ?? "" + if (g === "performance") return Color.mTertiary + if (g === "powersave") return Color.mPrimary + return Color.mOnSurface + } + } + + NText { + text: root.main?.governor ?? "—" + pointSize: Style.fontSizeM + font.weight: Font.Bold + font.family: Settings.data?.ui?.fontFixed ?? "monospace" + color: Color.mOnSurface + Layout.fillWidth: true + } + + NText { + visible: (root.main?.forceOverride ?? "default") !== "default" + text: pluginApi?.tr("panel.forced") + pointSize: Style.fontSizeXS + color: Color.mTertiary + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + + NIcon { + icon: "bolt" + color: (root.main?.turboState ?? "") === "on" + ? Color.mTertiary : Color.mOnSurfaceVariant + } + + NText { + text: pluginApi?.tr("panel.turbo") + ": " + (root.main?.turboState ?? "—") + pointSize: Style.fontSizeS + color: Color.mOnSurface + Layout.fillWidth: true + } + } + } + } + + // ── pkexec error banner ─────────────────────────────────────────── + Rectangle { + Layout.fillWidth: true + implicitHeight: errorRow.implicitHeight + Style.marginM * 2 + color: Qt.rgba(Color.mError.r, Color.mError.g, Color.mError.b, 0.15) + radius: Style.radiusM + visible: root.main?.pkexecFailed ?? false + + RowLayout { + id: errorRow + anchors { fill: parent; margins: Style.marginM } + spacing: Style.marginM + NIcon { icon: "alert-triangle"; color: Color.mError; pointSize: Style.fontSizeM } + NText { + text: pluginApi?.tr("panel.pkexec-error") + pointSize: Style.fontSizeXS + color: Color.mError + Layout.fillWidth: true + wrapMode: Text.WordWrap + } + } + } + + // ── Force override ──────────────────────────────────────────────── + Rectangle { + Layout.fillWidth: true + implicitHeight: forceCol.implicitHeight + Style.marginM * 2 + color: Color.mSurfaceVariant + radius: Style.radiusM + opacity: (root.main?.daemonRunning ?? false) ? 1.0 : 0.4 + + ColumnLayout { + id: forceCol + anchors { fill: parent; margins: Style.marginM } + spacing: Style.marginS + + NText { + text: pluginApi?.tr("panel.section-force") + pointSize: Style.fontSizeXS + color: Color.mOnSurfaceVariant + font.weight: Font.Medium + } + + Rectangle { + Layout.fillWidth: true + implicitHeight: 44 * Style.uiScaleRatio + color: Color.mSurface + radius: Style.radiusS + border.color: Color.mOutline + border.width: Style.borderS + clip: true + + Row { + anchors.fill: parent + + SegItem { + width: parent.width / 3; height: parent.height + icon: "leaf"; label: pluginApi?.tr("panel.powersave") + active: (root.main?.forceOverride ?? "") === "powersave" + enabled: root.main?.daemonRunning ?? false + showDivider: true + onClicked: root.main?.setForce("powersave") + } + SegItem { + width: parent.width / 3; height: parent.height + icon: "scale"; label: pluginApi?.tr("panel.auto") + active: (root.main?.forceOverride ?? "default") === "default" + enabled: root.main?.daemonRunning ?? false + showDivider: true + onClicked: root.main?.setForce("reset") + } + SegItem { + width: parent.width / 3; height: parent.height + icon: "gauge"; label: pluginApi?.tr("panel.performance") + active: (root.main?.forceOverride ?? "") === "performance" + enabled: root.main?.daemonRunning ?? false + showDivider: false + onClicked: root.main?.setForce("performance") + } + } + } + } + } + + // ── Turbo boost ─────────────────────────────────────────────────── + Rectangle { + Layout.fillWidth: true + implicitHeight: turboCol.implicitHeight + Style.marginM * 2 + color: Color.mSurfaceVariant + radius: Style.radiusM + property bool turboAvailable: (root.main?.turboState ?? "n/a") !== "n/a" + opacity: ((root.main?.daemonRunning ?? false) && turboAvailable) ? 1.0 : 0.4 + + ColumnLayout { + id: turboCol + anchors { fill: parent; margins: Style.marginM } + spacing: Style.marginS + + NText { + text: pluginApi?.tr("panel.section-turbo") + pointSize: Style.fontSizeXS + color: Color.mOnSurfaceVariant + font.weight: Font.Medium + } + + Rectangle { + Layout.fillWidth: true + implicitHeight: 44 * Style.uiScaleRatio + color: Color.mSurface + radius: Style.radiusS + border.color: Color.mOutline + border.width: Style.borderS + clip: true + + Row { + anchors.fill: parent + property bool avail: (root.main?.daemonRunning ?? false) && (root.main?.turboState ?? "n/a") !== "n/a" + + SegItem { + width: parent.width / 3; height: parent.height + icon: "bolt-off"; label: pluginApi?.tr("panel.turbo-never") + active: (root.main?.turboOverride ?? "") === "never" + enabled: parent.avail; showDivider: true + onClicked: root.main?.setTurbo("never") + } + SegItem { + width: parent.width / 3; height: parent.height + icon: "cpu"; label: pluginApi?.tr("panel.turbo-auto") + active: (root.main?.turboOverride ?? "auto") === "auto" + enabled: parent.avail; showDivider: true + onClicked: root.main?.setTurbo("auto") + } + SegItem { + width: parent.width / 3; height: parent.height + icon: "bolt"; label: pluginApi?.tr("panel.turbo-always") + active: (root.main?.turboOverride ?? "") === "always" + enabled: parent.avail; showDivider: false + onClicked: root.main?.setTurbo("always") + } + } + } + } + } + } + } + + component StatCell: ColumnLayout { + id: statCell + property string icon: "" + property string label: "" + property string value: "—" + property color valueColor: Color.mOnSurface + spacing: Style.marginXXS + Layout.fillWidth: true + NIcon { + icon: statCell.icon; pointSize: Style.fontSizeM + color: Color.mOnSurfaceVariant; Layout.alignment: Qt.AlignHCenter + } + NText { + text: statCell.value; pointSize: Style.fontSizeM; font.weight: Font.Bold + color: statCell.valueColor; Layout.alignment: Qt.AlignHCenter + } + NText { + text: statCell.label; pointSize: Style.fontSizeXS + color: Color.mOnSurfaceVariant; Layout.alignment: Qt.AlignHCenter + } + } + + component SegItem: Item { + id: segItem + property string icon: "" + property string label: "" + property bool active: false + property bool enabled: true + property bool showDivider: false + signal clicked() + + Rectangle { + anchors { fill: parent; margins: Style.marginXXS } + radius: Style.radiusS + color: segItem.active ? Color.mPrimary : "transparent" + } + Rectangle { + anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter + width: Style.borderS; height: parent.height * 0.5 + color: Color.mOutline; opacity: 0.5 + visible: segItem.showDivider + } + ColumnLayout { + anchors.centerIn: parent + spacing: Style.marginXXXS + NIcon { + icon: segItem.icon; pointSize: Style.fontSizeS + color: segItem.active ? Color.mOnPrimary : Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + NText { + text: segItem.label; pointSize: Style.fontSizeXS + color: segItem.active ? Color.mOnPrimary : Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + } + MouseArea { + anchors.fill: parent; enabled: segItem.enabled + cursorShape: Qt.PointingHandCursor + onClicked: segItem.clicked() + } + } +} diff --git a/auto-cpufreq/README.md b/auto-cpufreq/README.md new file mode 100644 index 000000000..61464320b --- /dev/null +++ b/auto-cpufreq/README.md @@ -0,0 +1,49 @@ +# auto-cpufreq + +Monitor and control the [auto-cpufreq](https://github.com/AdnanHodzic/auto-cpufreq) daemon from your Noctalia bar. + +## Features + +- **Bar widget** — active CPU governor and turbo state, color-coded by profile +- **Panel** — CPU usage, frequency, temperature, battery status, governor info, force override and turbo boost controls +- **Right-click menu** — quick force/turbo override without opening the panel +- Works with Intel (`intel_pstate`) and AMD (`k10temp`, `amd_pstate`) CPUs +- Reads override state directly from auto-cpufreq pickle files — always in sync with the daemon + +## Requirements + +- [auto-cpufreq](https://github.com/AdnanHodzic/auto-cpufreq) installed with daemon running +- polkit setup for force override / turbo boost controls (see below) + +## Polkit Setup + +Force override and turbo boost buttons use `pkexec`. Run the setup script once: + +```bash +cd ~/.config/noctalia/plugins/auto-cpufreq/ +chmod +x setup_polkit.sh +sudo ./setup_polkit.sh +``` + +### NixOS + +```nix +environment.etc."polkit-1/actions/org.auto-cpufreq.pkexec.policy".text = '' + + + + + Run auto-cpufreq + Authentication is required to run auto-cpufreq + + auth_admin + auth_admin + auth_admin_keep + + ${pkgs.auto-cpufreq}/bin/auto-cpufreq + + +''; +``` diff --git a/auto-cpufreq/Settings.qml b/auto-cpufreq/Settings.qml new file mode 100644 index 000000000..491f4262e --- /dev/null +++ b/auto-cpufreq/Settings.qml @@ -0,0 +1,96 @@ +import QtQuick +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + property var pluginApi: null + + property var cfg: pluginApi?.pluginSettings || ({}) + property var defaults: pluginApi?.manifest?.metadata?.defaultSettings || ({}) + + // Edit copies + property int editRefreshInterval: cfg.refreshInterval ?? defaults.refreshInterval ?? 3000 + property bool editShowGovernor: cfg.showGovernor ?? defaults.showGovernor ?? true + property bool editShowTurbo: cfg.showTurbo ?? defaults.showTurbo ?? true + property bool editCompactMode: cfg.compactMode ?? defaults.compactMode ?? false + + spacing: Style.marginL + + NToggle { + Layout.fillWidth: true + label: pluginApi?.tr("settings.compact.label") + description: pluginApi?.tr("settings.compact.desc") + checked: root.editCompactMode + onToggled: root.editCompactMode = checked + } + + NToggle { + Layout.fillWidth: true + label: pluginApi?.tr("settings.show-governor.label") + description: pluginApi?.tr("settings.show-governor.desc") + checked: root.editShowGovernor + onToggled: root.editShowGovernor = checked + } + + NToggle { + Layout.fillWidth: true + label: pluginApi?.tr("settings.show-turbo.label") + description: pluginApi?.tr("settings.show-turbo.desc") + checked: root.editShowTurbo + onToggled: root.editShowTurbo = checked + } + + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginS + + NText { + text: pluginApi?.tr("settings.refresh.label") + pointSize: Style.fontSizeS + font.weight: Font.Medium + color: Color.mOnSurface + } + + NText { + text: pluginApi?.tr("settings.refresh.desc") + pointSize: Style.fontSizeXS + color: Color.mOnSurfaceVariant + Layout.fillWidth: true + wrapMode: Text.WordWrap + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + + NSlider { + Layout.fillWidth: true + from: 1000 + to: 10000 + stepSize: 500 + value: root.editRefreshInterval + onMoved: root.editRefreshInterval = value + } + + NText { + text: (root.editRefreshInterval / 1000).toFixed(1) + "s" + pointSize: Style.fontSizeS + font.family: Settings.data?.ui?.fontFixed ?? "monospace" + color: Color.mOnSurface + Layout.preferredWidth: 36 * Style.uiScaleRatio + horizontalAlignment: Text.AlignRight + } + } + } + + function saveSettings() { + if (!pluginApi) return + pluginApi.pluginSettings.refreshInterval = root.editRefreshInterval + pluginApi.pluginSettings.showGovernor = root.editShowGovernor + pluginApi.pluginSettings.showTurbo = root.editShowTurbo + pluginApi.pluginSettings.compactMode = root.editCompactMode + pluginApi.saveSettings() + } +} diff --git a/auto-cpufreq/i18n/en.json b/auto-cpufreq/i18n/en.json new file mode 100644 index 000000000..2a0f2ab13 --- /dev/null +++ b/auto-cpufreq/i18n/en.json @@ -0,0 +1,57 @@ +{ + "panel": { + "title": "auto-cpufreq", + "daemon-running": "daemon running", + "daemon-stopped": "daemon not running", + "cpu-usage": "usage", + "cpu-freq": "frequency", + "cpu-temp": "temp", + "section-governor": "GOVERNOR", + "section-force": "FORCE OVERRIDE", + "section-turbo": "TURBO BOOST", + "forced": "forced", + "turbo": "turbo", + "pkexec-error": "pkexec not authorized. See README for polkit setup.", + "bat-charging": "Charging", + "bat-discharging": "Discharging", + "bat-plugged": "Plugged in", + "powersave": "powersave", + "auto": "auto", + "performance": "performance", + "turbo-never": "never", + "turbo-auto": "auto", + "turbo-always": "always" + }, + "menu": { + "force-performance": "Force performance", + "force-powersave": "Force powersave", + "force-reset": "Reset override", + "turbo-on": "Turbo: always on", + "turbo-off": "Turbo: always off", + "turbo-auto": "Turbo: auto" + }, + "widget": { + "turbo-label": "turbo: {state}" + }, + "actions": { + "widget-settings": "Widget settings" + }, + "settings": { + "compact": { + "label": "Compact mode", + "desc": "Show only the CPU icon, hide governor and turbo text" + }, + "show-governor": { + "label": "Show governor", + "desc": "Display the active CPU frequency scaling governor" + }, + "show-turbo": { + "label": "Show turbo state", + "desc": "Display whether turbo boost is on or off" + }, + "refresh": { + "label": "Refresh interval", + "desc": "How often to poll sysfs for governor, turbo, and CPU stats" + } + } +} diff --git a/auto-cpufreq/manifest.json b/auto-cpufreq/manifest.json new file mode 100644 index 000000000..b2813afc6 --- /dev/null +++ b/auto-cpufreq/manifest.json @@ -0,0 +1,32 @@ +{ + "id": "auto-cpufreq", + "name": "auto-cpufreq", + "version": "2.6.0", + "minNoctaliaVersion": "4.4.1", + "author": "4rmcyt", + "license": "MIT", + "repository": "https://github.com/noctalia-dev/noctalia-plugins", + "description": "Monitor and control auto-cpufreq daemon: governor, turbo boost, CPU stats, and force-override.", + "tags": [ + "Bar", + "Panel", + "System", + "Utility" + ], + "entryPoints": { + "barWidget": "BarWidget.qml", + "panel": "Panel.qml", + "settings": "Settings.qml" + }, + "dependencies": { + "plugins": [] + }, + "metadata": { + "defaultSettings": { + "refreshInterval": 3000, + "showGovernor": true, + "showTurbo": true, + "compactMode": false + } + } +} diff --git a/auto-cpufreq/preview.png b/auto-cpufreq/preview.png new file mode 100644 index 000000000..c7ec6d301 Binary files /dev/null and b/auto-cpufreq/preview.png differ diff --git a/auto-cpufreq/setup_polkit.sh b/auto-cpufreq/setup_polkit.sh new file mode 100644 index 000000000..cccb35989 --- /dev/null +++ b/auto-cpufreq/setup_polkit.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# Setup polkit rules for auto-cpufreq plugin +# Run once with: sudo ./setup_polkit.sh +set -e + +if [ "$EUID" -ne 0 ]; then + echo "Error: run as root: sudo ./setup_polkit.sh" + exit 1 +fi + +AUTO_CPUFREQ_BIN="$(which auto-cpufreq 2>/dev/null || true)" + +if [ -z "$AUTO_CPUFREQ_BIN" ]; then + echo "Error: auto-cpufreq not found in PATH" + exit 1 +fi + +# Resolve real path (handles symlinks like /run/current-system/sw/bin on NixOS) +AUTO_CPUFREQ_REAL="$(readlink -f "$AUTO_CPUFREQ_BIN")" +echo "Found auto-cpufreq at: $AUTO_CPUFREQ_REAL" + +# Detect wheel or sudo group +if getent group wheel > /dev/null 2>&1; then + GROUP="wheel" +elif getent group sudo > /dev/null 2>&1; then + GROUP="sudo" +else + echo "Error: neither 'wheel' nor 'sudo' group found" + exit 1 +fi +echo "Using group: $GROUP" + +# Write polkit policy +POLICY_DIR="/usr/share/polkit-1/actions" +mkdir -p "$POLICY_DIR" +cat > "$POLICY_DIR/org.auto-cpufreq.pkexec.policy" << EOF + + + + + Run auto-cpufreq + Authentication is required to run auto-cpufreq + + auth_admin + auth_admin + auth_admin + + $AUTO_CPUFREQ_REAL + + +EOF +echo "Written: $POLICY_DIR/org.auto-cpufreq.pkexec.policy" + +# Write polkit rules (passwordless for wheel/sudo group) +RULES_DIR="/etc/polkit-1/rules.d" +mkdir -p "$RULES_DIR" +cat > "$RULES_DIR/49-auto-cpufreq.rules" << EOF +polkit.addRule(function(action, subject) { + if (action.id === "org.auto-cpufreq.pkexec" && + subject.isInGroup("$GROUP")) { + return polkit.Result.YES; + } +}); +EOF +echo "Written: $RULES_DIR/49-auto-cpufreq.rules" + +# Reload polkit +if systemctl reload polkit 2>/dev/null; then + echo "polkit reloaded" +else + echo "Note: restart your session for polkit changes to take effect" +fi + +echo "Done! Force override and turbo boost controls are now enabled."