← back to Dot Palette
electron-main.js
86 lines
// dot-palette — Electron shell: a small always-on-top, NON-ACTIVATING NSPanel
// with 6 colored chips. Clicking a chip sets the CURRENT iTerm2 tab's status dot
// via the local server (which resolves iTerm2's current-session tty + calls the
// dot engine). Reuses the desktop-dotbar NSPanel recipe: type:'panel' +
// focusable:false + alwaysOnTop('screen-saver') so clicks never steal focus from
// the terminal you're working in.
'use strict';
const { app, BrowserWindow, ipcMain, screen } = require('electron');
const { spawn } = require('child_process');
const http = require('http');
const fs = require('fs');
const path = require('path');
const DIR = __dirname;
const W = 250, H = 52;
const POS_FILE = path.join(DIR, '.pos');
let win, serverProc, PORT = null;
function portFromFile() { try { return parseInt(fs.readFileSync(path.join(DIR, '.port'), 'utf8'), 10); } catch { return null; } }
function alive(port) {
return new Promise(res => {
if (!port) return res(false);
const r = http.get({ host: '127.0.0.1', port, path: '/health', timeout: 800 }, x => { x.resume(); res(x.statusCode === 200); });
r.on('error', () => res(false)); r.on('timeout', () => { r.destroy(); res(false); });
});
}
// Spawn our own server via Electron-as-node (no separate node install needed).
async function ensureServer() {
// Code-enforce production isolation at the real chokepoint every launch funnels
// through: strip server.js's test seams from the child env so they can NEVER leak
// in from an inherited shell env (belt-and-suspenders with start.command's unset).
const env = { ...process.env, ELECTRON_RUN_AS_NODE: '1' };
for (const k of ['DOTPALETTE_ENGINE', 'DOTPALETTE_ENGINE_BIN', 'DOTPALETTE_TTY_OVERRIDE', 'DOTPALETTE_PORT_FILE']) delete env[k];
serverProc = spawn(process.execPath, [path.join(DIR, 'server.js')], {
stdio: 'ignore', env });
for (let i = 0; i < 40; i++) {
await new Promise(r => setTimeout(r, 150));
const p = portFromFile();
if (await alive(p)) return p;
}
return portFromFile() || 9791;
}
function readPos() {
try {
const [x, y] = fs.readFileSync(POS_FILE, 'utf8').split(',').map(n => parseInt(n, 10));
if (Number.isFinite(x) && Number.isFinite(y)) return { x, y };
} catch {}
const wa = screen.getPrimaryDisplay().workArea; // default: top-right of primary
return { x: wa.x + wa.width - W - 24, y: wa.y + 24 };
}
function writePos() { try { if (win && !win.isDestroyed()) { const b = win.getBounds(); fs.writeFileSync(POS_FILE, `${b.x},${b.y}`); } } catch {} }
function raise() { try { if (win && !win.isDestroyed()) { win.setAlwaysOnTop(true, 'screen-saver'); win.moveTop(); } } catch {} }
async function createWindow() {
PORT = await ensureServer();
const pos = readPos();
win = new BrowserWindow({
x: pos.x, y: pos.y, width: W, height: H,
type: 'panel', // NSPanel: floats above normal windows, non-activating
frame: false, resizable: false, movable: true, minimizable: false, maximizable: false,
fullscreenable: false, skipTaskbar: true, hasShadow: true, transparent: false,
focusable: false, // never steal focus from the terminal being colored
backgroundColor: '#14161a',
webPreferences: { preload: path.join(DIR, 'preload.js'), contextIsolation: true, nodeIntegration: false },
});
win.setAlwaysOnTop(true, 'screen-saver');
win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
win.loadURL(`http://127.0.0.1:${PORT}/`);
win.on('moved', writePos);
setInterval(raise, 2000); // re-assert top above other apps
ipcMain.on('palette:quit', () => app.quit());
ipcMain.on('palette:move', (_e, dx, dy) => {
if (!win || win.isDestroyed()) return;
const b = win.getBounds(); win.setBounds({ ...b, x: b.x + Math.round(dx), y: b.y + Math.round(dy) }); writePos();
});
}
app.on('window-all-closed', () => app.quit());
app.on('before-quit', () => { if (serverProc) try { serverProc.kill(); } catch {} });
app.whenReady().then(() => {
if (app.dock) app.dock.hide(); // accessory app: panel floats above active apps
createWindow().catch(e => console.log('PALETTE createWindow ERROR', e && e.stack || e));
});
process.on('uncaughtException', e => console.log('PALETTE uncaught', e && e.stack || e));