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}