aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-05 00:15:55 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-05 13:32:26 +0200
commitb3362e6b25bc77dfe94f4c088a865e6fd2be2601 (patch)
treef783bdab2e3d9b5a5aa3465b049fa73e8fd1d478
parent89ed51e9b7ea38bd4475ce3a234f8c783b3d9b09 (diff)
downloadmeshbay-b3362e6b25bc77dfe94f4c088a865e6fd2be2601.tar.gz
feat(client): tray menu starts and stops the node, and a clearer icon
The first icon was an arrow dropping into a receptacle, which is the download glyph -- a vertical stem above a container reads that way whatever the context. Replaced with a window folding a chevron into itself: no stem, and the frame says which object is being minimised. Applied to both the nav button and the panel indicator, which carried the same wrong shape. The menu now offers Start or Stop for the node daemon, chosen from its actual state and shown only when there is a daemon to act on: `supported && installed`, so a machine with no node installed gets no entry rather than a control that fails when used. `restart` is the start verb -- systemd's restart starts a stopped unit, and there is no separate one to call. The three service handlers become named functions so the tray drives exactly what the Node page drives, instead of a second copy of the systemctl and Task Scheduler branches. A read of the state that throws is treated as no control at all. The menu is rebuilt on a 5s timer while an indicator exists, and again straight after an action. libappindicator has no "menu is about to open" event, so a menu built once would show a stale Start/Stop for the life of the process; `systemctl --user show` costs a few milliseconds. Locales: tray.start_node / tray.stop_node in all ten. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DtfG7z6wHWj8RKHCvxQtY1
-rw-r--r--packages/meshbay-client/src/main.js99
-rw-r--r--packages/meshbay-client/src/tray-icon.pngbin415 -> 504 bytes
-rw-r--r--packages/meshbay-client/src/tray-icon@2x.pngbin755 -> 967 bytes
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/app.js5
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/icon.js7
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/de.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/en.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/es.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/it.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js2
15 files changed, 105 insertions, 26 deletions
diff --git a/packages/meshbay-client/src/main.js b/packages/meshbay-client/src/main.js
index 71b56be..273d151 100644
--- a/packages/meshbay-client/src/main.js
+++ b/packages/meshbay-client/src/main.js
@@ -339,6 +339,19 @@ let mainWindow = null;
// files are the interface's, the main process has no i18n, and a second string
// table is how two of them start disagreeing.
let tray = null;
+let trayLabels = null;
+let trayTimer = null;
+let nodeService = null; // assigned by registerBridge()
+
+const TRAY_FALLBACK = {
+ show: 'Show MeshBay', quit: 'Quit',
+ start_node: 'Start the node', stop_node: 'Stop the node',
+};
+
+// libappindicator offers no "menu is about to open" event, so a menu built once
+// would show a stale Start/Stop for as long as the app runs. `systemctl --user
+// show` is a few milliseconds and this only ticks while an indicator exists.
+const TRAY_POLL_MS = 5000;
function showFromTray() {
if (!mainWindow) return;
@@ -347,18 +360,51 @@ function showFromTray() {
mainWindow.focus();
}
+async function buildTrayMenu() {
+ const text = { ...TRAY_FALLBACK, ...(trayLabels || {}) };
+ const items = [{ label: text.show, click: showFromTray }];
+
+ // Only when there is a daemon to act on: not installed means no entry at all,
+ // rather than a control that reports failure when used.
+ let status = null;
+ try {
+ status = nodeService ? await nodeService.status() : null;
+ } catch {
+ status = null; // unreadable state is the same as no control
+ }
+ if (status && status.supported && status.installed) {
+ const running = status.activeState === 'active';
+ items.push({ type: 'separator' });
+ items.push({
+ label: running ? text.stop_node : text.start_node,
+ click: async () => {
+ try {
+ // `restart` starts a stopped unit, which is what systemd's restart
+ // means; there is no separate start verb to call.
+ if (running) await nodeService.stop();
+ else await nodeService.restart();
+ } catch (err) {
+ console.error('[MeshBay] tray: node action failed', err);
+ }
+ refreshTrayMenu(); // reflect the new state without waiting a tick
+ },
+ });
+ }
+
+ items.push({ type: 'separator' },
+ { label: text.quit, click: () => app.quit() });
+ return Menu.buildFromTemplate(items);
+}
+
+async function refreshTrayMenu() {
+ if (!tray) return;
+ tray.setContextMenu(await buildTrayMenu());
+}
+
function ensureTray(labels) {
- const text = {
- show: (labels && labels.show) || 'Show MeshBay',
- quit: (labels && labels.quit) || 'Quit',
- };
+ if (labels) trayLabels = labels; // the person may have changed language
if (tray) {
- // Relabel in place: the person may have changed language since it was made.
- tray.setContextMenu(Menu.buildFromTemplate([
- { label: text.show, click: showFromTray },
- { type: 'separator' },
- { label: text.quit, click: () => app.quit() },
- ]));
+ refreshTrayMenu();
return tray;
}
// In src/, not build/: electron-builder packages only `src/**` and `ui/**`
@@ -366,17 +412,13 @@ function ensureTray(labels) {
// missing from every installed one.
tray = new Tray(path.join(__dirname, 'tray-icon.png'));
tray.setToolTip('MeshBay');
- tray.setContextMenu(Menu.buildFromTemplate([
- { label: text.show, click: showFromTray },
- { type: 'separator' },
- { label: text.quit, click: () => app.quit() },
- ]));
+ refreshTrayMenu();
// Harmless where it does nothing (GNOME), useful on desktops that do send it.
tray.on('click', showFromTray);
+ if (!trayTimer) trayTimer = setInterval(refreshTrayMenu, TRAY_POLL_MS);
return tray;
}
-
function createWindow() {
const bounds = (config.window && config.window.width) ? config.window : {
width: 1200, height: 800,
@@ -1073,7 +1115,16 @@ function registerBridge() {
// of the Node page. Deliberately not `probeNode()`: that asks the daemon's
// own HTTP API, which cannot answer while the daemon is stopped or crash-
// looping — exactly the states this panel exists to show and act on.
- ipcMain.handle('node:service-status', async () => {
+ // The tray menu drives the same daemon controls as the Node page. Declared
+ // here because these close over registerBridge's helpers; the tray is created
+ // long after, so it reads them through this binding rather than duplicating
+ // the systemctl and Task Scheduler branches.
+ nodeService = { status: nodeServiceStatus, stop: nodeServiceStop,
+ restart: nodeServiceRestart };
+
+ ipcMain.handle('node:service-status', () => nodeServiceStatus());
+
+ async function nodeServiceStatus() {
if (process.platform === 'win32') {
const svc = await winServiceTaskStatus();
if (svc.installed) {
@@ -1133,9 +1184,11 @@ function registerBridge() {
});
});
});
- });
+ }
- ipcMain.handle('node:service-stop', async () => {
+ ipcMain.handle('node:service-stop', () => nodeServiceStop());
+
+ async function nodeServiceStop() {
if (process.platform === 'win32') {
const svc = await winServiceTaskStatus();
if (svc.installed) await winServiceTaskEnd();
@@ -1154,9 +1207,11 @@ function registerBridge() {
});
});
return { stopped: true };
- });
+ }
- ipcMain.handle('node:service-restart', async () => {
+ ipcMain.handle('node:service-restart', () => nodeServiceRestart());
+
+ async function nodeServiceRestart() {
if (process.platform === 'win32') {
const svc = await winServiceTaskStatus();
if (svc.installed) await winServiceTaskEnd();
@@ -1181,7 +1236,7 @@ function registerBridge() {
});
});
return { restarted: true };
- });
+ }
// Install / remove the Windows Startup-folder launcher, and query it.
ipcMain.handle('node:autostart', async (_e, action) => {
diff --git a/packages/meshbay-client/src/tray-icon.png b/packages/meshbay-client/src/tray-icon.png
index 394604d..98fd63b 100644
--- a/packages/meshbay-client/src/tray-icon.png
+++ b/packages/meshbay-client/src/tray-icon.png
Binary files differ
diff --git a/packages/meshbay-client/src/tray-icon@2x.png b/packages/meshbay-client/src/tray-icon@2x.png
index 3e533fb..9cf613d 100644
--- a/packages/meshbay-client/src/tray-icon@2x.png
+++ b/packages/meshbay-client/src/tray-icon@2x.png
Binary files differ
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js
index 5857821..c7e46d8 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/app.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js
@@ -244,8 +244,9 @@ function Nav({ user, theme, onThemeChange, onLogout, onMenuToggle, unreadCount,
<div class="nav-right">
${user && html`<${TransferWidget} />`}
${platform.capabilities.tray && html`
- <button class="nav-tray" onClick=${() => platform.minimizeToTray(
- { show: t('tray.show'), quit: t('tray.quit') })}
+ <button class="nav-tray" onClick=${() => platform.minimizeToTray({
+ show: t('tray.show'), quit: t('tray.quit'),
+ start_node: t('tray.start_node'), stop_node: t('tray.stop_node') })}
title=${t('nav.minimize_tray')} aria-label=${t('nav.minimize_tray')}>
<${Icon} name="tray" />
</button>
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/icon.js b/packages/meshbay-hub/src/meshbay_hub/static/icon.js
index 79a3a08..90d9194 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/icon.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/icon.js
@@ -13,8 +13,11 @@ import { html } from './vendor/htm-preact.js';
const ICON_PATHS = {
menu: ['M4 7h16M4 12h16M4 17h16'],
- tray: ['M12 3.5v9', 'M8.5 9l3.5 3.5L15.5 9',
- 'M4 15v3a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-3'],
+ // A window with a chevron folding into it. Deliberately no shaft: a
+ // vertical stem above a receptacle is the download glyph, and the first
+ // attempt here read as one.
+ tray: ['M6 4h12a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V7a3 3 0 0 1 3-3z',
+ 'M8 10l4 4 4-4'],
bell: ['M18 9a6 6 0 1 0-12 0c0 6-2.5 7.5-2.5 7.5h17S18 15 18 9',
'M10.3 20a2 2 0 0 0 3.4 0'],
shield: ['M12 3l7.5 3v5.2c0 4.6-3.1 8.6-7.5 10.3-4.4-1.7-7.5-5.7-7.5-10.3V6z'],
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
index 24ff315..46168cf 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
@@ -17,6 +17,8 @@ export default {
'nav.minimize_tray': 'In den Infobereich minimieren',
'tray.show': 'MeshBay anzeigen',
'tray.quit': 'Beenden',
+ 'tray.start_node': 'Node starten',
+ 'tray.stop_node': 'Node stoppen',
// Sidebar
'sidebar.my_groups': 'Meine Gruppen',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
index ce08f1f..2d38e4b 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
@@ -20,6 +20,8 @@ export default {
'nav.minimize_tray': 'Minimize to tray',
'tray.show': 'Show MeshBay',
'tray.quit': 'Quit',
+ 'tray.start_node': 'Start the node',
+ 'tray.stop_node': 'Stop the node',
// Sidebar
'sidebar.my_groups': 'My Groups',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
index 7aa1d98..471ad82 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
@@ -16,6 +16,8 @@ export default {
'nav.minimize_tray': 'Minimizar en la bandeja',
'tray.show': 'Mostrar MeshBay',
'tray.quit': 'Salir',
+ 'tray.start_node': 'Iniciar el nodo',
+ 'tray.stop_node': 'Detener el nodo',
// Sidebar
'sidebar.my_groups': 'Mis grupos',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
index b87cb81..48f26a2 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
@@ -16,6 +16,8 @@ export default {
'nav.minimize_tray': 'Réduire dans la zone de notification',
'tray.show': 'Afficher MeshBay',
'tray.quit': 'Quitter',
+ 'tray.start_node': 'Démarrer le nœud',
+ 'tray.stop_node': 'Arrêter le nœud',
// Sidebar
'sidebar.my_groups': 'Mes groupes',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
index 242bdb4..564f34b 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
@@ -17,6 +17,8 @@ export default {
'nav.minimize_tray': 'Riduci nella barra di sistema',
'tray.show': 'Mostra MeshBay',
'tray.quit': 'Esci',
+ 'tray.start_node': 'Avvia il nodo',
+ 'tray.stop_node': 'Arresta il nodo',
// Sidebar
'sidebar.my_groups': 'I miei gruppi',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
index 8c8f654..3eed305 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
@@ -17,6 +17,8 @@ export default {
'nav.minimize_tray': 'トレイに最小化',
'tray.show': 'MeshBay を表示',
'tray.quit': '終了',
+ 'tray.start_node': 'ノードを開始',
+ 'tray.stop_node': 'ノードを停止',
// Sidebar
'sidebar.my_groups': 'マイグループ',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
index 566677d..711a22f 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
@@ -17,6 +17,8 @@ export default {
'nav.minimize_tray': 'Minimaliseren naar systeemvak',
'tray.show': 'MeshBay tonen',
'tray.quit': 'Afsluiten',
+ 'tray.start_node': 'Node starten',
+ 'tray.stop_node': 'Node stoppen',
// Sidebar
'sidebar.my_groups': 'Mijn groepen',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
index 4e75670..67615a2 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
@@ -20,6 +20,8 @@ export default {
'nav.minimize_tray': 'Zminimalizuj do zasobnika',
'tray.show': 'Pokaż MeshBay',
'tray.quit': 'Zakończ',
+ 'tray.start_node': 'Uruchom węzeł',
+ 'tray.stop_node': 'Zatrzymaj węzeł',
// Sidebar
'sidebar.my_groups': 'Moje grupy',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
index a9fd638..2d07994 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
@@ -18,6 +18,8 @@ export default {
'nav.minimize_tray': 'Minimizar na bandeja',
'tray.show': 'Mostrar o MeshBay',
'tray.quit': 'Sair',
+ 'tray.start_node': 'Iniciar o nó',
+ 'tray.stop_node': 'Parar o nó',
// Sidebar
'sidebar.my_groups': 'Meus grupos',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
index ca39837..fd0c7f9 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
@@ -17,6 +17,8 @@ export default {
'nav.minimize_tray': '最小化到托盘',
'tray.show': '显示 MeshBay',
'tray.quit': '退出',
+ 'tray.start_node': '启动节点',
+ 'tray.stop_node': '停止节点',
// Sidebar
'sidebar.my_groups': '我的群组',