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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
use std::fmt;

use super::{Color, Filled, Line, Paint};

use unicode_display_width as unicode;
use unicode_segmentation::UnicodeSegmentation as _;

/// Text that can be displayed on the terminal, measured, truncated and padded.
pub trait Cell: fmt::Display {
    /// Type after truncation.
    type Truncated: Cell;
    /// Type after padding.
    type Padded: Cell;

    /// Cell display width in number of terminal columns.
    fn width(&self) -> usize;
    /// Background color of cell.
    fn background(&self) -> Color {
        Color::Unset
    }
    /// Truncate cell if longer than given width. Shows the delimiter if truncated.
    #[must_use]
    fn truncate(&self, width: usize, delim: &str) -> Self::Truncated;
    /// Pad the cell so that it is the given width, while keeping the content left-aligned.
    #[must_use]
    fn pad(&self, width: usize) -> Self::Padded;
}

impl Cell for Paint<String> {
    type Truncated = Self;
    type Padded = Self;

    fn width(&self) -> usize {
        Cell::width(self.content())
    }

    fn background(&self) -> Color {
        self.style.background
    }

    fn truncate(&self, width: usize, delim: &str) -> Self {
        Self {
            item: self.item.truncate(width, delim),
            style: self.style,
        }
    }

    fn pad(&self, width: usize) -> Self {
        Self {
            item: self.item.pad(width),
            style: self.style,
        }
    }
}

impl Cell for Line {
    type Truncated = Line;
    type Padded = Line;

    fn width(&self) -> usize {
        Line::width(self)
    }

    fn pad(&self, width: usize) -> Self::Padded {
        let mut line = self.clone();
        Line::pad(&mut line, width);
        line
    }

    fn truncate(&self, width: usize, delim: &str) -> Self::Truncated {
        let mut line = self.clone();
        Line::truncate(&mut line, width, delim);
        line
    }
}

impl Cell for Paint<&str> {
    type Truncated = Paint<String>;
    type Padded = Paint<String>;

    fn width(&self) -> usize {
        Cell::width(self.item)
    }

    fn background(&self) -> Color {
        self.style.background
    }

    fn truncate(&self, width: usize, delim: &str) -> Paint<String> {
        Paint {
            item: self.item.truncate(width, delim),
            style: self.style,
        }
    }

    fn pad(&self, width: usize) -> Paint<String> {
        Paint {
            item: self.item.pad(width),
            style: self.style,
        }
    }
}

impl Cell for String {
    type Truncated = Self;
    type Padded = Self;

    fn width(&self) -> usize {
        Cell::width(self.as_str())
    }

    fn truncate(&self, width: usize, delim: &str) -> Self {
        self.as_str().truncate(width, delim)
    }

    fn pad(&self, width: usize) -> Self {
        self.as_str().pad(width)
    }
}

impl Cell for str {
    type Truncated = String;
    type Padded = String;

    fn width(&self) -> usize {
        self.graphemes(true)
            .map(|g| unicode::width(g) as usize)
            .sum()
    }

    fn truncate(&self, width: usize, delim: &str) -> String {
        if width < Cell::width(self) {
            let d = Cell::width(delim);
            if width < d {
                // If we can't even fit the delimiter, just return an empty string.
                return String::new();
            }
            // Find the unicode byte boundary where the display width is the largest,
            // while being smaller than the given max width.
            let mut cols = 0; // Number of visual columns we need.
            let mut boundary = 0; // Boundary in bytes.
            for g in self.graphemes(true) {
                let c = Cell::width(g);
                if cols + c + d > width {
                    break;
                }
                boundary += g.len();
                cols += c;
            }
            // Don't add the delimiter if we just trimmed whitespace.
            if self[boundary..].trim().is_empty() {
                self[..boundary + 1].to_owned()
            } else {
                format!("{}{delim}", &self[..boundary])
            }
        } else {
            self.to_owned()
        }
    }

    fn pad(&self, max: usize) -> String {
        let width = Cell::width(self);

        if width < max {
            format!("{self}{}", " ".repeat(max - width))
        } else {
            self.to_owned()
        }
    }
}

impl<T: Cell + ?Sized> Cell for &T {
    type Truncated = T::Truncated;
    type Padded = T::Padded;

    fn width(&self) -> usize {
        T::width(self)
    }

    fn truncate(&self, width: usize, delim: &str) -> Self::Truncated {
        T::truncate(self, width, delim)
    }

    fn pad(&self, width: usize) -> Self::Padded {
        T::pad(self, width)
    }
}

impl<T: Cell + fmt::Display> Cell for Filled<T> {
    type Truncated = T::Truncated;
    type Padded = T::Padded;

    fn width(&self) -> usize {
        T::width(&self.item)
    }

    fn background(&self) -> Color {
        self.color
    }

    fn truncate(&self, width: usize, delim: &str) -> Self::Truncated {
        T::truncate(&self.item, width, delim)
    }

    fn pad(&self, width: usize) -> Self::Padded {
        T::pad(&self.item, width)
    }
}

#[cfg(test)]
mod test {
    #[test]
    fn test_width() {
        assert_eq!(unicode_display_width::width("❤️"), 2);
        assert_eq!(unicode_display_width::width("🪵"), 2);
    }
}