[gnome] Update extensions

This commit is contained in:
2026-04-09 09:51:44 -04:00
parent dadc78f2f4
commit 505c67d292
61 changed files with 1729 additions and 365 deletions

View File

@ -17,6 +17,7 @@ import * as MessageTray from 'resource:///org/gnome/shell/ui/messageTray.js';
import * as Values from './values.js';
import * as Config from 'resource:///org/gnome/shell/misc/config.js';
import * as MenuItem from './menuItem.js';
import * as HistoryGraph from './history.js';
let vitalsMenu;
@ -51,23 +52,30 @@ var VitalsMenuButton = GObject.registerClass({
this._warnings = [];
this._sensorMenuItems = {};
this._hotLabels = {};
this._hotIcons = {};
this._hotItems = {};
this._groups = {};
this._widths = {};
this._numGpus = 1;
this._newGpuDetected = false;
this._newGpuDetectedCount = 0;
this._last_query = new Date().getTime();
this._historyPopout = null;
this._historyHideTimeoutId = null;
this._historyPopoutSensorKey = null;
this._historyPopoutLabel = null;
this._sensors = new Sensors.Sensors(this._settings, this._sensorIcons);
this._values = new Values.Values(this._settings, this._sensorIcons);
this._historyCachePath = GLib.get_user_cache_dir() + '/vitals/history.json';
this._values.loadTimeSeries(this._historyCachePath);
this._menuLayout = new St.BoxLayout({
vertical: false,
clip_to_allocation: true,
x_align: Clutter.ActorAlign.START,
y_align: Clutter.ActorAlign.CENTER,
reactive: true,
x_expand: true
x_expand: true,
style_class: 'vitals-panel-menu'
});
this._drawMenu();
@ -75,12 +83,12 @@ var VitalsMenuButton = GObject.registerClass({
this._settingChangedSignals = [];
this._refreshTimeoutId = null;
this._addSettingChangedSignal('update-time', this._updateTimeChanged.bind(this));
this._addSettingChangedSignal('update-time', this._updateTimeSettingChanged.bind(this));
this._addSettingChangedSignal('position-in-panel', this._positionInPanelChanged.bind(this));
this._addSettingChangedSignal('menu-centered', this._positionInPanelChanged.bind(this));
this._addSettingChangedSignal('icon-style', this._iconStyleChanged.bind(this));
let settings = [ 'use-higher-precision', 'alphabetize', 'hide-zeros', 'fixed-widths', 'hide-icons', 'unit', 'memory-measurement', 'include-public-ip', 'network-speed-format', 'storage-measurement', 'include-static-info', 'include-static-gpu-info' ];
let settings = [ 'use-higher-precision', 'alphabetize', 'hide-zeros', 'fixed-widths', 'hide-icons', 'unit', 'memory-measurement', 'include-public-ip', 'network-speed-format', 'storage-measurement', 'include-static-info', 'include-static-gpu-info', 'show-sensor-history-graph' ];
for (let setting of Object.values(settings))
this._addSettingChangedSignal(setting, this._redrawMenu.bind(this));
@ -89,6 +97,7 @@ var VitalsMenuButton = GObject.registerClass({
this._addSettingChangedSignal('show-' + sensor, this._showHideSensorsChanged.bind(this));
this._initializeMenu();
this._createHistoryPopout();
// start off with fresh sensors
this._querySensors();
@ -175,10 +184,216 @@ var VitalsMenuButton = GObject.registerClass({
// refresh sensors now
this._querySensors();
} else {
this._hideHistoryPopout();
}
});
}
_createHistoryPopout() {
const popoutWidth = 280;
const popoutHeight = 145;
this._historyPopout = new St.BoxLayout({
vertical: true,
style_class: 'vitals-history-popout',
width: popoutWidth,
height: popoutHeight,
reactive: true,
visible: false
});
this._historyPopout.clip_to_allocation = true;
this._historyGraph = new HistoryGraph.HistoryGraph();
this._historyTitleLabel = new St.Label({
text: '',
style_class: 'vitals-history-popout-label',
x_align: Clutter.ActorAlign.END
});
this._historyPopout.add_child(this._historyTitleLabel);
this._historyGraphRow = new St.BoxLayout({
vertical: false,
x_expand: true,
style_class: 'vitals-history-graph-row'
});
this._historyYAxis = new St.BoxLayout({
vertical: true,
width: 56,
style_class: 'vitals-history-y-axis'
});
this._historyYMax = new St.Label({
text: '',
style_class: 'vitals-history-popout-axis',
x_align: Clutter.ActorAlign.END
});
this._historyYMin = new St.Label({
text: '',
style_class: 'vitals-history-popout-axis',
x_align: Clutter.ActorAlign.END
});
this._historyYSpacer = new St.BoxLayout({ vertical: true, y_expand: true });
this._historyYAxis.add_child(this._historyYMax);
this._historyYAxis.add_child(this._historyYSpacer);
this._historyYAxis.add_child(this._historyYMin);
this._historyGraphRow.add_child(this._historyYAxis);
this._historyGraphRow.add_child(this._historyGraph);
this._historyGraphRightSpacer = new St.BoxLayout({
vertical: true,
width: 18,
style_class: 'vitals-history-graph-right-spacer'
});
this._historyGraphRow.add_child(this._historyGraphRightSpacer);
this._historyPopout.add_child(this._historyGraphRow);
this._historyXWrap = new St.BoxLayout({
vertical: false,
x_expand: true,
style_class: 'vitals-history-x-wrap'
});
this._historyXSpacer = new St.BoxLayout({
vertical: true,
width: 62,
style_class: 'vitals-history-x-spacer'
});
this._historyXRow = new St.BoxLayout({
vertical: false,
x_expand: true,
style_class: 'vitals-history-x-row'
});
this._historyXLeft = new St.Label({
text: '',
style_class: 'vitals-history-popout-axis',
x_align: Clutter.ActorAlign.START,
x_expand: true
});
this._historyXRight = new St.Label({
text: _('now'),
style_class: 'vitals-history-popout-axis',
x_align: Clutter.ActorAlign.END
});
this._historyXRow.add_child(this._historyXLeft);
this._historyXRow.add_child(this._historyXRight);
this._historyXWrap.add_child(this._historyXSpacer);
this._historyXWrap.add_child(this._historyXRow);
this._historyPopout.add_child(this._historyXWrap);
this._historyPopout.connect('enter-event', () => {
if (this._historyHideTimeoutId) {
GLib.Source.remove(this._historyHideTimeoutId);
this._historyHideTimeoutId = null;
}
});
this._historyPopout.connect('leave-event', () => {
this._scheduleHistoryPopoutHide();
});
}
_updateHistoryGraph(key, label, samples) {
const historyDuration = Math.max(60, this._settings.get_int('sensor-history-duration'));
const nowSec = Date.now() / 1000;
const cutoff = nowSec - historyDuration;
const windowed = samples.filter(s => s.t >= cutoff);
while (windowed.length > 0 && windowed[0].v === null) windowed.shift();
const base = Math.max(1, Math.ceil(windowed.length / 200));
this._historyGraph.setData(windowed, label, '', base);
const actualSpan = this._historyGraph.getTimeSpan();
const displayDuration = actualSpan > 0 ? Math.min(historyDuration, Math.round(actualSpan)) : historyDuration;
this._historyXLeft.text = this._values.formatDuration(displayDuration) + ' ' + _('ago');
const rawRange = this._historyGraph.getRawRange();
if (rawRange) {
this._historyYMax.text = this._values.formatValue(key, rawRange.max);
this._historyYMin.text = this._values.formatValue(key, rawRange.min);
this._historyYAxis.show();
} else {
this._historyYMax.text = '';
this._historyYMin.text = '';
this._historyYAxis.hide();
}
}
_showHistoryPopout(key, label, itemActor) {
if (!this._settings.get_boolean('show-sensor-history-graph')) return;
const samples = this._values.getTimeSeries(key);
if (samples.length === 0) return;
this._historyPopoutSensorKey = key;
this._historyPopoutLabel = label;
try {
this._historyTitleLabel.text = label + ' ' + _('history');
this._historyTitleLabel.show();
this._updateHistoryGraph(key, label, samples);
} catch (e) {
this._historyYMax.text = '';
this._historyYMin.text = '';
}
const parent = this.menu.actor.get_parent();
if (!parent) return;
if (this._historyPopout.get_parent() !== parent) {
if (this._historyPopout.get_parent())
this._historyPopout.get_parent().remove_child(this._historyPopout);
parent.add_child(this._historyPopout);
}
const menuX = this.menu.actor.get_x();
const menuY = this.menu.actor.get_y();
let popoutY = menuY;
if (itemActor) {
let relY = 0;
let node = itemActor;
while (node && node !== this.menu.actor) {
relY += node.get_y();
node = node.get_parent();
}
const rowH = itemActor.get_height();
const popoutH = this._historyPopout.get_height();
popoutY = menuY + relY + Math.round((rowH - popoutH) / 2);
popoutY = Math.max(0, popoutY);
}
const popoutW = this._historyPopout.get_width();
const menuW = this.menu.actor.get_width();
let popoutX = menuX - popoutW - 8;
if (popoutX < 0)
popoutX = menuX + menuW + 8;
this._historyPopout.set_position(popoutX, popoutY);
this._historyPopout.show();
if (this._historyHideTimeoutId) {
GLib.Source.remove(this._historyHideTimeoutId);
this._historyHideTimeoutId = null;
}
}
_scheduleHistoryPopoutHide() {
if (this._historyHideTimeoutId) return;
this._historyHideTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 250, () => {
this._hideHistoryPopout();
this._historyHideTimeoutId = null;
return GLib.SOURCE_REMOVE;
});
}
_hideHistoryPopout() {
if (this._historyHideTimeoutId) {
GLib.Source.remove(this._historyHideTimeoutId);
this._historyHideTimeoutId = null;
}
if (this._historyPopout && this._historyPopout.get_parent()) {
this._historyPopout.hide();
this._historyPopout.get_parent().remove_child(this._historyPopout);
}
this._historyPopoutSensorKey = null;
this._historyPopoutLabel = null;
}
_refreshHistoryPopout() {
const key = this._historyPopoutSensorKey;
const label = this._historyPopoutLabel;
if (!key || !label) return;
if (!this._historyPopout || !this._historyPopout.visible) return;
const samples = this._values.getTimeSeries(key);
if (samples.length === 0) return;
try {
this._updateHistoryGraph(key, label, samples);
} catch (e) {
// ignore
}
}
_initializeMenuGroup(groupName, optionName, menuSuffix = '', position = -1) {
this._groups[groupName] = new PopupMenu.PopupSubMenuMenuItem(_(this._ucFirst(groupName) + menuSuffix), true);
this._groups[groupName].icon.gicon = Gio.icon_new_for_string(this._sensorIconPath(groupName));
@ -219,8 +434,7 @@ var VitalsMenuButton = GObject.registerClass({
// removes sensors that are no longer available
if (!this._sensorMenuItems[sensor]) {
hotSensors.splice(i, 1);
this._removeHotLabel(sensor);
this._removeHotIcon(sensor);
this._removeHotItem(sensor);
}
}
@ -245,19 +459,24 @@ var VitalsMenuButton = GObject.registerClass({
GLib.PRIORITY_DEFAULT,
update_time,
(self) => {
// only update menu if we have hot sensors
if (Object.values(this._hotLabels).length > 0)
this._querySensors();
// keep the timer running
return GLib.SOURCE_CONTINUE;
// always query sensors (for panel display when hot, and for history graph data)
this._querySensors();
return GLib.SOURCE_CONTINUE;
}
);
}
_createHotItem(key, value) {
let icon = this._defaultIcon(key);
this._hotIcons[key] = icon;
this._menuLayout.add_child(icon)
let item = new St.BoxLayout({
style_class: 'vitals-panel-item',
});
this._hotItems[key] = item;
this._menuLayout.add_child(item);
if (!this._settings.get_boolean('hide-icons') || key == '_default_icon_') {
let icon = this._defaultIcon(key);
item.add_child(icon);
}
// don't add a label when no sensors are in the panel
if (key == '_default_icon_') return;
@ -268,18 +487,15 @@ var VitalsMenuButton = GObject.registerClass({
y_expand: true,
y_align: Clutter.ActorAlign.CENTER
});
// attempt to prevent ellipsizes
label.get_clutter_text().ellipsize = 0;
// keep track of label for removal later
this._hotLabels[key] = label;
// prevent "called on the widget" "which is not in the stage" errors by adding before width below
this._menuLayout.add_child(label);
item.add_child(label);
// support for fixed widths #55, save label (text) width
this._widths[key] = label.width;
this._widths[key] = label.get_clutter_text().width;
}
_showHideSensorsChanged(self, sensor) {
@ -329,35 +545,24 @@ var VitalsMenuButton = GObject.registerClass({
this._redrawMenu();
}
_removeHotLabel(key) {
if (key in this._hotLabels) {
let label = this._hotLabels[key];
_removeHotItems(){
for (let key in this._hotItems) {
this._removeHotItem(key);
}
}
_removeHotItem(key) {
if (key in this._hotItems) {
this._hotItems[key].destroy();
delete this._hotItems[key];
delete this._hotLabels[key];
// make sure set_label is not called on non existent actor
label.destroy();
delete this._widths[key];
}
}
_removeHotLabels() {
for (let key in this._hotLabels)
this._removeHotLabel(key);
}
_removeHotIcon(key) {
if (key in this._hotIcons) {
this._hotIcons[key].destroy();
delete this._hotIcons[key];
}
}
_removeHotIcons() {
for (let key in this._hotIcons)
this._removeHotIcon(key);
}
_redrawMenu() {
this._removeHotIcons();
this._removeHotLabels();
this._hideHistoryPopout();
this._removeHotItems();
for (let key in this._sensorMenuItems) {
if (key.includes('-group')) continue;
@ -391,6 +596,12 @@ var VitalsMenuButton = GObject.registerClass({
}
}
_updateTimeSettingChanged() {
this._destroyTimer();
this._values.clearTimeSeries(this._historyCachePath);
this._initializeTimer();
}
_updateTimeChanged() {
this._destroyTimer();
this._initializeTimer();
@ -452,8 +663,7 @@ var VitalsMenuButton = GObject.registerClass({
} else {
// remove selected sensor from panel
hotSensors.splice(hotSensors.indexOf(self.key), 1);
this._removeHotLabel(self.key);
this._removeHotIcon(self.key);
this._removeHotItem(self.key);
}
if (hotSensors.length <= 0) {
@ -465,7 +675,7 @@ var VitalsMenuButton = GObject.registerClass({
if (defIconPos >= 0) {
// remove generic icon from panel when sensors are selected
hotSensors.splice(defIconPos, 1);
this._removeHotIcon('_default_icon_');
this._removeHotItem('_default_icon_');
}
}
@ -486,6 +696,25 @@ var VitalsMenuButton = GObject.registerClass({
}
this._groups[type].menu.addMenuItem(item, i);
if (this._settings.get_boolean('show-sensor-history-graph')) {
const key = item.key;
const label = item.label;
item.actor.connect('enter-event', () => {
const samples = this._values.getTimeSeries(key);
if (this._historyHideTimeoutId) {
GLib.Source.remove(this._historyHideTimeoutId);
this._historyHideTimeoutId = null;
}
if (samples.length > 0)
this._showHistoryPopout(key, label, item.actor);
else
this._hideHistoryPopout();
});
item.actor.connect('leave-event', () => {
this._scheduleHistoryPopoutHide();
});
}
}
_defaultLabel() {
@ -508,7 +737,7 @@ var VitalsMenuButton = GObject.registerClass({
// don't use the default system icon if the type is a gpu; use the universal gpu icon instead
if (type == 'default' || (!(type in this._sensorIcons) && !type.startsWith('gpu'))) {
icon.gicon = Gio.icon_new_for_string(this._sensorIconPath('system'));
} else if (!this._settings.get_boolean('hide-icons')) { // support for hide icons #80
} else { // support for hide icons #80
let iconObj = (split.length == 2)?'icon-' + split[1]:'icon';
icon.gicon = Gio.icon_new_for_string(this._sensorIconPath(type, iconObj));
}
@ -627,6 +856,8 @@ var VitalsMenuButton = GObject.registerClass({
this._notify('Vitals', this._warnings.join("\n"), 'folder-symbolic');
this._warnings = [];
}
this._refreshHistoryPopout();
}
_notify(msg, details, icon) {
@ -638,7 +869,13 @@ var VitalsMenuButton = GObject.registerClass({
}
destroy() {
this._hideHistoryPopout();
if (this._historyPopout) {
this._historyPopout.destroy();
this._historyPopout = null;
}
this._destroyTimer();
this._values.saveTimeSeries(this._historyCachePath);
this._sensors.destroy();
for (let signal of Object.values(this._settingChangedSignals))

View File

@ -0,0 +1,157 @@
/*
Copyright (c) 2018, Chris Monahan <chris@corecoding.com>
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the GNOME nor the names of its contributors may be
used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import GObject from 'gi://GObject';
import St from 'gi://St';
const GRAPH_WIDTH = 208;
const GRAPH_HEIGHT = 90;
const PADDING = 4;
const MIN_BAR_WIDTH = 1;
export const HistoryGraph = GObject.registerClass({
GTypeName: 'HistoryGraph',
}, class HistoryGraph extends St.Widget {
_init(params = {}) {
super._init({
width: GRAPH_WIDTH,
height: GRAPH_HEIGHT,
style_class: 'vitals-history-graph',
...params
});
this._samples = [];
this._label = '';
this._unit = '';
this._vMin = 0;
this._vMax = 0;
this._base = 1;
this._dataOffset = 0;
this.clip_to_allocation = true;
this._barContainer = new St.Widget({
x_expand: true,
y_expand: true
});
this._barContainer.clip_to_allocation = true;
this.add_child(this._barContainer);
}
setData(samples, label, unit, base) {
this._samples = Array.isArray(samples) ? samples : [];
this._label = label || '';
this._unit = unit || '';
this._base = Math.max(1, base);
this._rebuildBars();
}
_rebuildBars() {
try {
const children = this._barContainer.get_children();
if (children && children.length > 0) {
for (let i = children.length - 1; i >= 0; i--)
children[i].destroy();
}
} catch (e) {
// ignore
}
const data = this._samples;
if (data.length === 0) return;
const graphW = GRAPH_WIDTH - 2 * PADDING;
const graphH = GRAPH_HEIGHT - PADDING;
if (graphW <= 0 || graphH <= 0) return;
const base = this._base;
const maxBars = Math.floor(graphW / MIN_BAR_WIDTH);
const totalBars = Math.ceil(data.length / base);
const numBars = Math.min(totalBars, maxBars);
const dataOffset = (totalBars - numBars) * base;
this._dataOffset = dataOffset;
const barWidth = graphW / numBars;
let vMin = Infinity, vMax = -Infinity;
for (let i = dataOffset; i < data.length; i++) {
if (data[i].v === null) continue;
if (data[i].v < vMin) vMin = data[i].v;
if (data[i].v > vMax) vMax = data[i].v;
}
if (vMin === Infinity) {
vMin = 0;
vMax = 1;
} else if (vMax <= vMin) {
const v = vMin;
if (v >= 0 && v <= 1) {
const margin = 0.05;
vMin = Math.max(0, v - margin);
vMax = Math.min(1, v + margin);
if (vMax <= vMin) vMax = vMin + margin;
} else {
vMin -= 1;
vMax += 1;
}
}
this._vMin = vMin;
this._vMax = vMax;
const vRange = vMax - vMin;
for (let b = 0; b < numBars; b++) {
const iStart = dataOffset + b * base;
const iEnd = Math.min(iStart + base, data.length);
let sum = 0;
let count = 0;
for (let i = iStart; i < iEnd; i++) {
if (data[i].v !== null) {
sum += data[i].v;
count++;
}
}
if (count === 0) continue;
const avg = sum / count;
const norm = (avg - vMin) / vRange;
const barH = Math.max(1, Math.round(norm * graphH));
const x = Math.round(b * barWidth);
const w = Math.round((b + 1) * barWidth) - x;
const bar = new St.Bin({
width: w,
height: barH,
style_class: 'vitals-history-graph-bar'
});
bar.set_position(x, graphH - barH);
this._barContainer.add_child(bar);
}
}
getRawRange() {
if (this._samples.length === 0) return null;
return { min: this._vMin, max: this._vMax };
}
getTimeSpan() {
const start = this._dataOffset;
if (this._samples.length - start < 2) return 0;
return this._samples[this._samples.length - 1].t - this._samples[start].t;
}
});

View File

@ -12,9 +12,10 @@
"46",
"47",
"48",
"49"
"49",
"50"
],
"url": "https://github.com/corecoding/Vitals",
"uuid": "Vitals@CoreCoding.com",
"version": 73
"version": 74
}

View File

@ -49,7 +49,8 @@ const Settings = new GObject.Class({
'alphabetize', 'hide-zeros', 'include-public-ip',
'show-battery', 'fixed-widths', 'hide-icons',
'menu-centered', 'include-static-info',
'show-gpu', 'include-static-gpu-info' ];
'show-gpu', 'include-static-gpu-info',
'show-sensor-history-graph' ];
for (let key in sensors) {
let sensor = sensors[key];

View File

@ -415,6 +415,35 @@
</property>
</object>
</child>
<child>
<object class="GtkListBoxRow">
<property name="width_request">100</property>
<property name="selectable">0</property>
<property name="child">
<object class="GtkBox">
<property name="can_focus">0</property>
<property name="margin_top">6</property>
<property name="margin_bottom">6</property>
<child>
<object class="GtkLabel">
<property name="hexpand">1</property>
<property name="can_focus">0</property>
<property name="halign">start</property>
<property name="margin-start">5</property>
<property name="margin-end">5</property>
<property name="label" translatable="yes">Show sensor history graph on hover</property>
</object>
</child>
<child>
<object class="GtkSwitch" id="show-sensor-history-graph">
<property name="halign">end</property>
<property name="margin-end">5</property>
</object>
</child>
</object>
</property>
</object>
</child>
</object>
</property>
<child type="label_item">

View File

@ -151,5 +151,15 @@
<summary>Icon styles</summary>
<description>Set the style for the displayed sensor icons ('original', 'updated')</description>
</key>
<key type="b" name="show-sensor-history-graph">
<default>true</default>
<summary>Show sensor history graph on hover</summary>
<description>When hovering a sensor row in the menu, show a pop-out graph of its history to the left</description>
</key>
<key type="i" name="sensor-history-duration">
<default>3600</default>
<summary>Sensor history duration (seconds)</summary>
<description>How many seconds of history to keep and display in the hover graph (e.g. 3600 for 1 hour)</description>
</key>
</schema>
</schemalist>

View File

@ -24,6 +24,7 @@
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import * as SubProcessModule from './helpers/subprocess.js';
import * as FileModule from './helpers/file.js';
@ -60,6 +61,12 @@ export const Sensors = GObject.registerClass({
this._nvidia_labels = [];
this._bad_split_count = 0;
this._frameMonitorSignalId = 0;
this._frameMonitorLastTime = 0;
this._frameMonitorFrameCount = 0;
this._frameMonitorAccTime = 0;
this._frameMonitorCurrentHz = 0;
if (hasGTop) {
this.storage = new GTop.glibtop_fsusage();
this._storageDevice = '';
@ -370,11 +377,19 @@ export const Sensors = GObject.registerClass({
let free = this.storage.bfree * this.storage.block_size;
let used = total - free;
let reserved = (total - avail) - used;
let freePercent = 0;
let usedPercent = 0;
if (total > 0) {
freePercent = Math.round((free / total) * 100);
usedPercent = Math.round((used / total) * 100);
}
this._returnValue(callback, 'Total', total, 'storage', 'storage');
this._returnValue(callback, 'Used', used, 'storage', 'storage');
this._returnValue(callback, 'Reserved', reserved, 'storage', 'storage');
this._returnValue(callback, 'Free', avail, 'storage', 'storage');
this._returnValue(callback, 'Used %', usedPercent + '%', 'storage', 'string');
this._returnValue(callback, 'Free %', freePercent + '%', 'storage', 'string');
this._returnValue(callback, 'storage', avail, 'storage-group', 'storage');
}
@ -427,8 +442,11 @@ export const Sensors = GObject.registerClass({
}
if ('POWER_NOW' in output) {
this._returnValue(callback, 'Rate', output['POWER_NOW'], 'battery', 'watt');
this._returnValue(callback, 'battery', output['POWER_NOW'], 'battery-group', 'watt');
const powerValue = (
parseFloat(output['POWER_NOW']) * (output['STATUS'] === 'Discharging' ? -1 : 1)
);
this._returnValue(callback, 'Power Rate', powerValue, 'battery', 'watt');
this._returnValue(callback, 'battery', powerValue, 'battery-group', 'watt');
}
if ('CHARGE_FULL' in output && 'VOLTAGE_MIN_DESIGN' in output && (!('ENERGY_FULL' in output))) {
@ -500,10 +518,56 @@ export const Sensors = GObject.registerClass({
}).catch(err => { });
}
_initFrameMonitor() {
if (this._frameMonitorSignalId) return;
this._frameMonitorLastTime = 0;
this._frameMonitorFrameCount = 0;
this._frameMonitorAccTime = 0;
this._frameMonitorCurrentHz = 0;
this._frameMonitorSignalId = global.stage.connect('after-paint', () => {
this._onAfterPaint();
});
}
_destroyFrameMonitor() {
if (this._frameMonitorSignalId) {
global.stage.disconnect(this._frameMonitorSignalId);
this._frameMonitorSignalId = 0;
}
this._frameMonitorLastTime = 0;
this._frameMonitorCurrentHz = 0;
}
_onAfterPaint() {
const now = GLib.get_monotonic_time();
if (this._frameMonitorLastTime === 0) {
this._frameMonitorLastTime = now;
return;
}
const delta = now - this._frameMonitorLastTime;
this._frameMonitorLastTime = now;
this._frameMonitorFrameCount++;
this._frameMonitorAccTime += delta;
if (this._frameMonitorAccTime >= 500000) {
this._frameMonitorCurrentHz = this._frameMonitorFrameCount / (this._frameMonitorAccTime / 1000000);
this._frameMonitorFrameCount = 0;
this._frameMonitorAccTime = 0;
}
}
_queryGpu(callback) {
if (this._frameMonitorCurrentHz > 0)
this._returnValue(callback, 'Refresh Rate', this._frameMonitorCurrentHz, 'gpu#1', 'hertz');
if (!this._nvidia_smi_process) {
// no nvidia-smi, so we use sysfs DRM if any cards was discovered
if (!this._gpu_drm_indices){
if (this._frameMonitorCurrentHz > 0)
this._returnValue(callback, 'Refresh Rate', this._frameMonitorCurrentHz, 'gpu#1-group', 'hertz');
this._disableGpuLabels(callback);
return;
} else {
@ -801,6 +865,7 @@ export const Sensors = GObject.registerClass({
// Launch nvidia-smi subprocess if nvidia querying is enabled
this._reconfigureNvidiaSmiProcess();
this._discoverGpuDrm();
this._initFrameMonitor();
}
_discoverGpuDrm() {
@ -992,9 +1057,13 @@ export const Sensors = GObject.registerClass({
this._battery_charge_status = '';
this._nvidia_labels = [];
this._bad_split_count = 0;
this._frameMonitorLastTime = 0;
this._frameMonitorFrameCount = 0;
this._frameMonitorAccTime = 0;
}
destroy() {
this._destroyFrameMonitor();
this._terminateNvidiaSmiProcess();
for (let signal of Object.values(this._settingChangedSignals))

View File

@ -1,16 +1,80 @@
.vitals-icon { icon-size: 16px; }
.vitals-menu-button-container {}
.vitals-panel-icon-temperature { margin: 0 1px 0 8px; padding: 0; }
.vitals-panel-icon-voltage { margin: 0 0 0 8px; padding: 0; }
.vitals-panel-icon-fan { margin: 0 4px 0 8px; padding: 0; }
.vitals-panel-icon-memory { margin: 0 2px 0 8px; padding: 0; }
.vitals-panel-icon-processor { margin: 0 3px 0 8px; padding: 0; }
.vitals-panel-icon-system { margin: 0 3px 0 8px; padding: 0; }
.vitals-panel-icon-network { margin: 0 3px 0 8px; padding: 0; }
.vitals-panel-icon-storage { margin: 0 2px 0 8px; padding: 0; }
.vitals-panel-icon-battery { margin: 0 4px 0 8px; padding: 0; }
.vitals-panel-label { margin: 0 3px 0 0; padding: 0; }
.vitals-panel-item{spacing: 0;}
.vitals-panel-menu{spacing: 11px; padding: 3px; }
.vitals-panel-icon-default {}
.vitals-panel-icon-temperature { margin: 0 1px 0 0; padding: 0; }
.vitals-panel-icon-voltage { margin: 0 0 0 0; padding: 0; }
.vitals-panel-icon-fan { margin: 0 4px 0 0; padding: 0; }
.vitals-panel-icon-memory { margin: 0 2px 0 0; padding: 0; }
.vitals-panel-icon-processor { margin: 0 3px 0 0; padding: 0; }
.vitals-panel-icon-system { margin: 0 3px 0 0; padding: 0; }
.vitals-panel-icon-network { margin: 0 3px 0 0; padding: 0; }
.vitals-panel-icon-storage { margin: 0 2px 0 0; padding: 0; }
.vitals-panel-icon-battery { margin: 0 4px 0 0; padding: 0; }
.vitals-panel-label {}
.vitals-button-action { -st-icon-style: symbolic; border-radius: 32px; margin: 0px; min-height: 22px; min-width: 22px; padding: 10px; font-size: 100%; border: 1px solid transparent; }
.vitals-button-action:hover, .vitals-button-action:focus { border-color: #777; }
.vitals-button-action > StIcon { icon-size: 16px; }
.vitals-button-box { padding: 0px; spacing: 22px; }
.vitals-history-popout {
padding: 6px 14px 6px 6px;
border-radius: 8px;
border: 1px solid rgba(128, 128, 128, 0.4);
background-color: rgba(30, 30, 30, 0.95);
}
.vitals-history-popout-label {
font-size: 10px;
color: rgba(200, 200, 200, 0.9);
}
.vitals-history-popout-axis {
font-size: 9px;
color: rgba(180, 180, 180, 0.85);
}
.vitals-history-graph-row {
margin-top: 2px;
margin-bottom: 2px;
}
.vitals-history-y-axis {
margin-right: 6px;
padding-top: 2px;
padding-bottom: 2px;
}
.vitals-history-x-wrap {
margin-top: 2px;
}
.vitals-history-x-spacer {
width: 62px;
min-width: 62px;
}
.vitals-history-x-row {
min-width: 0;
}
.vitals-history-x-row .vitals-history-popout-axis {
margin-left: 0;
margin-right: 4px;
}
.vitals-history-graph {
background-color: rgba(0, 0, 0, 0.25);
border-radius: 4px;
padding: 4px 4px 0 4px;
margin: 2px 0 2px 0;
}
.vitals-history-graph-bars {
spacing: 0px;
}
.vitals-history-graph-bar {
background-color: rgba(51, 128, 230, 0.87);
}

View File

@ -24,6 +24,8 @@
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import GLib from 'gi://GLib';
import Gio from 'gi://Gio';
import GObject from 'gi://GObject';
const cbFun = (d, c) => {
@ -47,9 +49,138 @@ export const Values = GObject.registerClass({
this._history = {};
//this._history2 = {};
this._timeSeries = {};
this._timeSeriesFormat = {};
this._graphableFormats = ['temp', 'in', 'fan', 'percent', 'hertz', 'memory', 'speed', 'storage', 'watt', 'watt-gpu', 'milliamp', 'milliamp-hour', 'load'];
this.resetHistory();
}
_getHistoryDurationSeconds() {
if (this._settings && this._settings.get_int)
return Math.max(60, this._settings.get_int('sensor-history-duration'));
return 3600;
}
_pushTimePoint(key, value, format) {
if (!this._graphableFormats.includes(format)) return;
const num = typeof value === 'number' ? value : parseFloat(value);
if (num !== num) return; // NaN check
this._timeSeriesFormat[key] = format;
const now = Date.now() / 1000;
if (!(key in this._timeSeries)) this._timeSeries[key] = [];
const buf = this._timeSeries[key];
const interval = Math.max(1, this._settings.get_int('update-time'));
const minInterval = interval - 0.5;
if (buf.length > 0 && buf[buf.length - 1].v !== null && (now - buf[buf.length - 1].t) < minInterval) {
buf[buf.length - 1].v = num;
return;
}
if (buf.length > 0) {
const lastT = buf[buf.length - 1].t;
const gap = now - lastT;
if (gap > interval * 3) {
let fillT = lastT + interval;
while (fillT < now - interval * 0.5) {
buf.push({ t: fillT, v: null });
fillT += interval;
}
}
}
buf.push({ t: now, v: num });
const maxAge = this._getHistoryDurationSeconds();
while (buf.length > 0 && buf[0].t < now - maxAge) buf.shift();
const maxPoints = 3600;
while (buf.length > maxPoints) buf.shift();
}
clearTimeSeries(cachePath) {
this._timeSeries = {};
this._timeSeriesFormat = {};
if (cachePath) {
try {
const file = Gio.File.new_for_path(cachePath);
if (file.query_exists(null))
file.delete(null);
} catch (e) {
// ignore
}
}
}
saveTimeSeries(path) {
try {
const obj = {
version: 1,
timeSeries: this._timeSeries,
timeSeriesFormat: this._timeSeriesFormat
};
const json = JSON.stringify(obj);
const dir = GLib.path_get_dirname(path);
GLib.mkdir_with_parents(dir, 0o755);
GLib.file_set_contents(path, json);
} catch (e) {
// ignore write failures
}
}
loadTimeSeries(path) {
try {
const file = Gio.File.new_for_path(path);
if (!file.query_exists(null)) return;
const [ok, contents] = GLib.file_get_contents(path);
if (!ok) return;
const decoder = new TextDecoder('utf-8');
const json = decoder.decode(contents);
const obj = JSON.parse(json);
if (!obj || obj.version !== 1) return;
if (obj.timeSeries && typeof obj.timeSeries === 'object')
this._timeSeries = obj.timeSeries;
if (obj.timeSeriesFormat && typeof obj.timeSeriesFormat === 'object')
this._timeSeriesFormat = obj.timeSeriesFormat;
const now = Date.now() / 1000;
const maxAge = this._getHistoryDurationSeconds();
const cutoff = now - maxAge;
for (const key in this._timeSeries) {
const buf = this._timeSeries[key];
if (!Array.isArray(buf)) {
delete this._timeSeries[key];
continue;
}
while (buf.length > 0 && buf[0].t < cutoff)
buf.shift();
while (buf.length > 0 && buf[0].v === null)
buf.shift();
if (buf.length === 0) {
delete this._timeSeries[key];
delete this._timeSeriesFormat[key];
}
}
} catch (e) {
// ignore corrupt or missing file
}
}
getTimeSeries(key) {
if (!(key in this._timeSeries)) return [];
return this._timeSeries[key].slice();
}
formatValue(key, rawValue) {
const format = key in this._timeSeriesFormat ? this._timeSeriesFormat[key] : 'percent';
return this._legible(rawValue, format);
}
formatDuration(seconds) {
seconds = Math.round(Math.abs(seconds));
if (seconds < 60) return seconds + 's';
const m = Math.floor(seconds / 60);
if (m < 60) return m + 'm';
const h = Math.floor(m / 60);
const rm = m % 60;
if (rm === 0) return h + 'h';
return h + 'h ' + rm + 'm';
}
_legible(value, sensorClass) {
let unit = 1000;
if (value === null) return 'N/A';
@ -186,7 +317,9 @@ export const Values = GObject.registerClass({
ending = 'mAh';
break;
case 'watt':
format = (use_higher_precision)?'%.2f %s':'%.1f %s';
format = (
((value > 0) ? '+' : '') + ((use_higher_precision)?'%.2f %s':'%.1f %s')
);
value = value / 1000000;
ending = 'W';
break;
@ -212,7 +345,7 @@ export const Values = GObject.registerClass({
break;
}
return format.format(value, ending);
return format.format(value, ending).trim();
}
returnIfDifferent(dwell, label, value, type, format, key) {
@ -242,6 +375,8 @@ export const Values = GObject.registerClass({
// save previous values to update screen on changes only
let previousValue = this._history[type][key];
this._history[type][key] = [legible, value];
if (type !== 'network-rx' && type !== 'network-tx')
this._pushTimePoint(key, value, format);
// process average, min and max values
if (type == 'temperature' || type == 'voltage' || type == 'fan') {
@ -272,6 +407,8 @@ export const Values = GObject.registerClass({
// appends total upload and download for all interfaces for #216
let vals = Object.values(this._history[type]).map(x => parseFloat(x[1]));
let sum = vals.reduce((partialSum, a) => partialSum + a, 0);
const memUnit = this._settings.get_int('memory-measurement') ? 1000 : 1024;
this._pushTimePoint('__' + type + '_boot__', sum / memUnit, 'memory');
output.push(['Boot ' + direction, this._legible(sum, format), type, '__' + type + '_boot__']);
// keeps track of session start point
@ -279,11 +416,14 @@ export const Values = GObject.registerClass({
this._networkSpeedOffset[key] = sum;
// outputs session upload and download for all interfaces for #234
const sessionVal = sum - this._networkSpeedOffset[key];
this._pushTimePoint('__' + type + '_ses__', sessionVal / memUnit, 'memory');
output.push(['Session ' + direction, this._legible(sum - this._networkSpeedOffset[key], format), type, '__' + type + '_ses__']);
// calculate speed for this interface
let speed = (value - previousValue[1]) / dwell;
output.push([label, this._legible(speed, 'speed'), type, key]);
this._pushTimePoint(key, speed, 'speed');
// store speed for Device report
if (!(direction in this._networkSpeeds)) this._networkSpeeds[direction] = {};
@ -295,11 +435,12 @@ export const Values = GObject.registerClass({
// calculate total upload and download device speed
for (let direction in this._networkSpeeds) {
let sum = 0;
let sumNum = 0;
for (let iface in this._networkSpeeds[direction])
sum += parseFloat(this._networkSpeeds[direction][iface]);
sumNum += parseFloat(this._networkSpeeds[direction][iface]);
sum = this._legible(sum, 'speed');
this._pushTimePoint('__network-' + direction + '_max__', sumNum, 'speed');
let sum = this._legible(sumNum, 'speed');
output.push(['Device ' + direction, sum, 'network-' + direction, '__network-' + direction + '_max__']);
// append download speed to group itself
if (direction == 'rx') output.push([type, sum, type + '-group', '']);