Skip to content

Commit 047fea2

Browse files
committed
Add new function ansi::slice_ansi_str
I also took my chance and suggested an non-allocating version of measure_text_width.
1 parent de2f15a commit 047fea2

File tree

4 files changed

+115
-12
lines changed

4 files changed

+115
-12
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "console"
33
description = "A terminal and console abstraction for Rust"
4-
version = "0.15.8"
4+
version = "0.16.0"
55
keywords = ["cli", "terminal", "colors", "console", "ansi"]
66
authors = ["Armin Ronacher <armin.ronacher@active-4.com>"]
77
license = "MIT"

src/ansi.rs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ use std::{
44
str::CharIndices,
55
};
66

7+
use crate::utils::char_width;
8+
79
#[derive(Debug, Clone, Copy)]
810
enum State {
911
Start,
@@ -267,8 +269,63 @@ impl<'a> Iterator for AnsiCodeIterator<'a> {
267269

268270
impl<'a> FusedIterator for AnsiCodeIterator<'a> {}
269271

272+
/// Slice a `&str` in terms of text width. This means that only the text
273+
/// columns strictly between `start` and `stop` will be kept.
274+
///
275+
/// If a multi-columns character overlaps with the end of the interval it will
276+
/// not be included. In such a case, the result will be less than `end - start`
277+
/// columns wide.
278+
pub fn slice_ansi_str(s: &str, start: usize, end: usize) -> &str {
279+
if end <= start {
280+
return "";
281+
}
282+
283+
let mut pos = 0;
284+
let mut res_start = 0;
285+
let mut res_end = 0;
286+
287+
'outer: for (sub, is_ansi) in AnsiCodeIterator::new(s) {
288+
// As ansi symbols have a width of 0 we can safely early-interupt
289+
// the outer for loop only if current pos strictly greater than
290+
// `end`.
291+
if pos > end {
292+
break;
293+
}
294+
295+
if is_ansi {
296+
if pos < start {
297+
res_start += sub.len();
298+
res_end = res_start;
299+
} else if pos <= end {
300+
res_end += sub.len();
301+
} else {
302+
break 'outer;
303+
}
304+
} else {
305+
for c in sub.chars() {
306+
let c_width = char_width(c);
307+
308+
if pos < start {
309+
res_start += c.len_utf8();
310+
res_end = res_start;
311+
} else if pos + c_width <= end {
312+
res_end += c.len_utf8();
313+
} else {
314+
break 'outer;
315+
}
316+
317+
pos += char_width(c);
318+
}
319+
}
320+
}
321+
322+
&s[res_start..res_end]
323+
}
324+
270325
#[cfg(test)]
271326
mod tests {
327+
use crate::measure_text_width;
328+
272329
use super::*;
273330

274331
use lazy_static::lazy_static;
@@ -435,4 +492,37 @@ mod tests {
435492
assert_eq!(iter.rest_slice(), "");
436493
assert_eq!(iter.next(), None);
437494
}
495+
496+
#[test]
497+
fn test_slice_ansi_str() {
498+
// Note that 🐶 is two columns wide
499+
let test_str = "Hello\x1b[31m🐶\x1b[1m🐶\x1b[0m world!";
500+
assert_eq!(slice_ansi_str(test_str, 5, 5), "");
501+
assert_eq!(slice_ansi_str(test_str, 0, test_str.len()), test_str);
502+
503+
if cfg!(feature = "unicode-width") {
504+
assert_eq!(slice_ansi_str(test_str, 0, 5), "Hello\x1b[31m");
505+
assert_eq!(slice_ansi_str(test_str, 0, 6), "Hello\x1b[31m");
506+
assert_eq!(measure_text_width(test_str), 16);
507+
assert_eq!(slice_ansi_str(test_str, 0, 5), "Hello\x1b[31m");
508+
assert_eq!(slice_ansi_str(test_str, 0, 6), "Hello\x1b[31m");
509+
assert_eq!(slice_ansi_str(test_str, 0, 7), "Hello\x1b[31m🐶\x1b[1m");
510+
assert_eq!(slice_ansi_str(test_str, 7, 21), "\x1b[1m🐶\x1b[0m world!");
511+
assert_eq!(slice_ansi_str(test_str, 8, 21), "\x1b[0m world!");
512+
assert_eq!(slice_ansi_str(test_str, 9, 21), "\x1b[0m world!");
513+
514+
assert_eq!(
515+
slice_ansi_str(test_str, 4, 9),
516+
"o\x1b[31m🐶\x1b[1m🐶\x1b[0m"
517+
);
518+
} else {
519+
assert_eq!(slice_ansi_str(test_str, 0, 5), "Hello\x1b[31m");
520+
assert_eq!(slice_ansi_str(test_str, 0, 6), "Hello\x1b[31m🐶\u{1b}[1m");
521+
522+
assert_eq!(
523+
slice_ansi_str(test_str, 4, 9),
524+
"o\x1b[31m🐶\x1b[1m🐶\x1b[0m w"
525+
);
526+
}
527+
}
438528
}

src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ pub use crate::utils::{
8787
};
8888

8989
#[cfg(feature = "ansi-parsing")]
90-
pub use crate::ansi::{strip_ansi_codes, AnsiCodeIterator};
90+
pub use crate::ansi::{slice_ansi_str, strip_ansi_codes, AnsiCodeIterator};
9191

9292
mod common_term;
9393
mod kb;

src/utils.rs

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use lazy_static::lazy_static;
99
use crate::term::{wants_emoji, Term};
1010

1111
#[cfg(feature = "ansi-parsing")]
12-
use crate::ansi::{strip_ansi_codes, AnsiCodeIterator};
12+
use crate::ansi::AnsiCodeIterator;
1313

1414
#[cfg(not(feature = "ansi-parsing"))]
1515
fn strip_ansi_codes(s: &str) -> &str {
@@ -71,7 +71,17 @@ pub fn set_colors_enabled_stderr(val: bool) {
7171

7272
/// Measure the width of a string in terminal characters.
7373
pub fn measure_text_width(s: &str) -> usize {
74-
str_width(&strip_ansi_codes(s))
74+
#[cfg(feature = "ansi-parsing")]
75+
{
76+
AnsiCodeIterator::new(s)
77+
.filter(|(_, is_ansi)| !is_ansi)
78+
.map(|(sub, _)| str_width(sub))
79+
.sum()
80+
}
81+
#[cfg(not(feature = "ansi-parsing"))]
82+
{
83+
str_width(s)
84+
}
7585
}
7686

7787
/// A terminal color.
@@ -719,7 +729,7 @@ fn str_width(s: &str) -> usize {
719729
}
720730

721731
#[cfg(feature = "ansi-parsing")]
722-
fn char_width(c: char) -> usize {
732+
pub(crate) fn char_width(c: char) -> usize {
723733
#[cfg(feature = "unicode-width")]
724734
{
725735
use unicode_width::UnicodeWidthChar;
@@ -868,15 +878,18 @@ fn test_text_width() {
868878
.on_black()
869879
.bold()
870880
.force_styling(true)
871-
.to_string();
881+
.to_string()
882+
+ "🐶bar";
872883
assert_eq!(
873884
measure_text_width(&s),
874-
if cfg!(feature = "ansi-parsing") {
875-
3
876-
} else if cfg!(feature = "unicode-width") {
877-
17
878-
} else {
879-
21
885+
match (
886+
cfg!(feature = "ansi-parsing"),
887+
cfg!(feature = "unicode-width")
888+
) {
889+
(true, true) => 8,
890+
(true, false) => 7,
891+
(false, true) => 22,
892+
(false, false) => 25,
880893
}
881894
);
882895
}

0 commit comments

Comments
 (0)