1use crate::outputs::ExecutionView;
2use alacritty_terminal::{term::Config, vte::ansi::Processor};
3use gpui::{canvas, size, AnyElement};
4use std::mem;
5use terminal::ZedListener;
6use terminal_view::terminal_element::TerminalElement;
7use ui::{prelude::*, IntoElement, ViewContext};
8
9/// Implements the most basic of terminal output for use by Jupyter outputs
10/// whether:
11///
12/// * stdout
13/// * stderr
14/// * text/plain
15/// * traceback from an error output
16///
17pub struct TerminalOutput {
18 parser: Processor,
19 handler: alacritty_terminal::Term<ZedListener>,
20}
21
22const DEFAULT_NUM_LINES: usize = 32;
23const DEFAULT_NUM_COLUMNS: usize = 128;
24
25pub fn terminal_size(cx: &mut WindowContext) -> terminal::TerminalSize {
26 let text_style = cx.text_style();
27 let text_system = cx.text_system();
28
29 let line_height = cx.line_height();
30
31 let font_pixels = text_style.font_size.to_pixels(cx.rem_size());
32 let font_id = text_system.resolve_font(&text_style.font());
33
34 let cell_width = text_system
35 .advance(font_id, font_pixels, 'w')
36 .unwrap()
37 .width;
38
39 let num_lines = DEFAULT_NUM_LINES;
40 let columns = DEFAULT_NUM_COLUMNS;
41
42 // Reversed math from terminal::TerminalSize to get pixel width according to terminal width
43 let width = columns as f32 * cell_width;
44 let height = num_lines as f32 * cx.line_height();
45
46 terminal::TerminalSize {
47 cell_width,
48 line_height,
49 size: size(width, height),
50 }
51}
52
53impl TerminalOutput {
54 pub fn new(cx: &mut WindowContext) -> Self {
55 let (events_tx, events_rx) = futures::channel::mpsc::unbounded();
56 let term = alacritty_terminal::Term::new(
57 Config::default(),
58 &terminal_size(cx),
59 terminal::ZedListener(events_tx.clone()),
60 );
61
62 mem::forget(events_rx);
63 Self {
64 parser: Processor::new(),
65 handler: term,
66 }
67 }
68
69 pub fn from(text: &str, cx: &mut WindowContext) -> Self {
70 let mut output = Self::new(cx);
71 output.append_text(text);
72 output
73 }
74
75 pub fn append_text(&mut self, text: &str) {
76 for byte in text.as_bytes() {
77 if *byte == b'\n' {
78 // Dirty (?) hack to move the cursor down
79 self.parser.advance(&mut self.handler, b'\r');
80 self.parser.advance(&mut self.handler, b'\n');
81 } else {
82 self.parser.advance(&mut self.handler, *byte);
83 }
84
85 // self.parser.advance(&mut self.handler, *byte);
86 }
87 }
88
89 pub fn render(&self, cx: &ViewContext<ExecutionView>) -> AnyElement {
90 let text_style = cx.text_style();
91 let text_system = cx.text_system();
92
93 let grid = self
94 .handler
95 .renderable_content()
96 .display_iter
97 .map(|ic| terminal::IndexedCell {
98 point: ic.point,
99 cell: ic.cell.clone(),
100 });
101 let (cells, rects) = TerminalElement::layout_grid(grid, &text_style, text_system, None, cx);
102
103 // lines are 0-indexed, so we must add 1 to get the number of lines
104 let num_lines = cells.iter().map(|c| c.point.line).max().unwrap_or(0) + 1;
105 let height = num_lines as f32 * cx.line_height();
106
107 let line_height = cx.line_height();
108
109 let font_pixels = text_style.font_size.to_pixels(cx.rem_size());
110 let font_id = text_system.resolve_font(&text_style.font());
111
112 let cell_width = text_system
113 .advance(font_id, font_pixels, 'w')
114 .map(|advance| advance.width)
115 .unwrap_or(Pixels(0.0));
116
117 canvas(
118 // prepaint
119 move |_bounds, _| {},
120 // paint
121 move |bounds, _, cx| {
122 for rect in rects {
123 rect.paint(
124 bounds.origin,
125 &terminal::TerminalSize {
126 cell_width,
127 line_height,
128 size: bounds.size,
129 },
130 cx,
131 );
132 }
133
134 for cell in cells {
135 cell.paint(
136 bounds.origin,
137 &terminal::TerminalSize {
138 cell_width,
139 line_height,
140 size: bounds.size,
141 },
142 bounds,
143 cx,
144 );
145 }
146 },
147 )
148 // We must set the height explicitly for the editor block to size itself correctly
149 .h(height)
150 .into_any_element()
151 }
152}