[object Object]

← back to Desktop Dotbar

dotbar: drop bare-Tab global resize hotkeys; keep drag grip (DTD-D 2026-09-16)

0d25ad66fcc2531de14bb9eb730f2978b5489039 · 2026-09-16 09:11:31 -0700 · Steve

A bare Tab/Shift+Tab registered via Electron globalShortcut is consumed OS-wide
while armed, so Tab silently died in the focused app whenever the mouse hovered
the top-of-screen strip; mouseleave could also be missed, leaving Tab hijacked.
An informed DTD panel (Claude+Codex+Qwen unanimous) chose D: remove the keyboard
path entirely since the drag grip already covers resize, eliminating the whole
bug class (no global shortcut, no arm/disarm state).

Also:
- perf: setBarH no longer writes .barh to disk or re-asserts always-on-top on
  every pointermove during a grip drag; disk write is debounced to drag-settle.
- grip hit target: anchor the grip fully inside the closed-bar window so it isn't
  a ~4px sliver (it's the sole resize affordance now).
- gitignore the runtime .barh file (its sibling .port was already ignored).

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

Files touched

Diff

commit 0d25ad66fcc2531de14bb9eb730f2978b5489039
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Sep 16 09:11:31 2026 -0700

    dotbar: drop bare-Tab global resize hotkeys; keep drag grip (DTD-D 2026-09-16)
    
    A bare Tab/Shift+Tab registered via Electron globalShortcut is consumed OS-wide
    while armed, so Tab silently died in the focused app whenever the mouse hovered
    the top-of-screen strip; mouseleave could also be missed, leaving Tab hijacked.
    An informed DTD panel (Claude+Codex+Qwen unanimous) chose D: remove the keyboard
    path entirely since the drag grip already covers resize, eliminating the whole
    bug class (no global shortcut, no arm/disarm state).
    
    Also:
    - perf: setBarH no longer writes .barh to disk or re-asserts always-on-top on
      every pointermove during a grip drag; disk write is debounced to drag-settle.
    - grip hit target: anchor the grip fully inside the closed-bar window so it isn't
      a ~4px sliver (it's the sole resize affordance now).
    - gitignore the runtime .barh file (its sibling .port was already ignored).
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01FFLLQmm7wJFvyHQkBb5Y21
---
 .gitignore        |  1 +
 electron-main.js  | 37 ++++++++++++++-----------------------
 preload.js        |  3 +--
 public/index.html | 23 ++++++++++++-----------
 4 files changed, 28 insertions(+), 36 deletions(-)

diff --git a/.gitignore b/.gitignore
index b81d59e..3a43e88 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,4 @@ tmp/
 .port
 .chrome-profile/
 debug.log
+.barh
diff --git a/electron-main.js b/electron-main.js
index e3144e9..1febbbd 100644
--- a/electron-main.js
+++ b/electron-main.js
@@ -2,7 +2,7 @@
 // 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, globalShortcut } = require('electron');
+const { app, BrowserWindow, ipcMain, screen } = require('electron');
 const { spawn } = require('child_process');
 const http = require('http');
 const fs = require('fs');
@@ -10,7 +10,7 @@ const path = require('path');
 
 const DIR = __dirname;
 const PANEL_H = 360;
-const BAR_DEFAULT = 48, BAR_MIN = 22, BAR_MAX = 140, BAR_STEP = 8;
+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
@@ -54,24 +54,16 @@ function bounds(open) {
 // 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 {}
+// 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).
+let _persistTimer = null;
+function setBarH(h) {
+  barH = clampBar(h);
+  if (win) win.setBounds(bounds(isOpen));                 // live resize only; no per-frame moveTop
+  pushBarH();
+  clearTimeout(_persistTimer); _persistTimer = setTimeout(writeBarH, 300);
 }
 
 async function createWindow() {
@@ -96,13 +88,12 @@ async function createWindow() {
   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 (edge-drag)
-  ipcMain.on('dotbar:hover', (_e, on) => setHotkeys(!!on));     // enable Tab/Shift+Tab only over the bar
+  ipcMain.on('dotbar:setBarH', (_e, h) => setBarH(h));          // absolute height (grip edge-drag)
   ipcMain.on('dotbar:quit', () => app.quit());
 }
 
 app.on('window-all-closed', () => app.quit());
-app.on('before-quit', () => { try { globalShortcut.unregisterAll(); } catch {} if (serverProc) try { serverProc.kill(); } catch {} });
+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));
diff --git a/preload.js b/preload.js
index 2985497..1928aee 100644
--- a/preload.js
+++ b/preload.js
@@ -1,8 +1,7 @@
 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
+  setBarH: (h) => ipcRenderer.send('dotbar:setBarH', h),        // absolute strip height (grip edge-drag)
   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 e3efd9a..6d080e7 100644
--- a/public/index.html
+++ b/public/index.html
@@ -17,9 +17,12 @@
   /* 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;
+  /* Anchor the grip so its full 8px height sits INSIDE the closed-bar window (which is only
+     --bar-h tall with overflow:hidden); otherwise its lower half is clipped and the grab target
+     shrinks to ~4px. It's the sole resize affordance now, so keep the target grabbable. */
+  #grip { position:fixed; left:0; right:0; top: calc(var(--bar-h) - 8px); 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;
+  #grip::after { content:""; position:absolute; left:50%; bottom:2px; 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;
@@ -61,7 +64,7 @@
 </head>
 <body>
   <div id="bar"></div>
-  <div id="grip" title="drag to resize · Tab = expand · Shift+Tab = contract"></div>
+  <div id="grip" title="drag to resize the bar"></div>
   <div id="panel"></div>
 <script>
 const BAR_H = 48, PANEL_H = 360;
@@ -138,10 +141,12 @@ 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.
+// ---- Resize the bar (Steve 2026-09-16): drag the bottom grip ----
+// Main process owns the window height; here we (a) mirror it into --bar-h and (b) turn grip drags
+// into absolute setBarH calls (screen-Y based so it stays stable as the window resizes under the
+// cursor). Keyboard resize (bare Tab/Shift+Tab global shortcuts) was removed: DTD 2026-09-16 —
+// a bare-Tab global shortcut is consumed OS-wide and silently kills Tab in the focused app; the
+// grip already covers resize, so no global keyboard hotkey is registered.
 if (window.dotbar && window.dotbar.onBarH) {
   window.dotbar.onBarH(h => { curBarH = h; document.documentElement.style.setProperty('--bar-h', h + 'px'); });
 }
@@ -163,10 +168,6 @@ if (window.dotbar && window.dotbar.onBarH) {
   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');

← f72f77b dotbar: keep-last-good so a slow/empty scan never flaps the  ·  back to Desktop Dotbar  ·  dotbar: add top-strip Arrange button — one-click re-tile eve 71dbcd1 →