← 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
M .gitignoreM electron-main.jsM preload.jsM public/index.html
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 →