diff --git a/extension.js b/extension.js
index de33e4e..6f86758 100644
--- a/extension.js
+++ b/extension.js
@@ -301,6 +301,8 @@ var VitalsMenuButton = GObject.registerClass({
this._warnings = [];
this._isDestroyed = false;
this._sensorMenuItems = {};
+ this._availableSensors = {};
+ this._availableSensorsSig = '';
this._hotLabels = {};
this._hotItems = {};
this._groups = {};
@@ -343,6 +345,7 @@ var VitalsMenuButton = GObject.registerClass({
'changed::position-in-panel', this._positionInPanelChanged.bind(this),
'changed::menu-centered', this._positionInPanelChanged.bind(this),
'changed::icon-style', this._iconStyleChanged.bind(this),
+ 'changed::hot-sensors', this._hotSensorsChanged.bind(this),
this);
let settings = [ 'use-higher-precision', 'alphabetize', 'hide-zeros',
@@ -498,6 +501,40 @@ var VitalsMenuButton = GObject.registerClass({
));
}
+ _hotSensorsChanged() {
+ // 'hot-sensors' changed. When the change came from the dropdown the
+ // panel already matches, so only redraw when it actually differs from
+ // what is currently shown (e.g. the change came from the preferences
+ // sensor selector, in this same or another process). Comparing sets is
+ // timing-independent, unlike a flag (the 'changed' signal is async).
+ let desired = this._settings.get_strv('hot-sensors');
+ let current = Object.keys(this._hotItems);
+ if (desired.length === current.length && desired.every(k => current.includes(k)))
+ return;
+
+ this._redrawMenu();
+ }
+
+ // publishes the list of sensors discovered so far into a setting the
+ // preferences window (a separate process) can read to build its selector
+ _publishAvailableSensors() {
+ let list = [];
+ for (let key in this._availableSensors) {
+ let sensor = this._availableSensors[key];
+ list.push({ key: key, label: sensor.label, group: sensor.group });
+ }
+
+ // stable ordering so the signature only changes when the set changes
+ list.sort((a, b) =>
+ a.group.localeCompare(b.group) ||
+ a.label.localeCompare(b.label, undefined, { numeric: true, sensitivity: 'base' }));
+
+ let json = JSON.stringify(list);
+ if (json === this._availableSensorsSig) return;
+ this._availableSensorsSig = json;
+ this._settings.set_string('available-sensors', json);
+ }
+
_initializeTimer() {
// used to query sensors and update display
let update_time = this._settings.get_int('update-time');
@@ -699,6 +736,10 @@ var VitalsMenuButton = GObject.registerClass({
let icon = (split.length == 2)?'icon-' + split[1]:'icon';
let gicon = Gio.icon_new_for_string(this._sensorIconPath(type, icon));
+ // remember this sensor so the preferences selector can list it
+ this._availableSensors[key] = { label: sensor.label, group: type };
+ this._publishAvailableSensors();
+
let item = new MenuItem.MenuItem(gicon, key, sensor.label, sensor.value, this._hotLabels[key]);
item.connect('toggle', (self) => {
let hotSensors = this._settings.get_strv('hot-sensors');
diff --git a/prefs.js b/prefs.js
index 332da0c..76edbca 100644
--- a/prefs.js
+++ b/prefs.js
@@ -138,12 +138,141 @@ export default class VitalsPrefs extends ExtensionPreferences {
let settings = new Settings(this);
let widget = settings.widget;
- const page = new Adw.PreferencesPage();
+ const page = new Adw.PreferencesPage({
+ title: _('General'),
+ icon_name: 'preferences-system-symbolic'
+ });
const group = new Adw.PreferencesGroup({});
group.add(widget);
page.add(group);
window.add(page);
window.set_default_size(widget.width, widget.height);
widget.show();
+
+ // second page: pick exactly which sensors show in the top bar
+ this._buildPanelSensorsPage(window, window._settings);
+ }
+
+ // Builds a page that lists every sensor the extension has discovered,
+ // grouped by category, with a switch per sensor. Toggling a switch adds or
+ // removes that sensor from 'hot-sensors' (the sensors shown in the panel).
+ _buildPanelSensorsPage(window, settings) {
+ const page = new Adw.PreferencesPage({
+ title: _('Panel'),
+ icon_name: 'utilities-system-monitor-symbolic'
+ });
+ window.add(page);
+
+ // groups we add dynamically, so we can rebuild when the sensor list changes
+ let dynamicGroups = [];
+ let keyToSwitch = {};
+ // guards against feedback loops between our writes and the change signals
+ let updating = false;
+
+ const rebuild = () => {
+ for (let g of dynamicGroups) page.remove(g);
+ dynamicGroups = [];
+ keyToSwitch = {};
+
+ let available = [];
+ try {
+ available = JSON.parse(settings.get_string('available-sensors'));
+ } catch (e) {
+ available = [];
+ }
+
+ if (!available.length) {
+ let empty = new Adw.PreferencesGroup({
+ title: _('Sensors in the panel'),
+ description: _('Open the Vitals menu in the top bar once so your sensors can be detected. They will then appear here to be toggled on or off.')
+ });
+ page.add(empty);
+ dynamicGroups.push(empty);
+ return;
+ }
+
+ let intro = new Adw.PreferencesGroup({
+ title: _('Sensors in the panel'),
+ description: _('Choose which sensors are shown in the top bar.')
+ });
+ page.add(intro);
+ dynamicGroups.push(intro);
+
+ // bucket sensors by their category
+ let byGroup = {};
+ for (let sensor of available) {
+ if (!byGroup[sensor.group]) byGroup[sensor.group] = [];
+ byGroup[sensor.group].push(sensor);
+ }
+
+ let hot = settings.get_strv('hot-sensors');
+
+ for (let groupName of Object.keys(byGroup).sort()) {
+ let pg = new Adw.PreferencesGroup({ title: this._groupDisplayName(groupName) });
+
+ for (let sensor of byGroup[groupName]) {
+ let row = new Adw.SwitchRow({
+ title: sensor.label,
+ active: hot.indexOf(sensor.key) >= 0
+ });
+ keyToSwitch[sensor.key] = row;
+
+ row.connect('notify::active', () => {
+ if (updating) return;
+
+ let list = settings.get_strv('hot-sensors');
+ let idx = list.indexOf(sensor.key);
+ if (row.active && idx < 0) list.push(sensor.key);
+ else if (!row.active && idx >= 0) list.splice(idx, 1);
+ else return;
+
+ // same invariant the panel uses: show the generic
+ // placeholder icon only when no real sensor is selected
+ let defIdx = list.indexOf('_default_icon_');
+ if (defIdx >= 0) list.splice(defIdx, 1);
+ if (list.length === 0) list.push('_default_icon_');
+
+ updating = true;
+ settings.set_strv('hot-sensors', list);
+ updating = false;
+ });
+
+ pg.add(row);
+ }
+
+ page.add(pg);
+ dynamicGroups.push(pg);
+ }
+ };
+
+ rebuild();
+
+ // extension discovered new sensors (or the app just started) -> rebuild
+ let availId = settings.connect('changed::available-sensors', () => rebuild());
+
+ // hot-sensors changed elsewhere (e.g. the dropdown) -> reflect in switches
+ let hotId = settings.connect('changed::hot-sensors', () => {
+ if (updating) return;
+ let hot = settings.get_strv('hot-sensors');
+ updating = true;
+ for (let key in keyToSwitch)
+ keyToSwitch[key].active = hot.indexOf(key) >= 0;
+ updating = false;
+ });
+
+ // avoid leaking the signal handlers when the window is closed
+ window.connect('close-request', () => {
+ settings.disconnect(availId);
+ settings.disconnect(hotId);
+ return false;
+ });
+ }
+
+ _groupDisplayName(group) {
+ if (group.startsWith('gpu')) {
+ let n = group.split('#')[1];
+ return n ? _('Graphics') + ' ' + n : _('Graphics');
+ }
+ return group.charAt(0).toUpperCase() + group.slice(1);
}
}
diff --git a/schemas/org.gnome.shell.extensions.vitals.gschema.xml b/schemas/org.gnome.shell.extensions.vitals.gschema.xml
index 4f0b186..e1933fb 100644
--- a/schemas/org.gnome.shell.extensions.vitals.gschema.xml
+++ b/schemas/org.gnome.shell.extensions.vitals.gschema.xml
@@ -6,6 +6,11 @@
Sensors to show in panel
List of sensors to be shown in the panel
+
+ "[]"
+ Discovered sensors
+ JSON list of sensors discovered by the extension, published so the preferences window can offer a selector for which sensors appear in the panel
+
5
Seconds between updates