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."