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

+2
View File
@@ -14,6 +14,7 @@ mod overlays;
mod primitives;
mod shell;
mod tables;
mod terminal;
mod theme;
mod typography;
@@ -49,6 +50,7 @@ pub const ALL: &[Page] = &[
charts::PAGE,
finance::PAGE,
editor::PAGE,
terminal::PAGE,
theme::PAGE,
];
+124
View File
@@ -0,0 +1,124 @@
use std::time::Duration;
use ely_gpui_component::{
buttons::Button,
terminal::{AnsiText, CommandBlock, CommandHistory, CommandState, PastCommand},
typography::Caption,
};
use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
use crate::ui::{keep, row, section, set};
const TESTS: &str = "running 5 tests\r\ntest palette::lookup_blends_toward_white ... \x1b[32mok\x1b[0m\r\ntest palette::unknown_names_fall_back ... \x1b[32mok\x1b[0m\r\n\r\ntest result: \x1b[32mok\x1b[0m. 5 passed; 0 failed; finished in 0.02s";
const FAILED: &str = "\x1b[1;31merror[E0308]\x1b[0m\x1b[1m: mismatched types\x1b[0m\r\n \x1b[1;34m-->\x1b[0m src/main.rs:66:21\r\n \x1b[1;34m|\x1b[0m\r\n\x1b[1;34m66\x1b[0m \x1b[1;34m|\x1b[0m let count: u32 = tints.len();\r\n \x1b[1;34m|\x1b[0m \x1b[1;34m---\x1b[0m \x1b[1;31m^^^^^^^^^^^\x1b[0m \x1b[1;31mexpected `u32`, found `usize`\x1b[0m";
fn history() -> Vec<PastCommand> {
let past = |command: &str, cwd: &str, when: &str, code| PastCommand {
command: command.to_string().into(),
cwd: cwd.to_string().into(),
when: when.to_string().into(),
code,
};
vec![
past("cargo test --lib palette", "~/palette", "just now", Some(0)),
past("cargo build", "~/palette", "2 min ago", Some(101)),
past("git status --short", "~/palette", "5 min ago", Some(0)),
past("rg lookup src", "~/palette", "12 min ago", Some(0)),
past(
"cargo run --example gallery",
"~/Ely-GPUI-Components",
"1 h ago",
None,
),
past(
"git log --oneline -5",
"~/Ely-GPUI-Components",
"yesterday",
Some(0),
),
]
}
pub fn blocks(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let folded = keep("terminal-folded", || false, window, cx);
let open = keep("terminal-history", || false, window, cx);
let said = keep("terminal-said", || None::<SharedString>, window, cx);
let (now_folded, now_open, told) = (*folded.read(cx), *open.read(cx), said.read(cx).clone());
let (fold, opener, close, pick, rerun) = (
folded.clone(),
open.clone(),
open.clone(),
said.clone(),
said.clone(),
);
section(
"CommandBlock / CommandHistory",
"Each command a block: where it ran, how it ended and how long it took, its output folding away; copy it or run it again. Past commands open as a palette, found by any of their letters.",
cx,
)
.child(
div()
.w(px(840.))
.flex()
.flex_col()
.gap_3()
.child(CommandBlock::new(
"terminal-block-tests",
"~/palette",
"cargo test --lib palette",
TESTS,
CommandState::Done {
code: 0,
took: Duration::from_secs(3),
},
))
.child(
CommandBlock::new(
"terminal-block-build",
"~/palette",
"cargo build",
FAILED,
CommandState::Done {
code: 101,
took: Duration::from_secs(9),
},
)
.folded(now_folded)
.on_fold(move |folded, _, cx| set(&fold, folded, cx))
.on_rerun(move |_, cx| set(&rerun, Some("Ran cargo build again.".into()), cx)),
)
.child(CommandBlock::new(
"terminal-block-running",
"~/Ely-GPUI-Components",
"cargo run --example gallery",
"",
CommandState::Running,
)),
)
.child(row().child(Button::new("terminal-history-open", "Search history…").on_click(move |_, _, cx| set(&opener, true, cx))))
.children(told.map(Caption::new))
.children(now_open.then(|| {
CommandHistory::new("terminal-history-palette", history(), move |_, cx| set(&close, false, cx))
.on_pick(move |command, _, cx| set(&pick, Some(format!("Ran {command}.").into()), cx))
}))
}
pub fn ansi(cx: &mut App) -> impl IntoElement + use<> {
section(
"AnsiText",
"Text colored by the escape codes in it, as a terminal prints it: the sixteen theme colors, bold, dim, italic, underline, inverse, the 256-color cube and true color. Other codes drop away.",
cx,
)
.child(
div()
.w(px(840.))
.flex()
.flex_col()
.gap_1()
.child(AnsiText::new("\x1b[30m■\x1b[31m■\x1b[32m■\x1b[33m■\x1b[34m■\x1b[35m■\x1b[36m■\x1b[37m■ \x1b[90m■\x1b[91m■\x1b[92m■\x1b[93m■\x1b[94m■\x1b[95m■\x1b[96m■\x1b[97m■\x1b[0m the sixteen, dark and bright"))
.child(AnsiText::new("\x1b[1mbold\x1b[0m \x1b[2mdim\x1b[0m \x1b[3mitalic\x1b[0m \x1b[4munderline\x1b[0m \x1b[9mstruck\x1b[0m \x1b[7m inverse \x1b[0m"))
.child(AnsiText::new("\x1b[38;5;24m██\x1b[38;5;30m██\x1b[38;5;36m██\x1b[38;5;72m██\x1b[38;5;108m██\x1b[38;5;144m██\x1b[38;5;180m██\x1b[38;5;216m██\x1b[0m the cube \x1b[38;2;81;130;193m██\x1b[38;2;0;149;137m██\x1b[38;2;170;115;43m██\x1b[0m true color"))
.child(AnsiText::new("\x1b[42;30m PASS \x1b[0m palette \x1b[41;97m FAIL \x1b[0m tokens \x1b[43;30m SKIP \x1b[0m motion")),
)
}
+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))
}
}),
)),
)
}
+44
View File
@@ -0,0 +1,44 @@
mod blocks;
mod live;
mod panels;
use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div};
use super::Page;
use crate::script::Step;
pub const PAGE: Page = Page {
number: 18,
slug: "terminal",
title: "Terminal",
summary: "A real shell drawn cell by cell, with tabs, splits, find and links; command blocks and history; colored output, logs and processes.",
render,
script: SCRIPT,
};
const SCRIPT: &[Step] = &[
Step::Rest,
Step::DownAt("terminal", 200.0, 150.0),
Step::UpAt("terminal", 200.0, 150.0),
Step::Wait(400),
Step::Type("ls -G src"),
Step::Key("enter"),
Step::Wait(800),
Step::Shot("shell"),
Step::Key("ctrl-l"),
Step::DownAt("terminal-tabs", 155.0, 16.0),
Step::UpAt("terminal-tabs", 155.0, 16.0),
Step::Wait(400),
Step::Shot("find"),
Step::Rest,
];
fn render(window: &mut Window, cx: &mut App) -> AnyElement {
div()
.child(live::live(window, cx))
.child(blocks::blocks(window, cx))
.child(blocks::ansi(cx))
.child(panels::logs(window, cx))
.child(panels::process_list(window, cx))
.into_any_element()
}
+163
View File
@@ -0,0 +1,163 @@
use std::rc::Rc;
use ely_gpui_component::{
forms::{Input, TextInput},
terminal::{LogLevel, LogLine, LogViewer, Process, ProcessList},
theme::{ActiveTheme, Radius},
typography::Caption,
};
use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
use crate::ui::{keep, section, set};
/// Two hundred lines of a service's day, the same each time.
fn log() -> Rc<Vec<LogLine>> {
let targets = [
"ely::shell",
"ely::pty",
"gpui::window",
"ely::store",
"net::http",
];
let messages = [
(LogLevel::Debug, "frame drawn in 4.1 ms"),
(LogLevel::Info, "window opened at 1280×820"),
(LogLevel::Trace, "key down: cmd-k"),
(LogLevel::Info, "\x1b[1mzsh\x1b[0m started, pid 4821"),
(LogLevel::Warn, "slow frame: 38 ms, over budget"),
(LogLevel::Debug, "layout cached: 212 nodes"),
(LogLevel::Info, "GET /api/quotes \x1b[32m200\x1b[0m 18 ms"),
(
LogLevel::Error,
"GET /api/orders \x1b[31m503\x1b[0m retry in 2 s",
),
(LogLevel::Info, "saved workspace, 3 panes"),
];
Rc::new(
(0..200)
.map(|ix: usize| {
let (level, message) = messages[(ix * 7 + ix / 3) % messages.len()];
LogLine {
at: format!(
"09:{:02}:{:02}.{:03}",
ix / 60 % 60,
ix % 60,
ix * 37 % 1000
)
.into(),
level,
target: targets[ix * 3 % targets.len()].into(),
message: message.into(),
}
})
.collect(),
)
}
fn field(
key: &'static str,
placeholder: &'static str,
window: &mut Window,
cx: &mut App,
) -> gpui::Entity<TextInput> {
window.use_keyed_state(key, cx, move |window, cx| {
TextInput::new(window, cx).placeholder(placeholder)
})
}
pub fn logs(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let lines = keep("terminal-log", log, window, cx);
let least = keep("terminal-least", || LogLevel::Debug, window, cx);
let follow = keep("terminal-follow", || true, window, cx);
let query = field("terminal-log-query", "Filter lines", window, cx);
let (now_lines, now_least, now_follow) =
(lines.read(cx).clone(), *least.read(cx), *follow.read(cx));
let text = query.read(cx).text().to_string();
let (pick, keep_up) = (least.clone(), follow.clone());
let theme = cx.theme();
section(
"LogViewer",
"A log from a chosen level up, filtered as you type, following new lines until a scroll up lets go. Long logs draw only the lines in view.",
cx,
)
.child(div().w(px(320.)).child(Input::new(&query)))
.child(
div()
.w(px(840.))
.h(px(280.))
.p_2()
.rounded(theme.radius(Radius::Md))
.border_1()
.border_color(theme.colors.border)
.child(
LogViewer::new("terminal-log-viewer", now_lines)
.least(now_least)
.query(text)
.follow(now_follow)
.on_least(move |level, _, cx| set(&pick, level, cx))
.on_follow(move |on, _, cx| set(&keep_up, on, cx)),
),
)
}
fn processes() -> Vec<Process> {
let process = |pid, name: &str, user: &str, cpu, memory_mb: u64| Process {
pid,
name: name.to_string().into(),
user: user.to_string().into(),
cpu,
memory: memory_mb * 1024 * 1024,
};
vec![
process(4821, "zsh", "ely", 0.2, 6),
process(512, "WindowServer", "_windowserver", 14.6, 412),
process(9001, "gallery", "ely", 23.4, 188),
process(733, "rust-analyzer", "ely", 41.8, 1_240),
process(88, "launchd", "root", 0.1, 12),
process(2210, "cargo", "ely", 6.3, 96),
process(3302, "Safari", "ely", 8.9, 734),
process(3303, "Safari Networking", "ely", 1.4, 88),
process(401, "mds_stores", "root", 3.2, 140),
process(6120, "codex", "ely", 2.7, 310),
]
}
pub fn process_list(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let running = keep("terminal-processes", processes, window, cx);
let chosen = keep("terminal-chosen", Vec::<u32>::new, window, cx);
let said = keep("terminal-ended", || None::<SharedString>, window, cx);
let query = field("terminal-process-query", "Find a process", window, cx);
let (now, picked, told) = (
running.read(cx).clone(),
chosen.read(cx).clone(),
said.read(cx).clone(),
);
let text = query.read(cx).text().to_string();
let (select, end, clear, tell) = (
chosen.clone(),
running.clone(),
chosen.clone(),
said.clone(),
);
section(
"ProcessList",
"Running processes, busiest first: found by a query, sorted by any column, and the selected ones ended.",
cx,
)
.child(div().w(px(320.)).child(Input::new(&query)))
.child(
div().w(px(840.)).child(
ProcessList::new("terminal-process-list", now)
.query(text)
.selected(picked)
.on_select(move |pids, _, cx| set(&select, pids, cx))
.on_end(move |pids, _, cx| {
let left: Vec<Process> = end.read(cx).iter().filter(|process| !pids.contains(&process.pid)).cloned().collect();
set(&end, left, cx);
set(&clear, Vec::new(), cx);
set(&tell, Some(format!("Ended {} process{}.", pids.len(), if pids.len() == 1 { "" } else { "es" }).into()), cx);
}),
),
)
.children(told.map(Caption::new))
}