///|
/// ANSI- and grapheme-aware word wrapping utilities.
///|
fn wrap_write_chars(
buf : StringBuilder,
chars : Array[Char],
start : Int,
end_index : Int,
) -> Unit {
for i in start.. String {
let n = chars.length()
let from = start.max(0).min(n)
let to = end_index.max(from).min(n)
let buf = StringBuilder::new(size_hint=to - from)
wrap_write_chars(buf, chars, from, to)
buf.to_string()
}
///|
fn wrap_cluster_first_text_char(cluster : String) -> Char? {
let chars = cluster.to_array()
let mut i = 0
while i < chars.length() {
if chars[i] == '\u001b' {
i = @internal.skip_ansi_sequence(chars, i)
} else {
return Some(chars[i])
}
}
None
}
///|
fn wrap_is_space_cluster(cluster : String) -> Bool {
match wrap_cluster_first_text_char(cluster) {
Some(ch) => ch == ' '
None => false
}
}
///|
fn wrap_clusters(line : String) -> (Array[String], Array[Int], Array[Bool]) {
let chars = line.to_array()
let boundaries = @internal.grapheme_boundaries(line)
let clusters : Array[String] = []
let widths : Array[Int] = []
let spaces : Array[Bool] = []
for i in 0..<(boundaries.length() - 1) {
let cluster = wrap_chars_to_string(chars, boundaries[i], boundaries[i + 1])
clusters.push(cluster)
widths.push(visible_width(cluster))
spaces.push(wrap_is_space_cluster(cluster))
}
let last_boundary = boundaries[boundaries.length() - 1]
if last_boundary < chars.length() {
clusters.push(wrap_chars_to_string(chars, last_boundary, chars.length()))
widths.push(0)
spaces.push(false)
}
(clusters, widths, spaces)
}
///|
fn wrap_write_clusters(
buf : StringBuilder,
clusters : Array[String],
start : Int,
end_index : Int,
) -> Unit {
for i in start.. Unit {
let chars = cluster.to_array()
let mut i = 0
while i < chars.length() {
if chars[i] == '\u001b' {
let next = @internal.skip_ansi_sequence(chars, i)
wrap_write_chars(buf, chars, i, next)
i = next
} else {
i = i + 1
}
}
}
///|
fn wrap_write_ansi_sequences_from_clusters(
buf : StringBuilder,
clusters : Array[String],
start : Int,
end_index : Int,
) -> Unit {
for i in start.. StringBuilder {
lines.push(current.to_string())
StringBuilder::new()
}
///|
fn wrap_line_by_words(line : String, width : Int) -> String {
let (clusters, widths, spaces) = wrap_clusters(line)
if clusters.length() == 0 {
return line
}
let lines : Array[String] = []
let mut current = StringBuilder::new(size_hint=line.to_array().length())
let mut current_width = 0
let mut index = 0
while index < clusters.length() {
let space_start = index
let mut space_width = 0
while index < clusters.length() && spaces[index] {
space_width = space_width + widths[index]
index = index + 1
}
let word_start = index
let mut word_width = 0
while index < clusters.length() && !spaces[index] {
word_width = word_width + widths[index]
index = index + 1
}
if word_start == index {
if current_width + space_width <= width {
wrap_write_clusters(current, clusters, space_start, index)
current_width = current_width + space_width
} else {
wrap_write_ansi_sequences_from_clusters(
current, clusters, space_start, index,
)
}
continue
}
if space_start < word_start {
if current_width == 0 {
if space_width + word_width <= width {
wrap_write_clusters(current, clusters, space_start, word_start)
current_width = current_width + space_width
} else {
wrap_write_ansi_sequences_from_clusters(
current, clusters, space_start, word_start,
)
}
} else if current_width + space_width + word_width <= width {
wrap_write_clusters(current, clusters, space_start, word_start)
current_width = current_width + space_width
} else {
wrap_write_ansi_sequences_from_clusters(
current, clusters, space_start, word_start,
)
current = wrap_new_line(lines, current)
current_width = 0
}
}
if word_width <= width - current_width {
wrap_write_clusters(current, clusters, word_start, index)
current_width = current_width + word_width
} else {
if current_width > 0 {
current = wrap_new_line(lines, current)
current_width = 0
}
let mut word_index = word_start
while word_index < index {
let cluster_width = widths[word_index]
if current_width >= width && cluster_width > 0 {
current = wrap_new_line(lines, current)
current_width = 0
} else if current_width > 0 &&
cluster_width > 0 &&
current_width + cluster_width > width {
current = wrap_new_line(lines, current)
current_width = 0
}
current.write_string(clusters[word_index])
current_width = current_width + cluster_width
word_index = word_index + 1
if current_width >= width && word_index < index {
current = wrap_new_line(lines, current)
current_width = 0
}
}
}
}
lines.push(current.to_string())
@internal.join_lines(lines)
}
///|
/// Wrap text to `width` visible terminal cells at word boundaries.
///
/// CSI, OSC, and bare two-byte ESC sequences follow `visible_width`'s
/// zero-width escape handling, and printable text is walked on
/// `@internal.grapheme_boundaries`, so hard breaks never split a grapheme
/// cluster. Newlines already present in `text` delimit independent paragraphs.
/// A non-positive `width` returns `text` unchanged.
pub fn word_wrap(text : String, width : Int) -> String {
if width <= 0 {
return text
}
let lines : Array[String] = []
for line in split_lines(text) {
lines.push(wrap_line_by_words(line, width))
}
@internal.join_lines(lines)
}