1use crate::{
2 Align, ColSpec, FancyTable, FancyTableBuilder, FancyTableOpts, Layout, Overflow, Separator,
3 TitleAlign, TitleSpec,
4 charset::Charset,
5 juststr::{JustedString, Justify},
6};
7
8const DEFAULT_COLUMN_WIDTH: usize = 10;
9
10impl Default for FancyTableOpts {
11 fn default() -> Self {
12 Self {
13 title_align: TitleAlign::LeftOffset(4),
14 charset: Charset::Modern,
15 headers_separator: Some(Separator::Double),
16 rows_separator: None,
17 max_lines: 3,
18 }
19 }
20}
21
22impl<'a, T: AsRef<str>> FancyTableBuilder<'a, T> {
23 fn new(opts: FancyTableOpts) -> Self {
24 Self {
25 headers: Vec::new(),
26 columns: Vec::new(),
27 padding: 1,
28 width: 80,
29 charset: opts.charset,
30 rows_separator: opts.rows_separator,
31 headers_separator: opts.headers_separator,
32 max_lines: opts.max_lines,
33 title: None,
34 title_align: opts.title_align,
35 }
36 }
37 fn add_column_spec(
38 mut self,
39 width: usize,
40 max_lines: usize,
41 layout: Layout,
42 align: Align,
43 overflow: Overflow,
44 ) -> Self {
45 self.columns.push(ColSpec {
46 width,
47 layout,
48 align,
49 overflow,
50 max_lines,
51 });
52 self
53 }
54
55 pub fn add_column(
56 mut self,
57 header: Option<T>,
58 layout: Layout,
59 align: Align,
60 overflow: Overflow,
61 max_lines: usize,
62 ) -> Self {
63 let len = match layout {
64 Layout::Fixed(f) => f,
65 _ => header
66 .as_ref()
67 .map(|h| h.as_ref().chars().count())
68 .unwrap_or(DEFAULT_COLUMN_WIDTH),
69 };
70 if let Some(header) = header {
71 self.headers.push(header);
72 }
73 self.add_column_spec(len, max_lines, layout, align, overflow)
74 }
75 pub fn add_column_named(self, header: T, layout: Layout) -> Self {
76 self.add_column_named_with_align(header, layout, Align::Left)
77 }
78 pub fn add_column_named_wrapping(self, header: T, layout: Layout) -> Self {
79 self.add_column_named_wrapping_with_align(header, layout, Align::Left)
80 }
81 pub fn add_column_named_with_align(mut self, header: T, layout: Layout, align: Align) -> Self {
82 let len = header.as_ref().len();
83 let max_lines = self.max_lines;
84
85 self.headers.push(header);
86 self.add_column_spec(len, max_lines, layout, align, Overflow::Truncate)
87 }
88 pub fn add_column_named_wrapping_with_align(
89 mut self,
90 header: T,
91 layout: Layout,
92 align: Align,
93 ) -> Self {
94 let len = header.as_ref().len();
95 let max_lines = self.max_lines;
96
97 self.headers.push(header);
98 self.add_column_spec(len, max_lines, layout, align, Overflow::Wrap)
99 }
100 pub fn add_title(mut self, title: &'a str) -> Self {
101 self.title = Some(title);
102 self
103 }
104 pub fn add_title_with_align(mut self, title: &'a str, align: TitleAlign) -> Self {
105 self.title_align = align;
106 self.add_title(title)
107 }
108 pub fn padding(mut self, padding: usize) -> Self {
109 self.padding = padding;
110 self
111 }
112 pub fn hseparator(mut self, separator: Option<Separator>) -> Self {
113 self.headers_separator = separator;
114 self
115 }
116 pub fn rseparator(mut self, separator: Option<Separator>) -> Self {
117 self.rows_separator = separator;
118 self
119 }
120 pub fn width(mut self, width: usize) -> Self {
121 self.width = width;
122 self
123 }
124
125 pub fn build(self) -> FancyTable<'a, T> {
126 let title = self.title.map(|t| TitleSpec {
127 title: t,
128 align: self.title_align,
129 });
130 let mut table = FancyTable {
131 width: self.width,
132 chars: self.charset.get_chars(),
133 rows_separator: self.rows_separator,
134 headers_separator: self.headers_separator,
135 padding: self.padding,
136 headers: self.headers,
137 columns: self.columns,
138 title,
139 };
140 table.recalculate(self.width);
141 table
142 }
143}
144
145impl<'a, T: AsRef<str>> FancyTable<'a, T> {
146 pub fn create(opts: FancyTableOpts) -> FancyTableBuilder<'a, T> {
147 FancyTableBuilder::new(opts)
148 }
149
150 fn recalculate(&mut self, table_width: usize) {
151 let cols_count = self.columns.len();
152 let mut min_table_width = 0;
153
154 for (i, spec) in self.columns.iter_mut().enumerate() {
156 let column_width = match spec.layout {
157 Layout::Fixed(width) => width,
158 Layout::Slim | Layout::Expandable(_) => self
159 .headers
160 .get(i)
161 .map(|h| h.as_ref().len() + (2 * self.padding))
162 .unwrap_or(0),
163 };
164 spec.width = column_width;
165 min_table_width += spec.width;
166 }
167
168 min_table_width += cols_count + 1;
169
170 let mut remaining_width = table_width.saturating_sub(min_table_width);
174
175 if remaining_width > 0 {
176 let mut expandable_count = self
178 .columns
179 .iter()
180 .filter(|c| matches!(c.layout, Layout::Expandable(_)))
181 .count();
182
183 for c in self.columns.iter_mut() {
185 if let Layout::Expandable(max_width) = c.layout {
186 let new_width = compensate(c.width, max_width, remaining_width / expandable_count);
187 let compensation = new_width.saturating_sub(c.width);
188
189 if new_width > c.width {
190 c.width = new_width;
191 }
192 remaining_width -= compensation;
193 expandable_count -= 1;
194 }
195 }
196 }
197 }
198
199 fn generate_empty_string(&self, col_idx: usize, padding: usize) -> String {
200 if let Some(col) = self.columns.get(col_idx) {
201 let width = col.width.saturating_sub(2 * padding);
202 let mut result = String::with_capacity(width);
203 result.push_str(&" ".repeat(width));
204 return result;
205 }
206 String::default()
207 }
208
209 fn separator_chars(&self, separator: &Option<Separator>) -> (char, char, char, char) {
210 let ch = &self.chars;
211 match separator {
212 Some(Separator::Single) => (ch.ew, ch.news, ch.nes, ch.nws),
213 Some(Separator::Double) => (ch.dew, ch.dnews, ch.dnes, ch.dnws),
214 Some(Separator::Custom(c)) => (*c, ch.news, ch.nes, ch.nws),
215 None => ('-', '|', '|', '|'),
216 }
217 }
218
219 fn render_row(&self, row: &'a [T]) {
220 let mut padded = row
221 .iter()
222 .enumerate()
223 .map(|(i, s)| {
224 let col = self.columns.get(i).unwrap();
225 let pad = match col.align {
226 Align::Left => Justify::Left,
227 Align::Right => Justify::Right,
228 Align::Center => Justify::Center,
229 };
230 match col.overflow {
231 Overflow::Truncate => JustedString::truncating(s.as_ref()),
232 Overflow::Wrap => JustedString::wrapping(s.as_ref()),
233 }
234 .justify(
235 col.width.saturating_sub(2 * self.padding),
236 col.max_lines,
237 pad,
238 )
239 })
240 .collect::<Vec<_>>();
241
242 let ns = self.chars.ns;
243 let len = padded.len();
244 let max_lines = padded.iter().map(|s| s.len()).max().unwrap_or(0);
245 let str_padding = self.padding;
246 let edg_padding = self.padding + 1;
247
248 for _ in 0..max_lines {
249 print!("{:edg_padding$}", ns);
250 for (i, vs) in padded.iter_mut().enumerate() {
251 let s = vs
252 .pop_front()
253 .unwrap_or_else(|| self.generate_empty_string(i, str_padding));
254 print!("{s}");
255 if i < len - 1 {
256 print!("{:>str_padding$}{ns}{:>str_padding$}", "", "");
257 }
258 }
259 println!("{:>edg_padding$}", ns);
260 }
261 }
262
263 pub fn render<R: AsRef<[T]>>(&self, rows: Vec<R>) {
264 let ch = &self.chars;
265 let cols_count = self.columns.len();
266 let rows_count = rows.len();
267 let rsep_chars = self.separator_chars(&self.rows_separator);
268 let hsep_chars = self.separator_chars(&self.headers_separator);
269 let title_width = self
270 .title
271 .as_ref()
272 .map(|ts| ts.title.len() + 4)
273 .unwrap_or(0);
274
275 let mut acc = 1;
276 let mut border_top = vec![ch.ew; self.width];
277 let mut border_btm = vec![ch.ew; self.width];
278 let mut hseparator = vec![hsep_chars.0; self.width];
279 let mut rseparator = vec![rsep_chars.0; self.width];
280
281 border_top[0] = ch.se;
282 border_btm[0] = ch.ne;
283 border_top[self.width - 1] = ch.sw;
284 border_btm[self.width - 1] = ch.nw;
285
286 hseparator[0] = hsep_chars.2;
287 rseparator[0] = rsep_chars.2;
288 hseparator[self.width - 1] = hsep_chars.3;
289 rseparator[self.width - 1] = rsep_chars.3;
290
291 for (i, spec) in self.columns.iter().enumerate() {
293 if i < cols_count - 1 {
294 acc += spec.width + 1;
295 border_top[acc - 1] = ch.ews;
296 border_btm[acc - 1] = ch.new;
297 hseparator[acc - 1] = hsep_chars.1;
298 rseparator[acc - 1] = rsep_chars.1;
299 }
300 }
301
302 if title_width > 0 && title_width < self.width - 4 {
304 let spec = self.title.as_ref().unwrap();
305 let start = match spec.align {
306 TitleAlign::LeftOffset(lo) => lo + 1,
307 TitleAlign::RightOffset(ro) => self.width - ro - title_width - 1,
308 };
309 let end = start + title_width;
310 let tch = ch.title;
311 border_top.splice(start..end, format!("{tch} {} {tch}", spec.title).chars());
312 }
313
314 let top = border_top.iter().collect::<String>();
315 let btm = border_btm.iter().collect::<String>();
316 let h_sep = hseparator.iter().collect::<String>();
317 let r_sep = rseparator.iter().collect::<String>();
318
319 println!("{top}");
320 if !self.headers.is_empty() {
321 self.render_row(self.headers.as_slice());
322 if self.headers_separator.is_some() {
323 println!("{h_sep}");
324 }
325 }
326 for (i, r) in rows.iter().enumerate() {
327 self.render_row(r.as_ref());
328 if i < rows_count - 1 && self.rows_separator.is_some() {
329 println!("{r_sep}");
330 }
331 }
332 println!("{btm}");
333 }
334}
335
336fn compensate(width: usize, max_width: usize, compensation: usize) -> usize {
337 let compensated = width + compensation;
338 if compensated > max_width {
339 max_width
340 } else {
341 compensated
342 }
343}
344
345#[cfg(test)]
346mod test {
347 use super::*;
348
349 #[test]
350 fn basic_constraints() {
351 let table = FancyTable::create(FancyTableOpts::default())
352 .add_column_named("ID", Layout::Fixed(8))
353 .add_column_named("NAME", Layout::Fixed(4))
354 .add_column_named("ROLE", Layout::Fixed(10))
355 .add_column_named("PERMISSION", Layout::Expandable(30))
356 .add_column_named("DESCRIPTION", Layout::Expandable(150))
357 .add_title("props")
358 .padding(0)
359 .build();
360
361 assert_eq!(table.columns.first().unwrap().width, 8);
362 assert_eq!(table.columns.get(1).unwrap().width, 4);
363 assert_eq!(table.columns.get(2).unwrap().width, 10);
364 assert_eq!(table.columns.get(3).unwrap().width, 25);
365 assert_eq!(
366 table.columns.get(4).unwrap().width,
367 80 - 6 - 8 - 4 - 10 - 25
368 );
369 }
370
371 #[test]
372 fn slim_table() {
373 let table = FancyTable::create(FancyTableOpts::default())
374 .add_column_named("ID", Layout::Slim)
375 .add_column_named("NAME", Layout::Slim)
376 .add_column_named("ROLE", Layout::Fixed(10))
377 .add_column_named("PERMISSION", Layout::Expandable(30))
378 .add_column_named("DESCRIPTION", Layout::Expandable(50))
379 .padding(0)
380 .width(0)
381 .build();
382
383 assert_eq!(table.columns.first().unwrap().width, 2);
384 assert_eq!(table.columns.get(1).unwrap().width, 4);
385 assert_eq!(table.columns.get(2).unwrap().width, 10);
386 assert_eq!(table.columns.get(3).unwrap().width, 10);
387 assert_eq!(table.columns.get(4).unwrap().width, 11);
388 }
389}