This commit is contained in:
2026-03-11 11:52:02 +09:00
committed by GitHub
commit 8d77a62e26
22 changed files with 3377 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
UUID = sensor-tray@local
SRC = src
INSTALL_DIR = $(HOME)/.local/share/gnome-shell/extensions/$(UUID)
.PHONY: all schemas install uninstall zip clean test
all: schemas
schemas:
glib-compile-schemas $(SRC)/schemas/
install: schemas
rm -rf $(INSTALL_DIR)
cp -r ./$(SRC) $(INSTALL_DIR)
uninstall:
rm -rf $(INSTALL_DIR)
zip: schemas
cd $(SRC) && zip -x "*.pot" -x "*.po" -r ../$(UUID).zip *
clean:
rm -f $(UUID).zip
rm -f $(SRC)/schemas/gschemas.compiled
test: install
dbus-run-session -- gnome-shell --devkit --wayland

View File

@@ -0,0 +1,424 @@
import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import St from 'gi://St';
import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js';
import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import {Extension, gettext as _} from 'resource:///org/gnome/shell/extensions/extension.js';
import SensorClient from './sensorClient.js';
import SensorItem from './sensorItem.js';
// ---------- helpers ----------
// Gio.icon_new_for_string only takes one name; use ThemedIcon for fallbacks
function safeIcon(names) {
if (typeof names === 'string')
names = [names];
return new Gio.ThemedIcon({ names });
}
// ---------- category config ----------
const CATEGORIES = {
Thermal: {
icon: ['sensors-temperature-symbolic', 'temperature-symbolic', 'dialog-warning-symbolic'],
format: (v, dec, unit) => {
if (unit === 1) v = v * 9 / 5 + 32;
return (dec ? '%.1f' : '%.0f').format(v) + (unit === 1 ? '\u00b0F' : '\u00b0C');
},
convert: (v, unit) => unit === 1 ? v * 9 / 5 + 32 : v,
summary: (r) => Math.max(...Object.values(r)),
sortOrder: 0,
},
Cpu: {
icon: ['utilities-system-monitor-symbolic', 'org.gnome.SystemMonitor-symbolic', 'computer-symbolic'],
format: (v, dec) => (dec ? '%.1f' : '%.0f').format(v) + '%',
summary: (r) => r['total'] ?? null,
sortOrder: 1,
},
Power: {
icon: ['battery-full-charged-symbolic', 'battery-symbolic', 'plug-symbolic'],
format: (v, dec) => (dec ? '%.2f' : '%.1f').format(v) + ' W',
summary: (r) => r['package-0'] ?? null,
sortOrder: 2,
},
Memory: {
icon: ['drive-harddisk-symbolic', 'media-memory-symbolic', 'computer-symbolic'],
format: (v, dec, _unit, key) => {
if (key === 'percent' || key === 'swap_percent')
return (dec ? '%.1f' : '%.0f').format(v) + '%';
if (v >= 1073741824)
return '%.1f GiB'.format(v / 1073741824);
if (v >= 1048576)
return '%.0f MiB'.format(v / 1048576);
return '%.0f KiB'.format(v / 1024);
},
summary: (r) => r['percent'] ?? null,
summaryKey: 'percent',
sortOrder: 3,
},
};
const DEFAULT_CATEGORY = {
icon: ['dialog-information-symbolic'],
format: (v, dec) => (dec ? '%.2f' : '%.1f').format(v),
sortOrder: 99,
};
function catCfg(cat) {
return CATEGORIES[cat] || DEFAULT_CATEGORY;
}
function formatSensor(cat, key, val, dec, unit) {
let cfg = catCfg(cat);
let v = cfg.convert ? cfg.convert(val, unit) : val;
return cfg.format(v, dec, unit, key);
}
// ---------- panel button ----------
class SensorTrayButton extends PanelMenu.Button {
static {
GObject.registerClass(this);
}
constructor(settings, path) {
super(0);
this._settings = settings;
this._path = path;
this._client = new SensorClient();
// menu state
this._subMenus = {}; // category → PopupSubMenuMenuItem
this._menuItems = {}; // fullKey → SensorItem
this._lastKeys = null;
// panel state
this._panelBox = new St.BoxLayout();
this.add_child(this._panelBox);
this._hotLabels = {}; // fullKey → St.Label
this._hotIcons = {}; // fullKey → St.Icon
this._buildPanel();
// settings
this._sigIds = [];
this._connectSetting('hot-sensors', () => { this._buildPanel(); this._updatePanel(); this._syncPinOrnaments(); });
this._connectSetting('show-icon-on-panel', () => { this._buildPanel(); this._updatePanel(); });
this._connectSetting('panel-spacing', () => { this._buildPanel(); this._updatePanel(); });
this._connectSetting('unit', () => this._refresh());
this._connectSetting('show-decimal-value', () => this._refresh());
this._connectSetting('position-in-panel', () => this._reposition());
this._connectSetting('panel-box-index', () => this._reposition());
this._connectSetting('update-interval', () => this._restartRefreshTimer());
// throttle UI repaints via a timer
this._dirty = false;
this._refreshTimerId = 0;
this._startRefreshTimer();
this._client.start((cat, readings) => this._onSensorChanged(cat, readings));
this.connect('destroy', () => this._onDestroy());
this._repositionTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 500, () => {
this._reposition();
this._repositionTimeoutId = 0;
return GLib.SOURCE_REMOVE;
});
}
_connectSetting(key, cb) {
this._sigIds.push(this._settings.connect('changed::' + key, cb));
}
// ---- panel (top bar): pinned sensors ----
_buildPanel() {
this._panelBox.destroy_all_children();
this._hotLabels = {};
this._hotIcons = {};
let hot = this._settings.get_strv('hot-sensors');
let showIcon = this._settings.get_boolean('show-icon-on-panel');
if (hot.length === 0) {
this._panelBox.add_child(new St.Icon({
style_class: 'system-status-icon',
gicon: safeIcon(CATEGORIES.Thermal.icon),
}));
return;
}
for (let i = 0; i < hot.length; i++) {
let fullKey = hot[i];
let cat = fullKey.split('/')[0];
let cfg = catCfg(cat);
// spacer between pinned items
if (i > 0) {
let spacing = this._settings.get_int('panel-spacing');
this._panelBox.add_child(new St.Widget({ width: spacing }));
}
if (showIcon) {
let icon = new St.Icon({
style_class: 'system-status-icon',
gicon: safeIcon(cfg.icon),
});
this._hotIcons[fullKey] = icon;
this._panelBox.add_child(icon);
}
let label = new St.Label({
text: '\u2026',
y_expand: true,
y_align: Clutter.ActorAlign.CENTER,
style_class: showIcon ? 'sensortray-panel-icon-label' : 'sensortray-panel-label',
});
this._hotLabels[fullKey] = label;
this._panelBox.add_child(label);
}
}
_updatePanel() {
let dec = this._settings.get_boolean('show-decimal-value');
let unit = this._settings.get_int('unit');
for (let [fullKey, label] of Object.entries(this._hotLabels)) {
let parts = fullKey.split('/');
let cat = parts[0];
let key = parts.slice(1).join('/');
let readings = this._client.readings.get(cat);
if (!readings || !(key in readings)) {
label.text = '\u2026';
continue;
}
label.text = formatSensor(cat, key, readings[key], dec, unit);
}
}
// ---- dropdown: collapsed submenus per category ----
_onSensorChanged(category, _readings) {
if (category === null) {
this._menuItems = {};
this._subMenus = {};
this._lastKeys = null;
this.menu.removeAll();
this.menu.addMenuItem(new PopupMenu.PopupMenuItem(_('sensord is not running')));
for (let l of Object.values(this._hotLabels))
l.text = '\u26a0';
return;
}
this._dirty = true;
}
_startRefreshTimer() {
let seconds = this._settings.get_int('update-interval');
let intervalMs = seconds * 1000;
this._refreshTimerId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, intervalMs, () => {
if (this._dirty) {
this._dirty = false;
this._rebuildMenuIfNeeded();
this._updateValues();
this._updatePanel();
}
return GLib.SOURCE_CONTINUE;
});
}
_restartRefreshTimer() {
if (this._refreshTimerId) {
GLib.Source.remove(this._refreshTimerId);
this._refreshTimerId = 0;
}
this._startRefreshTimer();
}
_sortedEntries() {
let entries = [];
for (let [cat, readings] of this._client.readings) {
let cfg = catCfg(cat);
for (let key of Object.keys(readings))
entries.push({ cat, key, fullKey: cat + '/' + key, sortOrder: cfg.sortOrder });
}
entries.sort((a, b) => {
if (a.sortOrder !== b.sortOrder) return a.sortOrder - b.sortOrder;
if (a.cat !== b.cat) return a.cat.localeCompare(b.cat);
return a.key.localeCompare(b.key, undefined, { numeric: true });
});
return entries;
}
_rebuildMenuIfNeeded() {
let entries = this._sortedEntries();
let keyStr = entries.map(e => e.fullKey).join('\n');
if (this._lastKeys === keyStr)
return;
this._lastKeys = keyStr;
this.menu.removeAll();
this._menuItems = {};
this._subMenus = {};
let hot = this._settings.get_strv('hot-sensors');
// group by category
let grouped = new Map();
for (let e of entries) {
if (!grouped.has(e.cat))
grouped.set(e.cat, []);
grouped.get(e.cat).push(e);
}
for (let [cat, catEntries] of grouped) {
let cfg = catCfg(cat);
// create a collapsed submenu for this category
let sub = new PopupMenu.PopupSubMenuMenuItem(cat, true);
sub.icon.gicon = safeIcon(cfg.icon);
this._subMenus[cat] = sub;
this.menu.addMenuItem(sub);
for (let e of catEntries) {
let gicon = safeIcon(cfg.icon);
let item = new SensorItem(gicon, e.fullKey, e.key, '\u2026');
if (hot.includes(e.fullKey))
item.pinned = true;
item.connect('activate', () => this._togglePin(item));
this._menuItems[e.fullKey] = item;
sub.menu.addMenuItem(item);
}
}
// settings footer
this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
let settingsItem = new PopupMenu.PopupMenuItem(_('Settings'));
settingsItem.connect('activate', () => {
try {
Gio.Subprocess.new(
['gnome-extensions', 'prefs', 'sensor-tray@local'],
Gio.SubprocessFlags.NONE,
);
} catch (e) {
console.error('sensor-tray: cannot open prefs:', e.message);
}
});
this.menu.addMenuItem(settingsItem);
}
_updateValues() {
let dec = this._settings.get_boolean('show-decimal-value');
let unit = this._settings.get_int('unit');
for (let [cat, readings] of this._client.readings) {
for (let [key, val] of Object.entries(readings)) {
let item = this._menuItems[cat + '/' + key];
if (item)
item.value = formatSensor(cat, key, val, dec, unit);
}
// update submenu header with a summary (e.g. max temp)
let sub = this._subMenus[cat];
if (sub && sub.status) {
let cfg = catCfg(cat);
if (cfg.summary) {
let sv = cfg.summary(readings);
if (sv !== null)
sub.status.text = formatSensor(cat, cfg.summaryKey || '', sv, dec, unit);
}
}
}
}
_syncPinOrnaments() {
let hot = this._settings.get_strv('hot-sensors');
for (let [key, item] of Object.entries(this._menuItems))
item.pinned = hot.includes(key);
}
_togglePin(item) {
let hot = this._settings.get_strv('hot-sensors');
if (item.pinned)
hot = hot.filter(k => k !== item.key);
else
hot.push(item.key);
this._settings.set_strv('hot-sensors', hot);
}
_refresh() {
this._lastKeys = null;
this._rebuildMenuIfNeeded();
this._updateValues();
this._updatePanel();
}
// ---- panel position ----
_reposition() {
try {
if (!this.container?.get_parent()) return;
this.container.get_parent().remove_child(this.container);
let boxes = {
0: Main.panel._leftBox,
1: Main.panel._centerBox,
2: Main.panel._rightBox,
};
let pos = this._settings.get_int('position-in-panel');
let idx = this._settings.get_int('panel-box-index');
(boxes[pos] || boxes[2]).insert_child_at_index(this.container, idx);
} catch (e) {
console.error('sensor-tray: reposition failed:', e.message);
}
}
// ---- cleanup ----
_onDestroy() {
this._client.destroy();
if (this._refreshTimerId) {
GLib.Source.remove(this._refreshTimerId);
this._refreshTimerId = 0;
}
if (this._repositionTimeoutId) {
GLib.Source.remove(this._repositionTimeoutId);
this._repositionTimeoutId = 0;
}
for (let id of this._sigIds)
this._settings.disconnect(id);
this._sigIds = [];
}
}
// ---------- extension entry point ----------
export default class SensorTrayExtension extends Extension {
enable() {
this._button = new SensorTrayButton(this.getSettings(), this.path);
Main.panel.addToStatusArea('sensor-tray', this._button);
}
disable() {
this._button?.destroy();
this._button = null;
}
}

View File

@@ -0,0 +1,8 @@
{
"uuid": "sensor-tray@local",
"name": "Sensor Tray",
"description": "System sensor monitor powered by sensord. Displays CPU, thermal, power, and memory readings from org.sensord D-Bus interfaces.",
"shell-version": ["45", "46", "47", "48", "49", "50"],
"settings-schema": "org.gnome.shell.extensions.sensortray",
"url": ""
}

View File

@@ -0,0 +1,186 @@
import Gio from 'gi://Gio';
import Gtk from 'gi://Gtk';
import Adw from 'gi://Adw';
import {ExtensionPreferences, gettext as _} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js';
export default class SensorTrayPreferences extends ExtensionPreferences {
fillPreferencesWindow(window) {
this._settings = this.getSettings();
let page = new Adw.PreferencesPage({
title: _('Sensor Tray'),
icon_name: 'utilities-system-monitor-symbolic',
});
page.add(this._createPinnedGroup());
page.add(this._createDisplayGroup());
page.add(this._createPositionGroup());
window.add(page);
}
_createPinnedGroup() {
let group = new Adw.PreferencesGroup({
title: _('Pinned Sensors'),
description: _('Sensors shown in the top bar. Toggle pins from the dropdown menu.'),
});
this._pinnedGroup = group;
this._rebuildPinnedRows();
this._settings.connect('changed::hot-sensors', () => this._rebuildPinnedRows());
return group;
}
_rebuildPinnedRows() {
// clear existing rows
if (this._pinnedRows) {
for (let row of this._pinnedRows)
this._pinnedGroup.remove(row);
}
this._pinnedRows = [];
let hot = this._settings.get_strv('hot-sensors');
if (hot.length === 0) {
let empty = new Adw.ActionRow({
title: _('No sensors pinned'),
subtitle: _('Click sensors in the dropdown menu to pin them'),
});
this._pinnedGroup.add(empty);
this._pinnedRows.push(empty);
return;
}
for (let idx = 0; idx < hot.length; idx++) {
let fullKey = hot[idx];
// show "Category / key" as title/subtitle
let parts = fullKey.split('/');
let cat = parts[0];
let key = parts.slice(1).join('/');
let row = new Adw.ActionRow({
title: key,
subtitle: cat,
});
let btnBox = new Gtk.Box({
orientation: Gtk.Orientation.HORIZONTAL,
spacing: 4,
valign: Gtk.Align.CENTER,
});
let upBtn = new Gtk.Button({
icon_name: 'go-up-symbolic',
css_classes: ['flat'],
sensitive: idx > 0,
});
upBtn.connect('clicked', () => {
let arr = this._settings.get_strv('hot-sensors');
let i = arr.indexOf(fullKey);
if (i > 0) {
[arr[i - 1], arr[i]] = [arr[i], arr[i - 1]];
this._settings.set_strv('hot-sensors', arr);
}
});
let downBtn = new Gtk.Button({
icon_name: 'go-down-symbolic',
css_classes: ['flat'],
sensitive: idx < hot.length - 1,
});
downBtn.connect('clicked', () => {
let arr = this._settings.get_strv('hot-sensors');
let i = arr.indexOf(fullKey);
if (i >= 0 && i < arr.length - 1) {
[arr[i], arr[i + 1]] = [arr[i + 1], arr[i]];
this._settings.set_strv('hot-sensors', arr);
}
});
let removeBtn = new Gtk.Button({
icon_name: 'edit-delete-symbolic',
css_classes: ['flat'],
});
removeBtn.connect('clicked', () => {
let arr = this._settings.get_strv('hot-sensors');
this._settings.set_strv('hot-sensors', arr.filter(k => k !== fullKey));
});
btnBox.append(upBtn);
btnBox.append(downBtn);
btnBox.append(removeBtn);
row.add_suffix(btnBox);
this._pinnedGroup.add(row);
this._pinnedRows.push(row);
}
}
_createDisplayGroup() {
let group = new Adw.PreferencesGroup({ title: _('Display') });
let unitRow = new Adw.ComboRow({
title: _('Temperature Unit'),
model: new Gtk.StringList({ strings: ['\u00b0C', '\u00b0F'] }),
});
this._settings.bind('unit', unitRow, 'selected', Gio.SettingsBindFlags.DEFAULT);
group.add(unitRow);
group.add(this._switch(_('Show Decimal Values'), 'show-decimal-value'));
group.add(this._switch(_('Show Icon on Panel'), 'show-icon-on-panel'));
let spacingRow = new Adw.SpinRow({
title: _('Panel Spacing'),
subtitle: _('Pixels between pinned values'),
adjustment: new Gtk.Adjustment({
lower: 0, upper: 32, value: 8, step_increment: 1,
}),
});
this._settings.bind('panel-spacing', spacingRow, 'value', Gio.SettingsBindFlags.DEFAULT);
group.add(spacingRow);
let intervalRow = new Adw.SpinRow({
title: _('Update Interval'),
subtitle: _('Seconds between UI updates'),
adjustment: new Gtk.Adjustment({
lower: 1, upper: 10, value: 1, step_increment: 1,
}),
});
this._settings.bind('update-interval', intervalRow, 'value', Gio.SettingsBindFlags.DEFAULT);
group.add(intervalRow);
return group;
}
_createPositionGroup() {
let group = new Adw.PreferencesGroup({ title: _('Panel Position') });
let posRow = new Adw.ComboRow({
title: _('Position'),
model: new Gtk.StringList({ strings: [_('Left'), _('Center'), _('Right')] }),
});
this._settings.bind('position-in-panel', posRow, 'selected', Gio.SettingsBindFlags.DEFAULT);
group.add(posRow);
let idxRow = new Adw.SpinRow({
title: _('Index'),
adjustment: new Gtk.Adjustment({
lower: -1, upper: 25, value: 0, step_increment: 1,
}),
});
this._settings.bind('panel-box-index', idxRow, 'value', Gio.SettingsBindFlags.DEFAULT);
group.add(idxRow);
return group;
}
_switch(title, key) {
let row = new Adw.SwitchRow({ title });
this._settings.bind(key, row, 'active', Gio.SettingsBindFlags.DEFAULT);
return row;
}
}

View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<schemalist>
<schema id="org.gnome.shell.extensions.sensortray"
path="/org/gnome/shell/extensions/sensortray/">
<key name="hot-sensors" type="as">
<default>[]</default>
<summary>Sensors pinned to the panel</summary>
<description>Ordered list of sensor keys shown in the top bar (e.g. "Thermal/coretemp/Core 0", "Cpu/total")</description>
</key>
<key name="position-in-panel" type="i">
<default>2</default>
<summary>Panel position (0=left, 1=center, 2=right)</summary>
</key>
<key name="panel-box-index" type="i">
<default>0</default>
<summary>Index within the panel box</summary>
</key>
<key name="show-icon-on-panel" type="b">
<default>true</default>
<summary>Show icon next to each pinned sensor value</summary>
</key>
<key name="unit" type="i">
<default>0</default>
<summary>Temperature unit (0=Celsius, 1=Fahrenheit)</summary>
</key>
<key name="show-decimal-value" type="b">
<default>false</default>
<summary>Show one decimal place</summary>
</key>
<key name="panel-spacing" type="i">
<default>8</default>
<summary>Spacing between pinned values in pixels</summary>
</key>
<key name="update-interval" type="i">
<default>1</default>
<summary>Seconds between UI updates (110)</summary>
<description>How many seconds between panel and menu repaints. D-Bus signals still arrive immediately; only the visual repaint is throttled.</description>
<range min="1" max="10"/>
</key>
</schema>
</schemalist>

View File

@@ -0,0 +1,166 @@
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
const BUS_NAME = 'org.sensord';
const OBJECT_PATH = '/org/sensord';
const IFACE_PREFIX = 'org.sensord.';
/**
* Generic client for all org.sensord.* D-Bus interfaces.
*
* Every interface has the same shape:
* method GetReadings() → a{sd}
* signal Changed(a{sd})
*
* The client introspects the object once, discovers all sensor interfaces,
* fetches initial readings, then subscribes to Changed signals.
* Callers get a flat Map<category, Map<key, double>> that stays current.
*/
export default class SensorClient {
constructor() {
this._conn = null;
this._signalIds = [];
// category → { key: value } e.g. "Power" → { "package-0": 42.3 }
this._readings = new Map();
this._onChanged = null;
this._available = false;
this._nameWatchId = 0;
}
/**
* Connect to the system bus and start receiving sensor data.
* @param {function(string, Object<string,number>)} onChanged
* Called with (category, readings) whenever a sensor interface emits Changed
* and also once per interface after initial GetReadings.
*/
start(onChanged) {
this._onChanged = onChanged;
try {
this._conn = Gio.bus_get_sync(Gio.BusType.SYSTEM, null);
} catch (e) {
console.error('sensortray: cannot connect to system bus:', e.message);
return;
}
// Watch for sensord appearing/disappearing on the bus
this._nameWatchId = Gio.bus_watch_name_on_connection(
this._conn,
BUS_NAME,
Gio.BusNameWatcherFlags.NONE,
() => this._onNameAppeared(),
() => this._onNameVanished(),
);
}
_onNameAppeared() {
this._available = true;
this._discover();
}
_onNameVanished() {
this._available = false;
this._unsubscribeAll();
this._readings.clear();
if (this._onChanged)
this._onChanged(null, null); // signal "all gone"
}
/**
* Introspect /org/sensord, find all org.sensord.* interfaces,
* call GetReadings on each, subscribe to Changed.
*/
_discover() {
this._unsubscribeAll();
this._readings.clear();
let introXml;
try {
let result = this._conn.call_sync(
BUS_NAME, OBJECT_PATH,
'org.freedesktop.DBus.Introspectable', 'Introspect',
null, GLib.VariantType.new('(s)'),
Gio.DBusCallFlags.NONE, 3000, null,
);
[introXml] = result.deep_unpack();
} catch (e) {
console.error('sensortray: introspect failed:', e.message);
return;
}
// Parse interface names from XML — simple regex is fine here
let ifaces = [];
let re = /interface\s+name="(org\.sensord\.[^"]+)"/g;
let m;
while ((m = re.exec(introXml)) !== null)
ifaces.push(m[1]);
for (let iface of ifaces) {
let category = iface.slice(IFACE_PREFIX.length); // "Power", "Thermal", etc.
// Subscribe to Changed signal
let sid = this._conn.signal_subscribe(
BUS_NAME, iface, 'Changed', OBJECT_PATH,
null, Gio.DBusSignalFlags.NONE,
(_conn, _sender, _path, _iface, _signal, params) => {
let [readings] = params.deep_unpack();
this._readings.set(category, readings);
if (this._onChanged)
this._onChanged(category, readings);
},
);
this._signalIds.push(sid);
// Fetch initial state
this._conn.call(
BUS_NAME, OBJECT_PATH, iface, 'GetReadings',
null, GLib.VariantType.new('(a{sd})'),
Gio.DBusCallFlags.NONE, 3000, null,
(conn, res) => {
try {
let result = conn.call_finish(res);
let [readings] = result.deep_unpack();
this._readings.set(category, readings);
if (this._onChanged)
this._onChanged(category, readings);
} catch (e) {
console.error(`sensortray: GetReadings(${iface}) failed:`, e.message);
}
},
);
}
}
_unsubscribeAll() {
if (!this._conn)
return;
for (let sid of this._signalIds)
this._conn.signal_unsubscribe(sid);
this._signalIds = [];
}
/** @returns {boolean} true if sensord is on the bus */
get available() {
return this._available;
}
/**
* @returns {Map<string, Object<string,number>>}
* category → { key: value } snapshot of all current readings
*/
get readings() {
return this._readings;
}
destroy() {
this._unsubscribeAll();
if (this._nameWatchId) {
Gio.bus_unwatch_name(this._nameWatchId);
this._nameWatchId = 0;
}
this._conn = null;
this._onChanged = null;
this._readings.clear();
}
}

View File

@@ -0,0 +1,37 @@
import Clutter from 'gi://Clutter';
import GObject from 'gi://GObject';
import St from 'gi://St';
import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
export default class SensorItem extends PopupMenu.PopupBaseMenuItem {
static {
GObject.registerClass(this);
}
constructor(gicon, key, label, value) {
super();
this._key = key;
this._gicon = gicon;
this._pinned = false;
this.add_child(new St.Icon({ style_class: 'popup-menu-icon', gicon }));
this._label = new St.Label({ text: label, x_expand: true });
this.add_child(this._label);
this._value = new St.Label({ text: value });
this.add_child(this._value);
}
get key() { return this._key; }
get gicon() { return this._gicon; }
get pinned() { return this._pinned; }
set pinned(v) {
this._pinned = v;
this.setOrnament(v ? PopupMenu.Ornament.DOT : PopupMenu.Ornament.NONE);
}
set value(v) { this._value.text = v; }
set label(v) { this._label.text = v; }
}

View File

@@ -0,0 +1,7 @@
.sensortray-panel-label {
padding: 0 6px;
}
.sensortray-panel-icon-label {
padding: 0 2px 0 0;
}