Files
gridpilot.gg/packages/infrastructure/adapters/automation/WindowFocusService.ts

240 lines
7.0 KiB
TypeScript

import { getWindows, getActiveWindow } from '@nut-tree-fork/nut-js';
import type { ScreenRegion } from '../../../domain/value-objects/ScreenRegion';
import type { WindowInfo, WindowFocusResult } from '../../../application/ports/IScreenAutomation';
import type { ILogger } from '../../../application/ports/ILogger';
import { NoOpLogAdapter } from '../logging/NoOpLogAdapter';
/**
* Service for managing browser window focus and enumeration.
* Uses nut.js window management APIs to find and focus browser windows.
*/
export class WindowFocusService {
private logger: ILogger;
private defaultTitlePattern: string;
constructor(defaultTitlePattern?: string, logger?: ILogger) {
this.logger = logger ?? new NoOpLogAdapter();
this.defaultTitlePattern = defaultTitlePattern ?? 'iRacing';
}
/**
* Find and focus a browser window matching the title pattern.
* @param titlePattern - Pattern to match in window title (default: 'iRacing')
* @returns WindowFocusResult with window info if successful
*/
async focusBrowserWindow(titlePattern?: string): Promise<WindowFocusResult> {
const pattern = titlePattern ?? this.defaultTitlePattern;
try {
this.logger.debug('Searching for browser window', { titlePattern: pattern });
const windows = await getWindows();
for (const windowObj of windows) {
try {
const title = await windowObj.getTitle();
if (title.toLowerCase().includes(pattern.toLowerCase())) {
this.logger.debug('Found matching window', { title });
await windowObj.focus();
// Get window bounds after focusing
const region = await windowObj.getRegion();
const bounds: ScreenRegion = {
x: region.left,
y: region.top,
width: region.width,
height: region.height,
};
const windowInfo: WindowInfo = {
title,
bounds,
handle: 0, // Window objects don't expose raw handles
isActive: true,
};
this.logger.info('Browser window focused', { title, bounds });
return {
success: true,
window: windowInfo,
};
}
} catch (windowError) {
// Skip windows that can't be accessed
this.logger.debug('Could not access window', { error: String(windowError) });
continue;
}
}
this.logger.warn('No matching browser window found', { titlePattern: pattern });
return {
success: false,
error: `No window found matching pattern: ${pattern}`,
};
} catch (error) {
const errorMsg = `Window focus failed: ${error}`;
this.logger.error('Window focus failed', error instanceof Error ? error : new Error(errorMsg));
return {
success: false,
error: errorMsg,
};
}
}
/**
* Get the currently active window.
* @returns WindowInfo for the active window, or null if unable to determine
*/
async getActiveWindow(): Promise<WindowInfo | null> {
try {
const activeWindow = await getActiveWindow();
const title = await activeWindow.getTitle();
const region = await activeWindow.getRegion();
const bounds: ScreenRegion = {
x: region.left,
y: region.top,
width: region.width,
height: region.height,
};
return {
title,
bounds,
handle: 0, // Active window doesn't expose handle directly
isActive: true,
};
} catch (error) {
this.logger.error('Failed to get active window', error instanceof Error ? error : new Error(String(error)));
return null;
}
}
/**
* List all visible windows.
* @returns Array of WindowInfo for all accessible windows
*/
async listWindows(): Promise<WindowInfo[]> {
const result: WindowInfo[] = [];
try {
const windows = await getWindows();
const activeWindow = await getActiveWindow();
const activeTitle = await activeWindow.getTitle();
for (const windowObj of windows) {
try {
const title = await windowObj.getTitle();
const region = await windowObj.getRegion();
const bounds: ScreenRegion = {
x: region.left,
y: region.top,
width: region.width,
height: region.height,
};
result.push({
title,
bounds,
handle: 0, // Window objects don't expose raw handles
isActive: title === activeTitle,
});
} catch {
// Skip inaccessible windows
continue;
}
}
} catch (error) {
this.logger.error('Failed to list windows', error instanceof Error ? error : new Error(String(error)));
}
return result;
}
/**
* Check if a window matching the pattern is currently visible.
* @param titlePattern - Pattern to match in window title
* @returns true if a matching window exists
*/
async isWindowVisible(titlePattern?: string): Promise<boolean> {
const pattern = titlePattern ?? this.defaultTitlePattern;
try {
const windows = await getWindows();
for (const windowObj of windows) {
try {
const title = await windowObj.getTitle();
if (title.toLowerCase().includes(pattern.toLowerCase())) {
return true;
}
} catch {
continue;
}
}
return false;
} catch {
return false;
}
}
/**
* Get the bounds of a window matching the pattern.
* Useful for targeted screen capture.
* @param titlePattern - Pattern to match in window title
* @returns ScreenRegion of the window, or null if not found
*/
async getWindowBounds(titlePattern?: string): Promise<ScreenRegion | null> {
const pattern = titlePattern ?? this.defaultTitlePattern;
try {
const windows = await getWindows();
for (const windowObj of windows) {
try {
const title = await windowObj.getTitle();
if (title.toLowerCase().includes(pattern.toLowerCase())) {
const region = await windowObj.getRegion();
return {
x: region.left,
y: region.top,
width: region.width,
height: region.height,
};
}
} catch {
continue;
}
}
return null;
} catch (error) {
this.logger.error('Failed to get window bounds', error instanceof Error ? error : new Error(String(error)));
return null;
}
}
/**
* Set the default title pattern for window searches.
*/
setDefaultTitlePattern(pattern: string): void {
this.defaultTitlePattern = pattern;
}
/**
* Get the current default title pattern.
*/
getDefaultTitlePattern(): string {
return this.defaultTitlePattern;
}
}