@bobfrankston/winpos 2.0.52 → 2.0.53

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,213 +1,296 @@
1
- # @bobfrankston/winpos
2
-
3
- TypeScript implementation of WinPos - A Windows window positioning utility for multi-monitor setups.
4
-
5
- ## Features
6
-
7
- - **Cross-Runtime**: Works with both Node.js (24+) and Bun
8
- - **FFI-based**: Direct Windows API calls via `koffi` (Node.js) or `bun:ffi` (Bun)
9
- - **Multi-Monitor**: Full support for multiple displays
10
- - **Flexible Matching**: Find windows by exact title, prefix, or regex
11
- - **Window State Management**: Minimize, maximize, restore windows programmatically
12
- - **Auto-Restore**: Windows are automatically restored from minimized/maximized state when positioning
13
- - **Library or CLI**: Use as a command-line tool or import as a library
14
-
15
- ## Installation
16
-
17
- ```bash
18
- npm install @bobfrankston/winpos
19
- ```
20
-
21
- Or with Bun:
22
-
23
- ```bash
24
- bun add @bobfrankston/winpos
25
- ```
26
-
27
- ## Usage
28
-
29
- ### Command Line
30
-
31
- ```bash
32
- # Move window to screen 0 at position (100, 200)
33
- winpos "Notepad" 100 200 0
34
-
35
- # Move window using percentages
36
- winpos "Chrome*" 50% 50% 1
37
-
38
- # Set window size as well
39
- winpos "Visual Studio Code" 0 0 0 1920 1080
40
-
41
- # List all windows
42
- winpos *
43
-
44
- # Get screen count
45
- winpos -c
46
-
47
- # Debug mode
48
- winpos -d "Firefox" 0 0 1
49
- ```
50
-
51
- ### Pattern Matching
52
-
53
- - **Exact match**: `winpos "Notepad" 0 0 0`
54
- - **Prefix match**: `winpos "Chrome*" 0 0 0`
55
- - **Regex match**: `winpos "/Visual.*Code/" 0 0 0`
56
-
57
- ### As a Library
58
-
59
- ```typescript
60
- import { run } from '@bobfrankston/winpos';
61
-
62
- // Move a window programmatically
63
- run(['Chrome*', '0', '0', '1', '1920', '1080']);
64
- ```
65
-
66
- ## Screen Numbering
67
-
68
- Screens are numbered starting from 0, sorted by position:
69
-
70
- ```
71
- +---+---+
72
- | 2 | 3 |
73
- +---+---+
74
- | 0 | 1 |
75
- +---+---+
76
- ```
77
-
78
- - 0: Lower right (primary)
79
- - 1: Lower left
80
- - 2: Upper left
81
- - 3: Upper right
82
-
83
- ## Options
84
-
85
- - `-d` - Debug mode
86
- - `-dbg` - Verbose debug output
87
- - `-c` - Return screen count
88
- - `-v` / `-version` - Show version
89
- - `-title <name>` - Window title/pattern (alternative to positional)
90
- - `-pos x,y[,screen]` - Position (comma-separated, screen optional)
91
- - `-size w,h` - Window size (width,height)
92
- - `-numsize <inches>` - Physical size of the screen-number overlays (default 2)
93
- - `-min` - Minimize window
94
- - `-max` - Maximize window
95
- - `-load <file>` - Load config from JSON file
96
- - `-save <file>` - Save config to JSON file (merges if exists)
97
- - `*` - List all windows
98
- - `**` - List all windows including ignored
99
-
100
- ## Identifying Screens
101
-
102
- `winpos` with no arguments, and `winpos *` (or `**`), flash each screen's number
103
- in the middle of that screen for 5 seconds - the same idea as the Identify
104
- button in Windows display settings, so you can see which physical monitor is
105
- screen 0, 1, 2 ... before positioning anything there. With no arguments the
106
- numbers come up alongside the usage text and its screen table.
107
-
108
- The numbers are drawn with [msger](https://www.npmjs.com/package/@bobfrankston/msger)
109
- and all appear at once. They close themselves; winpos returns immediately
110
- without waiting for them. If msger is unavailable the listing still works, just
111
- without the numbers.
112
-
113
- Each overlay is 2 inches square **on the glass**, not 2 inches' worth of pixels
114
- - `-numsize <inches>` changes that. Sizing anything in pixels (or in CSS
115
- inches, which are just 96 pixels) comes out a different physical size on every
116
- monitor, because Windows' scale factor is a preference rather than a
117
- measurement: a 28" 4K panel left at 100% claims 96 DPI while really being 160.
118
- winpos asks the display driver for the panel's physical millimetres (EDID, via
119
- a GDI display DC) and works out each screen's true DPI, so the same square of
120
- glass lights up on every monitor. Drivers that report nothing fall back to the
121
- old pixel sizing.
122
-
123
- ```typescript
124
- import { showScreenNumbers, enumerateScreens, sortScreens, screenDpi } from '@bobfrankston/winpos';
125
-
126
- const screens = sortScreens(enumerateScreens());
127
- await showScreenNumbers(screens, 5, 2); // 5 seconds, 2 inches
128
- console.log(screens.map(screenDpi)); // True DPI per screen
129
- ```
130
-
131
- ## Parameters
132
-
133
- ### Positional (legacy)
134
- ```
135
- winpos <title> <x> <y> <screen> [<width> <height>]
136
- ```
137
-
138
- ### Flag-based (new)
139
- ```
140
- winpos <title> -pos x,y[,screen] [-size w,h]
141
- winpos -title <name> -pos x,y,screen -size w,h
142
- winpos <title> -min
143
- ```
144
-
145
- ### JSON Config
146
- ```
147
- winpos layout.json
148
- winpos -load layout.json [-save output.json]
149
- ```
150
-
151
- ## JSON Config Format
152
-
153
- Single window:
154
- ```json
155
- {
156
- "name": "Notepad",
157
- "pos": { "x": 100, "y": 100, "screen": 0 },
158
- "size": { "w": 800, "h": 600 }
159
- }
160
- ```
161
-
162
- Multiple windows:
163
- ```json
164
- [
165
- { "name": "app1", "pos": { "x": 0, "y": 0, "screen": 0 } },
166
- { "name": "app2*", "pos": { "x": 100, "y": 100, "screen": 1 } },
167
- { "name": "pattern", "regex": true, "pos": { "x": 0, "y": 0, "screen": 2 } },
168
- { "name": "minimize-me", "minimize": true }
169
- ]
170
- ```
171
-
172
- - `name` - Window title (exact match, prefix with `*`, or regex if `regex: true`)
173
- - `regex` - If true, `name` is treated as a regex pattern
174
- - `pos` - Position with x, y, and optional screen index
175
- - `size` - Size with w (width) and h (height)
176
- - `minimize` / `maximize` - Window state
177
-
178
- ## Known Issues
179
-
180
- ### Windows batch file working directory
181
-
182
- When calling `winpos` from a batch file without `call`, the working directory may not be inherited correctly due to a Windows cmd.exe quirk.
183
-
184
- **Workarounds:**
185
- - Use `call winpos` instead of `winpos` in batch files
186
- - Use `winpos.ps1` instead of `winpos`
187
- - Use absolute paths for config files: `winpos "%~dp0config.json"`
188
-
189
- ## Requirements
190
-
191
- - Windows OS
192
- - Node.js 24+ or Bun 1.0+
193
-
194
- ## Development
195
-
196
- ```bash
197
- # Install dependencies
198
- npm install
199
-
200
- # Build
201
- npm run build
202
-
203
- # Watch mode
204
- npm run watch
205
- ```
206
-
207
- ## License
208
-
209
- MIT
210
-
211
- ## Author
212
-
213
- Bob Frankston
1
+ # @bobfrankston/winpos
2
+
3
+ TypeScript implementation of WinPos - A Windows window positioning utility for multi-monitor setups.
4
+
5
+ ## Features
6
+
7
+ - **Cross-Runtime**: Works with both Node.js (24+) and Bun
8
+ - **FFI-based**: Direct Windows API calls via `koffi` (Node.js) or `bun:ffi` (Bun)
9
+ - **Multi-Monitor**: Full support for multiple displays
10
+ - **Flexible Matching**: Find windows by exact title, prefix, or regex
11
+ - **Window State Management**: Minimize, maximize, restore windows programmatically
12
+ - **Auto-Restore**: Windows are automatically restored from minimized/maximized state when positioning
13
+ - **Monitor Arrangement**: Save the monitor layout and put it back when Windows reverts it (`-screens`)
14
+ - **Library or CLI**: Use as a command-line tool or import as a library
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install @bobfrankston/winpos
20
+ ```
21
+
22
+ Or with Bun:
23
+
24
+ ```bash
25
+ bun add @bobfrankston/winpos
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ ### Command Line
31
+
32
+ ```bash
33
+ # Move window to screen 0 at position (100, 200)
34
+ winpos "Notepad" 100 200 0
35
+
36
+ # Move window using percentages
37
+ winpos "Chrome*" 50% 50% 1
38
+
39
+ # Set window size as well
40
+ winpos "Visual Studio Code" 0 0 0 1920 1080
41
+
42
+ # List all windows
43
+ winpos *
44
+
45
+ # Get screen count
46
+ winpos -c
47
+
48
+ # Which physical monitor is screen 0, 1, 2...: id, name, position, mode
49
+ winpos -screens
50
+
51
+ # Save the monitor arrangement, and put it back after Windows reverts it
52
+ winpos -screens save
53
+ winpos -screens restore
54
+
55
+ # Debug mode
56
+ winpos -d "Firefox" 0 0 1
57
+ ```
58
+
59
+ ### Pattern Matching
60
+
61
+ - **Exact match**: `winpos "Notepad" 0 0 0`
62
+ - **Prefix match**: `winpos "Chrome*" 0 0 0`
63
+ - **Regex match**: `winpos "/Visual.*Code/" 0 0 0`
64
+
65
+ ### As a Library
66
+
67
+ ```typescript
68
+ import { run } from '@bobfrankston/winpos';
69
+
70
+ // Move a window programmatically
71
+ run(['Chrome*', '0', '0', '1', '1920', '1080']);
72
+ ```
73
+
74
+ ## Screen Numbering
75
+
76
+ Screens are numbered starting from 0, sorted by position:
77
+
78
+ ```
79
+ +---+---+
80
+ | 2 | 3 |
81
+ +---+---+
82
+ | 0 | 1 |
83
+ +---+---+
84
+ ```
85
+
86
+ - 0: Lower right (primary)
87
+ - 1: Lower left
88
+ - 2: Upper left
89
+ - 3: Upper right
90
+
91
+ ## Options
92
+
93
+ - `-d` - Debug mode
94
+ - `-dbg` - Verbose debug output
95
+ - `-c` - Return screen count
96
+ - `-v` / `-version` - Show version
97
+ - `-title <name>` - Window title/pattern (alternative to positional)
98
+ - `-pos x,y[,screen]` - Position (comma-separated, screen optional)
99
+ - `-size w,h` - Window size (width,height)
100
+ - `-numsize <inches>` - Physical size of the screen-number overlays (default 2)
101
+ - `-min` - Minimize window
102
+ - `-max` - Maximize window
103
+ - `-load <file>` - Load config from JSON file
104
+ - `-save <file>` - Save config to JSON file (merges if exists)
105
+ - `-screens [list | save [name] | restore [name] | layouts | watch [name]]` - Monitor arrangement (see below)
106
+ - `*` - List all windows
107
+ - `**` - List all windows including ignored
108
+
109
+ ## Identifying Screens
110
+
111
+ `winpos` with no arguments, and `winpos *` (or `**`), flash each screen's number
112
+ in the middle of that screen for 5 seconds - the same idea as the Identify
113
+ button in Windows display settings, so you can see which physical monitor is
114
+ screen 0, 1, 2 ... before positioning anything there. With no arguments the
115
+ numbers come up alongside the usage text and its screen table.
116
+
117
+ The numbers are drawn with [msger](https://www.npmjs.com/package/@bobfrankston/msger)
118
+ and all appear at once. They close themselves; winpos returns immediately
119
+ without waiting for them. If msger is unavailable the listing still works, just
120
+ without the numbers.
121
+
122
+ Each overlay is 2 inches square **on the glass**, not 2 inches' worth of pixels
123
+ - `-numsize <inches>` changes that. Sizing anything in pixels (or in CSS
124
+ inches, which are just 96 pixels) comes out a different physical size on every
125
+ monitor, because Windows' scale factor is a preference rather than a
126
+ measurement: a 28" 4K panel left at 100% claims 96 DPI while really being 160.
127
+ winpos asks the display driver for the panel's physical millimetres (EDID, via
128
+ a GDI display DC) and works out each screen's true DPI, so the same square of
129
+ glass lights up on every monitor. Drivers that report nothing fall back to the
130
+ old pixel sizing.
131
+
132
+ ```typescript
133
+ import { showScreenNumbers, enumerateScreens, sortScreens, screenDpi } from '@bobfrankston/winpos';
134
+
135
+ const screens = sortScreens(enumerateScreens());
136
+ await showScreenNumbers(screens, 5, 2); // 5 seconds, 2 inches
137
+ console.log(screens.map(screenDpi)); // True DPI per screen
138
+ ```
139
+
140
+ ## Monitor Arrangement
141
+
142
+ Screen numbers are the monitors sorted by position, and saved window layouts
143
+ address screens by that number. When Windows rearranges the monitors, every
144
+ number changes and every layout puts its windows on the wrong panel. So winpos
145
+ can save the monitor arrangement and put it back - before the windows, in the
146
+ same command.
147
+
148
+ Windows keeps one arrangement per *exact set* of connected monitors. A
149
+ DisplayPort monitor that powers off or deep-sleeps drops off the bus, the set
150
+ changes, and a set Windows has never seen gets the default left-to-right line.
151
+ That is the "my monitors keep reverting" problem this solves.
152
+
153
+ ```
154
+ winpos -screens list monitors: screen number, device, id, name, position, mode, primary
155
+ winpos -screens save [name] save the current arrangement
156
+ winpos -screens restore [name] put the saved arrangement back, then read it back and verify
157
+ winpos -screens layouts list the layouts saved for this machine
158
+ winpos -screens watch [name] stay resident and restore whenever Windows moves a saved monitor
159
+ ```
160
+
161
+ Monitors are identified by EDID id and serial number (`GSM7721:106NTKF55597`),
162
+ read from the monitor's EDID in the registry - never by `\\.\DISPLAYn`, which
163
+ renumbers whenever a monitor comes or goes. A machine normally has one standard
164
+ arrangement, so the name is optional: without one, every command uses the
165
+ layout called `default`. Give a name only for an alternate setup (a travel
166
+ layout, a single-monitor layout).
167
+
168
+ Layouts are kept per machine in `winpos-screens.json` in the user config
169
+ directory (`@bobfrankston/userconfig`). The file has the same shape as the
170
+ older `displays.ps1` tool's `displays.json`, so a layout copies between them.
171
+
172
+ `restore` applies the layout to whichever of its monitors are connected, and
173
+ says so: a saved monitor that is absent is reported and skipped, a connected
174
+ monitor the layout does not mention is left where Windows put it. It then
175
+ waits for Windows to settle, reads the arrangement back, and reports what
176
+ Windows actually did - Windows closes gaps when a monitor is missing, so the
177
+ result can differ from the request. Exit code 1 if anything did not land where
178
+ asked, or if the layout does not exist.
179
+
180
+ `watch` polls the arrangement every few seconds (no message loop, no
181
+ callbacks) and, once a change has settled, re-applies the layout if any saved
182
+ monitor is out of place. It ignores the change its own apply causes, and if an
183
+ apply changes nothing it stops retrying until the set of monitors changes
184
+ again, so it cannot loop. Run it from a logon task if you want it always on.
185
+
186
+ A window layout file can restore the monitors first by using the wrapped form:
187
+
188
+ ```json
189
+ {
190
+ "screens": "default",
191
+ "windows": [
192
+ { "name": "Notepad", "pos": { "x": 100, "y": 100, "screen": 0 } }
193
+ ]
194
+ }
195
+ ```
196
+
197
+ `screens` is a saved layout name, or an inline layout (the `monitors` array as
198
+ saved). Files without `screens` behave exactly as before.
199
+
200
+ ```typescript
201
+ import { enumerateMonitors, saveLayout, getLayout, restoreLayout, formatMonitors } from '@bobfrankston/winpos';
202
+
203
+ console.log(formatMonitors(enumerateMonitors()).join('\n'));
204
+ saveLayout(); // the machine's standard layout
205
+ const result = await restoreLayout(getLayout());
206
+ console.log(result.ok, result.absent, result.mismatches);
207
+ ```
208
+
209
+ ## Parameters
210
+
211
+ ### Positional (legacy)
212
+ ```
213
+ winpos <title> <x> <y> <screen> [<width> <height>]
214
+ ```
215
+
216
+ ### Flag-based (new)
217
+ ```
218
+ winpos <title> -pos x,y[,screen] [-size w,h]
219
+ winpos -title <name> -pos x,y,screen -size w,h
220
+ winpos <title> -min
221
+ ```
222
+
223
+ ### JSON Config
224
+ ```
225
+ winpos layout.json
226
+ winpos -load layout.json [-save output.json]
227
+ ```
228
+
229
+ ## JSON Config Format
230
+
231
+ Single window:
232
+ ```json
233
+ {
234
+ "name": "Notepad",
235
+ "pos": { "x": 100, "y": 100, "screen": 0 },
236
+ "size": { "w": 800, "h": 600 }
237
+ }
238
+ ```
239
+
240
+ Multiple windows:
241
+ ```json
242
+ [
243
+ { "name": "app1", "pos": { "x": 0, "y": 0, "screen": 0 } },
244
+ { "name": "app2*", "pos": { "x": 100, "y": 100, "screen": 1 } },
245
+ { "name": "pattern", "regex": true, "pos": { "x": 0, "y": 0, "screen": 2 } },
246
+ { "name": "minimize-me", "minimize": true }
247
+ ]
248
+ ```
249
+
250
+ - `name` - Window title (exact match, prefix with `*`, or regex if `regex: true`)
251
+ - `regex` - If true, `name` is treated as a regex pattern
252
+ - `pos` - Position with x, y, and optional screen index
253
+ - `size` - Size with w (width) and h (height)
254
+ - `minimize` / `maximize` - Window state
255
+
256
+ Wrapped form, monitors first then windows (see Monitor Arrangement):
257
+ ```json
258
+ { "screens": "default", "windows": [ { "name": "app1", "pos": { "x": 0, "y": 0, "screen": 0 } } ] }
259
+ ```
260
+
261
+ ## Known Issues
262
+
263
+ ### Windows batch file working directory
264
+
265
+ When calling `winpos` from a batch file without `call`, the working directory may not be inherited correctly due to a Windows cmd.exe quirk.
266
+
267
+ **Workarounds:**
268
+ - Use `call winpos` instead of `winpos` in batch files
269
+ - Use `winpos.ps1` instead of `winpos`
270
+ - Use absolute paths for config files: `winpos "%~dp0config.json"`
271
+
272
+ ## Requirements
273
+
274
+ - Windows OS
275
+ - Node.js 24+ or Bun 1.0+
276
+
277
+ ## Development
278
+
279
+ ```bash
280
+ # Install dependencies
281
+ npm install
282
+
283
+ # Build
284
+ npm run build
285
+
286
+ # Watch mode
287
+ npm run watch
288
+ ```
289
+
290
+ ## License
291
+
292
+ MIT
293
+
294
+ ## Author
295
+
296
+ Bob Frankston
package/ffi-wrapper.d.ts CHANGED
@@ -30,6 +30,41 @@ export interface MONITORINFO {
30
30
  }
31
31
  export declare const MONITORINFOF_PRIMARY = 1;
32
32
  export declare const DEFAULT_SCALE_PERCENT = 100;
33
+ export declare const DISPLAY_DEVICE_ATTACHED_TO_DESKTOP = 1;
34
+ export declare const DISPLAY_DEVICE_PRIMARY_DEVICE = 4;
35
+ export declare const DISPLAY_DEVICE_MIRRORING_DRIVER = 8;
36
+ export declare const EDD_GET_DEVICE_INTERFACE_NAME = 1;
37
+ /** ChangeDisplaySettingsExW return codes */
38
+ export declare const DISP_CHANGE_SUCCESSFUL = 0;
39
+ export declare const DISP_CHANGE_RESTART = 1;
40
+ export declare const DISP_CHANGE_FAILED = -1;
41
+ export declare const DISP_CHANGE_BADMODE = -2;
42
+ export declare const DISP_CHANGE_NOTUPDATED = -3;
43
+ export declare const DISP_CHANGE_BADFLAGS = -4;
44
+ export declare const DISP_CHANGE_BADPARAM = -5;
45
+ export declare const DISP_CHANGE_BADDUALVIEW = -6;
46
+ /** One entry from EnumDisplayDevicesW: a display adapter, or the monitor on one */
47
+ export interface DisplayDevice {
48
+ /** Adapter: \\.\DISPLAY1. Monitor: \\.\DISPLAY1\Monitor0 */
49
+ deviceName: string;
50
+ /** Driver's description: adapter model, or "Generic PnP Monitor"-style monitor text */
51
+ deviceString: string;
52
+ /** DISPLAY_DEVICE_* bits */
53
+ stateFlags: number;
54
+ /** Monitor with EDD_GET_DEVICE_INTERFACE_NAME: \\?\DISPLAY#GSM7721#4&11d6709e&0&UID53318#{guid} */
55
+ deviceID: string;
56
+ deviceKey: string;
57
+ }
58
+ /** An adapter's current mode from EnumDisplaySettingsW */
59
+ export interface DisplayMode {
60
+ /** Desktop position of the top-left corner, in pixels; the primary is 0,0 */
61
+ x: number;
62
+ y: number;
63
+ width: number;
64
+ height: number;
65
+ /** Refresh rate in Hz */
66
+ hz: number;
67
+ }
33
68
  export interface WindowsAPI {
34
69
  EnumWindows: (callback: (hwnd: bigint, lParam: bigint) => boolean, lParam: bigint) => boolean;
35
70
  GetWindowTextW: (hwnd: bigint, text: any, maxCount: number) => number;
@@ -49,6 +84,20 @@ export interface WindowsAPI {
49
84
  width: number;
50
85
  height: number;
51
86
  } | null;
87
+ /** Adapter iDevNum (device null), or the monitor on an adapter (device = its name, iDevNum 0). Null past the end. */
88
+ EnumDisplayDevicesW?: (device: string, iDevNum: number, flags: number) => DisplayDevice;
89
+ /** Current mode of an adapter. Null when the adapter has none (not attached to the desktop). */
90
+ EnumDisplaySettingsW?: (device: string) => DisplayMode;
91
+ /**
92
+ * Stage a new desktop position for one adapter: written to the registry,
93
+ * not applied yet. Apply all staged positions with ApplyDisplayChanges.
94
+ * Returns a DISP_CHANGE_* code.
95
+ */
96
+ StageDisplayPosition?: (device: string, x: number, y: number, primary: boolean) => number;
97
+ /** Apply every position staged with StageDisplayPosition. Returns a DISP_CHANGE_* code. */
98
+ ApplyDisplayChanges?: () => number;
99
+ /** A REG_BINARY value under HKEY_LOCAL_MACHINE. Null when the key or value is absent or unreadable. */
100
+ RegGetBinaryHKLM?: (subKey: string, valueName: string) => Buffer;
52
101
  }
53
102
  declare const user32: WindowsAPI;
54
103
  export { user32, isBun };