← 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
M .gitignoreM .portA README.mdA electron-main.jsA package-lock.jsonA package.jsonA preload.jsM public/index.htmlM server.jsM start-bar.command
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 →