1use itertools::Itertools;
2use lazy_static;
3use regex::Regex;
4use std::{borrow::Cow, cell::RefCell, fmt, iter};
5use unicode_width::UnicodeWidthChar;
6
7use unicode_linebreak::{linebreaks, BreakOpportunity};
8use unicode_width::UnicodeWidthStr;
9
10#[derive(Clone, Copy, Debug, Eq, PartialEq)]
12pub enum Alignment {
13 Left,
14 Right,
15 Center,
16}
17
18#[derive(Debug, Clone)]
24pub struct Cell<'txt> {
25 pub(crate) content: Cow<'txt, str>,
26 pub(crate) col_span: usize,
27 pub(crate) alignment: Alignment,
28 pub(crate) pad_content: bool,
29
30 layout_newlines: RefCell<Option<Vec<usize>>>,
34
35 content_without_ansi_esc: Option<String>,
36}
37
38impl<'txt> Default for Cell<'txt> {
39 fn default() -> Self {
40 Self {
41 content: Cow::Borrowed(""),
42 col_span: 1,
43 alignment: Alignment::Left,
44 pad_content: true,
45
46 layout_newlines: RefCell::new(None),
47 content_without_ansi_esc: None,
48 }
49 }
50}
51
52impl<'txt> Cell<'txt> {
53 fn owned(content: String) -> Cell<'txt> {
54 let mut this = Self {
55 content: Cow::Owned(content),
56 ..Default::default()
57 };
58 this.update_without_ansi_esc();
59 this
60 }
61
62 fn borrowed(content: &'txt str) -> Self {
64 let mut this = Self {
65 content: Cow::Borrowed(content.as_ref()),
66 ..Default::default()
67 };
68 this.update_without_ansi_esc();
69 this
70 }
71
72 pub fn with_content(mut self, content: impl Into<Cow<'txt, str>>) -> Self {
73 self.set_content(content);
74 self
75 }
76
77 pub fn set_content(&mut self, content: impl Into<Cow<'txt, str>>) -> &mut Self {
78 self.content = content.into();
79 self.update_without_ansi_esc();
80 self
81 }
82
83 fn content_for_layout(&self) -> &str {
84 self.content_without_ansi_esc
85 .as_ref()
86 .map(|s| s.as_str())
87 .unwrap_or(&self.content)
88 }
89
90 fn update_without_ansi_esc(&mut self) {
91 self.content_without_ansi_esc = if ANSI_ESC_RE.is_match(&self.content) {
92 Some(ANSI_ESC_RE.split(&self.content).collect())
93 } else {
94 None
95 };
96 }
97
98 pub fn with_col_span(mut self, col_span: usize) -> Self {
104 self.set_col_span(col_span);
105 self
106 }
107
108 pub fn set_col_span(&mut self, col_span: usize) -> &mut Self {
114 assert!(col_span > 0, "cannot have a col_span of 0");
115 self.col_span = col_span;
116 *self.layout_newlines.borrow_mut() = None;
117 self
118 }
119
120 pub fn with_alignment(mut self, alignment: Alignment) -> Self {
121 self.set_alignment(alignment);
122 self
123 }
124
125 pub fn set_alignment(&mut self, alignment: Alignment) -> &mut Self {
126 self.alignment = alignment;
127 *self.layout_newlines.borrow_mut() = None;
128 self
129 }
130
131 pub fn with_padding(mut self, padding: bool) -> Self {
132 self.set_padding(padding);
133 self
134 }
135
136 pub fn set_padding(&mut self, padding: bool) -> &mut Self {
137 self.pad_content = padding;
138 *self.layout_newlines.borrow_mut() = None;
139 self
140 }
141
142 pub(crate) fn layout(&self, width: Option<usize>) -> usize {
150 let width = width.unwrap_or(usize::MAX);
152 if width < 1 || (self.pad_content && width < 3) {
153 panic!("cell too small to show anything");
154 }
155 let content_width = if self.pad_content {
156 width.saturating_sub(2)
157 } else {
158 width
159 };
160 let mut ln = self.layout_newlines.borrow_mut();
161 let ln = ln.get_or_insert(vec![]);
162 ln.clear();
163 ln.push(0);
164
165 let mut s = self.content_for_layout();
166 let mut acc = 0;
168 while let Some(idx) = next_linebreak(s, content_width) {
169 s = &s[idx..];
170 ln.push(idx + acc);
171 acc += idx;
172 }
173 ln.pop();
175 ln.len()
177 }
178
179 pub(crate) fn min_width(&self, only_mandatory: bool) -> usize {
184 let content = self.content_for_layout();
185 let max_newline_gap = linebreaks(content).filter_map(|(idx, ty)| {
186 if only_mandatory && !matches!(ty, BreakOpportunity::Mandatory) {
187 None
188 } else {
189 Some(idx)
190 }
191 });
192 let max_newline_gap = iter::once(0)
193 .chain(max_newline_gap)
194 .chain(iter::once(content.len()))
195 .tuple_windows()
196 .map(|(start, end)| content[start..end].width())
197 .max()
198 .unwrap_or(0);
199
200 max_newline_gap + if self.pad_content { 2 } else { 0 }
202 }
203
204 pub(crate) fn width<'s>(
208 &self,
209 border_width: usize,
210 cell_widths: &'s [usize],
211 ) -> (usize, &'s [usize]) {
212 (
213 cell_widths[..self.col_span].iter().copied().sum::<usize>()
214 + border_width * self.col_span.saturating_sub(1),
215 &cell_widths[self.col_span..],
216 )
217 }
218
219 pub(crate) fn render_line(
224 &self,
225 line_idx: usize,
226 width: usize,
227 f: &mut fmt::Formatter,
228 ) -> fmt::Result {
229 let newlines = self.layout_newlines.borrow();
230 let newlines = newlines.as_ref().expect("missed call to `layout`");
231 let line = match newlines.get(line_idx) {
232 Some(&start_idx) => match newlines.get(line_idx + 1) {
233 Some(&end_idx) => &self.content[start_idx..end_idx],
234 None => &self.content[start_idx..],
235 },
236 None => "",
238 };
239
240 let (front_pad, back_pad) = self.get_padding(width, line.width());
241 let edge = self.edge_char();
242 f.write_str(edge)?;
243 for _ in 0..front_pad {
244 f.write_str(" ")?;
245 }
246 f.write_str(line)?;
247 for _ in 0..back_pad {
248 f.write_str(" ")?;
249 }
250 f.write_str(edge)
251 }
252
253 fn get_padding(&self, width: usize, line_width: usize) -> (usize, usize) {
258 let padding = if self.pad_content { 2 } else { 0 };
259 let gap = (width - line_width).saturating_sub(padding);
260 match self.alignment {
261 Alignment::Left => (0, gap),
262 Alignment::Center => (gap / 2, gap - gap / 2),
263 Alignment::Right => (gap, 0),
264 }
265 }
266
267 fn edge_char(&self) -> &'static str {
268 if self.pad_content {
269 " "
270 } else {
271 "\0"
272 }
273 }
274}
275
276impl<'txt> From<String> for Cell<'txt> {
277 fn from(other: String) -> Self {
278 Cell::owned(other)
279 }
280}
281
282impl<'txt> From<&'txt String> for Cell<'txt> {
283 fn from(other: &'txt String) -> Self {
284 Cell::borrowed(other)
285 }
286}
287
288impl<'txt> From<&'txt str> for Cell<'txt> {
289 fn from(other: &'txt str) -> Self {
290 Cell::borrowed(other)
291 }
292}
293
294lazy_static! {
297 static ref ANSI_ESC_RE: Regex =
298 Regex::new(r"[\x1b\x9b][\[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-PRZcf-nqry=><]")
299 .unwrap();
300}
301
302fn next_linebreak(text: &str, max_width: usize) -> Option<usize> {
304 let mut prev = None;
305 for (idx, ty) in linebreaks(text) {
306 if text[..idx].width() > max_width {
307 if let Some(prev) = prev {
309 return Some(prev);
310 };
311 if let Some(linebreak) = next_linebreak_midword(text, max_width) {
313 return Some(linebreak);
314 }
315 return text.chars().next().map(|ch| ch.width()).flatten();
317 } else if matches!(ty, BreakOpportunity::Mandatory) {
318 return Some(idx);
320 } else {
321 prev = Some(idx);
322 }
323 }
324 None
325}
326
327fn next_linebreak_midword(text: &str, max_width: usize) -> Option<usize> {
329 let mut prev = None;
330 for (idx, _) in text.char_indices() {
331 if text[..idx].width() > max_width {
332 return prev;
333 } else {
334 prev = Some(idx);
335 }
336 }
337 unreachable!()
340}