plain.rs

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