← back to Desktop Dotbar
dotbar: fix '0 live' (scan timeout) + resizable bar (drag/Tab/Shift+Tab)
ef1d433db12d43f9be50e2c9cb35ef8cc524d5b7 · 2026-09-16 08:31:20 -0700 · Steve Abrams
- 0 live: allcolordots --json takes ~9s but server execFile cap was 8s -> every
refresh timed out -> empty -> '0 live'. Bumped to 25s. Now shows 48 live.
- Resizable strip: drag the bottom grip (screen-Y absolute setBarH), or Tab=expand /
Shift+Tab=contract while the pointer is over the bar (hover-scoped globalShortcut so
Tab isn't hijacked fleet-wide). Height clamped 22-140px, persisted to .barh.
- Launcher: start-bar.command used `command -v node` which is empty under launchd
kickstart (no homebrew in PATH) -> server never started on a launchd restart. Added
absolute-path fallback so launchctl kickstart works.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AuZ7T3ffxRGXj5rrwgbquD
Files touched
M electron-main.jsM preload.jsM public/index.htmlM server.jsM start-bar.command
Diff
commit ef1d433db12d43f9be50e2c9cb35ef8cc524d5b7
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Sep 16 08:31:20 2026 -0700
dotbar: fix '0 live' (scan timeout) + resizable bar (drag/Tab/Shift+Tab)
- 0 live: allcolordots --json takes ~9s but server execFile cap was 8s -> every
refresh timed out -> empty -> '0 live'. Bumped to 25s. Now shows 48 live.
- Resizable strip: drag the bottom grip (screen-Y absolute setBarH), or Tab=expand /
Shift+Tab=contract while the pointer is over the bar (hover-scoped globalShortcut so
Tab isn't hijacked fleet-wide). Height clamped 22-140px, persisted to .barh.
- Launcher: start-bar.command used `command -v node` which is empty under launchd
kickstart (no homebrew in PATH) -> server never started on a launchd restart. Added
absolute-path fallback so launchctl kickstart works.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AuZ7T3ffxRGXj5rrwgbquD
---
electron-main.js | 45 +++++++++++++++++++++++++++++++++++++++------
preload.js | 3 +++
public/index.html | 41 ++++++++++++++++++++++++++++++++++++++++-
server.js | 4 +++-
start-bar.command | 7 +++++--
5 files changed, 90 insertions(+), 10 deletions(-)
diff --git a/electron-main.js b/electron-main.js
index 317fff5..e3144e9 100644
--- a/electron-main.js
+++ b/electron-main.js
@@ -2,15 +2,23 @@
// 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 { app, BrowserWindow, ipcMain, screen, globalShortcut } = 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;
+const PANEL_H = 360;
+const BAR_DEFAULT = 48, BAR_MIN = 22, BAR_MAX = 140, BAR_STEP = 8;
+const BARH_FILE = path.join(DIR, '.barh');
let win, serverProc, PORT = null;
+let isOpen = false; // is a dropdown panel open
+let barH = readBarH(); // resizable strip height (persisted)
+
+function clampBar(h) { return Math.max(BAR_MIN, Math.min(BAR_MAX, Math.round(h || BAR_DEFAULT))); }
+function readBarH() { try { return clampBar(parseInt(fs.readFileSync(BARH_FILE, 'utf8'), 10)); } catch { return BAR_DEFAULT; } }
+function writeBarH() { try { fs.writeFileSync(BARH_FILE, String(barH)); } catch {} }
function portFromFile() { try { return parseInt(fs.readFileSync(path.join(DIR, '.port'), 'utf8'), 10); } catch { return null; } }
function alive(port) {
@@ -40,7 +48,30 @@ async function ensureServer() {
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 };
+ return { x: wa.x, y: wa.y, width: wa.width, height: open ? barH + PANEL_H : barH };
+}
+
+// Apply the current barH (+ open state) to the window and tell the renderer its strip height.
+function applyBounds() { if (!win) return; win.setBounds(bounds(isOpen)); try { win.setAlwaysOnTop(true, 'screen-saver'); win.moveTop(); } catch {} }
+function pushBarH() { if (win) try { win.webContents.send('dotbar:barH', barH); } catch {} }
+function setBarH(h) { barH = clampBar(h); writeBarH(); applyBounds(); pushBarH(); }
+
+// Keyboard resize: Tab = expand, Shift+Tab = contract. The bar is a non-focusable panel so it
+// can't receive key events on its own; we register these as GLOBAL shortcuts but ONLY while the
+// pointer is over the bar (renderer sends dotbar:hover), so Tab isn't hijacked fleet-wide.
+let hotkeysOn = false;
+function setHotkeys(on) {
+ if (on === hotkeysOn) return;
+ try {
+ if (on) {
+ globalShortcut.register('Tab', () => setBarH(barH + BAR_STEP));
+ globalShortcut.register('Shift+Tab', () => setBarH(barH - BAR_STEP));
+ } else {
+ globalShortcut.unregister('Tab');
+ globalShortcut.unregister('Shift+Tab');
+ }
+ hotkeysOn = on;
+ } catch {}
}
async function createWindow() {
@@ -61,15 +92,17 @@ async function createWindow() {
const raise = () => { try { win.setAlwaysOnTop(true, 'screen-saver'); win.moveTop(); } catch {} };
const dbg = (tag) => { if (process.env.DOTBAR_DEBUG !== '1') return; 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-finish-load', () => { raise(); pushBarH(); 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:setOpen', (_e, open) => { isOpen = !!open; applyBounds(); });
+ ipcMain.on('dotbar:setBarH', (_e, h) => setBarH(h)); // absolute height (edge-drag)
+ ipcMain.on('dotbar:hover', (_e, on) => setHotkeys(!!on)); // enable Tab/Shift+Tab only over the bar
ipcMain.on('dotbar:quit', () => app.quit());
}
app.on('window-all-closed', () => app.quit());
-app.on('before-quit', () => { if (serverProc) try { serverProc.kill(); } catch {} });
+app.on('before-quit', () => { try { globalShortcut.unregisterAll(); } catch {} 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));
diff --git a/preload.js b/preload.js
index c5be619..2985497 100644
--- a/preload.js
+++ b/preload.js
@@ -1,5 +1,8 @@
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('dotbar', {
setOpen: (open) => ipcRenderer.send('dotbar:setOpen', !!open),
+ setBarH: (h) => ipcRenderer.send('dotbar:setBarH', h), // absolute strip height (edge-drag)
+ hover: (on) => ipcRenderer.send('dotbar:hover', !!on), // arm Tab/Shift+Tab only over the bar
+ onBarH: (cb) => ipcRenderer.on('dotbar:barH', (_e, h) => cb(h)),
quit: () => ipcRenderer.send('dotbar:quit'),
});
diff --git a/public/index.html b/public/index.html
index 642e00b..e3efd9a 100644
--- a/public/index.html
+++ b/public/index.html
@@ -14,6 +14,14 @@
#bar { height: var(--bar-h); display:flex; align-items:center; gap:4px; padding:0 10px;
background:linear-gradient(180deg,#191c22,#14161a); border-bottom:1px solid var(--line);
-webkit-app-region: drag; }
+ /* drag-to-resize strip along the bottom edge of the bar (Steve 2026-09-16). Sits exactly on the
+ bar's lower border; drag it up/down to change the strip height. Kept OUT of #bar (renderBar()
+ wipes #bar every tick), so it lives as a fixed sibling that tracks --bar-h. */
+ #grip { position:fixed; left:0; right:0; top: calc(var(--bar-h) - 4px); height:8px; z-index:50;
+ cursor:ns-resize; -webkit-app-region:no-drag; }
+ #grip::after { content:""; position:absolute; left:50%; top:3px; width:46px; height:2px;
+ transform:translateX(-50%); border-radius:2px; background:var(--line); opacity:.5; }
+ #grip:hover::after { opacity:1; background:var(--dim); }
.chip { -webkit-app-region:no-drag; cursor:pointer; display:flex; align-items:center; gap:6px;
padding:6px 10px; border-radius:9px; border:1px solid transparent; line-height:1; white-space:nowrap; }
.chip:hover { background:var(--bg2); border-color:var(--line); }
@@ -53,10 +61,11 @@
</head>
<body>
<div id="bar"></div>
+ <div id="grip" title="drag to resize · Tab = expand · Shift+Tab = contract"></div>
<div id="panel"></div>
<script>
const BAR_H = 48, PANEL_H = 360;
-let openColor = null, data = null, _miss = 0;
+let openColor = null, data = null, _miss = 0, curBarH = BAR_H;
// The four needs-Steve states pulse; green/pink stay solid (Steve 2026-09-15 dot-flash directive).
const WAIT = new Set(['lightblue','orange','purple','yellow']);
@@ -129,6 +138,36 @@ async function tick(){
renderBar(); renderPanel();
}
+// ---- Resize the bar (Steve 2026-09-16): drag the bottom grip, or Tab/Shift+Tab over the bar ----
+// Main process owns the window height; here we (a) mirror it into --bar-h, (b) turn grip drags into
+// absolute setBarH calls (screen-Y based so it stays stable as the window resizes under the cursor),
+// and (c) arm the Tab/Shift+Tab global shortcuts only while the pointer is over the bar.
+if (window.dotbar && window.dotbar.onBarH) {
+ window.dotbar.onBarH(h => { curBarH = h; document.documentElement.style.setProperty('--bar-h', h + 'px'); });
+}
+(function wireGrip(){
+ const grip = document.getElementById('grip');
+ if (!grip) return;
+ let dragging = false, startY = 0, startH = 0;
+ grip.addEventListener('pointerdown', e => {
+ dragging = true; startY = e.screenY; startH = curBarH;
+ try { grip.setPointerCapture(e.pointerId); } catch(_) {}
+ e.preventDefault();
+ });
+ grip.addEventListener('pointermove', e => {
+ if (!dragging) return;
+ const h = startH + (e.screenY - startY);
+ if (window.dotbar && window.dotbar.setBarH) window.dotbar.setBarH(h);
+ });
+ const end = e => { dragging = false; try { grip.releasePointerCapture(e.pointerId); } catch(_) {} };
+ grip.addEventListener('pointerup', end);
+ grip.addEventListener('pointercancel', end);
+})();
+// arm keyboard resize only while the pointer is over the bar (so Tab isn't hijacked fleet-wide)
+const arm = on => { if (window.dotbar && window.dotbar.hover) window.dotbar.hover(on); };
+document.documentElement.addEventListener('mouseenter', () => arm(true));
+document.documentElement.addEventListener('mouseleave', () => arm(false));
+
// optional deep-link: ?open=orange pre-opens that dropdown
const _pre = new URLSearchParams(location.search).get('open');
resize(false);
diff --git a/server.js b/server.js
index 24cf044..67f9297 100755
--- a/server.js
+++ b/server.js
@@ -42,7 +42,9 @@ function ticketOf(label) {
}
async function getDots() {
- const { stdout } = await run('bash', [ALLCOLORDOTS, '--json']);
+ // allcolordots scans every terminal and takes ~9-10s on a busy fleet; the default 8s cap
+ // timed out every refresh -> empty output -> the bar showed "0 live". Give it 25s headroom.
+ const { stdout } = await run('bash', [ALLCOLORDOTS, '--json'], 25000);
let rows = [];
try { rows = JSON.parse(stdout || '[]'); } catch { rows = []; }
rows = rows.filter(r => r && r.live); // only live sessions count as "running"
diff --git a/start-bar.command b/start-bar.command
index 508b21e..981019b 100755
--- a/start-bar.command
+++ b/start-bar.command
@@ -12,8 +12,11 @@ pkill -f "Electron.*desktop-dotbar" 2>/dev/null
pkill -f "desktop-dotbar/server.js" 2>/dev/null
sleep 0.4
-# 1) start the server with real node (absolute path so pkill can find it later)
-NODE="$(command -v node)"
+# 1) start the server with real node (absolute path so pkill can find it later).
+# launchd's PATH lacks homebrew, so `command -v node` is empty under `launchctl kickstart` —
+# fall back to the known homebrew/usr locations so a launchd restart actually starts the server.
+NODE="$(command -v node || true)"
+[ -x "$NODE" ] || NODE="$(for n in /opt/homebrew/bin/node /usr/local/bin/node /usr/bin/node; do [ -x "$n" ] && echo "$n" && break; done)"
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
← aa5dd2c TK-11794: pulse the 4 needs-Steve dots on the bar; green/pin
·
back to Desktop Dotbar
·
dotbar: keep-last-good so a slow/empty scan never flaps the f72f77b →