[gnome] Update extensions
This commit is contained in:
@ -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))
|
||||
|
||||
@ -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;
|
||||
}
|
||||
});
|
||||
Binary file not shown.
@ -12,9 +12,10 @@
|
||||
"46",
|
||||
"47",
|
||||
"48",
|
||||
"49"
|
||||
"49",
|
||||
"50"
|
||||
],
|
||||
"url": "https://github.com/corecoding/Vitals",
|
||||
"uuid": "Vitals@CoreCoding.com",
|
||||
"version": 73
|
||||
"version": 74
|
||||
}
|
||||
@ -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];
|
||||
|
||||
@ -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">
|
||||
|
||||
Binary file not shown.
@ -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>
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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', '']);
|
||||
|
||||
@ -24,6 +24,7 @@ import St from 'gi://St';
|
||||
import * as Params from 'resource:///org/gnome/shell/misc/params.js';
|
||||
import * as Signals from 'resource:///org/gnome/shell/misc/signals.js';
|
||||
|
||||
import * as DBusUtils from './dbusUtils.js';
|
||||
import * as IconCache from './iconCache.js';
|
||||
import * as Util from './util.js';
|
||||
import * as Interfaces from './interfaces.js';
|
||||
@ -218,6 +219,8 @@ class AppIndicatorProxy extends DBusProxy {
|
||||
return;
|
||||
}
|
||||
|
||||
const cancellable = this._cancellable;
|
||||
|
||||
if (!params.get_type().equal(AppIndicatorProxy.TUPLE_TYPE)) {
|
||||
// If the property includes arguments, we can just queue the signal emission
|
||||
const [value] = params.unpack();
|
||||
@ -238,7 +241,7 @@ class AppIndicatorProxy extends DBusProxy {
|
||||
return;
|
||||
|
||||
this._signalsAccumulator = new PromiseUtils.TimeoutPromise(
|
||||
MAX_UPDATE_FREQUENCY, GLib.PRIORITY_DEFAULT_IDLE, this._cancellable);
|
||||
MAX_UPDATE_FREQUENCY, GLib.PRIORITY_DEFAULT_IDLE, cancellable);
|
||||
try {
|
||||
await this._signalsAccumulator;
|
||||
const refreshPropertiesPromises =
|
||||
@ -460,7 +463,7 @@ export class AppIndicator extends Signals.EventEmitter {
|
||||
}
|
||||
|
||||
try {
|
||||
this._commandLine = await Util.getProcessName(this.busName,
|
||||
this._commandLine = await DBusUtils.getProcessName(this.busName,
|
||||
cancellable, GLib.PRIORITY_LOW);
|
||||
} catch (e) {
|
||||
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
|
||||
|
||||
@ -196,7 +196,7 @@ export class DbusMenuItem extends Signals.EventEmitter {
|
||||
if (!data)
|
||||
data = GLib.Variant.new_int32(0);
|
||||
|
||||
this._client.sendEvent(this._id, event, data, timestamp);
|
||||
return this._client.sendEvent(this._id, event, data, timestamp);
|
||||
}
|
||||
|
||||
getId() {
|
||||
@ -533,14 +533,18 @@ export const DBusClient = GObject.registerClass({
|
||||
}
|
||||
}
|
||||
|
||||
sendEvent(id, event, params, timestamp) {
|
||||
async sendEvent(id, event, params, timestamp) {
|
||||
if (!this.gNameOwner)
|
||||
return;
|
||||
|
||||
this.EventAsync(id, event, params, timestamp, this._cancellable).catch(e => {
|
||||
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
|
||||
logError(e);
|
||||
});
|
||||
try {
|
||||
await this.EventAsync(id, event, params, timestamp, this._cancellable);
|
||||
} catch (e) {
|
||||
if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
|
||||
return;
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
_onPropertiesUpdated([changed, removed]) {
|
||||
@ -628,6 +632,8 @@ const MenuItemFactory = {
|
||||
shellItem, MenuItemFactory._onActivate);
|
||||
|
||||
shellItem.connect('destroy', () => {
|
||||
shellItem._dbusItemCancellable?.cancel();
|
||||
shellItem._dbusItemCancellable = null;
|
||||
shellItem._dbusItem = null;
|
||||
shellItem._dbusClient = null;
|
||||
shellItem._icon = null;
|
||||
@ -655,7 +661,7 @@ const MenuItemFactory = {
|
||||
menu._parent._openedSubMenu = menu;
|
||||
}
|
||||
|
||||
this._dbusItem.handleEvent('opened', null, 0);
|
||||
this._dbusItem.handleEvent('opened', null, 0).catch(logError);
|
||||
this._dbusItem.sendAboutToShow();
|
||||
} else {
|
||||
if (NEED_NESTED_SUBMENU_FIX) {
|
||||
@ -664,7 +670,7 @@ const MenuItemFactory = {
|
||||
menu._openedSubMenu.close(false);
|
||||
}
|
||||
|
||||
this._dbusItem.handleEvent('closed', null, 0);
|
||||
this._dbusItem.handleEvent('closed', null, 0).catch(logError);
|
||||
}
|
||||
},
|
||||
|
||||
@ -674,7 +680,7 @@ const MenuItemFactory = {
|
||||
this._dbusClient.indicator.provideActivationToken(timestamp);
|
||||
|
||||
this._dbusItem.handleEvent('clicked', GLib.Variant.new('i', 0),
|
||||
timestamp);
|
||||
timestamp).catch(logError);
|
||||
},
|
||||
|
||||
_onPropertyChanged(dbusItem, prop, _value) {
|
||||
@ -756,10 +762,15 @@ const MenuItemFactory = {
|
||||
this._icon.icon_name = iconName;
|
||||
} else if (iconData) {
|
||||
try {
|
||||
if (!this._dbusItemCancellable) {
|
||||
this._dbusItemCancellable = new Util.CancellableChild(
|
||||
this._dbusClient.cancellable);
|
||||
}
|
||||
|
||||
const inputStream = Gio.MemoryInputStream.new_from_bytes(
|
||||
iconData.get_data_as_bytes());
|
||||
this._icon.gicon = await GdkPixbuf.Pixbuf.new_from_stream_async(
|
||||
inputStream, this._dbusClient.cancellable);
|
||||
inputStream, this._dbusItemCancellable);
|
||||
} catch (e) {
|
||||
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
|
||||
logError(e);
|
||||
@ -872,7 +883,7 @@ export class Client extends Signals.EventEmitter {
|
||||
menu._setOpenedSubMenu = this._setOpenedSubmenu.bind(this);
|
||||
|
||||
// connect handlers
|
||||
Util.connectSmart(menu, 'open-state-changed', this, this._onMenuOpened);
|
||||
Util.connectSmart(menu, 'open-state-changed', this, this._onMenuOpenStateChanged);
|
||||
Util.connectSmart(menu, 'destroy', this, this.destroy);
|
||||
|
||||
Util.connectSmart(this._rootItem, 'child-added', this, this._onRootChildAdded);
|
||||
@ -939,7 +950,7 @@ export class Client extends Signals.EventEmitter {
|
||||
MenuUtils.moveItemInMenu(this._rootMenu, dbusItem, newpos);
|
||||
}
|
||||
|
||||
_onMenuOpened(menu, state) {
|
||||
_onMenuOpenStateChanged(menu, state) {
|
||||
if (!this._rootItem)
|
||||
return;
|
||||
|
||||
@ -949,10 +960,18 @@ export class Client extends Signals.EventEmitter {
|
||||
if (this._openedSubMenu && this._openedSubMenu.isOpen)
|
||||
this._openedSubMenu.close();
|
||||
|
||||
this._rootItem.handleEvent('opened', null, 0);
|
||||
this._rootItem.handleEvent('opened', null, 0).catch(logError);
|
||||
this._rootItem.sendAboutToShow();
|
||||
} else {
|
||||
this._rootItem.handleEvent('closed', null, 0);
|
||||
this._rootItem.handleEvent('closed', null, 0).catch(e => {
|
||||
if (e.matches(Gio.DBusError, Gio.DBusError.UNKNOWN_OBJECT)) {
|
||||
// The menu hay have been removed at this point, thus do not
|
||||
// spam the users about this if it happens.
|
||||
return;
|
||||
}
|
||||
|
||||
logError(e);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -40,8 +40,13 @@ export const DBusProxy = GObject.registerClass({
|
||||
(_proxy, ...args) => this._onSignal(...args)));
|
||||
}
|
||||
|
||||
this._signalIds.push(this.connect('notify::g-name-owner', () =>
|
||||
this._onNameOwnerChanged()));
|
||||
this._signalIds.push(this.connect('notify::g-name-owner', () => {
|
||||
if (!this.gNameOwner) {
|
||||
this._cancellable.cancel();
|
||||
this._cancellable = new Gio.Cancellable();
|
||||
}
|
||||
this._onNameOwnerChanged();
|
||||
}));
|
||||
}
|
||||
|
||||
async initAsync(cancellable) {
|
||||
|
||||
@ -0,0 +1,121 @@
|
||||
// This file is part of the AppIndicator/KStatusNotifierItem GNOME Shell extension
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import Gio from 'gi://Gio';
|
||||
import GLib from 'gi://GLib';
|
||||
|
||||
import {Logger} from './logger.js';
|
||||
|
||||
export const BUS_ADDRESS_REGEX = /([a-zA-Z0-9._-]+\.[a-zA-Z0-9.-]+)|(:[0-9]+\.[0-9]+)$/;
|
||||
|
||||
Gio._promisify(Gio.DBusConnection.prototype, 'call');
|
||||
|
||||
export async function getUniqueBusName(bus, name, cancellable) {
|
||||
if (name[0] === ':')
|
||||
return name;
|
||||
|
||||
if (!bus)
|
||||
bus = Gio.DBus.session;
|
||||
|
||||
const variantName = new GLib.Variant('(s)', [name]);
|
||||
const [unique] = (await bus.call('org.freedesktop.DBus', '/', 'org.freedesktop.DBus',
|
||||
'GetNameOwner', variantName, new GLib.VariantType('(s)'),
|
||||
Gio.DBusCallFlags.NONE, -1, cancellable)).deep_unpack();
|
||||
|
||||
return unique;
|
||||
}
|
||||
|
||||
export async function getBusNames(bus, cancellable) {
|
||||
if (!bus)
|
||||
bus = Gio.DBus.session;
|
||||
|
||||
const [names] = (await bus.call('org.freedesktop.DBus', '/', 'org.freedesktop.DBus',
|
||||
'ListNames', null, new GLib.VariantType('(as)'), Gio.DBusCallFlags.NONE,
|
||||
-1, cancellable)).deep_unpack();
|
||||
|
||||
const uniqueNames = new Map();
|
||||
const requests = names.map(name => getUniqueBusName(bus, name, cancellable));
|
||||
const results = await Promise.allSettled(requests);
|
||||
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
const result = results[i];
|
||||
if (result.status === 'fulfilled') {
|
||||
let namesForBus = uniqueNames.get(result.value);
|
||||
if (!namesForBus) {
|
||||
namesForBus = new Set();
|
||||
uniqueNames.set(result.value, namesForBus);
|
||||
}
|
||||
if (result.value !== names[i])
|
||||
namesForBus.add(names[i]);
|
||||
} else if (!result.reason.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
|
||||
Logger.debug(`Impossible to get the unique name of ${names[i]}: ${result.reason}`);
|
||||
}
|
||||
}
|
||||
|
||||
return uniqueNames;
|
||||
}
|
||||
|
||||
async function getProcessId(connectionName, cancellable = null, bus = Gio.DBus.session) {
|
||||
const res = await bus.call('org.freedesktop.DBus', '/',
|
||||
'org.freedesktop.DBus', 'GetConnectionUnixProcessID',
|
||||
new GLib.Variant('(s)', [connectionName]),
|
||||
new GLib.VariantType('(u)'),
|
||||
Gio.DBusCallFlags.NONE,
|
||||
-1,
|
||||
cancellable);
|
||||
const [pid] = res.deepUnpack();
|
||||
return pid;
|
||||
}
|
||||
|
||||
export async function getProcessName(connectionName, cancellable = null,
|
||||
priority = GLib.PRIORITY_DEFAULT, bus = Gio.DBus.session) {
|
||||
const pid = await getProcessId(connectionName, cancellable, bus);
|
||||
const cmdFile = Gio.File.new_for_path(`/proc/${pid}/cmdline`);
|
||||
const inputStream = await cmdFile.read_async(priority, cancellable);
|
||||
const bytes = await inputStream.read_bytes_async(2048, priority, cancellable);
|
||||
const textDecoder = new TextDecoder();
|
||||
return textDecoder.decode(bytes.toArray().map(v => !v ? 0x20 : v));
|
||||
}
|
||||
|
||||
export async function* introspectBusObject(bus, name, cancellable,
|
||||
interfaces = undefined, path = undefined) {
|
||||
if (!path)
|
||||
path = '/';
|
||||
|
||||
const [introspection] = (await bus.call(name, path, 'org.freedesktop.DBus.Introspectable',
|
||||
'Introspect', null, new GLib.VariantType('(s)'), Gio.DBusCallFlags.NONE,
|
||||
5000, cancellable)).deep_unpack();
|
||||
|
||||
const nodeInfo = Gio.DBusNodeInfo.new_for_xml(introspection);
|
||||
|
||||
if (!interfaces || dbusNodeImplementsInterfaces(nodeInfo, interfaces))
|
||||
yield {nodeInfo, path};
|
||||
|
||||
if (path === '/')
|
||||
path = '';
|
||||
|
||||
for (const subNodeInfo of nodeInfo.nodes) {
|
||||
const subPath = `${path}/${subNodeInfo.path}`;
|
||||
yield* introspectBusObject(bus, name, cancellable, interfaces, subPath);
|
||||
}
|
||||
}
|
||||
|
||||
function dbusNodeImplementsInterfaces(nodeInfo, interfaces) {
|
||||
if (!(nodeInfo instanceof Gio.DBusNodeInfo) || !Array.isArray(interfaces))
|
||||
return false;
|
||||
|
||||
return interfaces.some(iface => nodeInfo.lookup_interface(iface));
|
||||
}
|
||||
@ -20,13 +20,14 @@ import * as StatusNotifierWatcher from './statusNotifierWatcher.js';
|
||||
import * as Interfaces from './interfaces.js';
|
||||
import * as TrayIconsManager from './trayIconsManager.js';
|
||||
import * as Util from './util.js';
|
||||
import {Logger} from './logger.js';
|
||||
import {SettingsManager} from './settingsManager.js';
|
||||
|
||||
export default class AppIndicatorExtension extends Extension.Extension {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
Util.Logger.init(this);
|
||||
Logger.init(this);
|
||||
Interfaces.initialize(this);
|
||||
|
||||
this._isEnabled = false;
|
||||
@ -42,7 +43,7 @@ export default class AppIndicatorExtension extends Extension.Extension {
|
||||
global['--appindicator-extension-on-reload']();
|
||||
|
||||
global['--appindicator-extension-on-reload'] = () => {
|
||||
Util.Logger.debug('Reload detected, destroying old watchdog');
|
||||
Logger.debug('Reload detected, destroying old watchdog');
|
||||
this._watchDog.destroy();
|
||||
this._watchDog = null;
|
||||
};
|
||||
@ -84,6 +85,6 @@ export default class AppIndicatorExtension extends Extension.Extension {
|
||||
return;
|
||||
|
||||
this._statusNotifierWatcher = new StatusNotifierWatcher.StatusNotifierWatcher(
|
||||
this._watchDog);
|
||||
this, this._watchDog);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
let _defaultTheme;
|
||||
export function getDefaultTheme() {
|
||||
if (_defaultTheme)
|
||||
return _defaultTheme;
|
||||
|
||||
_defaultTheme = new St.IconTheme();
|
||||
return _defaultTheme;
|
||||
}
|
||||
|
||||
export function destroyDefaultTheme() {
|
||||
_defaultTheme = null;
|
||||
}
|
||||
@ -246,6 +246,10 @@ class IndicatorStatusIcon extends BaseStatusIcon {
|
||||
_init(indicator) {
|
||||
super._init(0.5, indicator.accessibleName,
|
||||
new AppIndicator.IconActor(indicator, DEFAULT_ICON_SIZE));
|
||||
|
||||
// Disable upstream's click gesture and fall back to vfunc_button_press_event etc.
|
||||
this._clickGesture?.set_enabled(false);
|
||||
|
||||
this._indicator = indicator;
|
||||
|
||||
this._lastClickTime = -1;
|
||||
|
||||
@ -0,0 +1,105 @@
|
||||
// This file is part of the AppIndicator/KStatusNotifierItem GNOME Shell extension
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import GLib from 'gi://GLib';
|
||||
|
||||
/**
|
||||
* Helper class for logging stuff
|
||||
*/
|
||||
export class Logger {
|
||||
static _logStructured(logLevel, message, extraFields = {}) {
|
||||
if (!Object.values(GLib.LogLevelFlags).includes(logLevel)) {
|
||||
Logger._logStructured(GLib.LogLevelFlags.LEVEL_WARNING,
|
||||
'logLevel is not a valid GLib.LogLevelFlags');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Logger._levels.includes(logLevel))
|
||||
return;
|
||||
|
||||
let fields = {
|
||||
'SYSLOG_IDENTIFIER': Logger._uuid,
|
||||
'MESSAGE': `${message}`,
|
||||
};
|
||||
|
||||
let thisFile = null;
|
||||
const {stack} = new Error();
|
||||
for (let stackLine of stack.split('\n')) {
|
||||
stackLine = stackLine.replace('resource:///org/gnome/Shell/', '');
|
||||
const [code, line] = stackLine.split(':');
|
||||
const [func, file] = code.split(/@(.+)/);
|
||||
|
||||
if (!thisFile || thisFile === file) {
|
||||
thisFile = file;
|
||||
continue;
|
||||
}
|
||||
|
||||
fields = Object.assign(fields, {
|
||||
'CODE_FILE': file || '',
|
||||
'CODE_LINE': line || '',
|
||||
'CODE_FUNC': func || '',
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
GLib.log_structured(Logger._domain, logLevel, Object.assign(fields, extraFields));
|
||||
}
|
||||
|
||||
static init(extension) {
|
||||
if (Logger._domain)
|
||||
return;
|
||||
|
||||
const allLevels = Object.values(GLib.LogLevelFlags);
|
||||
const domains = GLib.getenv('G_MESSAGES_DEBUG');
|
||||
const {name: domain} = extension.metadata;
|
||||
Logger._uuid = extension.metadata.uuid;
|
||||
Logger._domain = domain.replaceAll(' ', '-');
|
||||
|
||||
if (domains === 'all' || (domains && domains.split(' ').includes(Logger._domain))) {
|
||||
Logger._levels = allLevels;
|
||||
} else {
|
||||
Logger._levels = allLevels.filter(
|
||||
l => l <= GLib.LogLevelFlags.LEVEL_WARNING);
|
||||
}
|
||||
}
|
||||
|
||||
static destroy() {
|
||||
delete Logger._domain;
|
||||
delete Logger._uuid;
|
||||
delete Logger._levels;
|
||||
}
|
||||
|
||||
static debug(message) {
|
||||
Logger._logStructured(GLib.LogLevelFlags.LEVEL_DEBUG, message);
|
||||
}
|
||||
|
||||
static message(message) {
|
||||
Logger._logStructured(GLib.LogLevelFlags.LEVEL_MESSAGE, message);
|
||||
}
|
||||
|
||||
static warn(message) {
|
||||
Logger._logStructured(GLib.LogLevelFlags.LEVEL_WARNING, message);
|
||||
}
|
||||
|
||||
static error(message) {
|
||||
Logger._logStructured(GLib.LogLevelFlags.LEVEL_ERROR, message);
|
||||
}
|
||||
|
||||
static critical(message) {
|
||||
Logger._logStructured(GLib.LogLevelFlags.LEVEL_CRITICAL, message);
|
||||
}
|
||||
}
|
||||
@ -14,5 +14,5 @@
|
||||
],
|
||||
"url": "https://github.com/ubuntu/gnome-shell-extension-appindicator",
|
||||
"uuid": "appindicatorsupport@rgcjonas.gmail.com",
|
||||
"version": 63
|
||||
"version": 64
|
||||
}
|
||||
@ -22,10 +22,15 @@ import * as IndicatorStatusIcon from './indicatorStatusIcon.js';
|
||||
import * as Interfaces from './interfaces.js';
|
||||
import * as PromiseUtils from './promiseUtils.js';
|
||||
import * as Util from './util.js';
|
||||
import * as DBusUtils from './dbusUtils.js';
|
||||
import * as DBusMenu from './dbusMenu.js';
|
||||
|
||||
import {DBusProxy} from './dbusProxy.js';
|
||||
|
||||
Gio._promisify(Gio.Subprocess.prototype, 'wait_async');
|
||||
Gio._promisify(Gio.Subprocess.prototype, 'communicate_async');
|
||||
Gio._promisify(Gio.DataInputStream.prototype, 'read_line_async', 'read_line_finish_utf8');
|
||||
|
||||
|
||||
// TODO: replace with org.freedesktop and /org/freedesktop when approved
|
||||
const KDE_PREFIX = 'org.kde';
|
||||
@ -39,7 +44,7 @@ const DEFAULT_ITEM_OBJECT_PATH = '/StatusNotifierItem';
|
||||
* The StatusNotifierWatcher class implements the StatusNotifierWatcher dbus object
|
||||
*/
|
||||
export class StatusNotifierWatcher {
|
||||
constructor(watchDog) {
|
||||
constructor(extension, watchDog) {
|
||||
this._watchDog = watchDog;
|
||||
this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(Interfaces.StatusNotifierWatcher, this);
|
||||
try {
|
||||
@ -62,7 +67,7 @@ export class StatusNotifierWatcher {
|
||||
Util.Logger.warn(`Failed to notify registered host ${WATCHER_OBJECT}`);
|
||||
}
|
||||
|
||||
this._seekStatusNotifierItems().catch(e => {
|
||||
this._seekStatusNotifierItems(extension).catch(e => {
|
||||
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
|
||||
logError(e, 'Looking for StatusNotifierItem\'s');
|
||||
});
|
||||
@ -140,23 +145,38 @@ export class StatusNotifierWatcher {
|
||||
await this._registerItem(service, busName, objPath);
|
||||
}
|
||||
|
||||
async _seekStatusNotifierItems() {
|
||||
async _seekStatusNotifierItems(extension) {
|
||||
// Some indicators (*coff*, dropbox, *coff*) do not re-register again
|
||||
// when the plugin is enabled/disabled, thus we need to manually look
|
||||
// for the objects in the session bus that implements the
|
||||
// StatusNotifierItem interface... However let's do it after a low
|
||||
// priority idle, so that it won't affect startup.
|
||||
// priority timeout, and using an external process so that it won't
|
||||
// affect startup or memory (as it seems that gjs is not great at
|
||||
// handling the memory of the bus analyzer async code).
|
||||
const cancellable = this._cancellable;
|
||||
const bus = Gio.DBus.session;
|
||||
const uniqueNames = await Util.getBusNames(bus, cancellable);
|
||||
const introspectName = async name => {
|
||||
const nodes = Util.introspectBusObject(bus, name, cancellable,
|
||||
['org.kde.StatusNotifierItem']);
|
||||
const services = [...uniqueNames.get(name)];
|
||||
await new PromiseUtils.TimeoutSecondsPromise(2, GLib.PRIORITY_LOW, cancellable);
|
||||
const busAnalyzer = GLib.build_filenamev([
|
||||
extension.path, 'tools', 'busAnalyzer.js',
|
||||
]);
|
||||
|
||||
const subProcess = Gio.Subprocess.new(['gjs', '-m', busAnalyzer],
|
||||
Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE);
|
||||
|
||||
const stdOut = subProcess.get_stdout_pipe();
|
||||
const dataInputStream = new Gio.DataInputStream({base_stream: stdOut});
|
||||
const textDecoder = new TextDecoder();
|
||||
|
||||
while (true) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const [line] = await dataInputStream.read_line_async(GLib.PRIORITY_DEFAULT,
|
||||
cancellable);
|
||||
if (!line)
|
||||
break;
|
||||
|
||||
try {
|
||||
const {services, name, path} = JSON.parse(textDecoder.decode(line));
|
||||
const ids = [null, ...services].map(s => Util.indicatorId(s, name, path));
|
||||
|
||||
for await (const node of nodes) {
|
||||
const {path} = node;
|
||||
const ids = services.map(s => Util.indicatorId(s, name, path));
|
||||
if (ids.every(id => !this._items.has(id))) {
|
||||
const service = services.find(s =>
|
||||
s && s.startsWith('org.kde.StatusNotifierItem')) || services[0];
|
||||
@ -164,11 +184,24 @@ export class StatusNotifierWatcher {
|
||||
path === DEFAULT_ITEM_OBJECT_PATH ? service : null,
|
||||
name, path);
|
||||
Util.Logger.warn(`Using Brute-force mode for StatusNotifierItem ${id}`);
|
||||
this._registerItem(service, name, path);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await this._registerItem(service, name, path);
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
}
|
||||
};
|
||||
await Promise.allSettled([...uniqueNames.keys()].map(n => introspectName(n)));
|
||||
}
|
||||
|
||||
const [, stdErr] = await subProcess.communicate_async(null, cancellable);
|
||||
await subProcess.wait_async(cancellable);
|
||||
|
||||
if (subProcess.get_exit_status() !== 0) {
|
||||
const errorLines = textDecoder.decode(stdErr.toArray()).split('\n');
|
||||
const error = new GLib.Error(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED,
|
||||
errorLines[0]);
|
||||
error.stack = `${errorLines.slice(3).join('\n')}${error.stack}`;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async RegisterStatusNotifierItemAsync(params, invocation) {
|
||||
@ -181,9 +214,9 @@ export class StatusNotifierWatcher {
|
||||
if (service.charAt(0) === '/') { // looks like a path
|
||||
busName = invocation.get_sender();
|
||||
objPath = service;
|
||||
} else if (service.match(Util.BUS_ADDRESS_REGEX)) {
|
||||
} else if (service.match(DBusUtils.BUS_ADDRESS_REGEX)) {
|
||||
try {
|
||||
busName = await Util.getUniqueBusName(invocation.get_connection(),
|
||||
busName = await DBusUtils.getUniqueBusName(invocation.get_connection(),
|
||||
service, this._cancellable);
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
|
||||
@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env gjs -m
|
||||
|
||||
import GLib from 'gi://GLib';
|
||||
import Gio from 'gi://Gio';
|
||||
import GioUnix from 'gi://GioUnix';
|
||||
|
||||
import * as DBusUtils from '../dbusUtils.js';
|
||||
|
||||
async function seekStatusNotifierItems() {
|
||||
// Some indicators (*coff*, dropbox, *coff*) do not re-register again
|
||||
// when the plugin is enabled/disabled, thus we need to manually look
|
||||
// for the objects in the session bus that implements the StatusNotifierItem
|
||||
// interface...
|
||||
const cancellable = null;
|
||||
const bus = Gio.DBus.session;
|
||||
const uniqueNames = await DBusUtils.getBusNames(bus, cancellable);
|
||||
|
||||
const stdErrOutputStream = new GioUnix.OutputStream({fd: 1, closeFd: true});
|
||||
const introspectName = async name => {
|
||||
const nodes = DBusUtils.introspectBusObject(bus, name, cancellable,
|
||||
['org.kde.StatusNotifierItem']);
|
||||
const services = [...uniqueNames.get(name)];
|
||||
|
||||
for await (const node of nodes) {
|
||||
const {path} = node;
|
||||
stdErrOutputStream.write(`${JSON.stringify({services, name, path})}\n`,
|
||||
cancellable);
|
||||
}
|
||||
};
|
||||
await Promise.allSettled([...uniqueNames.keys()].map(n => introspectName(n)));
|
||||
}
|
||||
|
||||
function main(_argv) {
|
||||
const loop = new GLib.MainLoop(null, false);
|
||||
|
||||
let exitCode = 0;
|
||||
seekStatusNotifierItems().catch(e => {
|
||||
logError(e);
|
||||
exitCode = 1;
|
||||
}).finally(() => loop.quit());
|
||||
loop.run();
|
||||
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
imports.system.exit(main(ARGV));
|
||||
@ -23,11 +23,10 @@ import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||||
import * as Config from 'resource:///org/gnome/shell/misc/config.js';
|
||||
import * as Signals from 'resource:///org/gnome/shell/misc/signals.js';
|
||||
|
||||
import {Logger} from './logger.js';
|
||||
import {BaseStatusIcon} from './indicatorStatusIcon.js';
|
||||
import {BUS_ADDRESS_REGEX} from './dbusUtils.js';
|
||||
|
||||
export const BUS_ADDRESS_REGEX = /([a-zA-Z0-9._-]+\.[a-zA-Z0-9.-]+)|(:[0-9]+\.[0-9]+)$/;
|
||||
|
||||
Gio._promisify(Gio.DBusConnection.prototype, 'call');
|
||||
Gio._promisify(Gio._LocalFilePrototype, 'read');
|
||||
Gio._promisify(Gio.InputStream.prototype, 'read_bytes_async');
|
||||
|
||||
@ -38,101 +37,6 @@ export function indicatorId(service, busName, objectPath) {
|
||||
return `${busName}@${objectPath}`;
|
||||
}
|
||||
|
||||
export async function getUniqueBusName(bus, name, cancellable) {
|
||||
if (name[0] === ':')
|
||||
return name;
|
||||
|
||||
if (!bus)
|
||||
bus = Gio.DBus.session;
|
||||
|
||||
const variantName = new GLib.Variant('(s)', [name]);
|
||||
const [unique] = (await bus.call('org.freedesktop.DBus', '/', 'org.freedesktop.DBus',
|
||||
'GetNameOwner', variantName, new GLib.VariantType('(s)'),
|
||||
Gio.DBusCallFlags.NONE, -1, cancellable)).deep_unpack();
|
||||
|
||||
return unique;
|
||||
}
|
||||
|
||||
export async function getBusNames(bus, cancellable) {
|
||||
if (!bus)
|
||||
bus = Gio.DBus.session;
|
||||
|
||||
const [names] = (await bus.call('org.freedesktop.DBus', '/', 'org.freedesktop.DBus',
|
||||
'ListNames', null, new GLib.VariantType('(as)'), Gio.DBusCallFlags.NONE,
|
||||
-1, cancellable)).deep_unpack();
|
||||
|
||||
const uniqueNames = new Map();
|
||||
const requests = names.map(name => getUniqueBusName(bus, name, cancellable));
|
||||
const results = await Promise.allSettled(requests);
|
||||
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
const result = results[i];
|
||||
if (result.status === 'fulfilled') {
|
||||
let namesForBus = uniqueNames.get(result.value);
|
||||
if (!namesForBus) {
|
||||
namesForBus = new Set();
|
||||
uniqueNames.set(result.value, namesForBus);
|
||||
}
|
||||
namesForBus.add(result.value !== names[i] ? names[i] : null);
|
||||
} else if (!result.reason.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
|
||||
Logger.debug(`Impossible to get the unique name of ${names[i]}: ${result.reason}`);
|
||||
}
|
||||
}
|
||||
|
||||
return uniqueNames;
|
||||
}
|
||||
|
||||
async function getProcessId(connectionName, cancellable = null, bus = Gio.DBus.session) {
|
||||
const res = await bus.call('org.freedesktop.DBus', '/',
|
||||
'org.freedesktop.DBus', 'GetConnectionUnixProcessID',
|
||||
new GLib.Variant('(s)', [connectionName]),
|
||||
new GLib.VariantType('(u)'),
|
||||
Gio.DBusCallFlags.NONE,
|
||||
-1,
|
||||
cancellable);
|
||||
const [pid] = res.deepUnpack();
|
||||
return pid;
|
||||
}
|
||||
|
||||
export async function getProcessName(connectionName, cancellable = null,
|
||||
priority = GLib.PRIORITY_DEFAULT, bus = Gio.DBus.session) {
|
||||
const pid = await getProcessId(connectionName, cancellable, bus);
|
||||
const cmdFile = Gio.File.new_for_path(`/proc/${pid}/cmdline`);
|
||||
const inputStream = await cmdFile.read_async(priority, cancellable);
|
||||
const bytes = await inputStream.read_bytes_async(2048, priority, cancellable);
|
||||
const textDecoder = new TextDecoder();
|
||||
return textDecoder.decode(bytes.toArray().map(v => !v ? 0x20 : v));
|
||||
}
|
||||
|
||||
export async function* introspectBusObject(bus, name, cancellable,
|
||||
interfaces = undefined, path = undefined) {
|
||||
if (!path)
|
||||
path = '/';
|
||||
|
||||
const [introspection] = (await bus.call(name, path, 'org.freedesktop.DBus.Introspectable',
|
||||
'Introspect', null, new GLib.VariantType('(s)'), Gio.DBusCallFlags.NONE,
|
||||
5000, cancellable)).deep_unpack();
|
||||
|
||||
const nodeInfo = Gio.DBusNodeInfo.new_for_xml(introspection);
|
||||
|
||||
if (!interfaces || dbusNodeImplementsInterfaces(nodeInfo, interfaces))
|
||||
yield {nodeInfo, path};
|
||||
|
||||
if (path === '/')
|
||||
path = '';
|
||||
|
||||
for (const subNodeInfo of nodeInfo.nodes) {
|
||||
const subPath = `${path}/${subNodeInfo.path}`;
|
||||
yield* introspectBusObject(bus, name, cancellable, interfaces, subPath);
|
||||
}
|
||||
}
|
||||
|
||||
function dbusNodeImplementsInterfaces(nodeInfo, interfaces) {
|
||||
if (!(nodeInfo instanceof Gio.DBusNodeInfo) || !Array.isArray(interfaces))
|
||||
return false;
|
||||
|
||||
return interfaces.some(iface => nodeInfo.lookup_interface(iface));
|
||||
}
|
||||
|
||||
export class NameWatcher extends Signals.EventEmitter {
|
||||
constructor(name) {
|
||||
@ -268,87 +172,7 @@ export async function waitForStartupCompletion(cancellable) {
|
||||
await Main.layoutManager.connect_once('startup-complete', cancellable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class for logging stuff
|
||||
*/
|
||||
export class Logger {
|
||||
static _logStructured(logLevel, message, extraFields = {}) {
|
||||
if (!Object.values(GLib.LogLevelFlags).includes(logLevel)) {
|
||||
Logger._logStructured(GLib.LogLevelFlags.LEVEL_WARNING,
|
||||
'logLevel is not a valid GLib.LogLevelFlags');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Logger._levels.includes(logLevel))
|
||||
return;
|
||||
|
||||
let fields = {
|
||||
'SYSLOG_IDENTIFIER': this.uuid,
|
||||
'MESSAGE': `${message}`,
|
||||
};
|
||||
|
||||
let thisFile = null;
|
||||
const {stack} = new Error();
|
||||
for (let stackLine of stack.split('\n')) {
|
||||
stackLine = stackLine.replace('resource:///org/gnome/Shell/', '');
|
||||
const [code, line] = stackLine.split(':');
|
||||
const [func, file] = code.split(/@(.+)/);
|
||||
|
||||
if (!thisFile || thisFile === file) {
|
||||
thisFile = file;
|
||||
continue;
|
||||
}
|
||||
|
||||
fields = Object.assign(fields, {
|
||||
'CODE_FILE': file || '',
|
||||
'CODE_LINE': line || '',
|
||||
'CODE_FUNC': func || '',
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
GLib.log_structured(Logger._domain, logLevel, Object.assign(fields, extraFields));
|
||||
}
|
||||
|
||||
static init(extension) {
|
||||
if (Logger._domain)
|
||||
return;
|
||||
|
||||
const allLevels = Object.values(GLib.LogLevelFlags);
|
||||
const domains = GLib.getenv('G_MESSAGES_DEBUG');
|
||||
const {name: domain} = extension.metadata;
|
||||
this.uuid = extension.metadata.uuid;
|
||||
Logger._domain = domain.replaceAll(' ', '-');
|
||||
|
||||
if (domains === 'all' || (domains && domains.split(' ').includes(Logger._domain))) {
|
||||
Logger._levels = allLevels;
|
||||
} else {
|
||||
Logger._levels = allLevels.filter(
|
||||
l => l <= GLib.LogLevelFlags.LEVEL_WARNING);
|
||||
}
|
||||
}
|
||||
|
||||
static debug(message) {
|
||||
Logger._logStructured(GLib.LogLevelFlags.LEVEL_DEBUG, message);
|
||||
}
|
||||
|
||||
static message(message) {
|
||||
Logger._logStructured(GLib.LogLevelFlags.LEVEL_MESSAGE, message);
|
||||
}
|
||||
|
||||
static warn(message) {
|
||||
Logger._logStructured(GLib.LogLevelFlags.LEVEL_WARNING, message);
|
||||
}
|
||||
|
||||
static error(message) {
|
||||
Logger._logStructured(GLib.LogLevelFlags.LEVEL_ERROR, message);
|
||||
}
|
||||
|
||||
static critical(message) {
|
||||
Logger._logStructured(GLib.LogLevelFlags.LEVEL_CRITICAL, message);
|
||||
}
|
||||
}
|
||||
export {Logger};
|
||||
|
||||
export function versionCheck(required) {
|
||||
const current = Config.PACKAGE_VERSION;
|
||||
|
||||
@ -1,10 +1,58 @@
|
||||
import Meta from 'gi://Meta';
|
||||
import Gio from 'gi://Gio';
|
||||
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||||
import * as Config from 'resource:///org/gnome/shell/misc/config.js';
|
||||
|
||||
import { ApplicationsService } from '../dbus/services.js';
|
||||
import { PaintSignals } from '../conveniences/paint_signals.js';
|
||||
import { DummyPipeline } from '../conveniences/dummy_pipeline.js';
|
||||
import { Pipeline } from '../conveniences/pipeline.js';
|
||||
|
||||
|
||||
/// Converts a wildcard pattern to a RegExp object.
|
||||
/// Supports * (matches any sequence) and ? (matches any single character).
|
||||
/// Matching is case-insensitive.
|
||||
///
|
||||
/// @param {string} pattern - The wildcard pattern (e.g., "Firefox*", "*Code*")
|
||||
/// @returns {RegExp} The compiled regex pattern
|
||||
function wildcardToRegex(pattern) {
|
||||
// Escape special regex characters except * and ?
|
||||
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
||||
// Convert wildcards: * -> .*, ? -> .
|
||||
const regex = '^' + escaped.replace(/\*/g, '.*').replace(/\?/g, '.') + '$';
|
||||
return new RegExp(regex, 'i');
|
||||
}
|
||||
|
||||
|
||||
/// Compiles an array of wildcard patterns into RegExp objects.
|
||||
/// Caches the results to avoid recompilation on every check.
|
||||
///
|
||||
/// @param {string[]} patterns - Array of wildcard patterns
|
||||
/// @param {Map} cache - Cache map storing pattern -> regex mappings
|
||||
/// @returns {RegExp[]} Array of compiled regex patterns
|
||||
function compilePatterns(patterns, cache) {
|
||||
return patterns.map(pattern => {
|
||||
if (cache.has(pattern)) {
|
||||
return cache.get(pattern);
|
||||
}
|
||||
const regex = wildcardToRegex(pattern);
|
||||
cache.set(pattern, regex);
|
||||
return regex;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/// Tests if a value matches any of the compiled patterns.
|
||||
///
|
||||
/// @param {string} value - The value to test (e.g., wm_class)
|
||||
/// @param {RegExp[]} patterns - Array of compiled regex patterns
|
||||
/// @returns {boolean} True if value matches any pattern
|
||||
function matchesAnyPattern(value, patterns) {
|
||||
if (!value || patterns.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return patterns.some(pattern => pattern.test(value));
|
||||
}
|
||||
|
||||
export const ApplicationsBlur = class ApplicationsBlur {
|
||||
constructor(connections, settings, effects_manager) {
|
||||
@ -15,6 +63,27 @@ export const ApplicationsBlur = class ApplicationsBlur {
|
||||
|
||||
// stores every blurred meta window
|
||||
this.meta_window_map = new Map();
|
||||
|
||||
// cache for compiled patterns to avoid recompilation
|
||||
this._whitelist_pattern_cache = new Map();
|
||||
this._blacklist_pattern_cache = new Map();
|
||||
this._compiled_whitelist = [];
|
||||
this._compiled_blacklist = [];
|
||||
|
||||
// compile initial patterns
|
||||
this._update_patterns();
|
||||
}
|
||||
|
||||
/// Updates the compiled whitelist and blacklist patterns from settings.
|
||||
/// Called during initialization and when whitelist/blacklist settings change.
|
||||
_update_patterns() {
|
||||
const whitelist = this.settings.applications.WHITELIST || [];
|
||||
const blacklist = this.settings.applications.BLACKLIST || [];
|
||||
|
||||
this._compiled_whitelist = compilePatterns(whitelist, this._whitelist_pattern_cache);
|
||||
this._compiled_blacklist = compilePatterns(blacklist, this._blacklist_pattern_cache);
|
||||
|
||||
this._log(`Patterns updated - whitelist: ${whitelist.length}, blacklist: ${blacklist.length}`);
|
||||
}
|
||||
|
||||
enable() {
|
||||
@ -97,7 +166,7 @@ export const ApplicationsBlur = class ApplicationsBlur {
|
||||
let window_actor = meta_window.get_compositor_private();
|
||||
|
||||
if (
|
||||
!meta_window.get_workspace().active
|
||||
(!meta_window.get_workspace().active) || meta_window.minimized
|
||||
)
|
||||
window_actor.hide();
|
||||
});
|
||||
@ -108,6 +177,9 @@ export const ApplicationsBlur = class ApplicationsBlur {
|
||||
|
||||
/// Iterate through all existing windows and add blur as needed.
|
||||
update_all_windows() {
|
||||
// Recompile patterns in case whitelist/blacklist changed
|
||||
this._update_patterns();
|
||||
|
||||
// remove all previously blurred windows, in the case where the
|
||||
// whitelist was changed
|
||||
this.meta_window_map.forEach(((_meta_window, pid) => {
|
||||
@ -145,11 +217,17 @@ export const ApplicationsBlur = class ApplicationsBlur {
|
||||
_ => this.check_blur(meta_window)
|
||||
);
|
||||
|
||||
// update the position and size when the window size changes
|
||||
// update the clip, position, and/or size when the window changes
|
||||
this.connections.connect(
|
||||
meta_window, 'size-changed',
|
||||
_ => this.update_size(pid)
|
||||
);
|
||||
if (this.settings.applications.STATIC_BLUR) {
|
||||
this.connections.connect(
|
||||
meta_window, 'position-changed',
|
||||
_ => this.update_size(pid)
|
||||
);
|
||||
}
|
||||
|
||||
// remove the blur when the window is unmanaged
|
||||
this.connections.connect(
|
||||
@ -158,6 +236,18 @@ export const ApplicationsBlur = class ApplicationsBlur {
|
||||
);
|
||||
|
||||
this.check_blur(meta_window);
|
||||
|
||||
if (this.settings.applications.STATIC_BLUR && meta_window.get_client_type() === Meta.WindowClientType.X11) {
|
||||
const window_actor = meta_window.get_compositor_private();
|
||||
window_actor.connect('child-added', _ => {
|
||||
if (!meta_window.blur_actor) {
|
||||
this._warn("can't move blur actor to back, it doesn't exist");
|
||||
return;
|
||||
}
|
||||
|
||||
window_actor.set_child_below_sibling(meta_window.blur_actor, null);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the size of the blur actor associated to a meta window from its pid.
|
||||
@ -167,11 +257,39 @@ export const ApplicationsBlur = class ApplicationsBlur {
|
||||
const meta_window = this.meta_window_map.get(pid);
|
||||
const blur_actor = meta_window.blur_actor;
|
||||
if (blur_actor) {
|
||||
const allocation = this.compute_allocation(meta_window);
|
||||
blur_actor.x = allocation.x;
|
||||
blur_actor.y = allocation.y;
|
||||
blur_actor.width = allocation.width;
|
||||
blur_actor.height = allocation.height;
|
||||
if (this.settings.applications.STATIC_BLUR) {
|
||||
const bg_manager = meta_window.bg_manager;
|
||||
const bg_actor_monitor_index = bg_manager.backgroundActor.monitor;
|
||||
const window_monitor_index = meta_window.get_monitor();
|
||||
const monitor = Main.layoutManager.monitors[window_monitor_index];
|
||||
|
||||
if (bg_actor_monitor_index !== window_monitor_index) {
|
||||
this._log(`application (pid ${pid}) switching to monitor: ${window_monitor_index}`);
|
||||
|
||||
// Recreate the BackgroundActor on the right monitor. This is necessary to make sure differently
|
||||
// sized monitors have the correct scaled image of the wallpaper.
|
||||
bg_manager._monitorIndex = window_monitor_index;
|
||||
bg_manager._updateBackgroundActor();
|
||||
|
||||
// Also to fix differently sized monitor issues.
|
||||
blur_actor.width = monitor.width;
|
||||
blur_actor.height = monitor.height;
|
||||
}
|
||||
|
||||
const frame = meta_window.get_frame_rect();
|
||||
const buffer = meta_window.get_buffer_rect();
|
||||
blur_actor.x = monitor.x - buffer.x;
|
||||
blur_actor.y = monitor.y - buffer.y;
|
||||
|
||||
// set_clip(x-offset, y-offset, width, height)
|
||||
blur_actor.set_clip(frame.x - monitor.x, frame.y - monitor.y, frame.width, frame.height);
|
||||
} else {
|
||||
const allocation = this.compute_allocation(meta_window);
|
||||
blur_actor.x = allocation.x;
|
||||
blur_actor.y = allocation.y;
|
||||
blur_actor.width = allocation.width;
|
||||
blur_actor.height = allocation.height;
|
||||
}
|
||||
}
|
||||
} else
|
||||
// the pid was visibly not removed
|
||||
@ -184,11 +302,14 @@ export const ApplicationsBlur = class ApplicationsBlur {
|
||||
/// In order to be blurred, a window either:
|
||||
/// - is whitelisted in the user preferences if not enable-all
|
||||
/// - is not blacklisted if enable-all
|
||||
///
|
||||
/// Whitelist and blacklist support wildcard patterns:
|
||||
/// - * matches any sequence of characters
|
||||
/// - ? matches any single character
|
||||
/// - Matching is case-insensitive
|
||||
check_blur(meta_window) {
|
||||
const window_wm_class = meta_window.get_wm_class();
|
||||
const enable_all = this.settings.applications.ENABLE_ALL;
|
||||
const whitelist = this.settings.applications.WHITELIST;
|
||||
const blacklist = this.settings.applications.BLACKLIST;
|
||||
if (window_wm_class)
|
||||
this._log(`pid ${meta_window.bms_pid} associated to wm class name ${window_wm_class}`);
|
||||
|
||||
@ -197,8 +318,8 @@ export const ApplicationsBlur = class ApplicationsBlur {
|
||||
// or if we are in whitelist mode and the window is whitelisted
|
||||
if (
|
||||
window_wm_class !== ""
|
||||
&& ((enable_all && !blacklist.includes(window_wm_class))
|
||||
|| (!enable_all && whitelist.includes(window_wm_class))
|
||||
&& ((enable_all && !matchesAnyPattern(window_wm_class, this._compiled_blacklist))
|
||||
|| (!enable_all && matchesAnyPattern(window_wm_class, this._compiled_whitelist))
|
||||
)
|
||||
&& [
|
||||
Meta.FrameType.NORMAL,
|
||||
@ -222,23 +343,37 @@ export const ApplicationsBlur = class ApplicationsBlur {
|
||||
const pid = meta_window.bms_pid;
|
||||
const window_actor = meta_window.get_compositor_private();
|
||||
|
||||
const pipeline = new DummyPipeline(this.effects_manager, this.settings.applications);
|
||||
let [blur_actor, bg_manager] = pipeline.create_background_with_effect(
|
||||
window_actor, 'bms-application-blurred-widget'
|
||||
);
|
||||
let blur_actor;
|
||||
|
||||
if (this.settings.applications.STATIC_BLUR) {
|
||||
const pipeline = new Pipeline(this.effects_manager, global.blur_my_shell._pipelines_manager, this.settings.applications.PIPELINE);
|
||||
const bg_managers = [];
|
||||
blur_actor = pipeline.create_background_with_effects(
|
||||
meta_window.get_monitor(), bg_managers, window_actor,
|
||||
'bms-application-blurred-widget'
|
||||
);
|
||||
|
||||
if (bg_managers.length > 0) meta_window.bg_manager = bg_managers[0];
|
||||
else // I've never seen this happen, but just in case
|
||||
this._warn(`no bg_manager on blur creation for pid ${pid}`);
|
||||
} else {
|
||||
const pipeline = new DummyPipeline(this.effects_manager, this.settings.applications);
|
||||
[blur_actor, meta_window.bg_manager] = pipeline.create_background_with_effect(
|
||||
window_actor, 'bms-application-blurred-widget'
|
||||
);
|
||||
|
||||
// if hacks are selected, force to repaint the window
|
||||
if (this.settings.HACKS_LEVEL === 1) {
|
||||
this._log("hack level 1");
|
||||
|
||||
this.paint_signals.disconnect_all_for_actor(blur_actor);
|
||||
this.paint_signals.connect(blur_actor, pipeline.effect);
|
||||
} else {
|
||||
this.paint_signals.disconnect_all_for_actor(blur_actor);
|
||||
}
|
||||
}
|
||||
|
||||
meta_window.blur_actor = blur_actor;
|
||||
meta_window.bg_manager = bg_manager;
|
||||
|
||||
// if hacks are selected, force to repaint the window
|
||||
if (this.settings.HACKS_LEVEL === 1) {
|
||||
this._log("hack level 1");
|
||||
|
||||
this.paint_signals.disconnect_all_for_actor(blur_actor);
|
||||
this.paint_signals.connect(blur_actor, pipeline.effect);
|
||||
} else {
|
||||
this.paint_signals.disconnect_all_for_actor(blur_actor);
|
||||
}
|
||||
|
||||
// make sure window is blurred in overview
|
||||
if (this.settings.applications.BLUR_ON_OVERVIEW)
|
||||
@ -328,8 +463,7 @@ export const ApplicationsBlur = class ApplicationsBlur {
|
||||
) {
|
||||
window_actor.show();
|
||||
window_actor.get_last_child().hide();
|
||||
}
|
||||
else if (
|
||||
} else if (
|
||||
window_actor.visible
|
||||
)
|
||||
window_actor.get_last_child().show();
|
||||
@ -358,18 +492,29 @@ export const ApplicationsBlur = class ApplicationsBlur {
|
||||
}));
|
||||
}
|
||||
|
||||
/// Compute the size and position for a blur actor.
|
||||
/// Find the system's window scaling.
|
||||
/// If `scale-monitor-framebuffer` experimental feature if on, we don't need to manage scaling.
|
||||
/// Else, on wayland, we need to divide by the scale to get the correct result.
|
||||
compute_allocation(meta_window) {
|
||||
const scale_monitor_framebuffer = this.mutter_gsettings.get_strv('experimental-features')
|
||||
.includes('scale-monitor-framebuffer');
|
||||
const is_wayland = Meta.is_wayland_compositor();
|
||||
compute_scale(meta_window) {
|
||||
// TODO: Drop GNOME <50 compatibility
|
||||
const gnome_shell_major_version = parseInt(Config.PACKAGE_VERSION.split('.')[0]);
|
||||
const scale_monitor_framebuffer =
|
||||
gnome_shell_major_version >= 50 ||
|
||||
this.mutter_gsettings
|
||||
.get_strv('experimental-features')
|
||||
.includes('scale-monitor-framebuffer');
|
||||
const is_wayland = gnome_shell_major_version >= 50 || Meta.is_wayland_compositor();
|
||||
const monitor_index = meta_window.get_monitor();
|
||||
// check if the window is using wayland, or xwayland/xorg for rendering
|
||||
const scale = !scale_monitor_framebuffer && is_wayland && meta_window.get_client_type() == 0
|
||||
return !scale_monitor_framebuffer && is_wayland && meta_window.get_client_type() == 0
|
||||
? Main.layoutManager.monitors[monitor_index].geometry_scale
|
||||
: 1;
|
||||
}
|
||||
|
||||
/// Compute the size and position for a blur actor.
|
||||
/// Coordinates are relative to window buffer's corner.
|
||||
compute_allocation(meta_window) {
|
||||
const scale = this.compute_scale(meta_window);
|
||||
|
||||
let frame = meta_window.get_frame_rect();
|
||||
let buffer = meta_window.get_buffer_rect();
|
||||
@ -382,6 +527,17 @@ export const ApplicationsBlur = class ApplicationsBlur {
|
||||
};
|
||||
}
|
||||
|
||||
change_blur_type() {
|
||||
this._log("resetting...");
|
||||
|
||||
this.disable();
|
||||
setTimeout(_ => this.enable(), 1);
|
||||
}
|
||||
|
||||
change_pipeline() {
|
||||
this.update_all_windows();
|
||||
}
|
||||
|
||||
/// Removes the blur actor to make a blurred window become normal again.
|
||||
/// It however does not untrack the meta window itself.
|
||||
/// Accepts a pid corresponding (or not) to a blurred (or not) meta window.
|
||||
@ -448,4 +604,8 @@ export const ApplicationsBlur = class ApplicationsBlur {
|
||||
if (this.settings.DEBUG)
|
||||
console.log(`[Blur my Shell > applications] ${str}`);
|
||||
}
|
||||
|
||||
_warn(str) {
|
||||
console.warn(`[Blur my Shell > applications] ${str}`);
|
||||
}
|
||||
};
|
||||
|
||||
@ -66,7 +66,7 @@ export const CoverflowAltTabBlur = class CoverflowAltTabBlur {
|
||||
}
|
||||
|
||||
remove_background_actors() {
|
||||
this.background_actors.forEach((actor) => actor.destroy);
|
||||
this.background_actors.forEach((actor) => actor.destroy());
|
||||
this.background_actors = [];
|
||||
|
||||
this.background_managers.forEach((background_manager) => {
|
||||
|
||||
@ -16,6 +16,9 @@ const PANEL_STYLES = [
|
||||
"contrasted-panel"
|
||||
];
|
||||
|
||||
// global listener, so we don't miss the panel destruction event
|
||||
let isMainPanelAlive = true;
|
||||
Main.panel.connect('destroy', () => isMainPanelAlive = false);
|
||||
|
||||
export const PanelBlur = class PanelBlur {
|
||||
constructor(connections, settings, effects_manager) {
|
||||
@ -28,6 +31,11 @@ export const PanelBlur = class PanelBlur {
|
||||
}
|
||||
|
||||
enable() {
|
||||
if (this.enabled) {
|
||||
this._log("blur already enabled");
|
||||
return;
|
||||
}
|
||||
|
||||
this._log("blurring top panel");
|
||||
|
||||
// check for panels when Dash to Panel is activated
|
||||
@ -64,6 +72,11 @@ export const PanelBlur = class PanelBlur {
|
||||
}
|
||||
|
||||
reset() {
|
||||
if (!this.enabled) {
|
||||
this._log("reset called but blur is not enabled");
|
||||
return;
|
||||
}
|
||||
|
||||
this._log("resetting...");
|
||||
|
||||
this.disable();
|
||||
@ -77,6 +90,10 @@ export const PanelBlur = class PanelBlur {
|
||||
// blur already existing ones
|
||||
if (global.dashToPanel.panels)
|
||||
this.blur_dtp_panels();
|
||||
|
||||
// blur main panel if requested in settings
|
||||
if (this.settings.dash_to_panel.BLUR_ORIGINAL_PANEL && isMainPanelAlive)
|
||||
this.maybe_blur_panel(Main.panel);
|
||||
} else {
|
||||
// if no dash-to-panel, blur the main and only panel
|
||||
this.maybe_blur_panel(Main.panel);
|
||||
@ -95,21 +112,12 @@ export const PanelBlur = class PanelBlur {
|
||||
|
||||
this._log("Blurring Dash to Panel panels after idle.");
|
||||
|
||||
// blur every panel found
|
||||
// blur every panel, except Main.panel (this is handled in blur_existing_panels() method above)
|
||||
global.dashToPanel.panels.forEach(p => {
|
||||
this.maybe_blur_panel(p.panel);
|
||||
if (p.panel != Main.panel)
|
||||
this.maybe_blur_panel(p.panel);
|
||||
});
|
||||
|
||||
// if main panel is not included in the previous panels, blur it
|
||||
if (
|
||||
!global.dashToPanel.panels
|
||||
.map(p => p.panel)
|
||||
.includes(Main.panel)
|
||||
&&
|
||||
this.settings.dash_to_panel.BLUR_ORIGINAL_PANEL
|
||||
)
|
||||
this.maybe_blur_panel(Main.panel);
|
||||
|
||||
return GLib.SOURCE_REMOVE;
|
||||
});
|
||||
};
|
||||
@ -377,6 +385,11 @@ export const PanelBlur = class PanelBlur {
|
||||
|
||||
/// Update the css classname of the panel for light theme
|
||||
update_light_text_classname(disable = false) {
|
||||
if (!isMainPanelAlive) {
|
||||
this._log("cannot update light text classname, Main.panel is not alive");
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.settings.panel.FORCE_LIGHT_TEXT && !disable)
|
||||
Main.panel.add_style_class_name("panel-light-text");
|
||||
else
|
||||
@ -408,7 +421,7 @@ export const PanelBlur = class PanelBlur {
|
||||
/// Update the visibility of the blur effect
|
||||
update_visibility() {
|
||||
if (
|
||||
Main.panel.has_style_pseudo_class('overview')
|
||||
isMainPanelAlive && Main.panel.has_style_pseudo_class('overview')
|
||||
|| !Main.sessionMode.hasWindows
|
||||
) {
|
||||
this.actors_list.forEach(
|
||||
@ -533,6 +546,11 @@ export const PanelBlur = class PanelBlur {
|
||||
}
|
||||
|
||||
disable() {
|
||||
if (!this.enabled) {
|
||||
this._log("blur already removed");
|
||||
return;
|
||||
}
|
||||
|
||||
this._log("removing blur from top panel");
|
||||
|
||||
this.disconnect_from_windows_and_overview();
|
||||
|
||||
@ -53,6 +53,8 @@ export const KEYS = [
|
||||
{
|
||||
component: "applications", schemas: [
|
||||
{ type: Type.B, name: "blur" },
|
||||
{ type: Type.B, name: "static-blur" },
|
||||
{ type: Type.S, name: "pipeline" },
|
||||
{ type: Type.I, name: "sigma" },
|
||||
{ type: Type.D, name: "brightness" },
|
||||
{ type: Type.I, name: "opacity" },
|
||||
|
||||
@ -3,11 +3,109 @@ uniform float red;
|
||||
uniform float green;
|
||||
uniform float blue;
|
||||
uniform float blend;
|
||||
uniform int mode;
|
||||
|
||||
const int NORMAL = 0;
|
||||
const int MULTIPLY = 1;
|
||||
const int SCREEN = 2;
|
||||
const int OVERLAY = 3;
|
||||
const int DARKEN = 4;
|
||||
const int LIGHTEN = 5;
|
||||
const int PLUS_DARKER = 6;
|
||||
const int PLUS_LIGHTER = 7;
|
||||
const int COLOR_DODGE = 8;
|
||||
const int COLOR_BURN = 9;
|
||||
const int HARD_LIGHT = 10;
|
||||
const int SOFT_LIGHT = 11;
|
||||
const int DIFFERENCE = 12;
|
||||
const int EXCLUSION = 13;
|
||||
const int HUE = 14;
|
||||
const int SATURATION = 15;
|
||||
const int COLOR = 16;
|
||||
const int LUMINOSITY = 17;
|
||||
|
||||
vec3 rgb_to_hsl(vec3 c) {
|
||||
vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
|
||||
vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
|
||||
vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
|
||||
|
||||
float d = q.x - min(q.w, q.y);
|
||||
float e = 1.0e-10;
|
||||
return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
|
||||
}
|
||||
|
||||
vec3 hsl_to_rgb(vec3 c) {
|
||||
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
|
||||
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
|
||||
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
|
||||
}
|
||||
|
||||
float soft_light_channel(float base, float _blend) {
|
||||
if (_blend < 0.5) {
|
||||
return base - (1.0 - 2.0 * _blend) * base * (1.0 - base);
|
||||
} else {
|
||||
float d = (base < 0.25)
|
||||
? ((16.0 * base - 12.0) * base + 4.0) * base
|
||||
: sqrt(base);
|
||||
return base + (2.0 * _blend - 1.0) * (d - base);
|
||||
}
|
||||
}
|
||||
|
||||
vec3 get_blend(vec3 base, vec3 _blend) {
|
||||
if (mode == MULTIPLY) return base * _blend;
|
||||
if (mode == SCREEN) return 1 - (1 - base) * (1 - _blend);
|
||||
if (mode == OVERLAY) {
|
||||
vec3 result;
|
||||
result.r = base.r < 0.5 ? (2.0 * base.r * _blend.r) : (1.0 - 2.0 * (1.0 - base.r) * (1.0 - _blend.r));
|
||||
result.g = base.g < 0.5 ? (2.0 * base.g * _blend.g) : (1.0 - 2.0 * (1.0 - base.g) * (1.0 - _blend.g));
|
||||
result.b = base.b < 0.5 ? (2.0 * base.b * _blend.b) : (1.0 - 2.0 * (1.0 - base.b) * (1.0 - _blend.b));
|
||||
return result;
|
||||
}
|
||||
if (mode == DARKEN) return min(base, _blend);
|
||||
if (mode == LIGHTEN) return max(base, _blend);
|
||||
if (mode == PLUS_DARKER) return base + _blend - 1;
|
||||
if (mode == PLUS_LIGHTER) return base + _blend;
|
||||
if (mode == COLOR_DODGE) return base / (1 - _blend);
|
||||
if (mode == COLOR_BURN) return 1 - (1 - base) / _blend;
|
||||
if (mode == HARD_LIGHT) {
|
||||
vec3 result;
|
||||
result.r = _blend.r < 0.5 ? (2.0 * base.r * _blend.r) : (1.0 - 2.0 * (1.0 - base.r) * (1.0 - _blend.r));
|
||||
result.g = _blend.g < 0.5 ? (2.0 * base.g * _blend.g) : (1.0 - 2.0 * (1.0 - base.g) * (1.0 - _blend.g));
|
||||
result.b = _blend.b < 0.5 ? (2.0 * base.b * _blend.b) : (1.0 - 2.0 * (1.0 - base.b) * (1.0 - _blend.b));
|
||||
return result;
|
||||
}
|
||||
if (mode == SOFT_LIGHT) {
|
||||
return vec3(soft_light_channel(base.r, _blend.r), soft_light_channel(base.g, _blend.g), soft_light_channel(base.b, _blend.b));
|
||||
}
|
||||
if (mode == DIFFERENCE) return abs(base - _blend);
|
||||
if (mode == EXCLUSION) return 0.5 - 2 * (base - 0.5) * (_blend - 0.5);
|
||||
if (mode == HUE) {
|
||||
vec3 base_hsl = rgb_to_hsl(base);
|
||||
vec3 blend_hsl = rgb_to_hsl(_blend);
|
||||
return hsl_to_rgb(vec3(blend_hsl.x, base_hsl.y, base_hsl.z));
|
||||
}
|
||||
if (mode == SATURATION) {
|
||||
vec3 base_hsl = rgb_to_hsl(base);
|
||||
vec3 blend_hsl = rgb_to_hsl(_blend);
|
||||
return hsl_to_rgb(vec3(base_hsl.x, blend_hsl.y, base_hsl.z));
|
||||
}
|
||||
if (mode == COLOR) {
|
||||
vec3 base_hsl = rgb_to_hsl(base);
|
||||
vec3 blend_hsl = rgb_to_hsl(_blend);
|
||||
return hsl_to_rgb(vec3(blend_hsl.x, blend_hsl.y, base_hsl.z));
|
||||
}
|
||||
if (mode == LUMINOSITY) {
|
||||
vec3 base_hsl = rgb_to_hsl(base);
|
||||
vec3 blend_hsl = rgb_to_hsl(_blend);
|
||||
return hsl_to_rgb(vec3(base_hsl.x, base_hsl.y, blend_hsl.z));
|
||||
}
|
||||
return _blend; // For NORMAL
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec4 c = texture2D(tex, cogl_tex_coord_in[0].st);
|
||||
vec3 pix_color = c.xyz;
|
||||
vec3 color = vec3(red, green, blue);
|
||||
vec3 color = get_blend(pix_color, vec3(red, green, blue));
|
||||
|
||||
cogl_color_out = vec4(mix(pix_color, color, blend), 1.);
|
||||
}
|
||||
@ -1,12 +1,14 @@
|
||||
import GObject from 'gi://GObject';
|
||||
|
||||
import * as utils from '../conveniences/utils.js';
|
||||
|
||||
const Shell = await utils.import_in_shell_only('gi://Shell');
|
||||
const Clutter = await utils.import_in_shell_only('gi://Clutter');
|
||||
|
||||
const SHADER_FILENAME = 'color.glsl';
|
||||
const DEFAULT_PARAMS = {
|
||||
color: [0.0, 0.0, 0.0, 0.0]
|
||||
color: [0.0, 0.0, 0.0, 0.0],
|
||||
blend_mode: 0
|
||||
};
|
||||
|
||||
|
||||
@ -47,7 +49,18 @@ export const ColorEffect = utils.IS_IN_PREFERENCES ?
|
||||
0.0, 1.0,
|
||||
0.0,
|
||||
),
|
||||
'blend_mode': GObject.ParamSpec.int(
|
||||
`blend_mode`,
|
||||
`Blend mode`,
|
||||
`Blend mode`,
|
||||
GObject.ParamFlags.READWRITE,
|
||||
0, 17,
|
||||
0,
|
||||
)
|
||||
}
|
||||
// Normal (0), Multiply (1), Screen (2), Overlay (3), Darken (4), Lighten (5), Plus darker (6), Plus lighter (7), Color dodge (8),
|
||||
// Color burn (9), Hard light (10), Soft light (11), Difference (12), Exclusion (13), Hue (14), Saturation (15), Color (16),
|
||||
// Luminosity (17)
|
||||
}, class ColorEffect extends Clutter.ShaderEffect {
|
||||
constructor(params) {
|
||||
// initialize without color as a parameter
|
||||
@ -58,14 +71,16 @@ export const ColorEffect = utils.IS_IN_PREFERENCES ?
|
||||
this._green = null;
|
||||
this._blue = null;
|
||||
this._blend = null;
|
||||
this._blend_mode = null;
|
||||
|
||||
// set shader source
|
||||
this._source = utils.get_shader_source(Shell, SHADER_FILENAME, import.meta.url);
|
||||
if (this._source)
|
||||
this.set_shader_source(this._source);
|
||||
|
||||
// set shader color
|
||||
// set params; utils.setup_params doesn't work here with color
|
||||
this.color = 'color' in params ? color : this.constructor.default_params.color;
|
||||
this.blend_mode = 'blend_mode' in params ? params.blend_mode : this.constructor.default_params.blend_mode;
|
||||
}
|
||||
|
||||
static get default_params() {
|
||||
@ -121,6 +136,18 @@ export const ColorEffect = utils.IS_IN_PREFERENCES ?
|
||||
}
|
||||
}
|
||||
|
||||
get blend_mode() {
|
||||
return this._blend_mode;
|
||||
}
|
||||
|
||||
set blend_mode(value) {
|
||||
if (this._blend_mode !== value) {
|
||||
this._blend_mode = value;
|
||||
|
||||
this.set_uniform_value('mode', this._blend_mode);
|
||||
}
|
||||
}
|
||||
|
||||
set color(rgba) {
|
||||
let [r, g, b, a] = rgba;
|
||||
this.red = r;
|
||||
@ -136,5 +163,6 @@ export const ColorEffect = utils.IS_IN_PREFERENCES ?
|
||||
/// False set function, only cares about the color. Too hard to change.
|
||||
set(params) {
|
||||
this.color = params.color;
|
||||
this.blend_mode = params.blend_mode;
|
||||
}
|
||||
});
|
||||
@ -168,6 +168,31 @@ export function get_supported_effects(_ = () => "") {
|
||||
name: _("Color"),
|
||||
description: _("The color to blend in. The blending amount is controled by the opacity of the color."),
|
||||
type: "rgba"
|
||||
},
|
||||
blend_mode: {
|
||||
name: _("Blend mode"),
|
||||
description: _("How the color is blended in."),
|
||||
type: "dropdown",
|
||||
options: [
|
||||
_("Normal"),
|
||||
_("Multiply"),
|
||||
_("Screen"),
|
||||
_("Overlay"),
|
||||
_("Darken"),
|
||||
_("Lighten"),
|
||||
_("Plus darker"),
|
||||
_("Plus lighter"),
|
||||
_("Color dodge"),
|
||||
_("Color burn"),
|
||||
_("Hard light"),
|
||||
_("Soft light"),
|
||||
_("Difference"),
|
||||
_("Exclusion"),
|
||||
_("Hue"),
|
||||
_("Saturation"),
|
||||
_("Color"),
|
||||
_("Luminosity")
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -476,6 +476,18 @@ export default class BlurMyShell extends Extension {
|
||||
this._applications_blur.disable();
|
||||
});
|
||||
|
||||
// static blur toggled on/off
|
||||
this._settings.applications.STATIC_BLUR_changed(() => {
|
||||
if (this._settings.applications.BLUR)
|
||||
this._applications_blur.change_blur_type();
|
||||
});
|
||||
|
||||
// pipeline changed
|
||||
this._settings.applications.PIPELINE_changed(() => {
|
||||
if (this._settings.applications.BLUR)
|
||||
this._applications_blur.change_pipeline();
|
||||
});
|
||||
|
||||
// application opacity changed
|
||||
this._settings.applications.OPACITY_changed(() => {
|
||||
if (this._settings.applications.BLUR)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -19,9 +19,10 @@
|
||||
"46",
|
||||
"47",
|
||||
"48",
|
||||
"49"
|
||||
"49",
|
||||
"50"
|
||||
],
|
||||
"url": "https://github.com/aunetx/blur-my-shell",
|
||||
"uuid": "blur-my-shell@aunetx",
|
||||
"version": 70
|
||||
"version": 71
|
||||
}
|
||||
@ -30,7 +30,12 @@ export const Applications = GObject.registerClass({
|
||||
Template: GLib.uri_resolve_relative(import.meta.url, '../ui/applications.ui', GLib.UriFlags.NONE),
|
||||
InternalChildren: [
|
||||
'blur',
|
||||
'pipeline_choose_row',
|
||||
'mode_static',
|
||||
'mode_dynamic',
|
||||
'sigma_row',
|
||||
'sigma',
|
||||
'brightness_row',
|
||||
'brightness',
|
||||
'opacity',
|
||||
'dynamic_opacity',
|
||||
@ -42,16 +47,32 @@ export const Applications = GObject.registerClass({
|
||||
'add_window_blacklist'
|
||||
],
|
||||
}, class Applications extends Adw.PreferencesPage {
|
||||
constructor(preferences, preferences_window) {
|
||||
constructor(preferences, preferences_window, pipelines_manager, pipelines_page) {
|
||||
super({});
|
||||
this._preferences_window = preferences_window;
|
||||
|
||||
this.preferences = preferences;
|
||||
this.pipelines_manager = pipelines_manager;
|
||||
this.pipelines_page = pipelines_page;
|
||||
|
||||
this.preferences.applications.settings.bind(
|
||||
'blur', this._blur, 'active',
|
||||
Gio.SettingsBindFlags.DEFAULT
|
||||
);
|
||||
|
||||
this._pipeline_choose_row.initialize(
|
||||
this.preferences.applications, this.pipelines_manager, this.pipelines_page
|
||||
);
|
||||
|
||||
this.change_blur_mode(this.preferences.applications.STATIC_BLUR, true);
|
||||
|
||||
this._mode_static.connect('toggled',
|
||||
() => this.preferences.applications.STATIC_BLUR = this._mode_static.active
|
||||
);
|
||||
this.preferences.applications.STATIC_BLUR_changed(
|
||||
() => this.change_blur_mode(this.preferences.applications.STATIC_BLUR, false)
|
||||
);
|
||||
|
||||
this.preferences.applications.settings.bind(
|
||||
'opacity', this._opacity, 'value',
|
||||
Gio.SettingsBindFlags.DEFAULT
|
||||
@ -183,4 +204,14 @@ export const Applications = GObject.registerClass({
|
||||
this._blacklist.remove(widget);
|
||||
this.update_blacklist_titles();
|
||||
}
|
||||
|
||||
change_blur_mode(is_static_blur, first_run) {
|
||||
this._mode_static.set_active(is_static_blur);
|
||||
if (first_run)
|
||||
this._mode_dynamic.set_active(!is_static_blur);
|
||||
|
||||
this._pipeline_choose_row.set_visible(is_static_blur);
|
||||
this._sigma_row.set_visible(!is_static_blur);
|
||||
this._brightness_row.set_visible(!is_static_blur);
|
||||
}
|
||||
});
|
||||
@ -43,7 +43,7 @@ export default class BlurMyShellPreferences extends ExtensionPreferences {
|
||||
window.add(new Panel(preferences, pipelines_manager, pipelines_page));
|
||||
window.add(new Overview(preferences, pipelines_manager, pipelines_page));
|
||||
window.add(new Dash(preferences, pipelines_manager, pipelines_page));
|
||||
window.add(new Applications(preferences, window));
|
||||
window.add(new Applications(preferences, window, pipelines_manager, pipelines_page));
|
||||
window.add(new Other(preferences, pipelines_manager, pipelines_page));
|
||||
|
||||
window.search_enabled = true;
|
||||
|
||||
Binary file not shown.
@ -351,6 +351,16 @@
|
||||
<default>false</default>
|
||||
<summary>Boolean, whether to blur activate the blur for this component or not</summary>
|
||||
</key>
|
||||
<!-- PIPELINE -->
|
||||
<key type="s" name="pipeline">
|
||||
<default>"pipeline_default"</default>
|
||||
<summary>String, the name of the pipeline to use. It must exist, else "pipeline_default" will be used</summary>
|
||||
</key>
|
||||
<!-- STATIC BLUR -->
|
||||
<key type="b" name="static-blur">
|
||||
<default>false</default>
|
||||
<summary>Boolean, whether to use static or dynamic blur for this component</summary>
|
||||
</key>
|
||||
<!-- CUSTOMIZE -->
|
||||
<key type="b" name="customize">
|
||||
<default>true</default>
|
||||
|
||||
@ -18,6 +18,57 @@ To get the best results possible, although with reduced performances, you can ch
|
||||
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<property name="title" translatable="yes">Blur type</property>
|
||||
<property name="subtitle" translatable="yes">The dynamic blur is slower and only compatible with a gaussian blur effect, but shows content behind windows.</property>
|
||||
<property name="activatable-widget">blur_mode_choose</property>
|
||||
<property name="sensitive" bind-source="blur" bind-property="state" bind-flags="sync-create" />
|
||||
|
||||
<child>
|
||||
<object class="GtkBox" id="blur_mode_choose">
|
||||
<property name="valign">center</property>
|
||||
<property name="hexpand">false</property>
|
||||
<style>
|
||||
<class name="linked" />
|
||||
</style>
|
||||
|
||||
<child>
|
||||
<object class="GtkToggleButton" id="mode_static">
|
||||
<property name="valign">center</property>
|
||||
<property name="hexpand">true</property>
|
||||
<property name="group">mode_dynamic</property>
|
||||
<property name="child">
|
||||
<object class="AdwButtonContent">
|
||||
<property name="icon-name">static-mode-symbolic</property>
|
||||
<property name="label" translatable="yes">Static</property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToggleButton" id="mode_dynamic">
|
||||
<property name="valign">center</property>
|
||||
<property name="hexpand">true</property>
|
||||
<property name="child">
|
||||
<object class="AdwButtonContent">
|
||||
<property name="icon-name">dynamic-mode-symbolic</property>
|
||||
<property name="label" translatable="yes">Dynamic</property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
|
||||
<child>
|
||||
<object class="PipelineChooseRow" id="pipeline_choose_row">
|
||||
<property name="sensitive" bind-source="blur" bind-property="state" bind-flags="sync-create" />
|
||||
</object>
|
||||
</child>
|
||||
|
||||
<child>
|
||||
<object class="AdwActionRow" id="sigma_row">
|
||||
<property name="title" translatable="yes">Sigma</property>
|
||||
<property name="subtitle" translatable="yes">The intensity of the blur.</property>
|
||||
<property name="activatable-widget">sigma_scale</property>
|
||||
@ -38,7 +89,7 @@ To get the best results possible, although with reduced performances, you can ch
|
||||
</child>
|
||||
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<object class="AdwActionRow" id="brightness_row">
|
||||
<property name="title" translatable="yes">Brightness</property>
|
||||
<property name="subtitle" translatable="yes">The brightness of the blur effect, a high value might make the text harder to read.</property>
|
||||
<property name="activatable-widget">brightness_scale</property>
|
||||
@ -131,7 +182,8 @@ This may cause some latency or performance issues.</property>
|
||||
<child>
|
||||
<object class="AdwPreferencesGroup" id="whitelist">
|
||||
<property name="title" translatable="yes">Whitelist</property>
|
||||
<property name="description" translatable="yes">A list of windows to blur.</property>
|
||||
<property name="description" translatable="yes">A list of windows to blur.
|
||||
Use * to match any sequence of characters (e.g., Firefox* or *Code*), and ? for a single one.</property>
|
||||
<property name="sensitive" bind-source="blur" bind-property="state" bind-flags="sync-create" />
|
||||
<property name="header-suffix">
|
||||
<object class="GtkButton" id="add_window_whitelist">
|
||||
@ -168,7 +220,8 @@ This may cause some latency or performance issues.</property>
|
||||
<child>
|
||||
<object class="AdwPreferencesGroup" id="blacklist">
|
||||
<property name="title" translatable="yes">Blacklist</property>
|
||||
<property name="description" translatable="yes">A list of windows not to blur.</property>
|
||||
<property name="description" translatable="yes">A list of windows not to blur.
|
||||
Use * to match any sequence of characters (e.g., Firefox* or *Code*), and ? for a single one.</property>
|
||||
<property name="sensitive" bind-source="blur" bind-property="state" bind-flags="sync-create" />
|
||||
<property name="header-suffix">
|
||||
<object class="GtkButton" id="add_window_blacklist">
|
||||
|
||||
@ -8,9 +8,10 @@
|
||||
"46",
|
||||
"47",
|
||||
"48",
|
||||
"49"
|
||||
"49",
|
||||
"50"
|
||||
],
|
||||
"url": "http://gfxmonk.net/dist/0install/gnome-shell-impatience.xml",
|
||||
"uuid": "impatience@gfxmonk.net",
|
||||
"version": 29
|
||||
"version": 30
|
||||
}
|
||||
@ -14,6 +14,14 @@ function getObjectLabel(name, values) {
|
||||
.map(([label, value]) => `${label}: '${value}'`);
|
||||
return `${name}(${labels.join(", ")})`;
|
||||
}
|
||||
function safeRegexTest(pattern, value) {
|
||||
try {
|
||||
return new RegExp(pattern).test(value);
|
||||
}
|
||||
catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
function getWindowLabel(window) {
|
||||
return getObjectLabel("Window", {
|
||||
["Title"]: window.title,
|
||||
@ -46,7 +54,11 @@ export default class JunkNotificationCleaner extends Extension {
|
||||
this.log(LogLevel.DEBUG, `${windowLabel}: received ${event}`);
|
||||
const excludedApps = this.settings.get_strv("excluded-apps");
|
||||
for (const wmClassPattern of excludedApps) {
|
||||
if (new RegExp(wmClassPattern).test(window.wmClass)) {
|
||||
const result = safeRegexTest(wmClassPattern, window.wmClass);
|
||||
if (result === null) {
|
||||
this.log(LogLevel.WARN, `${windowLabel}: invalid regex '${wmClassPattern}'`);
|
||||
}
|
||||
else if (result) {
|
||||
this.log(LogLevel.DEBUG, `${windowLabel}: excluded by '${wmClassPattern}'`);
|
||||
return;
|
||||
}
|
||||
@ -78,9 +90,11 @@ export default class JunkNotificationCleaner extends Extension {
|
||||
disable() {
|
||||
if (this.focusListenerId !== null) {
|
||||
global.display.disconnect(this.focusListenerId);
|
||||
this.focusListenerId = null;
|
||||
}
|
||||
if (this.closeListenerId !== null) {
|
||||
global.window_manager.disconnect(this.closeListenerId);
|
||||
this.closeListenerId = null;
|
||||
}
|
||||
if (this.settings) {
|
||||
this.settings = null;
|
||||
|
||||
@ -11,5 +11,5 @@
|
||||
],
|
||||
"url": "https://github.com/murar8/junk-notification-cleaner",
|
||||
"uuid": "junk-notification-cleaner@murar8.github.com",
|
||||
"version": 6
|
||||
"version": 10
|
||||
}
|
||||
@ -3,6 +3,15 @@ import Gio from "gi://Gio";
|
||||
import Gtk from "gi://Gtk";
|
||||
import { ExtensionPreferences } from "resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js";
|
||||
const LOG_LEVELS = ["debug", "info", "warn", "error"];
|
||||
function isValidRegex(pattern) {
|
||||
try {
|
||||
new RegExp(pattern);
|
||||
return true;
|
||||
}
|
||||
catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
export default class JunkNotificationCleanerPreferences extends ExtensionPreferences {
|
||||
async fillPreferencesWindow(window) {
|
||||
const settings = this.getSettings();
|
||||
@ -90,24 +99,43 @@ export default class JunkNotificationCleanerPreferences extends ExtensionPrefere
|
||||
placeholder_text: "Enter WM Class regex (e.g. .*firefox.*)",
|
||||
hexpand: true,
|
||||
});
|
||||
const errorLabel = new Gtk.Label({
|
||||
label: "Invalid regular expression",
|
||||
css_classes: ["error"],
|
||||
xalign: 0,
|
||||
visible: false,
|
||||
});
|
||||
const addButton = new Gtk.Button({
|
||||
label: "Add",
|
||||
css_classes: ["suggested-action"],
|
||||
});
|
||||
entry.connect("changed", () => {
|
||||
entry.remove_css_class("error");
|
||||
errorLabel.set_visible(false);
|
||||
});
|
||||
addButton.connect("clicked", () => {
|
||||
const text = entry.get_text().trim();
|
||||
if (!text)
|
||||
return;
|
||||
const currentApps = settings.get_strv("excluded-apps");
|
||||
if (currentApps.includes(text))
|
||||
return;
|
||||
settings.set_strv("excluded-apps", [...currentApps, text]);
|
||||
this.addExcludedAppRow(text, listBox, settings);
|
||||
entry.set_text("");
|
||||
if (!isValidRegex(text)) {
|
||||
entry.add_css_class("error");
|
||||
errorLabel.set_visible(true);
|
||||
}
|
||||
else {
|
||||
entry.remove_css_class("error");
|
||||
errorLabel.set_visible(false);
|
||||
const currentApps = settings.get_strv("excluded-apps");
|
||||
if (currentApps.includes(text))
|
||||
return;
|
||||
settings.set_strv("excluded-apps", [...currentApps, text]);
|
||||
this.addExcludedAppRow(text, listBox, settings);
|
||||
entry.set_text("");
|
||||
}
|
||||
});
|
||||
addBox.append(entry);
|
||||
addBox.append(addButton);
|
||||
excludedBox.append(addBox);
|
||||
excludedBox.append(errorLabel);
|
||||
excludedGroup.add(excludedBox);
|
||||
}
|
||||
addExcludedAppRow(app, listBox, settings) {
|
||||
|
||||
@ -9,10 +9,10 @@
|
||||
"46",
|
||||
"47",
|
||||
"48",
|
||||
"49"
|
||||
"49",
|
||||
"50"
|
||||
],
|
||||
"stylesheet": "stylesheet.css",
|
||||
"url": "https://github.com/comitanigiacomo/quicklaunch",
|
||||
"uuid": "quicklaunch@comitanigiacomo.github.com",
|
||||
"version": 15
|
||||
"version": 16
|
||||
}
|
||||
Binary file not shown.
@ -80,8 +80,9 @@ class ManualOrientationMenuToggle extends QuickMenuToggle {
|
||||
this._section.addMenuItem(this.portraitRightItem);
|
||||
|
||||
this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
|
||||
this.menu.addSettingsAction(_('Extension Settings'),
|
||||
'com.mattjakeman.ExtensionManager.desktop');
|
||||
const settingsItem = new PopupMenu.PopupMenuItem(_('Extension Settings'));
|
||||
settingsItem.connect('activate', () => ext.openPreferences());
|
||||
this.menu.addMenuItem(settingsItem);
|
||||
|
||||
this.connect('clicked', () => {
|
||||
if (this.checked === true) {
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
{
|
||||
"_generated": "Generated by SweetTooth, do not edit",
|
||||
"commit": "d22c7f03313b06b2af6b9d7becfef9f248351dc5",
|
||||
"description": "Enable screen rotation regardless of touch mode. Fork of Screen Autorotate by Kosmospredanie.",
|
||||
"donations": {
|
||||
"github": "shyzus"
|
||||
@ -17,9 +16,10 @@
|
||||
"46",
|
||||
"47",
|
||||
"48",
|
||||
"49"
|
||||
"49",
|
||||
"50"
|
||||
],
|
||||
"url": "https://github.com/shyzus/gnome-shell-extension-screen-autorotate",
|
||||
"uuid": "screen-rotate@shyzus.github.io",
|
||||
"version": 28
|
||||
"version": 29
|
||||
}
|
||||
Reference in New Issue
Block a user