@bobfrankston/winpos 2.0.49 → 2.0.51
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 +19 -0
- package/ffi-wrapper.d.ts +3 -0
- package/ffi-wrapper.js +22 -0
- package/ignores.txt +1 -0
- package/index.d.ts +2 -1
- package/index.js +26 -2
- package/package.json +15 -2
- package/screennums.d.ts +21 -0
- package/screennums.js +102 -0
- package/screens.d.ts +4 -0
- package/screens.js +12 -3
- package/shots/toosmall.png +0 -0
package/README.md
CHANGED
|
@@ -96,6 +96,25 @@ Screens are numbered starting from 0, sorted by position:
|
|
|
96
96
|
- `*` - List all windows
|
|
97
97
|
- `**` - List all windows including ignored
|
|
98
98
|
|
|
99
|
+
## Identifying Screens
|
|
100
|
+
|
|
101
|
+
`winpos` with no arguments, and `winpos *` (or `**`), flash each screen's number
|
|
102
|
+
in the middle of that screen for 5 seconds - the same idea as the Identify
|
|
103
|
+
button in Windows display settings, so you can see which physical monitor is
|
|
104
|
+
screen 0, 1, 2 ... before positioning anything there. With no arguments the
|
|
105
|
+
numbers come up alongside the usage text and its screen table.
|
|
106
|
+
|
|
107
|
+
The numbers are drawn with [msger](https://www.npmjs.com/package/@bobfrankston/msger)
|
|
108
|
+
and all appear at once. They close themselves; winpos returns immediately
|
|
109
|
+
without waiting for them. If msger is unavailable the listing still works, just
|
|
110
|
+
without the numbers.
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
import { showScreenNumbers, enumerateScreens, sortScreens } from '@bobfrankston/winpos';
|
|
114
|
+
|
|
115
|
+
await showScreenNumbers(sortScreens(enumerateScreens()), 5);
|
|
116
|
+
```
|
|
117
|
+
|
|
99
118
|
## Parameters
|
|
100
119
|
|
|
101
120
|
### Positional (legacy)
|
package/ffi-wrapper.d.ts
CHANGED
|
@@ -27,6 +27,7 @@ export interface MONITORINFO {
|
|
|
27
27
|
dwFlags: number;
|
|
28
28
|
}
|
|
29
29
|
export declare const MONITORINFOF_PRIMARY = 1;
|
|
30
|
+
export declare const DEFAULT_SCALE_PERCENT = 100;
|
|
30
31
|
export interface WindowsAPI {
|
|
31
32
|
EnumWindows: (callback: (hwnd: bigint, lParam: bigint) => boolean, lParam: bigint) => boolean;
|
|
32
33
|
GetWindowTextW: (hwnd: bigint, text: any, maxCount: number) => number;
|
|
@@ -39,6 +40,8 @@ export interface WindowsAPI {
|
|
|
39
40
|
GetWindowPlacement: (hwnd: bigint, placement: any) => boolean;
|
|
40
41
|
EnumDisplayMonitors: (callback: (hMonitor: bigint, hdcMonitor: bigint, lprcMonitor: any, dwData: bigint) => boolean, dwData: bigint) => boolean;
|
|
41
42
|
GetMonitorInfoW: (hMonitor: bigint, lpmi: any) => boolean;
|
|
43
|
+
/** Monitor scale factor as a percent (100, 125, 150...). 100 when unavailable. */
|
|
44
|
+
GetScaleFactorForMonitor?: (hMonitor: bigint) => number;
|
|
42
45
|
}
|
|
43
46
|
declare const user32: WindowsAPI;
|
|
44
47
|
export { user32, isBun };
|
package/ffi-wrapper.js
CHANGED
|
@@ -17,6 +17,8 @@ export const SW_SHOWMINNOACTIVE = 7;
|
|
|
17
17
|
export const SW_SHOWNA = 8;
|
|
18
18
|
export const SW_RESTORE = 9;
|
|
19
19
|
export const MONITORINFOF_PRIMARY = 1;
|
|
20
|
+
// Monitor scale factor as a percentage (100 = no scaling, 125 = 125%)
|
|
21
|
+
export const DEFAULT_SCALE_PERCENT = 100;
|
|
20
22
|
async function initializeFFI() {
|
|
21
23
|
if (isBun) {
|
|
22
24
|
// @ts-ignore - bun:ffi is only available in Bun
|
|
@@ -95,6 +97,19 @@ async function initializeFFI() {
|
|
|
95
97
|
const MonitorEnumProc = koffi.proto('bool __stdcall MonitorEnumProc(void *hMonitor, void *hdcMonitor, RECT *lprcMonitor, void *dwData)');
|
|
96
98
|
const EnumDisplayMonitors = lib.func('bool __stdcall EnumDisplayMonitors(void *hdc, RECT *lprcClip, MonitorEnumProc *lpfnEnum, void *dwData)');
|
|
97
99
|
const GetMonitorInfoW = lib.func('bool __stdcall GetMonitorInfoW(void *hMonitor, void *lpmi)');
|
|
100
|
+
// Per-monitor scaling lives in shcore.dll (Windows 8.1+). Optional: a
|
|
101
|
+
// missing shcore just means every monitor reports 100%.
|
|
102
|
+
// GetScaleFactorForMonitor, not GetDpiForMonitor: the DPI call reports the
|
|
103
|
+
// DPI *as the calling process sees it*, and winpos is DPI-unaware, so it
|
|
104
|
+
// always answers 96. This one reports the monitor's real scale factor.
|
|
105
|
+
let GetScaleFactorForMonitorRaw = null;
|
|
106
|
+
try {
|
|
107
|
+
const shcore = koffi.load('shcore.dll');
|
|
108
|
+
GetScaleFactorForMonitorRaw = shcore.func('int __stdcall GetScaleFactorForMonitor(void *hMonitor, void *pScale)');
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Leave null - callers fall back to 100%
|
|
112
|
+
}
|
|
98
113
|
// Keep references to registered callbacks to prevent garbage collection
|
|
99
114
|
// These must persist for the entire duration of the native callback invocation
|
|
100
115
|
const __callbackRefs = [];
|
|
@@ -166,6 +181,13 @@ async function initializeFFI() {
|
|
|
166
181
|
}
|
|
167
182
|
return result;
|
|
168
183
|
},
|
|
184
|
+
GetScaleFactorForMonitor: (hMonitor) => {
|
|
185
|
+
if (!GetScaleFactorForMonitorRaw)
|
|
186
|
+
return DEFAULT_SCALE_PERCENT;
|
|
187
|
+
const scale = Buffer.alloc(4);
|
|
188
|
+
const hr = GetScaleFactorForMonitorRaw(hMonitor, scale);
|
|
189
|
+
return hr === 0 ? (scale.readUInt32LE(0) || DEFAULT_SCALE_PERCENT) : DEFAULT_SCALE_PERCENT;
|
|
190
|
+
},
|
|
169
191
|
};
|
|
170
192
|
}
|
|
171
193
|
const user32 = await initializeFFI();
|
package/ignores.txt
CHANGED
package/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { user32, RECT } from './ffi-wrapper.js';
|
|
|
7
7
|
import { enumerateScreens, sortScreens, 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
11
|
export interface WindowConfig {
|
|
11
12
|
name: string;
|
|
12
13
|
regex?: boolean;
|
|
@@ -23,7 +24,7 @@ export interface WindowConfig {
|
|
|
23
24
|
maximize?: boolean;
|
|
24
25
|
}
|
|
25
26
|
export type ConfigFile = WindowConfig | WindowConfig[];
|
|
26
|
-
export { RECT, ScreenInfo, WindowInfo, WindowState, TabInfo, enumerateScreens, sortScreens, enumerateWindows, findWindowsByPattern, getWindowState, setWindowState, isMinimized, isMaximized, isNormal, minimizeWindow, maximizeWindow, restoreWindow, enumerateTabs, mightHaveTabs, TAB_ENUMERATION_NOTE, user32 };
|
|
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
28
|
/**
|
|
28
29
|
* Convert screen-relative coordinates to global coordinates
|
|
29
30
|
* @param x - X coordinate relative to screen
|
package/index.js
CHANGED
|
@@ -7,12 +7,13 @@ import { user32 } from './ffi-wrapper.js';
|
|
|
7
7
|
import { enumerateScreens, sortScreens } 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
11
|
import { join, resolve, isAbsolute } from 'path';
|
|
11
12
|
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
12
13
|
import { parse as parseJsonc, printParseErrorCode } from 'jsonc-parser';
|
|
13
14
|
import * as packageJson from './package.json' with { type: 'json' };
|
|
14
15
|
// Re-export public API for library usage
|
|
15
|
-
export { WindowState, enumerateScreens, sortScreens, enumerateWindows, findWindowsByPattern, getWindowState, setWindowState, isMinimized, isMaximized, isNormal, minimizeWindow, maximizeWindow, restoreWindow, enumerateTabs, mightHaveTabs, TAB_ENUMERATION_NOTE, user32 };
|
|
16
|
+
export { WindowState, enumerateScreens, sortScreens, enumerateWindows, findWindowsByPattern, getWindowState, setWindowState, isMinimized, isMaximized, isNormal, minimizeWindow, maximizeWindow, restoreWindow, enumerateTabs, mightHaveTabs, TAB_ENUMERATION_NOTE, showScreenNumbers, user32 };
|
|
16
17
|
// WindowConfig and ConfigFile types are exported from interface definitions above
|
|
17
18
|
// Note: screenPosToPos is exported separately below (after the function definition)
|
|
18
19
|
/**
|
|
@@ -447,8 +448,19 @@ function listAllWindows(includeIgnored = false) {
|
|
|
447
448
|
}
|
|
448
449
|
console.log();
|
|
449
450
|
console.log('Command format: tswinpos <title> <x> <y> <screen> [<width> <height>]');
|
|
451
|
+
// Flash each screen's number on that screen - launched last so the overlays
|
|
452
|
+
// can't turn up in the listing above. Detached, so we don't wait for them.
|
|
453
|
+
void showScreenNumbers(screens);
|
|
450
454
|
}
|
|
451
455
|
function usage(msg = '') {
|
|
456
|
+
printUsage(msg);
|
|
457
|
+
process.exit(1);
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* The usage text on its own - no exit, so the no-argument case can print it
|
|
461
|
+
* while the screen-number overlays are still being launched.
|
|
462
|
+
*/
|
|
463
|
+
function printUsage(msg = '') {
|
|
452
464
|
if (msg)
|
|
453
465
|
console.log(msg);
|
|
454
466
|
console.log('Usage: winpos [options] <title> <x> <y> <screen> [<width> <height>]');
|
|
@@ -474,6 +486,8 @@ function usage(msg = '') {
|
|
|
474
486
|
console.log('Position/size values can be pixels or percentages (e.g. 50%)');
|
|
475
487
|
console.log('Title can be regex (/pattern/) or prefix (title*)');
|
|
476
488
|
console.log('* = list windows, ** = list all including ignored');
|
|
489
|
+
console.log(` (both, like winpos with no arguments, also flash each screen's`);
|
|
490
|
+
console.log(` number on that screen for ${OVERLAY_SECONDS} seconds)`);
|
|
477
491
|
console.log();
|
|
478
492
|
console.log('JSON format: {"name":"app","regex":false,"pos":{"x":0,"y":0,"screen":0},"size":{"w":800,"h":600}}');
|
|
479
493
|
console.log(' or array: [{...}, {...}]');
|
|
@@ -484,7 +498,6 @@ function usage(msg = '') {
|
|
|
484
498
|
const screen = screens[i];
|
|
485
499
|
console.log(`${i.toString().padStart(6)} | ${screen.bounds.Left.toString().padStart(5)} ${screen.bounds.Top.toString().padStart(5)} | ${screen.bounds.Width.toString().padStart(5)} ${screen.bounds.Height.toString().padStart(5)}`);
|
|
486
500
|
}
|
|
487
|
-
process.exit(1);
|
|
488
501
|
}
|
|
489
502
|
export function run(args) {
|
|
490
503
|
// Initialize screens
|
|
@@ -596,6 +609,17 @@ export function run(args) {
|
|
|
596
609
|
showWindowInfo(posArgs[0]);
|
|
597
610
|
return;
|
|
598
611
|
}
|
|
612
|
+
// Nothing to act on (bare winpos, or -help): flash each screen's number on
|
|
613
|
+
// that screen while the usage text - screen table and all - prints, so the
|
|
614
|
+
// table has something to point at. The overlays time out and close
|
|
615
|
+
// themselves; we only wait for the launch, because process.exit here would
|
|
616
|
+
// kill the spawn in mid-flight and no numbers would ever appear.
|
|
617
|
+
if (posArgs.length === 0) {
|
|
618
|
+
const overlays = showScreenNumbers(screens);
|
|
619
|
+
printUsage();
|
|
620
|
+
void overlays.finally(() => process.exit(1));
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
599
623
|
if (posArgs.length < 4) {
|
|
600
624
|
usage();
|
|
601
625
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/winpos",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.51",
|
|
4
4
|
"description": "TypeScript implementation of winpos - Windows window positioning utility",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"@types/node": "^25.3.0"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
+
"@bobfrankston/msger": "^0.1.424",
|
|
35
36
|
"jsonc-parser": "^3.3.1",
|
|
36
37
|
"koffi": "^2.9.2"
|
|
37
38
|
},
|
|
@@ -45,5 +46,17 @@
|
|
|
45
46
|
"bugs": {
|
|
46
47
|
"url": "https://github.com/BobFrankston/winpos/issues"
|
|
47
48
|
},
|
|
48
|
-
"homepage": "https://github.com/BobFrankston/winpos#readme"
|
|
49
|
+
"homepage": "https://github.com/BobFrankston/winpos#readme",
|
|
50
|
+
".dependencies": {
|
|
51
|
+
"@bobfrankston/msger": "file:../msgx/msger",
|
|
52
|
+
"jsonc-parser": "^3.3.1",
|
|
53
|
+
"koffi": "^2.9.2"
|
|
54
|
+
},
|
|
55
|
+
".transformedSnapshot": {
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"@bobfrankston/msger": "^0.1.424",
|
|
58
|
+
"jsonc-parser": "^3.3.1",
|
|
59
|
+
"koffi": "^2.9.2"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
49
62
|
}
|
package/screennums.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Screen number overlay - briefly flashes each screen's winpos index in the
|
|
3
|
+
* middle of that screen, so you can see which physical display is screen 0,
|
|
4
|
+
* screen 1, ... without counting corners.
|
|
5
|
+
*
|
|
6
|
+
* Rendered with msger (@bobfrankston/msger). The overlays are detached and
|
|
7
|
+
* self-closing, so winpos exits immediately and leaves them up.
|
|
8
|
+
*/
|
|
9
|
+
import { type ScreenInfo } from './screens.js';
|
|
10
|
+
export declare const OVERLAY_SECONDS = 5;
|
|
11
|
+
/**
|
|
12
|
+
* Show the index of every screen in the middle of that screen.
|
|
13
|
+
*
|
|
14
|
+
* Never throws: the overlay is a convenience, so a missing or unhappy msger
|
|
15
|
+
* must not break the window listing it accompanies.
|
|
16
|
+
*
|
|
17
|
+
* @param screens - Screens in winpos order (the index shown is the array index)
|
|
18
|
+
* @param seconds - How long each overlay stays up
|
|
19
|
+
*/
|
|
20
|
+
export declare function showScreenNumbers(screens: ScreenInfo[], seconds?: number): Promise<void>;
|
|
21
|
+
//# sourceMappingURL=screennums.d.ts.map
|
package/screennums.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Screen number overlay - briefly flashes each screen's winpos index in the
|
|
3
|
+
* middle of that screen, so you can see which physical display is screen 0,
|
|
4
|
+
* screen 1, ... without counting corners.
|
|
5
|
+
*
|
|
6
|
+
* Rendered with msger (@bobfrankston/msger). The overlays are detached and
|
|
7
|
+
* self-closing, so winpos exits immediately and leaves them up.
|
|
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
|
|
13
|
+
export const OVERLAY_SECONDS = 5;
|
|
14
|
+
/**
|
|
15
|
+
* Full page for one overlay - big number, no msger template or buttons.
|
|
16
|
+
*/
|
|
17
|
+
function overlayHtml(index, screen, seconds) {
|
|
18
|
+
const b = screen.bounds;
|
|
19
|
+
// Size in winpos's own coordinates - the numbers you'd type at it - plus
|
|
20
|
+
// the scaling, since that's why they don't match the monitor's spec sheet.
|
|
21
|
+
const label = [
|
|
22
|
+
`${b.Width} x ${b.Height}`,
|
|
23
|
+
screen.scalePercent !== 100 ? `${screen.scalePercent}%` : '',
|
|
24
|
+
screen.primary ? 'primary' : ''
|
|
25
|
+
].filter(Boolean).join(' - ');
|
|
26
|
+
return `<!doctype html>
|
|
27
|
+
<html><head><meta charset="utf-8"><title>winpos screen ${index}</title>
|
|
28
|
+
<style>
|
|
29
|
+
html, body { height: 100%; margin: 0; }
|
|
30
|
+
body {
|
|
31
|
+
display: flex; flex-direction: column;
|
|
32
|
+
align-items: center; justify-content: center;
|
|
33
|
+
background: #1e1e1e; color: #f0f0f0;
|
|
34
|
+
font-family: "Segoe UI", system-ui, sans-serif;
|
|
35
|
+
user-select: none;
|
|
36
|
+
}
|
|
37
|
+
.num { font-size: 160px; font-weight: 700; line-height: 1; }
|
|
38
|
+
.label { font-size: 16px; opacity: 0.65; margin-top: 12px; }
|
|
39
|
+
</style></head>
|
|
40
|
+
<body><div class="num">${index}</div><div class="label">${label}</div>
|
|
41
|
+
<script>
|
|
42
|
+
// The page dismisses itself: msger's own timeout is only checked when the
|
|
43
|
+
// window sees an event, so an overlay nobody touches sits there forever.
|
|
44
|
+
// Posting a result over the IPC bridge is what the msger template does for
|
|
45
|
+
// a button click - it wakes the host, which then exits.
|
|
46
|
+
setTimeout(() => {
|
|
47
|
+
if (window.ipc && window.ipc.postMessage)
|
|
48
|
+
window.ipc.postMessage(JSON.stringify({ button: 'timeout', timeout: true }));
|
|
49
|
+
else
|
|
50
|
+
window.close();
|
|
51
|
+
}, ${Math.round(seconds * 1000)});
|
|
52
|
+
</script>
|
|
53
|
+
</body></html>`;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Show the index of every screen in the middle of that screen.
|
|
57
|
+
*
|
|
58
|
+
* Never throws: the overlay is a convenience, so a missing or unhappy msger
|
|
59
|
+
* must not break the window listing it accompanies.
|
|
60
|
+
*
|
|
61
|
+
* @param screens - Screens in winpos order (the index shown is the array index)
|
|
62
|
+
* @param seconds - How long each overlay stays up
|
|
63
|
+
*/
|
|
64
|
+
export async function showScreenNumbers(screens, seconds = OVERLAY_SECONDS) {
|
|
65
|
+
let showMessageBox;
|
|
66
|
+
try {
|
|
67
|
+
({ showMessageBox } = await import('@bobfrankston/msger'));
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return; // msger not installed - skip the overlay silently
|
|
71
|
+
}
|
|
72
|
+
// All of them are launched at once so the numbers appear together.
|
|
73
|
+
await Promise.all(screens.map((screen, i) => {
|
|
74
|
+
const b = screen.bounds;
|
|
75
|
+
// msger takes its position in device-independent pixels, which it then
|
|
76
|
+
// scales by the target monitor's DPI. winpos's own coordinates are
|
|
77
|
+
// already DIPs *within* a monitor, but each monitor's origin is in
|
|
78
|
+
// physical pixels - so only the origin needs dividing by the scale.
|
|
79
|
+
// Without this, the overlay on a 125% monitor whose origin is a few
|
|
80
|
+
// thousand pixels out lands far right of center (5K screen, 768px off).
|
|
81
|
+
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;
|
|
84
|
+
return showMessageBox({
|
|
85
|
+
title: `winpos screen ${i}`,
|
|
86
|
+
html: overlayHtml(i, screen, seconds),
|
|
87
|
+
rawHtml: true, // No msger template, no OK button
|
|
88
|
+
size: { width: CARD, height: CARD },
|
|
89
|
+
pos: { x: Math.round(x), y: Math.round(y) },
|
|
90
|
+
timeout: seconds + 5, // Backstop if the page never runs
|
|
91
|
+
alwaysOnTop: true,
|
|
92
|
+
detach: true, // Outlives winpos itself
|
|
93
|
+
// Own WebView2 profile per overlay: msger windows sharing one
|
|
94
|
+
// user-data dir also share a browser process, and starting all of
|
|
95
|
+
// them at once made that shared process fall over - overlays came
|
|
96
|
+
// up one at a time and some died early. Separate profiles start in
|
|
97
|
+
// parallel and are independent.
|
|
98
|
+
profile: `winpos-screen-${i}`
|
|
99
|
+
}).catch(() => { });
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=screennums.js.map
|
package/screens.d.ts
CHANGED
|
@@ -12,7 +12,11 @@ export interface ScreenInfo {
|
|
|
12
12
|
Height: number;
|
|
13
13
|
};
|
|
14
14
|
primary: boolean;
|
|
15
|
+
/** Display scaling of this monitor as a percent: 100, 125, 150... */
|
|
16
|
+
scalePercent: number;
|
|
15
17
|
}
|
|
18
|
+
/** Display scaling of a screen as a factor (1.25 for a 125% display) */
|
|
19
|
+
export declare function screenScale(screen: ScreenInfo): number;
|
|
16
20
|
export declare function enumerateScreens(): ScreenInfo[];
|
|
17
21
|
export declare function sortScreens(screens: ScreenInfo[]): ScreenInfo[];
|
|
18
22
|
//# sourceMappingURL=screens.d.ts.map
|
package/screens.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Screen management - enumerate and sort displays
|
|
3
3
|
*/
|
|
4
|
-
import { user32, isBun, MONITORINFOF_PRIMARY } from './ffi-wrapper.js';
|
|
4
|
+
import { user32, isBun, MONITORINFOF_PRIMARY, DEFAULT_SCALE_PERCENT } from './ffi-wrapper.js';
|
|
5
|
+
/** Display scaling of a screen as a factor (1.25 for a 125% display) */
|
|
6
|
+
export function screenScale(screen) {
|
|
7
|
+
return (screen.scalePercent || DEFAULT_SCALE_PERCENT) / 100;
|
|
8
|
+
}
|
|
5
9
|
export function enumerateScreens() {
|
|
6
10
|
const screens = [];
|
|
7
11
|
let monitorIndex = 0;
|
|
@@ -27,6 +31,7 @@ export function enumerateScreens() {
|
|
|
27
31
|
Height: rcMonitor.Bottom - rcMonitor.Top,
|
|
28
32
|
},
|
|
29
33
|
primary: (monitorInfo.dwFlags & MONITORINFOF_PRIMARY) !== 0,
|
|
34
|
+
scalePercent: user32.GetScaleFactorForMonitor?.(hMonitor) ?? DEFAULT_SCALE_PERCENT,
|
|
30
35
|
});
|
|
31
36
|
monitorIndex++;
|
|
32
37
|
}
|
|
@@ -53,6 +58,7 @@ export function enumerateScreens() {
|
|
|
53
58
|
Height: 1080,
|
|
54
59
|
},
|
|
55
60
|
primary: true,
|
|
61
|
+
scalePercent: DEFAULT_SCALE_PERCENT,
|
|
56
62
|
});
|
|
57
63
|
}
|
|
58
64
|
// If no screens were found, add a default one
|
|
@@ -68,14 +74,17 @@ export function enumerateScreens() {
|
|
|
68
74
|
Height: 1080,
|
|
69
75
|
},
|
|
70
76
|
primary: true,
|
|
77
|
+
scalePercent: DEFAULT_SCALE_PERCENT,
|
|
71
78
|
});
|
|
72
79
|
}
|
|
73
80
|
return screens;
|
|
74
81
|
}
|
|
82
|
+
// Tops within this many pixels are treated as the same row (DPI/rounding noise).
|
|
83
|
+
const ROW_TOLERANCE = 100;
|
|
75
84
|
export function sortScreens(screens) {
|
|
76
|
-
// Sort by top (descending), then by left (ascending)
|
|
85
|
+
// Sort by top (descending), then by left (ascending), with tolerance for rounding
|
|
77
86
|
return screens.sort((a, b) => {
|
|
78
|
-
if (a.bounds.Top
|
|
87
|
+
if (Math.abs(a.bounds.Top - b.bounds.Top) <= ROW_TOLERANCE)
|
|
79
88
|
return a.bounds.Left - b.bounds.Left;
|
|
80
89
|
return b.bounds.Top - a.bounds.Top;
|
|
81
90
|
});
|
|
Binary file
|