1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
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.`,
};
}
}
|