copyhub-cli 1.0.9 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/ui/main.mjs +173 -20
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "copyhub-cli",
3
- "version": "1.0.9",
3
+ "version": "1.1.0",
4
4
  "description": "CopyHub — clipboard, local history, Google Sheets sync (OAuth). Windows, macOS, Linux.",
5
5
  "type": "module",
6
6
  "bin": {
package/ui/main.mjs CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  Menu,
12
12
  nativeImage,
13
13
  systemPreferences,
14
+ powerMonitor,
14
15
  } from 'electron';
15
16
  import { loadCopyhubEnv } from '../src/load-env.js';
16
17
  import { readRecentHistorySync } from '../src/storage.js';
@@ -132,6 +133,28 @@ function applyAlwaysOnTopStack(w) {
132
133
  }
133
134
  }
134
135
 
136
+ /** Bring app + overlay forward (macOS often needs app focus for always-on-top popups after idle). */
137
+ function bringOverlayToFront(w) {
138
+ if (!w || w.isDestroyed()) return;
139
+ applyAlwaysOnTopStack(w);
140
+ if (process.platform === 'darwin') {
141
+ try {
142
+ app.focus({ steal: true });
143
+ } catch {
144
+ try {
145
+ app.focus();
146
+ } catch {
147
+ /* ignore */
148
+ }
149
+ }
150
+ }
151
+ try {
152
+ w.focus();
153
+ } catch {
154
+ /* ignore */
155
+ }
156
+ }
157
+
135
158
  function createWindow() {
136
159
  win = new BrowserWindow({
137
160
  width: OVERLAY_WIDTH,
@@ -154,6 +177,19 @@ function createWindow() {
154
177
 
155
178
  win.loadFile(path.join(__dirname, 'renderer', 'index.html'));
156
179
 
180
+ win.webContents.on('render-process-gone', (_event, details) => {
181
+ console.warn('[CopyHub overlay] Renderer process ended:', details.reason);
182
+ if (!win || win.isDestroyed()) return;
183
+ try {
184
+ win.webContents.reload();
185
+ } catch (e) {
186
+ console.warn(
187
+ '[CopyHub overlay] Reload after renderer exit failed:',
188
+ /** @type {Error} */ (e).message,
189
+ );
190
+ }
191
+ });
192
+
157
193
  win.on('show', () => {
158
194
  applyAlwaysOnTopStack(win);
159
195
  });
@@ -185,8 +221,7 @@ function createWindow() {
185
221
  }
186
222
  placeWindowAtCursor(win);
187
223
  win.show();
188
- applyAlwaysOnTopStack(win);
189
- win.focus();
224
+ bringOverlayToFront(win);
190
225
  win.webContents.send('overlay:open');
191
226
  setTimeout(() => applyAlwaysOnTopStack(win), 120);
192
227
  armBlurHideEnable(win);
@@ -218,18 +253,50 @@ function placeWindowAtCursor(w) {
218
253
  }
219
254
 
220
255
  function toggleOverlay() {
221
- if (!win) return;
222
- if (win.isVisible()) {
223
- win.hide();
224
- } else {
256
+ try {
257
+ if (!win || win.isDestroyed()) {
258
+ blurHideEnabled = false;
259
+ createWindow();
260
+ return;
261
+ }
262
+ if (win.isVisible()) {
263
+ win.hide();
264
+ return;
265
+ }
225
266
  blurHideEnabled = false;
226
267
  placeWindowAtCursor(win);
227
268
  win.show();
228
- applyAlwaysOnTopStack(win);
229
- win.focus();
230
- win.webContents.send('overlay:open');
269
+ bringOverlayToFront(win);
270
+ const wc = win.webContents;
271
+ if (!wc.isDestroyed()) {
272
+ wc.send('overlay:open');
273
+ }
231
274
  setTimeout(() => applyAlwaysOnTopStack(win), 120);
232
275
  armBlurHideEnable(win);
276
+ } catch (e) {
277
+ const msg = /** @type {Error} */ (e).message || String(e);
278
+ console.warn('[CopyHub overlay] toggle failed:', msg);
279
+ try {
280
+ if (win && !win.isDestroyed()) {
281
+ const wc = win.webContents;
282
+ if (!wc.isDestroyed()) {
283
+ wc.reload();
284
+ return;
285
+ }
286
+ }
287
+ } catch {
288
+ /* recreate below */
289
+ }
290
+ try {
291
+ if (win && !win.isDestroyed()) {
292
+ win.destroy();
293
+ }
294
+ } catch {
295
+ /* ignore */
296
+ }
297
+ win = null;
298
+ blurHideEnabled = false;
299
+ createWindow();
233
300
  }
234
301
  }
235
302
 
@@ -289,6 +356,70 @@ function registerHotkeys() {
289
356
  return { accelerator: '', usedFallback: false };
290
357
  }
291
358
 
359
+ /**
360
+ * macOS often drops Electron globalShortcut listeners (sleep/wake or while running); re-register.
361
+ * @param {{ silentSuccess?: boolean }} [opts] — omit success log for periodic refresh noise
362
+ */
363
+ function reregisterOverlayHotkeys(opts = {}) {
364
+ const silentSuccess = Boolean(opts.silentSuccess);
365
+ const prev = overlayHotkeyMeta.accelerator;
366
+ if (prev) {
367
+ try {
368
+ globalShortcut.unregister(prev);
369
+ } catch {
370
+ /* ignore */
371
+ }
372
+ }
373
+ const { accelerator, usedFallback } = registerHotkeys();
374
+ overlayHotkeyMeta = {
375
+ accelerator,
376
+ usedFallback,
377
+ requestedRaw:
378
+ process.env.COPYHUB_OVERLAY_ACCELERATOR?.trim() ||
379
+ loadOverlayAcceleratorFromConfigSync() ||
380
+ '',
381
+ };
382
+ if (accelerator) {
383
+ if (!silentSuccess) {
384
+ console.log('[CopyHub overlay] Shortcut active again:', accelerator);
385
+ }
386
+ } else {
387
+ console.warn(
388
+ '[CopyHub overlay] Shortcut re-registration failed — open from menu bar or restart CopyHub.',
389
+ );
390
+ }
391
+ refreshTrayContextMenu();
392
+ }
393
+
394
+ /** Detect shortcut unregistered while process still runs (common on macOS without sleep). */
395
+ function startGlobalShortcutHealthMonitor() {
396
+ const intervalMs = process.platform === 'darwin' ? 45_000 : 120_000;
397
+ setInterval(() => {
398
+ const acc = overlayHotkeyMeta.accelerator;
399
+ if (!acc || !gotLock) return;
400
+ try {
401
+ if (!globalShortcut.isRegistered(acc)) {
402
+ console.warn('[CopyHub overlay] Global shortcut registration lost — repairing.');
403
+ reregisterOverlayHotkeys({ silentSuccess: false });
404
+ }
405
+ } catch (e) {
406
+ console.warn(
407
+ '[CopyHub overlay] Shortcut health check failed:',
408
+ /** @type {Error} */ (e).message,
409
+ );
410
+ }
411
+ }, intervalMs);
412
+ }
413
+
414
+ /** Proactive refresh: Electron/macOS can leave shortcuts broken while isRegistered stays true. */
415
+ function startDarwinShortcutKeepalive() {
416
+ if (process.platform !== 'darwin') return;
417
+ const periodMs = 8 * 60 * 1000;
418
+ setInterval(() => {
419
+ reregisterOverlayHotkeys({ silentSuccess: true });
420
+ }, periodMs);
421
+ }
422
+
292
423
  function mergeHistoryForOverlay(localItems, sheetItems, cap) {
293
424
  const seen = new Set();
294
425
  /** @type {typeof localItems} */
@@ -547,23 +678,37 @@ function registerIpc() {
547
678
  });
548
679
  }
549
680
 
681
+ function buildTrayMenuTemplate() {
682
+ const accLabel = overlayHotkeyMeta.accelerator
683
+ ? `Shortcut: ${overlayHotkeyMeta.accelerator}`
684
+ : 'Shortcut: (see terminal)';
685
+ return [
686
+ { label: accLabel, enabled: false },
687
+ { label: 'Open history (always on top)', click: () => toggleOverlay() },
688
+ { type: 'separator' },
689
+ { label: 'Quit', click: () => app.quit() },
690
+ ];
691
+ }
692
+
693
+ function refreshTrayContextMenu() {
694
+ if (!tray) return;
695
+ try {
696
+ tray.setContextMenu(Menu.buildFromTemplate(buildTrayMenuTemplate()));
697
+ } catch (e) {
698
+ console.warn(
699
+ '[CopyHub overlay] Tray menu refresh failed:',
700
+ /** @type {Error} */ (e).message,
701
+ );
702
+ }
703
+ }
704
+
550
705
  function registerTray() {
551
706
  const icon = nativeImage.createFromDataURL(
552
707
  'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMUlEQVQ4T2NkYGAQYcAP3uCTZhn1IGOMJoAmBGOSMDEwMmABWDWHJjBCSpBKGBSDjBAAAeoRBIEs/x0AAAAASUVORK5CYII=',
553
708
  );
554
709
  tray = new Tray(icon);
555
710
  tray.setToolTip('CopyHub overlay');
556
- const accLabel = overlayHotkeyMeta.accelerator
557
- ? `Shortcut: ${overlayHotkeyMeta.accelerator}`
558
- : 'Shortcut: (see terminal)';
559
- tray.setContextMenu(
560
- Menu.buildFromTemplate([
561
- { label: accLabel, enabled: false },
562
- { label: 'Open history (always on top)', click: () => toggleOverlay() },
563
- { type: 'separator' },
564
- { label: 'Quit', click: () => app.quit() },
565
- ]),
566
- );
711
+ tray.setContextMenu(Menu.buildFromTemplate(buildTrayMenuTemplate()));
567
712
  tray.on('click', () => toggleOverlay());
568
713
  }
569
714
 
@@ -617,6 +762,14 @@ if (gotLock) {
617
762
  } catch (e) {
618
763
  console.warn('Could not create system tray icon:', /** @type {Error} */ (e).message);
619
764
  }
765
+
766
+ /** Delay slightly so macOS finishes restoring input / accessibility after wake. */
767
+ powerMonitor.on('resume', () => {
768
+ setTimeout(() => reregisterOverlayHotkeys({ silentSuccess: false }), 400);
769
+ });
770
+
771
+ startGlobalShortcutHealthMonitor();
772
+ startDarwinShortcutKeepalive();
620
773
  });
621
774
 
622
775
  app.on('will-quit', () => {