stdio.rs

  1use crate::outputs::{ExecutionView, SupportsClipboard};
  2use alacritty_terminal::{grid::Dimensions as _, term::Config, vte::ansi::Processor};
  3use gpui::{canvas, size, AnyElement, ClipboardItem, FontStyle, TextStyle, WhiteSpace};
  4use settings::Settings as _;
  5use std::mem;
  6use terminal::ZedListener;
  7use terminal_view::terminal_element::TerminalElement;
  8use theme::ThemeSettings;
  9use ui::{prelude::*, IntoElement, ViewContext};
 10
 11/// Implements the most basic of terminal output for use by Jupyter outputs
 12/// whether:
 13///
 14/// * stdout
 15/// * stderr
 16/// * text/plain
 17/// * traceback from an error output
 18///
 19pub struct TerminalOutput {
 20    parser: Processor,
 21    handler: alacritty_terminal::Term<ZedListener>,
 22}
 23
 24const DEFAULT_NUM_LINES: usize = 32;
 25const DEFAULT_NUM_COLUMNS: usize = 128;
 26
 27pub fn text_style(cx: &mut WindowContext) -> TextStyle {
 28    let settings = ThemeSettings::get_global(cx).clone();
 29
 30    let font_family = settings.buffer_font.family;
 31    let font_features = settings.buffer_font.features;
 32    let font_weight = settings.buffer_font.weight;
 33    let font_fallbacks = settings.buffer_font.fallbacks;
 34
 35    let theme = cx.theme();
 36
 37    let text_style = TextStyle {
 38        font_family,
 39        font_features,
 40        font_weight,
 41        font_fallbacks,
 42        font_size: theme::get_buffer_font_size(cx).into(),
 43        font_style: FontStyle::Normal,
 44        // todo
 45        line_height: cx.line_height().into(),
 46        background_color: Some(theme.colors().terminal_background),
 47        white_space: WhiteSpace::Normal,
 48        truncate: None,
 49        // These are going to be overridden per-cell
 50        underline: None,
 51        strikethrough: None,
 52        color: theme.colors().terminal_foreground,
 53    };
 54
 55    text_style
 56}
 57
 58pub fn terminal_size(cx: &mut WindowContext) -> terminal::TerminalSize {
 59    let text_style = text_style(cx);
 60    let text_system = cx.text_system();
 61
 62    let line_height = cx.line_height();
 63
 64    let font_pixels = text_style.font_size.to_pixels(cx.rem_size());
 65    let font_id = text_system.resolve_font(&text_style.font());
 66
 67    let cell_width = text_system
 68        .advance(font_id, font_pixels, 'w')
 69        .unwrap()
 70        .width;
 71
 72    let num_lines = DEFAULT_NUM_LINES;
 73    let columns = DEFAULT_NUM_COLUMNS;
 74
 75    // Reversed math from terminal::TerminalSize to get pixel width according to terminal width
 76    let width = columns as f32 * cell_width;
 77    let height = num_lines as f32 * cx.line_height();
 78
 79    terminal::TerminalSize {
 80        cell_width,
 81        line_height,
 82        size: size(width, height),
 83    }
 84}
 85
 86impl TerminalOutput {
 87    pub fn new(cx: &mut WindowContext) -> Self {
 88        let (events_tx, events_rx) = futures::channel::mpsc::unbounded();
 89        let term = alacritty_terminal::Term::new(
 90            Config::default(),
 91            &terminal_size(cx),
 92            terminal::ZedListener(events_tx.clone()),
 93        );
 94
 95        mem::forget(events_rx);
 96        Self {
 97            parser: Processor::new(),
 98            handler: term,
 99        }
100    }
101
102    pub fn from(text: &str, cx: &mut WindowContext) -> Self {
103        let mut output = Self::new(cx);
104        output.append_text(text);
105        output
106    }
107
108    pub fn append_text(&mut self, text: &str) {
109        for byte in text.as_bytes() {
110            if *byte == b'\n' {
111                // Dirty (?) hack to move the cursor down
112                self.parser.advance(&mut self.handler, b'\r');
113                self.parser.advance(&mut self.handler, b'\n');
114            } else {
115                self.parser.advance(&mut self.handler, *byte);
116            }
117
118            // self.parser.advance(&mut self.handler, *byte);
119        }
120    }
121
122    pub fn render(&self, cx: &mut ViewContext<ExecutionView>) -> AnyElement {
123        let text_style = text_style(cx);
124        let text_system = cx.text_system();
125
126        let grid = self
127            .handler
128            .renderable_content()
129            .display_iter
130            .map(|ic| terminal::IndexedCell {
131                point: ic.point,
132                cell: ic.cell.clone(),
133            });
134        let (cells, rects) = TerminalElement::layout_grid(grid, &text_style, text_system, None, cx);
135
136        // lines are 0-indexed, so we must add 1 to get the number of lines
137        let text_line_height = text_style.line_height_in_pixels(cx.rem_size());
138        let num_lines = cells.iter().map(|c| c.point.line).max().unwrap_or(0) + 1;
139        let height = num_lines as f32 * text_line_height;
140
141        let font_pixels = text_style.font_size.to_pixels(cx.rem_size());
142        let font_id = text_system.resolve_font(&text_style.font());
143
144        let cell_width = text_system
145            .advance(font_id, font_pixels, 'w')
146            .map(|advance| advance.width)
147            .unwrap_or(Pixels(0.0));
148
149        canvas(
150            // prepaint
151            move |_bounds, _| {},
152            // paint
153            move |bounds, _, cx| {
154                for rect in rects {
155                    rect.paint(
156                        bounds.origin,
157                        &terminal::TerminalSize {
158                            cell_width,
159                            line_height: text_line_height,
160                            size: bounds.size,
161                        },
162                        cx,
163                    );
164                }
165
166                for cell in cells {
167                    cell.paint(
168                        bounds.origin,
169                        &terminal::TerminalSize {
170                            cell_width,
171                            line_height: text_line_height,
172                            size: bounds.size,
173                        },
174                        bounds,
175                        cx,
176                    );
177                }
178            },
179        )
180        // We must set the height explicitly for the editor block to size itself correctly
181        .h(height)
182        .into_any_element()
183    }
184}
185
186impl SupportsClipboard for TerminalOutput {
187    fn clipboard_content(&self, _cx: &WindowContext) -> Option<ClipboardItem> {
188        let start = alacritty_terminal::index::Point::new(
189            alacritty_terminal::index::Line(0),
190            alacritty_terminal::index::Column(0),
191        );
192        let end = alacritty_terminal::index::Point::new(
193            alacritty_terminal::index::Line(self.handler.screen_lines() as i32 - 1),
194            alacritty_terminal::index::Column(self.handler.columns() - 1),
195        );
196        let text = self.handler.bounds_to_string(start, end);
197        Some(ClipboardItem::new_string(text.trim().into()))
198    }
199
200    fn has_clipboard_content(&self, _cx: &WindowContext) -> bool {
201        true
202    }
203}