co-mono/packages/pi-teams/src/adapters/iterm2-adapter.ts
2026-03-05 17:36:25 -08:00

298 lines
7.4 KiB
TypeScript

/**
* iTerm2 Terminal Adapter
*
* Implements the TerminalAdapter interface for iTerm2 terminal emulator.
* Uses AppleScript for all operations.
*/
import { spawnSync } from "node:child_process";
import { execCommand, type SpawnOptions, type TerminalAdapter } from "../utils/terminal-adapter";
/**
* Context needed for iTerm2 spawning (tracks last pane for layout)
*/
export interface Iterm2SpawnContext {
/** ID of the last spawned session, used for layout decisions */
lastSessionId?: string;
}
export class Iterm2Adapter implements TerminalAdapter {
readonly name = "iTerm2";
private spawnContext: Iterm2SpawnContext = {};
detect(): boolean {
return process.env.TERM_PROGRAM === "iTerm.app" && !process.env.TMUX && !process.env.ZELLIJ;
}
/**
* Helper to execute AppleScript via stdin to avoid escaping issues with -e
*/
private runAppleScript(script: string): { stdout: string; stderr: string; status: number | null } {
const result = spawnSync("osascript", ["-"], {
input: script,
encoding: "utf-8",
});
return {
stdout: result.stdout?.toString() ?? "",
stderr: result.stderr?.toString() ?? "",
status: result.status,
};
}
spawn(options: SpawnOptions): string {
const envStr = Object.entries(options.env)
.filter(([k]) => k.startsWith("PI_"))
.map(([k, v]) => `${k}=${v}`)
.join(" ");
const itermCmd = `cd '${options.cwd}' && ${envStr} ${options.command}`;
const escapedCmd = itermCmd.replace(/"/g, '\\"');
let script: string;
if (!this.spawnContext.lastSessionId) {
script = `tell application "iTerm2"
tell current session of current window
set newSession to split vertically with default profile
tell newSession
write text "${escapedCmd}"
return id
end tell
end tell
end tell`;
} else {
script = `tell application "iTerm2"
repeat with aWindow in windows
repeat with aTab in tabs of aWindow
repeat with aSession in sessions of aTab
if id of aSession is "${this.spawnContext.lastSessionId}" then
tell aSession
set newSession to split horizontally with default profile
tell newSession
write text "${escapedCmd}"
return id
end tell
end tell
end if
end repeat
end repeat
end repeat
end tell`;
}
const result = this.runAppleScript(script);
if (result.status !== 0) {
throw new Error(`osascript failed with status ${result.status}: ${result.stderr}`);
}
const sessionId = result.stdout.toString().trim();
this.spawnContext.lastSessionId = sessionId;
return `iterm_${sessionId}`;
}
kill(paneId: string): void {
if (!paneId || !paneId.startsWith("iterm_") || paneId.startsWith("iterm_win_")) {
return;
}
const itermId = paneId.replace("iterm_", "");
const script = `tell application "iTerm2"
repeat with aWindow in windows
repeat with aTab in tabs of aWindow
repeat with aSession in sessions of aTab
if id of aSession is "${itermId}" then
close aSession
return "Closed"
end if
end repeat
end repeat
end repeat
end tell`;
try {
this.runAppleScript(script);
} catch {
// Ignore errors
}
}
isAlive(paneId: string): boolean {
if (!paneId || !paneId.startsWith("iterm_") || paneId.startsWith("iterm_win_")) {
return false;
}
const itermId = paneId.replace("iterm_", "");
const script = `tell application "iTerm2"
repeat with aWindow in windows
repeat with aTab in tabs of aWindow
repeat with aSession in sessions of aTab
if id of aSession is "${itermId}" then
return "Alive"
end if
end repeat
end repeat
end repeat
end tell`;
try {
const result = this.runAppleScript(script);
return result.stdout.includes("Alive");
} catch {
return false;
}
}
setTitle(title: string): void {
const escapedTitle = title.replace(/"/g, '\\"');
const script = `tell application "iTerm2" to tell current session of current window
set name to "${escapedTitle}"
end tell`;
try {
this.runAppleScript(script);
} catch {
// Ignore errors
}
}
/**
* iTerm2 supports spawning separate OS windows via AppleScript
*/
supportsWindows(): boolean {
return true;
}
/**
* Spawn a new separate OS window with the given options.
*/
spawnWindow(options: SpawnOptions): string {
const envStr = Object.entries(options.env)
.filter(([k]) => k.startsWith("PI_"))
.map(([k, v]) => `${k}=${v}`)
.join(" ");
const itermCmd = `cd '${options.cwd}' && ${envStr} ${options.command}`;
const escapedCmd = itermCmd.replace(/"/g, '\\"');
const windowTitle = options.teamName ? `${options.teamName}: ${options.name}` : options.name;
const escapedTitle = windowTitle.replace(/"/g, '\\"');
const script = `tell application "iTerm2"
set newWindow to (create window with default profile)
tell current session of newWindow
-- Set the session name (tab title)
set name to "${escapedTitle}"
-- Set window title via escape sequence (OSC 2)
-- We use double backslashes for AppleScript to emit a single backslash to the shell
write text "printf '\\\\033]2;${escapedTitle}\\\\007'"
-- Execute the command
write text "cd '${options.cwd}' && ${escapedCmd}"
return id of newWindow
end tell
end tell`;
const result = this.runAppleScript(script);
if (result.status !== 0) {
throw new Error(`osascript failed with status ${result.status}: ${result.stderr}`);
}
const windowId = result.stdout.toString().trim();
return `iterm_win_${windowId}`;
}
/**
* Set the title of a specific window.
*/
setWindowTitle(windowId: string, title: string): void {
if (!windowId || !windowId.startsWith("iterm_win_")) {
return;
}
const itermId = windowId.replace("iterm_win_", "");
const escapedTitle = title.replace(/"/g, '\\"');
const script = `tell application "iTerm2"
repeat with aWindow in windows
if id of aWindow is "${itermId}" then
tell current session of aWindow
write text "printf '\\\\033]2;${escapedTitle}\\\\007'"
end tell
exit repeat
end if
end repeat
end tell`;
try {
this.runAppleScript(script);
} catch {
// Silently fail
}
}
/**
* Kill/terminate a window.
*/
killWindow(windowId: string): void {
if (!windowId || !windowId.startsWith("iterm_win_")) {
return;
}
const itermId = windowId.replace("iterm_win_", "");
const script = `tell application "iTerm2"
repeat with aWindow in windows
if id of aWindow is "${itermId}" then
close aWindow
return "Closed"
end if
end repeat
end tell`;
try {
this.runAppleScript(script);
} catch {
// Silently fail
}
}
/**
* Check if a window is still alive/active.
*/
isWindowAlive(windowId: string): boolean {
if (!windowId || !windowId.startsWith("iterm_win_")) {
return false;
}
const itermId = windowId.replace("iterm_win_", "");
const script = `tell application "iTerm2"
repeat with aWindow in windows
if id of aWindow is "${itermId}" then
return "Alive"
end if
end repeat
end tell`;
try {
const result = this.runAppleScript(script);
return result.stdout.includes("Alive");
} catch {
return false;
}
}
/**
* Set the spawn context (used to restore state when needed)
*/
setSpawnContext(context: Iterm2SpawnContext): void {
this.spawnContext = context;
}
/**
* Get current spawn context (useful for persisting state)
*/
getSpawnContext(): Iterm2SpawnContext {
return { ...this.spawnContext };
}
}