1use agent_client_protocol as acp;
2use anyhow::Result;
3use futures::{FutureExt as _, future::Shared};
4use gpui::{App, AppContext, AsyncApp, Context, Entity, Task};
5use language::LanguageRegistry;
6use markdown::Markdown;
7use project::Project;
8use settings::{Settings as _, SettingsLocation};
9use std::{path::PathBuf, process::ExitStatus, sync::Arc, time::Instant};
10use task::Shell;
11use terminal::terminal_settings::TerminalSettings;
12use util::get_default_system_shell_preferring_bash;
13
14pub struct Terminal {
15 id: acp::TerminalId,
16 command: Entity<Markdown>,
17 working_dir: Option<PathBuf>,
18 terminal: Entity<terminal::Terminal>,
19 started_at: Instant,
20 output: Option<TerminalOutput>,
21 output_byte_limit: Option<usize>,
22 _output_task: Shared<Task<acp::TerminalExitStatus>>,
23}
24
25pub struct TerminalOutput {
26 pub ended_at: Instant,
27 pub exit_status: Option<ExitStatus>,
28 pub content: String,
29 pub original_content_len: usize,
30 pub content_line_count: usize,
31}
32
33impl Terminal {
34 pub fn new(
35 id: acp::TerminalId,
36 command_label: &str,
37 working_dir: Option<PathBuf>,
38 output_byte_limit: Option<usize>,
39 terminal: Entity<terminal::Terminal>,
40 language_registry: Arc<LanguageRegistry>,
41 cx: &mut Context<Self>,
42 ) -> Self {
43 let command_task = terminal.read(cx).wait_for_completed_task(cx);
44 Self {
45 id,
46 command: cx.new(|cx| {
47 Markdown::new(
48 format!("```\n{}\n```", command_label).into(),
49 Some(language_registry.clone()),
50 None,
51 cx,
52 )
53 }),
54 working_dir,
55 terminal,
56 started_at: Instant::now(),
57 output: None,
58 output_byte_limit,
59 _output_task: cx
60 .spawn(async move |this, cx| {
61 let exit_status = command_task.await;
62
63 this.update(cx, |this, cx| {
64 let (content, original_content_len) = this.truncated_output(cx);
65 let content_line_count = this.terminal.read(cx).total_lines();
66
67 this.output = Some(TerminalOutput {
68 ended_at: Instant::now(),
69 exit_status,
70 content,
71 original_content_len,
72 content_line_count,
73 });
74 cx.notify();
75 })
76 .ok();
77
78 let exit_status = exit_status.map(portable_pty::ExitStatus::from);
79
80 acp::TerminalExitStatus {
81 exit_code: exit_status.as_ref().map(|e| e.exit_code()),
82 signal: exit_status.and_then(|e| e.signal().map(Into::into)),
83 meta: None,
84 }
85 })
86 .shared(),
87 }
88 }
89
90 pub fn id(&self) -> &acp::TerminalId {
91 &self.id
92 }
93
94 pub fn wait_for_exit(&self) -> Shared<Task<acp::TerminalExitStatus>> {
95 self._output_task.clone()
96 }
97
98 pub fn kill(&mut self, cx: &mut App) {
99 self.terminal.update(cx, |terminal, _cx| {
100 terminal.kill_active_task();
101 });
102 }
103
104 pub fn current_output(&self, cx: &App) -> acp::TerminalOutputResponse {
105 if let Some(output) = self.output.as_ref() {
106 let exit_status = output.exit_status.map(portable_pty::ExitStatus::from);
107
108 acp::TerminalOutputResponse {
109 output: output.content.clone(),
110 truncated: output.original_content_len > output.content.len(),
111 exit_status: Some(acp::TerminalExitStatus {
112 exit_code: exit_status.as_ref().map(|e| e.exit_code()),
113 signal: exit_status.and_then(|e| e.signal().map(Into::into)),
114 meta: None,
115 }),
116 meta: None,
117 }
118 } else {
119 let (current_content, original_len) = self.truncated_output(cx);
120
121 acp::TerminalOutputResponse {
122 truncated: current_content.len() < original_len,
123 output: current_content,
124 exit_status: None,
125 meta: None,
126 }
127 }
128 }
129
130 fn truncated_output(&self, cx: &App) -> (String, usize) {
131 let terminal = self.terminal.read(cx);
132 let mut content = terminal.get_content();
133
134 let original_content_len = content.len();
135
136 if let Some(limit) = self.output_byte_limit
137 && content.len() > limit
138 {
139 let mut end_ix = limit.min(content.len());
140 while !content.is_char_boundary(end_ix) {
141 end_ix -= 1;
142 }
143 // Don't truncate mid-line, clear the remainder of the last line
144 end_ix = content[..end_ix].rfind('\n').unwrap_or(end_ix);
145 content.truncate(end_ix);
146 }
147
148 (content, original_content_len)
149 }
150
151 pub fn command(&self) -> &Entity<Markdown> {
152 &self.command
153 }
154
155 pub fn working_dir(&self) -> &Option<PathBuf> {
156 &self.working_dir
157 }
158
159 pub fn started_at(&self) -> Instant {
160 self.started_at
161 }
162
163 pub fn output(&self) -> Option<&TerminalOutput> {
164 self.output.as_ref()
165 }
166
167 pub fn inner(&self) -> &Entity<terminal::Terminal> {
168 &self.terminal
169 }
170
171 pub fn to_markdown(&self, cx: &App) -> String {
172 format!(
173 "Terminal:\n```\n{}\n```\n",
174 self.terminal.read(cx).get_content()
175 )
176 }
177}
178
179pub async fn create_terminal_entity(
180 command: String,
181 args: &[String],
182 env_vars: Vec<(String, String)>,
183 cwd: Option<PathBuf>,
184 project: &Entity<Project>,
185 cx: &mut AsyncApp,
186) -> Result<Entity<terminal::Terminal>> {
187 let mut env = if let Some(dir) = &cwd {
188 project
189 .update(cx, |project, cx| {
190 let worktree = project.find_worktree(dir.as_path(), cx);
191 let shell = TerminalSettings::get(
192 worktree.as_ref().map(|(worktree, path)| SettingsLocation {
193 worktree_id: worktree.read(cx).id(),
194 path: &path,
195 }),
196 cx,
197 )
198 .shell
199 .clone();
200 project.directory_environment(&shell, dir.clone().into(), cx)
201 })?
202 .await
203 .unwrap_or_default()
204 } else {
205 Default::default()
206 };
207
208 // Disables paging for `git` and hopefully other commands
209 env.insert("PAGER".into(), "".into());
210 env.extend(env_vars);
211
212 // Use remote shell or default system shell, as appropriate
213 let shell = project
214 .update(cx, |project, cx| {
215 project
216 .remote_client()
217 .and_then(|r| r.read(cx).default_system_shell())
218 .map(Shell::Program)
219 })?
220 .unwrap_or_else(|| Shell::Program(get_default_system_shell_preferring_bash()));
221 let is_windows = project
222 .read_with(cx, |project, cx| project.path_style(cx).is_windows())
223 .unwrap_or(cfg!(windows));
224 let (task_command, task_args) = task::ShellBuilder::new(&shell, is_windows)
225 .redirect_stdin_to_dev_null()
226 .build(Some(command.clone()), &args);
227
228 project
229 .update(cx, |project, cx| {
230 project.create_terminal_task(
231 task::SpawnInTerminal {
232 command: Some(task_command),
233 args: task_args,
234 cwd,
235 env,
236 ..Default::default()
237 },
238 cx,
239 )
240 })?
241 .await
242}