From 2943655575e27e0306affd7400ec71135245b659 Mon Sep 17 00:00:00 2001 From: Morgan Date: Mon, 2 Mar 2026 02:24:32 +0900 Subject: [PATCH] Add CPU util --- .../cpuUsageUtil.js | 134 ++++++++++++++++++ .../extension.js | 55 ++++++- .../prefs.js | 21 +++ ...gnome.shell.extensions.sensors.gschema.xml | 18 +++ 4 files changed, 226 insertions(+), 2 deletions(-) create mode 100644 freon@UshakovVasilii_Github.yahoo.com/cpuUsageUtil.js diff --git a/freon@UshakovVasilii_Github.yahoo.com/cpuUsageUtil.js b/freon@UshakovVasilii_Github.yahoo.com/cpuUsageUtil.js new file mode 100644 index 0000000..69ef0ec --- /dev/null +++ b/freon@UshakovVasilii_Github.yahoo.com/cpuUsageUtil.js @@ -0,0 +1,134 @@ +import GLib from 'gi://GLib'; + +export default class CpuUsageUtil { + + constructor(intervalSeconds = 2) { + this._updated = false; + this._readings = []; + this._prevSnapshot = null; + this._intervalUsec = 0; + this._lastUpdateUsec = 0; + this.interval = intervalSeconds; + + // Take initial snapshot so the first real execute() can compute a delta + this._prevSnapshot = this._readProcStat(); + } + + get available() { + return this._prevSnapshot !== null; + } + + get updated() { + return this._updated; + } + + set updated(updated) { + this._updated = updated; + } + + execute(callback) { + const nowUsec = GLib.get_monotonic_time(); + const remaining = this._intervalUsec - (nowUsec - this._lastUpdateUsec); + if (this._intervalUsec > 0 && this._lastUpdateUsec !== 0 && remaining > 0) { + if (callback) + callback(); + return; + } + + this._updated = false; + + try { + const snapshot = this._readProcStat(); + if (!snapshot || !this._prevSnapshot) { + this._prevSnapshot = snapshot; + return; + } + + this._readings = []; + + for (const [cpu, fields] of snapshot) { + const prev = this._prevSnapshot.get(cpu); + if (!prev) + continue; + + const idle = (fields.idle + fields.iowait) - (prev.idle + prev.iowait); + const total = fields.total - prev.total; + + if (total <= 0) + continue; + + const usage = 100.0 * (1.0 - idle / total); + + this._readings.push({ + label: cpu === 'cpu' ? 'CPU Total' : cpu.toUpperCase().replace('CPU', 'CPU '), + usage: Math.max(0, Math.min(100, usage)), + }); + } + + this._prevSnapshot = snapshot; + } catch (e) { + logError(e, '[FREON] Failed to read CPU usage'); + this._readings = []; + } finally { + this._lastUpdateUsec = nowUsec; + this._updated = true; + if (callback) + callback(); + } + } + + get usage() { + return this._readings; + } + + destroy() { + this._readings = []; + this._prevSnapshot = null; + this._lastUpdateUsec = 0; + } + + set interval(seconds) { + const clamped = Math.max(1, seconds | 0); + this._intervalUsec = clamped * 1000000; + this._lastUpdateUsec = 0; + } + + _readProcStat() { + let ok, contents; + try { + [ok, contents] = GLib.file_get_contents('/proc/stat'); + } catch (e) { + return null; + } + + if (!ok) + return null; + + const text = new TextDecoder().decode(contents); + const result = new Map(); + + for (const line of text.split('\n')) { + if (!line.startsWith('cpu')) + continue; + + const parts = line.trim().split(/\s+/); + const name = parts[0]; + + // user nice system idle iowait irq softirq steal + const user = parseInt(parts[1]) || 0; + const nice = parseInt(parts[2]) || 0; + const system = parseInt(parts[3]) || 0; + const idle = parseInt(parts[4]) || 0; + const iowait = parseInt(parts[5]) || 0; + const irq = parseInt(parts[6]) || 0; + const softirq = parseInt(parts[7]) || 0; + const steal = parseInt(parts[8]) || 0; + + const total = user + nice + system + idle + iowait + irq + softirq + steal; + + result.set(name, { idle, iowait, total }); + } + + return result.size > 0 ? result : null; + } +} diff --git a/freon@UshakovVasilii_Github.yahoo.com/extension.js b/freon@UshakovVasilii_Github.yahoo.com/extension.js index 31bbbf1..eced464 100644 --- a/freon@UshakovVasilii_Github.yahoo.com/extension.js +++ b/freon@UshakovVasilii_Github.yahoo.com/extension.js @@ -25,6 +25,7 @@ import SmartctlUtil from './smartctlUtil.js'; import NvmecliUtil from './nvmecliUtil.js'; import BatteryUtil from './batteryUtil.js'; import WattdUtil from './wattdUtil.js'; +import CpuUsageUtil from './cpuUsageUtil.js'; import FreonItem from './freonItem.js'; @@ -80,6 +81,7 @@ class FreonMenuButton extends PanelMenu.Button { this._initLiquidctlUtility(); this._initBatteryUtility(); this._initWattdUtility(); + this._initCpuUsageUtility(); this._initNvidiaUtility(); this._initBumblebeeNvidiaUtility(); @@ -104,6 +106,7 @@ class FreonMenuButton extends PanelMenu.Button { 'fan' : Gio.icon_new_for_string(path + '/icons/freon-fan-symbolic.svg'), 'power' : voltageIcon, 'battery' : batteryIcon, + 'cpu-usage' : Gio.icon_new_for_string('utilities-system-monitor-symbolic'), } this._menuLayout = new St.BoxLayout(); @@ -145,6 +148,8 @@ class FreonMenuButton extends PanelMenu.Button { this._addSettingChangedSignal('use-generic-lmsensors', this._sensorsUtilityChanged.bind(this)); this._addSettingChangedSignal('freeimpi-selected', this._freeipmiUtilityChanged.bind(this)); this._addSettingChangedSignal('use-generic-liquidctl', this._liquidctlUtilityChanged.bind(this)); + this._addSettingChangedSignal('use-cpu-usage', this._cpuUsageUtilityChanged.bind(this)); + this._addSettingChangedSignal('cpu-usage-update-time', this._cpuUsageUpdateTimeChanged.bind(this)); this._addSettingChangedSignal('use-generic-wattd', this._wattdUtilityChanged.bind(this)); this._addSettingChangedSignal('show-battery-stats', this._batteryUtilityChanged.bind(this)); @@ -161,7 +166,8 @@ class FreonMenuButton extends PanelMenu.Button { this._addSettingChangedSignal('show-rotationrate', this._rerender.bind(this)); this._addSettingChangedSignal('show-voltage', this._rerender.bind(this)); this._addSettingChangedSignal('show-power', this._rerender.bind(this)); - + this._addSettingChangedSignal('show-cpu-usage', this._rerender.bind(this)); + this._addSettingChangedSignal('group-temperature', this._rerender.bind(this)) this._addSettingChangedSignal('group-rotationrate', this._rerender.bind(this)) @@ -392,6 +398,31 @@ class FreonMenuButton extends PanelMenu.Button { this._querySensors(); } + _initCpuUsageUtility() { + if (this._settings.get_boolean('use-cpu-usage')) + this._utils.cpuUsage = new CpuUsageUtil(this._settings.get_int('cpu-usage-update-time')); + } + + _destroyCpuUsageUtility() { + if (this._utils.cpuUsage) { + this._utils.cpuUsage.destroy(); + delete this._utils.cpuUsage; + } + } + + _cpuUsageUtilityChanged() { + this._destroyCpuUsageUtility(); + this._initCpuUsageUtility(); + this._querySensors(); + this._updateUI(true); + } + + _cpuUsageUpdateTimeChanged() { + if (this._utils.cpuUsage) + this._utils.cpuUsage.interval = this._settings.get_int('cpu-usage-update-time'); + this._querySensors(); + } + _initNvidiaUtility() { if (this._settings.get_boolean('use-gpu-nvidia')) this._utils.nvidia = new NvidiaUtil(); @@ -560,6 +591,7 @@ class FreonMenuButton extends PanelMenu.Button { this._destroyBatteryUtility(); this._destroyWattdUtility(); + this._destroyCpuUsageUtility(); GLib.Source.remove(this._timeoutId); GLib.Source.remove(this._updateUITimeoutId); @@ -628,6 +660,7 @@ class FreonMenuButton extends PanelMenu.Button { let fanInfo = []; let voltageInfo = []; let powerInfo = []; + let cpuUsageInfo = []; if (this._utils.sensors && this._utils.sensors.available) { if (this._settings.get_boolean('show-temperature')) { @@ -674,6 +707,10 @@ class FreonMenuButton extends PanelMenu.Button { if (this._settings.get_boolean('show-power')) powerInfo = powerInfo.concat(this._utils.wattd.power); + if (this._utils.cpuUsage && this._utils.cpuUsage.available) + if (this._settings.get_boolean('show-cpu-usage')) + cpuUsageInfo = cpuUsageInfo.concat(this._utils.cpuUsage.usage); + if (this._utils.nvidia && this._utils.nvidia.available) if (this._settings.get_boolean('show-temperature')) gpuTempInfo = gpuTempInfo.concat(this._utils.nvidia.temp); @@ -708,12 +745,14 @@ class FreonMenuButton extends PanelMenu.Button { fanInfo.sort(comparator); voltageInfo.sort(comparator); powerInfo.sort(comparator); + cpuUsageInfo.sort(comparator); let tempInfo = gpuTempInfo.concat(sensorsTempInfo).concat(driveTempInfo); if (tempInfo.length == 0 && fanInfo.length == 0 - && voltageInfo.length == 0) { + && voltageInfo.length == 0 + && cpuUsageInfo.length == 0) { this._sensorMenuItems = {}; this.menu.removeAll(); @@ -848,6 +887,18 @@ class FreonMenuButton extends PanelMenu.Button { }); } + if (cpuUsageInfo.length > 0 && (fanInfo.length > 0 || voltageInfo.length > 0 || powerInfo.length > 0)) + sensors.push({type : 'separator'}); + + for (let cpu of cpuUsageInfo){ + sensors.push({ + icon: 'cpu-usage', + type: 'cpu-usage', + label: cpu.label, + value: _("%.1f%%").format(cpu.usage) + }); + } + this._fixNames(sensors); for (let k in this._hotLabels) diff --git a/freon@UshakovVasilii_Github.yahoo.com/prefs.js b/freon@UshakovVasilii_Github.yahoo.com/prefs.js index ff34079..b38e226 100644 --- a/freon@UshakovVasilii_Github.yahoo.com/prefs.js +++ b/freon@UshakovVasilii_Github.yahoo.com/prefs.js @@ -110,6 +110,26 @@ export default class FreonPreferences extends ExtensionPreferences { width_request: 320, }); + const cpuUsageSwitch = this._addSwitch("CPU Usage", "use-cpu-usage", "Read CPU usage from /proc/stat"); + group.add(cpuUsageSwitch); + + const cpuUsageInterval = new Adw.SpinRow({ + title: _('CPU Usage Polling Interval'), + subtitle: _('Seconds between CPU usage updates'), + adjustment: new Gtk.Adjustment({ + lower: 1, + upper: 60, + value: this._settings.get_int('cpu-usage-update-time'), + step_increment: 1, + }), + }); + this._settings.bind('cpu-usage-update-time', cpuUsageInterval, 'value', Gio.SettingsBindFlags.DEFAULT); + cpuUsageInterval.sensitive = this._settings.get_boolean('use-cpu-usage'); + this._settings.connect('changed::use-cpu-usage', () => { + cpuUsageInterval.sensitive = this._settings.get_boolean('use-cpu-usage'); + }); + group.add(cpuUsageInterval); + group.add(this._addSwitch("lm-sensors", "use-generic-lmsensors", "Read sensors from lm-sensors")); group.add(this._addSwitch("liquidctl", "use-generic-liquidctl", "Read sensors from liquidctl (v1.7.0+)")); const wattdSwitch = this._addSwitch("wattd", "use-generic-wattd", "Read power data from wattd"); @@ -180,6 +200,7 @@ export default class FreonPreferences extends ExtensionPreferences { group.add(this._addSwitch("Voltage", "show-voltage")); group.add(this._addSwitch("Power", "show-power")); group.add(this._addSwitch("Battery", "show-battery-stats")); + group.add(this._addSwitch("CPU Usage", "show-cpu-usage")); return group } diff --git a/freon@UshakovVasilii_Github.yahoo.com/schemas/org.gnome.shell.extensions.sensors.gschema.xml b/freon@UshakovVasilii_Github.yahoo.com/schemas/org.gnome.shell.extensions.sensors.gschema.xml index d15166f..48aed8f 100644 --- a/freon@UshakovVasilii_Github.yahoo.com/schemas/org.gnome.shell.extensions.sensors.gschema.xml +++ b/freon@UshakovVasilii_Github.yahoo.com/schemas/org.gnome.shell.extensions.sensors.gschema.xml @@ -105,6 +105,24 @@ Read sensors from (requires liquidctl v1.7.0 or later) + + true + Enable CPU usage sensor + Read CPU usage from /proc/stat + + + + true + Show CPU usage + Display CPU usage percentage + + + + 2 + Seconds between CPU usage updates + Polling interval for CPU usage sensor + + false Read power data from wattd