[gnome] Extension update for Vitals
This commit is contained in:
@ -17,7 +17,6 @@ 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;
|
||||
|
||||
@ -59,15 +58,9 @@ var VitalsMenuButton = GObject.registerClass({
|
||||
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,
|
||||
@ -88,7 +81,7 @@ var VitalsMenuButton = GObject.registerClass({
|
||||
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', 'show-sensor-history-graph' ];
|
||||
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' ];
|
||||
for (let setting of Object.values(settings))
|
||||
this._addSettingChangedSignal(setting, this._redrawMenu.bind(this));
|
||||
|
||||
@ -97,7 +90,6 @@ var VitalsMenuButton = GObject.registerClass({
|
||||
this._addSettingChangedSignal('show-' + sensor, this._showHideSensorsChanged.bind(this));
|
||||
|
||||
this._initializeMenu();
|
||||
this._createHistoryPopout();
|
||||
|
||||
// start off with fresh sensors
|
||||
this._querySensors();
|
||||
@ -184,216 +176,10 @@ 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));
|
||||
@ -459,7 +245,7 @@ var VitalsMenuButton = GObject.registerClass({
|
||||
GLib.PRIORITY_DEFAULT,
|
||||
update_time,
|
||||
(self) => {
|
||||
// always query sensors (for panel display when hot, and for history graph data)
|
||||
// always query sensors (for panel display when hot)
|
||||
this._querySensors();
|
||||
return GLib.SOURCE_CONTINUE;
|
||||
}
|
||||
@ -561,7 +347,6 @@ var VitalsMenuButton = GObject.registerClass({
|
||||
}
|
||||
|
||||
_redrawMenu() {
|
||||
this._hideHistoryPopout();
|
||||
this._removeHotItems();
|
||||
|
||||
for (let key in this._sensorMenuItems) {
|
||||
@ -598,7 +383,6 @@ var VitalsMenuButton = GObject.registerClass({
|
||||
|
||||
_updateTimeSettingChanged() {
|
||||
this._destroyTimer();
|
||||
this._values.clearTimeSeries(this._historyCachePath);
|
||||
this._initializeTimer();
|
||||
}
|
||||
|
||||
@ -696,25 +480,6 @@ 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() {
|
||||
@ -856,8 +621,6 @@ var VitalsMenuButton = GObject.registerClass({
|
||||
this._notify('Vitals', this._warnings.join("\n"), 'folder-symbolic');
|
||||
this._warnings = [];
|
||||
}
|
||||
|
||||
this._refreshHistoryPopout();
|
||||
}
|
||||
|
||||
_notify(msg, details, icon) {
|
||||
@ -869,13 +632,7 @@ 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))
|
||||
|
||||
@ -1,157 +0,0 @@
|
||||
/*
|
||||
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;
|
||||
}
|
||||
});
|
||||
@ -17,5 +17,5 @@
|
||||
],
|
||||
"url": "https://github.com/corecoding/Vitals",
|
||||
"uuid": "Vitals@CoreCoding.com",
|
||||
"version": 74
|
||||
"version": 75
|
||||
}
|
||||
@ -49,8 +49,7 @@ 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-sensor-history-graph' ];
|
||||
'show-gpu', 'include-static-gpu-info' ];
|
||||
|
||||
for (let key in sensors) {
|
||||
let sensor = sensors[key];
|
||||
|
||||
@ -415,35 +415,6 @@
|
||||
</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">
|
||||
|
||||
Binary file not shown.
@ -151,15 +151,5 @@
|
||||
<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>
|
||||
|
||||
@ -17,64 +17,3 @@
|
||||
.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);
|
||||
}
|
||||
|
||||
@ -25,7 +25,6 @@
|
||||
*/
|
||||
|
||||
import GLib from 'gi://GLib';
|
||||
import Gio from 'gi://Gio';
|
||||
import GObject from 'gi://GObject';
|
||||
|
||||
const cbFun = (d, c) => {
|
||||
@ -49,138 +48,9 @@ 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';
|
||||
@ -375,8 +245,6 @@ 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') {
|
||||
@ -408,7 +276,6 @@ export const Values = GObject.registerClass({
|
||||
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
|
||||
@ -416,14 +283,11 @@ 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] = {};
|
||||
@ -439,7 +303,6 @@ export const Values = GObject.registerClass({
|
||||
for (let iface in this._networkSpeeds[direction])
|
||||
sumNum += parseFloat(this._networkSpeeds[direction][iface]);
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user