[object Object]

← back to Desktop Dotbar

dotbar: add top-strip Arrange button — one-click re-tile every screen via the dot-screen router

71dbcd1e5530ac98a5388f2e0f064fc4163e9614 · 2026-09-16 10:15:08 -0700 · Steve Abrams

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

Files touched

Diff

commit 71dbcd1e5530ac98a5388f2e0f064fc4163e9614
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Sep 16 10:15:08 2026 -0700

    dotbar: add top-strip Arrange button — one-click re-tile every screen via the dot-screen router
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_013Szengr4hMUDtYdw3jSCfA
---
 public/index.html | 22 ++++++++++++++++++++--
 server.js         | 17 +++++++++++++++++
 2 files changed, 37 insertions(+), 2 deletions(-)

diff --git a/public/index.html b/public/index.html
index 6d080e7..8951a88 100644
--- a/public/index.html
+++ b/public/index.html
@@ -44,6 +44,13 @@
   #meta b { color:var(--fg); font-variant-numeric:tabular-nums; }
   .icobtn { -webkit-app-region:no-drag; cursor:pointer; color:var(--dim); padding:4px 6px; border-radius:6px; }
   .icobtn:hover { background:var(--bg2); color:var(--fg); }
+  /* One-click re-tile of every screen (Steve 2026-09-16): after dragging windows around, click
+     this to snap them all back into the grid (green left, colour bands right). */
+  .arrbtn { -webkit-app-region:no-drag; cursor:pointer; font-weight:700; font-size:12px;
+    padding:5px 11px; border-radius:8px; border:1px solid #35507a; background:#1d2836; color:#cfe2ff;
+    white-space:nowrap; }
+  .arrbtn:hover { background:#25344a; border-color:#4a6ea8; }
+  .arrbtn.busy { opacity:.6; cursor:progress; }
   /* the dropdown region (only visible when a chip is open; window grows to fit) */
   #panel { display:none; background:var(--bg); border-bottom:1px solid var(--line);
     max-height: calc(100vh - var(--bar-h)); overflow:auto; }
@@ -68,7 +75,7 @@
   <div id="panel"></div>
 <script>
 const BAR_H = 48, PANEL_H = 360;
-let openColor = null, data = null, _miss = 0, curBarH = BAR_H;
+let openColor = null, data = null, _miss = 0, curBarH = BAR_H, arranging = false;
 // 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']);
 
@@ -94,7 +101,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>`
+  meta.innerHTML = `<button class="arrbtn${arranging?' busy':''}" title="re-tile every screen into the grid now" onclick="arrange()">${arranging?'⧉ Arranging…':'⧉ Arrange'}</button>`
+    + `<span><b>${data.total}</b> live</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);
@@ -129,6 +137,16 @@ async function reveal(tty){
   try { await fetch('/api/reveal', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({tty}) }); } catch(e){}
 }
 
+// One-click "Arrange": ask the server to run the dot-screen router now, re-tiling every screen
+// into the grid. Shows a busy state on the button until the (multi-pass, converging) run returns.
+async function arrange(){
+  if (arranging) return;
+  arranging = true; renderBar();
+  try { await fetch('/api/arrange', { method:'POST' }); } catch(e){}
+  arranging = false; renderBar();
+  tick();   // refresh counts/labels once windows have settled
+}
+
 async function tick(){
   const d = await fetchDots();
   if (!d) return;
diff --git a/server.js b/server.js
index 1018e47..e50b9e3 100755
--- a/server.js
+++ b/server.js
@@ -10,6 +10,7 @@ const fs = require('fs');
 const path = require('path');
 
 const ALLCOLORDOTS = `${process.env.HOME}/.claude/skills/allcolordots/allcolordots.sh`;
+const ROUTER = `${process.env.HOME}/.claude/skills/dot-screen-router/router.sh`;
 
 // Urgency order (matches allcolordots): needs-Steve first, working/parked last.
 const ORDER = ['lightblue', 'orange', 'purple', 'yellow', 'green', 'pink', 'none'];
@@ -98,6 +99,19 @@ return "notfound"`;
   return { ok: /ok/.test(stdout) && !err, result: (stdout || '').trim(), error: err ? String(err) : null };
 }
 
+// One-click re-tile: run the dot-screen router NOW (force a full grid re-pack of every screen —
+// green tiled on the left, colour bands row-major on the right). The launchd loop already does
+// this every ~30s; the Arrange button makes it instant after Steve manually drags windows around.
+// ONE pass only: a single router pass reads every window's current position and moves ALL the
+// misplaced ones to their slots in one osascript batch, so one click snaps everything back. (An
+// earlier 3-pass "converge" loop cost ~74s on a loaded box AND could never settle, because dot
+// colours change live between passes — it was chasing a moving target. The 30s loop covers drift.)
+async function arrange() {
+  const { stdout, err } = await run('bash', [ROUTER], 45000);
+  const m = /routed (\d+)/.exec(stdout || '');
+  return { ok: !err, moved: m ? parseInt(m[1], 10) : 0, err: err ? String(err).slice(0, 120) : null };
+}
+
 function send(res, code, body, type = 'application/json') {
   res.writeHead(code, { 'Content-Type': type, 'Cache-Control': 'no-store' });
   res.end(typeof body === 'string' ? body : JSON.stringify(body));
@@ -124,6 +138,9 @@ const server = http.createServer(async (req, res) => {
     const url = new URL(req.url, 'http://x');
     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/arrange' && req.method === 'POST') {
+      return send(res, 200, await arrange());
+    }
     if (url.pathname === '/api/reveal' && req.method === 'POST') {
       let raw = '';
       req.on('data', c => (raw += c));

← 0d25ad6 dotbar: drop bare-Tab global resize hotkeys; keep drag grip  ·  back to Desktop Dotbar  ·  auto-data-snapshot: 2026-09-16T10:40:23 (1 data files) — ele 158144e →