outputs.rs

  1use std::sync::Arc;
  2use std::time::Duration;
  3
  4use crate::stdio::TerminalOutput;
  5use anyhow::Result;
  6use base64::prelude::*;
  7use gpui::{
  8    img, percentage, Animation, AnimationExt, AnyElement, ClipboardItem, FontWeight, Image,
  9    ImageFormat, Render, RenderImage, Task, TextRun, Transformation, View,
 10};
 11use runtimelib::datatable::TableSchema;
 12use runtimelib::media::datatable::TabularDataResource;
 13use runtimelib::{ExecutionState, JupyterMessageContent, MimeBundle, MimeType};
 14use serde_json::Value;
 15use settings::Settings;
 16use theme::ThemeSettings;
 17use ui::{div, prelude::*, v_flex, IntoElement, Styled, Tooltip, ViewContext};
 18
 19use markdown_preview::{
 20    markdown_elements::ParsedMarkdown, markdown_parser::parse_markdown,
 21    markdown_renderer::render_markdown_block,
 22};
 23
 24/// When deciding what to render from a collection of mediatypes, we need to rank them in order of importance
 25fn rank_mime_type(mimetype: &MimeType) -> usize {
 26    match mimetype {
 27        MimeType::DataTable(_) => 6,
 28        MimeType::Png(_) => 4,
 29        MimeType::Jpeg(_) => 3,
 30        MimeType::Markdown(_) => 2,
 31        MimeType::Plain(_) => 1,
 32        // All other media types are not supported in Zed at this time
 33        _ => 0,
 34    }
 35}
 36
 37pub(crate) trait SupportsClipboard {
 38    fn clipboard_content(&self, cx: &WindowContext) -> Option<ClipboardItem>;
 39    fn has_clipboard_content(&self, cx: &WindowContext) -> bool;
 40}
 41
 42/// ImageView renders an image inline in an editor, adapting to the line height to fit the image.
 43pub struct ImageView {
 44    clipboard_image: Arc<Image>,
 45    height: u32,
 46    width: u32,
 47    image: Arc<RenderImage>,
 48}
 49
 50impl ImageView {
 51    fn render(&self, cx: &ViewContext<ExecutionView>) -> AnyElement {
 52        let line_height = cx.line_height();
 53
 54        let (height, width) = if self.height as f32 / line_height.0 == u8::MAX as f32 {
 55            let height = u8::MAX as f32 * line_height.0;
 56            let width = self.width as f32 * height / self.height as f32;
 57            (height, width)
 58        } else {
 59            (self.height as f32, self.width as f32)
 60        };
 61
 62        let image = self.image.clone();
 63
 64        div()
 65            .h(Pixels(height))
 66            .w(Pixels(width))
 67            .child(img(image))
 68            .into_any_element()
 69    }
 70
 71    fn from(base64_encoded_data: &str) -> Result<Self> {
 72        let bytes = BASE64_STANDARD.decode(base64_encoded_data)?;
 73
 74        let format = image::guess_format(&bytes)?;
 75        let mut data = image::load_from_memory_with_format(&bytes, format)?.into_rgba8();
 76
 77        // Convert from RGBA to BGRA.
 78        for pixel in data.chunks_exact_mut(4) {
 79            pixel.swap(0, 2);
 80        }
 81
 82        let height = data.height();
 83        let width = data.width();
 84
 85        let gpui_image_data = RenderImage::new(vec![image::Frame::new(data)]);
 86
 87        let format = match format {
 88            image::ImageFormat::Png => ImageFormat::Png,
 89            image::ImageFormat::Jpeg => ImageFormat::Jpeg,
 90            image::ImageFormat::Gif => ImageFormat::Gif,
 91            image::ImageFormat::WebP => ImageFormat::Webp,
 92            image::ImageFormat::Tiff => ImageFormat::Tiff,
 93            image::ImageFormat::Bmp => ImageFormat::Bmp,
 94            _ => {
 95                return Err(anyhow::anyhow!("unsupported image format"));
 96            }
 97        };
 98
 99        // Convert back to a GPUI image for use with the clipboard
100        let clipboard_image = Arc::new(Image {
101            format,
102            bytes,
103            id: gpui_image_data.id.0 as u64,
104        });
105
106        return Ok(ImageView {
107            clipboard_image,
108            height,
109            width,
110            image: Arc::new(gpui_image_data),
111        });
112    }
113}
114
115impl SupportsClipboard for ImageView {
116    fn clipboard_content(&self, _cx: &WindowContext) -> Option<ClipboardItem> {
117        Some(ClipboardItem::new_image(self.clipboard_image.as_ref()))
118    }
119
120    fn has_clipboard_content(&self, _cx: &WindowContext) -> bool {
121        true
122    }
123}
124
125/// TableView renders a static table inline in a buffer.
126/// It uses the https://specs.frictionlessdata.io/tabular-data-resource/ specification for data interchange.
127pub struct TableView {
128    pub table: TabularDataResource,
129    pub widths: Vec<Pixels>,
130    cached_clipboard_content: ClipboardItem,
131}
132
133fn cell_content(row: &Value, field: &str) -> String {
134    match row.get(&field) {
135        Some(Value::String(s)) => s.clone(),
136        Some(Value::Number(n)) => n.to_string(),
137        Some(Value::Bool(b)) => b.to_string(),
138        Some(Value::Array(arr)) => format!("{:?}", arr),
139        Some(Value::Object(obj)) => format!("{:?}", obj),
140        Some(Value::Null) | None => String::new(),
141    }
142}
143
144// Declare constant for the padding multiple on the line height
145const TABLE_Y_PADDING_MULTIPLE: f32 = 0.5;
146
147impl TableView {
148    pub fn new(table: TabularDataResource, cx: &mut WindowContext) -> Self {
149        let mut widths = Vec::with_capacity(table.schema.fields.len());
150
151        let text_system = cx.text_system();
152        let text_style = cx.text_style();
153        let text_font = ThemeSettings::get_global(cx).buffer_font.clone();
154        let font_size = ThemeSettings::get_global(cx).buffer_font_size;
155        let mut runs = [TextRun {
156            len: 0,
157            font: text_font,
158            color: text_style.color,
159            background_color: None,
160            underline: None,
161            strikethrough: None,
162        }];
163
164        for field in table.schema.fields.iter() {
165            runs[0].len = field.name.len();
166            let mut width = text_system
167                .layout_line(&field.name, font_size, &runs)
168                .map(|layout| layout.width)
169                .unwrap_or(px(0.));
170
171            let Some(data) = table.data.as_ref() else {
172                widths.push(width);
173                continue;
174            };
175
176            for row in data {
177                let content = cell_content(&row, &field.name);
178                runs[0].len = content.len();
179                let cell_width = cx
180                    .text_system()
181                    .layout_line(&content, font_size, &runs)
182                    .map(|layout| layout.width)
183                    .unwrap_or(px(0.));
184
185                width = width.max(cell_width)
186            }
187
188            widths.push(width)
189        }
190
191        let cached_clipboard_content = Self::create_clipboard_content(&table);
192
193        Self {
194            table,
195            widths,
196            cached_clipboard_content: ClipboardItem::new_string(cached_clipboard_content),
197        }
198    }
199
200    fn escape_markdown(s: &str) -> String {
201        s.replace('|', "\\|")
202            .replace('*', "\\*")
203            .replace('_', "\\_")
204            .replace('`', "\\`")
205            .replace('[', "\\[")
206            .replace(']', "\\]")
207            .replace('<', "&lt;")
208            .replace('>', "&gt;")
209    }
210
211    fn create_clipboard_content(table: &TabularDataResource) -> String {
212        let data = match table.data.as_ref() {
213            Some(data) => data,
214            None => &Vec::new(),
215        };
216        let schema = table.schema.clone();
217
218        let mut markdown = format!(
219            "| {} |\n",
220            table
221                .schema
222                .fields
223                .iter()
224                .map(|field| field.name.clone())
225                .collect::<Vec<_>>()
226                .join(" | ")
227        );
228
229        markdown.push_str("|---");
230        for _ in 1..table.schema.fields.len() {
231            markdown.push_str("|---");
232        }
233        markdown.push_str("|\n");
234
235        let body = data
236            .iter()
237            .map(|record: &Value| {
238                let row_content = schema
239                    .fields
240                    .iter()
241                    .map(|field| Self::escape_markdown(&cell_content(record, &field.name)))
242                    .collect::<Vec<_>>();
243
244                row_content.join(" | ")
245            })
246            .collect::<Vec<String>>();
247
248        for row in body {
249            markdown.push_str(&format!("| {} |\n", row));
250        }
251
252        markdown
253    }
254
255    pub fn render(&self, cx: &ViewContext<ExecutionView>) -> AnyElement {
256        let data = match &self.table.data {
257            Some(data) => data,
258            None => return div().into_any_element(),
259        };
260
261        let mut headings = serde_json::Map::new();
262        for field in &self.table.schema.fields {
263            headings.insert(field.name.clone(), Value::String(field.name.clone()));
264        }
265        let header = self.render_row(&self.table.schema, true, &Value::Object(headings), cx);
266
267        let body = data
268            .iter()
269            .map(|row| self.render_row(&self.table.schema, false, &row, cx));
270
271        v_flex()
272            .id("table")
273            .overflow_x_scroll()
274            .w_full()
275            .child(header)
276            .children(body)
277            .into_any_element()
278    }
279
280    pub fn render_row(
281        &self,
282        schema: &TableSchema,
283        is_header: bool,
284        row: &Value,
285        cx: &ViewContext<ExecutionView>,
286    ) -> AnyElement {
287        let theme = cx.theme();
288
289        let line_height = cx.line_height();
290
291        let row_cells = schema
292            .fields
293            .iter()
294            .zip(self.widths.iter())
295            .map(|(field, width)| {
296                let container = match field.field_type {
297                    runtimelib::datatable::FieldType::String => div(),
298
299                    runtimelib::datatable::FieldType::Number
300                    | runtimelib::datatable::FieldType::Integer
301                    | runtimelib::datatable::FieldType::Date
302                    | runtimelib::datatable::FieldType::Time
303                    | runtimelib::datatable::FieldType::Datetime
304                    | runtimelib::datatable::FieldType::Year
305                    | runtimelib::datatable::FieldType::Duration
306                    | runtimelib::datatable::FieldType::Yearmonth => v_flex().items_end(),
307
308                    _ => div(),
309                };
310
311                let value = cell_content(row, &field.name);
312
313                let mut cell = container
314                    .min_w(*width + px(22.))
315                    .w(*width + px(22.))
316                    .child(value)
317                    .px_2()
318                    .py((TABLE_Y_PADDING_MULTIPLE / 2.0) * line_height)
319                    .border_color(theme.colors().border);
320
321                if is_header {
322                    cell = cell.border_1().bg(theme.colors().border_focused)
323                } else {
324                    cell = cell.border_1()
325                }
326                cell
327            })
328            .collect::<Vec<_>>();
329
330        let mut total_width = px(0.);
331        for width in self.widths.iter() {
332            // Width fudge factor: border + 2 (heading), padding
333            total_width += *width + px(22.);
334        }
335
336        h_flex()
337            .w(total_width)
338            .children(row_cells)
339            .into_any_element()
340    }
341}
342
343impl SupportsClipboard for TableView {
344    fn clipboard_content(&self, _cx: &WindowContext) -> Option<ClipboardItem> {
345        Some(self.cached_clipboard_content.clone())
346    }
347
348    fn has_clipboard_content(&self, _cx: &WindowContext) -> bool {
349        true
350    }
351}
352
353/// Userspace error from the kernel
354pub struct ErrorView {
355    pub ename: String,
356    pub evalue: String,
357    pub traceback: TerminalOutput,
358}
359
360impl ErrorView {
361    fn render(&self, cx: &mut ViewContext<ExecutionView>) -> Option<AnyElement> {
362        let theme = cx.theme();
363
364        let padding = cx.line_height() / 2.;
365
366        Some(
367            v_flex()
368                .gap_3()
369                .child(
370                    h_flex()
371                        .font_buffer(cx)
372                        .child(
373                            Label::new(format!("{}: ", self.ename.clone()))
374                                // .size(LabelSize::Large)
375                                .color(Color::Error)
376                                .weight(FontWeight::BOLD),
377                        )
378                        .child(
379                            Label::new(self.evalue.clone())
380                                // .size(LabelSize::Large)
381                                .weight(FontWeight::BOLD),
382                        ),
383                )
384                .child(
385                    div()
386                        .w_full()
387                        .px(padding)
388                        .py(padding)
389                        .border_l_1()
390                        .border_color(theme.status().error_border)
391                        .child(self.traceback.render(cx)),
392                )
393                .into_any_element(),
394        )
395    }
396}
397
398pub struct MarkdownView {
399    raw_text: String,
400    contents: Option<ParsedMarkdown>,
401    parsing_markdown_task: Option<Task<Result<()>>>,
402}
403
404impl MarkdownView {
405    pub fn from(text: String, cx: &mut ViewContext<Self>) -> Self {
406        let task = cx.spawn(|markdown_view, mut cx| {
407            let text = text.clone();
408            let parsed = cx
409                .background_executor()
410                .spawn(async move { parse_markdown(&text, None, None).await });
411
412            async move {
413                let content = parsed.await;
414
415                markdown_view.update(&mut cx, |markdown, cx| {
416                    markdown.parsing_markdown_task.take();
417                    markdown.contents = Some(content);
418                    cx.notify();
419                })
420            }
421        });
422
423        Self {
424            raw_text: text.clone(),
425            contents: None,
426            parsing_markdown_task: Some(task),
427        }
428    }
429}
430
431impl SupportsClipboard for MarkdownView {
432    fn clipboard_content(&self, _cx: &WindowContext) -> Option<ClipboardItem> {
433        Some(ClipboardItem::new_string(self.raw_text.clone()))
434    }
435
436    fn has_clipboard_content(&self, _cx: &WindowContext) -> bool {
437        true
438    }
439}
440
441impl Render for MarkdownView {
442    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
443        let Some(parsed) = self.contents.as_ref() else {
444            return div().into_any_element();
445        };
446
447        let mut markdown_render_context =
448            markdown_preview::markdown_renderer::RenderContext::new(None, cx);
449
450        v_flex()
451            .gap_3()
452            .py_4()
453            .children(parsed.children.iter().map(|child| {
454                div().relative().child(
455                    div()
456                        .relative()
457                        .child(render_markdown_block(child, &mut markdown_render_context)),
458                )
459            }))
460            .into_any_element()
461    }
462}
463
464pub struct Output {
465    content: OutputContent,
466    display_id: Option<String>,
467}
468
469impl Output {
470    pub fn new(data: &MimeBundle, display_id: Option<String>, cx: &mut WindowContext) -> Self {
471        Self {
472            content: OutputContent::new(data, cx),
473            display_id,
474        }
475    }
476
477    pub fn from(content: OutputContent) -> Self {
478        Self {
479            content,
480            display_id: None,
481        }
482    }
483}
484
485impl SupportsClipboard for Output {
486    fn clipboard_content(&self, cx: &WindowContext) -> Option<ClipboardItem> {
487        match &self.content {
488            OutputContent::Plain(terminal) => terminal.clipboard_content(cx),
489            OutputContent::Stream(terminal) => terminal.clipboard_content(cx),
490            OutputContent::Image(image) => image.clipboard_content(cx),
491            OutputContent::ErrorOutput(error) => error.traceback.clipboard_content(cx),
492            OutputContent::Message(_) => None,
493            OutputContent::Table(table) => table.clipboard_content(cx),
494            OutputContent::Markdown(markdown) => markdown.read(cx).clipboard_content(cx),
495            OutputContent::ClearOutputWaitMarker => None,
496        }
497    }
498
499    fn has_clipboard_content(&self, cx: &WindowContext) -> bool {
500        match &self.content {
501            OutputContent::Plain(terminal) => terminal.has_clipboard_content(cx),
502            OutputContent::Stream(terminal) => terminal.has_clipboard_content(cx),
503            OutputContent::Image(image) => image.has_clipboard_content(cx),
504            OutputContent::ErrorOutput(error) => error.traceback.has_clipboard_content(cx),
505            OutputContent::Message(_) => false,
506            OutputContent::Table(table) => table.has_clipboard_content(cx),
507            OutputContent::Markdown(markdown) => markdown.read(cx).has_clipboard_content(cx),
508            OutputContent::ClearOutputWaitMarker => false,
509        }
510    }
511}
512
513pub enum OutputContent {
514    Plain(TerminalOutput),
515    Stream(TerminalOutput),
516    Image(ImageView),
517    ErrorOutput(ErrorView),
518    Message(String),
519    Table(TableView),
520    Markdown(View<MarkdownView>),
521    ClearOutputWaitMarker,
522}
523
524impl OutputContent {
525    fn render(&self, cx: &mut ViewContext<ExecutionView>) -> Option<AnyElement> {
526        let el = match self {
527            // Note: in typical frontends we would show the execute_result.execution_count
528            // Here we can just handle either
529            Self::Plain(stdio) => Some(stdio.render(cx)),
530            Self::Markdown(markdown) => Some(markdown.clone().into_any_element()),
531            Self::Stream(stdio) => Some(stdio.render(cx)),
532            Self::Image(image) => Some(image.render(cx)),
533            Self::Message(message) => Some(div().child(message.clone()).into_any_element()),
534            Self::Table(table) => Some(table.render(cx)),
535            Self::ErrorOutput(error_view) => error_view.render(cx),
536            Self::ClearOutputWaitMarker => None,
537        };
538
539        el
540    }
541
542    pub fn new(data: &MimeBundle, cx: &mut WindowContext) -> Self {
543        match data.richest(rank_mime_type) {
544            Some(MimeType::Plain(text)) => OutputContent::Plain(TerminalOutput::from(text, cx)),
545            Some(MimeType::Markdown(text)) => {
546                let view = cx.new_view(|cx| MarkdownView::from(text.clone(), cx));
547                OutputContent::Markdown(view)
548            }
549            Some(MimeType::Png(data)) | Some(MimeType::Jpeg(data)) => match ImageView::from(data) {
550                Ok(view) => OutputContent::Image(view),
551                Err(error) => OutputContent::Message(format!("Failed to load image: {}", error)),
552            },
553            Some(MimeType::DataTable(data)) => {
554                OutputContent::Table(TableView::new(data.clone(), cx))
555            }
556            // Any other media types are not supported
557            _ => OutputContent::Message("Unsupported media type".to_string()),
558        }
559    }
560}
561
562#[derive(Default, Clone, Debug)]
563pub enum ExecutionStatus {
564    #[default]
565    Unknown,
566    ConnectingToKernel,
567    Queued,
568    Executing,
569    Finished,
570    ShuttingDown,
571    Shutdown,
572    KernelErrored(String),
573    Restarting,
574}
575
576pub struct ExecutionView {
577    pub outputs: Vec<Output>,
578    pub status: ExecutionStatus,
579}
580
581impl ExecutionView {
582    pub fn new(status: ExecutionStatus, _cx: &mut ViewContext<Self>) -> Self {
583        Self {
584            outputs: Default::default(),
585            status,
586        }
587    }
588
589    /// Accept a Jupyter message belonging to this execution
590    pub fn push_message(&mut self, message: &JupyterMessageContent, cx: &mut ViewContext<Self>) {
591        let output: Output = match message {
592            JupyterMessageContent::ExecuteResult(result) => Output::new(
593                &result.data,
594                result.transient.as_ref().and_then(|t| t.display_id.clone()),
595                cx,
596            ),
597            JupyterMessageContent::DisplayData(result) => {
598                Output::new(&result.data, result.transient.display_id.clone(), cx)
599            }
600            JupyterMessageContent::StreamContent(result) => {
601                // Previous stream data will combine together, handling colors, carriage returns, etc
602                if let Some(new_terminal) = self.apply_terminal_text(&result.text, cx) {
603                    Output::from(new_terminal)
604                } else {
605                    return;
606                }
607            }
608            JupyterMessageContent::ErrorOutput(result) => {
609                let mut terminal = TerminalOutput::new(cx);
610                terminal.append_text(&result.traceback.join("\n"));
611
612                Output::from(OutputContent::ErrorOutput(ErrorView {
613                    ename: result.ename.clone(),
614                    evalue: result.evalue.clone(),
615                    traceback: terminal,
616                }))
617            }
618            JupyterMessageContent::ExecuteReply(reply) => {
619                for payload in reply.payload.iter() {
620                    match payload {
621                        // Pager data comes in via `?` at the end of a statement in Python, used for showing documentation.
622                        // Some UI will show this as a popup. For ease of implementation, it's included as an output here.
623                        runtimelib::Payload::Page { data, .. } => {
624                            let output = Output::new(data, None, cx);
625                            self.outputs.push(output);
626                        }
627
628                        // There are other payloads that could be handled here, such as updating the input.
629                        // Below are the other payloads that _could_ be handled, but are not required for Zed.
630
631                        // Set next input adds text to the next cell. Not required to support.
632                        // However, this could be implemented by adding text to the buffer.
633                        // Trigger in python using `get_ipython().set_next_input("text")`
634                        //
635                        // runtimelib::Payload::SetNextInput { text, replace } => {},
636
637                        // Not likely to be used in the context of Zed, where someone could just open the buffer themselves
638                        // Python users can trigger this with the `%edit` magic command
639                        // runtimelib::Payload::EditMagic { filename, line_number } => {},
640
641                        // Ask the user if they want to exit the kernel. Not required to support.
642                        // runtimelib::Payload::AskExit { keepkernel } => {},
643                        _ => {}
644                    }
645                }
646                cx.notify();
647                return;
648            }
649            JupyterMessageContent::ClearOutput(options) => {
650                if !options.wait {
651                    self.outputs.clear();
652                    cx.notify();
653                    return;
654                }
655
656                // Create a marker to clear the output after we get in a new output
657                Output::from(OutputContent::ClearOutputWaitMarker)
658            }
659            JupyterMessageContent::Status(status) => {
660                match status.execution_state {
661                    ExecutionState::Busy => {
662                        self.status = ExecutionStatus::Executing;
663                    }
664                    ExecutionState::Idle => self.status = ExecutionStatus::Finished,
665                }
666                cx.notify();
667                return;
668            }
669            _msg => {
670                return;
671            }
672        };
673
674        // Check for a clear output marker as the previous output, so we can clear it out
675        if let Some(output) = self.outputs.last() {
676            if let OutputContent::ClearOutputWaitMarker = output.content {
677                self.outputs.clear();
678            }
679        }
680
681        self.outputs.push(output);
682
683        cx.notify();
684    }
685
686    pub fn update_display_data(
687        &mut self,
688        data: &MimeBundle,
689        display_id: &str,
690        cx: &mut ViewContext<Self>,
691    ) {
692        let mut any = false;
693
694        self.outputs.iter_mut().for_each(|output| {
695            if let Some(other_display_id) = output.display_id.as_ref() {
696                if other_display_id == display_id {
697                    output.content = OutputContent::new(data, cx);
698                    any = true;
699                }
700            }
701        });
702
703        if any {
704            cx.notify();
705        }
706    }
707
708    fn apply_terminal_text(
709        &mut self,
710        text: &str,
711        cx: &mut ViewContext<Self>,
712    ) -> Option<OutputContent> {
713        if let Some(last_output) = self.outputs.last_mut() {
714            match &mut last_output.content {
715                OutputContent::Stream(last_stream) => {
716                    last_stream.append_text(text);
717                    // Don't need to add a new output, we already have a terminal output
718                    cx.notify();
719                    return None;
720                }
721                // Edge case note: a clear output marker
722                OutputContent::ClearOutputWaitMarker => {
723                    // Edge case note: a clear output marker is handled by the caller
724                    // since we will return a new output at the end here as a new terminal output
725                }
726                // A different output type is "in the way", so we need to create a new output,
727                // which is the same as having no prior output
728                _ => {}
729            }
730        }
731
732        let mut new_terminal = TerminalOutput::new(cx);
733        new_terminal.append_text(text);
734        Some(OutputContent::Stream(new_terminal))
735    }
736}
737
738impl Render for ExecutionView {
739    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
740        let status = match &self.status {
741            ExecutionStatus::ConnectingToKernel => Label::new("Connecting to kernel...")
742                .color(Color::Muted)
743                .into_any_element(),
744            ExecutionStatus::Executing => h_flex()
745                .gap_2()
746                .child(
747                    Icon::new(IconName::ArrowCircle)
748                        .size(IconSize::Small)
749                        .color(Color::Muted)
750                        .with_animation(
751                            "arrow-circle",
752                            Animation::new(Duration::from_secs(3)).repeat(),
753                            |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
754                        ),
755                )
756                .child(Label::new("Executing...").color(Color::Muted))
757                .into_any_element(),
758            ExecutionStatus::Finished => Icon::new(IconName::Check)
759                .size(IconSize::Small)
760                .into_any_element(),
761            ExecutionStatus::Unknown => Label::new("Unknown status")
762                .color(Color::Muted)
763                .into_any_element(),
764            ExecutionStatus::ShuttingDown => Label::new("Kernel shutting down...")
765                .color(Color::Muted)
766                .into_any_element(),
767            ExecutionStatus::Restarting => Label::new("Kernel restarting...")
768                .color(Color::Muted)
769                .into_any_element(),
770            ExecutionStatus::Shutdown => Label::new("Kernel shutdown")
771                .color(Color::Muted)
772                .into_any_element(),
773            ExecutionStatus::Queued => Label::new("Queued...")
774                .color(Color::Muted)
775                .into_any_element(),
776            ExecutionStatus::KernelErrored(error) => Label::new(format!("Kernel error: {}", error))
777                .color(Color::Error)
778                .into_any_element(),
779        };
780
781        if self.outputs.len() == 0 {
782            return v_flex()
783                .min_h(cx.line_height())
784                .justify_center()
785                .child(status)
786                .into_any_element();
787        }
788
789        div()
790            .w_full()
791            .children(self.outputs.iter().enumerate().map(|(index, output)| {
792                h_flex()
793                    .w_full()
794                    .items_start()
795                    .child(
796                        div().flex_1().child(
797                            output
798                                .content
799                                .render(cx)
800                                .unwrap_or_else(|| div().into_any_element()),
801                        ),
802                    )
803                    .when(output.has_clipboard_content(cx), |el| {
804                        let clipboard_content = output.clipboard_content(cx);
805
806                        el.child(
807                            div().pl_1().child(
808                                IconButton::new(
809                                    ElementId::Name(format!("copy-output-{}", index).into()),
810                                    IconName::Copy,
811                                )
812                                .style(ButtonStyle::Transparent)
813                                .tooltip(move |cx| Tooltip::text("Copy Output", cx))
814                                .on_click(cx.listener(
815                                    move |_, _, cx| {
816                                        if let Some(clipboard_content) = clipboard_content.as_ref()
817                                        {
818                                            cx.write_to_clipboard(clipboard_content.clone());
819                                            // todo!(): let the user know that the content was copied
820                                        }
821                                    },
822                                )),
823                            ),
824                        )
825                    })
826            }))
827            .children(match self.status {
828                ExecutionStatus::Executing => vec![status],
829                ExecutionStatus::Queued => vec![status],
830                _ => vec![],
831            })
832            .into_any_element()
833    }
834}