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
This commit is contained in:
1 parent
dd09d2802a
commit
7da1368cd9
30 files changed
+3527
-12
No files matched your search
@@ -0,0 +1,303 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
App, ElementId, Hsla, InteractiveElement, IntoElement, ParentElement, Pixels, RenderOnce,
|
||||
ScrollDelta, ScrollStrategy, ScrollWheelEvent, SharedString, Styled, UniformListScrollHandle,
|
||||
Window, div, prelude::*, uniform_list,
|
||||
};
|
||||
|
||||
use super::ansi::{AnsiText, printed};
|
||||
use crate::{
|
||||
buttons::{SegmentedControl, ToggleButton, ToggleItem},
|
||||
primitives::IconName,
|
||||
theme::{ActiveTheme, ControlSize, Palette, TextSize},
|
||||
typography::format,
|
||||
};
|
||||
|
||||
/// How loud a log line is, quietest first.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum LogLevel {
|
||||
Trace,
|
||||
Debug,
|
||||
Info,
|
||||
Warn,
|
||||
Error,
|
||||
}
|
||||
|
||||
const LEVELS: [LogLevel; 5] = [
|
||||
LogLevel::Trace,
|
||||
LogLevel::Debug,
|
||||
LogLevel::Info,
|
||||
LogLevel::Warn,
|
||||
LogLevel::Error,
|
||||
];
|
||||
|
||||
impl LogLevel {
|
||||
fn word(self) -> &'static str {
|
||||
match self {
|
||||
Self::Trace => "TRACE",
|
||||
Self::Debug => "DEBUG",
|
||||
Self::Info => "INFO",
|
||||
Self::Warn => "WARN",
|
||||
Self::Error => "ERROR",
|
||||
}
|
||||
}
|
||||
|
||||
fn color(self, colors: &Palette) -> Hsla {
|
||||
match self {
|
||||
Self::Trace | Self::Debug => colors.fg_subtle,
|
||||
Self::Info => colors.info,
|
||||
Self::Warn => colors.warning,
|
||||
Self::Error => colors.danger,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One line of a log: when, how loud, from where, and what it says, escape codes and all.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct LogLine {
|
||||
pub at: SharedString,
|
||||
pub level: LogLevel,
|
||||
pub target: SharedString,
|
||||
pub message: SharedString,
|
||||
}
|
||||
|
||||
/// The lines at or above `least` whose target or text holds `query`, case aside, by index.
|
||||
pub(crate) fn shown(lines: &[LogLine], least: LogLevel, query: &str) -> Vec<usize> {
|
||||
let query = query.trim().to_lowercase();
|
||||
lines
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, line)| line.level >= least)
|
||||
.filter(|(_, line)| {
|
||||
query.is_empty()
|
||||
|| line.target.to_lowercase().contains(&query)
|
||||
|| printed(&line.message).0.to_lowercase().contains(&query)
|
||||
})
|
||||
.map(|(ix, _)| ix)
|
||||
.collect()
|
||||
}
|
||||
|
||||
type OnLevel = Rc<dyn Fn(LogLevel, &mut Window, &mut App)>;
|
||||
type OnBool = Rc<dyn Fn(bool, &mut Window, &mut App)>;
|
||||
|
||||
/// A log, newest at the bottom: from a chosen level up, found by a query, following new lines until a scroll up lets go. Long logs draw only the lines in view.
|
||||
#[derive(IntoElement)]
|
||||
pub struct LogViewer {
|
||||
id: ElementId,
|
||||
lines: Rc<Vec<LogLine>>,
|
||||
least: LogLevel,
|
||||
query: SharedString,
|
||||
follow: bool,
|
||||
on_least: Option<OnLevel>,
|
||||
on_follow: Option<OnBool>,
|
||||
}
|
||||
|
||||
impl LogViewer {
|
||||
pub fn new(id: impl Into<ElementId>, lines: impl Into<Rc<Vec<LogLine>>>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
lines: lines.into(),
|
||||
least: LogLevel::Trace,
|
||||
query: SharedString::default(),
|
||||
follow: true,
|
||||
on_least: None,
|
||||
on_follow: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The quietest level shown.
|
||||
pub fn least(mut self, least: LogLevel) -> Self {
|
||||
self.least = least;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn query(mut self, query: impl Into<SharedString>) -> Self {
|
||||
self.query = query.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn follow(mut self, follow: bool) -> Self {
|
||||
self.follow = follow;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_least(mut self, handler: impl Fn(LogLevel, &mut Window, &mut App) + 'static) -> Self {
|
||||
self.on_least = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_follow(mut self, handler: impl Fn(bool, &mut Window, &mut App) + 'static) -> Self {
|
||||
self.on_follow = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for LogViewer {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let scroll = window
|
||||
.use_keyed_state((self.id.clone(), "scroll"), cx, |_, _| {
|
||||
UniformListScrollHandle::new()
|
||||
})
|
||||
.read(cx)
|
||||
.clone();
|
||||
let theme = cx.theme();
|
||||
let colors = theme.colors.clone();
|
||||
let rows = Rc::new(shown(&self.lines, self.least, &self.query));
|
||||
if self.follow && !rows.is_empty() {
|
||||
scroll.scroll_to_item(rows.len() - 1, ScrollStrategy::Bottom);
|
||||
}
|
||||
let levels = LEVELS.iter().fold(
|
||||
SegmentedControl::new((self.id.clone(), "levels"), self.least.word())
|
||||
.size(ControlSize::Sm),
|
||||
|control, level| control.segment(level.word(), level.word(), None),
|
||||
);
|
||||
let levels = match self.on_least {
|
||||
Some(on_least) => levels.on_change(move |word, window, cx| {
|
||||
let level = LEVELS
|
||||
.into_iter()
|
||||
.find(|level| level.word() == word.as_ref())
|
||||
.expect("a segment names a level");
|
||||
on_least(level, window, cx)
|
||||
}),
|
||||
None => levels,
|
||||
};
|
||||
let follow = ToggleButton::new(
|
||||
(self.id.clone(), "follow"),
|
||||
ToggleItem::new("follow")
|
||||
.icon(IconName::ArrowDownToLine)
|
||||
.tooltip("Follow new lines"),
|
||||
self.follow,
|
||||
)
|
||||
.size(ControlSize::Sm);
|
||||
let (release, follow) = match self.on_follow {
|
||||
Some(on_follow) => (
|
||||
Some(on_follow.clone()),
|
||||
follow.on_toggle(move |on, window, cx| on_follow(on, window, cx)),
|
||||
),
|
||||
None => (None, follow),
|
||||
};
|
||||
let (lines, painted) = (self.lines.clone(), rows.clone());
|
||||
let list = uniform_list(
|
||||
(self.id.clone(), "lines"),
|
||||
rows.len(),
|
||||
move |range, _, cx| {
|
||||
let colors = cx.theme().colors.clone();
|
||||
range
|
||||
.map(|ix| {
|
||||
let line = &lines[painted[ix]];
|
||||
div()
|
||||
.id(ix)
|
||||
.w_full()
|
||||
.flex()
|
||||
.gap_3()
|
||||
.px_2()
|
||||
.whitespace_nowrap()
|
||||
.overflow_hidden()
|
||||
.when(line.level == LogLevel::Error, |row| {
|
||||
row.bg(colors.danger.opacity(0.06))
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.text_color(colors.fg_subtle)
|
||||
.child(line.at.clone()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.w_12()
|
||||
.text_color(line.level.color(&colors))
|
||||
.child(line.level.word()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.w_24()
|
||||
.overflow_hidden()
|
||||
.text_color(colors.fg_muted)
|
||||
.child(line.target.clone()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.child(AnsiText::new(line.message.clone())),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
)
|
||||
.track_scroll(scroll)
|
||||
.size_full();
|
||||
div()
|
||||
.size_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.text_size(theme.text_size(TextSize::Sm))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.pb_2()
|
||||
.child(levels)
|
||||
.child(div().flex_1().text_color(colors.fg_subtle).child(format!(
|
||||
"{} of {}",
|
||||
rows.len(),
|
||||
format::plural(self.lines.len() as u64, "line", "lines")
|
||||
)))
|
||||
.child(follow),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id((self.id.clone(), "body"))
|
||||
.flex_1()
|
||||
.min_h_0()
|
||||
.font_family(theme.mono_family.clone())
|
||||
.text_size(theme.text_size(TextSize::Xs))
|
||||
.when_some(release.filter(|_| self.follow), |body, on_follow| {
|
||||
body.on_scroll_wheel(move |event: &ScrollWheelEvent, window, cx| {
|
||||
let back = match event.delta {
|
||||
ScrollDelta::Pixels(delta) => delta.y > Pixels::ZERO,
|
||||
ScrollDelta::Lines(delta) => delta.y > 0.0,
|
||||
};
|
||||
if back {
|
||||
on_follow(false, window, cx)
|
||||
}
|
||||
})
|
||||
})
|
||||
.child(list),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn line(level: LogLevel, target: &str, message: &str) -> LogLine {
|
||||
LogLine {
|
||||
at: "12:00:00".into(),
|
||||
level,
|
||||
target: target.to_string().into(),
|
||||
message: message.to_string().into(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lines_show_from_a_level_up_and_by_query() {
|
||||
let lines = [
|
||||
line(LogLevel::Debug, "gpui", "frame"),
|
||||
line(LogLevel::Info, "ely::terminal", "started \x1b[1mzsh\x1b[0m"),
|
||||
line(LogLevel::Error, "ely::pty", "the program ended"),
|
||||
];
|
||||
assert_eq!(shown(&lines, LogLevel::Info, ""), [1, 2]);
|
||||
assert_eq!(
|
||||
shown(&lines, LogLevel::Trace, "ZSH"),
|
||||
[1],
|
||||
"codes and case aside"
|
||||
);
|
||||
assert_eq!(shown(&lines, LogLevel::Trace, "pty"), [2], "targets count");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user