Files
SubMiner/src/window-trackers/win32.ts

314 lines
9.8 KiB
TypeScript

import koffi from 'koffi';
const user32 = koffi.load('user32.dll');
const dwmapi = koffi.load('dwmapi.dll');
const kernel32 = koffi.load('kernel32.dll');
const RECT = koffi.struct('RECT', {
Left: 'int',
Top: 'int',
Right: 'int',
Bottom: 'int',
});
const MARGINS = koffi.struct('MARGINS', {
cxLeftWidth: 'int',
cxRightWidth: 'int',
cyTopHeight: 'int',
cyBottomHeight: 'int',
});
const WNDENUMPROC = koffi.proto('bool __stdcall WNDENUMPROC(intptr hwnd, intptr lParam)');
const EnumWindows = user32.func('bool __stdcall EnumWindows(WNDENUMPROC *cb, intptr lParam)');
const IsWindowVisible = user32.func('bool __stdcall IsWindowVisible(intptr hwnd)');
const IsIconic = user32.func('bool __stdcall IsIconic(intptr hwnd)');
const GetForegroundWindow = user32.func('intptr __stdcall GetForegroundWindow()');
const SetWindowPos = user32.func(
'bool __stdcall SetWindowPos(intptr hwnd, intptr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags)',
);
const GetWindowThreadProcessId = user32.func(
'uint __stdcall GetWindowThreadProcessId(intptr hwnd, _Out_ uint *lpdwProcessId)',
);
const GetWindowLongW = user32.func('int __stdcall GetWindowLongW(intptr hwnd, int nIndex)');
const SetWindowLongPtrW = user32.func(
'intptr __stdcall SetWindowLongPtrW(intptr hwnd, int nIndex, intptr dwNewLong)',
);
const GetWindowFn = user32.func('intptr __stdcall GetWindow(intptr hwnd, uint uCmd)');
const GetWindowRect = user32.func('bool __stdcall GetWindowRect(intptr hwnd, _Out_ RECT *lpRect)');
const DwmGetWindowAttribute = dwmapi.func(
'int __stdcall DwmGetWindowAttribute(intptr hwnd, uint dwAttribute, _Out_ RECT *pvAttribute, uint cbAttribute)',
);
const DwmExtendFrameIntoClientArea = dwmapi.func(
'int __stdcall DwmExtendFrameIntoClientArea(intptr hwnd, MARGINS *pMarInset)',
);
const OpenProcess = kernel32.func(
'intptr __stdcall OpenProcess(uint dwDesiredAccess, bool bInheritHandle, uint dwProcessId)',
);
const CloseHandle = kernel32.func('bool __stdcall CloseHandle(intptr hObject)');
const GetLastError = kernel32.func('uint __stdcall GetLastError()');
const SetLastError = kernel32.func('void __stdcall SetLastError(uint dwErrCode)');
const QueryFullProcessImageNameW = kernel32.func(
'bool __stdcall QueryFullProcessImageNameW(intptr hProcess, uint dwFlags, _Out_ uint16 *lpExeName, _Inout_ uint *lpdwSize)',
);
const GWL_EXSTYLE = -20;
const WS_EX_TOPMOST = 0x00000008;
const GWLP_HWNDPARENT = -8;
const GW_HWNDPREV = 3;
const DWMWA_EXTENDED_FRAME_BOUNDS = 9;
const PROCESS_QUERY_LIMITED_INFORMATION = 0x1000;
const SWP_NOSIZE = 0x0001;
const SWP_NOMOVE = 0x0002;
const SWP_NOACTIVATE = 0x0010;
const SWP_NOOWNERZORDER = 0x0200;
const SWP_FLAGS = SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE | SWP_NOOWNERZORDER;
const HWND_TOP = 0;
const HWND_BOTTOM = 1;
const HWND_TOPMOST = -1;
const HWND_NOTOPMOST = -2;
function extendOverlayFrameIntoClientArea(overlayHwnd: number): void {
const hr = DwmExtendFrameIntoClientArea(overlayHwnd, {
cxLeftWidth: -1,
cxRightWidth: -1,
cyTopHeight: -1,
cyBottomHeight: -1,
});
if (hr !== 0) {
throw new Error(`DwmExtendFrameIntoClientArea failed (${hr})`);
}
}
function resetLastError(): void {
SetLastError(0);
}
function assertSetWindowLongPtrSucceeded(operation: string, result: number): void {
if (result !== 0) {
return;
}
if (GetLastError() === 0) {
return;
}
throw new Error(`${operation} failed (${GetLastError()})`);
}
function assertSetWindowPosSucceeded(operation: string, result: boolean): void {
if (result) {
return;
}
throw new Error(`${operation} failed (${GetLastError()})`);
}
function assertGetWindowLongSucceeded(operation: string, result: number): number {
if (result !== 0 || GetLastError() === 0) {
return result;
}
throw new Error(`${operation} failed (${GetLastError()})`);
}
export interface WindowBounds {
x: number;
y: number;
width: number;
height: number;
}
export interface MpvWindowMatch {
hwnd: number;
bounds: WindowBounds;
area: number;
isForeground: boolean;
}
export interface MpvPollResult {
matches: MpvWindowMatch[];
focusState: boolean;
windowState: 'visible' | 'minimized' | 'not-found';
}
function getWindowBounds(hwnd: number): WindowBounds | null {
const rect = { Left: 0, Top: 0, Right: 0, Bottom: 0 };
const hr = DwmGetWindowAttribute(hwnd, DWMWA_EXTENDED_FRAME_BOUNDS, rect, koffi.sizeof(RECT));
if (hr !== 0) {
if (!GetWindowRect(hwnd, rect)) {
return null;
}
}
const width = rect.Right - rect.Left;
const height = rect.Bottom - rect.Top;
if (width <= 0 || height <= 0) return null;
return { x: rect.Left, y: rect.Top, width, height };
}
function getProcessNameByPid(pid: number): string | null {
const hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid);
if (!hProcess) return null;
try {
const buffer = new Uint16Array(260);
const size = new Uint32Array([260]);
if (!QueryFullProcessImageNameW(hProcess, 0, buffer, size)) {
return null;
}
const fullPath = String.fromCharCode(...buffer.slice(0, size[0]));
const fileName = fullPath.split('\\').pop() || '';
return fileName.replace(/\.exe$/i, '');
} finally {
CloseHandle(hProcess);
}
}
export function findMpvWindows(): MpvPollResult {
const foregroundHwnd = GetForegroundWindow();
const matches: MpvWindowMatch[] = [];
let hasMinimized = false;
let hasFocused = false;
const processNameCache = new Map<number, string | null>();
const cb = koffi.register((hwnd: number, _lParam: number) => {
if (!IsWindowVisible(hwnd)) return true;
const pid = new Uint32Array(1);
GetWindowThreadProcessId(hwnd, pid);
const pidValue = pid[0]!;
if (pidValue === 0) return true;
let processName = processNameCache.get(pidValue);
if (processName === undefined) {
processName = getProcessNameByPid(pidValue);
processNameCache.set(pidValue, processName);
}
if (!processName || processName.toLowerCase() !== 'mpv') return true;
if (IsIconic(hwnd)) {
hasMinimized = true;
return true;
}
const bounds = getWindowBounds(hwnd);
if (!bounds) return true;
const isForeground = foregroundHwnd !== 0 && hwnd === foregroundHwnd;
if (isForeground) hasFocused = true;
matches.push({
hwnd,
bounds,
area: bounds.width * bounds.height,
isForeground,
});
return true;
}, koffi.pointer(WNDENUMPROC));
try {
EnumWindows(cb, 0);
} finally {
koffi.unregister(cb);
}
return {
matches,
focusState: hasFocused,
windowState: matches.length > 0 ? 'visible' : hasMinimized ? 'minimized' : 'not-found',
};
}
export function getForegroundProcessName(): string | null {
const foregroundHwnd = GetForegroundWindow();
if (!foregroundHwnd) return null;
const pid = new Uint32Array(1);
GetWindowThreadProcessId(foregroundHwnd, pid);
const pidValue = pid[0]!;
if (pidValue === 0) return null;
return getProcessNameByPid(pidValue);
}
export function setOverlayOwner(overlayHwnd: number, mpvHwnd: number): void {
resetLastError();
const result = SetWindowLongPtrW(overlayHwnd, GWLP_HWNDPARENT, mpvHwnd);
assertSetWindowLongPtrSucceeded('setOverlayOwner', result);
extendOverlayFrameIntoClientArea(overlayHwnd);
}
export function ensureOverlayTransparency(overlayHwnd: number): void {
extendOverlayFrameIntoClientArea(overlayHwnd);
}
export function clearOverlayOwner(overlayHwnd: number): void {
resetLastError();
const result = SetWindowLongPtrW(overlayHwnd, GWLP_HWNDPARENT, 0);
assertSetWindowLongPtrSucceeded('clearOverlayOwner', result);
}
export function bindOverlayAboveMpv(overlayHwnd: number, mpvHwnd: number): void {
resetLastError();
const ownerResult = SetWindowLongPtrW(overlayHwnd, GWLP_HWNDPARENT, mpvHwnd);
assertSetWindowLongPtrSucceeded('bindOverlayAboveMpv owner assignment', ownerResult);
resetLastError();
const mpvExStyle = assertGetWindowLongSucceeded(
'bindOverlayAboveMpv target window style',
GetWindowLongW(mpvHwnd, GWL_EXSTYLE),
);
const mpvIsTopmost = (mpvExStyle & WS_EX_TOPMOST) !== 0;
resetLastError();
const overlayExStyle = assertGetWindowLongSucceeded(
'bindOverlayAboveMpv overlay window style',
GetWindowLongW(overlayHwnd, GWL_EXSTYLE),
);
const overlayIsTopmost = (overlayExStyle & WS_EX_TOPMOST) !== 0;
if (mpvIsTopmost && !overlayIsTopmost) {
resetLastError();
const topmostResult = SetWindowPos(overlayHwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_FLAGS);
assertSetWindowPosSucceeded('bindOverlayAboveMpv topmost adjustment', topmostResult);
} else if (!mpvIsTopmost && overlayIsTopmost) {
resetLastError();
const notTopmostResult = SetWindowPos(overlayHwnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_FLAGS);
assertSetWindowPosSucceeded('bindOverlayAboveMpv notopmost adjustment', notTopmostResult);
}
const windowAboveMpv = GetWindowFn(mpvHwnd, GW_HWNDPREV);
if (windowAboveMpv !== 0 && windowAboveMpv === overlayHwnd) return;
let insertAfter = HWND_TOP;
if (windowAboveMpv !== 0) {
const aboveExStyle = GetWindowLongW(windowAboveMpv, GWL_EXSTYLE);
const aboveIsTopmost = (aboveExStyle & WS_EX_TOPMOST) !== 0;
if (aboveIsTopmost === mpvIsTopmost) {
insertAfter = windowAboveMpv;
}
}
resetLastError();
const positionResult = SetWindowPos(overlayHwnd, insertAfter, 0, 0, 0, 0, SWP_FLAGS);
assertSetWindowPosSucceeded('bindOverlayAboveMpv z-order adjustment', positionResult);
}
export function lowerOverlay(overlayHwnd: number): void {
resetLastError();
const notTopmostResult = SetWindowPos(overlayHwnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_FLAGS);
assertSetWindowPosSucceeded('lowerOverlay notopmost adjustment', notTopmostResult);
resetLastError();
const bottomResult = SetWindowPos(overlayHwnd, HWND_BOTTOM, 0, 0, 0, 0, SWP_FLAGS);
assertSetWindowPosSucceeded('lowerOverlay bottom adjustment', bottomResult);
}