Skip to content

Commit 21f52d0

Browse files
committed
Use one-off Shells, Test with All Supported Shells
1 parent 4f08786 commit 21f52d0

File tree

9 files changed

+86
-284
lines changed

9 files changed

+86
-284
lines changed

CodeEdit/Features/Tasks/Models/CEActiveTask.swift

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,15 @@ class CEActiveTask: ObservableObject, Identifiable, Hashable {
4444
}
4545

4646
@MainActor
47-
func run(workspaceURL: URL?) {
47+
func run(workspaceURL: URL?, shell: Shell? = nil) {
4848
self.workspaceURL = workspaceURL
4949
self.activeTaskID = UUID() // generate a new ID for this run
5050

5151
createStatusTaskNotification()
52+
updateTaskStatus(to: .running)
5253

5354
let view = output ?? CEActiveTaskTerminalView(activeTask: self)
54-
view.startProcess(workspaceURL: workspaceURL, shell: nil)
55+
view.startProcess(workspaceURL: workspaceURL, shell: shell)
5556

5657
output = view
5758
}
@@ -107,35 +108,35 @@ class CEActiveTask: ObservableObject, Identifiable, Hashable {
107108

108109
@MainActor
109110
func suspend() {
110-
if let groupPID = output?.getProcessGroup(), status == .running {
111-
kill(groupPID, SIGSTOP)
111+
if let shellPID = output?.runningPID(), status == .running {
112+
kill(shellPID, SIGSTOP)
112113
updateTaskStatus(to: .stopped)
113114
}
114115
}
115116

116117
@MainActor
117118
func resume() {
118-
if let groupPID = output?.getProcessGroup(), status == .running {
119-
kill(groupPID, SIGCONT)
119+
if let shellPID = output?.runningPID(), status == .running {
120+
kill(shellPID, SIGCONT)
120121
updateTaskStatus(to: .running)
121122
}
122123
}
123124

124125
func terminate() {
125-
if let groupPID = output?.getProcessGroup() {
126-
kill(groupPID, SIGTERM)
126+
if let shellPID = output?.runningPID() {
127+
kill(shellPID, SIGTERM)
127128
}
128129
}
129130

130131
func interrupt() {
131-
if let groupPID = output?.getProcessGroup() {
132-
kill(groupPID, SIGINT)
132+
if let shellPID = output?.runningPID() {
133+
kill(shellPID, SIGINT)
133134
}
134135
}
135136

136137
func waitForExit() {
137-
if let groupPID = output?.getProcessGroup() {
138-
waitid(P_PGID, UInt32(groupPID), nil, 0)
138+
if let shellPID = output?.runningPID() {
139+
waitid(P_PGID, UInt32(shellPID), nil, 0)
139140
}
140141
}
141142

CodeEdit/Features/TerminalEmulator/Model/Shell.swift

Lines changed: 22 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -28,56 +28,6 @@ enum Shell: String, CaseIterable {
2828
}
2929
}
3030

31-
/// Executes a shell command using a specified shell, with optional environment variables.
32-
///
33-
/// - Parameters:
34-
/// - process: The `Process` instance to be configured and run.
35-
/// - command: The shell command to execute.
36-
/// - environmentVariables: A dictionary of environment variables to set for the process. Default is `nil`.
37-
/// - shell: The shell to use for executing the command. Default is `.bash`.
38-
/// - outputPipe: The `Pipe` instance to capture standard output and standard error.
39-
/// - Throws: An error if the process fails to run.
40-
///
41-
/// ### Example
42-
/// ```swift
43-
/// let process = Process()
44-
/// let outputPipe = Pipe()
45-
/// try executeCommandWithShell(
46-
/// process: process,
47-
/// command: "echo 'Hello, World!'",
48-
/// environmentVariables: ["PATH": "/usr/bin"],
49-
/// shell: .bash,
50-
/// outputPipe: outputPipe
51-
/// )
52-
/// let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
53-
/// let outputString = String(data: outputData, encoding: .utf8)
54-
/// print(outputString) // Output: "Hello, World!"
55-
/// ```
56-
public static func executeCommandWithShell(
57-
process: Process,
58-
command: String,
59-
environmentVariables: [String: String]? = nil,
60-
shell: Shell = .bash,
61-
outputPipe: Pipe
62-
) throws {
63-
// Setup envs'
64-
process.environment = environmentVariables
65-
// Set the executable to bash
66-
process.executableURL = URL(fileURLWithPath: shell.url)
67-
68-
// Pass the command as an argument
69-
// `--login` argument is needed when using a shell with a process in Swift to ensure
70-
// that the shell loads the user's profile settings (like .bash_profile or .profile),
71-
// which configure the environment variables and other shell settings.
72-
process.arguments = ["--login", "-c", command]
73-
74-
process.standardOutput = outputPipe
75-
process.standardError = outputPipe
76-
77-
// Run the process
78-
try process.run()
79-
}
80-
8131
var defaultPath: String {
8232
switch self {
8333
case .bash:
@@ -86,6 +36,28 @@ enum Shell: String, CaseIterable {
8636
"/bin/zsh"
8737
}
8838
}
39+
40+
/// Create the exec arguments for a new shell with the given behavior.
41+
/// - Parameters:
42+
/// - interactive: The shell is interactive, accepts user input.
43+
/// - login: A login shell.
44+
/// - Returns: The argument string.
45+
func execArguments(interactive: Bool, login: Bool) -> String? {
46+
var args = ""
47+
48+
switch self {
49+
case .bash, .zsh:
50+
if interactive {
51+
args.append("i")
52+
}
53+
54+
if login {
55+
args.append("l")
56+
}
57+
}
58+
59+
return args.isEmpty ? nil : "-" + args
60+
}
8961

9062
/// Gets the default shell from the current user and returns the string of the shell path.
9163
///

CodeEdit/Features/TerminalEmulator/Model/ShellIntegration.swift

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,15 @@ enum ShellIntegration {
6565
// Enable injection in our scripts.
6666
environment.append("\(Variables.ceInjection)=1")
6767

68+
if let execArgs = shell.execArguments(interactive: interactive, login: useLogin) {
69+
args.append(execArgs)
70+
}
71+
6872
switch shell {
6973
case .bash:
70-
try bash(&args, interactive)
74+
try bash(&args)
7175
case .zsh:
72-
try zsh(&args, &environment, useLogin, interactive)
76+
try zsh(&args, &environment)
7377
}
7478

7579
if useLogin {
@@ -95,7 +99,7 @@ enum ShellIntegration {
9599
/// - Parameters:
96100
/// - args: The args to use for shell exec, will be modified by this function.
97101
/// - interactive: Set to true to use an interactive shell.
98-
private static func bash(_ args: inout [String], _ interactive: Bool) throws {
102+
private static func bash(_ args: inout [String]) throws {
99103
// Inject our own bash script that will execute the user's init files, then install our pre/post exec functions.
100104
guard let scriptURL = Bundle.main.url(
101105
forResource: "codeedit_shell_integration",
@@ -104,9 +108,6 @@ enum ShellIntegration {
104108
throw Error.bashShellFileNotFound
105109
}
106110
args.append(contentsOf: ["--init-file", scriptURL.path()])
107-
if interactive {
108-
args.append("-i")
109-
}
110111
}
111112

112113
/// Sets up the `zsh` shell integration.
@@ -125,22 +126,8 @@ enum ShellIntegration {
125126
/// - interactive: Whether to use an interactive shell.
126127
private static func zsh(
127128
_ args: inout [String],
128-
_ environment: inout [String],
129-
_ useLogin: Bool,
130-
_ interactive: Bool
129+
_ environment: inout [String]
131130
) throws {
132-
// Interactive, login shell.
133-
switch (useLogin, interactive) {
134-
case (true, true):
135-
args.append("-il")
136-
case (false, true):
137-
args.append("-i")
138-
case (true, false):
139-
args.append("-l")
140-
default:
141-
break
142-
}
143-
144131
// All injection script URLs
145132
guard let profileScriptURL = Bundle.main.url(
146133
forResource: "codeedit_shell_integration_profile",

CodeEdit/Features/TerminalEmulator/Views/CEActiveTaskTerminalView.swift

Lines changed: 30 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -26,90 +26,43 @@ class CEActiveTaskTerminalView: CELocalShellTerminalView {
2626
fatalError("init(coder:) has not been implemented")
2727
}
2828

29-
override func setup() {
30-
super.setup()
31-
terminal.parser.oscHandlers[133] = { [weak self] slice in
32-
guard let string = String(bytes: slice, encoding: .utf8), let self else { return }
33-
// There's some more commands we could handle but don't right now. See the section on the FinalTerm codes
34-
// here: https://iterm2.com/documentation-escape-codes.html
35-
// There's also a few codes we don't emit. This should be improved in the future.
36-
37-
switch string.first {
38-
case "C":
39-
if self.cachedCaretColor == nil {
40-
self.cachedCaretColor = self.caretColor
41-
}
42-
self.caretColor = self.cachedCaretColor ?? self.caretColor
43-
self.activeTask.updateTaskStatus(to: .running)
44-
45-
self.sendOutputMessage("Starting task: " + self.activeTask.task.name)
46-
self.sendOutputMessage(self.activeTask.task.command)
47-
self.newline()
48-
49-
// Command started
50-
self.enableOutput = true
51-
case "D":
52-
// Disabled before we've received the first C CMD from the task
53-
guard enableOutput else { return }
54-
// Command terminated with code
55-
let chunks = string.split(separator: ";")
56-
guard chunks.count == 2, let status = Int32(chunks[1]) else { return }
57-
self.activeTask.handleProcessFinished(terminationStatus: status)
58-
59-
self.enableOutput = false
60-
self.caretColor = .clear
61-
default:
62-
break
63-
}
64-
}
65-
}
66-
6729
override func startProcess(
6830
workspaceURL url: URL?,
6931
shell: Shell? = nil,
7032
environment: [String] = [],
7133
interactive: Bool = true
7234
) {
73-
// Start the shell
74-
do {
75-
let terminalSettings = Settings.shared.preferences.terminal
76-
77-
var terminalEnvironment: [String] = Terminal.getEnvironmentVariables()
78-
terminalEnvironment.append("TERM_PROGRAM=CodeEditApp_Terminal")
79-
80-
guard let (shell, shellPath) = getShell(shell, userSetting: terminalSettings.shell) else {
81-
return
82-
}
83-
84-
let shellArgs = try ShellIntegration.setUpIntegration(
85-
for: shell,
86-
environment: &terminalEnvironment,
87-
useLogin: terminalSettings.useLoginShell,
88-
interactive: interactive
89-
)
90-
91-
terminalEnvironment.append(contentsOf: environment)
92-
terminalEnvironment.append("\(ShellIntegration.Variables.disableHistory)=1")
93-
terminalEnvironment.append(
94-
contentsOf: activeTask.task.environmentVariables.map({ $0.key + "=" + $0.value })
95-
)
96-
97-
process.startProcess(
98-
executable: shellPath,
99-
args: shellArgs,
100-
environment: terminalEnvironment,
101-
execName: shell.rawValue,
102-
currentDirectory: URL(filePath: activeTask.task.workingDirectory, relativeTo: url).absolutePath
103-
)
104-
105-
// Feed the command and run it
106-
process.send(text: activeTask.task.command)
107-
process.send(data: EscapeSequences.cmdRet[0..<1])
108-
} catch {
109-
newline()
110-
sendOutputMessage("Failed to start a terminal session: \(error.localizedDescription)")
111-
newline()
35+
let terminalSettings = Settings.shared.preferences.terminal
36+
37+
var terminalEnvironment: [String] = Terminal.getEnvironmentVariables()
38+
terminalEnvironment.append("TERM_PROGRAM=CodeEditApp_Terminal")
39+
40+
guard let (shell, shellPath) = getShell(shell, userSetting: terminalSettings.shell) else {
41+
return
11242
}
43+
let shellArgs = ["-lic", activeTask.task.command]
44+
45+
terminalEnvironment.append(contentsOf: environment)
46+
terminalEnvironment.append("\(ShellIntegration.Variables.disableHistory)=1")
47+
terminalEnvironment.append(
48+
contentsOf: activeTask.task.environmentVariables.map({ $0.key + "=" + $0.value })
49+
)
50+
51+
sendOutputMessage("Starting task: " + self.activeTask.task.name)
52+
sendOutputMessage(self.activeTask.task.command)
53+
newline()
54+
55+
process.startProcess(
56+
executable: shellPath,
57+
args: shellArgs,
58+
environment: terminalEnvironment,
59+
execName: shell.rawValue,
60+
currentDirectory: URL(filePath: activeTask.task.workingDirectory, relativeTo: url).absolutePath
61+
)
62+
}
63+
64+
override func processTerminated(_ source: LocalProcess, exitCode: Int32?) {
65+
activeTask.handleProcessFinished(terminationStatus: exitCode ?? 1)
11366
}
11467

11568
func sendOutputMessage(_ message: String) {
@@ -139,31 +92,10 @@ class CEActiveTaskTerminalView: CELocalShellTerminalView {
13992
return nil
14093
}
14194

142-
func getProcessGroup() -> pid_t? {
143-
guard let shellPID = runningPID() else { return nil }
144-
let group = getpgid(shellPID)
145-
guard group >= 0 else { return nil }
146-
return group
147-
}
148-
14995
func getBufferAsString() -> String {
15096
terminal.getText(
15197
start: .init(col: 0, row: 0),
15298
end: .init(col: terminal.cols, row: terminal.rows + terminal.buffer.yDisp)
15399
)
154100
}
155-
156-
override func dataReceived(slice: ArraySlice<UInt8>) {
157-
if enableOutput {
158-
super.dataReceived(slice: slice)
159-
} else if slice.count >= 5 {
160-
// ESC [ 1 3 3
161-
let sequence: [UInt8] = [0x1B, 0x5D, 0x31, 0x33, 0x33]
162-
// Ignore until we see an OSC 133 code
163-
for idx in 0..<(slice.count - 5) where slice[idx..<idx + 5] == sequence[0..<5] {
164-
super.dataReceived(slice: slice[idx..<slice.count])
165-
return
166-
}
167-
}
168-
}
169101
}

CodeEdit/ShellIntegration/codeedit_shell_integration.bash

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -447,12 +447,10 @@ unset -f _install_bash_preexec
447447
__codeedit_status="$?"
448448

449449
__codeedit_preexec() {
450-
builtin printf "\033]133;C\007"
451450
builtin printf "\033]0;%s\007" "$1"
452451
}
453452

454453
__codeedit_precmd() {
455-
builtin printf "\033]133;D;%s\007" "$?"
456454
builtin printf "\033]0;bash\007"
457455
}
458456

CodeEdit/ShellIntegration/codeedit_shell_integration_rc.zsh

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,10 @@ fi
4747
builtin autoload -Uz add-zsh-hook
4848

4949
__codeedit_preexec() {
50-
builtin printf "\033]133;C\007"
5150
builtin printf "\033]0;%s\007" "$1"
5251
}
5352

5453
__codeedit_precmd() {
55-
builtin printf "\033]133;D;%s\007" "$?"
5654
builtin printf "\033]0;zsh\007"
5755
}
5856

0 commit comments

Comments
 (0)