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}