plain.rs

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