@bobfrankston/winpos 2.0.51 → 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 +296 -200
- package/ffi-wrapper.d.ts +56 -0
- package/ffi-wrapper.js +188 -4
- package/index.d.ts +15 -4
- package/index.js +195 -53
- package/monitors.d.ts +67 -0
- package/monitors.js +146 -0
- package/package.json +10 -3
- package/screenlayout.d.ts +108 -0
- package/screenlayout.js +243 -0
- package/screennums.d.ts +4 -1
- package/screennums.js +48 -16
- package/screens.d.ts +27 -0
- package/screens.js +54 -1
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
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export interface ScreenInfo {
|
|
5
5
|
deviceName: string;
|
|
6
|
+
/**
|
|
7
|
+
* Which physical monitor this is: EDID id + serial (e.g. GSM7721:106NTKF55597),
|
|
8
|
+
* the key monitor layouts use. Undefined when the identity could not be read.
|
|
9
|
+
* 2026-09-24 - Claude Code (Fable 5.1): added with the -screens commands.
|
|
10
|
+
*/
|
|
11
|
+
id?: string;
|
|
12
|
+
/** The monitor's friendly name from its EDID, e.g. "LG HDR 5K" */
|
|
13
|
+
name?: string;
|
|
6
14
|
bounds: {
|
|
7
15
|
Left: number;
|
|
8
16
|
Top: number;
|
|
@@ -14,9 +22,28 @@ export interface ScreenInfo {
|
|
|
14
22
|
primary: boolean;
|
|
15
23
|
/** Display scaling of this monitor as a percent: 100, 125, 150... */
|
|
16
24
|
scalePercent: number;
|
|
25
|
+
/** Physical panel size in millimetres (EDID), when the driver reports it */
|
|
26
|
+
physicalMm?: {
|
|
27
|
+
width: number;
|
|
28
|
+
height: number;
|
|
29
|
+
};
|
|
17
30
|
}
|
|
18
31
|
/** Display scaling of a screen as a factor (1.25 for a 125% display) */
|
|
19
32
|
export declare function screenScale(screen: ScreenInfo): number;
|
|
33
|
+
/** What CSS assumes an inch is, and what Windows calls 100% scaling */
|
|
34
|
+
export declare const NOMINAL_DPI = 96;
|
|
35
|
+
/**
|
|
36
|
+
* True pixels-per-inch of a screen, measured from the panel's physical size.
|
|
37
|
+
*
|
|
38
|
+
* Not the same thing as the scale factor: scaling is a preference, so a 28"
|
|
39
|
+
* 4K panel left at 100% reports 96 DPI while actually being 160. Anything
|
|
40
|
+
* sized in pixels - or in CSS inches, which are just 96 pixels - therefore
|
|
41
|
+
* comes out a different physical size on each monitor.
|
|
42
|
+
*
|
|
43
|
+
* Falls back to nominal (96 x scale) when the driver won't say, which is the
|
|
44
|
+
* old pixel-sized behaviour.
|
|
45
|
+
*/
|
|
46
|
+
export declare function screenDpi(screen: ScreenInfo): number;
|
|
20
47
|
export declare function enumerateScreens(): ScreenInfo[];
|
|
21
48
|
export declare function sortScreens(screens: ScreenInfo[]): ScreenInfo[];
|
|
22
49
|
//# sourceMappingURL=screens.d.ts.map
|
package/screens.js
CHANGED
|
@@ -2,10 +2,38 @@
|
|
|
2
2
|
* Screen management - enumerate and sort displays
|
|
3
3
|
*/
|
|
4
4
|
import { user32, isBun, MONITORINFOF_PRIMARY, DEFAULT_SCALE_PERCENT } from './ffi-wrapper.js';
|
|
5
|
+
import { enumerateMonitors } from './monitors.js';
|
|
5
6
|
/** Display scaling of a screen as a factor (1.25 for a 125% display) */
|
|
6
7
|
export function screenScale(screen) {
|
|
7
8
|
return (screen.scalePercent || DEFAULT_SCALE_PERCENT) / 100;
|
|
8
9
|
}
|
|
10
|
+
/** What CSS assumes an inch is, and what Windows calls 100% scaling */
|
|
11
|
+
export const NOMINAL_DPI = 96;
|
|
12
|
+
// A real monitor outside this range means the driver lied about its size
|
|
13
|
+
// (some report the whole video wall, projectors report nothing sensible).
|
|
14
|
+
const MIN_PLAUSIBLE_DPI = 30;
|
|
15
|
+
const MAX_PLAUSIBLE_DPI = 700;
|
|
16
|
+
/**
|
|
17
|
+
* True pixels-per-inch of a screen, measured from the panel's physical size.
|
|
18
|
+
*
|
|
19
|
+
* Not the same thing as the scale factor: scaling is a preference, so a 28"
|
|
20
|
+
* 4K panel left at 100% reports 96 DPI while actually being 160. Anything
|
|
21
|
+
* sized in pixels - or in CSS inches, which are just 96 pixels - therefore
|
|
22
|
+
* comes out a different physical size on each monitor.
|
|
23
|
+
*
|
|
24
|
+
* Falls back to nominal (96 x scale) when the driver won't say, which is the
|
|
25
|
+
* old pixel-sized behaviour.
|
|
26
|
+
*/
|
|
27
|
+
export function screenDpi(screen) {
|
|
28
|
+
const scale = screenScale(screen);
|
|
29
|
+
const mm = screen.physicalMm;
|
|
30
|
+
if (!mm || mm.width <= 0)
|
|
31
|
+
return NOMINAL_DPI * scale;
|
|
32
|
+
// bounds are logical pixels, so undo the scaling to get what the panel
|
|
33
|
+
// actually lights up - that is what spans those millimetres.
|
|
34
|
+
const dpi = (screen.bounds.Width * scale) / (mm.width / 25.4);
|
|
35
|
+
return dpi >= MIN_PLAUSIBLE_DPI && dpi <= MAX_PLAUSIBLE_DPI ? dpi : NOMINAL_DPI * scale;
|
|
36
|
+
}
|
|
9
37
|
export function enumerateScreens() {
|
|
10
38
|
const screens = [];
|
|
11
39
|
let monitorIndex = 0;
|
|
@@ -20,8 +48,12 @@ export function enumerateScreens() {
|
|
|
20
48
|
const monitorInfo = {};
|
|
21
49
|
if (user32.GetMonitorInfoW(hMonitor, monitorInfo)) {
|
|
22
50
|
const rcMonitor = monitorInfo.rcMonitor;
|
|
51
|
+
// The driver's own name for this monitor - enumeration
|
|
52
|
+
// order is not the DISPLAYn numbering, so a name made up
|
|
53
|
+
// from the index would point gdi32 at the wrong panel.
|
|
54
|
+
const deviceName = monitorInfo.szDevice || `\\\\.\\DISPLAY${monitorIndex + 1}`;
|
|
23
55
|
screens.push({
|
|
24
|
-
deviceName
|
|
56
|
+
deviceName,
|
|
25
57
|
bounds: {
|
|
26
58
|
Left: rcMonitor.Left,
|
|
27
59
|
Top: rcMonitor.Top,
|
|
@@ -32,6 +64,7 @@ export function enumerateScreens() {
|
|
|
32
64
|
},
|
|
33
65
|
primary: (monitorInfo.dwFlags & MONITORINFOF_PRIMARY) !== 0,
|
|
34
66
|
scalePercent: user32.GetScaleFactorForMonitor?.(hMonitor) ?? DEFAULT_SCALE_PERCENT,
|
|
67
|
+
physicalMm: user32.GetPhysicalSizeMm?.(deviceName) ?? undefined,
|
|
35
68
|
});
|
|
36
69
|
monitorIndex++;
|
|
37
70
|
}
|
|
@@ -77,6 +110,26 @@ export function enumerateScreens() {
|
|
|
77
110
|
scalePercent: DEFAULT_SCALE_PERCENT,
|
|
78
111
|
});
|
|
79
112
|
}
|
|
113
|
+
// 2026-09-24 - Claude Code (Fable 5.1), at Bob's direction: tag each screen
|
|
114
|
+
// with the monitor's identity, joined on the adapter name. One identity
|
|
115
|
+
// mechanism for both halves of winpos: the window layouts and the monitor
|
|
116
|
+
// layouts name the same panel the same way.
|
|
117
|
+
try {
|
|
118
|
+
const byDevice = new Map(enumerateMonitors().map(m => [m.device, m]));
|
|
119
|
+
for (const screen of screens) {
|
|
120
|
+
const monitor = byDevice.get(screen.deviceName);
|
|
121
|
+
if (!monitor)
|
|
122
|
+
continue;
|
|
123
|
+
screen.id = monitor.id;
|
|
124
|
+
screen.name = monitor.name;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
// Justified swallow: identity is decoration here - window positioning
|
|
129
|
+
// works from bounds alone and always has. The -screens commands call
|
|
130
|
+
// enumerateMonitors() themselves and report this same error there,
|
|
131
|
+
// so it is not lost, only deferred to the command that needs it.
|
|
132
|
+
}
|
|
80
133
|
return screens;
|
|
81
134
|
}
|
|
82
135
|
// Tops within this many pixels are treated as the same row (DPI/rounding noise).
|