1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
use crate::utils::ColumnDisplayInfo;

#[cfg(feature = "custom_styling")]
mod custom_styling;
#[cfg(not(feature = "custom_styling"))]
mod normal;

#[cfg(feature = "custom_styling")]
pub use custom_styling::*;
#[cfg(not(feature = "custom_styling"))]
pub use normal::*;

/// Split a line if it's longer than the allowed columns (width - padding).
///
/// This function tries to do this in a smart way, by splitting the content
/// with a given delimiter at the very beginning.
/// These "elements" then get added one-by-one to the lines, until a line is full.
/// As soon as the line is full, we add it to the result set and start a new line.
///
/// This is repeated until there're no more "elements".
///
/// Mid-element splits only occurs if a element doesn't fit in a single line by itself.
pub fn split_line(line: &str, info: &ColumnDisplayInfo, delimiter: char) -> Vec<String> {
    let mut lines = Vec::new();
    let content_width = usize::from(info.content_width);

    // Split the line by the given deliminator and turn the content into a stack.
    // Also clone it and convert it into a Vec<String>. Otherwise we get some burrowing problems
    // due to early drops of borrowed values that need to be inserted into `Vec<&str>`
    let mut elements = split_line_by_delimiter(line, delimiter);

    // Reverse it, since we want to push/pop without reversing the text.
    elements.reverse();

    let mut current_line = String::new();
    while let Some(next) = elements.pop() {
        let current_length = measure_text_width(&current_line);
        let next_length = measure_text_width(&next);

        // Some helper variables
        // The length of the current line when combining it with the next element
        // Add 1 for the delimiter if we are on a non-empty line.
        let mut added_length = next_length + current_length;
        if !current_line.is_empty() {
            added_length += 1;
        }
        // The remaining width for this column. If we are on a non-empty line, subtract 1 for the delimiter.
        let mut remaining_width = content_width - current_length;
        if !current_line.is_empty() {
            remaining_width = remaining_width.saturating_sub(1);
        }

        // The next element fits into the current line
        if added_length <= content_width {
            // Only add delimiter, if we're not on a fresh line
            if !current_line.is_empty() {
                current_line.push(delimiter);
            }
            current_line += &next;

            // Already complete the current line, if there isn't space for more than two chars
            current_line = check_if_full(&mut lines, content_width, current_line);
            continue;
        }

        // The next element doesn't fit in the current line

        // Check, if there's enough space in the current line in case we decide to split the
        // element and only append a part of it to the current line.
        // If there isn't enough space, we simply push the current line, put the element back
        // on stack and start with a fresh line.
        if !current_line.is_empty() && remaining_width <= MIN_FREE_CHARS {
            elements.push(next);
            lines.push(current_line);
            current_line = String::new();

            continue;
        }

        // Ok. There's still enough space to fit something in (more than MIN_FREE_CHARS characters)
        // There are two scenarios:
        //
        // 1. The word is too long for a single line.
        //    In this case, we have to split the element anyways. Let's fill the remaining space on
        //    the current line with, start a new line and push the remaining part on the stack.
        // 2. The word is short enough to fit as a whole into a line
        //    In that case we simply push the current line and start a new one with the current element

        // Case 1
        // The element is longer than the specified content_width
        // Split the word, push the remaining string back on the stack
        if next_length > content_width {
            let new_line = current_line.is_empty();

            // Only add delimiter, if we're not on a fresh line
            if !new_line {
                current_line.push(delimiter);
            }

            let (mut next, mut remaining) = split_long_word(remaining_width, &next);

            // This is a ugly hack, but it's needed for now.
            //
            // Scenario: The current column has to have a width of 1 and we work with a new line.
            // However, the next char is a multi-character UTF-8 symbol.
            //
            // Since a multi-character wide symbol doesn't fit into a 1-character column,
            // this code would loop endlessly. (There's no legitimate way to split that character.)
            // Hence, we have to live with the fact, that this line will look broken, as we put a
            // two-character wide symbol into it, despite the line being formatted for 1 character.
            if new_line && next.is_empty() {
                let mut chars = remaining.chars();
                next.push(chars.next().unwrap());
                remaining = chars.collect();
            }

            current_line += &next;
            elements.push(remaining);

            // Push the finished line, and start a new one
            lines.push(current_line);
            current_line = String::new();

            continue;
        }

        // Case 2
        // The element fits into a single line.
        // Push the current line and initialize the next line with the element.
        lines.push(current_line);
        current_line = next.to_string();
        current_line = check_if_full(&mut lines, content_width, current_line);
    }

    if !current_line.is_empty() {
        lines.push(current_line);
    }

    lines
}

/// This is the minimum of available characters per line.
/// It's used to check, whether another element can be added to the current line.
/// Otherwise the line will simply be left as it is and we start with a new one.
/// Two chars seems like a reasonable approach, since this would require next element to be
/// a single char + delimiter.
const MIN_FREE_CHARS: usize = 2;

/// Check if the current line is too long and whether we should start a new one
/// If it's too long, we add the current line to the list of lines and return a new [String].
/// Otherwise, we simply return the current line and basically don't do anything.
fn check_if_full(lines: &mut Vec<String>, content_width: usize, current_line: String) -> String {
    // Already complete the current line, if there isn't space for more than two chars
    if measure_text_width(&current_line) > content_width.saturating_sub(MIN_FREE_CHARS) {
        lines.push(current_line);
        return String::new();
    }

    current_line
}