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}