use ratatui::prelude::*; use ratatui::style::Stylize; use std::collections::BTreeSet; use unicode_width::UnicodeWidthChar; use unicode_width::UnicodeWidthStr; #[derive(Debug, Clone)] pub(crate) struct FieldFormatter { indent: &'static str, label_width: usize, value_offset: usize, value_indent: String, } impl FieldFormatter { pub(crate) const INDENT: &'static str = " "; pub(crate) fn from_labels(labels: impl IntoIterator) -> Self where S: AsRef, { let label_width = labels .into_iter() .map(|label| UnicodeWidthStr::width(label.as_ref())) .max() .unwrap_or(0); let indent_width = UnicodeWidthStr::width(Self::INDENT); let value_offset = indent_width + label_width + 1 + 3; Self { indent: Self::INDENT, label_width, value_offset, value_indent: " ".repeat(value_offset), } } pub(crate) fn line( &self, label: &'static str, value_spans: Vec>, ) -> Line<'static> { Line::from(self.full_spans(label, value_spans)) } pub(crate) fn continuation(&self, mut spans: Vec>) -> Line<'static> { let mut all_spans = Vec::with_capacity(spans.len() + 1); all_spans.push(Span::from(self.value_indent.clone()).dim()); all_spans.append(&mut spans); Line::from(all_spans) } pub(crate) fn value_width(&self, available_inner_width: usize) -> usize { available_inner_width.saturating_sub(self.value_offset) } pub(crate) fn full_spans( &self, label: &str, mut value_spans: Vec>, ) -> Vec> { let mut spans = Vec::with_capacity(value_spans.len() + 1); spans.push(self.label_span(label)); spans.append(&mut value_spans); spans } fn label_span(&self, label: &str) -> Span<'static> { let mut buf = String::with_capacity(self.value_offset); buf.push_str(self.indent); buf.push_str(label); buf.push(':'); let label_width = UnicodeWidthStr::width(label); let padding = 3 + self.label_width.saturating_sub(label_width); for _ in 0..padding { buf.push(' '); } Span::from(buf).dim() } } pub(crate) fn push_label(labels: &mut Vec, seen: &mut BTreeSet, label: &str) { if seen.contains(label) { return; } let owned = label.to_string(); seen.insert(owned.clone()); labels.push(owned); } pub(crate) fn line_display_width(line: &Line<'static>) -> usize { line.iter() .map(|span| UnicodeWidthStr::width(span.content.as_ref())) .sum() } pub(crate) fn truncate_line_to_width(line: Line<'static>, max_width: usize) -> Line<'static> { if max_width == 0 { return Line::from(Vec::>::new()); } let mut used = 0usize; let mut spans_out: Vec> = Vec::new(); for span in line.spans { let text = span.content.into_owned(); let style = span.style; let span_width = UnicodeWidthStr::width(text.as_str()); if span_width == 0 { spans_out.push(Span::styled(text, style)); continue; } if used >= max_width { break; } if used + span_width <= max_width { used += span_width; spans_out.push(Span::styled(text, style)); continue; } let mut truncated = String::new(); for ch in text.chars() { let ch_width = UnicodeWidthChar::width(ch).unwrap_or(0); if used + ch_width > max_width { break; } truncated.push(ch); used += ch_width; } if !truncated.is_empty() { spans_out.push(Span::styled(truncated, style)); } break; } Line::from(spans_out) }