[object Object]

← back to Desktop Dotbar

Electron always-on-top strip: color-dot counts, ticket dropdowns, jump-to-terminal, cached feed

994870a0cdf43ab09e2bd57d5ce6d36270e2d872 · 2026-09-15 18:10:21 -0700 · Steve

Files touched

Diff

commit 994870a0cdf43ab09e2bd57d5ce6d36270e2d872
Author: Steve <steve@designerwallcoverings.com>
Date:   Tue Sep 15 18:10:21 2026 -0700

    Electron always-on-top strip: color-dot counts, ticket dropdowns, jump-to-terminal, cached feed
---
 .gitignore        |   3 +
 .port             |   2 +-
 README.md         |  14 +++++
 electron-main.js  |  77 ++++++++++++++++++++++++++
 package-lock.json | 161 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 package.json      |  12 ++++
 preload.js        |   5 ++
 public/index.html |  10 +++-
 server.js         |  16 +++++-
 start-bar.command |  45 +++++++--------
 10 files changed, 315 insertions(+), 30 deletions(-)

diff --git a/.gitignore b/.gitignore
index b38eead..b81d59e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,6 @@ node_modules/
 *.log
 .DS_Store
 tmp/
+.port
+.chrome-profile/
+debug.log
diff --git a/.port b/.port
index 3252c40..f38b733 100644
--- a/.port
+++ b/.port
@@ -1 +1 @@
-9788
\ No newline at end of file
+9787
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e5084c4
--- /dev/null
+++ b/README.md
@@ -0,0 +1,14 @@
+# desktop-dotbar
+
+Always-on-top strip across the top of the desktop showing each terminal **color dot + live count**.
+Click a color → dropdown of that color's tickets + what each is doing → **open terminal** jumps to
+that iTerm2 session. Data comes from `allcolordots --json` (cached server-side for a smooth bar).
+
+## Run
+    bash start-bar.command      # starts server + floats the Electron strip
+Quit with the **✕** on the bar, or: `pkill -f desktop-dotbar`
+
+## Pieces
+- `server.js`     — node http server: `/api/dots` (cached), `/health`, `/api/reveal` (jump to tty)
+- `electron-main.js` — frameless NSPanel, alwaysOnTop 'screen-saver', self-resizes on dropdown
+- `public/index.html` — the bar UI (`?open=<color>` deep-links a dropdown open)
diff --git a/electron-main.js b/electron-main.js
new file mode 100644
index 0000000..0cd6b6d
--- /dev/null
+++ b/electron-main.js
@@ -0,0 +1,77 @@
+// Electron shell: a true always-on-top strip across the top of the desktop.
+// Spawns the local server, then floats a frameless bar above every window
+// (including fullscreen) at 'screen-saver' level. Grows only when a dropdown opens.
+'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 BAR_H = 48, PANEL_H = 360;
+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); });
+  });
+}
+// The launcher (start-bar.command) starts the node server; we just wait for it.
+async function ensureServer() {
+  for (let i = 0; i < 50; i++) {
+    const p = portFromFile();
+    if (await alive(p)) return p;
+    await new Promise(r => setTimeout(r, 200));
+  }
+  // last resort: spawn it ourselves via Electron-as-node
+  serverProc = spawn(process.execPath, [path.join(DIR, 'server.js')], {
+    stdio: 'ignore', env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' } });
+  for (let i = 0; i < 30; i++) {
+    await new Promise(r => setTimeout(r, 200));
+    const p = portFromFile();
+    if (await alive(p)) return p;
+  }
+  return portFromFile() || 9787;
+}
+
+function bounds(open) {
+  const wa = screen.getPrimaryDisplay().workArea; // excludes the menu bar
+  return { x: wa.x, y: wa.y, width: wa.width, height: open ? BAR_H + PANEL_H : BAR_H };
+}
+
+async function createWindow() {
+  PORT = await ensureServer();
+  win = new BrowserWindow({
+    ...bounds(false),
+    type: 'panel',                 // NSPanel: floats above normal windows, non-activating
+    frame: false, resizable: false, movable: true, minimizable: false, maximizable: false,
+    fullscreenable: false, skipTaskbar: true, hasShadow: false, transparent: false,
+    focusable: false, backgroundColor: '#14161a',
+    webPreferences: { preload: path.join(DIR, 'preload.js'), contextIsolation: true, nodeIntegration: false },
+  });
+  win.setAlwaysOnTop(true, 'screen-saver');       // above normal + fullscreen windows
+  win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
+  win.loadURL(`http://127.0.0.1:${PORT}/`);
+  // Re-assert top without the off→on toggle (which caused a visible drop-flicker).
+  // setAlwaysOnTop(true, ...) re-elevates on its own, including after setBounds.
+  const raise = () => { try { win.setAlwaysOnTop(true, 'screen-saver'); win.moveTop(); } catch {} };
+  const dbg = (tag) => { try { fs.appendFileSync(path.join(DIR, 'debug.log'),
+    JSON.stringify({ tag, t: new Date().toISOString(), bounds: win.getBounds(), visible: win.isVisible(), onTop: win.isAlwaysOnTop() }) + '\n'); } catch {} };
+  win.webContents.on('did-finish-load', () => { raise(); dbg('did-finish-load'); });
+  win.webContents.on('did-fail-load', (_e, code, desc) => dbg('FAIL:' + code + ':' + desc));
+  setInterval(raise, 2000);   // re-assert top above other apps
+  ipcMain.on('dotbar:setOpen', (_e, open) => { if (win) { win.setBounds(bounds(!!open)); raise(); } });
+  ipcMain.on('dotbar:quit', () => app.quit());
+}
+
+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: panels float above active apps
+  createWindow().catch(e => console.log('DOTBAR: createWindow ERROR', e && e.stack || e));
+});
+process.on('uncaughtException', e => console.log('DOTBAR: uncaught', e && e.stack || e));
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..1218f1c
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,161 @@
+{
+  "name": "desktop-dotbar",
+  "version": "1.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "desktop-dotbar",
+      "version": "1.0.0",
+      "dependencies": {
+        "electron": "^44.4.0"
+      }
+    },
+    "node_modules/@electron-internal/extract-zip": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/@electron-internal/extract-zip/-/extract-zip-1.0.5.tgz",
+      "integrity": "sha512-+bqFCP98pLI0Tt0XQo1TmlXtwjWchISndDOxCkEcIuUgXWpBnLyRI+2DU+mesvnMMX6L1XDqYNA0lXNDHd/yiA==",
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=22.12.0"
+      }
+    },
+    "node_modules/@electron/get": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/@electron/get/-/get-5.1.0.tgz",
+      "integrity": "sha512-3kSBtG8ObcTVfXanm5vVJ6UnBLEVmVsRk1M+vGqCuMBV+XLCbJYuWQful+yIy0GQDsSlK0kHEriEHn7SPk4EnA==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.1.1",
+        "env-paths": "^3.0.0",
+        "graceful-fs": "^4.2.11",
+        "progress": "^2.0.3",
+        "semver": "^7.6.3",
+        "sumchecker": "^3.0.1"
+      },
+      "engines": {
+        "node": ">=22.12.0"
+      },
+      "optionalDependencies": {
+        "undici": "^7.24.4"
+      }
+    },
+    "node_modules/@types/node": {
+      "version": "24.13.5",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.5.tgz",
+      "integrity": "sha512-TXyindR+lBr22aJIdMQzCFHPHR6cR4js838mRDCSz5hOKWZvZwsXSSiXDmjRj4iJmgl+sR9O+1mkoVBSMadNug==",
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~7.18.0"
+      }
+    },
+    "node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/electron": {
+      "version": "44.4.0",
+      "resolved": "https://registry.npmjs.org/electron/-/electron-44.4.0.tgz",
+      "integrity": "sha512-Za6Y3ZXfHWo0wcutNKjQXF/+Xjj1lGdA31ptYC3pqSXOKqwEBk+ip/nTxKMH+zH9Fl16Ia0ILN9FwSvnpC+NKA==",
+      "license": "MIT",
+      "dependencies": {
+        "@electron-internal/extract-zip": "^1.0.1",
+        "@electron/get": "^5.0.0",
+        "@types/node": "^24.9.0"
+      },
+      "bin": {
+        "electron": "cli.js",
+        "install-electron": "install.js"
+      },
+      "engines": {
+        "node": ">= 22.12.0"
+      }
+    },
+    "node_modules/env-paths": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz",
+      "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==",
+      "license": "MIT",
+      "engines": {
+        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/graceful-fs": {
+      "version": "4.2.11",
+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+      "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+      "license": "ISC"
+    },
+    "node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/progress": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
+      "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/semver": {
+      "version": "7.8.5",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+      "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/sumchecker": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
+      "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "debug": "^4.1.0"
+      },
+      "engines": {
+        "node": ">= 8.0"
+      }
+    },
+    "node_modules/undici": {
+      "version": "7.29.1",
+      "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.1.tgz",
+      "integrity": "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==",
+      "license": "MIT",
+      "optional": true,
+      "engines": {
+        "node": ">=20.18.1"
+      }
+    },
+    "node_modules/undici-types": {
+      "version": "7.18.2",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
+      "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
+      "license": "MIT"
+    }
+  }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..8a47d44
--- /dev/null
+++ b/package.json
@@ -0,0 +1,12 @@
+{
+  "name": "desktop-dotbar",
+  "version": "1.0.0",
+  "private": true,
+  "main": "electron-main.js",
+  "scripts": {
+    "start": "electron ."
+  },
+  "dependencies": {
+    "electron": "^44.4.0"
+  }
+}
diff --git a/preload.js b/preload.js
new file mode 100644
index 0000000..c5be619
--- /dev/null
+++ b/preload.js
@@ -0,0 +1,5 @@
+const { contextBridge, ipcRenderer } = require('electron');
+contextBridge.exposeInMainWorld('dotbar', {
+  setOpen: (open) => ipcRenderer.send('dotbar:setOpen', !!open),
+  quit: () => ipcRenderer.send('dotbar:quit'),
+});
diff --git a/public/index.html b/public/index.html
index 4c8ab38..b4a9d4e 100644
--- a/public/index.html
+++ b/public/index.html
@@ -54,7 +54,8 @@ let openColor = null, data = null;
 async function fetchDots(){ try { const r = await fetch('/api/dots'); return await r.json(); } catch { return null; } }
 
 function resize(open){
-  // Chrome --app windows own their size: thin strip when closed, grow when a dropdown is open.
+  // Electron shell owns the window bounds via IPC; fall back to resizeTo in a plain browser.
+  if (window.dotbar && window.dotbar.setOpen){ window.dotbar.setOpen(open); return; }
   try { window.resizeTo(screen.availWidth, open ? BAR_H + PANEL_H : BAR_H); } catch(e){}
 }
 
@@ -73,7 +74,8 @@ function renderBar(){
   const sp = document.createElement('div'); sp.id='spacer'; bar.appendChild(sp);
   const meta = document.createElement('div'); meta.id='meta';
   meta.innerHTML = `<span><b>${data.total}</b> live</span>`
-    + `<span class="icobtn" title="refresh" onclick="tick()">⟳</span>`;
+    + `<span class="icobtn" title="refresh" onclick="tick()">⟳</span>`
+    + `<span class="icobtn" title="quit bar" onclick="(window.dotbar&&window.dotbar.quit)&&window.dotbar.quit()">✕</span>`;
   bar.appendChild(meta);
 }
 
@@ -115,8 +117,10 @@ async function tick(){
   renderBar(); renderPanel();
 }
 
+// optional deep-link: ?open=orange pre-opens that dropdown
+const _pre = new URLSearchParams(location.search).get('open');
 resize(false);
-tick();
+tick().then(() => { if (_pre) toggle(_pre); });
 setInterval(tick, 3000);
 </script>
 </body>
diff --git a/server.js b/server.js
index 86cb864..24cf044 100755
--- a/server.js
+++ b/server.js
@@ -100,10 +100,22 @@ function send(res, code, body, type = 'application/json') {
   res.end(typeof body === 'string' ? body : JSON.stringify(body));
 }
 
+// allcolordots is slow (scans every terminal), so refresh in the background and
+// serve a cached snapshot — /api/dots must return instantly for a smooth bar.
+let snapshot = { updated: 0, total: 0, groups: [], stale: true };
+let refreshing = false;
+async function refresh() {
+  if (refreshing) return;
+  refreshing = true;
+  try { snapshot = { ...(await getDots()), stale: false }; } catch (e) { /* keep last */ }
+  finally { refreshing = false; }
+}
+
 const server = http.createServer(async (req, res) => {
   try {
     const url = new URL(req.url, 'http://x');
-    if (url.pathname === '/api/dots') return send(res, 200, await getDots());
+    if (url.pathname === '/health') return send(res, 200, { ok: true, port: server.address() && server.address().port });
+    if (url.pathname === '/api/dots') { if (!snapshot.updated) await refresh(); refresh(); return send(res, 200, snapshot); }
     if (url.pathname === '/api/reveal' && req.method === 'POST') {
       let raw = '';
       req.on('data', c => (raw += c));
@@ -133,6 +145,8 @@ function listen(port, tries = 20) {
     const p = server.address().port;
     fs.writeFileSync(path.join(__dirname, '.port'), String(p));
     console.log(`desktop-dotbar on http://127.0.0.1:${p}`);
+    refresh();                       // warm the cache
+    setInterval(refresh, 2500);      // keep it fresh in the background
   });
 }
 listen(parseInt(process.env.PORT || '9787', 10));
diff --git a/start-bar.command b/start-bar.command
index 3ea619f..508b21e 100755
--- a/start-bar.command
+++ b/start-bar.command
@@ -1,31 +1,26 @@
 #!/usr/bin/env bash
-# Double-click to launch the desktop dot bar (or run: bash start-bar.command).
-# Starts the local server, then opens a frameless Chrome --app window pinned as a
-# thin strip across the top of the desktop. Idempotent: reuses a running server.
+# Double-click (or: bash start-bar.command) to launch the always-on-top desktop dot bar.
+# Starts the node server, then floats a frameless Electron strip across the top of the
+# desktop, above every window. Quit with the ✕ on the bar (or: pkill -f desktop-dotbar).
 set -u
 DIR="$HOME/Projects/desktop-dotbar"
 cd "$DIR" || exit 1
 
-# 1) ensure the server is up
-if [ -f .port ] && curl -sf "http://127.0.0.1:$(cat .port)/api/dots" >/dev/null 2>&1; then
-  PORT="$(cat .port)"
-  echo "server already up on :$PORT"
-else
-  pkill -f "node .*desktop-dotbar/server.js" 2>/dev/null
-  nohup node "$DIR/server.js" >"$DIR/server.log" 2>&1 &
-  for i in $(seq 1 30); do [ -f "$DIR/.port" ] && break; sleep 0.2; done
-  PORT="$(cat "$DIR/.port" 2>/dev/null || echo 9787)"
-  echo "server started on :$PORT"
-fi
+# tear down any previous instance (electron + this app's node server)
+pkill -f "desktop-dotbar/electron-main.js" 2>/dev/null
+pkill -f "Electron.*desktop-dotbar" 2>/dev/null
+pkill -f "desktop-dotbar/server.js" 2>/dev/null
+sleep 0.4
 
-# 2) open the Chrome --app strip across the top (self-resizes via window.resizeTo)
-CHROME="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
-PROFILE="$DIR/.chrome-profile"
-"$CHROME" \
-  --app="http://127.0.0.1:$PORT/" \
-  --user-data-dir="$PROFILE" \
-  --window-position=0,0 \
-  --window-size=4480,48 \
-  --no-first-run --no-default-browser-check --disable-features=Translate \
-  >/dev/null 2>&1 &
-echo "dot bar opened. Close this window."
+# 1) start the server with real node (absolute path so pkill can find it later)
+NODE="$(command -v node)"
+nohup "$NODE" "$DIR/server.js" >"$DIR/server.log" 2>&1 &
+for i in $(seq 1 40); do
+  [ -f "$DIR/.port" ] && curl -sf "http://127.0.0.1:$(cat "$DIR/.port")/health" >/dev/null 2>&1 && break
+  sleep 0.2
+done
+echo "server on :$(cat "$DIR/.port" 2>/dev/null)"
+
+# 2) float the Electron strip
+nohup "$DIR/node_modules/.bin/electron" "$DIR" >"$DIR/bar.log" 2>&1 &
+echo "dot bar launching… (close this window)"

← 1f2db8a desktop-dotbar: always-on-top color-dot nav strip with ticke  ·  back to Desktop Dotbar  ·  stop tracking transient .port 98783dc →