import { execFile } from "node:child_process"; function execFileAsync( file: string, args: readonly string[], timeout: number, ): Promise<{ stdout: string }> { return new Promise((resolve, reject) => { execFile(file, args, { timeout }, (error, stdout) => { if (error) { reject(error); return; } resolve({ stdout }); }); }); } export const DEFAULT_WINDOW_TITLE_LENGTH = 36; export type TmuxEnvironment = Readonly<{ TMUX?: string; TMUX_PANE?: string; }>; export type SetPaneTitleOptions = Readonly<{ env?: TmuxEnvironment; tmuxBin?: string; timeoutMs?: number; windowTitleLength?: number; }>; export type PaneTitleSynchronizer = Readonly<{ synchronize(name: string | undefined): Promise; reset(): void; }>; export function normalizeSessionName(name: string | undefined): string | undefined { const normalized = name?.trim(); return normalized ? normalized : undefined; } export function truncateTitle(title: string, maxLength: number): string { const characters = [...title]; if (maxLength <= 0 || characters.length <= maxLength) { return title; } if (maxLength === 1) { return "…"; } return `${characters.slice(0, maxLength - 1).join("")}…`; } export function paneTitleArguments(paneId: string, title: string): readonly string[] { return ["select-pane", "-t", paneId, "-T", title]; } export function paneCountArguments(paneId: string): readonly string[] { return ["display-message", "-p", "-t", paneId, "#{window_panes}"]; } export function windowTitleArguments(paneId: string, title: string): readonly string[] { return ["rename-window", "-t", paneId, title]; } export async function setPaneTitle( name: string | undefined, options: SetPaneTitleOptions = {}, ): Promise { const title = normalizeSessionName(name); const env = options.env ?? process.env; const paneId = env.TMUX_PANE; if (!title || !env.TMUX || !paneId) { return false; } const tmuxBin = options.tmuxBin ?? process.env.PI_TMUX_PANE_TITLE_TMUX_BIN ?? "tmux"; const timeout = options.timeoutMs ?? 1000; try { await execFileAsync(tmuxBin, paneTitleArguments(paneId, title), timeout); } catch { return false; } try { const { stdout } = await execFileAsync(tmuxBin, paneCountArguments(paneId), timeout); if (stdout.trim() === "1") { const windowTitle = truncateTitle( title, options.windowTitleLength ?? DEFAULT_WINDOW_TITLE_LENGTH, ); await execFileAsync(tmuxBin, windowTitleArguments(paneId, windowTitle), timeout); } } catch { // The pane title is still useful when window inspection or renaming fails. } return true; } export function createPaneTitleSynchronizer( options: SetPaneTitleOptions = {}, ): PaneTitleSynchronizer { let lastPaneId: string | undefined; let lastTitle: string | undefined; return { async synchronize(name: string | undefined): Promise { const title = normalizeSessionName(name); const env = options.env ?? process.env; const paneId = env.TMUX_PANE; if (!title || !env.TMUX || !paneId) { return false; } if (paneId === lastPaneId && title === lastTitle) { return true; } const synchronized = await setPaneTitle(title, options); if (synchronized) { lastPaneId = paneId; lastTitle = title; } return synchronized; }, reset(): void { lastPaneId = undefined; lastTitle = undefined; }, }; }