use super::*;
#[derive(Debug, Clone, Copy)]
pub(super) struct DisplayWidthUnit<'a> {
pub(super) text: &'a str,
pub(super) end: usize,
pub(super) width: usize,
}
pub(super) fn next_display_width_unit(text: &str, start: usize) -> Option<DisplayWidthUnit<'_>> {
let (offset, unit) = text[start..]
.grapheme_indices(true)
.find(|(_, grapheme)| !grapheme.chars().any(char::is_control))?;
let end = start + offset + unit.len();
Some(DisplayWidthUnit {
text: unit,
end,
width: UnicodeWidthStr::width(unit),
})
}
pub(in crate::tui::transcript_cards) fn spans_text(spans: &[Span<'static>]) -> String {
spans
.iter()
.map(|span| span.content.as_ref())
.collect::<String>()
}
pub(in crate::tui::transcript_cards) fn spans_display_width(spans: &[Span<'static>]) -> usize {
spans
.iter()
.map(|span| UnicodeWidthStr::width(span.content.as_ref()))
.sum()
}
pub(super) fn transcript_gap_visual_line(
width: u16,
theme: MissionControlTheme,
) -> TranscriptVisualLine {
TranscriptVisualLine {
image_row: None,
status_indicators: Vec::new(),
line: Line::from(Span::styled(
" ".repeat(usize::from(width)),
theme.pane().bg(theme.surface_background()),
)),
copy_text: String::new(),
copy_byte_range: None,
copyable: true,
copy_continuation: false,
visual_copy_column_offset: 0,
visual_copy_byte_boundaries: Vec::new(),
hit_regions: Vec::new(),
}
}
pub(super) fn transcript_bottom_padding_visual_line(
width: u16,
theme: MissionControlTheme,
) -> TranscriptVisualLine {
let mut line = transcript_gap_visual_line(width, theme);
line.copyable = false;
line
}
pub(super) fn truncate_to_display_width(text: &str, max_width: usize) -> String {
let mut out = String::new();
let mut width = 0usize;
let mut index = 0usize;
while let Some(unit) = next_display_width_unit(text, index) {
if width.saturating_add(unit.width) > max_width {
break;
}
out.push_str(unit.text);
width = width.saturating_add(unit.width);
index = unit.end;
}
out
}
pub(super) fn pad_full_width_spans(
mut spans: Vec<Span<'static>>,
width: u16,
) -> Vec<Span<'static>> {
let used = spans
.iter()
.map(|span| UnicodeWidthStr::width(span.content.as_ref()))
.sum::<usize>();
let style = spans.last().map(|span| span.style).unwrap_or_default();
let padding = usize::from(width).saturating_sub(used);
if padding > 0 {
spans.push(Span::styled(" ".repeat(padding), style));
}
spans
}
pub(super) fn card_header_background(
role: TranscriptCardRole,
theme: MissionControlTheme,
) -> ratatui::style::Color {
if role == TranscriptCardRole::Error {
theme.surface_error()
} else {
theme.surface_panel_alt()
}
}
pub(super) fn status_header_style(
status: TranscriptCardStatus,
theme: MissionControlTheme,
) -> Style {
match status {
TranscriptCardStatus::Running => Style::new()
.fg(theme.activity_status_color(ActivityStatus::Running))
.add_modifier(Modifier::BOLD),
TranscriptCardStatus::Success => {
Style::new().fg(theme.activity_status_color(ActivityStatus::Success))
}
TranscriptCardStatus::Failed => Style::new()
.fg(theme.activity_status_color(ActivityStatus::Failed))
.add_modifier(Modifier::BOLD),
TranscriptCardStatus::Canceled => Style::new()
.fg(theme.activity_status_color(ActivityStatus::Canceled))
.add_modifier(Modifier::DIM),
TranscriptCardStatus::None => theme.muted(false),
}
}
pub(super) fn role_label_style(
role: TranscriptCardRole,
active: bool,
theme: MissionControlTheme,
) -> Style {
match role {
TranscriptCardRole::Error => theme.transcript_error_label(),
TranscriptCardRole::Session
| TranscriptCardRole::Diagnostic
| TranscriptCardRole::Compaction
| TranscriptCardRole::Empty => theme.muted(active),
TranscriptCardRole::Bash => theme.accent(active).add_modifier(Modifier::BOLD),
TranscriptCardRole::Reasoning => theme.dim(active).add_modifier(Modifier::BOLD),
TranscriptCardRole::Tool
| TranscriptCardRole::CodeMode
| TranscriptCardRole::MagiControl
| TranscriptCardRole::HashEdit
| TranscriptCardRole::Write
| TranscriptCardRole::ViewImage
| TranscriptCardRole::Read
| TranscriptCardRole::Grep
| TranscriptCardRole::AstGrep
| TranscriptCardRole::Find
| TranscriptCardRole::ListFiles
| TranscriptCardRole::Web
| TranscriptCardRole::SubagentBatch
| TranscriptCardRole::UserPrompt
| TranscriptCardRole::Assistant => theme.accent(active).add_modifier(Modifier::BOLD),
}
}
pub(super) fn role_body_style(
role: TranscriptCardRole,
_active: bool,
theme: MissionControlTheme,
) -> Style {
match role {
TranscriptCardRole::UserPrompt | TranscriptCardRole::Bash => theme.transcript_user_body(),
TranscriptCardRole::Tool
| TranscriptCardRole::CodeMode
| TranscriptCardRole::MagiControl
| TranscriptCardRole::HashEdit
| TranscriptCardRole::Write
| TranscriptCardRole::ViewImage
| TranscriptCardRole::Read
| TranscriptCardRole::Grep
| TranscriptCardRole::AstGrep
| TranscriptCardRole::Find
| TranscriptCardRole::ListFiles
| TranscriptCardRole::Web
| TranscriptCardRole::SubagentBatch => theme.transcript_tool_body(),
TranscriptCardRole::Error => theme.transcript_error_body(),
TranscriptCardRole::Session
| TranscriptCardRole::Diagnostic
| TranscriptCardRole::Compaction => theme.transcript_session_body(),
TranscriptCardRole::Reasoning => theme.dim(false),
TranscriptCardRole::Assistant | TranscriptCardRole::Empty => theme.pane(),
}
}
pub(super) fn card_body_background(
role: TranscriptCardRole,
theme: MissionControlTheme,
) -> ratatui::style::Color {
if role == TranscriptCardRole::Error {
theme.surface_error()
} else {
theme.surface_panel()
}
}
pub(super) fn display_status_mark(card: &TranscriptCard) -> &'static str {
match card.role {
TranscriptCardRole::Assistant
| TranscriptCardRole::UserPrompt
| TranscriptCardRole::Reasoning
| TranscriptCardRole::SubagentBatch => "",
_ => status_mark(card.status),
}
}
pub(in crate::tui::transcript_cards) fn status_mark(status: TranscriptCardStatus) -> &'static str {
match status {
TranscriptCardStatus::Running => activity_status_mark(ActivityStatus::Running),
TranscriptCardStatus::Success => activity_status_mark(ActivityStatus::Success),
TranscriptCardStatus::Failed => activity_status_mark(ActivityStatus::Failed),
TranscriptCardStatus::Canceled => activity_status_mark(ActivityStatus::Canceled),
TranscriptCardStatus::None => "",
}
}