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}