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 { 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 { 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 { 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 { 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 { 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; } }