use super::*;
pub(super) fn push_text_line(
lines: &mut Vec<TranscriptVisualLine>,
card: &TranscriptCard,
text: String,
style: Style,
is_body: bool,
width: u16,
body_style: Style,
) {
push_wide_spans(
lines,
card,
vec![Span::styled(text, style)],
is_body,
width,
body_style,
);
}
fn push_wide_spans(
lines: &mut Vec<TranscriptVisualLine>,
_card: &TranscriptCard,
spans: Vec<Span<'static>>,
_is_body: bool,
width: u16,
edge_style: Style,
) {
for (index, row_spans) in wrap_spans_to_width(spans, body_wrap_width(width))
.into_iter()
.enumerate()
{
let copy_text = spans_text(&row_spans);
lines.push(TranscriptVisualLine {
image_row: None,
status_indicators: Vec::new(),
line: wide_body_line(row_spans, width, edge_style),
copy_text,
copy_byte_range: None,
hit_regions: Vec::new(),
copyable: true,
copy_continuation: index > 0,
visual_copy_column_offset: body_line_copy_offset(width, 0),
visual_copy_byte_boundaries: Vec::new(),
});
}
}
#[derive(Debug, Clone)]
pub(super) struct AssistantVisualSpan {
pub(super) text: String,
pub(super) style: Style,
pub(super) role: Option<DisplayRole>,
}
impl AssistantVisualSpan {
pub(super) fn styled<T: Into<String>>(
text: T,
style: Style,
role: Option<DisplayRole>,
) -> Self {
Self {
text: text.into(),
style,
role,
}
}
fn into_ratatui_span(self) -> Span<'static> {
Span::styled(self.text, self.style)
}
}
pub(super) fn push_wide_assistant_spans(
lines: &mut Vec<TranscriptVisualLine>,
_card: &TranscriptCard,
spans: Vec<AssistantVisualSpan>,
_is_body: bool,
width: u16,
edge_style: Style,
) {
for (index, row_spans) in wrap_assistant_spans_to_width(spans, body_wrap_width(width))
.into_iter()
.enumerate()
{
let copy_text = assistant_wide_copy_text(&row_spans);
let gutter = assistant_code_gutter_width(&row_spans);
lines.push(TranscriptVisualLine {
image_row: None,
status_indicators: Vec::new(),
line: wide_body_line(
row_spans
.into_iter()
.map(AssistantVisualSpan::into_ratatui_span)
.collect(),
width,
edge_style,
),
copy_text,
copy_byte_range: None,
copyable: true,
hit_regions: Vec::new(),
copy_continuation: index > 0,
visual_copy_column_offset: body_line_copy_offset(width, gutter),
visual_copy_byte_boundaries: Vec::new(),
});
}
}
pub(super) fn assistant_wide_copy_text(spans: &[AssistantVisualSpan]) -> String {
spans
.iter()
.filter(|span| span.role != Some(DisplayRole::CodeBlockGutter))
.map(|span| span.text.as_str())
.collect()
}
fn assistant_code_gutter_width(spans: &[AssistantVisualSpan]) -> u16 {
display_width_to_u16(
spans
.iter()
.filter(|span| span.role == Some(DisplayRole::CodeBlockGutter))
.map(|span| UnicodeWidthStr::width(span.text.as_str()))
.sum(),
)
}
pub(super) fn display_width_to_u16(width: usize) -> u16 {
width.min(usize::from(u16::MAX)) as u16
}
pub(super) fn wide_body_line(
content_spans: Vec<Span<'static>>,
width: u16,
body_style: Style,
) -> Line<'static> {
padded_body_line(content_spans, width, body_style, horizontal_padding(width))
}
pub(super) fn padded_body_line(
mut content_spans: Vec<Span<'static>>,
width: u16,
body_style: Style,
(left, right): (usize, usize),
) -> Line<'static> {
for span in &mut content_spans {
span.style = span
.style
.bg(body_style.bg.unwrap_or(ratatui::style::Color::Reset));
}
let content_width = spans_display_width(&content_spans);
let available = usize::from(width)
.saturating_sub(left)
.saturating_sub(right);
let padding = available.saturating_sub(content_width);
let mut result = Vec::new();
if left > 0 {
result.push(Span::styled(" ".repeat(left), body_style));
}
result.extend(content_spans);
if padding.saturating_add(right) > 0 {
result.push(Span::styled(
" ".repeat(padding.saturating_add(right)),
body_style,
));
}
Line::from(result)
}
pub(in crate::tui::transcript_cards) fn wrap_spans_to_width(
spans: Vec<Span<'static>>,
max_width: usize,
) -> Vec<Vec<Span<'static>>> {
let max_width = max_width.max(1);
let mut rows = Vec::new();
let mut row = Vec::new();
let mut row_width = 0usize;
for span in spans {
let style = span.style;
let mut segment = String::new();
let mut index = 0usize;
let content = span.content.as_ref();
while let Some(unit) = next_display_width_unit(content, index) {
index = unit.end;
let unit_width = unit.width;
if row_width.saturating_add(unit_width) > max_width
&& (!row.is_empty() || !segment.is_empty())
{
if !segment.is_empty() {
row.push(Span::styled(std::mem::take(&mut segment), style));
}
rows.push(std::mem::take(&mut row));
row_width = 0;
}
segment.push_str(unit.text);
row_width = row_width.saturating_add(unit_width);
if row_width >= max_width {
row.push(Span::styled(std::mem::take(&mut segment), style));
rows.push(std::mem::take(&mut row));
row_width = 0;
}
}
if !segment.is_empty() {
row.push(Span::styled(segment, style));
}
}
if rows.is_empty() || !row.is_empty() {
rows.push(row);
}
rows
}
pub(super) fn wrap_assistant_spans_to_width(
spans: Vec<AssistantVisualSpan>,
max_width: usize,
) -> Vec<Vec<AssistantVisualSpan>> {
let max_width = max_width.max(1);
let mut rows = Vec::new();
let mut row = Vec::new();
let mut row_width = 0usize;
for span in spans {
let style = span.style;
let role = span.role;
let mut segment = String::new();
let mut index = 0usize;
while let Some(unit) = next_display_width_unit(&span.text, index) {
index = unit.end;
let unit_width = unit.width;
if row_width.saturating_add(unit_width) > max_width
&& (!row.is_empty() || !segment.is_empty())
{
if !segment.is_empty() {
row.push(AssistantVisualSpan::styled(
std::mem::take(&mut segment),
style,
role,
));
}
rows.push(std::mem::take(&mut row));
row_width = 0;
}
segment.push_str(unit.text);
row_width = row_width.saturating_add(unit_width);
if row_width >= max_width {
row.push(AssistantVisualSpan::styled(
std::mem::take(&mut segment),
style,
role,
));
rows.push(std::mem::take(&mut row));
row_width = 0;
}
}
if !segment.is_empty() {
row.push(AssistantVisualSpan::styled(segment, style, role));
}
}
if rows.is_empty() || !row.is_empty() {
rows.push(row);
}
rows
}