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