1use std::cmp::{max, min};
5use std::collections::HashMap;
6use std::fmt;
7use std::fmt::Formatter;
8use unicode_width::UnicodeWidthChar;
9
10#[derive(Clone, Debug)]
17pub struct Table<'a> {
18 rows: Vec<Row>,
19 style: &'a Style,
20 invert_header: bool,
21 border_attr: Attributes,
22 table_attr: Option<Attributes>,
23 col_alt_attr: [Option<Attributes>; 2],
24 col_attr: HashMap<usize, Attributes>,
25 row_alt_attr: [Option<Attributes>; 2],
26 row_attr: HashMap<usize, Attributes>,
27 cell_attr: HashMap<(usize, usize), Attributes>,
28 table_align: (Horizontal, Vertical),
29 col_align: HashMap<usize, (Option<Horizontal>, Option<Vertical>)>,
30 row_align: HashMap<usize, (Option<Horizontal>, Option<Vertical>)>,
31 cell_align: HashMap<(usize, usize), (Option<Horizontal>, Option<Vertical>)>,
32}
33
34#[derive(Clone, Debug)]
39struct Row {
40 cells: Vec<String>,
41}
42
43#[repr(usize)]
45pub enum StyleIdx {
46 Left = 0,
47 Pad = 1,
48 Right = 2,
49 Top = 3,
50 Inter = 4,
51 Bot = 5,
52}
53
54impl StyleIdx {
55 pub const COUNT: usize = 6;
57}
58
59#[derive(Clone, Debug)]
72pub struct Style {
73 pub top: [&'static str; StyleIdx::COUNT],
74 pub row: [&'static str; StyleIdx::COUNT],
75 pub sep: [&'static str; StyleIdx::COUNT],
76 pub bot: [&'static str; StyleIdx::COUNT],
77}
78
79#[derive(Clone, Debug)]
82pub struct Attributes {
83 on: String,
84 off: String,
85}
86
87#[derive(Clone, Debug)]
90struct LayoutLine {
91 line: String,
92 width: usize,
93}
94
95#[derive(Clone, Debug)]
98struct LayoutCell {
99 lines: Vec<LayoutLine>,
100 width: usize,
101}
102
103#[derive(Clone, Debug)]
105struct LayoutRow {
106 cells: Vec<LayoutCell>,
107 height: usize,
108}
109
110#[derive(Clone, Debug)]
115struct LayoutTable<'a> {
116 config: &'a Table<'a>,
117 rows: Vec<LayoutRow>,
118 widths: Vec<usize>,
119}
120
121#[rustfmt::skip]
128pub mod styles {
129 use super::Style;
130
131 pub const ASCII_BORDER: Style = Style {
132 top: ["+", "-", "+", "+", "+", "+"],
133 row: ["|", "-", "|", "|", "|", "|"],
134 sep: ["+", "-", "+", "+", "+", "+"],
135 bot: ["+", "-", "+", "+", "+", "+"],
136 };
137
138 pub const ASCII_NO_BORDER: Style = Style {
139 top: ["", "", "", "", "", "" ],
140 row: ["", " ", "", "|", "|", "|"],
141 sep: ["", "-", "", "+", "+", "+"],
142 bot: ["", "", "", "", "", "" ],
143 };
144
145 pub const ASCII_NO_ROW_SEPARATOR: Style = Style {
146 top: ["", "", "", "", "", "" ],
147 row: ["", " ", "", "|", "|", "|"],
148 sep: ["", "", "", "", "", "" ],
149 bot: ["", "", "", "", "", "" ],
150 };
151
152 pub const GLYPHS_SQUARE: Style = Style {
153 top: ["┌", "─", "┐", "┬", "┼", "┴"],
154 row: ["│", "─", "│", "│", "│", "│"],
155 sep: ["├", "─", "┤", "┬", "┼", "┴"],
156 bot: ["└", "─", "┘", "┬", "┼", "┴"],
157 };
158
159 pub const GLYPHS_ROUNDED: Style = Style {
160 top: ["╭", "─", "╮", "┬", "┼", "┴"],
161 row: ["│", "─", "│", "│", "│", "│"],
162 sep: ["├", "─", "┤", "┬", "┼", "┴"],
163 bot: ["╰", "─", "╯", "┬", "┼", "┴"],
164 };
165
166 pub const GLYPHS_ROUNDED_SPACED: Style = Style {
167 top: ["╭─", "─", "─╮", "─┬─", "─┼─", "─┴─"],
168 row: ["│ ", "─", " │", " │ ", " │ ", " │ "],
169 sep: ["├─", "─", "─┤", "─┬─", "─┼─", "─┴─"],
170 bot: ["╰─", "─", "─╯", "─┬─", "─┼─", "─┴─"],
171 };
172
173 pub const GLYPHS_NO_BORDER: Style = Style {
174 top: ["", "", "", "", "", "" ],
175 row: ["", "─", "", "│", "│", "│"],
176 sep: ["", "─", "", "┬", "┼", "┴"],
177 bot: ["", "", "", "", "", "" ],
178 };
179
180 pub const GLYPHS_NO_ROW_SEPARATOR: Style = Style {
181 top: ["", "", "", "", "", "" ],
182 row: ["", " ", "", "│", "│", "│"],
183 sep: ["", "", "", "", "", "" ],
184 bot: ["", "", "", "", "", "" ],
185 };
186
187 pub const SPACE: Style = Style {
188 top: ["", "", "", "", "", ""],
189 row: ["", " ", "", "", " ", ""],
190 sep: ["", "", "", "", "", ""],
191 bot: ["", "", "", "", "", ""],
192 };
193
194 pub const HEAVY: Style = Style {
195 top: ["┏", "━", "┓", "┳", "╋", "┻"],
196 row: ["┃", "━", "┃", "┃", "┃", "┃"],
197 sep: ["┣", "━", "┫", "┳", "╋", "┻"],
198 bot: ["┗", "━", "┛", "┳", "╋", "┻"],
199 };
200
201 pub const DOUBLE: Style = Style {
202 top: ["╔", "═", "╗", "╦", "╬", "╩"],
203 row: ["║", "═", "║", "║", "║", "║"],
204 sep: ["╠", "═", "╣", "╦", "╬", "╩"],
205 bot: ["╚", "═", "╝", "╦", "╬", "╩"],
206 };
207
208 pub const HEAVY_MIXED: Style = Style {
209 top: ["┏", "─", "┓", "┳", "╋", "┻"],
210 row: ["│", "─", "│", "│", "│", "│"],
211 sep: ["┣", "─", "┫", "┳", "╋", "┻"],
212 bot: ["┗", "─", "┛", "┳", "╋", "┻"],
213 };
214
215 pub const DOUBLE_HEAVY: Style = Style {
216 top: ["╔", "━", "╗", "╦", "╬", "╩"],
217 row: ["┃", "━", "┃", "┃", "┃", "┃"],
218 sep: ["╠", "━", "╣", "╦", "╬", "╩"],
219 bot: ["╚", "━", "╝", "╦", "╬", "╩"],
220 };
221}
222
223#[rustfmt::skip]
225pub mod ansi {
226 pub const RESET: &str = "\x1b[0;0m";
227
228 pub const BOLD: &str = "\x1b[1m";
229 pub const DIM: &str = "\x1b[2m";
230 pub const ITALIC: &str = "\x1b[3m";
231 pub const UNDERLINE: &str = "\x1b[4m";
232 pub const INVERT: &str = "\x1b[7m";
233 pub const INVERT_OFF: &str = "\x1b[27m";
234
235 pub const BLACK: &str = "\x1b[30m";
236 pub const RED: &str = "\x1b[31m";
237 pub const GREEN: &str = "\x1b[32m";
238 pub const YELLOW: &str = "\x1b[33m";
239 pub const BLUE: &str = "\x1b[34m";
240 pub const MAGENTA: &str = "\x1b[35m";
241 pub const CYAN: &str = "\x1b[36m";
242 pub const WHITE: &str = "\x1b[37m";
243
244 pub const BRIGHT_BLACK: &str = "\x1b[90m";
245 pub const BRIGHT_RED: &str = "\x1b[91m";
246 pub const BRIGHT_GREEN: &str = "\x1b[92m";
247 pub const BRIGHT_YELLOW: &str = "\x1b[93m";
248 pub const BRIGHT_BLUE: &str = "\x1b[94m";
249 pub const BRIGHT_MAGENTA: &str = "\x1b[95m";
250 pub const BRIGHT_CYAN: &str = "\x1b[96m";
251 pub const BRIGHT_WHITE: &str = "\x1b[97m";
252
253 pub const BG_BLACK: &str = "\x1b[40m";
254 pub const BG_RED: &str = "\x1b[41m";
255 pub const BG_GREEN: &str = "\x1b[42m";
256 pub const BG_YELLOW: &str = "\x1b[43m";
257 pub const BG_BLUE: &str = "\x1b[44m";
258 pub const BG_MAGENTA: &str = "\x1b[45m";
259 pub const BG_CYAN: &str = "\x1b[46m";
260 pub const BG_WHITE: &str = "\x1b[47m";
261
262 pub const BG_BRIGHT_BLACK: &str = "\x1b[100m";
263 pub const BG_BRIGHT_RED: &str = "\x1b[101m";
264 pub const BG_BRIGHT_GREEN: &str = "\x1b[102m";
265 pub const BG_BRIGHT_YELLOW: &str = "\x1b[103m";
266 pub const BG_BRIGHT_BLUE: &str = "\x1b[104m";
267 pub const BG_BRIGHT_MAGENTA: &str = "\x1b[105m";
268 pub const BG_BRIGHT_CYAN: &str = "\x1b[106m";
269 pub const BG_BRIGHT_WHITE: &str = "\x1b[107m";
270
271 pub fn fg_256(n: u8) -> String {
273 format!("\x1b[38;5;{}m", n)
274 }
275
276 pub fn fg_rgb(r: u8, g: u8, b: u8) -> String {
278 format!("\x1b[38;2;{};{};{}m", r, g, b)
279 }
280
281 pub fn bg_256(n: u8) -> String {
283 format!("\x1b[48;5;{}m", n)
284 }
285
286 pub fn bg_rgb(r: u8, g: u8, b: u8) -> String {
288 format!("\x1b[48;2;{};{};{}m", r, g, b)
289 }
290}
291
292#[derive(Clone, Debug, Copy)]
294pub enum Horizontal {
295 Left,
296 Centre,
297 Right,
298}
299
300#[derive(Clone, Debug, Copy)]
302pub enum Vertical {
303 Top,
304 Centre,
305 Bottom,
306}
307
308fn display_width(input: &str) -> usize {
310 let mut chars = input.chars().peekable();
311 let mut width = 0;
312
313 while let Some(c) = chars.next() {
314 if c == '\x1b' {
315 if let Some('[') = chars.peek().copied() {
317 chars.next(); let mut found_final = false;
321
322 while let Some(next) = chars.next() {
323 if ('@'..='~').contains(&next) {
324 found_final = true;
325 break;
326 }
327 }
328
329 if !found_final {
330 eprintln!("Warning: unterminated ANSI escape sequence");
331 }
332
333 continue;
334 } else {
335 eprintln!("Warning: unsupported ANSI escape sequence");
336 continue;
337 }
338 }
339
340 match UnicodeWidthChar::width(c) {
342 Some(w) => width += w,
343 None => {
344 let double = ('\u{1F300}'..='\u{1FAFF}').contains(&c);
346 width += if double { 2 } else { 1 };
347 }
348 }
349 }
350
351 width
352}
353
354impl Row {
356 fn new<I, S>(input: I) -> Self
358 where
359 I: IntoIterator<Item = S>,
360 S: Into<String>,
361 {
362 let cells = input.into_iter().map(Into::into).collect();
363
364 Self { cells: cells }
365 }
366}
367
368impl<'a> Table<'a> {
370 pub fn new() -> Self {
372 Self {
373 rows: Vec::new(),
374 invert_header: true,
375 style: &styles::GLYPHS_ROUNDED,
376 border_attr: Attributes::new(),
377 table_attr: None,
378 col_alt_attr: Default::default(),
379 col_attr: HashMap::new(),
380 row_alt_attr: Default::default(),
381 row_attr: HashMap::new(),
382 cell_attr: HashMap::new(),
383 table_align: (Horizontal::Left, Vertical::Top),
384 col_align: HashMap::new(),
385 row_align: HashMap::new(),
386 cell_align: HashMap::new(),
387 }
388 }
389
390 pub fn set_table_style(mut self, style: &'a Style) -> Self {
392 self.style = style;
393 self
394 }
395
396 pub fn set_invert_header(mut self, value: bool) -> Self {
398 self.invert_header = value;
399 self
400 }
401
402 pub fn set_border_attr(mut self, attr: Attributes) -> Self {
404 self.border_attr = attr;
405 self
406 }
407
408 pub fn set_table_attr(mut self, attr: Attributes) -> Self {
410 self.table_attr = Some(attr);
411 self
412 }
413
414 pub fn set_column_alt_attr(mut self, even: Option<Attributes>, odd: Option<Attributes>) -> Self {
416 self.col_alt_attr = [even, odd];
417 self
418 }
419
420 pub fn set_column_attr(mut self, index: usize, attr: Attributes) -> Self {
422 self.col_attr.insert(index, attr);
423 self
424 }
425
426 pub fn set_row_alt_attr(mut self, even: Option<Attributes>, odd: Option<Attributes>) -> Self {
428 self.row_alt_attr = [even, odd];
429 self
430 }
431
432 pub fn set_row_attr(mut self, index: usize, attr: Attributes) -> Self {
434 self.row_attr.insert(index, attr);
435 self
436 }
437
438 pub fn set_cell_attr(mut self, row: usize, col: usize, attr: Attributes) -> Self {
440 self.cell_attr.insert((row, col), attr);
441 self
442 }
443
444 fn resolve_attr(&self, row: usize, col: usize) -> Option<&Attributes> {
447 self.cell_attr
448 .get(&(row, col))
449 .or_else(|| self.row_attr.get(&row))
450 .or_else(|| {
451 if self.invert_header && row == 0 {
452 None
453 } else {
454 let offset = self.invert_header as usize;
455 self.row_alt_attr[(row + offset) % 2].as_ref()
456 }
457 })
458 .or_else(|| self.col_attr.get(&col))
459 .or_else(|| self.col_alt_attr[col % 2].as_ref())
460 .or(self.table_attr.as_ref())
461 }
462
463 pub fn set_table_align(mut self, horizontal: Horizontal, vertical: Vertical) -> Self {
465 self.table_align = (horizontal, vertical);
466 self
467 }
468
469 pub fn set_col_align(mut self, col: usize, h: Option<Horizontal>, v: Option<Vertical>) -> Self {
471 self.col_align.insert(col, (h, v));
472 self
473 }
474
475 pub fn set_row_align(mut self, row: usize, h: Option<Horizontal>, v: Option<Vertical>) -> Self {
477 self.row_align.insert(row, (h, v));
478 self
479 }
480
481 pub fn set_cell_align(mut self, r: usize, c: usize, h: Option<Horizontal>, v: Option<Vertical>) -> Self {
483 self.cell_align.insert((r, c), (h, v));
484 self
485 }
486
487 fn resolve_align(&self, row: usize, col: usize) -> (Horizontal, Vertical) {
489 let mut h = None;
490 let mut v = None;
491
492 if let Some((ch, cv)) = self.cell_align.get(&(row, col)) {
493 h = *ch;
494 v = *cv;
495 }
496
497 if h.is_none() || v.is_none() {
498 if let Some((rh, rv)) = self.row_align.get(&row) {
499 h = h.or(*rh);
500 v = v.or(*rv);
501 }
502 }
503
504 if h.is_none() || v.is_none() {
505 if let Some((ch, cv)) = self.col_align.get(&col) {
506 h = h.or(*ch);
507 v = v.or(*cv);
508 }
509 }
510
511 (h.unwrap_or(self.table_align.0), v.unwrap_or(self.table_align.1))
512 }
513
514 pub fn add_rows<I, R, S>(mut self, input: I) -> Self
516 where
517 I: IntoIterator<Item = R>,
518 R: IntoIterator<Item = S>,
519 S: Into<String>,
520 {
521 self.rows.extend(input.into_iter().map(Row::new));
522 self
523 }
524
525 pub fn add_row<I, S>(mut self, input: I) -> Self
527 where
528 I: IntoIterator<Item = S>,
529 S: Into<String>,
530 {
531 self.rows.push(Row::new(input));
532 self
533 }
534
535 pub fn push_row<I, S>(&mut self, input: I)
537 where
538 I: IntoIterator<Item = S>,
539 S: Into<String>,
540 {
541 self.rows.push(Row::new(input));
542 }
543}
544
545impl Attributes {
546 pub fn new() -> Self {
548 Self { on: "".to_string(), off: "".to_string() }
549 }
550
551 pub fn with(value: &str) -> Self {
553 Self { on: value.to_string(), off: ansi::RESET.to_string() }
554 }
555
556 pub fn as_styled<I, S>(input: I) -> Self
558 where
559 I: IntoIterator<Item = S>,
560 S: Into<String>,
561 {
562 Self { on: input.into_iter().map(|s| s.into()).collect::<String>(), off: ansi::RESET.to_string() }
563 }
564}
565
566#[macro_export]
568macro_rules! attr {
569 ( $( $x:expr ),* $(,)? ) => {
570 Attributes::as_styled([ $( $x ),* ])
571 };
572}
573
574#[macro_export]
576macro_rules! styles {
577 ( $( $x:ident ),* ) => {
578 [ $( (stringify!($x), styles::$x) ),* ]
579 };
580}
581
582impl LayoutLine {
583 fn new(line: &str) -> Self {
585 let width = display_width(&line);
586 Self { line: line.to_string(), width: width }
587 }
588}
589
590impl LayoutCell {
591 pub fn new(line: &str) -> Self {
596 let mut max_width = 0;
597
598 let lines: Vec<LayoutLine> = line
599 .lines()
600 .map(|l| {
601 let layout_line = LayoutLine::new(&l);
602 max_width = max(layout_line.width, max_width);
603 layout_line
604 })
605 .collect();
606
607 Self { lines: lines, width: max_width }
608 }
609}
610
611impl LayoutRow {
612 pub fn new(row: &Row) -> Self {
615 let mut max_height = 0;
616
617 let cells: Vec<LayoutCell> = row
618 .cells
619 .iter()
620 .map(|c| {
621 let cell = LayoutCell::new(&c);
622 max_height = max(cell.lines.len(), max_height);
623 cell
624 })
625 .collect();
626
627 Self { cells: cells, height: max_height }
628 }
629
630 pub fn offsets(&self, widths: &Vec<usize>, full_width: usize) -> Vec<usize> {
632 let mut result = Vec::<usize>::new();
633 let mut total = 0;
634 let count = self.cells.len();
635 let decrease = if count == widths.len() { 0 } else { 1 };
636 for index in 0..count - decrease {
637 total += widths[index];
638 result.push(total);
639 }
640 result.push(full_width);
641 result
642 }
643}
644
645impl<'a> LayoutTable<'a> {
646 fn new(table: &'a Table<'a>) -> Self {
652 let mut max_cols = 0;
653 let mut min_cols = usize::MAX;
654
655 let rows: Vec<LayoutRow> = table
657 .rows
658 .iter()
659 .map(|r| {
660 let row = LayoutRow::new(&r);
661 max_cols = max(max_cols, row.cells.len());
662 min_cols = min(min_cols, row.cells.len());
663 row
664 })
665 .collect();
666
667 let mut widths = Vec::with_capacity(max_cols);
669 widths.resize(max_cols, 0);
670
671 let mut map = Vec::with_capacity(max_cols);
673 map.resize(max_cols, 0);
674
675 for row in &rows {
677 if row.cells.len() == max_cols {
678 for cell in 0..row.cells.len() {
679 widths[cell] = max(widths[cell], row.cells[cell].width);
680 }
681 } else if row.cells.len() > 0 {
682 for cell in 0..row.cells.len() - 1 {
683 widths[cell] = max(widths[cell], row.cells[cell].width);
684 }
685 let index = row.cells.len() - 1;
686 map[index] = max(map[index], row.cells[index].width);
687 }
688 }
689
690 let mut total = widths.iter().sum();
692 for index in 0..map.len() {
693 let span = map[index];
694 if span != 0 {
695 let width: usize = widths[0..index].iter().sum();
696 let required = width + span;
697 if required > total {
698 let delta = required - total;
699 let cols = max_cols - index;
700 let extra_per_col = delta / cols;
701 let remainder = delta % cols;
702 for i in index..max_cols {
703 widths[i] += extra_per_col;
704 }
705 for i in index..index + remainder {
706 widths[i] += 1;
707 }
708 total = required;
709 }
710 }
711 }
712
713 Self { config: table, rows: rows, widths: widths }
714 }
715
716 fn offsets(&self, index: usize, widths: &Vec<usize>, full_width: usize) -> Vec<usize> {
718 self.rows.get(index).map(|r| r.offsets(widths, full_width)).unwrap_or_else(|| vec![full_width])
719 }
720}
721
722impl fmt::Display for Table<'_> {
723 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
727 let table = LayoutTable::new(self);
728 if table.rows.len() > 0 && table.widths.len() > 0 {
729 let draw =
732 |f: &mut Formatter<'_>, style: &[&str; 6], cols: usize, ante: &Vec<usize>, post: &Vec<usize>| -> fmt::Result {
733 if style[0].len() + style[1].len() + style[2].len() + style[3].len() > 0 {
734 let mut off = 0;
735 let mut ain = 0;
736 let mut pin = 0;
737 let mut cols = cols;
738 let end = *ante.last().unwrap();
739 write!(f, "{}{}", self.border_attr.on, style[StyleIdx::Left as usize])?;
740 while off < end {
741 let width;
742 let glyph;
743 if ante[ain] == post[pin] {
744 width = post[pin] - off;
745 glyph = style[StyleIdx::Inter as usize];
746 ain += 1;
747 pin += 1;
748 } else if ante[ain] < post[pin] {
749 width = ante[ain] - off;
750 glyph = style[StyleIdx::Bot as usize];
751 ain += 1;
752 } else {
753 width = post[pin] - off;
754 glyph = style[StyleIdx::Top as usize];
755 pin += 1;
756 }
757
758 off += width;
759 cols -= 1;
760
761 if off < end {
762 write!(f, "{}", style[StyleIdx::Pad as usize].repeat(width))?;
763 write!(f, "{}", glyph)?;
764 } else {
765 let width = width + cols * display_width(glyph);
766 write!(f, "{}", style[StyleIdx::Pad as usize].repeat(width))?;
767 }
768 }
769 writeln!(f, "{}{}", style[StyleIdx::Right as usize], self.border_attr.off)?;
770 }
771 Ok(())
772 };
773
774 let invert_on = |row: usize| -> &str {
776 let active = row == 0 && table.config.invert_header;
777 if active { ansi::INVERT } else { "" }
778 };
779
780 let invert_off = |row: usize| -> &str {
781 let active = row == 0 && table.config.invert_header;
782 if active { ansi::INVERT_OFF } else { "" }
783 };
784
785 let attr_on =
787 |row: usize, col: usize| -> &str { table.config.resolve_attr(row, col).map_or("", |attr| attr.on.as_str()) };
788
789 let attr_off =
791 |row: usize, col: usize| -> &str { table.config.resolve_attr(row, col).map_or("", |attr| attr.off.as_str()) };
792
793 let full_width: usize = table.widths.iter().sum();
795
796 let mut style = &table.config.style.top;
798 let csw = display_width(table.config.style.row[StyleIdx::Inter as usize]);
799
800 let mut ante = vec![full_width];
802 let mut post = table.offsets(0, &table.widths, full_width);
803
804 let on = &table.config.border_attr.on;
806 let off = &table.config.border_attr.off;
807
808 for (index, row) in table.rows.iter().enumerate() {
810 draw(f, style, table.widths.len(), &ante, &post)?;
811 style = &table.config.style.sep;
812 for i in 0..row.height {
813 let mut remaining = full_width;
814 write!(f, "{}{}{}", on, table.config.style.row[StyleIdx::Left as usize], off)?;
815 for (col, cell) in row.cells.iter().enumerate() {
816 let align = table.config.resolve_align(index, col);
818 let horizontal = align.0;
819 let vertical = align.1;
820
821 let oob = cell.lines.len();
823 let offset = match vertical {
824 Vertical::Top => 0,
825 Vertical::Centre => row.height / 2 - oob / 2,
826 Vertical::Bottom => row.height - oob,
827 };
828 let i = (i >= offset).then(|| i - offset).unwrap_or(oob);
829
830 let line: (&str, usize) =
832 if i < cell.lines.len() { (&cell.lines[i].line, cell.lines[i].width) } else { ("", 0) };
833
834 let line_width = line.1;
836 let is_last = col == row.cells.len() - 1;
837 let span = table.widths.len() - row.cells.len();
838 let is_spanning = span > 0 && is_last;
839
840 let padding = if !is_spanning {
841 let pad = table.widths[col] - line_width;
842 remaining -= line_width + pad;
843 pad
844 } else {
845 remaining - line_width + csw * span
846 };
847
848 let left = padding / 2;
850 let lr = match horizontal {
851 Horizontal::Left => (0, padding),
852 Horizontal::Centre => (left, padding - left),
853 Horizontal::Right => (padding, 0),
854 };
855
856 write!(f, "{}", invert_on(index))?;
858 write!(f, "{}", attr_on(index, col))?;
859 write!(f, "{:width$}", "", width = lr.0)?;
860 write!(f, "{}", line.0)?;
861 write!(f, "{:width$}", "", width = lr.1)?;
862 write!(f, "{}", attr_off(index, col))?;
863 write!(f, "{}", invert_off(index))?;
864
865 if col < row.cells.len() - 1 {
867 write!(f, "{}{}{}", on, table.config.style.row[StyleIdx::Inter as usize], off)?;
868 } else {
869 writeln!(f, "{}{}{}", on, table.config.style.row[StyleIdx::Right as usize], off)?;
870 }
871 }
872 }
873
874 ante = post;
876 post = table.offsets(index + 1, &table.widths, full_width);
877 }
878
879 draw(f, &table.config.style.bot, table.widths.len(), &ante, &post)?;
881 }
882 Ok(())
883 }
884}