#![forbid(unsafe_code)]
use crate::grapheme_width;
use ftui_style::Style;
use smallvec::SmallVec;
use std::borrow::Cow;
use unicode_segmentation::UnicodeSegmentation;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ControlCode {
CarriageReturn,
LineFeed,
Bell,
Backspace,
Tab,
Home,
ClearToEndOfLine,
ClearLine,
}
impl ControlCode {
#[inline]
#[must_use]
pub const fn is_newline(&self) -> bool {
matches!(self, Self::LineFeed)
}
#[inline]
#[must_use]
pub const fn is_cr(&self) -> bool {
matches!(self, Self::CarriageReturn)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Segment<'a> {
pub text: Cow<'a, str>,
pub style: Option<Style>,
pub link: Option<Cow<'a, str>>,
pub control: Option<SmallVec<[ControlCode; 2]>>,
}
impl<'a> Segment<'a> {
#[inline]
#[must_use]
pub fn text(s: impl Into<Cow<'a, str>>) -> Self {
Self {
text: s.into(),
style: None,
link: None,
control: None,
}
}
#[inline]
#[must_use]
pub fn styled(s: impl Into<Cow<'a, str>>, style: Style) -> Self {
Self {
text: s.into(),
style: Some(style),
link: None,
control: None,
}
}
#[inline]
#[must_use]
pub fn control(code: ControlCode) -> Self {
let mut codes = SmallVec::new();
codes.push(code);
Self {
text: Cow::Borrowed(""),
style: None,
link: None,
control: Some(codes),
}
}
#[inline]
#[must_use]
pub fn newline() -> Self {
Self::control(ControlCode::LineFeed)
}
#[inline]
#[must_use]
pub const fn empty() -> Self {
Self {
text: Cow::Borrowed(""),
style: None,
link: None,
control: None,
}
}
#[inline]
#[must_use]
pub fn as_str(&self) -> &str {
&self.text
}
#[inline]
#[must_use]
pub fn is_empty(&self) -> bool {
self.text.is_empty() && self.control.is_none()
}
#[inline]
#[must_use]
pub fn has_text(&self) -> bool {
!self.text.is_empty()
}
#[inline]
#[must_use]
pub fn is_control(&self) -> bool {
self.control.is_some() && self.text.is_empty()
}
#[inline]
#[must_use]
pub fn is_newline(&self) -> bool {
self.control
.as_ref()
.is_some_and(|codes| codes.iter().any(|c| c.is_newline()))
}
#[inline]
#[must_use]
pub fn cell_length(&self) -> usize {
if self.is_control() {
return 0;
}
crate::display_width(&self.text)
}
#[inline]
#[must_use]
pub fn cell_length_with<F>(&self, width_fn: F) -> usize
where
F: Fn(&str) -> usize,
{
if self.is_control() {
return 0;
}
width_fn(&self.text)
}
#[must_use]
pub fn split_at_cell(&self, cell_pos: usize) -> (Self, Self) {
if self.is_control() {
if cell_pos == 0 {
return (Self::empty(), self.clone());
}
return (self.clone(), Self::empty());
}
if self.text.is_empty() || cell_pos == 0 {
return (
Self {
text: Cow::Borrowed(""),
style: self.style,
link: self.link.clone(),
control: None,
},
self.clone(),
);
}
let total_width = self.cell_length();
if cell_pos >= total_width {
return (
self.clone(),
Self {
text: Cow::Borrowed(""),
style: self.style,
link: self.link.clone(),
control: None,
},
);
}
let (byte_pos, _actual_width) = find_cell_boundary(&self.text, cell_pos);
let left_text = &self.text[..byte_pos];
let right_text = &self.text[byte_pos..];
(
Self {
text: Cow::Owned(left_text.to_string()),
style: self.style,
link: self.link.clone(),
control: None,
},
Self {
text: Cow::Owned(right_text.to_string()),
style: self.style,
link: self.link.clone(),
control: None,
},
)
}
#[inline]
#[must_use]
pub fn with_style(mut self, style: Style) -> Self {
self.style = Some(style);
self
}
#[must_use]
pub fn into_owned(self) -> Segment<'static> {
Segment {
text: Cow::Owned(self.text.into_owned()),
style: self.style,
control: self.control,
link: self.link.map(|l| std::borrow::Cow::Owned(l.into_owned())),
}
}
#[must_use]
pub fn with_control(mut self, code: ControlCode) -> Self {
if let Some(ref mut codes) = self.control {
codes.push(code);
} else {
let mut codes = SmallVec::new();
codes.push(code);
self.control = Some(codes);
}
self
}
}
impl<'a> Default for Segment<'a> {
fn default() -> Self {
Self::empty()
}
}
impl<'a> From<&'a str> for Segment<'a> {
fn from(s: &'a str) -> Self {
Self::text(s)
}
}
impl From<String> for Segment<'static> {
fn from(s: String) -> Self {
Self::text(s)
}
}
pub fn find_cell_boundary(text: &str, target_cells: usize) -> (usize, usize) {
let mut current_cells = 0;
let mut byte_pos = 0;
for grapheme in text.graphemes(true) {
let grapheme_width = grapheme_width(grapheme);
if current_cells + grapheme_width > target_cells {
break;
}
current_cells += grapheme_width;
byte_pos += grapheme.len();
if current_cells >= target_cells {
break;
}
}
(byte_pos, current_cells)
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SegmentLine<'a> {
segments: Vec<Segment<'a>>,
}
impl<'a> SegmentLine<'a> {
#[inline]
#[must_use]
pub const fn new() -> Self {
Self {
segments: Vec::new(),
}
}
#[inline]
#[must_use]
pub fn from_segments(segments: Vec<Segment<'a>>) -> Self {
Self { segments }
}
#[inline]
#[must_use]
pub fn from_segment(segment: Segment<'a>) -> Self {
Self {
segments: vec![segment],
}
}
#[inline]
#[must_use]
pub fn is_empty(&self) -> bool {
self.segments.is_empty() || self.segments.iter().all(|s| s.is_empty())
}
#[inline]
#[must_use]
pub fn len(&self) -> usize {
self.segments.len()
}
#[must_use]
pub fn cell_length(&self) -> usize {
self.segments.iter().map(|s| s.cell_length()).sum()
}
#[inline]
pub fn push(&mut self, segment: Segment<'a>) {
self.segments.push(segment);
}
#[inline]
#[must_use]
pub fn segments(&self) -> &[Segment<'a>] {
&self.segments
}
#[inline]
pub fn segments_mut(&mut self) -> &mut Vec<Segment<'a>> {
&mut self.segments
}
#[inline]
pub fn iter(&self) -> impl Iterator<Item = &Segment<'a>> {
self.segments.iter()
}
#[must_use]
pub fn split_at_cell(&self, cell_pos: usize) -> (Self, Self) {
if cell_pos == 0 {
return (Self::new(), self.clone());
}
let total_width = self.cell_length();
if cell_pos >= total_width {
return (self.clone(), Self::new());
}
let mut left_segments = Vec::new();
let mut right_segments = Vec::new();
let mut consumed = 0;
let mut found_split = false;
for segment in &self.segments {
if found_split {
right_segments.push(segment.clone());
continue;
}
let seg_width = segment.cell_length();
if consumed + seg_width <= cell_pos {
left_segments.push(segment.clone());
consumed += seg_width;
} else if consumed >= cell_pos {
right_segments.push(segment.clone());
found_split = true;
} else {
let split_at = cell_pos - consumed;
let (left, right) = segment.split_at_cell(split_at);
if left.has_text() {
left_segments.push(left);
}
if right.has_text() {
right_segments.push(right);
}
found_split = true;
}
}
(
Self::from_segments(left_segments),
Self::from_segments(right_segments),
)
}
#[must_use]
pub fn to_plain_text(&self) -> String {
self.segments.iter().map(|s| s.as_str()).collect()
}
#[must_use]
pub fn into_owned(self) -> SegmentLine<'static> {
SegmentLine {
segments: self.segments.into_iter().map(|s| s.into_owned()).collect(),
}
}
}
impl<'a> IntoIterator for SegmentLine<'a> {
type Item = Segment<'a>;
type IntoIter = std::vec::IntoIter<Segment<'a>>;
fn into_iter(self) -> Self::IntoIter {
self.segments.into_iter()
}
}
impl<'a, 'b> IntoIterator for &'b SegmentLine<'a> {
type Item = &'b Segment<'a>;
type IntoIter = std::slice::Iter<'b, Segment<'a>>;
fn into_iter(self) -> Self::IntoIter {
self.segments.iter()
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SegmentLines<'a> {
lines: Vec<SegmentLine<'a>>,
}
impl<'a> SegmentLines<'a> {
#[inline]
#[must_use]
pub const fn new() -> Self {
Self { lines: Vec::new() }
}
#[inline]
#[must_use]
pub fn from_lines(lines: Vec<SegmentLine<'a>>) -> Self {
Self { lines }
}
#[inline]
#[must_use]
pub fn is_empty(&self) -> bool {
self.lines.is_empty()
}
#[inline]
#[must_use]
pub fn len(&self) -> usize {
self.lines.len()
}
#[inline]
pub fn push(&mut self, line: SegmentLine<'a>) {
self.lines.push(line);
}
#[inline]
#[must_use]
pub fn lines(&self) -> &[SegmentLine<'a>] {
&self.lines
}
#[inline]
pub fn iter(&self) -> impl Iterator<Item = &SegmentLine<'a>> {
self.lines.iter()
}
#[must_use]
pub fn max_width(&self) -> usize {
self.lines
.iter()
.map(|l| l.cell_length())
.max()
.unwrap_or(0)
}
#[must_use]
pub fn into_owned(self) -> SegmentLines<'static> {
SegmentLines {
lines: self.lines.into_iter().map(|l| l.into_owned()).collect(),
}
}
}
impl<'a> IntoIterator for SegmentLines<'a> {
type Item = SegmentLine<'a>;
type IntoIter = std::vec::IntoIter<SegmentLine<'a>>;
fn into_iter(self) -> Self::IntoIter {
self.lines.into_iter()
}
}
#[must_use]
pub fn split_into_lines<'a>(segments: impl IntoIterator<Item = Segment<'a>>) -> SegmentLines<'a> {
let mut lines = SegmentLines::new();
let mut current_line = SegmentLine::new();
let mut has_content = false;
for segment in segments {
has_content = true;
if segment.is_newline() {
lines.push(std::mem::take(&mut current_line));
} else if segment.has_text() {
let text = segment.as_str();
if text.contains('\n') {
let parts: Vec<&str> = text.split('\n').collect();
for (i, part) in parts.iter().enumerate() {
if !part.is_empty() {
current_line.push(Segment {
text: Cow::Owned((*part).to_string()),
style: segment.style,
control: None,
link: segment.link.clone(),
});
}
if i < parts.len() - 1 {
lines.push(std::mem::take(&mut current_line));
}
}
} else {
current_line.push(segment);
}
} else if !segment.is_empty() {
current_line.push(segment);
}
}
if has_content || lines.is_empty() {
lines.push(current_line);
}
lines
}
pub fn join_lines<'a>(lines: &SegmentLines<'a>) -> Vec<Segment<'a>> {
let mut result = Vec::new();
let line_count = lines.len();
for (i, line) in lines.iter().enumerate() {
for segment in line.iter() {
result.push(segment.clone());
}
if i < line_count - 1 {
result.push(Segment::newline());
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn segment_text_creates_unstyled_segment() {
let seg = Segment::text("hello");
assert_eq!(seg.as_str(), "hello");
assert!(seg.style.is_none());
assert!(seg.control.is_none());
}
#[test]
fn segment_styled_creates_styled_segment() {
let style = Style::new().bold();
let seg = Segment::styled("hello", style);
assert_eq!(seg.as_str(), "hello");
assert_eq!(seg.style, Some(style));
}
#[test]
fn segment_control_creates_control_segment() {
let seg = Segment::control(ControlCode::LineFeed);
assert!(seg.is_control());
assert!(seg.is_newline());
assert_eq!(seg.cell_length(), 0);
}
#[test]
fn segment_empty_is_empty() {
let seg = Segment::empty();
assert!(seg.is_empty());
assert!(!seg.has_text());
assert_eq!(seg.cell_length(), 0);
}
#[test]
fn cell_length_ascii() {
let seg = Segment::text("hello");
assert_eq!(seg.cell_length(), 5);
}
#[test]
fn cell_length_cjk() {
let seg = Segment::text("你好");
assert_eq!(seg.cell_length(), 4); }
#[test]
fn cell_length_mixed() {
let seg = Segment::text("hi你好");
assert_eq!(seg.cell_length(), 6); }
#[test]
fn cell_length_emoji() {
let seg = Segment::text("😀");
assert!(seg.cell_length() >= 1);
}
#[test]
fn cell_length_zwj_sequence() {
let seg = Segment::text("👨👩👧");
let _width = seg.cell_length();
}
#[test]
fn cell_length_control_is_zero() {
let seg = Segment::control(ControlCode::Bell);
assert_eq!(seg.cell_length(), 0);
}
#[test]
fn split_at_cell_ascii() {
let seg = Segment::text("hello world");
let (left, right) = seg.split_at_cell(5);
assert_eq!(left.as_str(), "hello");
assert_eq!(right.as_str(), " world");
}
#[test]
fn split_at_cell_zero() {
let seg = Segment::text("hello");
let (left, right) = seg.split_at_cell(0);
assert_eq!(left.as_str(), "");
assert_eq!(right.as_str(), "hello");
}
#[test]
fn split_at_cell_beyond_length() {
let seg = Segment::text("hi");
let (left, right) = seg.split_at_cell(10);
assert_eq!(left.as_str(), "hi");
assert_eq!(right.as_str(), "");
}
#[test]
fn split_at_cell_cjk() {
let seg = Segment::text("你好世界");
let (left, right) = seg.split_at_cell(2);
assert_eq!(left.as_str(), "你");
assert_eq!(right.as_str(), "好世界");
}
#[test]
fn split_at_cell_cjk_mid_char() {
let seg = Segment::text("你好");
let (left, right) = seg.split_at_cell(1);
assert_eq!(left.as_str(), "");
assert_eq!(right.as_str(), "你好");
}
#[test]
fn split_at_cell_mixed() {
let seg = Segment::text("hi你");
let (left, right) = seg.split_at_cell(2);
assert_eq!(left.as_str(), "hi");
assert_eq!(right.as_str(), "你");
}
#[test]
fn split_at_cell_preserves_style() {
let style = Style::new().bold();
let seg = Segment::styled("hello", style);
let (left, right) = seg.split_at_cell(2);
assert_eq!(left.style, Some(style));
assert_eq!(right.style, Some(style));
}
#[test]
fn split_at_cell_control_segment() {
let seg = Segment::control(ControlCode::LineFeed);
let (left, right) = seg.split_at_cell(0);
assert!(left.is_empty());
assert!(right.is_control());
}
#[test]
fn segment_line_cell_length() {
let mut line = SegmentLine::new();
line.push(Segment::text("hello "));
line.push(Segment::text("world"));
assert_eq!(line.cell_length(), 11);
}
#[test]
fn segment_line_split_at_cell() {
let mut line = SegmentLine::new();
line.push(Segment::text("hello "));
line.push(Segment::text("world"));
let (left, right) = line.split_at_cell(8);
assert_eq!(left.to_plain_text(), "hello wo");
assert_eq!(right.to_plain_text(), "rld");
}
#[test]
fn segment_line_split_at_segment_boundary() {
let mut line = SegmentLine::new();
line.push(Segment::text("hello"));
line.push(Segment::text(" world"));
let (left, right) = line.split_at_cell(5);
assert_eq!(left.to_plain_text(), "hello");
assert_eq!(right.to_plain_text(), " world");
}
#[test]
fn split_into_lines_single_line() {
let segments = vec![Segment::text("hello world")];
let lines = split_into_lines(segments);
assert_eq!(lines.len(), 1);
assert_eq!(lines.lines()[0].to_plain_text(), "hello world");
}
#[test]
fn split_into_lines_with_newline_control() {
let segments = vec![
Segment::text("line one"),
Segment::newline(),
Segment::text("line two"),
];
let lines = split_into_lines(segments);
assert_eq!(lines.len(), 2);
assert_eq!(lines.lines()[0].to_plain_text(), "line one");
assert_eq!(lines.lines()[1].to_plain_text(), "line two");
}
#[test]
fn split_into_lines_with_embedded_newline() {
let segments = vec![Segment::text("line one\nline two")];
let lines = split_into_lines(segments);
assert_eq!(lines.len(), 2);
assert_eq!(lines.lines()[0].to_plain_text(), "line one");
assert_eq!(lines.lines()[1].to_plain_text(), "line two");
}
#[test]
fn split_into_lines_empty_input() {
let segments: Vec<Segment> = vec![];
let lines = split_into_lines(segments);
assert_eq!(lines.len(), 1); assert!(lines.lines()[0].is_empty());
}
#[test]
fn join_lines_roundtrip() {
let segments = vec![
Segment::text("line one"),
Segment::newline(),
Segment::text("line two"),
];
let lines = split_into_lines(segments);
let joined = join_lines(&lines);
assert_eq!(joined.len(), 3);
assert_eq!(joined[0].as_str(), "line one");
assert!(joined[1].is_newline());
assert_eq!(joined[2].as_str(), "line two");
}
#[test]
fn segment_into_owned() {
let s = String::from("hello");
let seg: Segment = Segment::text(&s[..]);
let owned: Segment<'static> = seg.into_owned();
assert_eq!(owned.as_str(), "hello");
}
#[test]
fn segment_from_string() {
let seg: Segment<'static> = Segment::from(String::from("hello"));
assert_eq!(seg.as_str(), "hello");
}
#[test]
fn segment_from_str() {
let seg: Segment = Segment::from("hello");
assert_eq!(seg.as_str(), "hello");
}
#[test]
fn control_code_is_newline() {
assert!(ControlCode::LineFeed.is_newline());
assert!(!ControlCode::CarriageReturn.is_newline());
assert!(!ControlCode::Bell.is_newline());
}
#[test]
fn control_code_is_cr() {
assert!(ControlCode::CarriageReturn.is_cr());
assert!(!ControlCode::LineFeed.is_cr());
}
#[test]
fn segment_with_control() {
let seg = Segment::text("hello").with_control(ControlCode::Bell);
assert!(seg.control.is_some());
assert_eq!(seg.control.as_ref().unwrap().len(), 1);
}
#[test]
fn split_empty_segment() {
let seg = Segment::text("");
let (left, right) = seg.split_at_cell(5);
assert_eq!(left.as_str(), "");
assert_eq!(right.as_str(), "");
}
#[test]
fn combining_characters() {
let seg = Segment::text("e\u{0301}"); let width = seg.cell_length();
assert!(width >= 1);
let (left, right) = seg.split_at_cell(1);
assert_eq!(left.cell_length() + right.cell_length(), width);
}
#[test]
fn segment_line_is_empty() {
let line = SegmentLine::new();
assert!(line.is_empty());
let mut line2 = SegmentLine::new();
line2.push(Segment::empty());
assert!(line2.is_empty());
let mut line3 = SegmentLine::new();
line3.push(Segment::text("x"));
assert!(!line3.is_empty());
}
#[test]
fn cow_borrowed_from_static_str() {
let seg = Segment::text("static string");
assert!(matches!(seg.text, Cow::Borrowed(_)));
}
#[test]
fn cow_owned_from_string() {
let owned = String::from("owned string");
let seg = Segment::text(owned);
assert!(matches!(seg.text, Cow::Owned(_)));
}
#[test]
fn cow_borrowed_reference() {
let s = String::from("reference");
let seg = Segment::text(&s[..]);
assert!(matches!(seg.text, Cow::Borrowed(_)));
}
#[test]
fn into_owned_converts_borrowed_to_owned() {
let seg = Segment::text("borrowed");
assert!(matches!(seg.text, Cow::Borrowed(_)));
let owned = seg.into_owned();
assert!(matches!(owned.text, Cow::Owned(_)));
assert_eq!(owned.as_str(), "borrowed");
}
#[test]
fn clone_borrowed_segment_stays_borrowed() {
let seg = Segment::text("static");
let cloned = seg.clone();
assert!(matches!(cloned.text, Cow::Borrowed(_)));
}
#[test]
fn clone_owned_segment_allocates() {
let owned = String::from("owned");
let seg = Segment::text(owned);
let cloned = seg.clone();
assert!(matches!(cloned.text, Cow::Owned(_)));
assert_eq!(cloned.as_str(), "owned");
}
#[test]
fn segment_default_is_empty() {
let seg = Segment::default();
assert!(seg.is_empty());
assert_eq!(seg.as_str(), "");
assert!(seg.style.is_none());
assert!(seg.control.is_none());
}
#[test]
fn segment_line_default_is_empty() {
let line = SegmentLine::default();
assert!(line.is_empty());
assert_eq!(line.len(), 0);
}
#[test]
fn segment_lines_default_is_empty() {
let lines = SegmentLines::default();
assert!(lines.is_empty());
assert_eq!(lines.len(), 0);
}
#[test]
fn segment_debug_impl() {
let seg = Segment::text("hello");
let debug = format!("{:?}", seg);
assert!(debug.contains("Segment"));
assert!(debug.contains("hello"));
}
#[test]
fn control_code_debug_impl() {
let code = ControlCode::LineFeed;
let debug = format!("{:?}", code);
assert!(debug.contains("LineFeed"));
}
#[test]
fn segment_line_debug_impl() {
let line = SegmentLine::from_segment(Segment::text("test"));
let debug = format!("{:?}", line);
assert!(debug.contains("SegmentLine"));
}
#[test]
fn segment_line_into_owned() {
let s = String::from("test");
let mut line = SegmentLine::new();
line.push(Segment::text(&s[..]));
let owned = line.into_owned();
drop(s); assert_eq!(owned.to_plain_text(), "test");
}
#[test]
fn segment_line_segments_mut() {
let mut line = SegmentLine::from_segment(Segment::text("hello"));
line.segments_mut().push(Segment::text(" world"));
assert_eq!(line.to_plain_text(), "hello world");
}
#[test]
fn segment_line_iter() {
let line = SegmentLine::from_segments(vec![Segment::text("a"), Segment::text("b")]);
let collected: Vec<_> = line.iter().collect();
assert_eq!(collected.len(), 2);
}
#[test]
fn segment_line_into_iter_ref() {
let line = SegmentLine::from_segments(vec![Segment::text("x"), Segment::text("y")]);
let mut count = 0;
for _seg in &line {
count += 1;
}
assert_eq!(count, 2);
}
#[test]
fn segment_lines_into_owned() {
let s = String::from("line one");
let mut lines = SegmentLines::new();
let mut line = SegmentLine::new();
line.push(Segment::text(&s[..]));
lines.push(line);
let owned = lines.into_owned();
drop(s);
assert_eq!(owned.lines()[0].to_plain_text(), "line one");
}
#[test]
fn segment_lines_iter() {
let mut lines = SegmentLines::new();
lines.push(SegmentLine::from_segment(Segment::text("a")));
lines.push(SegmentLine::from_segment(Segment::text("b")));
let collected: Vec<_> = lines.iter().collect();
assert_eq!(collected.len(), 2);
}
#[test]
fn segment_lines_max_width() {
let mut lines = SegmentLines::new();
lines.push(SegmentLine::from_segment(Segment::text("short")));
lines.push(SegmentLine::from_segment(Segment::text("longer line here")));
lines.push(SegmentLine::from_segment(Segment::text("med")));
assert_eq!(lines.max_width(), 16);
}
#[test]
fn segment_lines_max_width_empty() {
let lines = SegmentLines::new();
assert_eq!(lines.max_width(), 0);
}
#[test]
fn segment_with_style_applies_style() {
let style = Style::new().bold();
let seg = Segment::text("hello").with_style(style);
assert_eq!(seg.style, Some(style));
}
#[test]
fn segment_with_multiple_controls() {
let seg = Segment::text("x")
.with_control(ControlCode::Bell)
.with_control(ControlCode::Tab);
let codes = seg.control.unwrap();
assert_eq!(codes.len(), 2);
}
#[test]
fn cell_length_with_custom_width() {
let seg = Segment::text("hello");
let width = seg.cell_length_with(|s| s.len() * 2);
assert_eq!(width, 10);
}
#[test]
fn cell_length_with_on_control_is_zero() {
let seg = Segment::control(ControlCode::Bell);
let width = seg.cell_length_with(|_| 100);
assert_eq!(width, 0);
}
#[test]
fn control_code_equality() {
assert_eq!(ControlCode::LineFeed, ControlCode::LineFeed);
assert_ne!(ControlCode::LineFeed, ControlCode::CarriageReturn);
}
#[test]
fn control_code_hash_consistency() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(ControlCode::LineFeed);
set.insert(ControlCode::CarriageReturn);
assert_eq!(set.len(), 2);
assert!(set.contains(&ControlCode::LineFeed));
}
#[test]
fn split_at_cell_exact_boundary() {
let seg = Segment::text("abcde");
let (left, right) = seg.split_at_cell(5);
assert_eq!(left.as_str(), "abcde");
assert_eq!(right.as_str(), "");
}
#[test]
fn segment_line_split_at_zero() {
let line = SegmentLine::from_segment(Segment::text("hello"));
let (left, right) = line.split_at_cell(0);
assert!(left.is_empty());
assert_eq!(right.to_plain_text(), "hello");
}
#[test]
fn segment_line_split_at_end() {
let line = SegmentLine::from_segment(Segment::text("hello"));
let (left, right) = line.split_at_cell(100);
assert_eq!(left.to_plain_text(), "hello");
assert!(right.is_empty());
}
#[test]
fn join_lines_single_line() {
let mut lines = SegmentLines::new();
lines.push(SegmentLine::from_segment(Segment::text("only")));
let joined = join_lines(&lines);
assert_eq!(joined.len(), 1);
assert_eq!(joined[0].as_str(), "only");
}
#[test]
fn join_lines_empty() {
let lines = SegmentLines::new();
let joined = join_lines(&lines);
assert!(joined.is_empty());
}
#[test]
fn split_into_lines_multiple_newlines() {
let segments = vec![
Segment::text("a"),
Segment::newline(),
Segment::newline(),
Segment::text("b"),
];
let lines = split_into_lines(segments);
assert_eq!(lines.len(), 3);
assert_eq!(lines.lines()[0].to_plain_text(), "a");
assert!(lines.lines()[1].is_empty());
assert_eq!(lines.lines()[2].to_plain_text(), "b");
}
#[test]
fn split_into_lines_trailing_newline() {
let segments = vec![Segment::text("hello"), Segment::newline()];
let lines = split_into_lines(segments);
assert_eq!(lines.len(), 2);
assert_eq!(lines.lines()[0].to_plain_text(), "hello");
assert!(lines.lines()[1].is_empty());
}
}
#[cfg(test)]
mod proptests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn split_preserves_total_width(s in "[a-zA-Z0-9 ]{1,100}", pos in 0usize..200) {
let seg = Segment::text(s);
let total = seg.cell_length();
let (left, right) = seg.split_at_cell(pos);
prop_assert_eq!(left.cell_length() + right.cell_length(), total);
}
#[test]
fn split_preserves_content(s in "[a-zA-Z0-9 ]{1,100}", pos in 0usize..200) {
let seg = Segment::text(s.clone());
let (left, right) = seg.split_at_cell(pos);
let combined = format!("{}{}", left.as_str(), right.as_str());
prop_assert_eq!(combined, s);
}
#[test]
fn cell_length_matches_display_width(s in "[a-zA-Z0-9 ]{1,100}") {
let seg = Segment::text(s.clone());
let expected = crate::display_width(s.as_str());
prop_assert_eq!(seg.cell_length(), expected);
}
#[test]
fn line_split_preserves_total_width(
parts in prop::collection::vec("[a-z]{1,10}", 1..5),
pos in 0usize..100
) {
let mut line = SegmentLine::new();
for part in &parts {
line.push(Segment::text(part.as_str()));
}
let total = line.cell_length();
let (left, right) = line.split_at_cell(pos);
prop_assert_eq!(left.cell_length() + right.cell_length(), total);
}
#[test]
fn split_into_lines_preserves_content(s in "[a-zA-Z0-9 \n]{1,200}") {
let segments = vec![Segment::text(s.clone())];
let lines = split_into_lines(segments);
let mut result = String::new();
for (i, line) in lines.lines().iter().enumerate() {
if i > 0 {
result.push('\n');
}
result.push_str(&line.to_plain_text());
}
prop_assert_eq!(result, s);
}
}
}