@bobfrankston/winpos 2.0.51 → 2.0.52

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
@@ -89,6 +89,7 @@ Screens are numbered starting from 0, sorted by position:
89
89
  - `-title <name>` - Window title/pattern (alternative to positional)
90
90
  - `-pos x,y[,screen]` - Position (comma-separated, screen optional)
91
91
  - `-size w,h` - Window size (width,height)
92
+ - `-numsize <inches>` - Physical size of the screen-number overlays (default 2)
92
93
  - `-min` - Minimize window
93
94
  - `-max` - Maximize window
94
95
  - `-load <file>` - Load config from JSON file
@@ -109,10 +110,22 @@ and all appear at once. They close themselves; winpos returns immediately
109
110
  without waiting for them. If msger is unavailable the listing still works, just
110
111
  without the numbers.
111
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
+
112
123
  ```typescript
113
- import { showScreenNumbers, enumerateScreens, sortScreens } from '@bobfrankston/winpos';
124
+ import { showScreenNumbers, enumerateScreens, sortScreens, screenDpi } from '@bobfrankston/winpos';
114
125
 
115
- await showScreenNumbers(sortScreens(enumerateScreens()), 5);
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
116
129
  ```
117
130
 
118
131
  ## Parameters
package/ffi-wrapper.d.ts CHANGED
@@ -25,6 +25,8 @@ export interface MONITORINFO {
25
25
  rcMonitor: RECT;
26
26
  rcWork: RECT;
27
27
  dwFlags: number;
28
+ /** Display device name, e.g. \\.\DISPLAY1 (MONITORINFOEXW szDevice) */
29
+ szDevice?: string;
28
30
  }
29
31
  export declare const MONITORINFOF_PRIMARY = 1;
30
32
  export declare const DEFAULT_SCALE_PERCENT = 100;
@@ -42,6 +44,11 @@ export interface WindowsAPI {
42
44
  GetMonitorInfoW: (hMonitor: bigint, lpmi: any) => boolean;
43
45
  /** Monitor scale factor as a percent (100, 125, 150...). 100 when unavailable. */
44
46
  GetScaleFactorForMonitor?: (hMonitor: bigint) => number;
47
+ /** Physical panel size in millimetres from the display driver (EDID). Null when unavailable. */
48
+ GetPhysicalSizeMm?: (deviceName: string) => {
49
+ width: number;
50
+ height: number;
51
+ } | null;
45
52
  }
46
53
  declare const user32: WindowsAPI;
47
54
  export { user32, isBun };
package/ffi-wrapper.js CHANGED
@@ -16,6 +16,9 @@ export const SW_MINIMIZE = 6;
16
16
  export const SW_SHOWMINNOACTIVE = 7;
17
17
  export const SW_SHOWNA = 8;
18
18
  export const SW_RESTORE = 9;
19
+ // GetDeviceCaps indices for the panel's physical size in millimetres
20
+ const HORZSIZE = 4;
21
+ const VERTSIZE = 6;
19
22
  export const MONITORINFOF_PRIMARY = 1;
20
23
  // Monitor scale factor as a percentage (100 = no scaling, 125 = 125%)
21
24
  export const DEFAULT_SCALE_PERCENT = 100;
@@ -110,6 +113,21 @@ async function initializeFFI() {
110
113
  catch {
111
114
  // Leave null - callers fall back to 100%
112
115
  }
116
+ // A display DC reports the panel's physical size in millimetres, straight
117
+ // from the driver's EDID. That is the only measurement of a monitor
118
+ // available: the scale factor above is a user preference, so a 160 DPI
119
+ // panel left at 100% claims to be a 96 DPI one. Optional, like shcore -
120
+ // callers fall back to nominal DPI when this returns null.
121
+ let CreateDCW = null, GetDeviceCaps = null, DeleteDC = null;
122
+ try {
123
+ const gdi32 = koffi.load('gdi32.dll');
124
+ CreateDCW = gdi32.func('void * __stdcall CreateDCW(const char16_t *pwszDriver, const char16_t *pwszDevice, const char16_t *pszPort, const void *pdm)');
125
+ GetDeviceCaps = gdi32.func('int __stdcall GetDeviceCaps(void *hdc, int index)');
126
+ DeleteDC = gdi32.func('bool __stdcall DeleteDC(void *hdc)');
127
+ }
128
+ catch {
129
+ CreateDCW = null;
130
+ }
113
131
  // Keep references to registered callbacks to prevent garbage collection
114
132
  // These must persist for the entire duration of the native callback invocation
115
133
  const __callbackRefs = [];
@@ -154,14 +172,18 @@ async function initializeFFI() {
154
172
  return result;
155
173
  },
156
174
  GetMonitorInfoW: (hMonitor, lpmi) => {
157
- // MONITORINFO structure:
175
+ // MONITORINFOEXW structure:
158
176
  // DWORD cbSize (4 bytes)
159
177
  // RECT rcMonitor (16 bytes)
160
178
  // RECT rcWork (16 bytes)
161
179
  // DWORD dwFlags (4 bytes)
162
- // Total: 40 bytes
163
- const buffer = Buffer.alloc(40);
164
- buffer.writeUInt32LE(40, 0); // cbSize
180
+ // WCHAR szDevice[32] (64 bytes) - the EX part, and the reason for
181
+ // asking: the device name is what gdi32 wants to open a DC on
182
+ // this monitor, and the only reliable tie from an HMONITOR to a
183
+ // \\.\DISPLAYn. cbSize is what selects EX over plain MONITORINFO.
184
+ // Total: 104 bytes
185
+ const buffer = Buffer.alloc(104);
186
+ buffer.writeUInt32LE(104, 0); // cbSize
165
187
  const result = GetMonitorInfoW(hMonitor, buffer);
166
188
  if (result) {
167
189
  lpmi.cbSize = buffer.readUInt32LE(0);
@@ -178,6 +200,9 @@ async function initializeFFI() {
178
200
  Bottom: buffer.readInt32LE(32),
179
201
  };
180
202
  lpmi.dwFlags = buffer.readUInt32LE(36);
203
+ const device = buffer.toString('utf16le', 40, 104);
204
+ const end = device.indexOf('\0');
205
+ lpmi.szDevice = end >= 0 ? device.substring(0, end) : device;
181
206
  }
182
207
  return result;
183
208
  },
@@ -188,6 +213,32 @@ async function initializeFFI() {
188
213
  const hr = GetScaleFactorForMonitorRaw(hMonitor, scale);
189
214
  return hr === 0 ? (scale.readUInt32LE(0) || DEFAULT_SCALE_PERCENT) : DEFAULT_SCALE_PERCENT;
190
215
  },
216
+ GetPhysicalSizeMm: (deviceName) => {
217
+ if (!CreateDCW || !deviceName)
218
+ return null;
219
+ let hdc = null;
220
+ try {
221
+ // An information DC would do, but CreateDCW on the DISPLAY
222
+ // driver is what every monitor exposes; the caps are the same.
223
+ hdc = CreateDCW('DISPLAY', deviceName, null, null);
224
+ if (!hdc)
225
+ return null;
226
+ const width = GetDeviceCaps(hdc, HORZSIZE);
227
+ const height = GetDeviceCaps(hdc, VERTSIZE);
228
+ return width > 0 && height > 0 ? { width, height } : null;
229
+ }
230
+ catch {
231
+ return null; // Best-effort: no measurement is not an error
232
+ }
233
+ finally {
234
+ if (hdc) {
235
+ try {
236
+ DeleteDC(hdc);
237
+ }
238
+ catch { /* nothing useful to do */ }
239
+ }
240
+ }
241
+ },
191
242
  };
192
243
  }
193
244
  const user32 = await initializeFFI();
package/index.d.ts CHANGED
@@ -4,10 +4,10 @@
4
4
  * Cross-platform Node.js/Bun implementation with FFI
5
5
  */
6
6
  import { user32, RECT } from './ffi-wrapper.js';
7
- import { enumerateScreens, sortScreens, ScreenInfo } from './screens.js';
7
+ import { enumerateScreens, sortScreens, screenScale, screenDpi, ScreenInfo } from './screens.js';
8
8
  import { enumerateWindows, findWindowsByPattern, WindowInfo, WindowState, getWindowState, setWindowState, isMinimized, isMaximized, isNormal, minimizeWindow, maximizeWindow, restoreWindow } from './windows.js';
9
9
  import { enumerateTabs, TabInfo, mightHaveTabs, TAB_ENUMERATION_NOTE } from './tabs.js';
10
- import { showScreenNumbers } from './screennums.js';
10
+ import { showScreenNumbers, OVERLAY_SECONDS, OVERLAY_INCHES } from './screennums.js';
11
11
  export interface WindowConfig {
12
12
  name: string;
13
13
  regex?: boolean;
@@ -24,7 +24,7 @@ export interface WindowConfig {
24
24
  maximize?: boolean;
25
25
  }
26
26
  export type ConfigFile = WindowConfig | WindowConfig[];
27
- export { RECT, ScreenInfo, WindowInfo, WindowState, TabInfo, enumerateScreens, sortScreens, enumerateWindows, findWindowsByPattern, getWindowState, setWindowState, isMinimized, isMaximized, isNormal, minimizeWindow, maximizeWindow, restoreWindow, enumerateTabs, mightHaveTabs, TAB_ENUMERATION_NOTE, showScreenNumbers, user32 };
27
+ export { RECT, ScreenInfo, WindowInfo, WindowState, TabInfo, enumerateScreens, sortScreens, screenScale, screenDpi, enumerateWindows, findWindowsByPattern, getWindowState, setWindowState, isMinimized, isMaximized, isNormal, minimizeWindow, maximizeWindow, restoreWindow, enumerateTabs, mightHaveTabs, TAB_ENUMERATION_NOTE, showScreenNumbers, OVERLAY_INCHES, OVERLAY_SECONDS, user32 };
28
28
  /**
29
29
  * Convert screen-relative coordinates to global coordinates
30
30
  * @param x - X coordinate relative to screen
package/index.js CHANGED
@@ -4,16 +4,16 @@
4
4
  * Cross-platform Node.js/Bun implementation with FFI
5
5
  */
6
6
  import { user32 } from './ffi-wrapper.js';
7
- import { enumerateScreens, sortScreens } from './screens.js';
7
+ import { enumerateScreens, sortScreens, screenScale, screenDpi } from './screens.js';
8
8
  import { enumerateWindows, findWindowsByPattern, loadIgnorePatterns, WindowState, getWindowState, setWindowState, isMinimized, isMaximized, isNormal, minimizeWindow, maximizeWindow, restoreWindow } from './windows.js';
9
9
  import { enumerateTabs, mightHaveTabs, TAB_ENUMERATION_NOTE } from './tabs.js';
10
- import { showScreenNumbers, OVERLAY_SECONDS } from './screennums.js';
10
+ import { showScreenNumbers, OVERLAY_SECONDS, OVERLAY_INCHES } from './screennums.js';
11
11
  import { join, resolve, isAbsolute } from 'path';
12
12
  import { readFileSync, writeFileSync, existsSync } from 'fs';
13
13
  import { parse as parseJsonc, printParseErrorCode } from 'jsonc-parser';
14
14
  import * as packageJson from './package.json' with { type: 'json' };
15
15
  // Re-export public API for library usage
16
- export { WindowState, enumerateScreens, sortScreens, enumerateWindows, findWindowsByPattern, getWindowState, setWindowState, isMinimized, isMaximized, isNormal, minimizeWindow, maximizeWindow, restoreWindow, enumerateTabs, mightHaveTabs, TAB_ENUMERATION_NOTE, showScreenNumbers, user32 };
16
+ export { WindowState, enumerateScreens, sortScreens, screenScale, screenDpi, enumerateWindows, findWindowsByPattern, getWindowState, setWindowState, isMinimized, isMaximized, isNormal, minimizeWindow, maximizeWindow, restoreWindow, enumerateTabs, mightHaveTabs, TAB_ENUMERATION_NOTE, showScreenNumbers, OVERLAY_INCHES, OVERLAY_SECONDS, user32 };
17
17
  // WindowConfig and ConfigFile types are exported from interface definitions above
18
18
  // Note: screenPosToPos is exported separately below (after the function definition)
19
19
  /**
@@ -179,6 +179,16 @@ function parseArgs(args) {
179
179
  opts.windowTitle = args[i];
180
180
  }
181
181
  break;
182
+ case 'numsize':
183
+ i++;
184
+ if (i < args.length) {
185
+ const inches = parseFloat(args[i]);
186
+ if (inches > 0)
187
+ opts.numSize = inches;
188
+ else
189
+ console.log(`Ignoring -numsize ${args[i]}: expected a size in inches`);
190
+ }
191
+ break;
182
192
  case 'min':
183
193
  opts.minimize = true;
184
194
  break;
@@ -289,6 +299,7 @@ function applyWindowConfig(config, screensInfo) {
289
299
  let dbg = false;
290
300
  let dbgVerbose = false; // -dbg flag for detailed output
291
301
  let screens = [];
302
+ let numSize = OVERLAY_INCHES; // -numsize: screen-number overlay size in inches
292
303
  /**
293
304
  * Convert screen-relative coordinates to global coordinates
294
305
  * @param x - X coordinate relative to screen
@@ -450,7 +461,7 @@ function listAllWindows(includeIgnored = false) {
450
461
  console.log('Command format: tswinpos <title> <x> <y> <screen> [<width> <height>]');
451
462
  // Flash each screen's number on that screen - launched last so the overlays
452
463
  // can't turn up in the listing above. Detached, so we don't wait for them.
453
- void showScreenNumbers(screens);
464
+ void showScreenNumbers(screens, OVERLAY_SECONDS, numSize);
454
465
  }
455
466
  function usage(msg = '') {
456
467
  printUsage(msg);
@@ -478,6 +489,7 @@ function printUsage(msg = '') {
478
489
  console.log(' -title name Window title/pattern (alternative to positional)');
479
490
  console.log(' -pos x,y[,s] Position (x,y with optional screen index)');
480
491
  console.log(' -size w,h Window size (width,height)');
492
+ console.log(` -numsize n Screen-number overlay size in inches (default ${OVERLAY_INCHES})`);
481
493
  console.log(' -min Minimize window');
482
494
  console.log(' -max Maximize window');
483
495
  console.log(' -load file Load config from JSON/JSONC file (comments + trailing commas OK)');
@@ -519,6 +531,8 @@ export function run(args) {
519
531
  // Set debug flags
520
532
  dbg = opts.debug;
521
533
  dbgVerbose = opts.debugVerbose;
534
+ if (opts.numSize)
535
+ numSize = opts.numSize;
522
536
  if (dbg)
523
537
  console.log('Debug mode enabled.');
524
538
  if (dbgVerbose)
@@ -615,7 +629,7 @@ export function run(args) {
615
629
  // themselves; we only wait for the launch, because process.exit here would
616
630
  // kill the spawn in mid-flight and no numbers would ever appear.
617
631
  if (posArgs.length === 0) {
618
- const overlays = showScreenNumbers(screens);
632
+ const overlays = showScreenNumbers(screens, OVERLAY_SECONDS, numSize);
619
633
  printUsage();
620
634
  void overlays.finally(() => process.exit(1));
621
635
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/winpos",
3
- "version": "2.0.51",
3
+ "version": "2.0.52",
4
4
  "description": "TypeScript implementation of winpos - Windows window positioning utility",
5
5
  "type": "module",
6
6
  "main": "index.js",
package/screennums.d.ts CHANGED
@@ -7,6 +7,8 @@
7
7
  * self-closing, so winpos exits immediately and leaves them up.
8
8
  */
9
9
  import { type ScreenInfo } from './screens.js';
10
+ /** Default overlay size, in real inches on the glass */
11
+ export declare const OVERLAY_INCHES = 2;
10
12
  export declare const OVERLAY_SECONDS = 5;
11
13
  /**
12
14
  * Show the index of every screen in the middle of that screen.
@@ -16,6 +18,7 @@ export declare const OVERLAY_SECONDS = 5;
16
18
  *
17
19
  * @param screens - Screens in winpos order (the index shown is the array index)
18
20
  * @param seconds - How long each overlay stays up
21
+ * @param inches - Size of each overlay in real inches, measured on the glass
19
22
  */
20
- export declare function showScreenNumbers(screens: ScreenInfo[], seconds?: number): Promise<void>;
23
+ export declare function showScreenNumbers(screens: ScreenInfo[], seconds?: number, inches?: number): Promise<void>;
21
24
  //# sourceMappingURL=screennums.d.ts.map
package/screennums.js CHANGED
@@ -6,13 +6,33 @@
6
6
  * Rendered with msger (@bobfrankston/msger). The overlays are detached and
7
7
  * self-closing, so winpos exits immediately and leaves them up.
8
8
  */
9
- import { screenScale } from './screens.js';
10
- const CARD = 260; // Overlay content size in pixels (square)
11
- const FRAME_W = 16; // Window border added around that content
12
- const FRAME_H = 46; // Border + title bar
9
+ import { screenDpi, screenScale } from './screens.js';
10
+ /** Default overlay size, in real inches on the glass */
11
+ export const OVERLAY_INCHES = 2;
13
12
  export const OVERLAY_SECONDS = 5;
13
+ // Guard rails for the computed window size, in case a driver's reported
14
+ // dimensions survive the DPI sanity check but still make no sense.
15
+ const MIN_CARD = 80;
16
+ const MAX_CARD = 1200;
17
+ /**
18
+ * Overlay size in logical pixels so it measures `inches` on that panel.
19
+ *
20
+ * Pixels are not a length: the same 360-pixel square is 3.9 inches on a 93 DPI
21
+ * 32" 1440p panel and 2.2 inches on a 160 DPI 28" 4K one. Going through the
22
+ * screen's true DPI is what makes the number the same physical size on every
23
+ * monitor. The scale factor divides back out because msger positions and sizes
24
+ * windows in logical pixels, which Windows then multiplies by that factor.
25
+ */
26
+ function cardSize(screen, inches) {
27
+ const dips = (inches * screenDpi(screen)) / screenScale(screen);
28
+ return Math.round(Math.min(MAX_CARD, Math.max(MIN_CARD, dips)));
29
+ }
14
30
  /**
15
31
  * Full page for one overlay - big number, no msger template or buttons.
32
+ *
33
+ * Everything is sized in vh so the page fits whatever window the caller asked
34
+ * for: the window is what carries the physical size, and the digit is simply a
35
+ * fraction of it.
16
36
  */
17
37
  function overlayHtml(index, screen, seconds) {
18
38
  const b = screen.bounds;
@@ -26,18 +46,28 @@ function overlayHtml(index, screen, seconds) {
26
46
  return `<!doctype html>
27
47
  <html><head><meta charset="utf-8"><title>winpos screen ${index}</title>
28
48
  <style>
29
- html, body { height: 100%; margin: 0; }
49
+ html, body { height: 100%; margin: 0; background: transparent; }
30
50
  body {
31
- display: flex; flex-direction: column;
32
- align-items: center; justify-content: center;
33
- background: #1e1e1e; color: #f0f0f0;
51
+ display: flex; align-items: center; justify-content: center;
34
52
  font-family: "Segoe UI", system-ui, sans-serif;
35
53
  user-select: none;
36
54
  }
37
- .num { font-size: 160px; font-weight: 700; line-height: 1; }
38
- .label { font-size: 16px; opacity: 0.65; margin-top: 12px; }
55
+ .card {
56
+ display: flex; flex-direction: column;
57
+ align-items: center; justify-content: center;
58
+ width: 100%; height: 100%;
59
+ box-sizing: border-box;
60
+ /* Solid black, white digits - the highest-contrast reading of the number.
61
+ msger's overlay window composites onto white rather than the desktop
62
+ here, so a translucent background just came out gray; painting it
63
+ opaque is both clearer and honest about what's actually drawn. */
64
+ background: #000000;
65
+ color: #ffffff;
66
+ }
67
+ .num { font-size: 62vh; font-weight: 700; line-height: 1; }
68
+ .label { font-size: 4.5vh; opacity: 0.75; margin-top: 3vh; }
39
69
  </style></head>
40
- <body><div class="num">${index}</div><div class="label">${label}</div>
70
+ <body><div class="card"><div class="num">${index}</div><div class="label">${label}</div></div>
41
71
  <script>
42
72
  // The page dismisses itself: msger's own timeout is only checked when the
43
73
  // window sees an event, so an overlay nobody touches sits there forever.
@@ -60,8 +90,9 @@ function overlayHtml(index, screen, seconds) {
60
90
  *
61
91
  * @param screens - Screens in winpos order (the index shown is the array index)
62
92
  * @param seconds - How long each overlay stays up
93
+ * @param inches - Size of each overlay in real inches, measured on the glass
63
94
  */
64
- export async function showScreenNumbers(screens, seconds = OVERLAY_SECONDS) {
95
+ export async function showScreenNumbers(screens, seconds = OVERLAY_SECONDS, inches = OVERLAY_INCHES) {
65
96
  let showMessageBox;
66
97
  try {
67
98
  ({ showMessageBox } = await import('@bobfrankston/msger'));
@@ -79,16 +110,17 @@ export async function showScreenNumbers(screens, seconds = OVERLAY_SECONDS) {
79
110
  // Without this, the overlay on a 125% monitor whose origin is a few
80
111
  // thousand pixels out lands far right of center (5K screen, 768px off).
81
112
  const scale = screenScale(screen);
82
- const x = b.Left / scale + (b.Width - CARD - FRAME_W) / 2;
83
- const y = b.Top / scale + (b.Height - CARD - FRAME_H) / 2;
113
+ const card = cardSize(screen, inches);
114
+ const x = b.Left / scale + (b.Width - card) / 2;
115
+ const y = b.Top / scale + (b.Height - card) / 2;
84
116
  return showMessageBox({
85
117
  title: `winpos screen ${i}`,
86
118
  html: overlayHtml(i, screen, seconds),
87
119
  rawHtml: true, // No msger template, no OK button
88
- size: { width: CARD, height: CARD },
120
+ overlay: true, // Frameless, transparent, click-through, on top
121
+ size: { width: card, height: card },
89
122
  pos: { x: Math.round(x), y: Math.round(y) },
90
123
  timeout: seconds + 5, // Backstop if the page never runs
91
- alwaysOnTop: true,
92
124
  detach: true, // Outlives winpos itself
93
125
  // Own WebView2 profile per overlay: msger windows sharing one
94
126
  // user-data dir also share a browser process, and starting all of
package/screens.d.ts CHANGED
@@ -14,9 +14,28 @@ export interface ScreenInfo {
14
14
  primary: boolean;
15
15
  /** Display scaling of this monitor as a percent: 100, 125, 150... */
16
16
  scalePercent: number;
17
+ /** Physical panel size in millimetres (EDID), when the driver reports it */
18
+ physicalMm?: {
19
+ width: number;
20
+ height: number;
21
+ };
17
22
  }
18
23
  /** Display scaling of a screen as a factor (1.25 for a 125% display) */
19
24
  export declare function screenScale(screen: ScreenInfo): number;
25
+ /** What CSS assumes an inch is, and what Windows calls 100% scaling */
26
+ export declare const NOMINAL_DPI = 96;
27
+ /**
28
+ * True pixels-per-inch of a screen, measured from the panel's physical size.
29
+ *
30
+ * Not the same thing as the scale factor: scaling is a preference, so a 28"
31
+ * 4K panel left at 100% reports 96 DPI while actually being 160. Anything
32
+ * sized in pixels - or in CSS inches, which are just 96 pixels - therefore
33
+ * comes out a different physical size on each monitor.
34
+ *
35
+ * Falls back to nominal (96 x scale) when the driver won't say, which is the
36
+ * old pixel-sized behaviour.
37
+ */
38
+ export declare function screenDpi(screen: ScreenInfo): number;
20
39
  export declare function enumerateScreens(): ScreenInfo[];
21
40
  export declare function sortScreens(screens: ScreenInfo[]): ScreenInfo[];
22
41
  //# sourceMappingURL=screens.d.ts.map
package/screens.js CHANGED
@@ -6,6 +6,33 @@ import { user32, isBun, MONITORINFOF_PRIMARY, DEFAULT_SCALE_PERCENT } from './ff
6
6
  export function screenScale(screen) {
7
7
  return (screen.scalePercent || DEFAULT_SCALE_PERCENT) / 100;
8
8
  }
9
+ /** What CSS assumes an inch is, and what Windows calls 100% scaling */
10
+ export const NOMINAL_DPI = 96;
11
+ // A real monitor outside this range means the driver lied about its size
12
+ // (some report the whole video wall, projectors report nothing sensible).
13
+ const MIN_PLAUSIBLE_DPI = 30;
14
+ const MAX_PLAUSIBLE_DPI = 700;
15
+ /**
16
+ * True pixels-per-inch of a screen, measured from the panel's physical size.
17
+ *
18
+ * Not the same thing as the scale factor: scaling is a preference, so a 28"
19
+ * 4K panel left at 100% reports 96 DPI while actually being 160. Anything
20
+ * sized in pixels - or in CSS inches, which are just 96 pixels - therefore
21
+ * comes out a different physical size on each monitor.
22
+ *
23
+ * Falls back to nominal (96 x scale) when the driver won't say, which is the
24
+ * old pixel-sized behaviour.
25
+ */
26
+ export function screenDpi(screen) {
27
+ const scale = screenScale(screen);
28
+ const mm = screen.physicalMm;
29
+ if (!mm || mm.width <= 0)
30
+ return NOMINAL_DPI * scale;
31
+ // bounds are logical pixels, so undo the scaling to get what the panel
32
+ // actually lights up - that is what spans those millimetres.
33
+ const dpi = (screen.bounds.Width * scale) / (mm.width / 25.4);
34
+ return dpi >= MIN_PLAUSIBLE_DPI && dpi <= MAX_PLAUSIBLE_DPI ? dpi : NOMINAL_DPI * scale;
35
+ }
9
36
  export function enumerateScreens() {
10
37
  const screens = [];
11
38
  let monitorIndex = 0;
@@ -20,8 +47,12 @@ export function enumerateScreens() {
20
47
  const monitorInfo = {};
21
48
  if (user32.GetMonitorInfoW(hMonitor, monitorInfo)) {
22
49
  const rcMonitor = monitorInfo.rcMonitor;
50
+ // The driver's own name for this monitor - enumeration
51
+ // order is not the DISPLAYn numbering, so a name made up
52
+ // from the index would point gdi32 at the wrong panel.
53
+ const deviceName = monitorInfo.szDevice || `\\\\.\\DISPLAY${monitorIndex + 1}`;
23
54
  screens.push({
24
- deviceName: `\\\\.\\DISPLAY${monitorIndex + 1}`,
55
+ deviceName,
25
56
  bounds: {
26
57
  Left: rcMonitor.Left,
27
58
  Top: rcMonitor.Top,
@@ -32,6 +63,7 @@ export function enumerateScreens() {
32
63
  },
33
64
  primary: (monitorInfo.dwFlags & MONITORINFOF_PRIMARY) !== 0,
34
65
  scalePercent: user32.GetScaleFactorForMonitor?.(hMonitor) ?? DEFAULT_SCALE_PERCENT,
66
+ physicalMm: user32.GetPhysicalSizeMm?.(deviceName) ?? undefined,
35
67
  });
36
68
  monitorIndex++;
37
69
  }