use ratatui::style::Style;
use ratatui::text::{Line, Span};
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
pub(crate) fn hard_break_plain_token(
token: &str,
out: &mut Vec<String>,
current_line: &mut String,
current_length: &mut usize,
width: usize,
continuation_indent: usize,
initial_budget: usize,
) {
let cont_budget = width.saturating_sub(continuation_indent).max(1);
let mut line_budget = initial_budget.max(1);
if *current_length > 0 {
out.push(std::mem::take(current_line));
current_line.push_str(&" ".repeat(continuation_indent));
*current_length = 0;
line_budget = cont_budget;
}
for ch in token.chars() {
let cw = ch.width().unwrap_or(0);
if *current_length + cw > line_budget && *current_length > 0 {
out.push(std::mem::take(current_line));
current_line.push_str(&" ".repeat(continuation_indent));
*current_length = 0;
line_budget = cont_budget;
}
current_line.push(ch);
*current_length += cw;
}
}
pub(crate) fn wrap_text_with_indent(
text: &str,
width: usize,
first_line_indent: usize,
continuation_indent: usize,
) -> Vec<String> {
let mut wrapped_lines = Vec::new();
for (line_idx, line) in text.lines().enumerate() {
if line.is_empty() {
wrapped_lines.push(String::new());
continue;
}
let current_indent = if line_idx == 0 {
first_line_indent
} else {
continuation_indent
};
let available_width = width.saturating_sub(current_indent);
if available_width == 0 {
wrapped_lines.push(" ".repeat(current_indent));
continue;
}
let words: Vec<&str> = line.split_whitespace().collect();
if words.is_empty() {
wrapped_lines.push(" ".repeat(current_indent));
continue;
}
let mut current_line = String::with_capacity(width);
current_line.push_str(&" ".repeat(current_indent));
let mut current_length = 0;
for (word_idx, word) in words.iter().enumerate() {
let word_width = word.width();
if word_idx == 0 {
if word_width <= available_width {
current_line.push_str(word);
current_length = word_width;
} else {
hard_break_plain_token(
word,
&mut wrapped_lines,
&mut current_line,
&mut current_length,
width,
continuation_indent,
available_width,
);
}
} else if current_length + 1 + word_width <= available_width {
current_line.push(' ');
current_line.push_str(word);
current_length += 1 + word_width;
} else if word_width <= available_width {
wrapped_lines.push(current_line);
current_line = String::with_capacity(width);
current_line.push_str(&" ".repeat(continuation_indent));
current_line.push_str(word);
current_length = word_width;
} else {
hard_break_plain_token(
word,
&mut wrapped_lines,
&mut current_line,
&mut current_length,
width,
continuation_indent,
available_width,
);
}
}
if !current_line.trim().is_empty() {
wrapped_lines.push(current_line);
}
}
wrapped_lines
}
pub(crate) fn hard_break_styled_word(
fragments: &[(String, Style)],
result_lines: &mut Vec<Line<'static>>,
current_line_spans: &mut Vec<Span<'static>>,
current_line_width: &mut usize,
continuation_indent: usize,
continuation_capacity: usize,
mut line_capacity: usize,
) {
for (text, style) in fragments {
let mut buf = String::new();
for ch in text.chars() {
let cw = ch.width().unwrap_or(0);
if *current_line_width + cw > line_capacity && *current_line_width > 0 {
if !buf.is_empty() {
current_line_spans.push(Span::styled(std::mem::take(&mut buf), *style));
}
result_lines.push(Line::from(std::mem::take(current_line_spans)));
current_line_spans.push(Span::raw(" ".repeat(continuation_indent)));
*current_line_width = 0;
line_capacity = continuation_capacity.max(1);
}
buf.push(ch);
*current_line_width += cw;
}
if !buf.is_empty() {
current_line_spans.push(Span::styled(buf, *style));
}
}
}
#[expect(
clippy::too_many_lines,
reason = "predates the lint; see .github/baselines/expect_budget.txt"
)]
pub(crate) fn wrap_styled_line(
line: Line<'static>,
width: usize,
continuation_indent: usize,
) -> Vec<Line<'static>> {
let total_width: usize = line.spans.iter().map(|s| s.content.width()).sum();
if total_width <= width {
return vec![line];
}
let mut result_lines = Vec::new();
let mut current_line_spans: Vec<Span<'static>> = Vec::new();
let mut current_line_width = 0usize;
let available_width = width.saturating_sub(continuation_indent);
let leading_indent: usize = {
let mut n = 0;
for span in &line.spans {
let spaces = span.content.len() - span.content.trim_start_matches(' ').len();
n += spaces;
if spaces < span.content.len() {
break; }
}
n
};
struct Word {
fragments: Vec<(String, Style)>,
separator: Style,
}
let mut words: Vec<Word> = Vec::new();
let mut current_word: Vec<(String, Style)> = Vec::new();
let mut separator = Style::default();
for span in &line.spans {
let mut frag = String::new();
for ch in span.content.chars() {
if ch.is_whitespace() {
if !frag.is_empty() {
current_word.push((std::mem::take(&mut frag), span.style));
}
if !current_word.is_empty() {
words.push(Word {
fragments: std::mem::take(&mut current_word),
separator,
});
}
separator = span.style;
} else {
frag.push(ch);
}
}
if !frag.is_empty() {
current_word.push((frag, span.style));
}
}
if !current_word.is_empty() {
words.push(Word {
fragments: current_word,
separator,
});
}
fn emit_word(spans: &mut Vec<Span<'static>>, word: Vec<(String, Style)>) {
for (text, style) in word {
spans.push(Span::styled(text, style));
}
}
for Word {
fragments: word,
separator,
} in words
{
let word_width: usize = word.iter().map(|(text, _)| text.width()).sum();
if current_line_width == 0 && result_lines.is_empty() {
if leading_indent > 0 {
current_line_spans.push(Span::raw(" ".repeat(leading_indent)));
current_line_width += leading_indent;
}
if word_width <= available_width {
current_line_width += word_width;
emit_word(&mut current_line_spans, word);
} else {
hard_break_styled_word(
&word,
&mut result_lines,
&mut current_line_spans,
&mut current_line_width,
continuation_indent,
available_width,
width,
);
}
continue;
}
let sep = usize::from(current_line_width > 0);
if current_line_width + sep + word_width <= available_width {
if sep == 1 {
current_line_spans.push(Span::styled(" ", separator));
}
current_line_width += sep + word_width;
emit_word(&mut current_line_spans, word);
} else if word_width <= available_width {
result_lines.push(Line::from(std::mem::take(&mut current_line_spans)));
current_line_spans.push(Span::raw(" ".repeat(continuation_indent)));
current_line_width = word_width;
emit_word(&mut current_line_spans, word);
} else {
result_lines.push(Line::from(std::mem::take(&mut current_line_spans)));
current_line_spans.push(Span::raw(" ".repeat(continuation_indent)));
current_line_width = 0;
hard_break_styled_word(
&word,
&mut result_lines,
&mut current_line_spans,
&mut current_line_width,
continuation_indent,
available_width,
available_width,
);
}
}
if !current_line_spans.is_empty() {
result_lines.push(Line::from(current_line_spans));
}
if result_lines.is_empty() {
vec![line]
} else {
result_lines
}
}