1//! # Plain Text Output
2//!
3//! This module provides functionality for rendering plain text output in a terminal-like format.
4//! It uses the Alacritty terminal emulator backend to process and display text, supporting
5//! ANSI escape sequences for formatting, colors, and other terminal features.
6//!
7//! The main component of this module is the `TerminalOutput` struct, which handles the parsing
8//! and rendering of text input, simulating a basic terminal environment within REPL output.
9//!
10//! This module is used for displaying:
11//!
12//! - Standard output (stdout)
13//! - Standard error (stderr)
14//! - Plain text content
15//! - Error tracebacks
16//!
17
18use alacritty_terminal::{
19 grid::Dimensions as _,
20 index::{Column, Line, Point},
21 term::Config,
22 vte::ansi::Processor,
23};
24use gpui::{canvas, size, ClipboardItem, FontStyle, Model, TextStyle, WhiteSpace};
25use language::Buffer;
26use settings::Settings as _;
27use std::mem;
28use terminal::ZedListener;
29use terminal_view::terminal_element::TerminalElement;
30use theme::ThemeSettings;
31use ui::{prelude::*, IntoElement};
32
33use crate::outputs::OutputContent;
34
35/// The `TerminalOutput` struct handles the parsing and rendering of text input,
36/// simulating a basic terminal environment within REPL output.
37///
38/// `TerminalOutput` is designed to handle various types of text-based output, including:
39///
40/// * stdout (standard output)
41/// * stderr (standard error)
42/// * text/plain content
43/// * error tracebacks
44///
45/// It uses the Alacritty terminal emulator backend to process and render text,
46/// supporting ANSI escape sequences for text formatting and colors.
47///
48pub struct TerminalOutput {
49 full_buffer: Option<Model<Buffer>>,
50 /// ANSI escape sequence processor for parsing input text.
51 parser: Processor,
52 /// Alacritty terminal instance that manages the terminal state and content.
53 handler: alacritty_terminal::Term<ZedListener>,
54}
55
56const DEFAULT_NUM_LINES: usize = 32;
57const DEFAULT_NUM_COLUMNS: usize = 128;
58
59/// Returns the default text style for the terminal output.
60pub fn text_style(cx: &mut WindowContext) -> TextStyle {
61 let settings = ThemeSettings::get_global(cx).clone();
62
63 let font_family = settings.buffer_font.family;
64 let font_features = settings.buffer_font.features;
65 let font_weight = settings.buffer_font.weight;
66 let font_fallbacks = settings.buffer_font.fallbacks;
67
68 let theme = cx.theme();
69
70 let text_style = TextStyle {
71 font_family,
72 font_features,
73 font_weight,
74 font_fallbacks,
75 font_size: theme::get_buffer_font_size(cx).into(),
76 font_style: FontStyle::Normal,
77 line_height: cx.line_height().into(),
78 background_color: Some(theme.colors().terminal_ansi_background),
79 white_space: WhiteSpace::Normal,
80 truncate: None,
81 // These are going to be overridden per-cell
82 underline: None,
83 strikethrough: None,
84 color: theme.colors().terminal_foreground,
85 };
86
87 text_style
88}
89
90/// Returns the default terminal size for the terminal output.
91pub fn terminal_size(cx: &mut WindowContext) -> terminal::TerminalSize {
92 let text_style = text_style(cx);
93 let text_system = cx.text_system();
94
95 let line_height = cx.line_height();
96
97 let font_pixels = text_style.font_size.to_pixels(cx.rem_size());
98 let font_id = text_system.resolve_font(&text_style.font());
99
100 let cell_width = text_system
101 .advance(font_id, font_pixels, 'w')
102 .unwrap()
103 .width;
104
105 let num_lines = DEFAULT_NUM_LINES;
106 let columns = DEFAULT_NUM_COLUMNS;
107
108 // Reversed math from terminal::TerminalSize to get pixel width according to terminal width
109 let width = columns as f32 * cell_width;
110 let height = num_lines as f32 * cx.line_height();
111
112 terminal::TerminalSize {
113 cell_width,
114 line_height,
115 size: size(width, height),
116 }
117}
118
119impl TerminalOutput {
120 /// Creates a new `TerminalOutput` instance.
121 ///
122 /// This method initializes a new terminal emulator with default configuration
123 /// and sets up the necessary components for handling terminal events and rendering.
124 ///
125 pub fn new(cx: &mut WindowContext) -> Self {
126 let (events_tx, events_rx) = futures::channel::mpsc::unbounded();
127 let term = alacritty_terminal::Term::new(
128 Config::default(),
129 &terminal_size(cx),
130 terminal::ZedListener(events_tx.clone()),
131 );
132
133 mem::forget(events_rx);
134 Self {
135 parser: Processor::new(),
136 handler: term,
137 full_buffer: None,
138 }
139 }
140
141 /// Creates a new `TerminalOutput` instance with initial content.
142 ///
143 /// Initializes a new terminal output and populates it with the provided text.
144 ///
145 /// # Arguments
146 ///
147 /// * `text` - A string slice containing the initial text for the terminal output.
148 /// * `cx` - A mutable reference to the `WindowContext` for initialization.
149 ///
150 /// # Returns
151 ///
152 /// A new instance of `TerminalOutput` containing the provided text.
153 pub fn from(text: &str, cx: &mut WindowContext) -> Self {
154 let mut output = Self::new(cx);
155 output.append_text(text, cx);
156 output
157 }
158
159 /// Appends text to the terminal output.
160 ///
161 /// Processes each byte of the input text, handling newline characters specially
162 /// to ensure proper cursor movement. Uses the ANSI parser to process the input
163 /// and update the terminal state.
164 ///
165 /// As an example, if the user runs the following Python code in this REPL:
166 ///
167 /// ```python
168 /// import time
169 /// print("Hello,", end="")
170 /// time.sleep(1)
171 /// print(" world!")
172 /// ```
173 ///
174 /// Then append_text will be called twice, with the following arguments:
175 ///
176 /// ```rust
177 /// terminal_output.append_text("Hello,")
178 /// terminal_output.append_text(" world!")
179 /// ```
180 /// Resulting in a single output of "Hello, world!".
181 ///
182 /// # Arguments
183 ///
184 /// * `text` - A string slice containing the text to be appended.
185 pub fn append_text(&mut self, text: &str, cx: &mut WindowContext) {
186 for byte in text.as_bytes() {
187 if *byte == b'\n' {
188 // Dirty (?) hack to move the cursor down
189 self.parser.advance(&mut self.handler, b'\r');
190 self.parser.advance(&mut self.handler, b'\n');
191 } else {
192 self.parser.advance(&mut self.handler, *byte);
193 }
194 }
195
196 // This will keep the buffer up to date, though with some terminal codes it won't be perfect
197 if let Some(buffer) = self.full_buffer.as_ref() {
198 buffer.update(cx, |buffer, cx| {
199 buffer.edit([(buffer.len()..buffer.len(), text)], None, cx);
200 });
201 }
202 }
203
204 fn full_text(&self) -> String {
205 let mut full_text = String::new();
206
207 // Get the total number of lines, including history
208 let total_lines = self.handler.grid().total_lines();
209 let visible_lines = self.handler.screen_lines();
210 let history_lines = total_lines - visible_lines;
211
212 // Capture history lines in correct order (oldest to newest)
213 for line in (0..history_lines).rev() {
214 let line_index = Line(-(line as i32) - 1);
215 let start = Point::new(line_index, Column(0));
216 let end = Point::new(line_index, Column(self.handler.columns() - 1));
217 let line_content = self.handler.bounds_to_string(start, end);
218
219 if !line_content.trim().is_empty() {
220 full_text.push_str(&line_content);
221 full_text.push('\n');
222 }
223 }
224
225 // Capture visible lines
226 for line in 0..visible_lines {
227 let line_index = Line(line as i32);
228 let start = Point::new(line_index, Column(0));
229 let end = Point::new(line_index, Column(self.handler.columns() - 1));
230 let line_content = self.handler.bounds_to_string(start, end);
231
232 if !line_content.trim().is_empty() {
233 full_text.push_str(&line_content);
234 full_text.push('\n');
235 }
236 }
237
238 // Trim any trailing newlines
239 full_text.trim_end().to_string()
240 }
241}
242
243impl Render for TerminalOutput {
244 /// Renders the terminal output as a GPUI element.
245 ///
246 /// Converts the current terminal state into a renderable GPUI element. It handles
247 /// the layout of the terminal grid, calculates the dimensions of the output, and
248 /// creates a canvas element that paints the terminal cells and background rectangles.
249 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
250 let text_style = text_style(cx);
251 let text_system = cx.text_system();
252
253 let grid = self
254 .handler
255 .renderable_content()
256 .display_iter
257 .map(|ic| terminal::IndexedCell {
258 point: ic.point,
259 cell: ic.cell.clone(),
260 });
261 let (cells, rects) = TerminalElement::layout_grid(grid, &text_style, text_system, None, cx);
262
263 // lines are 0-indexed, so we must add 1 to get the number of lines
264 let text_line_height = text_style.line_height_in_pixels(cx.rem_size());
265 let num_lines = cells.iter().map(|c| c.point.line).max().unwrap_or(0) + 1;
266 let height = num_lines as f32 * text_line_height;
267
268 let font_pixels = text_style.font_size.to_pixels(cx.rem_size());
269 let font_id = text_system.resolve_font(&text_style.font());
270
271 let cell_width = text_system
272 .advance(font_id, font_pixels, 'w')
273 .map(|advance| advance.width)
274 .unwrap_or(Pixels(0.0));
275
276 canvas(
277 // prepaint
278 move |_bounds, _| {},
279 // paint
280 move |bounds, _, cx| {
281 for rect in rects {
282 rect.paint(
283 bounds.origin,
284 &terminal::TerminalSize {
285 cell_width,
286 line_height: text_line_height,
287 size: bounds.size,
288 },
289 cx,
290 );
291 }
292
293 for cell in cells {
294 cell.paint(
295 bounds.origin,
296 &terminal::TerminalSize {
297 cell_width,
298 line_height: text_line_height,
299 size: bounds.size,
300 },
301 bounds,
302 cx,
303 );
304 }
305 },
306 )
307 // We must set the height explicitly for the editor block to size itself correctly
308 .h(height)
309 }
310}
311
312impl OutputContent for TerminalOutput {
313 fn clipboard_content(&self, _cx: &WindowContext) -> Option<ClipboardItem> {
314 Some(ClipboardItem::new_string(self.full_text()))
315 }
316
317 fn has_clipboard_content(&self, _cx: &WindowContext) -> bool {
318 true
319 }
320
321 fn has_buffer_content(&self, _cx: &WindowContext) -> bool {
322 true
323 }
324
325 fn buffer_content(&mut self, cx: &mut WindowContext) -> Option<Model<Buffer>> {
326 if self.full_buffer.as_ref().is_some() {
327 return self.full_buffer.clone();
328 }
329
330 let buffer = cx.new_model(|cx| {
331 let mut buffer =
332 Buffer::local(self.full_text(), cx).with_language(language::PLAIN_TEXT.clone(), cx);
333 buffer.set_capability(language::Capability::ReadOnly, cx);
334 buffer
335 });
336
337 self.full_buffer = Some(buffer.clone());
338 Some(buffer)
339 }
340}