summaryrefslogtreecommitdiffstats
path: root/packages
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 /packages
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
Diffstat (limited to 'packages')
-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 71b56bed..273d1511 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 394604da..98fd63b2 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 3e533fb3..9cf613d8 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 58578217..c7e46d8e 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 79a3a087..90d91948 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 24ff315d..46168cff 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 ce08f1f3..2d38e4bc 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 7aa1d98e..471ad820 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 b87cb810..48f26a2b 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 242bdb48..564f34b5 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 8c8f6547..3eed3059 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 566677d7..711a22f4 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 4e75670d..67615a2c 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 a9fd6381..2d079949 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 ca39837d..fd0c7f96 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': '我的群组',