stdio.rs

  1use crate::outputs::ExecutionView;
  2use alacritty_terminal::{term::Config, vte::ansi::Processor};
  3use gpui::{canvas, size, AnyElement, 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        // These are going to be overridden per-cell
 49        underline: None,
 50        strikethrough: None,
 51        color: theme.colors().terminal_foreground,
 52    };
 53
 54    text_style
 55}
 56
 57pub fn terminal_size(cx: &mut WindowContext) -> terminal::TerminalSize {
 58    let text_style = text_style(cx);
 59    let text_system = cx.text_system();
 60
 61    let line_height = cx.line_height();
 62
 63    let font_pixels = text_style.font_size.to_pixels(cx.rem_size());
 64    let font_id = text_system.resolve_font(&text_style.font());
 65
 66    let cell_width = text_system
 67        .advance(font_id, font_pixels, 'w')
 68        .unwrap()
 69        .width;
 70
 71    let num_lines = DEFAULT_NUM_LINES;
 72    let columns = DEFAULT_NUM_COLUMNS;
 73
 74    // Reversed math from terminal::TerminalSize to get pixel width according to terminal width
 75    let width = columns as f32 * cell_width;
 76    let height = num_lines as f32 * cx.line_height();
 77
 78    terminal::TerminalSize {
 79        cell_width,
 80        line_height,
 81        size: size(width, height),
 82    }
 83}
 84
 85impl TerminalOutput {
 86    pub fn new(cx: &mut WindowContext) -> Self {
 87        let (events_tx, events_rx) = futures::channel::mpsc::unbounded();
 88        let term = alacritty_terminal::Term::new(
 89            Config::default(),
 90            &terminal_size(cx),
 91            terminal::ZedListener(events_tx.clone()),
 92        );
 93
 94        mem::forget(events_rx);
 95        Self {
 96            parser: Processor::new(),
 97            handler: term,
 98        }
 99    }
100
101    pub fn from(text: &str, cx: &mut WindowContext) -> Self {
102        let mut output = Self::new(cx);
103        output.append_text(text);
104        output
105    }
106
107    pub fn append_text(&mut self, text: &str) {
108        for byte in text.as_bytes() {
109            if *byte == b'\n' {
110                // Dirty (?) hack to move the cursor down
111                self.parser.advance(&mut self.handler, b'\r');
112                self.parser.advance(&mut self.handler, b'\n');
113            } else {
114                self.parser.advance(&mut self.handler, *byte);
115            }
116
117            // self.parser.advance(&mut self.handler, *byte);
118        }
119    }
120
121    pub fn render(&self, cx: &mut ViewContext<ExecutionView>) -> AnyElement {
122        let text_style = text_style(cx);
123        let text_system = cx.text_system();
124
125        let grid = self
126            .handler
127            .renderable_content()
128            .display_iter
129            .map(|ic| terminal::IndexedCell {
130                point: ic.point,
131                cell: ic.cell.clone(),
132            });
133        let (cells, rects) = TerminalElement::layout_grid(grid, &text_style, text_system, None, cx);
134
135        // lines are 0-indexed, so we must add 1 to get the number of lines
136        let text_line_height = text_style.line_height_in_pixels(cx.rem_size());
137        let num_lines = cells.iter().map(|c| c.point.line).max().unwrap_or(0) + 1;
138        let height = num_lines as f32 * text_line_height;
139
140        let font_pixels = text_style.font_size.to_pixels(cx.rem_size());
141        let font_id = text_system.resolve_font(&text_style.font());
142
143        let cell_width = text_system
144            .advance(font_id, font_pixels, 'w')
145            .map(|advance| advance.width)
146            .unwrap_or(Pixels(0.0));
147
148        canvas(
149            // prepaint
150            move |_bounds, _| {},
151            // paint
152            move |bounds, _, cx| {
153                for rect in rects {
154                    rect.paint(
155                        bounds.origin,
156                        &terminal::TerminalSize {
157                            cell_width,
158                            line_height: text_line_height,
159                            size: bounds.size,
160                        },
161                        cx,
162                    );
163                }
164
165                for cell in cells {
166                    cell.paint(
167                        bounds.origin,
168                        &terminal::TerminalSize {
169                            cell_width,
170                            line_height: text_line_height,
171                            size: bounds.size,
172                        },
173                        bounds,
174                        cx,
175                    );
176                }
177            },
178        )
179        // We must set the height explicitly for the editor block to size itself correctly
180        .h(height)
181        .into_any_element()
182    }
183}