import type { DesktopWindow } from "@oh-my-pi/pi-natives"; import { truncate } from "@oh-my-pi/pi-utils/format"; const MAX_APPS = 12; const WINDOWS_PER_APP = 3; const WINDOWS_PER_MATCHED_APP = 10; const MAX_TITLE_CHARS = 80; /** * What `computer.apps.open` does for an app with no listed window: on macOS it also shows a window of a running app * that has none and returns it; elsewhere it only launches. A read-only run still gets the hint, since the next call * can open the app. */ export type AppOpenRoute = "shows-window" | "launches"; /** Matches a lowercased app-name substring or PID membership for a resolved application identity. */ export function matchesWindowApp(window: DesktopWindow, app: string, pids?: ReadonlySet): boolean { return window.app.toLocaleLowerCase().includes(app) || (window.pid !== undefined && pids?.has(window.pid) === true); } /** Lists the open windows by app, apps matching `app` first, for a selector that matched nothing. */ export function describeWindowMiss( windows: readonly DesktopWindow[], app: string | undefined, route: AppOpenRoute, pids?: ReadonlySet, ): string { // An empty `app` filters nothing in `matchesFilter`, so it matches no app here either. const needle = app?.toLocaleLowerCase() || undefined; const matched = (window: DesktopWindow): boolean => needle !== undefined && matchesWindowApp(window, needle, pids); const call = `computer.apps.open(${JSON.stringify(app)})`; const hint = route === "shows-window" ? `${call} opens it by that exact name or ID, or shows a window of it if it runs without one, and returns it with that window (window: null if none appears within 3 s).\n` : `${call} launches it by that exact name or ID.\n`; const note = needle !== undefined && !windows.some(matched) ? `No open window belongs to an app matching ${JSON.stringify(app)}.\n${hint}` : ""; if (windows.length === 0) return `${note}No windows are open.`; const byApp = Map.groupBy(windows, window => window.app); const focusedApp = windows.find(window => window.focused)?.app; const matchedApp = (name: string): boolean => byApp.get(name)!.some(matched); const rank = (name: string): number => (matchedApp(name) ? 0 : name === focusedApp ? 1 : 2); const apps = [...byApp.keys()].sort((left, right) => rank(left) - rank(right) || left.localeCompare(right)); const lines = apps.slice(0, MAX_APPS).map(name => { const group = byApp.get(name)!; // Titled windows first; untitled ones still list their ids, since macOS without Screen Recording blanks other apps' titles. const ordered = group.toSorted((left, right) => Number(!left.title.trim()) - Number(!right.title.trim())); const limit = matchedApp(name) ? WINDOWS_PER_MATCHED_APP : WINDOWS_PER_APP; const entries = ordered .slice(0, limit) .map(window => `${window.id} ${JSON.stringify(truncate(window.title, MAX_TITLE_CHARS))}`); if (ordered.length > limit) entries.push(`${ordered.length - limit} more`); return `- ${name}: ${entries.join(", ")}`; }); const omitted = apps.slice(MAX_APPS); if (omitted.length > 0) { const count = omitted.reduce((sum, name) => sum + byApp.get(name)!.length, 0); lines.push(`- ${omitted.length} more ${omitted.length === 1 ? "app" : "apps"} with ${count} windows`); } return `${note}Open windows by app (id "title"):\n${lines.join("\n")}`; }