terminal.rs

  1use agent_client_protocol as acp;
  2
  3use futures::{FutureExt as _, future::Shared};
  4use gpui::{App, AppContext, Context, Entity, Task};
  5use language::LanguageRegistry;
  6use markdown::Markdown;
  7use std::{path::PathBuf, process::ExitStatus, sync::Arc, time::Instant};
  8
  9pub struct Terminal {
 10    id: acp::TerminalId,
 11    command: Entity<Markdown>,
 12    working_dir: Option<PathBuf>,
 13    terminal: Entity<terminal::Terminal>,
 14    started_at: Instant,
 15    output: Option<TerminalOutput>,
 16    output_byte_limit: Option<usize>,
 17    _output_task: Shared<Task<acp::TerminalExitStatus>>,
 18}
 19
 20pub struct TerminalOutput {
 21    pub ended_at: Instant,
 22    pub exit_status: Option<ExitStatus>,
 23    pub content: String,
 24    pub original_content_len: usize,
 25    pub content_line_count: usize,
 26}
 27
 28impl Terminal {
 29    pub fn new(
 30        id: acp::TerminalId,
 31        command: String,
 32        working_dir: Option<PathBuf>,
 33        output_byte_limit: Option<usize>,
 34        terminal: Entity<terminal::Terminal>,
 35        language_registry: Arc<LanguageRegistry>,
 36        cx: &mut Context<Self>,
 37    ) -> Self {
 38        let command_task = terminal.read(cx).wait_for_completed_task(cx);
 39        Self {
 40            id,
 41            command: cx.new(|cx| {
 42                Markdown::new(
 43                    format!("```\n{}\n```", command).into(),
 44                    Some(language_registry.clone()),
 45                    None,
 46                    cx,
 47                )
 48            }),
 49            working_dir,
 50            terminal,
 51            started_at: Instant::now(),
 52            output: None,
 53            output_byte_limit,
 54            _output_task: cx
 55                .spawn(async move |this, cx| {
 56                    let exit_status = command_task.await;
 57
 58                    this.update(cx, |this, cx| {
 59                        let (content, original_content_len) = this.truncated_output(cx);
 60                        let content_line_count = this.terminal.read(cx).total_lines();
 61
 62                        this.output = Some(TerminalOutput {
 63                            ended_at: Instant::now(),
 64                            exit_status,
 65                            content,
 66                            original_content_len,
 67                            content_line_count,
 68                        });
 69                        cx.notify();
 70                    })
 71                    .ok();
 72
 73                    let exit_status = exit_status.map(portable_pty::ExitStatus::from);
 74
 75                    acp::TerminalExitStatus {
 76                        exit_code: exit_status.as_ref().map(|e| e.exit_code()),
 77                        signal: exit_status.and_then(|e| e.signal().map(Into::into)),
 78                    }
 79                })
 80                .shared(),
 81        }
 82    }
 83
 84    pub fn id(&self) -> &acp::TerminalId {
 85        &self.id
 86    }
 87
 88    pub fn wait_for_exit(&self) -> Shared<Task<acp::TerminalExitStatus>> {
 89        self._output_task.clone()
 90    }
 91
 92    pub fn kill(&mut self, cx: &mut App) {
 93        self.terminal.update(cx, |terminal, _cx| {
 94            terminal.kill_active_task();
 95        });
 96    }
 97
 98    pub fn current_output(&self, cx: &App) -> acp::TerminalOutputResponse {
 99        if let Some(output) = self.output.as_ref() {
100            let exit_status = output.exit_status.map(portable_pty::ExitStatus::from);
101
102            acp::TerminalOutputResponse {
103                output: output.content.clone(),
104                truncated: output.original_content_len > output.content.len(),
105                exit_status: Some(acp::TerminalExitStatus {
106                    exit_code: exit_status.as_ref().map(|e| e.exit_code()),
107                    signal: exit_status.and_then(|e| e.signal().map(Into::into)),
108                }),
109            }
110        } else {
111            let (current_content, original_len) = self.truncated_output(cx);
112
113            acp::TerminalOutputResponse {
114                truncated: current_content.len() < original_len,
115                output: current_content,
116                exit_status: None,
117            }
118        }
119    }
120
121    fn truncated_output(&self, cx: &App) -> (String, usize) {
122        let terminal = self.terminal.read(cx);
123        let mut content = terminal.get_content();
124
125        let original_content_len = content.len();
126
127        if let Some(limit) = self.output_byte_limit
128            && content.len() > limit
129        {
130            let mut end_ix = limit.min(content.len());
131            while !content.is_char_boundary(end_ix) {
132                end_ix -= 1;
133            }
134            // Don't truncate mid-line, clear the remainder of the last line
135            end_ix = content[..end_ix].rfind('\n').unwrap_or(end_ix);
136            content.truncate(end_ix);
137        }
138
139        (content, original_content_len)
140    }
141
142    pub fn command(&self) -> &Entity<Markdown> {
143        &self.command
144    }
145
146    pub fn working_dir(&self) -> &Option<PathBuf> {
147        &self.working_dir
148    }
149
150    pub fn started_at(&self) -> Instant {
151        self.started_at
152    }
153
154    pub fn output(&self) -> Option<&TerminalOutput> {
155        self.output.as_ref()
156    }
157
158    pub fn inner(&self) -> &Entity<terminal::Terminal> {
159        &self.terminal
160    }
161
162    pub fn to_markdown(&self, cx: &App) -> String {
163        format!(
164            "Terminal:\n```\n{}\n```\n",
165            self.terminal.read(cx).get_content()
166        )
167    }
168}