/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ /** * Terminal setup utility for configuring Shift+Enter and Ctrl+Enter support. * * This module provides automatic detection and configuration of various terminal * emulators to support multiline input through modified Enter keys. * * Supported terminals: * - VS Code: Configures keybindings.json to send \\\r\n * - Cursor: Configures keybindings.json to send \\\r\n (VS Code fork) * - Windsurf: Configures keybindings.json to send \\\r\n (VS Code fork) * * For VS Code and its forks: * - Shift+Enter: Sends \\\r\n (backslash followed by CRLF) * - Ctrl+Enter: Sends \\\r\n (backslash followed by CRLF) * * The module will not modify existing shift+enter or ctrl+enter keybindings * to avoid conflicts with user customizations. */ import { promises as fs } from 'fs'; import * as os from 'os'; import * as path from 'path'; import { exec } from 'child_process'; import { promisify } from 'util'; import { isKittyProtocolEnabled } from './kittyProtocolDetector.js'; import { VSCODE_SHIFT_ENTER_SEQUENCE } from './platformConstants.js'; const execAsync = promisify(exec); /** * Removes single-line JSON comments (// ...) from a string to allow parsing * VS Code style JSON files that may contain comments. */ function stripJsonComments(content: string): string { // Remove single-line comments (// ...) return content.replace(/^\s*\/\/.*$/gm, ''); } export interface TerminalSetupResult { success: boolean; message: string; requiresRestart?: boolean; } type SupportedTerminal = 'vscode' | 'cursor' | 'windsurf'; // Terminal detection async function detectTerminal(): Promise { const termProgram = process.env.TERM_PROGRAM; // Check VS Code and its forks - check forks first to avoid false positives // Check for Cursor-specific indicators if ( process.env.CURSOR_TRACE_ID || process.env.VSCODE_GIT_ASKPASS_MAIN?.toLowerCase().includes('cursor') ) { return 'cursor'; } // Check for Windsurf-specific indicators if (process.env.VSCODE_GIT_ASKPASS_MAIN?.toLowerCase().includes('windsurf')) { return 'windsurf'; } // Check VS Code last since forks may also set VSCODE env vars if (termProgram === 'vscode' || process.env.VSCODE_GIT_IPC_HANDLE) { return 'vscode'; } // Check parent process name if (os.platform() !== 'win32') { try { const { stdout } = await execAsync('ps -o comm= -p $PPID'); const parentName = stdout.trim(); // Check forks before VS Code to avoid false positives if (parentName.includes('windsurf') || parentName.includes('Windsurf')) return 'windsurf'; if (parentName.includes('cursor') || parentName.includes('Cursor')) return 'cursor'; if (parentName.includes('code') || parentName.includes('Code')) return 'vscode'; } catch (error) { // Continue detection even if process check fails console.debug('Parent process detection failed:', error); } } return null; } // Backup file helper async function backupFile(filePath: string): Promise { try { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const backupPath = `${filePath}.backup.${timestamp}`; await fs.copyFile(filePath, backupPath); } catch (error) { // Log backup errors but continue with operation console.warn(`Failed to create backup of ${filePath}:`, error); } } // Helper function to get VS Code-style config directory function getVSCodeStyleConfigDir(appName: string): string | null { const platform = os.platform(); if (platform === 'darwin') { return path.join( os.homedir(), 'Library', 'Application Support', appName, 'User', ); } else if (platform === 'win32') { if (!process.env.APPDATA) { return null; } return path.join(process.env.APPDATA, appName, 'User'); } else { return path.join(os.homedir(), '.config', appName, 'User'); } } // Generic VS Code-style terminal configuration async function configureVSCodeStyle( terminalName: string, appName: string, ): Promise { const configDir = getVSCodeStyleConfigDir(appName); if (!configDir) { return { success: false, message: `Could not determine ${terminalName} config path on Windows: APPDATA environment variable is not set.`, }; } const keybindingsFile = path.join(configDir, 'keybindings.json'); try { await fs.mkdir(configDir, { recursive: true }); let keybindings: unknown[] = []; try { const content = await fs.readFile(keybindingsFile, 'utf8'); await backupFile(keybindingsFile); try { const cleanContent = stripJsonComments(content); const parsedContent = JSON.parse(cleanContent); if (!Array.isArray(parsedContent)) { return { success: false, message: `${terminalName} keybindings.json exists but is not a valid JSON array. ` + `Please fix the file manually or delete it to allow automatic configuration.\n` + `File: ${keybindingsFile}`, }; } keybindings = parsedContent; } catch (parseError) { return { success: false, message: `Failed to parse ${terminalName} keybindings.json. The file contains invalid JSON.\n` + `Please fix the file manually or delete it to allow automatic configuration.\n` + `File: ${keybindingsFile}\n` + `Error: ${parseError}`, }; } } catch { // File doesn't exist, will create new one } const shiftEnterBinding = { key: 'shift+enter', command: 'workbench.action.terminal.sendSequence', when: 'terminalFocus', args: { text: VSCODE_SHIFT_ENTER_SEQUENCE }, }; const ctrlEnterBinding = { key: 'ctrl+enter', command: 'workbench.action.terminal.sendSequence', when: 'terminalFocus', args: { text: VSCODE_SHIFT_ENTER_SEQUENCE }, }; // Check if ANY shift+enter or ctrl+enter bindings already exist const existingShiftEnter = keybindings.find((kb) => { const binding = kb as { key?: string }; return binding.key === 'shift+enter'; }); const existingCtrlEnter = keybindings.find((kb) => { const binding = kb as { key?: string }; return binding.key === 'ctrl+enter'; }); if (existingShiftEnter || existingCtrlEnter) { const messages: string[] = []; if (existingShiftEnter) { messages.push(`- Shift+Enter binding already exists`); } if (existingCtrlEnter) { messages.push(`- Ctrl+Enter binding already exists`); } return { success: false, message: `Existing keybindings detected. Will not modify to avoid conflicts.\n` + messages.join('\n') + '\n' + `Please check and modify manually if needed: ${keybindingsFile}`, }; } // Check if our specific bindings already exist const hasOurShiftEnter = keybindings.some((kb) => { const binding = kb as { command?: string; args?: { text?: string }; key?: string; }; return ( binding.key === 'shift+enter' && binding.command === 'workbench.action.terminal.sendSequence' && binding.args?.text === '\\\r\n' ); }); const hasOurCtrlEnter = keybindings.some((kb) => { const binding = kb as { command?: string; args?: { text?: string }; key?: string; }; return ( binding.key === 'ctrl+enter' && binding.command === 'workbench.action.terminal.sendSequence' && binding.args?.text === '\\\r\n' ); }); if (!hasOurShiftEnter || !hasOurCtrlEnter) { if (!hasOurShiftEnter) keybindings.unshift(shiftEnterBinding); if (!hasOurCtrlEnter) keybindings.unshift(ctrlEnterBinding); await fs.writeFile(keybindingsFile, JSON.stringify(keybindings, null, 4)); return { success: true, message: `Added Shift+Enter and Ctrl+Enter keybindings to ${terminalName}.\nModified: ${keybindingsFile}`, requiresRestart: true, }; } else { return { success: true, message: `${terminalName} keybindings already configured.`, }; } } catch (error) { return { success: false, message: `Failed to configure ${terminalName}.\nFile: ${keybindingsFile}\nError: ${error}`, }; } } // Terminal-specific configuration functions async function configureVSCode(): Promise { return configureVSCodeStyle('VS Code', 'Code'); } async function configureCursor(): Promise { return configureVSCodeStyle('Cursor', 'Cursor'); } async function configureWindsurf(): Promise { return configureVSCodeStyle('Windsurf', 'Windsurf'); } /** * Main terminal setup function that detects and configures the current terminal. * * This function: * 1. Detects the current terminal emulator * 2. Applies appropriate configuration for Shift+Enter and Ctrl+Enter support * 3. Creates backups of configuration files before modifying them * * @returns Promise Result object with success status and message * * @example * const result = await terminalSetup(); * if (result.success) { * console.log(result.message); * if (result.requiresRestart) { * console.log('Please restart your terminal'); * } * } */ export async function terminalSetup(): Promise { // Check if terminal already has optimal keyboard support if (isKittyProtocolEnabled()) { return { success: true, message: 'Your terminal is already configured for an optimal experience with multiline input (Shift+Enter and Ctrl+Enter).', }; } const terminal = await detectTerminal(); if (!terminal) { return { success: false, message: 'Could not detect terminal type. Supported terminals: VS Code, Cursor, and Windsurf.', }; } switch (terminal) { case 'vscode': return configureVSCode(); case 'cursor': return configureCursor(); case 'windsurf': return configureWindsurf(); default: return { success: false, message: `Terminal "${terminal}" is not supported yet.`, }; } }