Skip to main content

ui/components/
terminal_panel.rs

1use gpui::{AnyElement, Context, Render, rgb};
2
3use crate::prelude::*;
4
5/// Monospace font used for terminal output (matches [`crate::CodeEditor`]'s
6/// `CODE_FONT_FAMILY` so panes in the same [`crate::PaneGroup`] look
7/// consistent).
8const TERMINAL_FONT_FAMILY: &str = "IBM Plex Mono";
9const TERMINAL_FONT_SIZE: Pixels = px(12.5);
10const TERMINAL_LINE_HEIGHT: f32 = 1.5;
11
12/// Chrome-only terminal panel: renders whatever text is set via
13/// [`Self::set_output`]/[`Self::append_output`] in a monospace block with a
14/// dark terminal-style background. There is NO real process behind this —
15/// no PTY, no shell spawn, no ANSI escape-sequence parsing. That is Phase C
16/// (`plans/20260705-1722-zed-ui-component-enrichment/phase-03-*.md`), which
17/// requires `alacritty_terminal` + platform PTY syscalls routed through
18/// `gpui_platform` and is gated on the user accepting that dependency and
19/// the cross-platform risk it carries (see the plan's Unresolved Questions).
20/// This exists purely so a `PaneGroup` layout can be composed and reviewed
21/// before Phase C lands.
22///
23/// Stateful view — create with `cx.new(|_| TerminalPanel::new())` and store
24/// the resulting `Entity<TerminalPanel>`.
25pub struct TerminalPanel {
26    output: SharedString,
27}
28
29impl TerminalPanel {
30    pub fn new() -> Self {
31        Self {
32            output: SharedString::default(),
33        }
34    }
35
36    /// Replaces the displayed output entirely.
37    pub fn set_output(&mut self, output: impl Into<SharedString>, cx: &mut Context<Self>) {
38        self.output = output.into();
39        cx.notify();
40    }
41
42    /// Appends a line to the displayed output (e.g. echoing a command a
43    /// caller wants to simulate having "run").
44    pub fn append_output(&mut self, line: impl AsRef<str>, cx: &mut Context<Self>) {
45        let mut output = self.output.to_string();
46        if !output.is_empty() {
47            output.push('\n');
48        }
49        output.push_str(line.as_ref());
50        self.output = output.into();
51        cx.notify();
52    }
53}
54
55impl Default for TerminalPanel {
56    fn default() -> Self {
57        Self::new()
58    }
59}
60
61impl Render for TerminalPanel {
62    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
63        div()
64            .id(("terminal-panel", cx.entity_id()))
65            .w_full()
66            .h_full()
67            .overflow_y_scroll()
68            .p_3()
69            .bg(rgb(0x0D1117))
70            .text_color(rgb(0xC9D1D9))
71            .font_family(TERMINAL_FONT_FAMILY)
72            .text_size(TERMINAL_FONT_SIZE)
73            .line_height(relative(TERMINAL_LINE_HEIGHT))
74            .child(if self.output.is_empty() {
75                div()
76                    .text_color(rgb(0x545D68))
77                    .child("No output.")
78                    .into_any_element()
79            } else {
80                self.output.clone().into_any_element()
81            })
82    }
83}
84
85/// Standalone gallery preview for `TerminalPanel` (not registered in the
86/// `Component` catalog since it is a stateful `Entity`, matching
87/// `CodeEditor`/`SearchInput`'s existing convention in this crate).
88pub fn terminal_panel_preview(_window: &mut Window, cx: &mut App) -> AnyElement {
89    div()
90        .h(px(200.))
91        .rounded_lg()
92        .overflow_hidden()
93        .child(cx.new(|cx| {
94            let mut panel = TerminalPanel::new();
95            panel.set_output(
96                "$ echo \"chrome only — no PTY yet\"\nchrome only — no PTY yet\n$ ",
97                cx,
98            );
99            panel
100        }))
101        .into_any_element()
102}