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::{grid::Dimensions as _, term::Config, vte::ansi::Processor};
19use gpui::{canvas, size, AnyElement, ClipboardItem, FontStyle, TextStyle, WhiteSpace};
20use settings::Settings as _;
21use std::mem;
22use terminal::ZedListener;
23use terminal_view::terminal_element::TerminalElement;
24use theme::ThemeSettings;
25use ui::{prelude::*, IntoElement};
26
27use crate::outputs::SupportsClipboard;
28
29/// The `TerminalOutput` struct handles the parsing and rendering of text input,
30/// simulating a basic terminal environment within REPL output.
31///
32/// `TerminalOutput` is designed to handle various types of text-based output, including:
33///
34/// * stdout (standard output)
35/// * stderr (standard error)
36/// * text/plain content
37/// * error tracebacks
38///
39/// It uses the Alacritty terminal emulator backend to process and render text,
40/// supporting ANSI escape sequences for text formatting and colors.
41///
42pub struct TerminalOutput {
43 /// ANSI escape sequence processor for parsing input text.
44 parser: Processor,
45 /// Alacritty terminal instance that manages the terminal state and content.
46 handler: alacritty_terminal::Term<ZedListener>,
47}
48
49const DEFAULT_NUM_LINES: usize = 32;
50const DEFAULT_NUM_COLUMNS: usize = 128;
51
52/// Returns the default text style for the terminal output.
53pub fn text_style(cx: &mut WindowContext) -> TextStyle {
54 let settings = ThemeSettings::get_global(cx).clone();
55
56 let font_family = settings.buffer_font.family;
57 let font_features = settings.buffer_font.features;
58 let font_weight = settings.buffer_font.weight;
59 let font_fallbacks = settings.buffer_font.fallbacks;
60
61 let theme = cx.theme();
62
63 let text_style = TextStyle {
64 font_family,
65 font_features,
66 font_weight,
67 font_fallbacks,
68 font_size: theme::get_buffer_font_size(cx).into(),
69 font_style: FontStyle::Normal,
70 // todo
71 line_height: cx.line_height().into(),
72 background_color: Some(theme.colors().terminal_background),
73 white_space: WhiteSpace::Normal,
74 truncate: None,
75 // These are going to be overridden per-cell
76 underline: None,
77 strikethrough: None,
78 color: theme.colors().terminal_foreground,
79 };
80
81 text_style
82}
83
84/// Returns the default terminal size for the terminal output.
85pub fn terminal_size(cx: &mut WindowContext) -> terminal::TerminalSize {
86 let text_style = text_style(cx);
87 let text_system = cx.text_system();
88
89 let line_height = cx.line_height();
90
91 let font_pixels = text_style.font_size.to_pixels(cx.rem_size());
92 let font_id = text_system.resolve_font(&text_style.font());
93
94 let cell_width = text_system
95 .advance(font_id, font_pixels, 'w')
96 .unwrap()
97 .width;
98
99 let num_lines = DEFAULT_NUM_LINES;
100 let columns = DEFAULT_NUM_COLUMNS;
101
102 // Reversed math from terminal::TerminalSize to get pixel width according to terminal width
103 let width = columns as f32 * cell_width;
104 let height = num_lines as f32 * cx.line_height();
105
106 terminal::TerminalSize {
107 cell_width,
108 line_height,
109 size: size(width, height),
110 }
111}
112
113impl TerminalOutput {
114 /// Creates a new `TerminalOutput` instance.
115 ///
116 /// This method initializes a new terminal emulator with default configuration
117 /// and sets up the necessary components for handling terminal events and rendering.
118 ///
119 pub fn new(cx: &mut WindowContext) -> Self {
120 let (events_tx, events_rx) = futures::channel::mpsc::unbounded();
121 let term = alacritty_terminal::Term::new(
122 Config::default(),
123 &terminal_size(cx),
124 terminal::ZedListener(events_tx.clone()),
125 );
126
127 mem::forget(events_rx);
128 Self {
129 parser: Processor::new(),
130 handler: term,
131 }
132 }
133
134 /// Creates a new `TerminalOutput` instance with initial content.
135 ///
136 /// Initializes a new terminal output and populates it with the provided text.
137 ///
138 /// # Arguments
139 ///
140 /// * `text` - A string slice containing the initial text for the terminal output.
141 /// * `cx` - A mutable reference to the `WindowContext` for initialization.
142 ///
143 /// # Returns
144 ///
145 /// A new instance of `TerminalOutput` containing the provided text.
146 pub fn from(text: &str, cx: &mut WindowContext) -> Self {
147 let mut output = Self::new(cx);
148 output.append_text(text);
149 output
150 }
151
152 /// Appends text to the terminal output.
153 ///
154 /// Processes each byte of the input text, handling newline characters specially
155 /// to ensure proper cursor movement. Uses the ANSI parser to process the input
156 /// and update the terminal state.
157 ///
158 /// As an example, if the user runs the following Python code in this REPL:
159 ///
160 /// ```python
161 /// import time
162 /// print("Hello,", end="")
163 /// time.sleep(1)
164 /// print(" world!")
165 /// ```
166 ///
167 /// Then append_text will be called twice, with the following arguments:
168 ///
169 /// ```rust
170 /// terminal_output.append_text("Hello,")
171 /// terminal_output.append_text(" world!")
172 /// ```
173 /// Resulting in a single output of "Hello, world!".
174 ///
175 /// # Arguments
176 ///
177 /// * `text` - A string slice containing the text to be appended.
178 pub fn append_text(&mut self, text: &str) {
179 for byte in text.as_bytes() {
180 if *byte == b'\n' {
181 // Dirty (?) hack to move the cursor down
182 self.parser.advance(&mut self.handler, b'\r');
183 self.parser.advance(&mut self.handler, b'\n');
184 } else {
185 self.parser.advance(&mut self.handler, *byte);
186 }
187
188 // self.parser.advance(&mut self.handler, *byte);
189 }
190 }
191
192 /// Renders the terminal output as a GPUI element.
193 ///
194 /// Converts the current terminal state into a renderable GPUI element. It handles
195 /// the layout of the terminal grid, calculates the dimensions of the output, and
196 /// creates a canvas element that paints the terminal cells and background rectangles.
197 pub fn render(&self, cx: &mut WindowContext) -> AnyElement {
198 let text_style = text_style(cx);
199 let text_system = cx.text_system();
200
201 let grid = self
202 .handler
203 .renderable_content()
204 .display_iter
205 .map(|ic| terminal::IndexedCell {
206 point: ic.point,
207 cell: ic.cell.clone(),
208 });
209 let (cells, rects) = TerminalElement::layout_grid(grid, &text_style, text_system, None, cx);
210
211 // lines are 0-indexed, so we must add 1 to get the number of lines
212 let text_line_height = text_style.line_height_in_pixels(cx.rem_size());
213 let num_lines = cells.iter().map(|c| c.point.line).max().unwrap_or(0) + 1;
214 let height = num_lines as f32 * text_line_height;
215
216 let font_pixels = text_style.font_size.to_pixels(cx.rem_size());
217 let font_id = text_system.resolve_font(&text_style.font());
218
219 let cell_width = text_system
220 .advance(font_id, font_pixels, 'w')
221 .map(|advance| advance.width)
222 .unwrap_or(Pixels(0.0));
223
224 canvas(
225 // prepaint
226 move |_bounds, _| {},
227 // paint
228 move |bounds, _, cx| {
229 for rect in rects {
230 rect.paint(
231 bounds.origin,
232 &terminal::TerminalSize {
233 cell_width,
234 line_height: text_line_height,
235 size: bounds.size,
236 },
237 cx,
238 );
239 }
240
241 for cell in cells {
242 cell.paint(
243 bounds.origin,
244 &terminal::TerminalSize {
245 cell_width,
246 line_height: text_line_height,
247 size: bounds.size,
248 },
249 bounds,
250 cx,
251 );
252 }
253 },
254 )
255 // We must set the height explicitly for the editor block to size itself correctly
256 .h(height)
257 .into_any_element()
258 }
259}
260
261impl SupportsClipboard for TerminalOutput {
262 fn clipboard_content(&self, _cx: &WindowContext) -> Option<ClipboardItem> {
263 let start = alacritty_terminal::index::Point::new(
264 alacritty_terminal::index::Line(0),
265 alacritty_terminal::index::Column(0),
266 );
267 let end = alacritty_terminal::index::Point::new(
268 alacritty_terminal::index::Line(self.handler.screen_lines() as i32 - 1),
269 alacritty_terminal::index::Column(self.handler.columns() - 1),
270 );
271 let text = self.handler.bounds_to_string(start, end);
272 Some(ClipboardItem::new_string(text.trim().into()))
273 }
274
275 fn has_clipboard_content(&self, _cx: &WindowContext) -> bool {
276 true
277 }
278}