reedline/painting/
utils.rs

1use std::borrow::Cow;
2use unicode_width::UnicodeWidthStr;
3
4/// Ensures input uses CRLF line endings.
5///
6/// Needed for correct output in raw mode.
7/// Only replaces solitary LF with CRLF.
8pub(crate) fn coerce_crlf(input: &str) -> Cow<str> {
9    let mut result = Cow::Borrowed(input);
10    let mut cursor: usize = 0;
11    for (idx, _) in input.match_indices('\n') {
12        if !(idx > 0 && input.as_bytes()[idx - 1] == b'\r') {
13            if let Cow::Borrowed(_) = result {
14                // Best case 1 allocation, worst case 2 allocations
15                let mut owned = String::with_capacity(input.len() + 1);
16                // Optimization to avoid the `AddAssign for Cow<str>`
17                // optimization for `Cow<str>.is_empty` that would replace the
18                // preallocation
19                owned.push_str(&input[cursor..idx]);
20                result = Cow::Owned(owned);
21            } else {
22                result += &input[cursor..idx];
23            }
24            result += "\r\n";
25            // Advance beyond the matched LF char (single byte)
26            cursor = idx + 1;
27        }
28    }
29    if let Cow::Owned(_) = result {
30        result += &input[cursor..input.len()];
31    }
32    result
33}
34
35/// Returns string with the ANSI escape codes removed
36///
37/// If parsing fails silently returns the input string
38pub(crate) fn strip_ansi(string: &str) -> String {
39    String::from_utf8(strip_ansi_escapes::strip(string))
40        .map_err(|_| ())
41        .unwrap_or_else(|_| string.to_owned())
42}
43
44pub(crate) fn estimate_required_lines(input: &str, screen_width: u16) -> usize {
45    input.lines().fold(0, |acc, line| {
46        let wrap = estimate_single_line_wraps(line, screen_width);
47
48        acc + 1 + wrap
49    })
50}
51
52/// Reports the additional lines needed due to wrapping for the given line.
53///
54/// Does not account for any potential linebreaks in `line`
55///
56/// If `line` fits in `terminal_columns` returns 0
57pub(crate) fn estimate_single_line_wraps(line: &str, terminal_columns: u16) -> usize {
58    let estimated_width = line_width(line);
59    let terminal_columns: usize = terminal_columns.into();
60
61    // integer ceiling rounding division for positive divisors
62    let estimated_line_count = (estimated_width + terminal_columns - 1) / terminal_columns;
63
64    // Any wrapping will add to our overall line count
65    estimated_line_count.saturating_sub(1)
66}
67
68/// Compute the line width for ANSI escaped text
69pub(crate) fn line_width(line: &str) -> usize {
70    strip_ansi(line).width()
71}
72
73#[cfg(test)]
74mod test {
75    use super::*;
76    use pretty_assertions::assert_eq;
77    use rstest::rstest;
78
79    #[rstest]
80    #[case("sentence\nsentence", "sentence\r\nsentence")]
81    #[case("sentence\r\nsentence", "sentence\r\nsentence")]
82    #[case("sentence\nsentence\n", "sentence\r\nsentence\r\n")]
83    #[case("šŸ˜‡\nsentence", "šŸ˜‡\r\nsentence")]
84    #[case("sentence\nšŸ˜‡", "sentence\r\nšŸ˜‡")]
85    #[case("\n", "\r\n")]
86    #[case("", "")]
87    fn test_coerce_crlf(#[case] input: &str, #[case] expected: &str) {
88        let result = coerce_crlf(input);
89
90        assert_eq!(result, expected);
91
92        assert!(
93            input != expected || matches!(result, Cow::Borrowed(_)),
94            "Unnecessary allocation"
95        )
96    }
97}