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
@@ -0,0 +1,206 @@
|
||||
use std::ops::Range;
|
||||
|
||||
use alacritty_terminal::{
|
||||
event::EventListener,
|
||||
grid::Dimensions,
|
||||
term::{Term, cell::Cell, cell::Flags, color::Colors, search::Match},
|
||||
vte::ansi::CursorShape,
|
||||
};
|
||||
use gpui::Hsla;
|
||||
|
||||
use super::colors::Ink;
|
||||
|
||||
/// How a run of cells paints.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub(crate) struct Style {
|
||||
pub fg: Hsla,
|
||||
/// None where the terminal's ground shows.
|
||||
pub bg: Option<Hsla>,
|
||||
pub bold: bool,
|
||||
pub italic: bool,
|
||||
pub underline: bool,
|
||||
pub strike: bool,
|
||||
}
|
||||
|
||||
/// One screen line: a char per cell, and style runs over its bytes.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub(crate) struct Row {
|
||||
pub text: String,
|
||||
pub runs: Vec<(usize, Style)>,
|
||||
}
|
||||
|
||||
/// Why cells are lit: selected, or a find match, the current one stronger.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum Lit {
|
||||
Selected,
|
||||
Found,
|
||||
Current,
|
||||
}
|
||||
|
||||
/// A terminal's screen as it paints.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub(crate) struct Frame {
|
||||
pub rows: Vec<Row>,
|
||||
/// Lit cells: the row, its columns, and why.
|
||||
pub lit: Vec<(usize, Range<usize>, Lit)>,
|
||||
/// The cursor's row, column and shape, when it shows.
|
||||
pub cursor: Option<(usize, usize, CursorShape)>,
|
||||
}
|
||||
|
||||
impl Frame {
|
||||
/// The screen's text, trailing spaces gone.
|
||||
pub(crate) fn text(&self) -> String {
|
||||
let lines: Vec<&str> = self.rows.iter().map(|row| row.text.trim_end()).collect();
|
||||
lines.join("\n").trim_end().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// The screen as it shows now, scrolled back or not, with `found` lit and match `current` stronger.
|
||||
pub(crate) fn frame<T: EventListener>(
|
||||
term: &Term<T>,
|
||||
ink: &Ink,
|
||||
found: &[Match],
|
||||
current: Option<usize>,
|
||||
) -> Frame {
|
||||
let content = term.renderable_content();
|
||||
let offset = content.display_offset as i32;
|
||||
let mut rows = vec![Row::default(); term.screen_lines()];
|
||||
let mut lit: Vec<(usize, Range<usize>, Lit)> = Vec::new();
|
||||
let (top, bottom) = (-offset, rows.len() as i32 - 1 - offset);
|
||||
let near: Vec<(usize, &Match)> = found
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, hit)| hit.end().line.0 >= top && hit.start().line.0 <= bottom)
|
||||
.collect();
|
||||
for indexed in content.display_iter {
|
||||
let (point, cell) = (indexed.point, indexed.cell);
|
||||
let row = (point.line.0 + offset) as usize;
|
||||
let spacer = cell
|
||||
.flags
|
||||
.intersects(Flags::WIDE_CHAR_SPACER | Flags::LEADING_WIDE_CHAR_SPACER);
|
||||
push(
|
||||
&mut rows[row],
|
||||
if spacer { ' ' } else { cell.c },
|
||||
style(cell, ink, content.colors),
|
||||
);
|
||||
let why = if content.selection.is_some_and(|range| range.contains(point)) {
|
||||
Some(Lit::Selected)
|
||||
} else {
|
||||
near.iter()
|
||||
.find(|(_, hit)| hit.contains(&point))
|
||||
.map(|(ix, _)| {
|
||||
if Some(*ix) == current {
|
||||
Lit::Current
|
||||
} else {
|
||||
Lit::Found
|
||||
}
|
||||
})
|
||||
};
|
||||
if let Some(why) = why {
|
||||
light(&mut lit, row, point.column.0, why);
|
||||
}
|
||||
}
|
||||
let at = content.cursor.point.line.0 + offset;
|
||||
let shows = content.cursor.shape != CursorShape::Hidden && (0..rows.len() as i32).contains(&at);
|
||||
let cursor = shows.then_some((
|
||||
at as usize,
|
||||
content.cursor.point.column.0,
|
||||
content.cursor.shape,
|
||||
));
|
||||
Frame { rows, lit, cursor }
|
||||
}
|
||||
|
||||
fn push(row: &mut Row, ch: char, style: Style) {
|
||||
let len = ch.len_utf8();
|
||||
row.text.push(ch);
|
||||
match row.runs.last_mut() {
|
||||
Some((bytes, last)) if *last == style => *bytes += len,
|
||||
_ => row.runs.push((len, style)),
|
||||
}
|
||||
}
|
||||
|
||||
fn light(lit: &mut Vec<(usize, Range<usize>, Lit)>, row: usize, column: usize, why: Lit) {
|
||||
match lit.last_mut() {
|
||||
Some((at, columns, last)) if *at == row && columns.end == column && *last == why => {
|
||||
columns.end += 1
|
||||
}
|
||||
_ => lit.push((row, column..column + 1, why)),
|
||||
}
|
||||
}
|
||||
|
||||
fn style(cell: &Cell, ink: &Ink, set: &Colors) -> Style {
|
||||
let flags = cell.flags;
|
||||
let (mut fg, mut bg) = (ink.resolve(cell.fg, set), ink.resolve(cell.bg, set));
|
||||
if flags.contains(Flags::INVERSE) {
|
||||
std::mem::swap(&mut fg, &mut bg);
|
||||
}
|
||||
if flags.contains(Flags::DIM) {
|
||||
fg = fg.opacity(0.7);
|
||||
}
|
||||
if flags.contains(Flags::HIDDEN) {
|
||||
fg = bg;
|
||||
}
|
||||
Style {
|
||||
fg,
|
||||
bg: (bg != ink.bg).then_some(bg),
|
||||
bold: flags.contains(Flags::BOLD),
|
||||
italic: flags.contains(Flags::ITALIC),
|
||||
underline: flags.intersects(Flags::ALL_UNDERLINES),
|
||||
strike: flags.contains(Flags::STRIKEOUT),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use alacritty_terminal::{
|
||||
event::VoidListener,
|
||||
term::{Config, test::TermSize},
|
||||
vte::ansi::Processor,
|
||||
};
|
||||
use gpui::{black, white};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn replayed(bytes: &[u8]) -> Term<VoidListener> {
|
||||
let mut term = Term::new(Config::default(), &TermSize::new(12, 3), VoidListener);
|
||||
let mut parser: Processor = Processor::new();
|
||||
parser.advance(&mut term, bytes);
|
||||
term
|
||||
}
|
||||
|
||||
fn ink() -> Ink {
|
||||
let mut ansi = [black(); 16];
|
||||
ansi[1] = gpui::red();
|
||||
Ink {
|
||||
ansi,
|
||||
fg: black(),
|
||||
bg: white(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cells_join_into_runs_of_one_style() {
|
||||
let term = replayed(b"\x1b[31mred\x1b[0m ok\r\n\x1b[7minv\x1b[0m");
|
||||
let shown = frame(&term, &ink(), &[], None);
|
||||
assert_eq!(shown.text(), "red ok\ninv");
|
||||
let (bytes, red) = shown.rows[0].runs[0];
|
||||
assert_eq!((bytes, red.fg, red.bg), (3, gpui::red(), None));
|
||||
let (_, inverse) = shown.rows[1].runs[0];
|
||||
assert_eq!(
|
||||
(inverse.fg, inverse.bg),
|
||||
(white(), Some(black())),
|
||||
"inverse swaps"
|
||||
);
|
||||
assert_eq!(shown.cursor, Some((1, 3, CursorShape::Block)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_wide_char_keeps_the_columns_after_it() {
|
||||
let term = replayed("中a".as_bytes());
|
||||
let shown = frame(&term, &ink(), &[], None);
|
||||
assert_eq!(
|
||||
shown.rows[0].text.chars().take(3).collect::<String>(),
|
||||
"中 a"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user