T18 Terminal: a real shell on a pseudo-terminal through alacritty_terminal 0.26, drawn cell by cell in the theme's ANSI colors, with keys, text input, selection, scrollback, links and regex find; replayed output; tabs, splits and toolbar with a shell selector; command blocks and history; AnsiText, LogViewer and ProcessList

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-09-26 01:27:44 -04:00
1 parent 601a29a374
commit 8374b77906
30 files changed
+3527 -12

No files matched your search

+196
View File
@@ -0,0 +1,196 @@
use std::path::PathBuf;
use ely_gpui_component::{
editor::{FindOptions, FindWidget},
forms::TextInput,
layout::{PaneGroup, PaneId},
navigation::{EditorTab, EditorTabs},
primitives::IconName,
terminal::{Launch, ShellChoice, ShellSelector, Terminal, TerminalToolbar},
theme::{ActiveTheme, Radius},
};
use gpui::{
App, AppContext as _, Axis, Entity, IntoElement, ParentElement, SharedString, Styled, Window,
div, prelude::*, px,
};
use crate::{
probe::probe,
ui::{keep, section, set},
};
/// A build's output as a terminal printed it, colors and all.
const BUILD: &str = "\x1b[1;32m Compiling\x1b[0m ely-gpui-component v0.1.0\r\n\x1b[1;33mwarning\x1b[0m\x1b[1m: unused variable: `spare`\x1b[0m\r\n \x1b[1;34m-->\x1b[0m src/main.rs:47:9\r\n\x1b[1;33mwarning\x1b[0m\x1b[1m: field `hint` is never read\x1b[0m\r\n \x1b[1;34m-->\x1b[0m src/palette.rs:12:5\r\n\x1b[1;32m Finished\x1b[0m `dev` profile [unoptimized + debuginfo] target(s) in 4.21s\r\n\x1b[1;32m Running\x1b[0m `target/debug/palette`\r\n\x1b[38;5;110m◆\x1b[0m 5 tints of accent, the last \x1b[38;2;232;243;252m██\x1b[0m #e8f3fc\r\n\x1b[2mdocs: https://github.com/leidianyayi/Ely-GPUI-Components\x1b[0m\r\n";
const SHELLS: [(&str, &str); 3] = [
("bash", "/bin/bash"),
("zsh", "/bin/zsh"),
("sh", "/bin/sh"),
];
/// A shell with no startup files, its prompt the folder's name, in this repository.
fn launch(shell: usize) -> Launch {
let (name, program) = SHELLS[shell];
let args = match name {
"bash" => vec!["--noprofile".into(), "--norc".into(), "-i".into()],
"zsh" => vec!["-f".into(), "-i".into()],
_ => vec!["-i".into()],
};
Launch {
program: Some((program.into(), args)),
cwd: Some(PathBuf::from(env!("CARGO_MANIFEST_DIR"))),
env: vec![
("PS1".into(), "\\W › ".into()),
("PROMPT".into(), "%1~ › ".into()),
("BASH_SILENCE_DEPRECATION_WARNING".into(), "1".into()),
],
}
}
/// A pane group of shells, and the shell each pane holds.
type Panes = (Entity<PaneGroup>, Entity<Vec<(PaneId, Entity<Terminal>)>>);
fn panes(shell: usize, window: &mut Window, cx: &mut App) -> Panes {
let sessions = window.use_keyed_state(("terminal-sessions", shell), cx, |_, _| Vec::new());
let held = sessions.clone();
let group = window.use_keyed_state(("terminal-panes", shell), cx, move |_, _| {
PaneGroup::new(move |pane, _, cx| session(&held, pane, shell, cx).into_any_element())
});
(group, sessions)
}
/// The shell pane `pane` holds, started the first time the pane draws.
fn session(
sessions: &Entity<Vec<(PaneId, Entity<Terminal>)>>,
pane: PaneId,
shell: usize,
cx: &mut App,
) -> Entity<Terminal> {
if let Some((_, terminal)) = sessions.read(cx).iter().find(|(id, _)| *id == pane) {
return terminal.clone();
}
let terminal = cx.new(|cx| Terminal::spawn(launch(shell), cx).expect("the shell starts"));
sessions.update(cx, |all, _| all.push((pane, terminal.clone())));
terminal
}
fn field(key: &'static str, text: &str, window: &mut Window, cx: &mut App) -> Entity<TextInput> {
let seed = text.to_string();
window.use_keyed_state(key, cx, move |window, cx| {
let mut field = TextInput::new(window, cx);
field.set_text(seed, cx);
field
})
}
pub fn live(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let tab = keep("terminal-tab", || SharedString::from("shell"), window, cx);
let shell = keep("terminal-shell-choice", || 0usize, window, cx);
let (now_tab, now_shell) = (tab.read(cx).clone(), *shell.read(cx));
let (group, sessions) = panes(now_shell, window, cx);
let build = window.use_keyed_state("terminal-build", cx, |_, cx| {
Terminal::replay(BUILD.as_bytes(), 92, 12, cx)
});
let query = field("terminal-find", "warning", window, cx);
let options = keep("terminal-options", FindOptions::default, window, cx);
let searched = keep(
"terminal-searched",
|| None::<(String, FindOptions)>,
window,
cx,
);
let wanted = (query.read(cx).text().to_string(), *options.read(cx));
let error = keep("terminal-find-error", || None::<SharedString>, window, cx);
if searched.read(cx).as_ref() != Some(&wanted) {
let found = build.update(cx, |build, cx| build.find(&wanted.0, wanted.1, cx));
set(&error, found.err().map(SharedString::from), cx);
set(&searched, Some(wanted.clone()), cx);
}
let (total, current) = build.read(cx).matches();
let focused = group.read(cx).focused();
let active = session(&sessions, focused, now_shell, cx);
let (pick, choose, splitter, clearer, stopper) = (
tab.clone(),
shell.clone(),
group.clone(),
active.clone(),
active.clone(),
);
let shells = ShellSelector::new(
"terminal-shells",
SHELLS.map(|(name, program)| ShellChoice {
name: name.into(),
program: program.into(),
}),
)
.default_shell(now_shell)
.on_pick(move |ix, _, cx| set(&choose, ix, cx));
let on_shell = now_tab.as_ref() == "shell";
let exit = active
.read(cx)
.exit_status()
.and_then(|status| status.code());
let toolbar = TerminalToolbar::new(
"terminal-toolbar",
format!("{} — Ely-GPUI-Components", SHELLS[now_shell].0),
exit,
)
.shells(shells)
.on_split(move |_, cx| {
splitter.update(cx, |group, cx| {
let pane = group.focused();
group.split(pane, Axis::Horizontal, cx);
})
})
.on_clear(move |_, cx| clearer.update(cx, |terminal, cx| terminal.clear(cx)))
.on_stop(move |_, cx| stopper.update(cx, |terminal, _| terminal.write(&b"\x03"[..])));
let (step, flip) = (build.clone(), options.clone());
let find = FindWidget::new("terminal-find-widget", &query, current, total)
.options(*options.read(cx))
.on_options(move |next, _, cx| set(&flip, next, cx))
.on_step(move |forward, _, cx| step.update(cx, |build, cx| build.step(forward, cx)));
let find = match error.read(cx).clone() {
Some(message) => find.error(message),
None => find,
};
let theme = cx.theme();
section(
"Terminal / TerminalTabs / TerminalSplit / TerminalToolbar / ShellSelector / TerminalSearch / TerminalLink",
"A real shell on a pseudo-terminal, drawn cell by cell in the theme's sixteen colors. Tabs hold shells and output; split for another shell beside it. Find lights every match through the scrollback. Cmd-press a link or a path:line.",
cx,
)
.child(
div()
.w(px(840.))
.rounded(theme.radius(Radius::Md))
.border_1()
.border_color(theme.colors.border)
.overflow_hidden()
.child(probe(
"terminal-tabs",
div().w(px(838.)).child(
EditorTabs::new(
"terminal-tabs",
[
EditorTab::new("shell", SHELLS[now_shell].0).icon(IconName::Terminal),
EditorTab::new("build", "cargo run").icon(IconName::FileText),
],
)
.selected(now_tab.clone())
.on_select(move |id, _, cx| set(&pick, id.clone(), cx)),
),
))
.children(on_shell.then_some(toolbar))
.child(probe(
"terminal",
div().w(px(838.)).h(px(300.)).relative().map(|body| {
if on_shell {
body.child(group)
} else {
body.child(build)
.child(div().absolute().top_1().right_4().child(find))
}
}),
)),
)
}