@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 +15 -2
- package/ffi-wrapper.d.ts +7 -0
- package/ffi-wrapper.js +55 -4
- package/index.d.ts +3 -3
- package/index.js +19 -5
- package/package.json +1 -1
- package/screennums.d.ts +4 -1
- package/screennums.js +48 -16
- package/screens.d.ts +19 -0
- package/screens.js +33 -1
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
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
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
|
-
|
|
11
|
-
const
|
|
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;
|
|
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
|
-
.
|
|
38
|
-
|
|
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
|
|
83
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
}
|