summaryrefslogtreecommitdiff
path: root/packages/cli/src/ui/utils/terminalSetup.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/cli/src/ui/utils/terminalSetup.ts')
-rw-r--r--packages/cli/src/ui/utils/terminalSetup.ts340
1 files changed, 340 insertions, 0 deletions
diff --git a/packages/cli/src/ui/utils/terminalSetup.ts b/packages/cli/src/ui/utils/terminalSetup.ts
new file mode 100644
index 00000000..7f944847
--- /dev/null
+++ b/packages/cli/src/ui/utils/terminalSetup.ts
@@ -0,0 +1,340 @@
+/**
+ * @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<SupportedTerminal | null> {
+ 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<void> {
+ 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<TerminalSetupResult> {
+ 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<TerminalSetupResult> {
+ return configureVSCodeStyle('VS Code', 'Code');
+}
+
+async function configureCursor(): Promise<TerminalSetupResult> {
+ return configureVSCodeStyle('Cursor', 'Cursor');
+}
+
+async function configureWindsurf(): Promise<TerminalSetupResult> {
+ 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<TerminalSetupResult> 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<TerminalSetupResult> {
+ // 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.`,
+ };
+ }
+}