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:
1 parent
601a29a374
commit
8374b77906
30 files changed
+3527
-12
No files matched your search
@@ -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,
|
||||
];
|
||||
|
||||
|
||||
@@ -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")),
|
||||
)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}),
|
||||
)),
|
||||
)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
Reference in new issue
Block a user