← 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));