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 event::VoidListener,
20 grid::Dimensions as _,
21 index::{Column, Line, Point},
22 term::Config,
23 vte::ansi::Processor,
24};
25use gpui::{Bounds, ClipboardItem, Entity, FontStyle, TextStyle, WhiteSpace, canvas, size};
26use language::Buffer;
27use settings::Settings as _;
28use terminal_view::terminal_element::TerminalElement;
29use theme::ThemeSettings;
30use ui::{IntoElement, prelude::*};
31
32use crate::outputs::OutputContent;
33
34/// The `TerminalOutput` struct handles the parsing and rendering of text input,
35/// simulating a basic terminal environment within REPL output.
36///
37/// `TerminalOutput` is designed to handle various types of text-based output, including:
38///
39/// * stdout (standard output)
40/// * stderr (standard error)
41/// * text/plain content
42/// * error tracebacks
43///
44/// It uses the Alacritty terminal emulator backend to process and render text,
45/// supporting ANSI escape sequences for text formatting and colors.
46///
47pub struct TerminalOutput {
48 full_buffer: Option<Entity<Buffer>>,
49 /// ANSI escape sequence processor for parsing input text.
50 parser: Processor,
51 /// Alacritty terminal instance that manages the terminal state and content.
52 handler: alacritty_terminal::Term<VoidListener>,
53}
54
55const DEFAULT_NUM_LINES: usize = 32;
56const DEFAULT_NUM_COLUMNS: usize = 128;
57
58/// Returns the default text style for the terminal output.
59pub fn text_style(window: &mut Window, cx: &mut App) -> TextStyle {
60 let settings = ThemeSettings::get_global(cx).clone();
61
62 let font_size = settings.buffer_font_size(cx).into();
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,
76 font_style: FontStyle::Normal,
77 line_height: window.line_height().into(),
78 background_color: Some(theme.colors().terminal_ansi_background),
79 white_space: WhiteSpace::Normal,
80 // These are going to be overridden per-cell
81 color: theme.colors().terminal_foreground,
82 ..Default::default()
83 };
84
85 text_style
86}
87
88/// Returns the default terminal size for the terminal output.
89pub fn terminal_size(window: &mut Window, cx: &mut App) -> terminal::TerminalBounds {
90 let text_style = text_style(window, cx);
91 let text_system = window.text_system();
92
93 let line_height = window.line_height();
94
95 let font_pixels = text_style.font_size.to_pixels(window.rem_size());
96 let font_id = text_system.resolve_font(&text_style.font());
97
98 let cell_width = text_system
99 .advance(font_id, font_pixels, 'w')
100 .unwrap()
101 .width;
102
103 let num_lines = DEFAULT_NUM_LINES;
104 let columns = DEFAULT_NUM_COLUMNS;
105
106 // Reversed math from terminal::TerminalSize to get pixel width according to terminal width
107 let width = columns as f32 * cell_width;
108 let height = num_lines as f32 * window.line_height();
109
110 terminal::TerminalBounds {
111 cell_width,
112 line_height,
113 bounds: Bounds {
114 origin: gpui::Point::default(),
115 size: size(width, height),
116 },
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(window: &mut Window, cx: &mut App) -> Self {
127 let term = alacritty_terminal::Term::new(
128 Config::default(),
129 &terminal_size(window, cx),
130 VoidListener,
131 );
132
133 Self {
134 parser: Processor::new(),
135 handler: term,
136 full_buffer: None,
137 }
138 }
139
140 /// Creates a new `TerminalOutput` instance with initial content.
141 ///
142 /// Initializes a new terminal output and populates it with the provided text.
143 ///
144 /// # Arguments
145 ///
146 /// * `text` - A string slice containing the initial text for the terminal output.
147 /// * `cx` - A mutable reference to the `WindowContext` for initialization.
148 ///
149 /// # Returns
150 ///
151 /// A new instance of `TerminalOutput` containing the provided text.
152 pub fn from(text: &str, window: &mut Window, cx: &mut App) -> Self {
153 let mut output = Self::new(window, cx);
154 output.append_text(text, cx);
155 output
156 }
157
158 /// Appends text to the terminal output.
159 ///
160 /// Processes each byte of the input text, handling newline characters specially
161 /// to ensure proper cursor movement. Uses the ANSI parser to process the input
162 /// and update the terminal state.
163 ///
164 /// As an example, if the user runs the following Python code in this REPL:
165 ///
166 /// ```python
167 /// import time
168 /// print("Hello,", end="")
169 /// time.sleep(1)
170 /// print(" world!")
171 /// ```
172 ///
173 /// Then append_text will be called twice, with the following arguments:
174 ///
175 /// ```rust
176 /// terminal_output.append_text("Hello,")
177 /// terminal_output.append_text(" world!")
178 /// ```
179 /// Resulting in a single output of "Hello, world!".
180 ///
181 /// # Arguments
182 ///
183 /// * `text` - A string slice containing the text to be appended.
184 pub fn append_text(&mut self, text: &str, cx: &mut App) {
185 for byte in text.as_bytes() {
186 if *byte == b'\n' {
187 // Dirty (?) hack to move the cursor down
188 self.parser.advance(&mut self.handler, &[b'\r']);
189 self.parser.advance(&mut self.handler, &[b'\n']);
190 } else {
191 self.parser.advance(&mut self.handler, &[*byte]);
192 }
193 }
194
195 // This will keep the buffer up to date, though with some terminal codes it won't be perfect
196 if let Some(buffer) = self.full_buffer.as_ref() {
197 buffer.update(cx, |buffer, cx| {
198 buffer.edit([(buffer.len()..buffer.len(), text)], None, cx);
199 });
200 }
201 }
202
203 fn full_text(&self) -> String {
204 let mut full_text = String::new();
205
206 // Get the total number of lines, including history
207 let total_lines = self.handler.grid().total_lines();
208 let visible_lines = self.handler.screen_lines();
209 let history_lines = total_lines - visible_lines;
210
211 // Capture history lines in correct order (oldest to newest)
212 for line in (0..history_lines).rev() {
213 let line_index = Line(-(line as i32) - 1);
214 let start = Point::new(line_index, Column(0));
215 let end = Point::new(line_index, Column(self.handler.columns() - 1));
216 let line_content = self.handler.bounds_to_string(start, end);
217
218 if !line_content.trim().is_empty() {
219 full_text.push_str(&line_content);
220 full_text.push('\n');
221 }
222 }
223
224 // Capture visible lines
225 for line in 0..visible_lines {
226 let line_index = Line(line as i32);
227 let start = Point::new(line_index, Column(0));
228 let end = Point::new(line_index, Column(self.handler.columns() - 1));
229 let line_content = self.handler.bounds_to_string(start, end);
230
231 if !line_content.trim().is_empty() {
232 full_text.push_str(&line_content);
233 full_text.push('\n');
234 }
235 }
236
237 // Trim any trailing newlines
238 full_text.trim_end().to_string()
239 }
240}
241
242impl Render for TerminalOutput {
243 /// Renders the terminal output as a GPUI element.
244 ///
245 /// Converts the current terminal state into a renderable GPUI element. It handles
246 /// the layout of the terminal grid, calculates the dimensions of the output, and
247 /// creates a canvas element that paints the terminal cells and background rectangles.
248 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
249 let text_style = text_style(window, cx);
250 let text_system = window.text_system();
251
252 let grid = self
253 .handler
254 .renderable_content()
255 .display_iter
256 .map(|ic| terminal::IndexedCell {
257 point: ic.point,
258 cell: ic.cell.clone(),
259 });
260 let (cells, rects) =
261 TerminalElement::layout_grid(grid, &text_style, text_system, None, window, 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(window.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(window.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, _, window, cx| {
281 for rect in rects {
282 rect.paint(
283 bounds.origin,
284 &terminal::TerminalBounds {
285 cell_width,
286 line_height: text_line_height,
287 bounds,
288 },
289 window,
290 );
291 }
292
293 for cell in cells {
294 cell.paint(
295 bounds.origin,
296 &terminal::TerminalBounds {
297 cell_width,
298 line_height: text_line_height,
299 bounds,
300 },
301 bounds,
302 window,
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, _window: &Window, _cx: &App) -> Option<ClipboardItem> {
315 Some(ClipboardItem::new_string(self.full_text()))
316 }
317
318 fn has_clipboard_content(&self, _window: &Window, _cx: &App) -> bool {
319 true
320 }
321
322 fn has_buffer_content(&self, _window: &Window, _cx: &App) -> bool {
323 true
324 }
325
326 fn buffer_content(&mut self, _: &mut Window, cx: &mut App) -> Option<Entity<Buffer>> {
327 if self.full_buffer.as_ref().is_some() {
328 return self.full_buffer.clone();
329 }
330
331 let buffer = cx.new(|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}