@bun-win32/uia 1.0.0
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/AI.md +130 -0
- package/README.md +79 -0
- package/agent.ts +83 -0
- package/automation.ts +51 -0
- package/cache.ts +67 -0
- package/com.ts +62 -0
- package/condition.ts +132 -0
- package/constants.ts +233 -0
- package/element.ts +512 -0
- package/index.ts +40 -0
- package/input.ts +149 -0
- package/msaa.ts +99 -0
- package/package.json +86 -0
- package/patterns.ts +234 -0
- package/png.ts +75 -0
- package/reads.ts +66 -0
- package/tree.ts +95 -0
- package/window.ts +107 -0
package/window.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Window targeting (find / enumerate / by-process) and PrintWindow screenshots for visual assertions.
|
|
2
|
+
// EnumWindows invokes its callback synchronously on the calling thread (no foreign-thread hazard).
|
|
3
|
+
// PrintWindow + GDI capture a window's own rendering; the PNG can be blank on a locked session.
|
|
4
|
+
|
|
5
|
+
import { FFIType, JSCallback } from 'bun:ffi';
|
|
6
|
+
|
|
7
|
+
import Gdi32 from '@bun-win32/gdi32';
|
|
8
|
+
import User32 from '@bun-win32/user32';
|
|
9
|
+
|
|
10
|
+
import { encodePNG } from './png';
|
|
11
|
+
|
|
12
|
+
export interface WindowInfo {
|
|
13
|
+
hWnd: bigint;
|
|
14
|
+
title: string;
|
|
15
|
+
className: string;
|
|
16
|
+
processId: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function readWindowText(hWnd: bigint): string {
|
|
20
|
+
const buffer = Buffer.alloc(1024);
|
|
21
|
+
const length = User32.GetWindowTextW(hWnd, buffer.ptr!, 512);
|
|
22
|
+
return length > 0 ? buffer.subarray(0, length * 2).toString('utf16le') : '';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readClassName(hWnd: bigint): string {
|
|
26
|
+
const buffer = Buffer.alloc(512);
|
|
27
|
+
const length = User32.GetClassNameW(hWnd, buffer.ptr!, 256);
|
|
28
|
+
return length > 0 ? buffer.subarray(0, length * 2).toString('utf16le') : '';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readProcessId(hWnd: bigint): number {
|
|
32
|
+
const out = Buffer.alloc(4);
|
|
33
|
+
User32.GetWindowThreadProcessId(hWnd, out.ptr!);
|
|
34
|
+
return out.readUInt32LE(0);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Find a top-level window by exact title and/or class name. Returns 0n if none. */
|
|
38
|
+
export function findWindow(target: { className?: string; title?: string }): bigint {
|
|
39
|
+
const classBuffer = target.className === undefined ? null : Buffer.from(`${target.className}\0`, 'utf16le').ptr!;
|
|
40
|
+
const titleBuffer = target.title === undefined ? null : Buffer.from(`${target.title}\0`, 'utf16le').ptr!;
|
|
41
|
+
return User32.FindWindowW(classBuffer, titleBuffer);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Enumerate visible, titled top-level windows with their class and owning process id. */
|
|
45
|
+
export function listWindows(): WindowInfo[] {
|
|
46
|
+
const windows: WindowInfo[] = [];
|
|
47
|
+
const callback = new JSCallback(
|
|
48
|
+
(hWnd: bigint) => {
|
|
49
|
+
if (User32.IsWindowVisible(hWnd) !== 0) {
|
|
50
|
+
const title = readWindowText(hWnd);
|
|
51
|
+
if (title.length > 0) windows.push({ hWnd, title, className: readClassName(hWnd), processId: readProcessId(hWnd) });
|
|
52
|
+
}
|
|
53
|
+
return 1;
|
|
54
|
+
},
|
|
55
|
+
{ args: [FFIType.u64, FFIType.i64], returns: FFIType.i32 },
|
|
56
|
+
);
|
|
57
|
+
User32.EnumWindows(callback.ptr!, 0n);
|
|
58
|
+
callback.close();
|
|
59
|
+
return windows;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The first visible, titled top-level window owned by the process, or 0n. */
|
|
63
|
+
export function windowForProcess(processId: number): bigint {
|
|
64
|
+
for (const window of listWindows()) {
|
|
65
|
+
if (window.processId === processId) return window.hWnd;
|
|
66
|
+
}
|
|
67
|
+
return 0n;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Capture a window via PrintWindow and encode it as PNG bytes (BGRA→RGB). Empty Uint8Array on failure. */
|
|
71
|
+
export function screenshot(hWnd: bigint): Uint8Array {
|
|
72
|
+
const PW_RENDERFULLCONTENT = 0x0000_0002;
|
|
73
|
+
const rect = Buffer.alloc(16);
|
|
74
|
+
if (User32.GetWindowRect(hWnd, rect.ptr!) === 0) return new Uint8Array(0);
|
|
75
|
+
const width = rect.readInt32LE(8) - rect.readInt32LE(0);
|
|
76
|
+
const height = rect.readInt32LE(12) - rect.readInt32LE(4);
|
|
77
|
+
if (width <= 0 || height <= 0) return new Uint8Array(0);
|
|
78
|
+
|
|
79
|
+
const hdcWindow = User32.GetWindowDC(hWnd);
|
|
80
|
+
const hdcMem = Gdi32.CreateCompatibleDC(hdcWindow);
|
|
81
|
+
const hBitmap = Gdi32.CreateCompatibleBitmap(hdcWindow, width, height);
|
|
82
|
+
Gdi32.SelectObject(hdcMem, hBitmap);
|
|
83
|
+
User32.PrintWindow(hWnd, hdcMem, PW_RENDERFULLCONTENT);
|
|
84
|
+
|
|
85
|
+
const info = Buffer.alloc(40); // BITMAPINFOHEADER
|
|
86
|
+
info.writeUInt32LE(40, 0); // biSize
|
|
87
|
+
info.writeInt32LE(width, 4); // biWidth
|
|
88
|
+
info.writeInt32LE(-height, 8); // biHeight (negative → top-down rows)
|
|
89
|
+
info.writeUInt16LE(1, 12); // biPlanes
|
|
90
|
+
info.writeUInt16LE(32, 14); // biBitCount (BGRA)
|
|
91
|
+
info.writeUInt32LE(0, 16); // biCompression = BI_RGB
|
|
92
|
+
|
|
93
|
+
const bgra = Buffer.alloc(width * height * 4);
|
|
94
|
+
Gdi32.GetDIBits(hdcMem, hBitmap, 0, height, bgra.ptr!, info.ptr!, 0); // DIB_RGB_COLORS
|
|
95
|
+
|
|
96
|
+
Gdi32.DeleteObject(hBitmap);
|
|
97
|
+
Gdi32.DeleteDC(hdcMem);
|
|
98
|
+
User32.ReleaseDC(hWnd, hdcWindow);
|
|
99
|
+
|
|
100
|
+
const rgb = new Uint8Array(width * height * 3);
|
|
101
|
+
for (let source = 0, target = 0; source < bgra.length; source += 4, target += 3) {
|
|
102
|
+
rgb[target] = bgra[source + 2]!; // R
|
|
103
|
+
rgb[target + 1] = bgra[source + 1]!; // G
|
|
104
|
+
rgb[target + 2] = bgra[source]!; // B
|
|
105
|
+
}
|
|
106
|
+
return encodePNG(rgb, width, height);
|
|
107
|
+
}
|