240 lines
7.0 KiB
TypeScript
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;
|
|
}
|
|
} |