[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 Values from './values.js';
|
||||||
import * as Config from 'resource:///org/gnome/shell/misc/config.js';
|
import * as Config from 'resource:///org/gnome/shell/misc/config.js';
|
||||||
import * as MenuItem from './menuItem.js';
|
import * as MenuItem from './menuItem.js';
|
||||||
import * as HistoryGraph from './history.js';
|
|
||||||
|
|
||||||
let vitalsMenu;
|
let vitalsMenu;
|
||||||
|
|
||||||
@ -59,15 +58,9 @@ var VitalsMenuButton = GObject.registerClass({
|
|||||||
this._newGpuDetected = false;
|
this._newGpuDetected = false;
|
||||||
this._newGpuDetectedCount = 0;
|
this._newGpuDetectedCount = 0;
|
||||||
this._last_query = new Date().getTime();
|
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._sensors = new Sensors.Sensors(this._settings, this._sensorIcons);
|
||||||
this._values = new Values.Values(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({
|
this._menuLayout = new St.BoxLayout({
|
||||||
vertical: false,
|
vertical: false,
|
||||||
clip_to_allocation: true,
|
clip_to_allocation: true,
|
||||||
@ -88,7 +81,7 @@ var VitalsMenuButton = GObject.registerClass({
|
|||||||
this._addSettingChangedSignal('menu-centered', this._positionInPanelChanged.bind(this));
|
this._addSettingChangedSignal('menu-centered', this._positionInPanelChanged.bind(this));
|
||||||
this._addSettingChangedSignal('icon-style', this._iconStyleChanged.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))
|
for (let setting of Object.values(settings))
|
||||||
this._addSettingChangedSignal(setting, this._redrawMenu.bind(this));
|
this._addSettingChangedSignal(setting, this._redrawMenu.bind(this));
|
||||||
|
|
||||||
@ -97,7 +90,6 @@ var VitalsMenuButton = GObject.registerClass({
|
|||||||
this._addSettingChangedSignal('show-' + sensor, this._showHideSensorsChanged.bind(this));
|
this._addSettingChangedSignal('show-' + sensor, this._showHideSensorsChanged.bind(this));
|
||||||
|
|
||||||
this._initializeMenu();
|
this._initializeMenu();
|
||||||
this._createHistoryPopout();
|
|
||||||
|
|
||||||
// start off with fresh sensors
|
// start off with fresh sensors
|
||||||
this._querySensors();
|
this._querySensors();
|
||||||
@ -184,216 +176,10 @@ var VitalsMenuButton = GObject.registerClass({
|
|||||||
|
|
||||||
// refresh sensors now
|
// refresh sensors now
|
||||||
this._querySensors();
|
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) {
|
_initializeMenuGroup(groupName, optionName, menuSuffix = '', position = -1) {
|
||||||
this._groups[groupName] = new PopupMenu.PopupSubMenuMenuItem(_(this._ucFirst(groupName) + menuSuffix), true);
|
this._groups[groupName] = new PopupMenu.PopupSubMenuMenuItem(_(this._ucFirst(groupName) + menuSuffix), true);
|
||||||
this._groups[groupName].icon.gicon = Gio.icon_new_for_string(this._sensorIconPath(groupName));
|
this._groups[groupName].icon.gicon = Gio.icon_new_for_string(this._sensorIconPath(groupName));
|
||||||
@ -459,7 +245,7 @@ var VitalsMenuButton = GObject.registerClass({
|
|||||||
GLib.PRIORITY_DEFAULT,
|
GLib.PRIORITY_DEFAULT,
|
||||||
update_time,
|
update_time,
|
||||||
(self) => {
|
(self) => {
|
||||||
// always query sensors (for panel display when hot, and for history graph data)
|
// always query sensors (for panel display when hot)
|
||||||
this._querySensors();
|
this._querySensors();
|
||||||
return GLib.SOURCE_CONTINUE;
|
return GLib.SOURCE_CONTINUE;
|
||||||
}
|
}
|
||||||
@ -561,7 +347,6 @@ var VitalsMenuButton = GObject.registerClass({
|
|||||||
}
|
}
|
||||||
|
|
||||||
_redrawMenu() {
|
_redrawMenu() {
|
||||||
this._hideHistoryPopout();
|
|
||||||
this._removeHotItems();
|
this._removeHotItems();
|
||||||
|
|
||||||
for (let key in this._sensorMenuItems) {
|
for (let key in this._sensorMenuItems) {
|
||||||
@ -598,7 +383,6 @@ var VitalsMenuButton = GObject.registerClass({
|
|||||||
|
|
||||||
_updateTimeSettingChanged() {
|
_updateTimeSettingChanged() {
|
||||||
this._destroyTimer();
|
this._destroyTimer();
|
||||||
this._values.clearTimeSeries(this._historyCachePath);
|
|
||||||
this._initializeTimer();
|
this._initializeTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -696,25 +480,6 @@ var VitalsMenuButton = GObject.registerClass({
|
|||||||
}
|
}
|
||||||
|
|
||||||
this._groups[type].menu.addMenuItem(item, i);
|
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() {
|
_defaultLabel() {
|
||||||
@ -856,8 +621,6 @@ var VitalsMenuButton = GObject.registerClass({
|
|||||||
this._notify('Vitals', this._warnings.join("\n"), 'folder-symbolic');
|
this._notify('Vitals', this._warnings.join("\n"), 'folder-symbolic');
|
||||||
this._warnings = [];
|
this._warnings = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
this._refreshHistoryPopout();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_notify(msg, details, icon) {
|
_notify(msg, details, icon) {
|
||||||
@ -869,13 +632,7 @@ var VitalsMenuButton = GObject.registerClass({
|
|||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
this._hideHistoryPopout();
|
|
||||||
if (this._historyPopout) {
|
|
||||||
this._historyPopout.destroy();
|
|
||||||
this._historyPopout = null;
|
|
||||||
}
|
|
||||||
this._destroyTimer();
|
this._destroyTimer();
|
||||||
this._values.saveTimeSeries(this._historyCachePath);
|
|
||||||
this._sensors.destroy();
|
this._sensors.destroy();
|
||||||
|
|
||||||
for (let signal of Object.values(this._settingChangedSignals))
|
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",
|
"url": "https://github.com/corecoding/Vitals",
|
||||||
"uuid": "Vitals@CoreCoding.com",
|
"uuid": "Vitals@CoreCoding.com",
|
||||||
"version": 74
|
"version": 75
|
||||||
}
|
}
|
||||||
@ -49,8 +49,7 @@ const Settings = new GObject.Class({
|
|||||||
'alphabetize', 'hide-zeros', 'include-public-ip',
|
'alphabetize', 'hide-zeros', 'include-public-ip',
|
||||||
'show-battery', 'fixed-widths', 'hide-icons',
|
'show-battery', 'fixed-widths', 'hide-icons',
|
||||||
'menu-centered', 'include-static-info',
|
'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) {
|
for (let key in sensors) {
|
||||||
let sensor = sensors[key];
|
let sensor = sensors[key];
|
||||||
|
|||||||
@ -415,35 +415,6 @@
|
|||||||
</property>
|
</property>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</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>
|
</object>
|
||||||
</property>
|
</property>
|
||||||
<child type="label_item">
|
<child type="label_item">
|
||||||
|
|||||||
Binary file not shown.
@ -151,15 +151,5 @@
|
|||||||
<summary>Icon styles</summary>
|
<summary>Icon styles</summary>
|
||||||
<description>Set the style for the displayed sensor icons ('original', 'updated')</description>
|
<description>Set the style for the displayed sensor icons ('original', 'updated')</description>
|
||||||
</key>
|
</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>
|
</schema>
|
||||||
</schemalist>
|
</schemalist>
|
||||||
|
|||||||
@ -17,64 +17,3 @@
|
|||||||
.vitals-button-action:hover, .vitals-button-action:focus { border-color: #777; }
|
.vitals-button-action:hover, .vitals-button-action:focus { border-color: #777; }
|
||||||
.vitals-button-action > StIcon { icon-size: 16px; }
|
.vitals-button-action > StIcon { icon-size: 16px; }
|
||||||
.vitals-button-box { padding: 0px; spacing: 22px; }
|
.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 GLib from 'gi://GLib';
|
||||||
import Gio from 'gi://Gio';
|
|
||||||
import GObject from 'gi://GObject';
|
import GObject from 'gi://GObject';
|
||||||
|
|
||||||
const cbFun = (d, c) => {
|
const cbFun = (d, c) => {
|
||||||
@ -49,138 +48,9 @@ export const Values = GObject.registerClass({
|
|||||||
|
|
||||||
this._history = {};
|
this._history = {};
|
||||||
//this._history2 = {};
|
//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();
|
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) {
|
_legible(value, sensorClass) {
|
||||||
let unit = 1000;
|
let unit = 1000;
|
||||||
if (value === null) return 'N/A';
|
if (value === null) return 'N/A';
|
||||||
@ -375,8 +245,6 @@ export const Values = GObject.registerClass({
|
|||||||
// save previous values to update screen on changes only
|
// save previous values to update screen on changes only
|
||||||
let previousValue = this._history[type][key];
|
let previousValue = this._history[type][key];
|
||||||
this._history[type][key] = [legible, value];
|
this._history[type][key] = [legible, value];
|
||||||
if (type !== 'network-rx' && type !== 'network-tx')
|
|
||||||
this._pushTimePoint(key, value, format);
|
|
||||||
|
|
||||||
// process average, min and max values
|
// process average, min and max values
|
||||||
if (type == 'temperature' || type == 'voltage' || type == 'fan') {
|
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 vals = Object.values(this._history[type]).map(x => parseFloat(x[1]));
|
||||||
let sum = vals.reduce((partialSum, a) => partialSum + a, 0);
|
let sum = vals.reduce((partialSum, a) => partialSum + a, 0);
|
||||||
const memUnit = this._settings.get_int('memory-measurement') ? 1000 : 1024;
|
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__']);
|
output.push(['Boot ' + direction, this._legible(sum, format), type, '__' + type + '_boot__']);
|
||||||
|
|
||||||
// keeps track of session start point
|
// keeps track of session start point
|
||||||
@ -416,14 +283,11 @@ export const Values = GObject.registerClass({
|
|||||||
this._networkSpeedOffset[key] = sum;
|
this._networkSpeedOffset[key] = sum;
|
||||||
|
|
||||||
// outputs session upload and download for all interfaces for #234
|
// 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__']);
|
output.push(['Session ' + direction, this._legible(sum - this._networkSpeedOffset[key], format), type, '__' + type + '_ses__']);
|
||||||
|
|
||||||
// calculate speed for this interface
|
// calculate speed for this interface
|
||||||
let speed = (value - previousValue[1]) / dwell;
|
let speed = (value - previousValue[1]) / dwell;
|
||||||
output.push([label, this._legible(speed, 'speed'), type, key]);
|
output.push([label, this._legible(speed, 'speed'), type, key]);
|
||||||
this._pushTimePoint(key, speed, 'speed');
|
|
||||||
|
|
||||||
// store speed for Device report
|
// store speed for Device report
|
||||||
if (!(direction in this._networkSpeeds)) this._networkSpeeds[direction] = {};
|
if (!(direction in this._networkSpeeds)) this._networkSpeeds[direction] = {};
|
||||||
@ -439,7 +303,6 @@ export const Values = GObject.registerClass({
|
|||||||
for (let iface in this._networkSpeeds[direction])
|
for (let iface in this._networkSpeeds[direction])
|
||||||
sumNum += parseFloat(this._networkSpeeds[direction][iface]);
|
sumNum += parseFloat(this._networkSpeeds[direction][iface]);
|
||||||
|
|
||||||
this._pushTimePoint('__network-' + direction + '_max__', sumNum, 'speed');
|
|
||||||
let sum = this._legible(sumNum, 'speed');
|
let sum = this._legible(sumNum, 'speed');
|
||||||
output.push(['Device ' + direction, sum, 'network-' + direction, '__network-' + direction + '_max__']);
|
output.push(['Device ' + direction, sum, 'network-' + direction, '__network-' + direction + '_max__']);
|
||||||
// append download speed to group itself
|
// append download speed to group itself
|
||||||
|
|||||||
Reference in New Issue
Block a user