-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathTerminal.ts
More file actions
210 lines (172 loc) · 7.06 KB
/
Copy pathTerminal.ts
File metadata and controls
210 lines (172 loc) · 7.06 KB
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
import * as vscode from "vscode"
import pWaitFor from "p-wait-for"
import type { ShoferTerminalCallbacks, ShoferTerminalProcessResultPromise } from "@shofer/types"
import { BaseTerminal } from "@shofer/core"
import { TerminalProcess } from "./TerminalProcess"
import { ShellIntegrationManager } from "./ShellIntegrationManager"
import { mergePromise } from "@shofer/core"
import { webviewLog } from "@shofer/core"
export class Terminal extends BaseTerminal {
public terminal: vscode.Terminal
get platformTerminal(): object {
return this.terminal
}
public cmdCounter: number = 0
constructor(id: number, terminal: vscode.Terminal | undefined, cwd: string) {
super("vscode", id, cwd)
const env = Terminal.getEnv()
const iconPath = new vscode.ThemeIcon("rocket")
this.terminal = terminal ?? vscode.window.createTerminal({ cwd, name: "Shofer", iconPath, env })
if (Terminal.getTerminalZdotdir()) {
ShellIntegrationManager.terminalTmpDirs.set(id, env.ZDOTDIR)
}
}
/**
* Gets the current working directory from shell integration or falls back to initial cwd.
* @returns The current working directory
*/
public override getCurrentWorkingDirectory(): string {
return this.terminal.shellIntegration?.cwd ? this.terminal.shellIntegration.cwd.fsPath : this.initialCwd
}
/**
* Reveals the underlying vscode terminal panel.
* @param preserveFocus When true, the terminal is shown without stealing focus.
*/
public override show(preserveFocus?: boolean): void {
this.terminal.show(preserveFocus)
}
/**
* The exit status of the terminal will be undefined while the terminal is
* active. (This value is set when onDidCloseTerminal is fired.)
*/
public override isClosed(): boolean {
return this.terminal.exitStatus !== undefined
}
public override runCommand(
command: string,
callbacks: ShoferTerminalCallbacks,
): ShoferTerminalProcessResultPromise {
// We set busy before the command is running because the terminal may be
// waiting on terminal integration, and we must prevent another instance
// from selecting the terminal for use during that time.
this.busy = true
const process = new TerminalProcess(this)
process.command = command
this.process = process
// Set up event handlers from callbacks before starting process.
// This ensures that we don't miss any events because they are
// configured before the process starts.
process.on("line", (line) => callbacks.onLine(line, process))
process.once("completed", (output) => callbacks.onCompleted(output, process))
process.once("shell_execution_started", (pid) => callbacks.onShellExecutionStarted(pid, process))
process.once("shell_execution_complete", (details) => callbacks.onShellExecutionComplete(details, process))
process.once("no_shell_integration", (msg) => callbacks.onNoShellIntegration?.(msg, process))
const promise = new Promise<void>((resolve, reject) => {
// Set up event handlers
process.once("continue", () => resolve())
process.once("error", (error) => {
webviewLog.error(`[Terminal ${this.id}] error:`, error)
reject(error)
})
// Wait for shell integration before executing the command
pWaitFor(() => this.terminal.shellIntegration !== undefined, {
timeout: Terminal.getShellIntegrationTimeout(),
})
.then(() => {
// Clean up temporary directory if shell integration is available, zsh did its job:
ShellIntegrationManager.zshCleanupTmpDir(this.id)
// Run the command in the terminal
process.run(command)
})
.catch(() => {
webviewLog.info(`[Terminal ${this.id}] Shell integration not available. Command execution aborted.`)
// Clean up temporary directory if shell integration is not available
ShellIntegrationManager.zshCleanupTmpDir(this.id)
process.emit(
"no_shell_integration",
`Shell integration initialization sequence '\\x1b]633;A' was not received within ${Terminal.getShellIntegrationTimeout() / 1000}s. Shell integration has been disabled for this terminal instance. Increase the timeout in the settings if necessary.`,
)
})
})
return mergePromise(process, promise)
}
/**
* Gets the terminal contents based on the number of commands to include
* @param commands Number of previous commands to include (-1 for all)
* @returns The selected terminal contents
*/
public static async getTerminalContents(commands = -1): Promise<string> {
// Save current clipboard content
const tempCopyBuffer = await vscode.env.clipboard.readText()
try {
// Select terminal content
if (commands < 0) {
await vscode.commands.executeCommand("workbench.action.terminal.selectAll")
} else {
for (let i = 0; i < commands; i++) {
await vscode.commands.executeCommand("workbench.action.terminal.selectToPreviousCommand")
}
}
// Copy selection and clear it
await vscode.commands.executeCommand("workbench.action.terminal.copySelection")
await vscode.commands.executeCommand("workbench.action.terminal.clearSelection")
// Get copied content
let terminalContents = (await vscode.env.clipboard.readText()).trim()
// Restore original clipboard content
await vscode.env.clipboard.writeText(tempCopyBuffer)
if (tempCopyBuffer === terminalContents) {
// No terminal content was copied
return ""
}
// Process multi-line content
const lines = terminalContents.split("\n")
const lastLine = lines.pop()?.trim()
if (lastLine) {
let i = lines.length - 1
while (i >= 0 && !lines[i].trim().startsWith(lastLine)) {
i--
}
terminalContents = lines.slice(Math.max(i, 0)).join("\n")
}
return terminalContents
} catch (error) {
// Ensure clipboard is restored even if an error occurs
await vscode.env.clipboard.writeText(tempCopyBuffer)
throw error
}
}
public static getEnv(): Record<string, string> {
const env: Record<string, string> = {
SHOFER_ACTIVE: "true",
PAGER: process.platform === "win32" ? "" : "cat",
// VTE must be disabled because it prevents the prompt command from executing
// See https://wiki.gnome.org/Apps/Terminal/VTE
VTE_VERSION: "0",
}
// Set Oh My Zsh shell integration if enabled
if (Terminal.getTerminalZshOhMy()) {
env.ITERM_SHELL_INTEGRATION_INSTALLED = "Yes"
}
// Set Powerlevel10k shell integration if enabled
if (Terminal.getTerminalZshP10k()) {
env.POWERLEVEL9K_TERM_SHELL_INTEGRATION = "true"
}
// VSCode bug#237208: Command output can be lost due to a race between completion
// sequences and consumers. Add delay via PROMPT_COMMAND to ensure the
// \x1b]633;D escape sequence arrives after command output is processed.
// Only add this if commandDelay is not zero
if (Terminal.getCommandDelay() > 0) {
env.PROMPT_COMMAND = `sleep ${Terminal.getCommandDelay() / 1000}`
}
// Clear the ZSH EOL mark to prevent issues with command output interpretation
// when output ends with special characters like '%'
if (Terminal.getTerminalZshClearEolMark()) {
env.PROMPT_EOL_MARK = ""
}
// Handle ZDOTDIR for zsh if enabled
if (Terminal.getTerminalZdotdir()) {
env.ZDOTDIR = ShellIntegrationManager.zshInitTmpDir(env)
}
return env
}
}