feat: make terminal the primary workspace surface
CI / rust (push) Successful in 2m7s

This commit is contained in:
2026-08-31 17:18:55 -07:00
parent b838d000db
commit 1204689de0
11 changed files with 643 additions and 157 deletions
+3 -2
View File
@@ -23,8 +23,9 @@ GPUI also has an integration mode with one real local PTY owned by
shared model retain the all-deterministic mode for like-for-like framework
comparison. GPUI feeds raw output through `lumbridge-terminal` and sends encoded
keyboard input and terminal protocol replies through the bounded runtime actor.
Its visual adapter currently flattens the VT snapshot to plain rows, so styled
cells, selection, and cursor painting remain intentionally unfinished.
Its visual adapter coalesces VT cells into native styled runs, paints cursor
shapes, and resizes the engine and PTY from the middle 60% work band. Selection,
scrollback navigation, and mouse input remain intentionally unfinished.
Build independently:
+552 -121
View File
@@ -2,8 +2,8 @@ use std::collections::VecDeque;
use std::time::{Duration, Instant};
use gpui::{
App, Application, Bounds, Context, FocusHandle, KeyBinding, KeyDownEvent, Window, WindowBounds,
WindowOptions, actions, div, prelude::*, px, rgb, size,
App, Application, Bounds, Context, FocusHandle, FontWeight, KeyBinding, KeyDownEvent, Pixels,
Size, Window, WindowBounds, WindowOptions, actions, div, prelude::*, px, relative, rgb, size,
};
use lumbridge_runtime::{
CommandConfig, PtyOptions, RuntimeActor, RuntimeActorError, RuntimeActorOptions,
@@ -14,8 +14,9 @@ use lumbridge_spike_model::{
ShellModel, SurfaceKind, WORKSPACES,
};
use lumbridge_terminal::{
KeyModifiers, TerminalDimensions, TerminalEngine, TerminalEngineOptions, TerminalKey,
TerminalKeyEvent,
KeyModifiers, TerminalCellStyle, TerminalColor, TerminalCursorShape, TerminalDimensions,
TerminalEngine, TerminalEngineOptions, TerminalKey, TerminalKeyEvent, TerminalNamedColor,
TerminalSnapshot,
};
const BG: u32 = 0x090c12;
@@ -34,6 +35,14 @@ const RUNTIME_POLL_INTERVAL: Duration = Duration::from_millis(16);
const RUNTIME_DRAIN_LIMIT: usize = 64;
const LIVE_PANE: PaneId = PaneId::CodexRuntime;
const LIVE_PTY_SCRIPT: &str = "printf 'Lumbridge interactive PTY · type here\\n'; exec /bin/sh -i";
const SIDEBAR_WIDTH: f32 = 248.0;
const APP_HEADER_HEIGHT: f32 = 48.0;
const TAB_BAR_HEIGHT: f32 = 38.0;
const APP_FOOTER_HEIGHT: f32 = 32.0;
const TERMINAL_CHROME_HEIGHT: f32 = 72.0;
const TERMINAL_HORIZONTAL_INSET: f32 = 32.0;
const TERMINAL_CELL_WIDTH: f32 = 8.4;
const TERMINAL_CELL_HEIGHT: f32 = 18.0;
const TERMINAL_ROW_STEP: u16 = 2;
const TERMINAL_COLUMN_STEP: u16 = 10;
@@ -65,6 +74,7 @@ struct LumbridgeShell {
runtime: Option<RuntimeActor>,
runtime_status: LiveRuntimeStatus,
terminal: TerminalEngine,
terminal_snapshot: TerminalSnapshot,
last_runtime_sequence: u64,
root_focus: FocusHandle,
pane_focus: [FocusHandle; 6],
@@ -123,6 +133,171 @@ struct RenderTiming {
dispatch_to_element_micros: VecDeque<u128>,
}
#[derive(Clone, Eq, PartialEq)]
struct TerminalPaintRun {
text: String,
columns: u16,
foreground: u32,
background: u32,
style: TerminalCellStyle,
cursor: Option<TerminalCursorShape>,
hyperlink: bool,
}
impl TerminalPaintRun {
fn can_merge(&self, other: &Self) -> bool {
self.foreground == other.foreground
&& self.background == other.background
&& self.style == other.style
&& self.cursor == other.cursor
&& self.hyperlink == other.hyperlink
}
}
fn terminal_paint_rows(snapshot: &TerminalSnapshot) -> Vec<Vec<TerminalPaintRun>> {
(0..snapshot.dimensions.rows())
.map(|row| {
let mut runs: Vec<TerminalPaintRun> = Vec::new();
for column in 0..snapshot.dimensions.columns() {
let Some(cell) = snapshot.cell(row, column) else {
continue;
};
if cell.wide_spacer {
continue;
}
let mut foreground = terminal_color(cell.foreground);
let mut background = terminal_color(cell.background);
if cell.style.contains(TerminalCellStyle::INVERSE) {
std::mem::swap(&mut foreground, &mut background);
}
if cell.style.contains(TerminalCellStyle::DIM) {
foreground = dim_color(foreground);
}
if cell.style.contains(TerminalCellStyle::HIDDEN) {
foreground = background;
}
let cursor = (snapshot.cursor.row == row
&& snapshot.cursor.column == column
&& snapshot.cursor.shape != TerminalCursorShape::Hidden)
.then_some(snapshot.cursor.shape);
let text = if cell.text.is_empty() {
" ".to_owned()
} else {
cell.text.clone()
};
let next = TerminalPaintRun {
text,
columns: if cell.wide { 2 } else { 1 },
foreground,
background,
style: cell.style,
cursor,
hyperlink: cell.hyperlink.is_some(),
};
if let Some(current) = runs.last_mut()
&& current.can_merge(&next)
{
current.text.push_str(&next.text);
current.columns = current.columns.saturating_add(next.columns);
continue;
}
runs.push(next);
}
runs
})
.collect()
}
fn terminal_color(color: TerminalColor) -> u32 {
match color {
TerminalColor::Rgb { red, green, blue } => {
(u32::from(red) << 16) | (u32::from(green) << 8) | u32::from(blue)
}
TerminalColor::Indexed(index) => indexed_terminal_color(index),
TerminalColor::Named(named) => match named {
TerminalNamedColor::Black => 0x1d2430,
TerminalNamedColor::Red => 0xff6b6b,
TerminalNamedColor::Green => 0x70d6a8,
TerminalNamedColor::Yellow => 0xf1c76a,
TerminalNamedColor::Blue => 0x68b5f8,
TerminalNamedColor::Magenta => 0xc79bf2,
TerminalNamedColor::Cyan => 0x63d5da,
TerminalNamedColor::White => 0xdbe5f4,
TerminalNamedColor::BrightBlack => 0x6d7a91,
TerminalNamedColor::BrightRed => 0xff8b8b,
TerminalNamedColor::BrightGreen => 0x93e6be,
TerminalNamedColor::BrightYellow => 0xf8d98c,
TerminalNamedColor::BrightBlue => 0x8bc8ff,
TerminalNamedColor::BrightMagenta => 0xd9b4fb,
TerminalNamedColor::BrightCyan => 0x86e7eb,
TerminalNamedColor::BrightWhite | TerminalNamedColor::BrightForeground => 0xf4f8ff,
TerminalNamedColor::Foreground => TEXT,
TerminalNamedColor::Background => BG,
TerminalNamedColor::Cursor => 0xf4f8ff,
TerminalNamedColor::DimBlack => 0x121820,
TerminalNamedColor::DimRed => 0x9f4a4a,
TerminalNamedColor::DimGreen => 0x4e9878,
TerminalNamedColor::DimYellow => 0xa88a49,
TerminalNamedColor::DimBlue => 0x477fac,
TerminalNamedColor::DimMagenta => 0x896ca4,
TerminalNamedColor::DimCyan => 0x459397,
TerminalNamedColor::DimWhite | TerminalNamedColor::DimForeground => MUTED,
},
}
}
fn indexed_terminal_color(index: u8) -> u32 {
const ANSI: [u32; 16] = [
0x1d2430, 0xff6b6b, 0x70d6a8, 0xf1c76a, 0x68b5f8, 0xc79bf2, 0x63d5da, 0xdbe5f4, 0x6d7a91,
0xff8b8b, 0x93e6be, 0xf8d98c, 0x8bc8ff, 0xd9b4fb, 0x86e7eb, 0xf4f8ff,
];
match index {
0..=15 => ANSI[usize::from(index)],
16..=231 => {
const LEVELS: [u32; 6] = [0, 95, 135, 175, 215, 255];
let offset = u32::from(index - 16);
let red = LEVELS[(offset / 36) as usize];
let green = LEVELS[((offset % 36) / 6) as usize];
let blue = LEVELS[(offset % 6) as usize];
(red << 16) | (green << 8) | blue
}
232..=255 => {
let level = 8 + 10 * u32::from(index - 232);
(level << 16) | (level << 8) | level
}
}
}
fn dim_color(color: u32) -> u32 {
let dim = |channel: u32| channel * 13 / 20;
(dim((color >> 16) & 0xff) << 16) | (dim((color >> 8) & 0xff) << 8) | dim(color & 0xff)
}
fn terminal_dimensions_for_window(window_size: Size<Pixels>) -> TerminalDimensions {
let width = f32::from(window_size.width);
let height = f32::from(window_size.height);
let workspace_height =
(height - APP_HEADER_HEIGHT - TAB_BAR_HEIGHT - APP_FOOTER_HEIGHT).max(0.0);
let terminal_height = (workspace_height * 0.60 - TERMINAL_CHROME_HEIGHT).max(0.0);
let terminal_width = (width - SIDEBAR_WIDTH - TERMINAL_HORIZONTAL_INSET).max(0.0);
let rows = (terminal_height / TERMINAL_CELL_HEIGHT)
.floor()
.clamp(2.0, f32::from(u16::MAX));
let columns = (terminal_width / TERMINAL_CELL_WIDTH)
.floor()
.clamp(20.0, f32::from(u16::MAX));
TerminalDimensions::with_cell_size(
rows as u16,
columns as u16,
TERMINAL_CELL_WIDTH.round() as u16,
TERMINAL_CELL_HEIGHT.round() as u16,
)
.expect("geometry clamps terminal dimensions above zero")
}
impl RenderTiming {
fn mark_dispatch(&mut self) {
self.pending_dispatch = Some(Instant::now());
@@ -207,12 +382,23 @@ impl LumbridgeShell {
})
.detach();
let terminal = TerminalEngine::new(TerminalEngineOptions::default());
let terminal_dimensions = terminal_dimensions_for_window(window.bounds().size);
let terminal = TerminalEngine::new(TerminalEngineOptions {
dimensions: terminal_dimensions,
..TerminalEngineOptions::default()
});
let terminal_snapshot = terminal.snapshot();
let (runtime, runtime_status) = match start_live_runtime(terminal.dimensions()) {
Ok(runtime) => (Some(runtime), LiveRuntimeStatus::Starting),
Err(error) => (None, LiveRuntimeStatus::Fault(error.to_string())),
};
cx.observe_window_bounds(window, |shell, window, cx| {
let dimensions = terminal_dimensions_for_window(window.bounds().size);
shell.resize_terminal(dimensions.rows(), dimensions.columns(), cx);
})
.detach();
Self {
model: ShellModel::with_external_output(LIVE_PANE)
.expect("the live comparison pane is a terminal"),
@@ -220,6 +406,7 @@ impl LumbridgeShell {
runtime,
runtime_status,
terminal,
terminal_snapshot,
last_runtime_sequence: 0,
root_focus,
pane_focus,
@@ -316,7 +503,9 @@ impl LumbridgeShell {
}
fn publish_terminal_snapshot(&mut self) {
let lines = self.terminal.snapshot().plain_rows();
let snapshot = self.terminal.snapshot();
let lines = snapshot.plain_rows();
self.terminal_snapshot = snapshot;
self.dispatch(ShellAction::ReplaceExternalOutput {
pane: LIVE_PANE,
lines,
@@ -474,11 +663,10 @@ impl LumbridgeShell {
cx.notify();
}
fn pane_card(
fn pane_selector(
pane: &PaneState,
selected: bool,
runtime_status: LiveRuntimeStatus,
terminal_dimensions: TerminalDimensions,
focus: FocusHandle,
cx: &mut Context<Self>,
) -> gpui::AnyElement {
@@ -500,147 +688,296 @@ impl LumbridgeShell {
} else {
ACCENT
};
let line_start = pane.lines().len().saturating_sub(12);
div()
.id(("pane", id.index()))
.track_focus(&focus)
.tab_index(id.index() as isize + 1)
.flex()
.flex_col()
.min_w_0()
.min_h_0()
.overflow_hidden()
.bg(rgb(if selected { PANEL_ACTIVE } else { PANEL }))
.items_center()
.justify_between()
.gap_3()
.h_full()
.min_w(px(172.0))
.px_3()
.bg(rgb(if selected { PANEL_ACTIVE } else { PANEL_ALT }))
.border_1()
.border_color(rgb(if selected || needs_input {
state_color
} else {
BORDER
BORDER_QUIET
}))
.rounded(px(5.0))
.when(selected, |view| view.border_2())
.on_click(cx.listener(move |shell, _, window, cx| {
shell.select_pane(id, window, cx);
}))
.child(
div()
.flex()
.items_center()
.justify_between()
.px_3()
.h(px(36.0))
.flex_none()
.bg(rgb(if selected { PANEL_ACTIVE } else { PANEL_ALT }))
.border_b_1()
.border_color(rgb(BORDER_QUIET))
.text_sm()
.text_color(rgb(TEXT))
.flex_col()
.min_w_0()
.child(
div()
.flex()
.items_center()
.gap_2()
.child(
div()
.text_xs()
.text_color(rgb(MUTED))
.child(format!("{}", id.index() + 1)),
)
.truncate()
.text_sm()
.text_color(rgb(TEXT))
.child(if external {
"Shell · VT session"
"Terminal · local shell"
} else {
pane.fixture().title
}),
)
.child(div().text_xs().text_color(rgb(state_color)).child(label)),
.child(
div()
.mt_1()
.truncate()
.text_xs()
.text_color(rgb(MUTED))
.child(pane.fixture().target.to_owned()),
),
)
.child(div().text_xs().text_color(rgb(state_color)).child(label))
.into_any_element()
}
fn terminal_run(run: TerminalPaintRun) -> gpui::AnyElement {
let cursor = run.cursor;
div()
.flex_none()
.h(px(TERMINAL_CELL_HEIGHT))
.w(px(f32::from(run.columns) * TERMINAL_CELL_WIDTH))
.overflow_hidden()
.whitespace_nowrap()
.text_color(rgb(run.foreground))
.bg(rgb(run.background))
.when(run.style.contains(TerminalCellStyle::BOLD), |view| {
view.font_weight(FontWeight::BOLD)
})
.when(run.style.contains(TerminalCellStyle::ITALIC), |view| {
view.italic()
})
.when(
run.style.contains(TerminalCellStyle::UNDERLINE)
|| run.style.contains(TerminalCellStyle::DOUBLE_UNDERLINE)
|| run.style.contains(TerminalCellStyle::UNDERCURL)
|| run.style.contains(TerminalCellStyle::DOTTED_UNDERLINE)
|| run.style.contains(TerminalCellStyle::DASHED_UNDERLINE)
|| run.hyperlink,
|view| view.underline(),
)
.when(run.style.contains(TerminalCellStyle::STRIKEOUT), |view| {
view.line_through()
})
.when(cursor == Some(TerminalCursorShape::Block), |view| {
view.bg(rgb(TEXT)).text_color(rgb(BG))
})
.when(cursor == Some(TerminalCursorShape::HollowBlock), |view| {
view.border_1().border_color(rgb(TEXT))
})
.when(cursor == Some(TerminalCursorShape::Underline), |view| {
view.border_b_2().border_color(rgb(TEXT))
})
.when(cursor == Some(TerminalCursorShape::Beam), |view| {
view.border_l_2().border_color(rgb(TEXT))
})
.child(run.text)
.into_any_element()
}
fn terminal_view(&self) -> gpui::AnyElement {
let rows = terminal_paint_rows(&self.terminal_snapshot)
.into_iter()
.map(|runs| {
div()
.flex()
.h(px(TERMINAL_CELL_HEIGHT))
.flex_none()
.children(runs.into_iter().map(Self::terminal_run))
})
.collect::<Vec<_>>();
div()
.flex()
.flex_col()
.size_full()
.overflow_hidden()
.bg(rgb(BG))
.font_family("monospace")
.text_size(px(13.0))
.children(rows)
.into_any_element()
}
fn work_surface(&self) -> gpui::AnyElement {
let pane = self.model.pane(self.model.selected_pane());
let external = pane.output_source() == OutputSource::External;
let status = if external {
self.runtime_status.badge()
} else {
pane.fixture().badge
};
let detail = if external {
self.runtime_status.detail()
} else {
pane.fixture().target.to_owned()
};
let surface_status = if external {
format!(
"{} · {}×{} · auto-fit",
status,
self.terminal.dimensions().columns(),
self.terminal.dimensions().rows()
)
} else {
status.to_owned()
};
let surface = match pane.kind() {
SurfaceKind::Terminal => "TERMINAL",
SurfaceKind::Markdown => "CONTEXT",
SurfaceKind::Browser => "BROWSER",
SurfaceKind::Review => "REVIEW",
};
let tabs = ["TERMINAL", "BROWSER", "TOOLS", "CONTEXT", "GOAL", "REVIEW"]
.into_iter()
.map(|label| {
div()
.h_full()
.flex()
.items_center()
.px_3()
.text_xs()
.text_color(rgb(if label == surface { ACCENT } else { MUTED }))
.when(label == surface, |view| {
view.border_b_2().border_color(rgb(ACCENT))
})
.child(label)
})
.collect::<Vec<_>>();
let content = if external {
self.terminal_view()
} else {
let start = pane.lines().len().saturating_sub(18);
div()
.flex()
.flex_col()
.gap_1()
.size_full()
.overflow_hidden()
.font_family("monospace")
.text_sm()
.text_color(rgb(TEXT))
.children(pane.lines()[start..].iter().cloned())
.into_any_element()
};
div()
.flex()
.flex_col()
.size_full()
.min_h_0()
.overflow_hidden()
.bg(rgb(PANEL))
.border_y_1()
.border_color(rgb(BORDER))
.child(
div()
.flex()
.items_center()
.justify_between()
.h(px(36.0))
.flex_none()
.px_3()
.py_2()
.text_xs()
.text_color(rgb(MUTED))
.child(if external {
format!(
"{} · {}×{} · Alt+Shift+arrows resize",
runtime_status.detail(),
terminal_dimensions.columns(),
terminal_dimensions.rows()
)
} else {
pane.fixture().target.to_owned()
})
.when(selected, |view| view.child("FOCUSED")),
.bg(rgb(PANEL_ALT))
.child(
div()
.flex()
.items_center()
.gap_3()
.text_sm()
.child(pane.fixture().title)
.child(div().text_xs().text_color(rgb(MUTED)).child(detail)),
)
.child(
div()
.text_xs()
.text_color(rgb(SUCCESS))
.child(surface_status),
),
)
.when(external && selected, |view| {
view.child(
div()
.flex()
.items_center()
.gap_3()
.h(px(26.0))
.flex_none()
.px_3()
.border_y_1()
.border_color(rgb(BORDER_QUIET))
.bg(rgb(PANEL_ALT))
.text_xs()
.child(div().text_color(rgb(ACCENT)).child("TERMINAL"))
.child(div().text_color(rgb(MUTED)).child("BROWSER"))
.child(div().text_color(rgb(MUTED)).child("TOOLS"))
.child(div().text_color(rgb(MUTED)).child("CONTEXT"))
.child(div().text_color(rgb(MUTED)).child("GOAL"))
.child(div().text_color(rgb(MUTED)).child("REVIEW")),
)
})
.child(
div()
.flex()
.flex_col()
.h(px(28.0))
.flex_none()
.px_1()
.bg(rgb(PANEL_ALT))
.border_t_1()
.border_color(rgb(BORDER_QUIET))
.children(tabs),
)
.child(
div()
.flex_1()
.min_h_0()
.overflow_hidden()
.gap_1()
.px_3()
.pb_3()
.text_sm()
.text_color(rgb(TEXT))
.children(pane.lines()[line_start..].iter().cloned()),
.p_3()
.child(content),
)
.into_any_element()
}
fn decision_region(&self) -> gpui::AnyElement {
let choice = |label: &'static str, detail: &'static str| {
div()
.flex()
.flex_col()
.min_w(px(200.0))
.px_3()
.py_2()
.rounded(px(5.0))
.border_1()
.border_color(rgb(BORDER))
.bg(rgb(PANEL_ALT))
.text_sm()
.text_color(rgb(TEXT))
.child(label)
.child(div().mt_1().text_xs().text_color(rgb(MUTED)).child(detail))
};
div()
.flex()
.flex_col()
.size_full()
.min_h_0()
.overflow_hidden()
.px_3()
.py_2()
.bg(rgb(PANEL))
.child(
div()
.flex()
.items_center()
.justify_between()
.text_xs()
.child(
div()
.text_color(rgb(MUTED))
.child("DECISION SHELF · answer, choices, approvals"),
)
.child(
div()
.text_color(rgb(ATTENTION))
.child("LOCAL ANALYST OFF · suggestions inert"),
),
)
.child(
div()
.flex()
.items_center()
.gap_2()
.mt_2()
.child(choice("Continue", "Keep the selected agent moving"))
.child(choice("Review plan", "Inspect capability-scoped commands"))
.child(choice("Ask…", "Refine before any action")),
)
.when(external && selected, |view| {
view.child(
div()
.flex()
.items_center()
.justify_between()
.gap_2()
.h(px(38.0))
.flex_none()
.px_3()
.border_t_1()
.border_color(rgb(BORDER_QUIET))
.bg(rgb(PANEL_ALT))
.text_xs()
.child(
div()
.text_color(rgb(MUTED))
.child("LOCAL ANALYST · OFF · suggestions only"),
)
.child(
div()
.flex()
.gap_2()
.text_color(rgb(TEXT))
.child("Continue")
.child("Review plan")
.child("Ask…"),
),
)
})
.into_any_element()
}
@@ -899,29 +1236,91 @@ impl Render for LumbridgeShell {
.child("1 interactive VT · 5 deterministic · 1 waiting"),
);
let pane_cards = self
let pane_selectors = self
.model
.panes()
.iter()
.map(|pane| {
Self::pane_card(
Self::pane_selector(
pane,
self.model.selected_pane() == pane.id(),
self.runtime_status.clone(),
self.terminal.dimensions(),
self.pane_focus[pane.id().index()].clone(),
cx,
)
})
.collect::<Vec<_>>();
let grid = div()
.grid()
.grid_cols(3)
.grid_rows(2)
.gap_2()
.p_2()
let selected = self.model.pane(self.model.selected_pane());
let workspace_stack = div()
.flex()
.flex_col()
.size_full()
.children(pane_cards);
.min_h_0()
.overflow_hidden()
.child(
div()
.h(relative(0.20))
.flex_none()
.flex()
.flex_col()
.min_h_0()
.overflow_hidden()
.px_3()
.py_2()
.bg(rgb(PANEL))
.child(
div()
.flex()
.items_center()
.justify_between()
.text_xs()
.child(
div()
.text_color(rgb(MUTED))
.child("CONTEXT · panes, agents, tools, goal"),
)
.child(
div()
.text_color(rgb(if selected.needs_input() {
ATTENTION
} else {
SUCCESS
}))
.child(if selected.needs_input() {
"NEEDS INPUT · decision below"
} else {
"WORKSPACE HEALTHY"
}),
),
)
.child(
div()
.flex()
.items_center()
.gap_2()
.mt_2()
.flex_1()
.min_h_0()
.overflow_hidden()
.children(pane_selectors),
),
)
.child(
div()
.h(relative(0.60))
.flex_none()
.min_h_0()
.overflow_hidden()
.child(self.work_surface()),
)
.child(
div()
.h(relative(0.20))
.flex_none()
.min_h_0()
.overflow_hidden()
.child(self.decision_region()),
);
let counters = self.model.counters();
let footer_left = format!(
@@ -1014,7 +1413,7 @@ impl Render for LumbridgeShell {
.flex_1()
.min_w_0()
.child(tabs)
.child(grid),
.child(workspace_stack),
),
)
.child(
@@ -1141,7 +1540,11 @@ fn main() {
#[cfg(test)]
mod tests {
use super::{KeyModifiers, TerminalKey, terminal_key_from_parts};
use super::{
KeyModifiers, TerminalEngine, TerminalEngineOptions, TerminalKey, indexed_terminal_color,
terminal_dimensions_for_window, terminal_key_from_parts, terminal_paint_rows,
};
use gpui::{px, size};
#[test]
fn maps_named_and_composed_gpui_keys_to_terminal_input() {
@@ -1158,4 +1561,32 @@ mod tests {
let control = terminal_key_from_parts("c", Some("c"), KeyModifiers::CONTROL).unwrap();
assert_eq!(control.key, TerminalKey::Text("c".into()));
}
#[test]
fn terminal_geometry_tracks_the_middle_sixty_percent() {
let dimensions = terminal_dimensions_for_window(size(px(1500.0), px(960.0)));
assert_eq!(dimensions.rows(), 24);
assert_eq!(dimensions.columns(), 145);
}
#[test]
fn terminal_paint_rows_merge_runs_and_preserve_cursor_boundary() {
let mut terminal = TerminalEngine::new(TerminalEngineOptions::default());
terminal.process(b"\x1b[31mAB\x1b[0m");
let rows = terminal_paint_rows(&terminal.snapshot());
assert!(
rows[0]
.iter()
.any(|run| run.text == "AB" && run.columns == 2)
);
assert!(rows[0].iter().any(|run| run.cursor.is_some()));
}
#[test]
fn xterm_color_cube_and_grayscale_are_deterministic() {
assert_eq!(indexed_terminal_color(16), 0x000000);
assert_eq!(indexed_terminal_color(231), 0xffffff);
assert_eq!(indexed_terminal_color(232), 0x080808);
assert_eq!(indexed_terminal_color(255), 0xeeeeee);
}
}