[object Object]

← back to Desktop Dotbar

dotbar: render a strip on EVERY display (Arrange button reachable on both monitors)

2f67aa2df96cae050d352ccc102fb19f81c3f3c5 · 2026-09-16 10:42:58 -0700 · Steve Abrams

One BrowserWindow per screen, pinned to each display's top edge; dropdown-open tracked
per-window, grip-resize applied to all; rebuilds on monitor plug/unplug. (Steve 2026-09-16)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Szengr4hMUDtYdw3jSCfA

Files touched

Diff

commit 2f67aa2df96cae050d352ccc102fb19f81c3f3c5
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Sep 16 10:42:58 2026 -0700

    dotbar: render a strip on EVERY display (Arrange button reachable on both monitors)
    
    One BrowserWindow per screen, pinned to each display's top edge; dropdown-open tracked
    per-window, grip-resize applied to all; rebuilds on monitor plug/unplug. (Steve 2026-09-16)
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_013Szengr4hMUDtYdw3jSCfA
---
 .gitignore       |  1 +
 electron-main.js | 93 ++++++++++++++++++++++++++++++++++++++++----------------
 2 files changed, 67 insertions(+), 27 deletions(-)

diff --git a/.gitignore b/.gitignore
index 3a43e88..389368a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@ tmp/
 .chrome-profile/
 debug.log
 .barh
+electron-main.js.bak-*
diff --git a/electron-main.js b/electron-main.js
index 1febbbd..5a6768d 100644
--- a/electron-main.js
+++ b/electron-main.js
@@ -1,6 +1,12 @@
 // 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.
+//
+// MULTI-DISPLAY (Steve 2026-09-16): one strip PER display, each pinned to the top edge of
+// its OWN screen, so the dot chips + the ⧉ Arrange button are reachable on every monitor
+// (the button used to live only on the primary/left screen while Steve works on the right).
+// Each strip is an independent BrowserWindow; open-state (dropdown grow) is tracked per window,
+// and grip-resize applies to all strips so they stay the same height.
 'use strict';
 const { app, BrowserWindow, ipcMain, screen } = require('electron');
 const { spawn } = require('child_process');
@@ -12,9 +18,10 @@ const DIR = __dirname;
 const PANEL_H = 360;
 const BAR_DEFAULT = 48, BAR_MIN = 22, BAR_MAX = 140;
 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)
+let wins = [];                                       // one strip per display
+let serverProc, PORT = null;
+const openState = new Map();                         // win.id -> is that strip's dropdown open
+let barH = readBarH();                               // resizable strip height (persisted, shared)
 
 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; } }
@@ -46,49 +53,81 @@ async function ensureServer() {
   return portFromFile() || 9787;
 }
 
-function bounds(open) {
-  const wa = screen.getPrimaryDisplay().workArea; // excludes the menu bar
+// Bounds for a strip on a given display: full display width, at its workArea top (below the menu
+// bar, which only the primary display has). Grows downward by PANEL_H when its dropdown is open.
+function boundsFor(display, open) {
+  const wa = display.workArea;
   return { x: wa.x, y: wa.y, width: wa.width, height: open ? barH + PANEL_H : barH };
 }
+function displayOf(w) {
+  return screen.getAllDisplays().find(d => d.id === w._displayId) || screen.getPrimaryDisplay();
+}
+function raise(w) { try { if (!w.isDestroyed()) { w.setAlwaysOnTop(true, 'screen-saver'); w.moveTop(); } } catch {} }
+function applyBoundsFor(w) {
+  if (!w || w.isDestroyed()) return;
+  w.setBounds(boundsFor(displayOf(w), !!openState.get(w.id)));
+  raise(w);
+}
+function pushBarH(w) { try { if (!w.isDestroyed()) w.webContents.send('dotbar:barH', barH); } catch {} }
 
-// 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 {} }
-// setBarH fires on EVERY pointermove during a grip drag, so keep it cheap: resize the window and
-// mirror the height to the renderer live, but do NOT re-assert always-on-top per frame (the 2s
-// `raise` interval covers that) and do NOT hit the disk per frame — persist once, debounced, after
-// the drag settles. Resize is the sole affordance now (keyboard resize was dropped, DTD 2026-09-16).
+// setBarH fires on EVERY pointermove during a grip drag, so keep it cheap: resize each strip and
+// mirror the height to its renderer live, but do NOT re-assert always-on-top per frame (the 2s
+// raise interval covers that) and do NOT hit the disk per frame — persist once, debounced.
 let _persistTimer = null;
 function setBarH(h) {
   barH = clampBar(h);
-  if (win) win.setBounds(bounds(isOpen));                 // live resize only; no per-frame moveTop
-  pushBarH();
+  for (const w of wins) {
+    if (w.isDestroyed()) continue;
+    w.setBounds(boundsFor(displayOf(w), !!openState.get(w.id)));   // live resize only; no per-frame moveTop
+    pushBarH(w);
+  }
   clearTimeout(_persistTimer); _persistTimer = setTimeout(writeBarH, 300);
 }
 
-async function createWindow() {
-  PORT = await ensureServer();
-  win = new BrowserWindow({
-    ...bounds(false),
+function createBarFor(display) {
+  const win = new BrowserWindow({
+    ...boundsFor(display, 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._displayId = display.id;
   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) => { 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(); pushBarH(); dbg('did-finish-load'); });
+    JSON.stringify({ tag, id: win.id, t: new Date().toISOString(), bounds: win.isDestroyed() ? null : win.getBounds() }) + '\n'); } catch {} };
+  win.webContents.on('did-finish-load', () => { raise(win); pushBarH(win); 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) => { isOpen = !!open; applyBounds(); });
-  ipcMain.on('dotbar:setBarH', (_e, h) => setBarH(h));          // absolute height (grip edge-drag)
+  wins.push(win);
+  return win;
+}
+
+// Tear down and recreate every strip — used on monitor plug/unplug so a display always has its bar
+// and a removed display leaves no orphan window.
+function rebuildBars() {
+  for (const w of wins) { try { if (!w.isDestroyed()) w.destroy(); } catch {} }
+  wins = []; openState.clear();
+  for (const d of screen.getAllDisplays()) createBarFor(d);
+}
+
+async function createWindows() {
+  PORT = await ensureServer();
+  for (const d of screen.getAllDisplays()) createBarFor(d);
+  setInterval(() => wins.forEach(raise), 2000);   // re-assert top above other apps, every strip
+  // React to monitor changes (a display added/removed, or geometry changed e.g. arrangement swap).
+  screen.on('display-added', rebuildBars);
+  screen.on('display-removed', rebuildBars);
+  // Per-strip dropdown open/grow: grow the window that actually sent the event, not a global one.
+  ipcMain.on('dotbar:setOpen', (e, open) => {
+    const w = BrowserWindow.fromWebContents(e.sender);
+    if (!w) return;
+    openState.set(w.id, !!open);
+    applyBoundsFor(w);
+  });
+  ipcMain.on('dotbar:setBarH', (_e, h) => setBarH(h));   // grip drag: applies to all strips
   ipcMain.on('dotbar:quit', () => app.quit());
 }
 
@@ -96,6 +135,6 @@ 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));
+  createWindows().catch(e => console.log('DOTBAR: createWindows ERROR', e && e.stack || e));
 });
 process.on('uncaughtException', e => console.log('DOTBAR: uncaught', e && e.stack || e));

← 158144e auto-data-snapshot: 2026-09-16T10:40:23 (1 data files) — ele  ·  back to Desktop Dotbar  ·  dotbar: surface Arrange outcome on the button instead of swa 534bea8 →