extern crate alloc;
use alloc::string::{ String, ToString };
use super::{ Segment, parse_segments };
use super::strip::strip;
#[ derive( Debug, Clone, PartialEq, Eq ) ]
pub struct TruncateOptions
{
pub max_width : usize,
pub suffix : Option< String >,
pub append_reset : bool,
}
impl TruncateOptions
{
pub fn new( max_width : usize ) -> Self
{
assert!( max_width != 0, "TruncateOptions: max_width must be greater than 0" );
Self
{
max_width,
suffix : None,
append_reset : false,
}
}
pub fn with_suffix( mut self, suffix : impl Into< String > ) -> Self
{
self.suffix = Some( suffix.into() );
self
}
pub fn with_reset( mut self, reset : bool ) -> Self
{
self.append_reset = reset;
self
}
}
impl Default for TruncateOptions
{
fn default() -> Self
{
Self
{
max_width : 80,
suffix : None,
append_reset : false,
}
}
}
pub fn truncate( text : &str, options : &TruncateOptions ) -> String
{
truncate_internal( text, options, &CharCounter )
}
#[ cfg( feature = "ansi_unicode" ) ]
pub fn truncate_unicode( text : &str, options : &TruncateOptions ) -> String
{
truncate_internal( text, options, &GraphemeCounter )
}
pub fn truncate_if_needed( text : &str, max_width : usize, options : &TruncateOptions ) -> String
{
truncate_if_needed_internal( text, max_width, options, &CharCounter )
}
#[ cfg( feature = "ansi_unicode" ) ]
pub fn truncate_if_needed_unicode( text : &str, max_width : usize, options : &TruncateOptions ) -> String
{
truncate_if_needed_internal( text, max_width, options, &GraphemeCounter )
}
fn truncate_if_needed_internal< C : VisibleCounter >(
text : &str,
max_width : usize,
options : &TruncateOptions,
counter : &C
) -> String
{
let visible_width = counter.count( &strip( text ) );
if visible_width > max_width
{
truncate_internal( text, options, counter )
}
else
{
text.to_string()
}
}
pub fn truncate_lines( text : &str, max_width : usize, options : &TruncateOptions ) -> ( String, bool )
{
truncate_lines_internal( text, max_width, options, &CharCounter )
}
#[ cfg( feature = "ansi_unicode" ) ]
pub fn truncate_lines_unicode( text : &str, max_width : usize, options : &TruncateOptions ) -> ( String, bool )
{
truncate_lines_internal( text, max_width, options, &GraphemeCounter )
}
fn truncate_lines_internal< C : VisibleCounter >(
text : &str,
max_width : usize,
options : &TruncateOptions,
counter : &C
) -> ( String, bool )
{
let mut any_truncated = false;
let lines : alloc::vec::Vec< String > = text
.lines()
.map( | line |
{
let visible_width = counter.count( &strip( line ) );
if visible_width > max_width
{
any_truncated = true;
truncate_internal( line, options, counter )
}
else
{
line.to_string()
}
} )
.collect();
( lines.join( "\n" ), any_truncated )
}
trait VisibleCounter
{
fn count( &self, text : &str ) -> usize;
fn take_first< 'a >( &self, text : &'a str, n : usize ) -> &'a str;
}
struct CharCounter;
impl VisibleCounter for CharCounter
{
fn count( &self, text : &str ) -> usize
{
text.chars().count()
}
fn take_first< 'a >( &self, text : &'a str, n : usize ) -> &'a str
{
let end = text
.char_indices()
.nth( n )
.map_or( text.len(), | ( idx, _ ) | idx );
&text[ ..end ]
}
}
#[ cfg( feature = "ansi_unicode" ) ]
struct GraphemeCounter;
#[ cfg( feature = "ansi_unicode" ) ]
impl VisibleCounter for GraphemeCounter
{
fn count( &self, text : &str ) -> usize
{
use unicode_segmentation::UnicodeSegmentation;
text.graphemes( true ).count()
}
fn take_first< 'a >( &self, text : &'a str, n : usize ) -> &'a str
{
use unicode_segmentation::UnicodeSegmentation;
let mut end = 0;
for ( idx, grapheme ) in text.grapheme_indices( true ).take( n )
{
end = idx + grapheme.len();
}
&text[ ..end ]
}
}
fn truncate_internal< C : VisibleCounter >(
text : &str,
options : &TruncateOptions,
counter : &C,
) -> String
{
let segments = parse_segments( text );
let suffix_len = options.suffix.as_ref().map_or( 0, | s | counter.count( s ) );
let ( effective_max, use_suffix ) = if suffix_len >= options.max_width
{
( options.max_width, false )
}
else
{
( options.max_width - suffix_len, true )
};
let mut result = String::new();
let mut visible_count = 0;
let mut truncated = false;
for segment in segments
{
match segment
{
Segment::Ansi( code ) =>
{
result.push_str( code );
}
Segment::Text( text_content ) =>
{
let text_len = counter.count( text_content );
if visible_count + text_len <= effective_max
{
result.push_str( text_content );
visible_count += text_len;
}
else if visible_count < effective_max
{
let remaining = effective_max - visible_count;
let truncated_text = counter.take_first( text_content, remaining );
result.push_str( truncated_text );
truncated = true;
break;
}
else
{
truncated = true;
break;
}
}
}
}
if truncated && use_suffix
{
if let Some( ref suffix ) = options.suffix
{
result.push_str( suffix );
}
}
if options.append_reset
{
result.push_str( "\x1b[0m" );
}
result
}