This commit is contained in:
mjallen18
2026-03-23 17:42:47 -05:00
parent ecce28b498
commit 309e224a72

View File

@@ -35,9 +35,9 @@ function runCommand(argv) {
log(`runCommand: ${argv.join(' ')}`);
try {
const [ok, stdout, stderr, exitCode] = GLib.spawn_sync(
null, // working dir
null,
argv,
null, // inherit env
null,
GLib.SpawnFlags.SEARCH_PATH,
null,
);
@@ -62,7 +62,7 @@ function isServiceActive() {
}
/**
* Get interface info for IFACE_NAME.
* Get interface addresses for IFACE_NAME.
* Returns { found, ipv4, ipv6 } where ipv4/ipv6 are strings or null.
*/
function getIfaceInfo() {
@@ -74,16 +74,82 @@ function getIfaceInfo() {
return {found: false, ipv4: null, ipv6: null};
}
const parts = r.stdout.split(/\s+/);
const parts = r.stdout.split(/\s+/);
// parts[0] = iface, parts[1] = state, parts[2..] = addresses
const addrs = parts.slice(2);
const ipv4 = addrs.find(a => /^\d+\.\d+\.\d+\.\d+\//.test(a)) ?? null;
const ipv6 = addrs.find(a => a.includes(':') && !a.startsWith('fe80')) ?? null;
const addrs = parts.slice(2);
const ipv4 = addrs.find(a => /^\d+\.\d+\.\d+\.\d+\//.test(a)) ?? null;
const ipv6 = addrs.find(a => a.includes(':') && !a.startsWith('fe80')) ?? null;
log(`getIfaceInfo: found=true state=${parts[1]} ipv4=${ipv4} ipv6=${ipv6}`);
return {found: true, ipv4, ipv6};
}
/** Start or stop the systemd service (requires polkit / passwordless sudo). */
/**
* Read a single integer from a sysfs file, returning null on failure.
*/
function readSysfs(path) {
try {
const f = Gio.File.new_for_path(path);
const [ok, contents] = f.load_contents(null);
if (!ok) return null;
const val = parseInt(new TextDecoder().decode(contents).trim(), 10);
return isNaN(val) ? null : val;
} catch (_e) {
return null;
}
}
/**
* Get TX/RX byte counters for IFACE_NAME from sysfs.
* Returns { rx, tx } in bytes, or null values if unavailable.
*/
function getIfaceStats() {
const base = `/sys/class/net/${IFACE_NAME}/statistics`;
const rx = readSysfs(`${base}/rx_bytes`);
const tx = readSysfs(`${base}/tx_bytes`);
log(`getIfaceStats: rx=${rx} tx=${tx}`);
return {rx, tx};
}
/** Format bytes into a human-readable string. */
function formatBytes(bytes) {
if (bytes === null) return '—';
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
}
/**
* Get the service ActiveEnterTimestamp from systemctl show.
* Returns a human-readable uptime string, or null.
*/
function getServiceUptime() {
const r = runCommand([
'systemctl', 'show', SERVICE_NAME,
'--property=ActiveEnterTimestampMonotonic',
]);
if (!r.success) return null;
// Output: ActiveEnterTimestampMonotonic=1234567890
const match = r.stdout.match(/=(\d+)/);
if (!match) return null;
const startMonoUs = parseInt(match[1], 10);
const nowMonoUs = GLib.get_monotonic_time();
const elapsedSecs = Math.floor((nowMonoUs - startMonoUs) / 1_000_000);
if (elapsedSecs < 0) return null;
const h = Math.floor(elapsedSecs / 3600);
const m = Math.floor((elapsedSecs % 3600) / 60);
const s = elapsedSecs % 60;
if (h > 0) return `${h}h ${m}m`;
if (m > 0) return `${m}m ${s}s`;
return `${s}s`;
}
/** Start or stop the systemd service (requires polkit). */
function setServiceActive(enable) {
const action = enable ? 'start' : 'stop';
log(`setServiceActive: ${action} ${SERVICE_NAME}`);
@@ -118,7 +184,7 @@ class NebulaIndicator extends PanelMenu.Button {
hbox.add_child(this._label);
this.add_child(hbox);
// ── dropdown menu ──
// ── top-level status ──
this._statusItem = new PopupMenu.PopupMenuItem('', {reactive: false});
this._statusItem.label.set_style('font-weight: bold;');
this.menu.addMenuItem(this._statusItem);
@@ -131,6 +197,23 @@ class NebulaIndicator extends PanelMenu.Button {
this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
// ── "Network" expandable submenu ──
this._networkSubmenu = new PopupMenu.PopupSubMenuMenuItem('Network');
this._uptimeItem = new PopupMenu.PopupMenuItem('', {reactive: false});
this._networkSubmenu.menu.addMenuItem(this._uptimeItem);
this._rxItem = new PopupMenu.PopupMenuItem('', {reactive: false});
this._networkSubmenu.menu.addMenuItem(this._rxItem);
this._txItem = new PopupMenu.PopupMenuItem('', {reactive: false});
this._networkSubmenu.menu.addMenuItem(this._txItem);
this.menu.addMenuItem(this._networkSubmenu);
this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
// ── actions ──
this._toggleItem = new PopupMenu.PopupMenuItem('');
this._toggleItem.connect('activate', () => this._onToggle());
this.menu.addMenuItem(this._toggleItem);
@@ -159,31 +242,38 @@ class NebulaIndicator extends PanelMenu.Button {
log('_refresh: checking service and interface');
this._active = isServiceActive();
const iface = getIfaceInfo();
log(`_refresh: active=${this._active} iface=${JSON.stringify(iface)}`);
this._updateUI(iface);
const stats = getIfaceStats();
const uptime = this._active ? getServiceUptime() : null;
log(`_refresh: active=${this._active} iface=${JSON.stringify(iface)} stats=${JSON.stringify(stats)} uptime=${uptime}`);
this._updateUI(iface, stats, uptime);
}
_updateUI(iface) {
_updateUI(iface, stats, uptime) {
log(`_updateUI: active=${this._active}`);
if (this._active) {
this._icon.icon_name = 'network-vpn-symbolic';
this._label.text = 'VPN';
this._label.style = 'margin-left: 4px; margin-right: 2px; color: #57e389;'; // green
this._statusItem.label.text = `Status: Connected`;
this._label.style = 'margin-left: 4px; margin-right: 2px; color: #57e389;';
this._statusItem.label.text = 'Status: Connected';
this._ipv4Item.label.text = iface.ipv4 ? `IPv4: ${iface.ipv4}` : 'IPv4: —';
this._ipv6Item.label.text = iface.ipv6 ? `IPv6: ${iface.ipv6}` : 'IPv6: —';
this._ipv4Item.visible = true;
this._ipv6Item.visible = true;
this._toggleItem.label.text = 'Disconnect';
this._networkSubmenu.visible = true;
this._uptimeItem.label.text = `Uptime: ${uptime ?? '—'}`;
this._rxItem.label.text = `RX: ${formatBytes(stats.rx)}`;
this._txItem.label.text = `TX: ${formatBytes(stats.tx)}`;
} else {
this._icon.icon_name = 'network-vpn-disabled-symbolic';
this._label.text = 'VPN';
this._label.style = 'margin-left: 4px; margin-right: 2px; color: #c01c28;'; // red
this._statusItem.label.text = `Status: Disconnected`;
this._label.style = 'margin-left: 4px; margin-right: 2px; color: #c01c28;';
this._statusItem.label.text = 'Status: Disconnected';
this._ipv4Item.visible = false;
this._ipv6Item.visible = false;
this._toggleItem.label.text = 'Connect';
this._networkSubmenu.visible = false;
}
// label is always "VPN"
this._label.text = 'VPN';
}
_onToggle() {
@@ -192,7 +282,7 @@ class NebulaIndicator extends PanelMenu.Button {
setServiceActive(target);
// Optimistic update while systemd races
this._active = target;
this._updateUI(getIfaceInfo());
this._updateUI(getIfaceInfo(), getIfaceStats(), null);
// Re-poll shortly after to confirm real state
GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 2, () => {
log('_onToggle: confirmation poll');