electron-menubar 11.1.1 โ†’ 11.1.3

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.
package/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  <h1 align="center">โž– electron-menubar</h1>
2
2
 
3
3
  <p align="center">
4
+ <a href="https://electron-menubar.gitify.io"><img src="https://img.shields.io/badge/docs-mintlify-18E299?logo=mintlify&amp;logoColor=black" alt="Documentation" /></a>
4
5
  <a href="https://github.com/gitify-app/electron-menubar/actions"><img src="https://img.shields.io/github/actions/workflow/status/gitify-app/electron-menubar/test.yml?logo=github&amp;label=ci" alt="CI Workflow" /></a>
5
6
  <a href="https://github.com/gitify-app/electron-menubar/actions"><img src="https://img.shields.io/github/actions/workflow/status/gitify-app/electron-menubar/release.yml?logo=github&amp;label=release" alt="Release Workflow" /></a>
6
7
  <a href="https://sonarcloud.io/summary/new_code?id=gitify-app_electron-menubar"><img src="https://img.shields.io/sonar/quality_gate/gitify-app_electron-menubar?server=https%3A%2F%2Fsonarcloud.io&amp;logo=sonarqubecloud" alt="Quality Gate Status" /></a>
@@ -14,11 +15,11 @@
14
15
  </p>
15
16
 
16
17
  <h5 align="center"><i>formerly known as menubar</i></h5>
17
- <h4 align="center">High level way to create menubar desktop applications with Electron.</h4>
18
+ <h4 align="center">A high-level way to build menubar apps with Electron.</h4>
18
19
 
19
20
  <br />
20
21
 
21
- ## Features
22
+ ## ๐Ÿ“ฆ Features
22
23
 
23
24
  - โšก๏ธ Quick start for creating menubar applications using Electron.
24
25
  - ๐Ÿš€ Zero runtime dependencies.
@@ -31,7 +32,7 @@
31
32
  > [!NOTE]
32
33
  > On native Wayland, the desktop compositor controls window placement, so the popover cannot be anchored to the tray icon and is positioned by the desktop (usually centered). This is a Wayland limitation, not something the library can override. Tray-relative positioning works under X11/XWayland. See [window positioning on Linux][platforms] for the detail and a workaround.
33
34
 
34
- ## Why electron-menubar?
35
+ ## ๐Ÿ’ก Why electron-menubar?
35
36
 
36
37
  `electron-menubar` builds on [max-mapper/menubar][github-upstream-repo], with fixes and controls developed for [Gitify](https://github.com/gitify-app/gitify). The original provides the tray-app foundation. This fork brings more of the platform-specific behavior into the library so applications have less to implement themselves.
37
38
 
@@ -46,7 +47,7 @@
46
47
  | Packaging | Zero runtime dependencies, including a built-in positioner. Ships CommonJS and ESM entry points with TypeScript declarations. |
47
48
  | Verification | Unit tests, native Electron end-to-end tests on macOS, Windows, and Linux, and visual checks that the tray icon and window render. The [platform report][platforms] includes screenshots and positioning limitations. |
48
49
 
49
- ### Popup behavior without platform checks
50
+ ### ๐ŸชŸ Popup behavior without platform checks
50
51
 
51
52
  Menubar manages popup stacking and focus on every platform. A plain `menubar()` dismisses on click-away and opens above the Windows hidden-icons panel. To keep it open, change one option:
52
53
 
@@ -59,20 +60,20 @@ mb.setOption('hideOnBlur', true); // Resume click-away dismissal.
59
60
 
60
61
  Opening DevTools temporarily keeps the popup open. Closing DevTools restores the latest `hideOnBlur` preference. Options set before window creation also apply when the window is recreated.
61
62
 
62
- #### Migrating from 10.x
63
+ #### ๐Ÿšš Migrating from 10.x
63
64
 
64
65
  These defaults apply starting with 11.0.0. Previously, menubar inferred dismissal from `window.isAlwaysOnTop()`. Use `hideOnBlur: false` for persistent popups instead of `browserWindow.alwaysOnTop: true` or runtime `window.setAlwaysOnTop(true)` calls. `browserWindow.alwaysOnTop` now controls stacking only; it does not disable dismissal. Setting `hideOnBlur: false` also keeps the popup on top on macOS and Linux.
65
66
 
66
67
  Remove Windows `pop-up-menu` workarounds and DevTools focus handlers from apps. Menubar owns stacking during window creation, showing, preference changes, and DevTools transitions. Apps can still customize DevTools dimensions and layout.
67
68
  See the [changelog](CHANGELOG.md) for released changes and the [API documentation](#api-documentation) for all options.
68
69
 
69
- ## Installation
70
+ ## ๐Ÿš€ Installation
70
71
 
71
72
  ```bash
72
73
  pnpm add electron-menubar
73
74
  ```
74
75
 
75
- ## Usage
76
+ ## ๐Ÿ”ง Usage
76
77
 
77
78
  Starting with your own new project, run these commands:
78
79
 
@@ -101,17 +102,17 @@ Then use `electron` to run the app:
101
102
  $ electron myApp.js
102
103
  ```
103
104
 
104
- ## Examples
105
+ ## ๐Ÿ“š Examples
105
106
 
106
107
  See [`examples`][examples] folder for a selection of working examples.
107
108
 
108
- ## API Documentation
109
+ ## ๐Ÿ“– API Documentation
109
110
 
110
- ### `Menubar` Class
111
+ ### ๐Ÿงฑ `Menubar` Class
111
112
 
112
113
  The return value of `menubar()` is a `Menubar` class instance, which exposes the following properties and methods.
113
114
 
114
- #### Properties
115
+ #### ๐Ÿงพ Properties
115
116
 
116
117
  | Property | Description |
117
118
  | ------------ | ----------------------------------------------------------------------------- |
@@ -120,7 +121,7 @@ The return value of `menubar()` is a `Menubar` class instance, which exposes the
120
121
  | `tray` | The [Electron Tray][electron-docs-tray] instance. |
121
122
  | `positioner` | The `Positioner` instance used to compute the window's on-screen coordinates. |
122
123
 
123
- #### Methods
124
+ #### ๐Ÿงฐ Methods
124
125
 
125
126
  | Method | Description |
126
127
  | -------------------------------- | ------------------------------------------------------------------------------------------------------------ |
@@ -136,7 +137,7 @@ The return value of `menubar()` is a `Menubar` class instance, which exposes the
136
137
  | `destroy()` | Tear down the menubar instance. |
137
138
  | `isDestroyed()` | Whether the menubar is currently destroyed. |
138
139
 
139
- ### `menubar()` Options
140
+ ### ๐ŸŽ›๏ธ `menubar()` Options
140
141
 
141
142
  You can pass an optional options object into the `menubar({ ... })` function:
142
143
 
@@ -162,7 +163,7 @@ You can pass an optional options object into the `menubar({ ... })` function:
162
163
  | `ignoreDoubleClickEvents` | `true` (macOS only) | Calls `tray.setIgnoreDoubleClickEvents(true)` so an accidental double-click doesn't race the close-on-blur handler and flicker the tray icon. Pass `false` to opt out. No-op on Linux/Windows. |
163
164
  | `globalShortcut` | | An [Accelerator][electron-docs-accelerator] string registered as a global keyboard shortcut that toggles the menubar window. Unregistered automatically on `destroy()`. Use `mb.setGlobalShortcut(accelerator)` to change or clear it at runtime. |
164
165
 
165
- ### Events
166
+ ### ๐Ÿ“ก Events
166
167
 
167
168
  The `Menubar` class is an event emitter:
168
169
 
@@ -179,7 +180,7 @@ The `Menubar` class is an event emitter:
179
180
  | `after-close` | After the `.window` (BrowserWindow) property has been deleted. |
180
181
  | `focus-lost` | Emitted on click-away when `hideOnBlur` is `false` or DevTools is open. |
181
182
 
182
- ## Tips
183
+ ## ๐Ÿง  Tips
183
184
 
184
185
  - Use `mb.on('after-create-window', callback)` to run things after your app has loaded. For example you could run `mb.window.openDevTools()` to open the developer tools for debugging, or load a different URL with `mb.window.loadURL()`
185
186
  - Use `mb.on('focus-lost')` if you would like to perform some operation when using the option `hideOnBlur: false`
@@ -187,7 +188,7 @@ The `Menubar` class is an event emitter:
187
188
  - To attach a native context menu, pass it as `contextMenu`: `menubar({ contextMenu })`. The library wires it via `setContextMenu` on Linux and `popUpContextMenu` on right-click on macOS/Windows so left-click still toggles the window. See [this example][examples-native] for more information.
188
189
  - To avoid a flash when opening your menubar app, you can disable backgrounding the app using the following: `mb.app.commandLine.appendSwitch('disable-backgrounding-occluded-windows', 'true');`
189
190
 
190
- ## Acknowledgements
191
+ ## ๐Ÿค Acknowledgements
191
192
 
192
193
  Originally created by [Max][github-upstream-creator] โ€” hard-forked from [max-mapper/menubar][github-upstream-repo].
193
194
 
package/lib/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- //#region \0rolldown/runtime.js
2
+ // \0rolldown/runtime.js
3
3
  var __create = Object.create;
4
4
  var __defProp = Object.defineProperty;
5
5
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -20,7 +20,6 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
20
20
  value: mod,
21
21
  enumerable: true
22
22
  }) : target, mod));
23
- //#endregion
24
23
  let electron = require("electron");
25
24
  let node_events = require("node:events");
26
25
  let node_fs = require("node:fs");
@@ -29,7 +28,7 @@ let node_path = require("node:path");
29
28
  node_path = __toESM(node_path);
30
29
  let node_url = require("node:url");
31
30
  node_url = __toESM(node_url);
32
- //#region src/Positioner.ts
31
+ // src/Positioner.ts
33
32
  /**
34
33
  * Computes `{x, y}` coordinates for placing a {@link BrowserWindow} at a named
35
34
  * position, optionally relative to a tray icon's bounds. Ported from
@@ -40,10 +39,14 @@ var Positioner = class {
40
39
  constructor(browserWindow) {
41
40
  this.browserWindow = browserWindow;
42
41
  }
43
- calculate(position, trayBounds) {
42
+ /**
43
+ * @param windowSize - Size to place, when it differs from the window's
44
+ * current size (e.g. the size about to be applied with `setBounds`).
45
+ */
46
+ calculate(position, trayBounds, windowSize) {
44
47
  if (!position) throw new TypeError("Positioner.calculate: a `position` argument is required.");
45
48
  const screenSize = trayBounds ? electron.screen.getDisplayMatching(trayBounds).workArea : electron.screen.getDisplayNearestPoint(electron.screen.getCursorScreenPoint()).workArea;
46
- const [windowWidth, windowHeight] = this.browserWindow.getSize();
49
+ const [windowWidth, windowHeight] = windowSize ? [windowSize.width, windowSize.height] : this.browserWindow.getSize();
47
50
  const trayX = trayBounds?.x ?? NaN;
48
51
  const trayWidth = trayBounds?.width ?? NaN;
49
52
  const positions = {
@@ -118,8 +121,7 @@ var Positioner = class {
118
121
  return coords;
119
122
  }
120
123
  };
121
- //#endregion
122
- //#region src/util/cleanOptions.ts
124
+ // src/util/cleanOptions.ts
123
125
  /**
124
126
  * @ignore
125
127
  */
@@ -150,8 +152,7 @@ function cleanOptions(opts) {
150
152
  options.browserWindow.height = options.browserWindow.height !== void 0 ? options.browserWindow.height : DEFAULT_WINDOW_HEIGHT;
151
153
  return options;
152
154
  }
153
- //#endregion
154
- //#region src/util/getWindowPosition.ts
155
+ // src/util/getWindowPosition.ts
155
156
  /**
156
157
  * Utilities to get taskbar position and consequently menubar's position
157
158
  */
@@ -221,8 +222,7 @@ function getWindowPosition(tray) {
221
222
  }
222
223
  return "topRight";
223
224
  }
224
- //#endregion
225
- //#region src/Menubar.ts
225
+ // src/Menubar.ts
226
226
  /**
227
227
  * Grace period after `showWindow()` during which a `blur` on the popup is
228
228
  * treated as the spurious Windows post-show blur and ignored instead of
@@ -230,6 +230,12 @@ function getWindowPosition(tray) {
230
230
  */
231
231
  const BLUR_HIDE_GRACE_MS = 400;
232
232
  /**
233
+ * Window after a blur-triggered hide during which a tray click on Linux is
234
+ * taken as the click that caused the blur, so it leaves the popup closed. See
235
+ * {@link Menubar.clicked}.
236
+ */
237
+ const TRAY_CLICK_AFTER_BLUR_HIDE_MS = 500;
238
+ /**
233
239
  * Delay before re-asserting the hidden dock after startup. macOS can silently
234
240
  * drop an `app.dock.hide()` that races the app's launch activation
235
241
  * transition, leaving the dock icon stuck for the whole session. See the
@@ -237,6 +243,12 @@ const BLUR_HIDE_GRACE_MS = 400;
237
243
  */
238
244
  const DOCK_REHIDE_DELAY_MS = 2e3;
239
245
  /**
246
+ * Largest size change per dimension, in DIPs, taken as rounding drift from
247
+ * moving the window on a display with a fractional scale factor. A move there
248
+ * changes each dimension by up to 2 DIPs. See {@link Menubar.holdSize}.
249
+ */
250
+ const FRACTIONAL_SCALE_DRIFT = 4;
251
+ /**
240
252
  * The main Menubar class.
241
253
  */
242
254
  var Menubar = class extends node_events.EventEmitter {
@@ -254,8 +266,10 @@ var Menubar = class extends node_events.EventEmitter {
254
266
  _rightClickContextMenuBound = false;
255
267
  _warnedNoPositioning = false;
256
268
  _lastShowTime = 0;
269
+ _lastBlurHideTime = 0;
257
270
  _dockRehideTimeout;
258
271
  _repositioning = false;
272
+ _heldSize;
259
273
  _showOnAllWorkspaces = false;
260
274
  _tray;
261
275
  constructor(app, options) {
@@ -404,8 +418,13 @@ var Menubar = class extends node_events.EventEmitter {
404
418
  recenterOnTray() {
405
419
  if (!this._browserWindow || !this._tray) return;
406
420
  const bounds = this._tray.getBounds();
407
- const { x, y } = this.positioner.calculate("trayCenter", bounds);
408
- this._browserWindow.setPosition(Math.round(x), Math.round(y));
421
+ const size = this.holdSize(this._browserWindow);
422
+ const { x, y } = this.positioner.calculate("trayCenter", bounds, size);
423
+ this._browserWindow.setBounds({
424
+ x: Math.round(x),
425
+ y: Math.round(y),
426
+ ...size
427
+ });
409
428
  }
410
429
  /**
411
430
  * Replace the tray context menu after construction. On Linux this also
@@ -480,6 +499,7 @@ var Menubar = class extends node_events.EventEmitter {
480
499
  this.positionWindow();
481
500
  this.applyWindowFocusBehavior();
482
501
  this._lastShowTime = Date.now();
502
+ this._lastBlurHideTime = 0;
483
503
  this._browserWindow.show();
484
504
  this._isVisible = true;
485
505
  this.emit("after-show");
@@ -502,6 +522,52 @@ var Menubar = class extends node_events.EventEmitter {
502
522
  }
503
523
  };
504
524
  /**
525
+ * Re-anchor the window after the app resizes it (e.g. via `setSize`).
526
+ * Ignores the resize that moving the window itself causes; see
527
+ * {@link holdSize}.
528
+ */
529
+ onWindowResize = () => {
530
+ if (!this._browserWindow) return;
531
+ const [width, height] = this._browserWindow.getSize();
532
+ const held = this._heldSize;
533
+ const drift = this.sizeDrift(this._browserWindow);
534
+ if (held && Math.abs(width - held.width) <= drift && Math.abs(height - held.height) <= drift) return;
535
+ this.positionWindow();
536
+ };
537
+ /**
538
+ * Pick the size to apply with the next move and remember it.
539
+ *
540
+ * On X11 with fractional display scaling, `setPosition` grows the window by
541
+ * a pixel or two per call, and so does `setBounds` with the size `getSize`
542
+ * reports, because each conversion between DIPs and device pixels rounds
543
+ * up. That drift often lands without a `resize` event, so only the size can
544
+ * tell it apart: a dimension within {@link sizeDrift} of the held size keeps
545
+ * the held value, and any other value came from the app and becomes the new
546
+ * held one.
547
+ */
548
+ holdSize(win) {
549
+ const [width, height] = win.getSize();
550
+ const held = this._heldSize;
551
+ const drift = this.sizeDrift(win);
552
+ const size = {
553
+ width: held && Math.abs(width - held.width) <= drift ? held.width : width,
554
+ height: held && Math.abs(height - held.height) <= drift ? held.height : height
555
+ };
556
+ this._heldSize = size;
557
+ return size;
558
+ }
559
+ /**
560
+ * Size change per dimension, in DIPs, that moving the window can cause.
561
+ * Rounding drift only happens at a fractional scale factor, so on any other
562
+ * display every size change counts as the app's. At a fractional scale, an
563
+ * app resize of up to this many DIPs is indistinguishable from drift and is
564
+ * not kept.
565
+ */
566
+ sizeDrift(win) {
567
+ const { scaleFactor } = electron.screen.getDisplayMatching(win.getBounds());
568
+ return Number.isInteger(scaleFactor) ? 0 : FRACTIONAL_SCALE_DRIFT;
569
+ }
570
+ /**
505
571
  * Compute and apply the tray-anchored position. Always call through
506
572
  * {@link positionWindow}, which guards against re-entrancy.
507
573
  */
@@ -511,12 +577,17 @@ var Menubar = class extends node_events.EventEmitter {
511
577
  const trayPos = this._cachedBounds ?? this._tray.getBounds?.();
512
578
  let noBoundsPosition;
513
579
  if ((trayPos === void 0 || trayPos.x === 0) && this._options.windowPosition?.startsWith("tray")) noBoundsPosition = process.platform === "win32" ? "bottomRight" : "topRight";
514
- const position = this.positioner.calculate(this._options.windowPosition || noBoundsPosition, trayPos);
580
+ const size = this.holdSize(this._browserWindow);
581
+ const position = this.positioner.calculate(this._options.windowPosition || noBoundsPosition, trayPos, size);
515
582
  const x = this._options.browserWindow.x !== void 0 ? this._options.browserWindow.x : position.x;
516
583
  const y = this._options.browserWindow.y !== void 0 ? this._options.browserWindow.y : position.y;
517
584
  const targetX = Math.round(x);
518
585
  const targetY = Math.round(y);
519
- this._browserWindow.setPosition(targetX, targetY);
586
+ this._browserWindow.setBounds({
587
+ x: targetX,
588
+ y: targetY,
589
+ ...size
590
+ });
520
591
  if (process.platform === "linux" && !!process.env.WAYLAND_DISPLAY && !this._warnedNoPositioning) {
521
592
  const [actualX, actualY] = this._browserWindow.getPosition();
522
593
  if (Math.abs(actualX - targetX) > 24 || Math.abs(actualY - targetY) > 24) {
@@ -560,6 +631,10 @@ var Menubar = class extends node_events.EventEmitter {
560
631
  if (event && (event.shiftKey || event.ctrlKey || event.metaKey)) return this.hideWindow();
561
632
  if (this._blurTimeout) clearInterval(this._blurTimeout);
562
633
  if (this._browserWindow && this._isVisible) return this.hideWindow();
634
+ if (process.platform === "linux" && Date.now() - this._lastBlurHideTime < TRAY_CLICK_AFTER_BLUR_HIDE_MS) {
635
+ this._lastBlurHideTime = 0;
636
+ return;
637
+ }
563
638
  this._cachedBounds = bounds || this._cachedBounds;
564
639
  await this.showWindow(this._cachedBounds);
565
640
  };
@@ -629,7 +704,10 @@ var Menubar = class extends node_events.EventEmitter {
629
704
  if (this._blurTimeout) clearTimeout(this._blurTimeout);
630
705
  this._blurTimeout = setTimeout(() => {
631
706
  this._blurTimeout = null;
632
- if (this.shouldHideOnBlur()) this.hideWindow();
707
+ if (this.shouldHideOnBlur()) {
708
+ this.hideWindow();
709
+ this._lastBlurHideTime = Date.now();
710
+ }
633
711
  }, 100);
634
712
  });
635
713
  this._showOnAllWorkspaces = this._options.showOnAllWorkspaces !== false;
@@ -643,7 +721,7 @@ var Menubar = class extends node_events.EventEmitter {
643
721
  if (input.type === "keyDown" && input.key === "Escape") this.hideWindow();
644
722
  });
645
723
  this._browserWindow.on("closed", this.windowClear.bind(this));
646
- this._browserWindow.on("resize", this.positionWindow);
724
+ this._browserWindow.on("resize", this.onWindowResize);
647
725
  this.emit("before-load");
648
726
  if (this._options.index !== false) await this._browserWindow.loadURL(this._options.index, this._options.loadUrlOptions);
649
727
  this.emit("after-create-window");
@@ -669,11 +747,11 @@ var Menubar = class extends node_events.EventEmitter {
669
747
  }
670
748
  windowClear() {
671
749
  this._browserWindow = void 0;
750
+ this._heldSize = void 0;
672
751
  this.emit("after-close");
673
752
  }
674
753
  };
675
- //#endregion
676
- //#region src/index.ts
754
+ // src/index.ts
677
755
  /**
678
756
  * Entry point of menubar
679
757
  * @example
@@ -691,7 +769,6 @@ var Menubar = class extends node_events.EventEmitter {
691
769
  function menubar(options) {
692
770
  return new Menubar(electron.app, options);
693
771
  }
694
- //#endregion
695
772
  exports.Menubar = Menubar;
696
773
  exports.getWindowPosition = getWindowPosition;
697
774
  exports.menubar = menubar;