From ebb99561e4a242bfad821a20e47488425f77277f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Dupr=C3=A9?= Date: Wed, 7 Feb 2024 13:20:26 +0100 Subject: [PATCH 1/2] fix truncation on ascii chars (with bad asymptotic complexity) --- src/style.rs | 43 ++++++++++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/src/style.rs b/src/style.rs index e3f39705..c97ed7c8 100644 --- a/src/style.rs +++ b/src/style.rs @@ -684,6 +684,27 @@ impl<'a> fmt::Display for RepeatedStringDisplay<'a> { } } +fn truncate_to_fit(text: &str, align: Alignment, target_width: usize) -> Option<&str> { + let mut start = 0; + let mut end = text.len(); + let mut left_priority = true; + + // Iter next truncation positions from left or right + let mut char_pos_from_left = text.char_indices().map(|(idx, _)| idx).skip(1); + let mut char_pos_from_right = text.char_indices().map(|(idx, _)| idx).rev(); + + while measure_text_width(text.get(start..end).unwrap_or("")) > target_width { + match (align, left_priority) { + (Alignment::Left, _) | (Alignment::Center, true) => end = char_pos_from_right.next()?, + _ => start = char_pos_from_left.next()?, + } + + left_priority = !left_priority; + } + + text.get(start..end) +} + struct PaddedStringDisplay<'a> { str: &'a str, width: usize, @@ -694,20 +715,10 @@ struct PaddedStringDisplay<'a> { impl<'a> fmt::Display for PaddedStringDisplay<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let cols = measure_text_width(self.str); - let excess = cols.saturating_sub(self.width); - if excess > 0 && !self.truncate { - return f.write_str(self.str); - } else if excess > 0 { - let (start, end) = match self.align { - Alignment::Left => (0, self.str.len() - excess), - Alignment::Right => (excess, self.str.len()), - Alignment::Center => ( - excess / 2, - self.str.len() - excess.saturating_sub(excess / 2), - ), - }; - return f.write_str(self.str.get(start..end).unwrap_or(self.str)); + if cols > self.width && self.truncate { + return f + .write_str(truncate_to_fit(self.str, self.align, self.width).unwrap_or(self.str)); } let diff = self.width.saturating_sub(cols); @@ -918,6 +929,12 @@ mod tests { state.message = TabExpandedString::NoTabs("abcdefghijklmnopqrst".into()); style.format_state(&state, &mut buf, WIDTH); assert_eq!(&buf[0], "fghijklmno"); + + buf.clear(); + let style = ProgressStyle::with_template("{wide_msg}").unwrap(); + state.message = TabExpandedString::NoTabs("\x1b[31mabcdefghijklmnopqrst\x1b[0m".into()); + style.format_state(&state, &mut buf, WIDTH); + assert_eq!(&buf[0], "\x1b[31mabcdefghij"); } #[test] From d7a1da44b70cd2d34233a48df60873484043042c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Dupr=C3=A9?= Date: Wed, 7 Feb 2024 18:35:42 +0100 Subject: [PATCH 2/2] fix truncation on ascii chars --- Cargo.toml | 3 ++- src/style.rs | 48 +++++++++++++++++++++--------------------------- 2 files changed, 23 insertions(+), 28 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 211ed5bd..20ce7bc0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,8 @@ readme = "README.md" exclude = ["screenshots/*"] [dependencies] -console = { version = "0.15", default-features = false, features = ["ansi-parsing"] } +# console = { version = "0.16", default-features = false, features = ["ansi-parsing"] } +console = { git = "https://github.com/remi-dupre/console.git", branch = "ansi-slice", default-features = false, features = ["ansi-parsing"] } futures-core = { version = "0.3", default-features = false, optional = true } number_prefix = "0.4" portable-atomic = "1.0.0" diff --git a/src/style.rs b/src/style.rs index c97ed7c8..632c6564 100644 --- a/src/style.rs +++ b/src/style.rs @@ -4,7 +4,7 @@ use std::mem; #[cfg(not(target_arch = "wasm32"))] use std::time::Instant; -use console::{measure_text_width, Style}; +use console::{measure_text_width, slice_str, Style}; #[cfg(target_arch = "wasm32")] use instant::Instant; #[cfg(feature = "unicode-segmentation")] @@ -684,27 +684,6 @@ impl<'a> fmt::Display for RepeatedStringDisplay<'a> { } } -fn truncate_to_fit(text: &str, align: Alignment, target_width: usize) -> Option<&str> { - let mut start = 0; - let mut end = text.len(); - let mut left_priority = true; - - // Iter next truncation positions from left or right - let mut char_pos_from_left = text.char_indices().map(|(idx, _)| idx).skip(1); - let mut char_pos_from_right = text.char_indices().map(|(idx, _)| idx).rev(); - - while measure_text_width(text.get(start..end).unwrap_or("")) > target_width { - match (align, left_priority) { - (Alignment::Left, _) | (Alignment::Center, true) => end = char_pos_from_right.next()?, - _ => start = char_pos_from_left.next()?, - } - - left_priority = !left_priority; - } - - text.get(start..end) -} - struct PaddedStringDisplay<'a> { str: &'a str, width: usize, @@ -715,11 +694,26 @@ struct PaddedStringDisplay<'a> { impl<'a> fmt::Display for PaddedStringDisplay<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let cols = measure_text_width(self.str); + let text_width = measure_text_width(self.str); + let excess = text_width.saturating_sub(self.width); + + if excess > 0 { + let truncated = { + if self.truncate { + match self.align { + Alignment::Left => slice_str(self.str, "", 0..self.width, ""), + Alignment::Right => slice_str(self.str, "", excess..text_width, ""), + Alignment::Center => { + slice_str(self.str, "", excess / 2..text_width - (excess + 1) / 2, "") + } + } + } else { + self.str.into() + } + }; - if cols > self.width && self.truncate { - return f - .write_str(truncate_to_fit(self.str, self.align, self.width).unwrap_or(self.str)); - } + return f.write_str(&truncated); + }; let diff = self.width.saturating_sub(cols); let (left_pad, right_pad) = match self.align { @@ -934,7 +928,7 @@ mod tests { let style = ProgressStyle::with_template("{wide_msg}").unwrap(); state.message = TabExpandedString::NoTabs("\x1b[31mabcdefghijklmnopqrst\x1b[0m".into()); style.format_state(&state, &mut buf, WIDTH); - assert_eq!(&buf[0], "\x1b[31mabcdefghij"); + assert_eq!(&buf[0], "\x1b[31mabcdefghij\u{1b}[0m"); } #[test]