@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 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
@@ -68,6 +68,7 @@ ViewDeferd*
68
68
  W
69
69
  Widgets
70
70
  Windows*
71
+ winpos screen *
71
72
  WinUI*
72
73
  WISPTIS
73
74
  WMS*
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.49",
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
  }
@@ -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 === b.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