From 7a8c2f238c08c6f493753f7e009047868292a5ac Mon Sep 17 00:00:00 2001 From: Anton Vakhrushev Date: Sat, 18 Jul 2026 15:36:40 +0300 Subject: [PATCH] Fix latency-probe races, stale offline menu, and byte formatting --- lib/format.js | 2 +- lib/indicator.js | 34 +++++++++++++++++++++++++++++----- prefs.js | 7 +++++-- 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/lib/format.js b/lib/format.js index 449d60e..8cd6243 100644 --- a/lib/format.js +++ b/lib/format.js @@ -16,7 +16,7 @@ function scale(n) { // "2.4 MB", human readable with a space. export function formatBytes(n) { const [v, i] = scale(n); - const digits = i === 0 ? 0 : v < 10 ? 1 : v < 100 ? 1 : 0; + const digits = i === 0 ? 0 : v < 10 ? 2 : v < 100 ? 1 : 0; return `${v.toFixed(digits)} ${UNITS[i]}`; } diff --git a/lib/indicator.js b/lib/indicator.js index 076bc9b..1f298bc 100644 --- a/lib/indicator.js +++ b/lib/indicator.js @@ -35,6 +35,8 @@ class SingBoxIndicator extends PanelMenu.Button { this._selectorItems = new Map(); this._proxiesMap = {}; this._delays = new Map(); // node name -> ms number | 'timeout' + this._probing = new Set(); // nodes with a latency probe in flight + this._apiUrl = ''; this._polling = false; this._cancellable = new Gio.Cancellable(); @@ -131,14 +133,18 @@ class SingBoxIndicator extends PanelMenu.Button { // ---- Settings ----------------------------------------------------- _onSettingsChanged() { + // Latencies measured against one instance are meaningless for another. + if (this._settings.get_string('api-url') !== this._apiUrl) + this._delays.clear(); this._applySettings(); this._restartPolling(); this._poll(); } _applySettings() { + this._apiUrl = this._settings.get_string('api-url'); this._api.setEndpoint( - this._settings.get_string('api-url'), + this._apiUrl, this._settings.get_string('api-secret') ); this._interval = this._settings.get_int('refresh-interval'); @@ -299,6 +305,9 @@ class SingBoxIndicator extends PanelMenu.Button { entry.setOrnament(isCurrent ? PopupMenu.Ornament.DOT : PopupMenu.Ornament.NONE); + // Don't overwrite the "…" placeholder while a probe is running. + if (this._probing.has(member)) + continue; entry.label.text = this._memberLabelText(member, map[member]); } } @@ -340,6 +349,16 @@ class SingBoxIndicator extends PanelMenu.Button { } } + // Drop menu content that would otherwise stay clickable while offline. + _clearMenuData() { + this._selectorsSection.removeAll(); + this._selectorItems.clear(); + this._selectorSignature = null; + this._probing.clear(); + this._connItem.menu.removeAll(); + this._connItem.label.text = `${_('Connections')}: —`; + } + _pickPanelGroup(selectors) { if (selectors.length === 0) return null; @@ -384,6 +403,7 @@ class SingBoxIndicator extends PanelMenu.Button { if (!stored) return; for (const [member, item] of stored.members) { + this._probing.add(member); item.label.text = `${member} · …`; this._api.getDelay(member, {}, this._cancellable) .then(res => { @@ -394,9 +414,14 @@ class SingBoxIndicator extends PanelMenu.Button { this._delays.set(member, 'timeout'); }) .finally(() => { + this._probing.delete(member); if (this._cancellable.is_cancelled()) return; - item.label.text = this._memberLabelText(member, this._proxiesMap?.[member]); + // Re-resolve the row: the menu may have been rebuilt while + // the probe was in flight, disposing the captured actor. + const cur = this._selectorItems.get(groupName)?.members.get(member); + if (cur) + cur.label.text = this._memberLabelText(member, this._proxiesMap?.[member]); }); } } @@ -423,13 +448,12 @@ class SingBoxIndicator extends PanelMenu.Button { break; case STATE.OFFLINE: this._dotFilled = false; - this._statusItem.label.text = error - ? _('sing-box · Unreachable') - : _('sing-box · Offline'); + this._statusItem.label.text = _('sing-box · Unreachable'); this._outboundLabel.text = ''; this._speedLabel.text = ''; this._speedItem.label.text = _('API unreachable — check URL and secret in Settings'); this._prevSample = null; + this._clearMenuData(); break; default: this._dotFilled = false; diff --git a/prefs.js b/prefs.js index 8a71a23..4ca867d 100644 --- a/prefs.js +++ b/prefs.js @@ -50,8 +50,9 @@ export default class SingBoxStatusPreferences extends ExtensionPreferences { _entryRow(settings, key, title, subtitle) { const row = new Adw.EntryRow({ title }); - if (subtitle && row.set_subtitle) // EntryRow gained subtitle later; guard. - row.set_subtitle(subtitle); + // Adw.EntryRow has no subtitle; surface the hint as a tooltip instead. + if (subtitle) + row.set_tooltip_text(subtitle); row.text = settings.get_string(key); row.connect('changed', () => settings.set_string(key, row.text)); settings.connect(`changed::${key}`, () => { @@ -64,6 +65,8 @@ export default class SingBoxStatusPreferences extends ExtensionPreferences { _passwordRow(settings, key, title, subtitle) { const row = new Adw.PasswordEntryRow({ title }); + if (subtitle) + row.set_tooltip_text(subtitle); row.text = settings.get_string(key); row.connect('changed', () => settings.set_string(key, row.text)); settings.connect(`changed::${key}`, () => {