deskband11lib 0.0.0-stage → 1.5.1

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,3 +1,151 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # deskband11lib for Node.js and Electron
2
+
3
+ [![npm](https://img.shields.io/npm/v/deskband11lib.svg)](https://www.npmjs.com/package/deskband11lib)
4
+ [![Build and Publish Node Package](https://github.com/airtaxi/Deskband11Lib/actions/workflows/node-package.yml/badge.svg)](https://github.com/airtaxi/Deskband11Lib/actions/workflows/node-package.yml)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/airtaxi/Deskband11Lib/blob/master/LICENSE)
6
+
7
+ deskband11lib places an Electron window inside the Windows 11 taskbar. It finds the free space between the taskbar buttons and the notification area, moves the window there, and keeps it aligned when the taskbar layout changes, when the taskbar moves to another screen edge, and when other hosted windows share the same area.
8
+
9
+ The package calls the Deskband11Lib native library through koffi. It includes a controller for Electron and a lower-level host that works with any window handle.
10
+
11
+ For how the library measures the taskbar, shares space between several hosted windows, and handles each taskbar position, see the [Deskband11Lib README](https://github.com/airtaxi/Deskband11Lib/blob/master/README.md). Deskband11Lib is also available for WinUI 3 and WPF as NuGet packages, and for Python on PyPI.
12
+
13
+ ## Requirements
14
+
15
+ - Windows 11, x64 or ARM64
16
+ - Node.js 22 or later. In Electron, use the package from the main process.
17
+
18
+ ## Install
19
+
20
+ ```powershell
21
+ npm install deskband11lib
22
+ ```
23
+
24
+ ## Electron
25
+
26
+ `ElectronTaskbarWindowController` creates your window, attaches it to the taskbar, and creates it again whenever the taskbar requires it.
27
+
28
+ ```js
29
+ const { app, BrowserWindow } = require('electron');
30
+ const { ElectronTaskbarWindowController, createTaskbarWindowOptions } = require('deskband11lib/electron');
31
+
32
+ app.whenReady().then(() => {
33
+ const controller = new ElectronTaskbarWindowController(() => {
34
+ const window = new BrowserWindow(createTaskbarWindowOptions({ transparent: true, backgroundColor: '#00000000' }));
35
+ window.loadFile('widget.html');
36
+ return window;
37
+ }, { preferredWidth: 200 });
38
+
39
+ controller.start();
40
+ });
41
+ ```
42
+
43
+ - `createTaskbarWindowOptions()` adds what a taskbar window needs: hidden until attached, no frame, no taskbar button or Alt+Tab entry, and no resizing or rounded corners. Use `transparent: true` to let the taskbar material show through the page.
44
+ - The taskbar layout decides the size of the window, so let the page fill the window. Do not show, move, or resize the window yourself.
45
+ - The taskbar follows the Windows mode, which can differ from the app mode the page sees through `prefers-color-scheme`. `nativeTheme.shouldUseDarkColorsForSystemIntegratedUI` tells you which one the taskbar uses.
46
+ - The taskbar destroys hosted windows when Explorer restarts or when the monitor they sit on is disconnected, and hosted windows are detached when the system DPI changes. The controller handles all three cases by destroying the old window and calling your factory again, so keep the state you want to preserve outside the window.
47
+ - While the controller runs, it keeps Electron from quitting when the last window closes. If your app quits from its own `window-all-closed` listener, skip that while `controller.isRecreating` is true, and give users another way to exit, such as a context menu.
48
+
49
+ Listen to the controller to react to the taskbar:
50
+
51
+ ```js
52
+ const { isVerticalTaskbarEdge } = require('deskband11lib');
53
+
54
+ controller.on('taskbarEdgeChanged', event => {
55
+ controller.window.webContents.send('taskbar-orientation', isVerticalTaskbarEdge(event.taskbarEdge));
56
+ });
57
+ ```
58
+
59
+ [`samples/electron-clock`](https://github.com/airtaxi/Deskband11Lib/tree/master/bindings/node/samples/electron-clock) shows a complete widget that follows the taskbar theme and switches its layout for vertical and thin taskbars.
60
+
61
+ ### Packaging
62
+
63
+ Windows cannot load a native library from inside an asar archive. Unpack the native files of deskband11lib and koffi; deskband11lib loads its library from `app.asar.unpacked` automatically.
64
+
65
+ - electron-builder unpacks koffi by itself. Add `"asarUnpack": ["**/node_modules/deskband11lib/bin/**"]`.
66
+ - Electron Forge: set `packagerConfig.asar` to `{ unpack: "**/*.{node,dll}" }`.
67
+
68
+ ## Any window
69
+
70
+ `TaskbarContentHost` takes a native window handle (HWND), as a number, a bigint, or the buffer returned by `BrowserWindow.getNativeWindowHandle()`.
71
+
72
+ ```js
73
+ const { TaskbarContentHost } = require('deskband11lib');
74
+
75
+ const host = new TaskbarContentHost(windowHandle, { preferredWidth: 200 });
76
+ host.on('taskbarWindowRecreationRequired', onRecreationRequired);
77
+ await host.attachWhenLayoutReady();
78
+ ```
79
+
80
+ Follow these rules when you host a window yourself:
81
+
82
+ - Create the host on a thread that runs a Win32 message loop and call every method on that thread. The main process of Electron runs one; a plain Node.js process does not.
83
+ - The window must be per-monitor DPI aware. Electron windows are.
84
+ - Create the window hidden and without a frame. An Electron window must be shown once while it is still a top-level window, or it displays only its background color after it is attached; show it off-screen without activation, then attach it.
85
+ - When `taskbarWindowRecreationRequired` is emitted, close the host, create the window again, and create a new host for it.
86
+ - Call `close()` when you no longer need the host. It restores the original parent and style of the window.
87
+
88
+ Events are read from the native library every `eventPollInterval` milliseconds, 50 by default, and emitted on the same thread.
89
+
90
+ ## Events
91
+
92
+ `TaskbarContentHost` emits these events. Each listener receives a `HostEvent`.
93
+
94
+ | Event | Emitted when |
95
+ | --- | --- |
96
+ | `attached` | `attachWhenLayoutReady()` completed. |
97
+ | `attachFailed` | `attachWhenLayoutReady()` failed. `errorMessage` describes why. |
98
+ | `taskbarWindowRecreated` | Explorer restarted and created the taskbar again. |
99
+ | `taskbarWindowDisappeared` | The taskbar the window sat on disappeared, for example because its monitor was disconnected. |
100
+ | `taskbarDpiChanged` | The system DPI changed. The window has been hidden and detached. |
101
+ | `taskbarWindowRecreationRequired` | Emitted together with each of the three events above. One listener covers all of them. |
102
+ | `taskbarEdgeChanged` | The taskbar moved to another screen edge, including the first layout after attaching. |
103
+ | `contentBoundsChanged` | The window received a new size, in effective pixels. |
104
+ | `layoutError` | An unexpected error occurred while the layout was updated. |
105
+
106
+ ## Options
107
+
108
+ Options that are left out keep the defaults of the native library.
109
+
110
+ | Option | Default | Description |
111
+ | --- | --- | --- |
112
+ | `preferredWidth` | `360` | Length along the taskbar in effective pixels: the width on a bottom or top taskbar, the height on a left or right taskbar. `STRETCH` fills the space left by fixed-width hosts. |
113
+ | `preferredHeight` | `48` | Size across the taskbar, capped by its thickness. |
114
+ | `placement` | `Auto` | `Auto`, `LeftEdge`, `BeforeNotificationArea`, or `BeforeStartButton`. |
115
+ | `preferredMonitorIdentity` | `0` | `0` is the primary taskbar. `1` and above are secondary taskbars ordered from left to right. |
116
+ | `manualSlotPriority` | `65535` | Order among hosts that share an area. Lower values are placed first. `65535` orders by arrival. |
117
+ | `allowFixedSlotResize` | `true` | Lets fixed-width hosts shrink proportionally when space runs out. |
118
+ | `animateLayoutChanges` | `true` | Animates position and size changes. |
119
+ | `highRefreshRateMode` | `true` | Matches the animation frame interval to the monitor refresh rate. |
120
+ | `layoutAnimationDuration` | `500` | Animation duration in milliseconds. |
121
+ | `layoutAnimationEasing` | `CircleOut` | One of the `Easing` values. |
122
+ | `trackTaskbarButtons` | `true` | Measures the taskbar buttons through UI Automation so the window never covers them. |
123
+ | `trackNotificationArea` | `true` | Keeps the window away from the notification area. |
124
+ | `layoutRefreshInterval` | `500` | Milliseconds between layout refreshes. |
125
+ | `eventPollInterval` | `50` | Milliseconds between reads of host events. |
126
+
127
+ Invalid values throw `Deskband11LibError` with `result` set to `ResultCode.InvalidArgument` and a message that names the option.
128
+
129
+ ## Building from source
130
+
131
+ Clone the [repository](https://github.com/airtaxi/Deskband11Lib). Building the native library requires the .NET 10 SDK and the Visual Studio C++ build tools.
132
+
133
+ ```powershell
134
+ cd bindings/node
135
+ npm install
136
+
137
+ # Publish the native library for the running Node.js and stage it in the package
138
+ npm run stage-native
139
+
140
+ # Or stage several architectures
141
+ npm run stage-native -- x64 arm64
142
+
143
+ npm run build
144
+ npm test
145
+ ```
146
+
147
+ `npm run test:taskbar` attaches windows to the taskbar of the current session, and `npm run sample` runs the clock sample. Set `DESKBAND11LIB_LIBRARY_PATH` to load the native library from another location.
148
+
149
+ ## License
150
+
151
+ MIT
Binary file
@@ -0,0 +1,48 @@
1
+ export declare const ABI_VERSION = 1;
2
+ export declare const LIBRARY_PATH_ENVIRONMENT_VARIABLE = "DESKBAND11LIB_LIBRARY_PATH";
3
+ export type HostHandle = number | bigint;
4
+ export interface NativeOptions {
5
+ struct_size: number;
6
+ animate_layout_changes: number;
7
+ high_refresh_rate_mode: number;
8
+ layout_animation_easing: number;
9
+ track_taskbar_buttons: number;
10
+ track_notification_area: number;
11
+ preferred_monitor_identity: number;
12
+ allow_fixed_slot_resize: number;
13
+ placement: number;
14
+ manual_slot_priority: number;
15
+ layout_animation_duration: number;
16
+ preferred_width: number;
17
+ preferred_height: number;
18
+ layout_refresh_interval: number;
19
+ }
20
+ export interface NativeEvent {
21
+ kind: number;
22
+ result: number;
23
+ taskbar_edge: number;
24
+ reserved: number;
25
+ max_width: number;
26
+ width: number;
27
+ height: number;
28
+ }
29
+ export interface NativeLibrary {
30
+ readonly optionsSize: number;
31
+ getAbiVersion(): number;
32
+ getLastErrorMessage(buffer: Buffer | null, bufferSize: number): number;
33
+ initializeOptions(options: NativeOptions, optionsSize: number): number;
34
+ createHost(windowHandle: bigint, options: NativeOptions, callback: null, userData: null, hostHandle: [HostHandle]): number;
35
+ destroyHost(hostHandle: HostHandle): number;
36
+ attachHost(hostHandle: HostHandle): number;
37
+ attachHostWhenLayoutReady(hostHandle: HostHandle): number;
38
+ detachHost(hostHandle: HostHandle): number;
39
+ refreshHostLayout(hostHandle: HostHandle): number;
40
+ pollHostEvent(hostHandle: HostHandle, nativeEvent: NativeEvent): number;
41
+ isHostAttached(hostHandle: HostHandle, value: [number]): number;
42
+ getHostTaskbarEdge(hostHandle: HostHandle, value: [number]): number;
43
+ getHostTaskbarAlignment(hostHandle: HostHandle, value: [number]): number;
44
+ isWindowAlive(windowHandle: bigint): number;
45
+ }
46
+ export declare function resolveLibraryPath(): string;
47
+ export declare function loadLibrary(): NativeLibrary;
48
+ export declare function getLastErrorMessage(): string;
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.LIBRARY_PATH_ENVIRONMENT_VARIABLE = exports.ABI_VERSION = void 0;
37
+ exports.resolveLibraryPath = resolveLibraryPath;
38
+ exports.loadLibrary = loadLibrary;
39
+ exports.getLastErrorMessage = getLastErrorMessage;
40
+ // koffi declarations for deskband11lib.dll, mirroring include/deskband11lib.h.
41
+ const fs = __importStar(require("node:fs"));
42
+ const path = __importStar(require("node:path"));
43
+ const koffi = __importStar(require("koffi"));
44
+ exports.ABI_VERSION = 1;
45
+ exports.LIBRARY_PATH_ENVIRONMENT_VARIABLE = 'DESKBAND11LIB_LIBRARY_PATH';
46
+ // Anonymous structs keep koffi's global type registry free of names that a second copy of this package would collide with.
47
+ const nativeOptionsType = koffi.struct({
48
+ struct_size: 'uint32_t',
49
+ animate_layout_changes: 'int32_t',
50
+ high_refresh_rate_mode: 'int32_t',
51
+ layout_animation_easing: 'int32_t',
52
+ track_taskbar_buttons: 'int32_t',
53
+ track_notification_area: 'int32_t',
54
+ preferred_monitor_identity: 'int32_t',
55
+ allow_fixed_slot_resize: 'int32_t',
56
+ placement: 'int32_t',
57
+ manual_slot_priority: 'uint32_t',
58
+ layout_animation_duration: 'double',
59
+ preferred_width: 'double',
60
+ preferred_height: 'double',
61
+ layout_refresh_interval: 'double',
62
+ });
63
+ const nativeEventType = koffi.struct({
64
+ kind: 'int32_t',
65
+ result: 'int32_t',
66
+ taskbar_edge: 'int32_t',
67
+ reserved: 'int32_t',
68
+ max_width: 'double',
69
+ width: 'double',
70
+ height: 'double',
71
+ });
72
+ let cachedLibrary;
73
+ function resolveLibraryPath() {
74
+ const overridePath = process.env[exports.LIBRARY_PATH_ENVIRONMENT_VARIABLE];
75
+ if (overridePath)
76
+ return overridePath;
77
+ if (process.platform !== 'win32')
78
+ throw new Error(`deskband11lib supports Windows only, not ${process.platform}.`);
79
+ const libraryPath = path.join(__dirname, '..', 'bin', `win32-${process.arch}`, 'deskband11lib.dll');
80
+ // Windows cannot load a library from inside an asar archive, so packaged Electron apps load the unpacked copy.
81
+ return libraryPath.replace(/\.asar([\\/])/, '.asar.unpacked$1');
82
+ }
83
+ function loadLibrary() {
84
+ if (cachedLibrary)
85
+ return cachedLibrary;
86
+ const libraryPath = resolveLibraryPath();
87
+ if (!fs.existsSync(libraryPath))
88
+ throw new Error(`The deskband11lib native library was not found at ${libraryPath}.`);
89
+ const library = koffi.load(libraryPath);
90
+ const int32Output = koffi.out(koffi.pointer('int32_t'));
91
+ const loadedLibrary = {
92
+ optionsSize: koffi.sizeof(nativeOptionsType),
93
+ getAbiVersion: library.func('db11l_get_abi_version', 'uint32_t', []),
94
+ getLastErrorMessage: library.func('db11l_get_last_error_message', 'int32_t', ['uint8_t *', 'int32_t']),
95
+ initializeOptions: library.func('db11l_options_init', 'int32_t', [koffi.out(koffi.pointer(nativeOptionsType)), 'uint32_t']),
96
+ createHost: library.func('db11l_host_create', 'int32_t', ['intptr_t', koffi.pointer(nativeOptionsType), 'void *', 'void *', koffi.out(koffi.pointer('intptr_t'))]),
97
+ destroyHost: library.func('db11l_host_destroy', 'int32_t', ['intptr_t']),
98
+ attachHost: library.func('db11l_host_attach', 'int32_t', ['intptr_t']),
99
+ attachHostWhenLayoutReady: library.func('db11l_host_attach_when_layout_ready', 'int32_t', ['intptr_t']),
100
+ detachHost: library.func('db11l_host_detach', 'int32_t', ['intptr_t']),
101
+ refreshHostLayout: library.func('db11l_host_refresh_layout', 'int32_t', ['intptr_t']),
102
+ pollHostEvent: library.func('db11l_host_poll_event', 'int32_t', ['intptr_t', koffi.out(koffi.pointer(nativeEventType))]),
103
+ isHostAttached: library.func('db11l_host_is_attached', 'int32_t', ['intptr_t', int32Output]),
104
+ getHostTaskbarEdge: library.func('db11l_host_get_taskbar_edge', 'int32_t', ['intptr_t', int32Output]),
105
+ getHostTaskbarAlignment: library.func('db11l_host_get_taskbar_alignment', 'int32_t', ['intptr_t', int32Output]),
106
+ isWindowAlive: library.func('db11l_is_window_alive', 'int32_t', ['intptr_t']),
107
+ };
108
+ const abiVersion = loadedLibrary.getAbiVersion();
109
+ if (abiVersion !== exports.ABI_VERSION)
110
+ throw new Error(`${libraryPath} implements ABI version ${abiVersion}, but this package requires version ${exports.ABI_VERSION}.`);
111
+ cachedLibrary = loadedLibrary;
112
+ return loadedLibrary;
113
+ }
114
+ function getLastErrorMessage() {
115
+ const library = loadLibrary();
116
+ const size = library.getLastErrorMessage(null, 0);
117
+ const buffer = Buffer.alloc(size);
118
+ library.getLastErrorMessage(buffer, size);
119
+ const terminatorIndex = buffer.indexOf(0);
120
+ return buffer.toString('utf8', 0, terminatorIndex < 0 ? buffer.length : terminatorIndex);
121
+ }
@@ -0,0 +1,48 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { type BrowserWindow, type BrowserWindowConstructorOptions } from 'electron';
3
+ import { TaskbarContentHost, type HostEvent } from './host';
4
+ import type { TaskbarContentHostOptions } from './options';
5
+ /** Returns the native window handle (HWND) of a BrowserWindow. */
6
+ export declare function getWindowHandle(window: BrowserWindow): bigint;
7
+ /**
8
+ * Adds the BrowserWindow options a taskbar window needs: hidden until attached, no frame, no taskbar button
9
+ * or Alt+Tab entry, and no resizing, snapping or rounded corners. Pass the result to new BrowserWindow().
10
+ */
11
+ export declare function createTaskbarWindowOptions(options?: BrowserWindowConstructorOptions): BrowserWindowConstructorOptions;
12
+ export interface ElectronTaskbarWindowControllerOptions extends TaskbarContentHostOptions {
13
+ /** Milliseconds to wait for the taskbar before the window is created again. Defaults to 1000. */
14
+ recreationDelay?: number;
15
+ }
16
+ export interface ElectronTaskbarWindowControllerEventMap {
17
+ /** A window was created, before it is attached. */
18
+ windowCreated: [window: BrowserWindow];
19
+ /** The window was attached to the taskbar. */
20
+ attached: [event: HostEvent];
21
+ /** The window could not be attached. It has been hidden. */
22
+ attachFailed: [event: HostEvent];
23
+ /** The taskbar moved to another screen edge, including the first layout after attaching. */
24
+ taskbarEdgeChanged: [event: HostEvent];
25
+ layoutError: [event: HostEvent];
26
+ }
27
+ /**
28
+ * Creates a BrowserWindow, hosts it in the taskbar, and creates it again whenever the taskbar requires it.
29
+ *
30
+ * The taskbar destroys the hosted window when Explorer restarts or when the monitor it sits on is
31
+ * disconnected, and the window is detached when the system DPI changes. In each case the controller releases
32
+ * the host, waits recreationDelay milliseconds for the taskbar to settle, then creates a new window and host.
33
+ *
34
+ * createWindow must return a new, hidden BrowserWindow built with createTaskbarWindowOptions(). Do not show,
35
+ * move or resize the window yourself. While the controller runs, it handles window-all-closed so that Electron
36
+ * does not quit when the taskbar destroys the window.
37
+ */
38
+ export declare class ElectronTaskbarWindowController extends EventEmitter<ElectronTaskbarWindowControllerEventMap> {
39
+ #private;
40
+ constructor(createWindow: () => BrowserWindow, options?: ElectronTaskbarWindowControllerOptions);
41
+ /** The current window, or null while it is being created again. */
42
+ get window(): BrowserWindow | null;
43
+ get host(): TaskbarContentHost | null;
44
+ /** True between the release of a window and the creation of its replacement. */
45
+ get isRecreating(): boolean;
46
+ start(): void;
47
+ stop(): void;
48
+ }
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ElectronTaskbarWindowController = void 0;
4
+ exports.getWindowHandle = getWindowHandle;
5
+ exports.createTaskbarWindowOptions = createTaskbarWindowOptions;
6
+ const node_events_1 = require("node:events");
7
+ const electron_1 = require("electron");
8
+ const host_1 = require("./host");
9
+ const DEFAULT_RECREATION_DELAY = 1000;
10
+ const OFF_SCREEN_POSITION = -32000;
11
+ /** Returns the native window handle (HWND) of a BrowserWindow. */
12
+ function getWindowHandle(window) {
13
+ return (0, host_1.toWindowHandle)(window.getNativeWindowHandle());
14
+ }
15
+ /**
16
+ * Adds the BrowserWindow options a taskbar window needs: hidden until attached, no frame, no taskbar button
17
+ * or Alt+Tab entry, and no resizing, snapping or rounded corners. Pass the result to new BrowserWindow().
18
+ */
19
+ function createTaskbarWindowOptions(options = {}) {
20
+ return {
21
+ ...options,
22
+ type: 'toolbar',
23
+ show: false,
24
+ frame: false,
25
+ thickFrame: false,
26
+ skipTaskbar: true,
27
+ resizable: false,
28
+ minimizable: false,
29
+ maximizable: false,
30
+ fullscreenable: false,
31
+ roundedCorners: false,
32
+ hasShadow: false,
33
+ };
34
+ }
35
+ /**
36
+ * Creates a BrowserWindow, hosts it in the taskbar, and creates it again whenever the taskbar requires it.
37
+ *
38
+ * The taskbar destroys the hosted window when Explorer restarts or when the monitor it sits on is
39
+ * disconnected, and the window is detached when the system DPI changes. In each case the controller releases
40
+ * the host, waits recreationDelay milliseconds for the taskbar to settle, then creates a new window and host.
41
+ *
42
+ * createWindow must return a new, hidden BrowserWindow built with createTaskbarWindowOptions(). Do not show,
43
+ * move or resize the window yourself. While the controller runs, it handles window-all-closed so that Electron
44
+ * does not quit when the taskbar destroys the window.
45
+ */
46
+ class ElectronTaskbarWindowController extends node_events_1.EventEmitter {
47
+ #createWindow;
48
+ #options;
49
+ #window = null;
50
+ #host = null;
51
+ #recreationTimer = null;
52
+ #isRunning = false;
53
+ constructor(createWindow, options = {}) {
54
+ super();
55
+ this.#createWindow = createWindow;
56
+ this.#options = options;
57
+ }
58
+ /** The current window, or null while it is being created again. */
59
+ get window() {
60
+ return this.#window;
61
+ }
62
+ get host() {
63
+ return this.#host;
64
+ }
65
+ /** True between the release of a window and the creation of its replacement. */
66
+ get isRecreating() {
67
+ return this.#recreationTimer !== null;
68
+ }
69
+ start() {
70
+ if (this.#isRunning)
71
+ return;
72
+ this.#isRunning = true;
73
+ electron_1.app.on('window-all-closed', keepApplicationRunning);
74
+ this.#create();
75
+ }
76
+ stop() {
77
+ this.#isRunning = false;
78
+ if (this.#recreationTimer !== null)
79
+ clearTimeout(this.#recreationTimer);
80
+ this.#recreationTimer = null;
81
+ electron_1.app.removeListener('window-all-closed', keepApplicationRunning);
82
+ destroyWindow(this.#release());
83
+ }
84
+ #create() {
85
+ const window = this.#createWindow();
86
+ let host;
87
+ try {
88
+ host = new host_1.TaskbarContentHost(getWindowHandle(window), this.#options);
89
+ }
90
+ catch (error) {
91
+ destroyWindow(window);
92
+ throw error;
93
+ }
94
+ host.on('attached', event => this.emit('attached', event));
95
+ host.on('attachFailed', event => this.#onHostAttachFailed(window, event));
96
+ host.on('taskbarEdgeChanged', event => this.emit('taskbarEdgeChanged', event));
97
+ host.on('layoutError', event => this.emit('layoutError', event));
98
+ host.on('taskbarWindowRecreationRequired', () => this.#scheduleRecreation());
99
+ this.#window = window;
100
+ this.#host = host;
101
+ this.emit('windowCreated', window);
102
+ void this.#showAndAttach(window, host);
103
+ }
104
+ // Chromium sets up the surface a window renders into when the window is first shown. A window first shown
105
+ // after it became a child of the taskbar displays only its background color, and a transparent window
106
+ // shown after attaching is moved back to the screen position Chromium remembers. The window is therefore
107
+ // shown off-screen while it is still a top-level window, and attached afterwards.
108
+ async #showAndAttach(window, host) {
109
+ window.setPosition(OFF_SCREEN_POSITION, OFF_SCREEN_POSITION);
110
+ window.showInactive();
111
+ // A failure is reported through attachFailed.
112
+ await host.attachWhenLayoutReady().catch(() => undefined);
113
+ }
114
+ #onHostAttachFailed(window, event) {
115
+ if (!window.isDestroyed())
116
+ window.hide();
117
+ this.emit('attachFailed', event);
118
+ }
119
+ #scheduleRecreation() {
120
+ const previousWindow = this.#release();
121
+ this.#recreationTimer = setTimeout(() => {
122
+ this.#recreationTimer = null;
123
+ if (this.#isRunning && this.#host === null)
124
+ this.#create();
125
+ destroyWindow(previousWindow);
126
+ }, this.#options.recreationDelay ?? DEFAULT_RECREATION_DELAY);
127
+ }
128
+ #release() {
129
+ const host = this.#host;
130
+ const window = this.#window;
131
+ this.#host = null;
132
+ this.#window = null;
133
+ // Hiding first keeps the detached window from appearing as a top-level window before it is destroyed.
134
+ if (window !== null && !window.isDestroyed())
135
+ window.hide();
136
+ host?.close();
137
+ return window;
138
+ }
139
+ }
140
+ exports.ElectronTaskbarWindowController = ElectronTaskbarWindowController;
141
+ function keepApplicationRunning() { }
142
+ function destroyWindow(window) {
143
+ if (window !== null && !window.isDestroyed())
144
+ window.destroy();
145
+ }
@@ -0,0 +1,67 @@
1
+ export declare enum ResultCode {
2
+ Ok = 0,
3
+ NoEvent = 1,
4
+ InvalidArgument = -1,
5
+ InvalidHandle = -2,
6
+ WrongThread = -3,
7
+ InvalidState = -4,
8
+ UnsupportedDpiAwareness = -5,
9
+ Win32Error = -6,
10
+ Unexpected = -100
11
+ }
12
+ export declare enum TaskbarContentPlacement {
13
+ Auto = 0,
14
+ LeftEdge = 1,
15
+ BeforeNotificationArea = 2,
16
+ BeforeStartButton = 3
17
+ }
18
+ export declare enum Easing {
19
+ Linear = 0,
20
+ SineIn = 1,
21
+ SineOut = 2,
22
+ SineInOut = 3,
23
+ QuadraticIn = 4,
24
+ QuadraticOut = 5,
25
+ QuadraticInOut = 6,
26
+ CubicIn = 7,
27
+ CubicOut = 8,
28
+ CubicInOut = 9,
29
+ QuarticIn = 10,
30
+ QuarticOut = 11,
31
+ QuarticInOut = 12,
32
+ QuinticIn = 13,
33
+ QuinticOut = 14,
34
+ QuinticInOut = 15,
35
+ ExponentialIn = 16,
36
+ ExponentialOut = 17,
37
+ ExponentialInOut = 18,
38
+ CircleIn = 19,
39
+ CircleOut = 20,
40
+ CircleInOut = 21
41
+ }
42
+ export declare enum TaskbarEdge {
43
+ Unknown = 0,
44
+ Left = 1,
45
+ Top = 2,
46
+ Right = 3,
47
+ Bottom = 4
48
+ }
49
+ export declare enum TaskbarAlignment {
50
+ Unknown = 0,
51
+ Left = 1,
52
+ Center = 2
53
+ }
54
+ export declare enum HostEventKind {
55
+ None = 0,
56
+ Attached = 1,
57
+ AttachFailed = 2,
58
+ TaskbarWindowRecreated = 3,
59
+ TaskbarWindowDisappeared = 4,
60
+ TaskbarDpiChanged = 5,
61
+ TaskbarWindowRecreationRequired = 6,
62
+ TaskbarEdgeChanged = 7,
63
+ ContentBoundsChanged = 8,
64
+ Error = 9
65
+ }
66
+ /** Returns whether the taskbar sits on the left or right edge, where content is laid out from top to bottom. */
67
+ export declare function isVerticalTaskbarEdge(edge: TaskbarEdge): boolean;
package/dist/enums.js ADDED
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HostEventKind = exports.TaskbarAlignment = exports.TaskbarEdge = exports.Easing = exports.TaskbarContentPlacement = exports.ResultCode = void 0;
4
+ exports.isVerticalTaskbarEdge = isVerticalTaskbarEdge;
5
+ var ResultCode;
6
+ (function (ResultCode) {
7
+ ResultCode[ResultCode["Ok"] = 0] = "Ok";
8
+ ResultCode[ResultCode["NoEvent"] = 1] = "NoEvent";
9
+ ResultCode[ResultCode["InvalidArgument"] = -1] = "InvalidArgument";
10
+ ResultCode[ResultCode["InvalidHandle"] = -2] = "InvalidHandle";
11
+ ResultCode[ResultCode["WrongThread"] = -3] = "WrongThread";
12
+ ResultCode[ResultCode["InvalidState"] = -4] = "InvalidState";
13
+ ResultCode[ResultCode["UnsupportedDpiAwareness"] = -5] = "UnsupportedDpiAwareness";
14
+ ResultCode[ResultCode["Win32Error"] = -6] = "Win32Error";
15
+ ResultCode[ResultCode["Unexpected"] = -100] = "Unexpected";
16
+ })(ResultCode || (exports.ResultCode = ResultCode = {}));
17
+ var TaskbarContentPlacement;
18
+ (function (TaskbarContentPlacement) {
19
+ TaskbarContentPlacement[TaskbarContentPlacement["Auto"] = 0] = "Auto";
20
+ TaskbarContentPlacement[TaskbarContentPlacement["LeftEdge"] = 1] = "LeftEdge";
21
+ TaskbarContentPlacement[TaskbarContentPlacement["BeforeNotificationArea"] = 2] = "BeforeNotificationArea";
22
+ TaskbarContentPlacement[TaskbarContentPlacement["BeforeStartButton"] = 3] = "BeforeStartButton";
23
+ })(TaskbarContentPlacement || (exports.TaskbarContentPlacement = TaskbarContentPlacement = {}));
24
+ var Easing;
25
+ (function (Easing) {
26
+ Easing[Easing["Linear"] = 0] = "Linear";
27
+ Easing[Easing["SineIn"] = 1] = "SineIn";
28
+ Easing[Easing["SineOut"] = 2] = "SineOut";
29
+ Easing[Easing["SineInOut"] = 3] = "SineInOut";
30
+ Easing[Easing["QuadraticIn"] = 4] = "QuadraticIn";
31
+ Easing[Easing["QuadraticOut"] = 5] = "QuadraticOut";
32
+ Easing[Easing["QuadraticInOut"] = 6] = "QuadraticInOut";
33
+ Easing[Easing["CubicIn"] = 7] = "CubicIn";
34
+ Easing[Easing["CubicOut"] = 8] = "CubicOut";
35
+ Easing[Easing["CubicInOut"] = 9] = "CubicInOut";
36
+ Easing[Easing["QuarticIn"] = 10] = "QuarticIn";
37
+ Easing[Easing["QuarticOut"] = 11] = "QuarticOut";
38
+ Easing[Easing["QuarticInOut"] = 12] = "QuarticInOut";
39
+ Easing[Easing["QuinticIn"] = 13] = "QuinticIn";
40
+ Easing[Easing["QuinticOut"] = 14] = "QuinticOut";
41
+ Easing[Easing["QuinticInOut"] = 15] = "QuinticInOut";
42
+ Easing[Easing["ExponentialIn"] = 16] = "ExponentialIn";
43
+ Easing[Easing["ExponentialOut"] = 17] = "ExponentialOut";
44
+ Easing[Easing["ExponentialInOut"] = 18] = "ExponentialInOut";
45
+ Easing[Easing["CircleIn"] = 19] = "CircleIn";
46
+ Easing[Easing["CircleOut"] = 20] = "CircleOut";
47
+ Easing[Easing["CircleInOut"] = 21] = "CircleInOut";
48
+ })(Easing || (exports.Easing = Easing = {}));
49
+ var TaskbarEdge;
50
+ (function (TaskbarEdge) {
51
+ TaskbarEdge[TaskbarEdge["Unknown"] = 0] = "Unknown";
52
+ TaskbarEdge[TaskbarEdge["Left"] = 1] = "Left";
53
+ TaskbarEdge[TaskbarEdge["Top"] = 2] = "Top";
54
+ TaskbarEdge[TaskbarEdge["Right"] = 3] = "Right";
55
+ TaskbarEdge[TaskbarEdge["Bottom"] = 4] = "Bottom";
56
+ })(TaskbarEdge || (exports.TaskbarEdge = TaskbarEdge = {}));
57
+ var TaskbarAlignment;
58
+ (function (TaskbarAlignment) {
59
+ TaskbarAlignment[TaskbarAlignment["Unknown"] = 0] = "Unknown";
60
+ TaskbarAlignment[TaskbarAlignment["Left"] = 1] = "Left";
61
+ TaskbarAlignment[TaskbarAlignment["Center"] = 2] = "Center";
62
+ })(TaskbarAlignment || (exports.TaskbarAlignment = TaskbarAlignment = {}));
63
+ var HostEventKind;
64
+ (function (HostEventKind) {
65
+ HostEventKind[HostEventKind["None"] = 0] = "None";
66
+ HostEventKind[HostEventKind["Attached"] = 1] = "Attached";
67
+ HostEventKind[HostEventKind["AttachFailed"] = 2] = "AttachFailed";
68
+ HostEventKind[HostEventKind["TaskbarWindowRecreated"] = 3] = "TaskbarWindowRecreated";
69
+ HostEventKind[HostEventKind["TaskbarWindowDisappeared"] = 4] = "TaskbarWindowDisappeared";
70
+ HostEventKind[HostEventKind["TaskbarDpiChanged"] = 5] = "TaskbarDpiChanged";
71
+ HostEventKind[HostEventKind["TaskbarWindowRecreationRequired"] = 6] = "TaskbarWindowRecreationRequired";
72
+ HostEventKind[HostEventKind["TaskbarEdgeChanged"] = 7] = "TaskbarEdgeChanged";
73
+ HostEventKind[HostEventKind["ContentBoundsChanged"] = 8] = "ContentBoundsChanged";
74
+ HostEventKind[HostEventKind["Error"] = 9] = "Error";
75
+ })(HostEventKind || (exports.HostEventKind = HostEventKind = {}));
76
+ /** Returns whether the taskbar sits on the left or right edge, where content is laid out from top to bottom. */
77
+ function isVerticalTaskbarEdge(edge) {
78
+ return edge === TaskbarEdge.Left || edge === TaskbarEdge.Right;
79
+ }
@@ -0,0 +1,7 @@
1
+ import { ResultCode } from './enums';
2
+ /** Thrown when the native library reports an error. result holds the ResultCode of the failure. */
3
+ export declare class Deskband11LibError extends Error {
4
+ readonly result: ResultCode;
5
+ constructor(result: ResultCode, message: string);
6
+ }
7
+ export declare function checkResult(result: number): number;
package/dist/errors.js ADDED
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Deskband11LibError = void 0;
4
+ exports.checkResult = checkResult;
5
+ const bindings_1 = require("./bindings");
6
+ /** Thrown when the native library reports an error. result holds the ResultCode of the failure. */
7
+ class Deskband11LibError extends Error {
8
+ result;
9
+ constructor(result, message) {
10
+ super(message);
11
+ this.name = 'Deskband11LibError';
12
+ this.result = result;
13
+ }
14
+ }
15
+ exports.Deskband11LibError = Deskband11LibError;
16
+ function checkResult(result) {
17
+ if (result < 0)
18
+ throw new Deskband11LibError(result, (0, bindings_1.getLastErrorMessage)() || `The native call failed with result ${result}.`);
19
+ return result;
20
+ }
package/dist/host.d.ts ADDED
@@ -0,0 +1,75 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { HostEventKind, ResultCode, TaskbarAlignment, TaskbarEdge } from './enums';
3
+ import { type TaskbarContentHostOptions } from './options';
4
+ /** A native window handle (HWND), or the buffer returned by BrowserWindow.getNativeWindowHandle(). */
5
+ export type WindowHandle = number | bigint | Buffer;
6
+ /**
7
+ * An event raised by a TaskbarContentHost.
8
+ *
9
+ * maxWidth, width and height are set for ContentBoundsChanged and are in effective pixels.
10
+ * errorMessage is set when result is negative, for AttachFailed and Error.
11
+ */
12
+ export interface HostEvent {
13
+ kind: HostEventKind;
14
+ result: ResultCode;
15
+ taskbarEdge: TaskbarEdge;
16
+ maxWidth: number;
17
+ width: number;
18
+ height: number;
19
+ errorMessage?: string;
20
+ }
21
+ export interface TaskbarContentHostEventMap {
22
+ /** attachWhenLayoutReady() completed. */
23
+ attached: [event: HostEvent];
24
+ /** attachWhenLayoutReady() failed. The event carries the error message. */
25
+ attachFailed: [event: HostEvent];
26
+ /** Explorer restarted and created the taskbar again. */
27
+ taskbarWindowRecreated: [event: HostEvent];
28
+ /** The taskbar the window sat on disappeared, for example because its monitor was disconnected. */
29
+ taskbarWindowDisappeared: [event: HostEvent];
30
+ /** The system DPI changed. The window has been hidden and detached. */
31
+ taskbarDpiChanged: [event: HostEvent];
32
+ /** Raised together with each of the three events above. One handler covers all of them. */
33
+ taskbarWindowRecreationRequired: [event: HostEvent];
34
+ /** The taskbar moved to another screen edge, including the first layout after attaching. */
35
+ taskbarEdgeChanged: [event: HostEvent];
36
+ /** The window received a new size. */
37
+ contentBoundsChanged: [event: HostEvent];
38
+ /** An unexpected error occurred while the layout was updated. */
39
+ layoutError: [event: HostEvent];
40
+ }
41
+ /**
42
+ * Places a top-level window inside the Windows 11 taskbar and keeps it aligned with the taskbar layout.
43
+ *
44
+ * A host belongs to the thread that creates it, and that thread must run a Win32 message loop. The main
45
+ * process of Electron does. Events are read from the native library on a short interval and emitted on the
46
+ * same thread.
47
+ *
48
+ * The window must be per-monitor DPI aware, which Electron windows are. Create it hidden and without a frame,
49
+ * attach it, then show it without activating it. When taskbarWindowRecreationRequired is emitted, close the
50
+ * host, create the window again and create a new host for it.
51
+ */
52
+ export declare class TaskbarContentHost extends EventEmitter<TaskbarContentHostEventMap> {
53
+ #private;
54
+ constructor(windowHandle: WindowHandle, options?: TaskbarContentHostOptions);
55
+ get windowHandle(): bigint;
56
+ get isClosed(): boolean;
57
+ get isAttached(): boolean;
58
+ /** The edge the taskbar sits on. Unknown until the first layout. */
59
+ get taskbarEdge(): TaskbarEdge;
60
+ get isTaskbarVertical(): boolean;
61
+ get taskbarAlignment(): TaskbarAlignment;
62
+ /** Attaches the window immediately. Throws Deskband11LibError when the taskbar window cannot be found. */
63
+ attach(): void;
64
+ /** Starts attaching once the taskbar layout can be measured. The promise settles when attached or attachFailed is emitted. */
65
+ attachWhenLayoutReady(): Promise<void>;
66
+ /** Restores the original parent and style of the window. */
67
+ detach(): void;
68
+ /** Recalculates the layout immediately instead of waiting for the next refresh. */
69
+ refreshLayout(): void;
70
+ /** Detaches the window when attached and releases the host. Can be called from an event listener. */
71
+ close(): void;
72
+ }
73
+ /** Returns whether windowHandle still refers to an existing window, for example after Explorer restarted. */
74
+ export declare function isWindowAlive(windowHandle: WindowHandle): boolean;
75
+ export declare function toWindowHandle(windowHandle: WindowHandle): bigint;
package/dist/host.js ADDED
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TaskbarContentHost = void 0;
4
+ exports.isWindowAlive = isWindowAlive;
5
+ exports.toWindowHandle = toWindowHandle;
6
+ const node_events_1 = require("node:events");
7
+ const bindings_1 = require("./bindings");
8
+ const enums_1 = require("./enums");
9
+ const errors_1 = require("./errors");
10
+ const options_1 = require("./options");
11
+ const DEFAULT_EVENT_POLL_INTERVAL = 50;
12
+ const EVENT_NAMES = {
13
+ [enums_1.HostEventKind.Attached]: 'attached',
14
+ [enums_1.HostEventKind.AttachFailed]: 'attachFailed',
15
+ [enums_1.HostEventKind.TaskbarWindowRecreated]: 'taskbarWindowRecreated',
16
+ [enums_1.HostEventKind.TaskbarWindowDisappeared]: 'taskbarWindowDisappeared',
17
+ [enums_1.HostEventKind.TaskbarDpiChanged]: 'taskbarDpiChanged',
18
+ [enums_1.HostEventKind.TaskbarWindowRecreationRequired]: 'taskbarWindowRecreationRequired',
19
+ [enums_1.HostEventKind.TaskbarEdgeChanged]: 'taskbarEdgeChanged',
20
+ [enums_1.HostEventKind.ContentBoundsChanged]: 'contentBoundsChanged',
21
+ [enums_1.HostEventKind.Error]: 'layoutError',
22
+ };
23
+ /**
24
+ * Places a top-level window inside the Windows 11 taskbar and keeps it aligned with the taskbar layout.
25
+ *
26
+ * A host belongs to the thread that creates it, and that thread must run a Win32 message loop. The main
27
+ * process of Electron does. Events are read from the native library on a short interval and emitted on the
28
+ * same thread.
29
+ *
30
+ * The window must be per-monitor DPI aware, which Electron windows are. Create it hidden and without a frame,
31
+ * attach it, then show it without activating it. When taskbarWindowRecreationRequired is emitted, close the
32
+ * host, create the window again and create a new host for it.
33
+ */
34
+ class TaskbarContentHost extends node_events_1.EventEmitter {
35
+ #library;
36
+ #windowHandle;
37
+ #pollTimer;
38
+ #hostHandle;
39
+ #pendingAttaches = [];
40
+ constructor(windowHandle, options = {}) {
41
+ super();
42
+ this.#library = (0, bindings_1.loadLibrary)();
43
+ this.#windowHandle = toWindowHandle(windowHandle);
44
+ const hostHandle = [0];
45
+ (0, errors_1.checkResult)(this.#library.createHost(this.#windowHandle, (0, options_1.toNativeOptions)(options), null, null, hostHandle));
46
+ this.#hostHandle = hostHandle[0];
47
+ this.#pollTimer = setInterval(() => this.#pollEvents(), options.eventPollInterval ?? DEFAULT_EVENT_POLL_INTERVAL);
48
+ this.#pollTimer.unref();
49
+ }
50
+ get windowHandle() {
51
+ return this.#windowHandle;
52
+ }
53
+ get isClosed() {
54
+ return this.#hostHandle === null;
55
+ }
56
+ get isAttached() {
57
+ return this.#readValue(this.#library.isHostAttached) !== 0;
58
+ }
59
+ /** The edge the taskbar sits on. Unknown until the first layout. */
60
+ get taskbarEdge() {
61
+ return this.#readValue(this.#library.getHostTaskbarEdge);
62
+ }
63
+ get isTaskbarVertical() {
64
+ return (0, enums_1.isVerticalTaskbarEdge)(this.taskbarEdge);
65
+ }
66
+ get taskbarAlignment() {
67
+ return this.#readValue(this.#library.getHostTaskbarAlignment);
68
+ }
69
+ /** Attaches the window immediately. Throws Deskband11LibError when the taskbar window cannot be found. */
70
+ attach() {
71
+ (0, errors_1.checkResult)(this.#library.attachHost(this.#requireHandle()));
72
+ }
73
+ /** Starts attaching once the taskbar layout can be measured. The promise settles when attached or attachFailed is emitted. */
74
+ attachWhenLayoutReady() {
75
+ const hostHandle = this.#requireHandle();
76
+ return new Promise((resolve, reject) => {
77
+ const pendingAttach = { resolve, reject };
78
+ this.#pendingAttaches.push(pendingAttach);
79
+ try {
80
+ (0, errors_1.checkResult)(this.#library.attachHostWhenLayoutReady(hostHandle));
81
+ }
82
+ catch (error) {
83
+ this.#pendingAttaches = this.#pendingAttaches.filter(candidate => candidate !== pendingAttach);
84
+ reject(error);
85
+ }
86
+ });
87
+ }
88
+ /** Restores the original parent and style of the window. */
89
+ detach() {
90
+ (0, errors_1.checkResult)(this.#library.detachHost(this.#requireHandle()));
91
+ }
92
+ /** Recalculates the layout immediately instead of waiting for the next refresh. */
93
+ refreshLayout() {
94
+ (0, errors_1.checkResult)(this.#library.refreshHostLayout(this.#requireHandle()));
95
+ }
96
+ /** Detaches the window when attached and releases the host. Can be called from an event listener. */
97
+ close() {
98
+ if (this.#hostHandle === null)
99
+ return;
100
+ (0, errors_1.checkResult)(this.#library.destroyHost(this.#hostHandle));
101
+ this.#hostHandle = null;
102
+ clearInterval(this.#pollTimer);
103
+ this.#settlePendingAttaches(new errors_1.Deskband11LibError(enums_1.ResultCode.InvalidHandle, 'The host was closed before it attached.'));
104
+ }
105
+ #requireHandle() {
106
+ if (this.#hostHandle === null)
107
+ throw new errors_1.Deskband11LibError(enums_1.ResultCode.InvalidHandle, 'The host is closed.');
108
+ return this.#hostHandle;
109
+ }
110
+ #readValue(readNativeValue) {
111
+ const value = [0];
112
+ (0, errors_1.checkResult)(readNativeValue(this.#requireHandle(), value));
113
+ return value[0];
114
+ }
115
+ // Every pending event is emitted in one pass, so a burst does not wait for later intervals.
116
+ #pollEvents() {
117
+ while (this.#hostHandle !== null) {
118
+ const nativeEvent = {};
119
+ if (this.#library.pollHostEvent(this.#hostHandle, nativeEvent) !== enums_1.ResultCode.Ok)
120
+ return;
121
+ this.#dispatchEvent(createHostEvent(nativeEvent));
122
+ }
123
+ }
124
+ #dispatchEvent(event) {
125
+ const eventName = EVENT_NAMES[event.kind];
126
+ if (eventName !== undefined)
127
+ this.emit(eventName, event);
128
+ if (event.kind === enums_1.HostEventKind.Attached)
129
+ this.#settlePendingAttaches();
130
+ else if (event.kind === enums_1.HostEventKind.AttachFailed)
131
+ this.#settlePendingAttaches(new errors_1.Deskband11LibError(event.result, event.errorMessage ?? 'Attaching to the taskbar failed.'));
132
+ }
133
+ #settlePendingAttaches(error) {
134
+ const pendingAttaches = this.#pendingAttaches;
135
+ this.#pendingAttaches = [];
136
+ for (const pendingAttach of pendingAttaches) {
137
+ if (error)
138
+ pendingAttach.reject(error);
139
+ else
140
+ pendingAttach.resolve();
141
+ }
142
+ }
143
+ }
144
+ exports.TaskbarContentHost = TaskbarContentHost;
145
+ /** Returns whether windowHandle still refers to an existing window, for example after Explorer restarted. */
146
+ function isWindowAlive(windowHandle) {
147
+ return (0, bindings_1.loadLibrary)().isWindowAlive(toWindowHandle(windowHandle)) !== 0;
148
+ }
149
+ function toWindowHandle(windowHandle) {
150
+ if (!Buffer.isBuffer(windowHandle))
151
+ return BigInt(windowHandle);
152
+ return windowHandle.length >= 8 ? windowHandle.readBigUInt64LE(0) : BigInt(windowHandle.readUInt32LE(0));
153
+ }
154
+ function createHostEvent(nativeEvent) {
155
+ const event = { kind: nativeEvent.kind, result: nativeEvent.result, taskbarEdge: nativeEvent.taskbar_edge, maxWidth: nativeEvent.max_width, width: nativeEvent.width, height: nativeEvent.height };
156
+ if (nativeEvent.result < 0)
157
+ event.errorMessage = (0, bindings_1.getLastErrorMessage)();
158
+ return event;
159
+ }
@@ -0,0 +1,4 @@
1
+ export { Easing, HostEventKind, ResultCode, TaskbarAlignment, TaskbarContentPlacement, TaskbarEdge, isVerticalTaskbarEdge } from './enums';
2
+ export { Deskband11LibError } from './errors';
3
+ export { TaskbarContentHost, isWindowAlive, type HostEvent, type TaskbarContentHostEventMap, type WindowHandle } from './host';
4
+ export { STRETCH, type TaskbarContentHostOptions } from './options';
package/dist/index.js ADDED
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.STRETCH = exports.isWindowAlive = exports.TaskbarContentHost = exports.Deskband11LibError = exports.isVerticalTaskbarEdge = exports.TaskbarEdge = exports.TaskbarContentPlacement = exports.TaskbarAlignment = exports.ResultCode = exports.HostEventKind = exports.Easing = void 0;
4
+ var enums_1 = require("./enums");
5
+ Object.defineProperty(exports, "Easing", { enumerable: true, get: function () { return enums_1.Easing; } });
6
+ Object.defineProperty(exports, "HostEventKind", { enumerable: true, get: function () { return enums_1.HostEventKind; } });
7
+ Object.defineProperty(exports, "ResultCode", { enumerable: true, get: function () { return enums_1.ResultCode; } });
8
+ Object.defineProperty(exports, "TaskbarAlignment", { enumerable: true, get: function () { return enums_1.TaskbarAlignment; } });
9
+ Object.defineProperty(exports, "TaskbarContentPlacement", { enumerable: true, get: function () { return enums_1.TaskbarContentPlacement; } });
10
+ Object.defineProperty(exports, "TaskbarEdge", { enumerable: true, get: function () { return enums_1.TaskbarEdge; } });
11
+ Object.defineProperty(exports, "isVerticalTaskbarEdge", { enumerable: true, get: function () { return enums_1.isVerticalTaskbarEdge; } });
12
+ var errors_1 = require("./errors");
13
+ Object.defineProperty(exports, "Deskband11LibError", { enumerable: true, get: function () { return errors_1.Deskband11LibError; } });
14
+ var host_1 = require("./host");
15
+ Object.defineProperty(exports, "TaskbarContentHost", { enumerable: true, get: function () { return host_1.TaskbarContentHost; } });
16
+ Object.defineProperty(exports, "isWindowAlive", { enumerable: true, get: function () { return host_1.isWindowAlive; } });
17
+ var options_1 = require("./options");
18
+ Object.defineProperty(exports, "STRETCH", { enumerable: true, get: function () { return options_1.STRETCH; } });
@@ -0,0 +1,42 @@
1
+ import { type NativeOptions } from './bindings';
2
+ import type { Easing, TaskbarContentPlacement } from './enums';
3
+ /** A preferredWidth that fills the space left after fixed-width hosts are served. */
4
+ export declare const STRETCH: number;
5
+ /**
6
+ * Options for a TaskbarContentHost. Sizes are in effective pixels and durations in milliseconds.
7
+ * Options that are left out keep the defaults of the native library.
8
+ *
9
+ * preferredWidth is the length along the taskbar and preferredHeight the size across it, so on a left or
10
+ * right taskbar they map to the height and the width of the window.
11
+ */
12
+ export interface TaskbarContentHostOptions {
13
+ /** Length along the taskbar. Defaults to 360. Use STRETCH to fill the space left by fixed-width hosts. */
14
+ preferredWidth?: number;
15
+ /** Size across the taskbar, capped by its thickness. Defaults to 48. */
16
+ preferredHeight?: number;
17
+ /** Defaults to TaskbarContentPlacement.Auto. */
18
+ placement?: TaskbarContentPlacement;
19
+ /** 0 is the primary taskbar. 1 and above are secondary taskbars ordered from left to right. Defaults to 0. */
20
+ preferredMonitorIdentity?: number;
21
+ /** 0 to 65535. Lower values are placed first when several hosts share an area. Defaults to 65535, which orders by arrival. */
22
+ manualSlotPriority?: number;
23
+ /** Lets fixed-width hosts shrink proportionally when the available space runs out. Defaults to true. */
24
+ allowFixedSlotResize?: boolean;
25
+ /** Defaults to true. */
26
+ animateLayoutChanges?: boolean;
27
+ /** Matches the animation frame interval to the refresh rate of the monitor. Defaults to true. */
28
+ highRefreshRateMode?: boolean;
29
+ /** Defaults to 500. */
30
+ layoutAnimationDuration?: number;
31
+ /** Defaults to Easing.CircleOut. */
32
+ layoutAnimationEasing?: Easing;
33
+ /** Measures the taskbar buttons through UI Automation so the window never covers them. Defaults to true. */
34
+ trackTaskbarButtons?: boolean;
35
+ /** Keeps the window away from the notification area. Defaults to true. */
36
+ trackNotificationArea?: boolean;
37
+ /** Milliseconds between layout refreshes. Defaults to 500. */
38
+ layoutRefreshInterval?: number;
39
+ /** Milliseconds between checks for host events. Defaults to 50. */
40
+ eventPollInterval?: number;
41
+ }
42
+ export declare function toNativeOptions(options: TaskbarContentHostOptions): NativeOptions;
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.STRETCH = void 0;
4
+ exports.toNativeOptions = toNativeOptions;
5
+ const bindings_1 = require("./bindings");
6
+ const errors_1 = require("./errors");
7
+ /** A preferredWidth that fills the space left after fixed-width hosts are served. */
8
+ exports.STRETCH = Number.MAX_VALUE;
9
+ function toNativeOptions(options) {
10
+ if (options.manualSlotPriority !== undefined && !(options.manualSlotPriority >= 0 && options.manualSlotPriority <= 65535))
11
+ throw new RangeError('manualSlotPriority must be between 0 and 65535.');
12
+ const library = (0, bindings_1.loadLibrary)();
13
+ const nativeOptions = {};
14
+ (0, errors_1.checkResult)(library.initializeOptions(nativeOptions, library.optionsSize));
15
+ if (options.preferredWidth !== undefined)
16
+ nativeOptions.preferred_width = options.preferredWidth;
17
+ if (options.preferredHeight !== undefined)
18
+ nativeOptions.preferred_height = options.preferredHeight;
19
+ if (options.placement !== undefined)
20
+ nativeOptions.placement = options.placement;
21
+ if (options.preferredMonitorIdentity !== undefined)
22
+ nativeOptions.preferred_monitor_identity = options.preferredMonitorIdentity;
23
+ if (options.manualSlotPriority !== undefined)
24
+ nativeOptions.manual_slot_priority = options.manualSlotPriority;
25
+ if (options.allowFixedSlotResize !== undefined)
26
+ nativeOptions.allow_fixed_slot_resize = toNativeBoolean(options.allowFixedSlotResize);
27
+ if (options.animateLayoutChanges !== undefined)
28
+ nativeOptions.animate_layout_changes = toNativeBoolean(options.animateLayoutChanges);
29
+ if (options.highRefreshRateMode !== undefined)
30
+ nativeOptions.high_refresh_rate_mode = toNativeBoolean(options.highRefreshRateMode);
31
+ if (options.layoutAnimationDuration !== undefined)
32
+ nativeOptions.layout_animation_duration = options.layoutAnimationDuration;
33
+ if (options.layoutAnimationEasing !== undefined)
34
+ nativeOptions.layout_animation_easing = options.layoutAnimationEasing;
35
+ if (options.trackTaskbarButtons !== undefined)
36
+ nativeOptions.track_taskbar_buttons = toNativeBoolean(options.trackTaskbarButtons);
37
+ if (options.trackNotificationArea !== undefined)
38
+ nativeOptions.track_notification_area = toNativeBoolean(options.trackNotificationArea);
39
+ if (options.layoutRefreshInterval !== undefined)
40
+ nativeOptions.layout_refresh_interval = options.layoutRefreshInterval;
41
+ return nativeOptions;
42
+ }
43
+ function toNativeBoolean(value) {
44
+ return value ? 1 : 0;
45
+ }
package/package.json CHANGED
@@ -1,6 +1,73 @@
1
- {
2
- "name": "deskband11lib",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
1
+ {
2
+ "name": "deskband11lib",
3
+ "version": "1.5.1",
4
+ "description": "Host Electron windows and other native windows inside the Windows 11 taskbar.",
5
+ "keywords": [
6
+ "windows11",
7
+ "taskbar",
8
+ "deskband",
9
+ "widget",
10
+ "electron"
11
+ ],
12
+ "homepage": "https://db11l.kagamine-rin.com",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/airtaxi/Deskband11Lib.git",
16
+ "directory": "bindings/node"
17
+ },
18
+ "bugs": "https://github.com/airtaxi/Deskband11Lib/issues",
19
+ "license": "MIT",
20
+ "author": "airtaxi",
21
+ "type": "commonjs",
22
+ "main": "dist/index.js",
23
+ "types": "dist/index.d.ts",
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/index.d.ts",
27
+ "default": "./dist/index.js"
28
+ },
29
+ "./electron": {
30
+ "types": "./dist/electron.d.ts",
31
+ "default": "./dist/electron.js"
32
+ },
33
+ "./package.json": "./package.json"
34
+ },
35
+ "files": [
36
+ "dist",
37
+ "bin",
38
+ "README.md"
39
+ ],
40
+ "os": [
41
+ "win32"
42
+ ],
43
+ "cpu": [
44
+ "x64",
45
+ "arm64"
46
+ ],
47
+ "engines": {
48
+ "node": ">=22"
49
+ },
50
+ "peerDependencies": {
51
+ "electron": ">=30"
52
+ },
53
+ "peerDependenciesMeta": {
54
+ "electron": {
55
+ "optional": true
56
+ }
57
+ },
58
+ "scripts": {
59
+ "build": "tsc -p tsconfig.json",
60
+ "stage-native": "node scripts/stage-native.mjs",
61
+ "test": "node --test \"test/*.test.js\"",
62
+ "test:taskbar": "electron test/electron/taskbar.js",
63
+ "sample": "electron samples/electron-clock/main.js"
64
+ },
65
+ "dependencies": {
66
+ "koffi": "^3.3.7"
67
+ },
68
+ "devDependencies": {
69
+ "@types/node": "^26.6.6",
70
+ "electron": "^45.2.3",
71
+ "typescript": "^7.0.2"
72
+ }
73
+ }