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}