Files
galaxy/crates/warp_tui/src/terminal_block.rs
T

232 lines
7.7 KiB
Rust

//! Simple terminal block rendering for the TUI transcript.
use std::ops::Range;
use std::sync::Arc;
use parking_lot::FairMutex;
use warp::tui_export::{Block, BlockGrid, BlockId, BlockList, TerminalColorList, TerminalModel};
use warp_terminal::model::ansi::Color;
use warp_terminal::model::grid::cell::{Cell, Flags};
use warp_terminal::model::grid::Dimensions as _;
use warpui_core::elements::tui::{
Color as TuiColor, Modifier, TuiBuffer, TuiConstraint, TuiElement, TuiLayoutContext, TuiRect,
TuiSize, TuiStyle,
};
use warpui_core::AppContext;
/// Paints a pre-clipped row window from one terminal block.
///
/// This is a bespoke [`TuiElement`], unlike agent blocks which compose generic
/// `TuiText`/`TuiContainer`: terminal cells each carry their own fg/bg/flags,
/// which no generic single-style text element can express, and a block can be
/// thousands of rows — painting only the visible slice into the buffer avoids
/// materializing a huge element tree per frame.
pub(super) struct TerminalBlockVisibleRowsElement {
model: Arc<FairMutex<TerminalModel>>,
block_id: BlockId,
visible_rows: Range<usize>,
width: u16,
}
impl TerminalBlockVisibleRowsElement {
/// Creates a terminal block element for a visible row window.
pub(super) fn new(
model: Arc<FairMutex<TerminalModel>>,
block_id: BlockId,
visible_rows: Range<usize>,
width: u16,
) -> Self {
Self {
model,
block_id,
visible_rows,
width,
}
}
}
impl TuiElement for TerminalBlockVisibleRowsElement {
fn layout(
&mut self,
constraint: TuiConstraint,
_ctx: &mut TuiLayoutContext,
_app: &AppContext,
) -> TuiSize {
constraint.clamp(TuiSize::new(
constraint.max.width,
self.visible_rows
.end
.saturating_sub(self.visible_rows.start)
.min(usize::from(u16::MAX)) as u16,
))
}
fn render(&self, area: TuiRect, buffer: &mut TuiBuffer, _ctx: &mut TuiLayoutContext) {
let model = self.model.lock();
let colors = model.colors();
let Some(block) = model.block_list().block_with_id(&self.block_id) else {
return;
};
// A block stacks its prompt/command grid above its output grid; each call
// paints only that grid's rows overlapping this element's visible window,
// positioned within `area`, so the two grids don't overlap.
let max_width = self.width.min(area.width);
if !block.should_hide_command_grid() {
render_grid_rows(
block.prompt_and_command_grid(),
block
.prompt_and_command_grid_offset()
.as_f64()
.ceil()
.max(0.0) as usize,
self.visible_rows.clone(),
max_width,
area,
buffer,
&colors,
);
}
if !block.should_hide_output_grid() {
render_grid_rows(
block.output_grid(),
block.output_grid_offset().as_f64().ceil().max(0.0) as usize,
self.visible_rows.clone(),
max_width,
area,
buffer,
&colors,
);
}
}
}
/// Returns whether the TUI transcript should include this terminal block.
pub(super) fn should_render_terminal_block(block: &Block, block_list: &BlockList) -> bool {
block.is_visible(block_list.agent_view_state()) && (block.started() || block.finished())
}
/// Paints consecutive displayed rows of one grid starting at `*y`, advancing
/// `y` past each row drawn and stopping at the bottom of `area`.
fn render_displayed_rows(
block_grid: &BlockGrid,
displayed_rows: Range<usize>,
max_width: u16,
area: TuiRect,
buffer: &mut TuiBuffer,
colors: &TerminalColorList,
y: &mut u16,
) {
let grid = block_grid.grid_handler();
let end = displayed_rows.end.min(block_grid.len_displayed());
for displayed_row in displayed_rows.start.min(end)..end {
if *y >= area.bottom() {
break;
}
let original_row = grid.maybe_translate_row_from_displayed_to_original(displayed_row);
let Some(row) = grid.row(original_row) else {
continue;
};
for column in 0..grid.columns().min(usize::from(max_width)) {
let cell = &row[column];
if let Some(buffer_cell) = buffer.cell_mut((area.x.saturating_add(column as u16), *y)) {
buffer_cell
.set_symbol(&sanitized_symbol(cell))
.set_style(cell_to_style(cell, colors));
}
}
*y = (*y).saturating_add(1);
}
}
/// Paints the rows of one grid that fall within the element's visible window.
///
/// `grid_start_row` is where this grid begins relative to the top of the block
/// (the command grid starts at 0; the output grid starts below it). Only the
/// intersection of the grid's rows with `visible_rows` is drawn, offset within
/// `area` so it lands at the correct vertical position.
fn render_grid_rows(
block_grid: &BlockGrid,
grid_start_row: usize,
visible_rows: Range<usize>,
max_width: u16,
area: TuiRect,
buffer: &mut TuiBuffer,
colors: &TerminalColorList,
) {
let grid_end_row = grid_start_row.saturating_add(block_grid.len_displayed());
let visible_start = visible_rows.start.max(grid_start_row);
let visible_end = visible_rows.end.min(grid_end_row);
if visible_start >= visible_end {
return;
}
let displayed_rows =
visible_start.saturating_sub(grid_start_row)..visible_end.saturating_sub(grid_start_row);
let y_offset = visible_start.saturating_sub(visible_rows.start);
let mut y = area
.y
.saturating_add(y_offset.min(usize::from(u16::MAX)) as u16);
render_displayed_rows(
block_grid,
displayed_rows,
max_width,
area,
buffer,
colors,
&mut y,
);
}
fn cell_to_color(color: &Color, colors: &TerminalColorList) -> TuiColor {
match color {
Color::Named(named) => {
let color = &colors[named.into_color_index()];
TuiColor::Rgb(color.r, color.g, color.b)
}
Color::Spec(color) => TuiColor::Rgb(color.r, color.g, color.b),
Color::Indexed(index) => {
let color = &colors[*index as usize];
TuiColor::Rgb(color.r, color.g, color.b)
}
}
}
fn cell_to_style(cell: &Cell, colors: &TerminalColorList) -> TuiStyle {
let mut style = TuiStyle::default()
.fg(cell_to_color(&cell.fg, colors))
.bg(cell_to_color(&cell.bg, colors));
if cell.flags.contains(Flags::BOLD) {
style = style.add_modifier(Modifier::BOLD);
}
if cell.flags.contains(Flags::ITALIC) {
style = style.add_modifier(Modifier::ITALIC);
}
if cell.flags.contains(Flags::UNDERLINE) || cell.flags.contains(Flags::DOUBLE_UNDERLINE) {
style = style.add_modifier(Modifier::UNDERLINED);
}
if cell.flags.contains(Flags::INVERSE) {
style = style.add_modifier(Modifier::REVERSED);
}
if cell.flags.contains(Flags::DIM) {
style = style.add_modifier(Modifier::DIM);
}
if cell.flags.contains(Flags::HIDDEN) {
style = style.add_modifier(Modifier::HIDDEN);
}
if cell.flags.contains(Flags::STRIKEOUT) {
style = style.add_modifier(Modifier::CROSSED_OUT);
}
style
}
fn sanitized_symbol(cell: &Cell) -> String {
let content = cell.content_for_display().to_string();
if content.is_empty() || content.chars().any(char::is_control) {
" ".to_owned()
} else {
content
}
}