//! 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>, block_id: BlockId, visible_rows: Range, width: u16, } impl TerminalBlockVisibleRowsElement { /// Creates a terminal block element for a visible row window. pub(super) fn new( model: Arc>, block_id: BlockId, visible_rows: Range, 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, 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, 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 } }