use smallvec::SmallVec;
use std::borrow::Cow;
use crate::cells::{cell_len, char_width, set_cell_size};
use crate::style::{Style, StyleMeta};
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ControlType {
Bell,
CarriageReturn,
Home,
Clear,
ShowCursor,
HideCursor,
EnableAltScreen,
DisableAltScreen,
SetTitle,
CursorUp(u16),
CursorDown(u16),
CursorForward(u16),
CursorBackward(u16),
EraseInLine(u8),
HyperlinkStart { url: Arc<str>, id: Option<Arc<str>> },
HyperlinkEnd,
MoveTo { x: u16, y: u16 },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Segment {
pub text: Cow<'static, str>,
pub style: Option<Style>,
pub meta: Option<StyleMeta>,
pub control: Option<ControlType>,
}
impl Segment {
pub fn new(text: impl Into<Cow<'static, str>>) -> Self {
Segment {
text: text.into(),
style: None,
meta: None,
control: None,
}
}
pub fn styled(text: impl Into<Cow<'static, str>>, style: Style) -> Self {
Segment {
text: text.into(),
style: Some(style),
meta: None,
control: None,
}
}
pub fn styled_with_meta(
text: impl Into<Cow<'static, str>>,
style: Style,
meta: StyleMeta,
) -> Self {
Segment {
text: text.into(),
style: Some(style),
meta: if meta.is_empty() { None } else { Some(meta) },
control: None,
}
}
pub fn new_with_meta(text: impl Into<Cow<'static, str>>, meta: StyleMeta) -> Self {
Segment {
text: text.into(),
style: None,
meta: if meta.is_empty() { None } else { Some(meta) },
control: None,
}
}
pub fn control(control: ControlType) -> Self {
Segment {
text: Cow::Borrowed(""),
style: None,
meta: None,
control: Some(control),
}
}
pub fn line() -> Self {
Segment::new("\n")
}
pub fn is_control(&self) -> bool {
self.control.is_some()
}
pub fn cell_len(&self) -> usize {
crate::cells::cell_len(&self.text)
}
pub fn apply_style(&self, style: &Style) -> Self {
Segment {
text: self.text.clone(),
style: Some(match &self.style {
Some(existing) => existing.combine(style),
None => *style,
}),
meta: self.meta.clone(),
control: self.control.clone(),
}
}
pub fn split_cells(&self, cut: usize) -> (Segment, Segment) {
let text = &self.text;
let style = self.style;
let meta = self.meta.clone();
let control = self.control.clone();
if control.is_some() {
return (
self.clone(),
Segment::new_with_style_control("", style, meta, control),
);
}
let segment_cell_len = cell_len(text);
if cut >= segment_cell_len {
return (
self.clone(),
Segment::new_with_style_control("", style, meta, control),
);
}
if cut == 0 {
return (
Segment::new_with_style_control("", style, meta, control),
self.clone(),
);
}
if text.is_ascii() {
let before = &text[..cut];
let after = &text[cut..];
return (
Segment::new_with_style_control(
before.to_string(),
style,
meta.clone(),
control.clone(),
),
Segment::new_with_style_control(after.to_string(), style, meta, control),
);
}
let mut current_cell_pos = 0;
for (byte_idx, c) in text.char_indices() {
let c_width = char_width(c);
if current_cell_pos == cut {
let before = &text[..byte_idx];
let after = &text[byte_idx..];
return (
Segment::new_with_style_control(
before.to_string(),
style,
meta.clone(),
control.clone(),
),
Segment::new_with_style_control(
after.to_string(),
style,
meta.clone(),
control,
),
);
}
if current_cell_pos + c_width > cut {
let before = &text[..byte_idx];
let after_start_byte = byte_idx + c.len_utf8();
let after = &text[after_start_byte..];
let before_with_space = format!("{} ", before);
let after_with_space = format!(" {}", after);
return (
Segment::new_with_style_control(
before_with_space,
style,
meta.clone(),
control.clone(),
),
Segment::new_with_style_control(after_with_space, style, meta.clone(), control),
);
}
current_cell_pos += c_width;
}
(
self.clone(),
Segment::new_with_style_control("", style, meta, control),
)
}
fn new_with_style_control(
text: impl Into<Cow<'static, str>>,
style: Option<Style>,
meta: Option<StyleMeta>,
control: Option<ControlType>,
) -> Self {
Segment {
text: text.into(),
style,
meta,
control,
}
}
pub fn split_lines(segments: impl IntoIterator<Item = Segment>) -> Vec<Vec<Segment>> {
let mut lines: Vec<Vec<Segment>> = Vec::new();
let mut current_line: Vec<Segment> = Vec::new();
for segment in segments {
if segment.text.contains('\n') && segment.control.is_none() {
let text = segment.text.to_string();
let style = segment.style;
let meta = segment.meta.clone();
let mut remaining = text.as_str();
while !remaining.is_empty() {
if let Some(newline_pos) = remaining.find('\n') {
let before = &remaining[..newline_pos];
if !before.is_empty() {
current_line.push(Segment::new_with_style_control(
before.to_string(),
style,
meta.clone(),
None,
));
}
lines.push(std::mem::take(&mut current_line));
remaining = &remaining[newline_pos + 1..];
} else {
if !remaining.is_empty() {
current_line.push(Segment::new_with_style_control(
remaining.to_string(),
style,
meta.clone(),
None,
));
}
break;
}
}
} else {
current_line.push(segment);
}
}
if !current_line.is_empty() {
lines.push(current_line);
}
lines
}
pub fn split_and_crop_lines(
segments: impl IntoIterator<Item = Segment>,
length: usize,
style: Option<Style>,
pad: bool,
include_new_lines: bool,
) -> Vec<Vec<Segment>> {
let mut lines: Vec<Vec<Segment>> = Vec::new();
let mut current_line: Vec<Segment> = Vec::new();
let new_line_segment = Segment::line();
for segment in segments {
if segment.text.contains('\n') && segment.control.is_none() {
let text = segment.text.to_string();
let segment_style = segment.style;
let segment_meta = segment.meta.clone();
let mut remaining = text.as_str();
while !remaining.is_empty() {
if let Some(newline_pos) = remaining.find('\n') {
let before = &remaining[..newline_pos];
if !before.is_empty() {
current_line.push(Segment::new_with_style_control(
before.to_string(),
segment_style,
segment_meta.clone(),
None,
));
}
let mut cropped =
Self::adjust_line_length(¤t_line, length, style, pad);
if include_new_lines {
cropped.push(new_line_segment.clone());
}
lines.push(cropped);
current_line.clear();
remaining = &remaining[newline_pos + 1..];
} else {
if !remaining.is_empty() {
current_line.push(Segment::new_with_style_control(
remaining.to_string(),
segment_style,
segment_meta.clone(),
None,
));
}
break;
}
}
} else {
current_line.push(segment);
}
}
if !current_line.is_empty() {
lines.push(Self::adjust_line_length(¤t_line, length, style, pad));
}
lines
}
pub fn adjust_line_length(
line: &[Segment],
length: usize,
style: Option<Style>,
pad: bool,
) -> Vec<Segment> {
let line_length = Self::get_line_length(line);
if line_length < length {
if pad {
let mut new_line = line.to_vec();
let padding = " ".repeat(length - line_length);
let end_style = line.iter().rev().find_map(|seg| {
if seg.control.is_some() {
return None;
}
seg.style
});
let padding_style = match (style, end_style) {
(Some(mut base), Some(end)) => {
if base.bgcolor.is_none() {
if let Some(bg) = end.bgcolor {
base.bgcolor = Some(bg);
}
}
Some(base)
}
(Some(base), None) => Some(base),
(None, Some(end)) => end.bgcolor.map(|bg| Style::new().with_bgcolor(bg)),
(None, None) => None,
};
new_line.push(Segment::new_with_style_control(
padding,
padding_style,
None,
None,
));
new_line
} else {
line.to_vec()
}
} else if line_length > length {
let mut new_line = Vec::new();
let mut current_length = 0;
for segment in line {
let segment_length = segment.cell_len();
if segment.control.is_some() {
new_line.push(segment.clone());
continue;
}
if current_length + segment_length <= length {
new_line.push(segment.clone());
current_length += segment_length;
} else {
let remaining_space = length - current_length;
if remaining_space > 0 {
let cropped_text = set_cell_size(&segment.text, remaining_space);
new_line.push(Segment::new_with_style_control(
cropped_text,
segment.style,
segment.meta.clone(),
None,
));
}
break;
}
}
new_line
} else {
line.to_vec()
}
}
pub fn get_last_style(line: &[Segment]) -> Option<Style> {
line.iter().rev().find_map(|seg| {
if seg.control.is_some() {
None
} else {
seg.style
}
})
}
pub fn simplify(segments: impl IntoIterator<Item = Segment>) -> Segments {
let mut result = Segments::new();
let mut iter = segments.into_iter();
let Some(mut last_segment) = iter.next() else {
return result;
};
for segment in iter {
if last_segment.style == segment.style
&& last_segment.meta == segment.meta
&& last_segment.control.is_none()
&& segment.control.is_none()
{
let merged_text = format!("{}{}", last_segment.text, segment.text);
last_segment = Segment::new_with_style_control(
merged_text,
last_segment.style,
last_segment.meta.clone(),
None,
);
} else {
result.push(last_segment);
last_segment = segment;
}
}
result.push(last_segment);
result
}
pub fn divide(
segments: impl IntoIterator<Item = Segment>,
cuts: &[usize],
) -> Vec<Vec<Segment>> {
debug_assert!(
cuts.windows(2).all(|w| w[0] <= w[1]),
"cuts must be sorted in ascending order"
);
if cuts.is_empty() {
return Vec::new();
}
let mut result: Vec<Vec<Segment>> = Vec::new();
let mut split_segments: Vec<Segment> = Vec::new();
let mut cut_iter = cuts.iter().copied();
let mut current_cut;
loop {
match cut_iter.next() {
None => return result,
Some(0) => result.push(Vec::new()),
Some(c) => {
current_cut = c;
break;
}
}
}
let mut pos: usize = 0;
let mut cuts_exhausted = false;
for segment in segments {
if segment.control.is_some() {
split_segments.push(segment);
continue;
}
if cuts_exhausted {
split_segments.push(segment);
continue;
}
let mut current_segment = segment;
loop {
let text = ¤t_segment.text;
if text.is_empty() {
break;
}
let seg_len = cell_len(text);
let end_pos = pos + seg_len;
if end_pos < current_cut {
split_segments.push(current_segment);
pos = end_pos;
break;
} else if end_pos == current_cut {
split_segments.push(current_segment);
result.push(std::mem::take(&mut split_segments));
pos = end_pos;
match cut_iter.next() {
None => {
cuts_exhausted = true;
}
Some(next_cut) => current_cut = next_cut,
}
break;
} else {
let split_point = current_cut - pos;
let (before, after) = current_segment.split_cells(split_point);
if !before.text.is_empty() {
split_segments.push(before);
}
result.push(std::mem::take(&mut split_segments));
pos = current_cut;
current_segment = after;
match cut_iter.next() {
None => {
if !current_segment.text.is_empty() {
split_segments.push(current_segment);
}
cuts_exhausted = true;
break;
}
Some(next_cut) => current_cut = next_cut,
}
}
}
}
result.push(split_segments);
result
}
pub fn apply_style_to_segments(
segments: impl IntoIterator<Item = Segment>,
style: Option<Style>,
post_style: Option<Style>,
) -> Segments {
let mut result = Segments::new();
for segment in segments {
if segment.control.is_some() {
result.push(segment);
continue;
}
let mut new_style = segment.style;
if let Some(base) = style {
new_style = Some(match new_style {
Some(existing) => base.combine(&existing),
None => base,
});
}
if let Some(post) = post_style {
new_style = Some(match new_style {
Some(existing) => existing.combine(&post),
None => post,
});
}
result.push(Segment {
text: segment.text,
style: new_style,
meta: segment.meta,
control: None,
});
}
result
}
pub fn filter_control(
segments: impl IntoIterator<Item = Segment>,
is_control: bool,
) -> Segments {
segments
.into_iter()
.filter(|s| s.is_control() == is_control)
.collect()
}
pub fn strip_styles(segments: impl IntoIterator<Item = Segment>) -> Segments {
segments
.into_iter()
.map(|s| Segment {
text: s.text,
style: None,
meta: None,
control: s.control,
})
.collect()
}
pub fn get_line_length(line: &[Segment]) -> usize {
line.iter()
.filter(|s| s.control.is_none())
.map(|s| cell_len(&s.text))
.sum()
}
pub fn get_shape(lines: &[Vec<Segment>]) -> (usize, usize) {
if lines.is_empty() {
return (0, 0);
}
let max_width = lines
.iter()
.map(|line| Self::get_line_length(line))
.max()
.unwrap_or(0);
(max_width, lines.len())
}
pub fn set_shape(
lines: &[Vec<Segment>],
width: usize,
height: Option<usize>,
style: Option<Style>,
new_lines: bool,
) -> Vec<Vec<Segment>> {
let target_height = height.unwrap_or(lines.len());
let blank_text = if new_lines {
format!("{}\n", " ".repeat(width))
} else {
" ".repeat(width)
};
let blank = vec![Segment::new_with_style_control(
blank_text, style, None, None,
)];
let mut result: Vec<Vec<Segment>> = lines
.iter()
.take(target_height)
.map(|line| Self::adjust_line_length(line, width, style, true))
.collect();
while result.len() < target_height {
result.push(blank.clone());
}
result
}
pub fn align_top(
lines: &[Vec<Segment>],
width: usize,
height: usize,
style: Option<Style>,
new_lines: bool,
) -> Vec<Vec<Segment>> {
let extra_lines = height.saturating_sub(lines.len());
if extra_lines == 0 {
return lines[..height.min(lines.len())].to_vec();
}
let mut result: Vec<Vec<Segment>> = lines[..height.min(lines.len())].to_vec();
let blank_text = if new_lines {
format!("{}\n", " ".repeat(width))
} else {
" ".repeat(width)
};
let blank = vec![Segment::new_with_style_control(
blank_text, style, None, None,
)];
for _ in 0..extra_lines {
result.push(blank.clone());
}
result
}
pub fn align_bottom(
lines: &[Vec<Segment>],
width: usize,
height: usize,
style: Option<Style>,
new_lines: bool,
) -> Vec<Vec<Segment>> {
let extra_lines = height.saturating_sub(lines.len());
if extra_lines == 0 {
return lines[..height.min(lines.len())].to_vec();
}
let blank_text = if new_lines {
format!("{}\n", " ".repeat(width))
} else {
" ".repeat(width)
};
let blank = vec![Segment::new_with_style_control(
blank_text, style, None, None,
)];
let mut result: Vec<Vec<Segment>> = Vec::with_capacity(height);
for _ in 0..extra_lines {
result.push(blank.clone());
}
result.extend_from_slice(&lines[..height.min(lines.len())]);
result
}
pub fn align_middle(
lines: &[Vec<Segment>],
width: usize,
height: usize,
style: Option<Style>,
new_lines: bool,
) -> Vec<Vec<Segment>> {
let extra_lines = height.saturating_sub(lines.len());
if extra_lines == 0 {
return lines[..height.min(lines.len())].to_vec();
}
let blank_text = if new_lines {
format!("{}\n", " ".repeat(width))
} else {
" ".repeat(width)
};
let blank = vec![Segment::new_with_style_control(
blank_text, style, None, None,
)];
let top_lines = extra_lines / 2;
let bottom_lines = extra_lines - top_lines;
let mut result: Vec<Vec<Segment>> = Vec::with_capacity(height);
for _ in 0..top_lines {
result.push(blank.clone());
}
result.extend_from_slice(&lines[..height.min(lines.len())]);
for _ in 0..bottom_lines {
result.push(blank.clone());
}
result
}
pub fn split_lines_terminator(
segments: impl IntoIterator<Item = Segment>,
) -> Vec<(Vec<Segment>, bool)> {
let mut result: Vec<(Vec<Segment>, bool)> = Vec::new();
let mut current_line: Vec<Segment> = Vec::new();
for segment in segments {
if segment.text.contains('\n') && segment.control.is_none() {
let text = segment.text.to_string();
let style = segment.style;
let meta = segment.meta.clone();
let mut remaining = text.as_str();
while !remaining.is_empty() {
if let Some(newline_pos) = remaining.find('\n') {
let before = &remaining[..newline_pos];
if !before.is_empty() {
current_line.push(Segment::new_with_style_control(
before.to_string(),
style,
meta.clone(),
None,
));
}
result.push((std::mem::take(&mut current_line), true));
remaining = &remaining[newline_pos + 1..];
} else {
if !remaining.is_empty() {
current_line.push(Segment::new_with_style_control(
remaining.to_string(),
style,
meta.clone(),
None,
));
}
break;
}
}
} else {
current_line.push(segment);
}
}
if !current_line.is_empty() {
result.push((current_line, false));
}
result
}
pub fn strip_links(segments: impl IntoIterator<Item = Segment>) -> Segments {
segments
.into_iter()
.map(|s| Segment {
text: s.text,
style: s.style,
meta: None,
control: s.control,
})
.collect()
}
pub fn remove_color(segments: impl IntoIterator<Item = Segment>) -> Segments {
segments
.into_iter()
.map(|s| {
let new_style = s.style.map(|style| style.without_color());
Segment {
text: s.text,
style: new_style,
meta: s.meta,
control: s.control,
}
})
.collect()
}
}
#[derive(Debug, Clone)]
pub struct SegmentLines {
pub lines: Vec<Vec<Segment>>,
pub new_lines: bool,
}
impl SegmentLines {
pub fn new(lines: Vec<Vec<Segment>>, new_lines: bool) -> Self {
SegmentLines { lines, new_lines }
}
pub fn to_segments(&self) -> Segments {
let mut result = Segments::new();
let new_line = Segment::line();
for line in &self.lines {
for seg in line {
result.push(seg.clone());
}
if self.new_lines {
result.push(new_line.clone());
}
}
result
}
}
impl Default for Segment {
fn default() -> Self {
Segment::new("")
}
}
impl From<&'static str> for Segment {
fn from(s: &'static str) -> Self {
Segment::new(s)
}
}
impl From<String> for Segment {
fn from(s: String) -> Self {
Segment::new(s)
}
}
#[derive(Debug, Clone, Default)]
pub struct Segments(SmallVec<[Segment; 8]>);
impl Segments {
pub fn new() -> Self {
Segments(SmallVec::new())
}
pub fn one(segment: Segment) -> Self {
let mut sv = SmallVec::new();
sv.push(segment);
Segments(sv)
}
pub fn push(&mut self, segment: Segment) {
self.0.push(segment);
}
pub fn extend(&mut self, iter: impl IntoIterator<Item = Segment>) {
self.0.extend(iter);
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = &Segment> {
self.0.iter()
}
pub fn cell_len(&self) -> usize {
self.0.iter().map(|s| s.cell_len()).sum()
}
pub fn into_vec(self) -> Vec<Segment> {
self.0.into_vec()
}
}
impl From<Segment> for Segments {
fn from(segment: Segment) -> Self {
Segments::one(segment)
}
}
impl From<Vec<Segment>> for Segments {
fn from(vec: Vec<Segment>) -> Self {
Segments(SmallVec::from_vec(vec))
}
}
impl FromIterator<Segment> for Segments {
fn from_iter<I: IntoIterator<Item = Segment>>(iter: I) -> Self {
Segments(iter.into_iter().collect())
}
}
impl IntoIterator for Segments {
type Item = Segment;
type IntoIter = smallvec::IntoIter<[Segment; 8]>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl<'a> IntoIterator for &'a Segments {
type Item = &'a Segment;
type IntoIter = std::slice::Iter<'a, Segment>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_segment() {
let seg = Segment::new("hello");
assert_eq!(&*seg.text, "hello");
assert!(seg.style.is_none());
assert!(seg.control.is_none());
}
#[test]
fn test_cell_len() {
let seg = Segment::new("hello");
assert_eq!(seg.cell_len(), 5);
}
#[test]
fn test_segments_collection() {
let mut segs = Segments::new();
segs.push(Segment::new("hello"));
segs.push(Segment::new(" "));
segs.push(Segment::new("world"));
assert_eq!(segs.len(), 3);
assert_eq!(segs.cell_len(), 11);
}
#[test]
fn test_segments_from_iter() {
let segs: Segments = vec![Segment::new("a"), Segment::new("b"), Segment::new("c")]
.into_iter()
.collect();
assert_eq!(segs.len(), 3);
}
#[test]
fn test_split_cells_ascii() {
let seg = Segment::new("hello");
let (before, after) = seg.split_cells(3);
assert_eq!(&*before.text, "hel");
assert_eq!(&*after.text, "lo");
}
#[test]
fn test_split_cells_at_start() {
let seg = Segment::new("hello");
let (before, after) = seg.split_cells(0);
assert_eq!(&*before.text, "");
assert_eq!(&*after.text, "hello");
}
#[test]
fn test_split_cells_at_end() {
let seg = Segment::new("hello");
let (before, after) = seg.split_cells(5);
assert_eq!(&*before.text, "hello");
assert_eq!(&*after.text, "");
}
#[test]
fn test_split_cells_beyond_end() {
let seg = Segment::new("hello");
let (before, after) = seg.split_cells(10);
assert_eq!(&*before.text, "hello");
assert_eq!(&*after.text, "");
}
#[test]
fn test_split_cells_cjk_exact() {
let seg = Segment::new("ä½ å¥½");
let (before, after) = seg.split_cells(2);
assert_eq!(&*before.text, "ä½ ");
assert_eq!(&*after.text, "好");
}
#[test]
fn test_split_cells_cjk_middle() {
let seg = Segment::new("ä½ å¥½");
let (before, after) = seg.split_cells(1);
assert_eq!(&*before.text, " "); assert_eq!(&*after.text, " 好"); }
#[test]
fn test_split_cells_cjk_middle_complex() {
let seg = Segment::new("ä½ å¥½ä¸–ç•Œ");
let (before, after) = seg.split_cells(3);
assert_eq!(&*before.text, "ä½ "); assert_eq!(&*after.text, " 世界"); }
#[test]
fn test_split_cells_mixed_content() {
let seg = Segment::new("aä½ b");
let (before, after) = seg.split_cells(3);
assert_eq!(&*before.text, "aä½ ");
assert_eq!(&*after.text, "b");
}
#[test]
fn test_split_cells_preserves_style() {
let style = Style::new().with_bold(true);
let seg = Segment::styled("hello", style);
let (before, after) = seg.split_cells(2);
assert_eq!(before.style, Some(style));
assert_eq!(after.style, Some(style));
}
#[test]
fn test_split_cells_control_segment() {
let seg = Segment::control(ControlType::Bell);
let (before, after) = seg.split_cells(5);
assert!(before.control.is_some());
assert_eq!(&*after.text, "");
}
#[test]
fn test_split_cells_emoji() {
let seg = Segment::new("😀hello");
let (before, after) = seg.split_cells(2);
assert_eq!(&*before.text, "😀");
assert_eq!(&*after.text, "hello");
}
#[test]
fn test_split_lines_no_newlines() {
let segments = vec![Segment::new("hello"), Segment::new(" world")];
let lines = Segment::split_lines(segments);
assert_eq!(lines.len(), 1);
assert_eq!(lines[0].len(), 2);
}
#[test]
fn test_split_lines_single_newline() {
let segments = vec![Segment::new("hello\nworld")];
let lines = Segment::split_lines(segments);
assert_eq!(lines.len(), 2);
assert_eq!(&*lines[0][0].text, "hello");
assert_eq!(&*lines[1][0].text, "world");
}
#[test]
fn test_split_lines_multiple_newlines() {
let segments = vec![Segment::new("a\nb\nc")];
let lines = Segment::split_lines(segments);
assert_eq!(lines.len(), 3);
assert_eq!(&*lines[0][0].text, "a");
assert_eq!(&*lines[1][0].text, "b");
assert_eq!(&*lines[2][0].text, "c");
}
#[test]
fn test_split_lines_trailing_newline() {
let segments = vec![Segment::new("hello\n")];
let lines = Segment::split_lines(segments);
assert_eq!(lines.len(), 1);
assert_eq!(&*lines[0][0].text, "hello");
}
#[test]
fn test_split_lines_preserves_style() {
let style = Style::new().with_bold(true);
let segments = vec![Segment::styled("hello\nworld", style)];
let lines = Segment::split_lines(segments);
assert_eq!(lines[0][0].style, Some(style));
assert_eq!(lines[1][0].style, Some(style));
}
#[test]
fn test_split_lines_control_segment_unaffected() {
let segments = vec![
Segment::new("hello"),
Segment::control(ControlType::Bell),
Segment::new("world"),
];
let lines = Segment::split_lines(segments);
assert_eq!(lines.len(), 1);
assert_eq!(lines[0].len(), 3);
}
#[test]
fn test_split_and_crop_lines_basic() {
let segments = vec![Segment::new("hello world")];
let lines = Segment::split_and_crop_lines(segments, 5, None, true, false);
assert_eq!(lines.len(), 1);
let total_len: usize = lines[0].iter().map(|s| cell_len(&s.text)).sum();
assert_eq!(total_len, 5);
}
#[test]
fn test_split_and_crop_lines_with_newlines() {
let segments = vec![Segment::new("hello\nworld")];
let lines = Segment::split_and_crop_lines(segments, 10, None, true, false);
assert_eq!(lines.len(), 2);
}
#[test]
fn test_split_and_crop_lines_include_newlines() {
let segments = vec![Segment::new("hello\nworld")];
let lines = Segment::split_and_crop_lines(segments, 10, None, true, true);
assert_eq!(lines.len(), 2);
let last_seg = lines[0].last().unwrap();
assert_eq!(&*last_seg.text, "\n");
}
#[test]
fn test_split_and_crop_lines_padding() {
let segments = vec![Segment::new("hi")];
let lines = Segment::split_and_crop_lines(segments, 5, None, true, false);
let total_len: usize = lines[0].iter().map(|s| cell_len(&s.text)).sum();
assert_eq!(total_len, 5); }
#[test]
fn test_split_and_crop_lines_no_padding() {
let segments = vec![Segment::new("hi")];
let lines = Segment::split_and_crop_lines(segments, 5, None, false, false);
let total_len: usize = lines[0].iter().map(|s| cell_len(&s.text)).sum();
assert_eq!(total_len, 2); }
#[test]
fn test_adjust_line_length_exact() {
let line = vec![Segment::new("hello")];
let result = Segment::adjust_line_length(&line, 5, None, true);
assert_eq!(Segment::get_line_length(&result), 5);
}
#[test]
fn test_adjust_line_length_pad() {
let line = vec![Segment::new("hi")];
let result = Segment::adjust_line_length(&line, 5, None, true);
assert_eq!(Segment::get_line_length(&result), 5);
assert_eq!(result.len(), 2); }
#[test]
fn test_adjust_line_length_pad_inherits_end_style() {
let end_style = Style::new().with_bgcolor(crate::SimpleColor::Rgb { r: 1, g: 2, b: 3 });
let line = vec![Segment::styled("x", end_style)];
let result = Segment::adjust_line_length(&line, 3, None, true);
assert_eq!(Segment::get_line_length(&result), 3);
let padding = result.last().unwrap();
assert_eq!(&*padding.text, " ");
assert_eq!(padding.style.unwrap().bgcolor, end_style.bgcolor);
}
#[test]
fn test_adjust_line_length_pad_combines_base_and_end_style() {
let base = Style::new().with_bold(true);
let end_style = Style::new().with_bgcolor(crate::SimpleColor::Rgb { r: 4, g: 5, b: 6 });
let line = vec![Segment::styled("x", end_style)];
let result = Segment::adjust_line_length(&line, 3, Some(base), true);
let padding = result.last().unwrap().style.unwrap();
assert_eq!(padding.bold, Some(true));
assert_eq!(padding.bgcolor, end_style.bgcolor);
}
#[test]
fn test_adjust_line_length_no_pad() {
let line = vec![Segment::new("hi")];
let result = Segment::adjust_line_length(&line, 5, None, false);
assert_eq!(Segment::get_line_length(&result), 2);
}
#[test]
fn test_adjust_line_length_crop() {
let line = vec![Segment::new("hello world")];
let result = Segment::adjust_line_length(&line, 5, None, true);
assert_eq!(Segment::get_line_length(&result), 5);
}
#[test]
fn test_adjust_line_length_crop_multiple_segments() {
let line = vec![Segment::new("hello"), Segment::new(" world")];
let result = Segment::adjust_line_length(&line, 7, None, true);
assert_eq!(Segment::get_line_length(&result), 7);
}
#[test]
fn test_adjust_line_length_preserves_control() {
let line = vec![Segment::control(ControlType::Bell), Segment::new("hello")];
let result = Segment::adjust_line_length(&line, 3, None, true);
assert!(result[0].control.is_some());
}
#[test]
fn test_adjust_line_length_crop_cjk() {
let line = vec![Segment::new("ä½ å¥½ä¸–ç•Œ")]; let result = Segment::adjust_line_length(&line, 5, None, true);
assert_eq!(Segment::get_line_length(&result), 5);
}
#[test]
fn test_simplify_empty() {
let segments: Vec<Segment> = vec![];
let result = Segment::simplify(segments);
assert!(result.is_empty());
}
#[test]
fn test_simplify_single() {
let segments = vec![Segment::new("hello")];
let result = Segment::simplify(segments);
assert_eq!(result.len(), 1);
assert_eq!(&*result.iter().next().unwrap().text, "hello");
}
#[test]
fn test_simplify_same_style() {
let segments = vec![Segment::new("hello"), Segment::new(" world")];
let result = Segment::simplify(segments);
assert_eq!(result.len(), 1);
assert_eq!(&*result.iter().next().unwrap().text, "hello world");
}
#[test]
fn test_simplify_different_styles() {
let style1 = Style::new().with_bold(true);
let style2 = Style::new().with_italic(true);
let segments = vec![
Segment::styled("hello", style1),
Segment::styled(" world", style2),
];
let result = Segment::simplify(segments);
assert_eq!(result.len(), 2);
}
#[test]
fn test_simplify_control_not_merged() {
let segments = vec![
Segment::new("hello"),
Segment::control(ControlType::Bell),
Segment::new(" world"),
];
let result = Segment::simplify(segments);
assert_eq!(result.len(), 3);
}
#[test]
fn test_simplify_mixed() {
let style = Style::new().with_bold(true);
let segments = vec![
Segment::new("a"),
Segment::new("b"),
Segment::styled("c", style),
Segment::styled("d", style),
Segment::new("e"),
];
let result = Segment::simplify(segments);
assert_eq!(result.len(), 3);
let texts: Vec<&str> = result.iter().map(|s| &*s.text).collect();
assert_eq!(texts, vec!["ab", "cd", "e"]);
}
#[test]
fn test_divide_empty_cuts() {
let segments = vec![Segment::new("hello")];
let result = Segment::divide(segments, &[]);
assert!(result.is_empty());
}
#[test]
fn test_divide_single_cut() {
let segments = vec![Segment::new("hello world")];
let result = Segment::divide(segments, &[5]);
assert_eq!(result.len(), 2);
let first_text: String = result[0].iter().map(|s| s.text.to_string()).collect();
let second_text: String = result[1].iter().map(|s| s.text.to_string()).collect();
assert_eq!(first_text, "hello");
assert_eq!(second_text, " world");
}
#[test]
fn test_divide_multiple_cuts() {
let segments = vec![Segment::new("hello world!")];
let result = Segment::divide(segments, &[5, 11]);
assert_eq!(result.len(), 3);
let first: String = result[0].iter().map(|s| s.text.to_string()).collect();
let second: String = result[1].iter().map(|s| s.text.to_string()).collect();
let third: String = result[2].iter().map(|s| s.text.to_string()).collect();
assert_eq!(first, "hello");
assert_eq!(second, " world");
assert_eq!(third, "!");
}
#[test]
fn test_divide_includes_remainder() {
let segments = vec![Segment::new("hello world!")];
let result = Segment::divide(segments, &[5, 12]);
assert_eq!(result.len(), 3);
let first: String = result[0].iter().map(|s| s.text.to_string()).collect();
let second: String = result[1].iter().map(|s| s.text.to_string()).collect();
let third: String = result[2].iter().map(|s| s.text.to_string()).collect();
assert_eq!(first, "hello");
assert_eq!(second, " world!");
assert_eq!(third, ""); }
#[test]
fn test_divide_zero_cut() {
let segments = vec![Segment::new("hello")];
let result = Segment::divide(segments, &[0, 3]);
assert_eq!(result.len(), 3);
assert!(result[0].is_empty()); let second: String = result[1].iter().map(|s| s.text.to_string()).collect();
let third: String = result[2].iter().map(|s| s.text.to_string()).collect();
assert_eq!(second, "hel");
assert_eq!(third, "lo");
}
#[test]
fn test_divide_cjk() {
let segments = vec![Segment::new("ä½ å¥½ä¸–ç•Œ")];
let result = Segment::divide(segments, &[4]);
assert_eq!(result.len(), 2);
let first: String = result[0].iter().map(|s| s.text.to_string()).collect();
let second: String = result[1].iter().map(|s| s.text.to_string()).collect();
assert_eq!(first, "ä½ å¥½");
assert_eq!(second, "世界");
}
#[test]
fn test_divide_trailing_content_after_last_cut() {
let segments = vec![Segment::new("abc123xyz")];
let result = Segment::divide(segments, &[3, 6]);
assert_eq!(result.len(), 3);
let first: String = result[0].iter().map(|s| s.text.to_string()).collect();
let second: String = result[1].iter().map(|s| s.text.to_string()).collect();
let third: String = result[2].iter().map(|s| s.text.to_string()).collect();
assert_eq!(first, "abc");
assert_eq!(second, "123");
assert_eq!(third, "xyz");
}
#[test]
fn test_divide_empty_trailing_partition_when_content_ends_at_cut() {
let segments = vec![Segment::new("hello")];
let result = Segment::divide(segments, &[5]);
assert_eq!(result.len(), 2);
let first: String = result[0].iter().map(|s| s.text.to_string()).collect();
let second: String = result[1].iter().map(|s| s.text.to_string()).collect();
assert_eq!(first, "hello");
assert_eq!(second, "");
}
#[test]
fn test_divide_multiple_segments_with_trailing() {
let segments = vec![
Segment::new("hello"),
Segment::new(" "),
Segment::new("world"),
];
let result = Segment::divide(segments, &[6]);
assert_eq!(result.len(), 2);
let first: String = result[0].iter().map(|s| s.text.to_string()).collect();
let second: String = result[1].iter().map(|s| s.text.to_string()).collect();
assert_eq!(first, "hello ");
assert_eq!(second, "world");
}
#[test]
fn test_apply_style_base() {
let base = Style::new().with_bold(true);
let segments = vec![Segment::new("hello")];
let result = Segment::apply_style_to_segments(segments, Some(base), None);
assert_eq!(result.iter().next().unwrap().style, Some(base));
}
#[test]
fn test_apply_style_post() {
let post = Style::new().with_italic(true);
let segments = vec![Segment::new("hello")];
let result = Segment::apply_style_to_segments(segments, None, Some(post));
assert_eq!(result.iter().next().unwrap().style, Some(post));
}
#[test]
fn test_apply_style_both() {
let base = Style::new().with_bold(true);
let post = Style::new().with_italic(true);
let segments = vec![Segment::new("hello")];
let result = Segment::apply_style_to_segments(segments, Some(base), Some(post));
let style = result.iter().next().unwrap().style.unwrap();
assert_eq!(style.bold, Some(true));
assert_eq!(style.italic, Some(true));
}
#[test]
fn test_apply_style_combines_with_existing() {
let base = Style::new().with_bold(true);
let existing = Style::new().with_italic(true);
let segments = vec![Segment::styled("hello", existing)];
let result = Segment::apply_style_to_segments(segments, Some(base), None);
let style = result.iter().next().unwrap().style.unwrap();
assert_eq!(style.bold, Some(true));
assert_eq!(style.italic, Some(true));
}
#[test]
fn test_apply_style_control_unchanged() {
let base = Style::new().with_bold(true);
let segments = vec![Segment::control(ControlType::Bell)];
let result = Segment::apply_style_to_segments(segments, Some(base), None);
let seg = result.iter().next().unwrap();
assert!(seg.control.is_some());
}
#[test]
fn test_filter_control_keep_control() {
let segments = vec![
Segment::new("hello"),
Segment::control(ControlType::Bell),
Segment::new("world"),
];
let result = Segment::filter_control(segments, true);
assert_eq!(result.len(), 1);
assert!(result.iter().next().unwrap().control.is_some());
}
#[test]
fn test_filter_control_keep_non_control() {
let segments = vec![
Segment::new("hello"),
Segment::control(ControlType::Bell),
Segment::new("world"),
];
let result = Segment::filter_control(segments, false);
assert_eq!(result.len(), 2);
for seg in result.iter() {
assert!(seg.control.is_none());
}
}
#[test]
fn test_strip_styles() {
let style = Style::new().with_bold(true);
let segments = vec![
Segment::styled("hello", style),
Segment::styled("world", style),
];
let result = Segment::strip_styles(segments);
for seg in result.iter() {
assert!(seg.style.is_none());
}
}
#[test]
fn test_strip_styles_preserves_control() {
let segments = vec![Segment::control(ControlType::Bell)];
let result = Segment::strip_styles(segments);
assert!(result.iter().next().unwrap().control.is_some());
}
#[test]
fn test_get_line_length_simple() {
let line = vec![Segment::new("hello")];
assert_eq!(Segment::get_line_length(&line), 5);
}
#[test]
fn test_get_line_length_multiple() {
let line = vec![Segment::new("hello"), Segment::new(" world")];
assert_eq!(Segment::get_line_length(&line), 11);
}
#[test]
fn test_get_line_length_ignores_control() {
let line = vec![
Segment::new("hello"),
Segment::control(ControlType::Bell),
Segment::new("world"),
];
assert_eq!(Segment::get_line_length(&line), 10);
}
#[test]
fn test_get_line_length_cjk() {
let line = vec![Segment::new("ä½ å¥½")];
assert_eq!(Segment::get_line_length(&line), 4);
}
#[test]
fn test_get_shape_empty() {
let lines: Vec<Vec<Segment>> = vec![];
assert_eq!(Segment::get_shape(&lines), (0, 0));
}
#[test]
fn test_get_shape_single_line() {
let lines = vec![vec![Segment::new("hello")]];
assert_eq!(Segment::get_shape(&lines), (5, 1));
}
#[test]
fn test_get_shape_multiple_lines() {
let lines = vec![
vec![Segment::new("hello")],
vec![Segment::new("world!")],
vec![Segment::new("hi")],
];
assert_eq!(Segment::get_shape(&lines), (6, 3));
}
#[test]
fn test_set_shape_pad_width() {
let lines = vec![vec![Segment::new("hi")]];
let result = Segment::set_shape(&lines, 5, None, None, false);
assert_eq!(result.len(), 1);
assert_eq!(Segment::get_line_length(&result[0]), 5);
}
#[test]
fn test_set_shape_add_height() {
let lines = vec![vec![Segment::new("hello")]];
let result = Segment::set_shape(&lines, 5, Some(3), None, false);
assert_eq!(result.len(), 3);
}
#[test]
fn test_set_shape_crop_height() {
let lines = vec![
vec![Segment::new("a")],
vec![Segment::new("b")],
vec![Segment::new("c")],
];
let result = Segment::set_shape(&lines, 5, Some(2), None, false);
assert_eq!(result.len(), 2);
}
#[test]
fn test_set_shape_with_newlines() {
let lines = vec![vec![Segment::new("hi")]];
let result = Segment::set_shape(&lines, 5, Some(2), None, true);
assert_eq!(result.len(), 2);
let blank_text = result[1]
.iter()
.map(|s| s.text.to_string())
.collect::<String>();
assert!(blank_text.ends_with('\n'));
}
#[test]
fn test_set_shape_with_style() {
let style = Style::new().with_bold(true);
let lines: Vec<Vec<Segment>> = vec![];
let result = Segment::set_shape(&lines, 5, Some(1), Some(style), false);
assert_eq!(result.len(), 1);
assert_eq!(result[0][0].style, Some(style));
}
#[test]
fn test_align_top() {
let lines = vec![vec![Segment::new("hello")]];
let result = Segment::align_top(&lines, 5, 3, None, false);
assert_eq!(result.len(), 3);
assert_eq!(&*result[0][0].text, "hello");
assert_eq!(Segment::get_line_length(&result[1]), 5);
assert_eq!(Segment::get_line_length(&result[2]), 5);
}
#[test]
fn test_align_bottom() {
let lines = vec![vec![Segment::new("hello")]];
let result = Segment::align_bottom(&lines, 5, 3, None, false);
assert_eq!(result.len(), 3);
assert_eq!(&*result[2][0].text, "hello");
assert_eq!(Segment::get_line_length(&result[0]), 5);
assert_eq!(Segment::get_line_length(&result[1]), 5);
}
#[test]
fn test_align_middle() {
let lines = vec![vec![Segment::new("hello")]];
let result = Segment::align_middle(&lines, 5, 3, None, false);
assert_eq!(result.len(), 3);
assert_eq!(&*result[1][0].text, "hello");
}
#[test]
fn test_align_no_extra_lines() {
let lines = vec![
vec![Segment::new("a")],
vec![Segment::new("b")],
vec![Segment::new("c")],
];
let result = Segment::align_top(&lines, 5, 3, None, false);
assert_eq!(result.len(), 3);
}
#[test]
fn test_split_lines_terminator_basic() {
let segments = vec![Segment::new("hello\nworld")];
let lines = Segment::split_lines_terminator(segments);
assert_eq!(lines.len(), 2);
assert!(lines[0].1); assert!(!lines[1].1); assert_eq!(&*lines[0].0[0].text, "hello");
assert_eq!(&*lines[1].0[0].text, "world");
}
#[test]
fn test_split_lines_terminator_trailing_newline() {
let segments = vec![Segment::new("hello\n")];
let lines = Segment::split_lines_terminator(segments);
assert_eq!(lines.len(), 1);
assert!(lines[0].1);
}
#[test]
fn test_strip_links() {
let meta = StyleMeta::with_link("https://example.com");
let segments = vec![
Segment::styled_with_meta("link", Style::new().with_bold(true), meta),
Segment::new("plain"),
];
let result = Segment::strip_links(segments);
for seg in result.iter() {
assert!(seg.meta.is_none());
}
}
#[test]
fn test_remove_color() {
let style = Style::new()
.with_bold(true)
.with_color(crate::SimpleColor::Standard(1));
let segments = vec![Segment::styled("hello", style)];
let result = Segment::remove_color(segments);
let seg = result.iter().next().unwrap();
assert_eq!(seg.style.unwrap().bold, Some(true));
assert_eq!(seg.style.unwrap().color, None);
}
#[test]
fn test_segment_lines_to_segments() {
let lines = vec![vec![Segment::new("hello")], vec![Segment::new("world")]];
let sl = SegmentLines::new(lines, true);
let segs = sl.to_segments();
assert_eq!(segs.len(), 4);
}
#[test]
fn test_segment_lines_no_newlines() {
let lines = vec![vec![Segment::new("hello")], vec![Segment::new("world")]];
let sl = SegmentLines::new(lines, false);
let segs = sl.to_segments();
assert_eq!(segs.len(), 2);
}
#[test]
fn test_segment_is_send_sync() {
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
assert_send::<Segment>();
assert_sync::<Segment>();
}
#[test]
fn test_segments_is_send_sync() {
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
assert_send::<Segments>();
assert_sync::<Segments>();
}
}