first pass of merging in warp (doesn't build)
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
//! 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user