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