summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-client/src/main.js
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-client/src/main.js')
-rw-r--r--packages/meshbay-client/src/main.js68
1 files changed, 63 insertions, 5 deletions
diff --git a/packages/meshbay-client/src/main.js b/packages/meshbay-client/src/main.js
index e996829..71b56be 100644
--- a/packages/meshbay-client/src/main.js
+++ b/packages/meshbay-client/src/main.js
@@ -20,7 +20,8 @@
'use strict';
-const { app, BrowserWindow, dialog, ipcMain, protocol, safeStorage, shell } =
+const { app, BrowserWindow, dialog, ipcMain, Menu, protocol, safeStorage,
+ shell, Tray } =
require('electron');
const { execFile, spawn } = require('node:child_process');
const crypto = require('node:crypto');
@@ -326,6 +327,56 @@ const HUB_FETCH_TIMEOUT_MS = 30000;
let mainWindow = null;
+// ── System tray ──────────────────────────────────────────────────────────────
+// Linux only for now; Windows is being done on that OS.
+//
+// The context menu is not optional. Under libappindicator -- which is how GNOME
+// shows a tray at all, via the AppIndicator extension -- `tray.on('click')`
+// never fires: the indicator only opens its menu. A tray whose only affordance
+// was a click would be inert on the one desktop this targets.
+//
+// Labels arrive from the renderer rather than being translated here. The locale
+// 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;
+
+function showFromTray() {
+ if (!mainWindow) return;
+ if (!mainWindow.isVisible()) mainWindow.show();
+ if (mainWindow.isMinimized()) mainWindow.restore();
+ mainWindow.focus();
+}
+
+function ensureTray(labels) {
+ const text = {
+ show: (labels && labels.show) || 'Show MeshBay',
+ quit: (labels && labels.quit) || 'Quit',
+ };
+ 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() },
+ ]));
+ return tray;
+ }
+ // In src/, not build/: electron-builder packages only `src/**` and `ui/**`
+ // (package.json `files`), so an icon under build/ exists in a dev run and is
+ // 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() },
+ ]));
+ // Harmless where it does nothing (GNOME), useful on desktops that do send it.
+ tray.on('click', showFromTray);
+ return tray;
+}
+
+
function createWindow() {
const bounds = (config.window && config.window.width) ? config.window : {
width: 1200, height: 800,
@@ -541,6 +592,16 @@ function registerBridge() {
} catch { return null; }
});
+ // Hide, never close: `window-all-closed` quits the app, and closing here would
+ // make "minimise to tray" mean "exit". A hidden window keeps the session, the
+ // transfers and the node connection exactly as they were.
+ ipcMain.handle('window:minimize-to-tray', (_e, labels) => {
+ if (process.platform !== 'linux' || !mainWindow) return false;
+ ensureTray(labels);
+ mainWindow.hide();
+ return true;
+ });
+
ipcMain.handle('device:ensure', () => ensureDeviceKey());
ipcMain.handle('device:public', () => {
const key = deviceKey();
@@ -1477,10 +1538,7 @@ if (!app.requestSingleInstanceLock()) {
app.quit();
} else {
app.on('second-instance', () => {
- if (mainWindow) {
- if (mainWindow.isMinimized()) mainWindow.restore();
- mainWindow.focus();
- }
+ showFromTray();
});
app.whenReady().then(() => {