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