Repository: Nukesor/comfy-table (rust)

Issue:
Dynamic truncate for rows
The idea is to set a fixed row-height and everything below this height will be truncated.

Original issue from Pueue:
https://github.com/Nukesor/pueue/issues/123

Pull request:
Add option for max-height to rows
Resolves #12 


Patch:
diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
index 8411568..28cab06 100644
--- a/.github/workflows/coverage.yml
+++ b/.github/workflows/coverage.yml
@@ -19,7 +19,7 @@ jobs:
 
     - run: cargo build
     - run: cargo install cargo-tarpaulin
-    - run: cargo tarpaulin --out Xml
+    - run: cargo tarpaulin --run-types Tests,Doctests --out Xml
 
     - uses: codecov/codecov-action@v1
       with:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3740d23..1cdad21 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file.
 
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## [1.2.0] - 
+
+### Added
+
+- Add the option to set a max-height on rows. Long content will be truncated.
+
 ## [1.1.1] - 2020-08-23
 
 ### Changed
diff --git a/Cargo.toml b/Cargo.toml
index 23a2d6a..2a1cdba 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -15,7 +15,7 @@ edition = "2018"
 maintenance = { status = "actively-developed" }
 
 [dependencies]
-crossterm = "^0.17"
+crossterm = "^0.18"
 strum = "^0.19"
 strum_macros = "^0.19"
 
diff --git a/src/row.rs b/src/row.rs
index 02d045b..fa356db 100644
--- a/src/row.rs
+++ b/src/row.rs
@@ -8,6 +8,7 @@ pub struct Row {
     /// Index of the row. This will be set as soon as the row is added to the table
     pub(crate) index: Option<usize>,
     pub(crate) cells: Vec<Cell>,
+    pub(crate) max_height: Option<usize>,
 }
 
 impl Default for Row {
@@ -21,6 +22,7 @@ impl Row {
         Row {
             index: None,
             cells: Vec::new(),
+            max_height: None,
         }
     }
 
@@ -39,6 +41,7 @@ impl Row {
         Row {
             index: None,
             cells: cells.to_cells(),
+            max_height: None,
         }
     }
 
@@ -55,6 +58,19 @@ impl Row {
         self
     }
 
+    /// Truncate content of cells which occupies more than X lines of space.
+    /// ```
+    /// use comfy_table::{Row, Cell};
+    ///
+    /// let mut row = Row::new();
+    /// row.max_height(5);
+    /// ```
+    pub fn max_height(&mut self, lines: usize) -> &mut Self {
+        self.max_height = Some(lines);
+
+        self
+    }
+
     /// Get the longest content width for all cells of this row
     pub(crate) fn max_content_widths(&self) -> Vec<usize> {
         // Iterate over all cells
diff --git a/src/table.rs b/src/table.rs
index 87a71e8..18ff199 100644
--- a/src/table.rs
+++ b/src/table.rs
@@ -143,7 +143,7 @@ impl Table {
     }
 
     /// In case you are sure you don't want export tables to a tty
-    /// or you experience problems with tty checking code, you can
+    /// or you experience problems with tty specific code, you can
     /// enforce a non_tty mode.
     ///
     /// This disables:
@@ -160,7 +160,7 @@ impl Table {
     }
 
     /// Returns whether the table will be handled as if it's printed to a tty.
-    /// This function respects the [Table::force_no_tty] and [Table::force_tty] functions.
+    /// This function respects the [Table::force_no_tty] functions.
     /// Otherwise we try to determine, if we are on a tty.
     pub fn is_tty(&self) -> bool {
         if self.no_tty {
@@ -205,7 +205,6 @@ impl Table {
     ///     .set_constraints(vec![
     ///         ColumnConstraint::MaxWidth(15),
     ///         ColumnConstraint::MinWidth(20),
-    ///         ColumnConstraint::Width(40),
     /// ]);
     /// ```
     pub fn set_constraints<T: IntoIterator<Item = ColumnConstraint>>(
@@ -415,6 +414,37 @@ impl Table {
         self.columns.iter_mut()
     }
 
+    /// Reference to a specific row
+    pub fn get_row(&self, index: usize) -> Option<&Row> {
+        self.rows.get(index)
+    }
+
+    /// Mutable reference to a specific row
+    pub fn get_row_mut(&mut self, index: usize) -> Option<&mut Row> {
+        self.rows.get_mut(index)
+    }
+
+    /// Iterator over all rows
+    pub fn row_iter(&mut self) -> Iter<Row> {
+        self.rows.iter()
+    }
+
+    /// Mutable iterator over all rows
+    /// ```
+    /// use comfy_table::Table;
+    /// let mut table = Table::new();
+    /// table.add_row(&vec!["First", "Second", "Third"]);
+    ///
+    /// // Add the constraints to their respective row
+    /// for row in table.row_iter_mut() {
+    ///     row.max_height(5);
+    /// }
+    /// assert!(table.row_iter_mut().len() == 1);
+    /// ```
+    pub fn row_iter_mut(&mut self) -> IterMut<Row> {
+        self.rows.iter_mut()
+    }
+
     /// Return a vector representing the maximum amount of characters in any line of this column. \
     /// This is mostly needed for internal testing and formatting, but can be interesting
     /// if you want to see the widths of the longest lines for each column.
diff --git a/src/utils/arrangement.rs b/src/utils/arrangement.rs
index a826742..43a2919 100644
--- a/src/utils/arrangement.rs
+++ b/src/utils/arrangement.rs
@@ -39,7 +39,7 @@ impl ColumnDisplayInfo {
             cell_alignment: column.cell_alignment,
         }
     }
-    pub fn padding_width(&self) -> u16 {
+    fn padding_width(&self) -> u16 {
         self.padding.0 + self.padding.1
     }
 
@@ -47,7 +47,7 @@ impl ColumnDisplayInfo {
         self.content_width
     }
 
-    pub fn set_content_width(&mut self, width: u16) {
+    fn set_content_width(&mut self, width: u16) {
         // Don't allow content widths of 0.
         if width == 0 {
             self.content_width = 1;
@@ -57,7 +57,7 @@ impl ColumnDisplayInfo {
         self.content_width = width;
     }
 
-    pub fn max_width(&self) -> u16 {
+    fn max_width(&self) -> u16 {
         self.max_content_width + self.padding.0 + self.padding.1
     }
 
@@ -79,7 +79,7 @@ impl ColumnDisplayInfo {
 
 /// Determine the width of each column depending on the content of the given table.
 /// The results uses Option<usize>, since users can choose to hide columns.
-pub fn arrange_content(table: &Table) -> Vec<ColumnDisplayInfo> {
+pub(crate) fn arrange_content(table: &Table) -> Vec<ColumnDisplayInfo> {
     let table_width = table.get_table_width();
     let mut display_infos = Vec::new();
     for column in table.columns.iter() {
diff --git a/src/utils/borders.rs b/src/utils/borders.rs
index b0d6085..526b002 100644
--- a/src/utils/borders.rs
+++ b/src/utils/borders.rs
@@ -2,7 +2,7 @@ use crate::style::TableComponent;
 use crate::table::Table;
 use crate::utils::arrangement::ColumnDisplayInfo;
 
-pub fn draw_borders(
+pub(crate) fn draw_borders(
     table: &Table,
     rows: Vec<Vec<Vec<String>>>,
     display_info: &[ColumnDisplayInfo],
diff --git a/src/utils/format.rs b/src/utils/format.rs
index 43695c4..7d51b86 100644
--- a/src/utils/format.rs
+++ b/src/utils/format.rs
@@ -23,8 +23,7 @@ use crate::utils::arrangement::ColumnDisplayInfo;
 ///      tc[1][2][0]      tc[1][2][1]
 /// ```
 ///
-/// The strings for each row will be padded and aligned accordingly
-/// to their respective column.
+/// The strings for each row will be padded and aligned accordingly to their respective column.
 pub fn format_content(table: &Table, display_info: &[ColumnDisplayInfo]) -> Vec<Vec<Vec<String>>> {
     // The content of the whole table
     let mut table_content = Vec::new();
@@ -53,15 +52,15 @@ pub fn format_row(
     for info in display_info.iter() {
         // Each cell is devided into several lines devided by newline
         // Every line that's too long will be split into two/several lines
-        let mut cell_content = Vec::new();
+        let mut cell_lines = Vec::new();
 
         // Check if the row has as many cells as the table has columns
         // If that's not the case, fill the missing cell with empty spaces
         let cell = if let Some(cell) = cell_iter.next() {
             cell
         } else {
-            cell_content.push(" ".repeat(info.width() as usize));
-            temp_row_content.push(cell_content);
+            cell_lines.push(" ".repeat(info.width() as usize));
+            temp_row_content.push(cell_lines);
             continue;
         };
 
@@ -69,18 +68,46 @@ pub fn format_row(
         // Newlines added by the user will be preserved.
         for line in cell.content.iter() {
             if (line.chars().count() as u16) > info.content_width() {
-                let mut splitted = split_line(line.clone(), &info, cell, table);
-                cell_content.append(&mut splitted);
+                let mut splitted = split_line(&line, &info);
+                cell_lines.append(&mut splitted);
             } else {
-                let mut line = align_line(line.clone(), info, cell);
-                if table.should_style() {
-                    line = style_line(line, cell);
+                cell_lines.push(line.into());
+            }
+        }
+
+        // Remove all unneeded lines of this cell, if the row's content should be
+        // truncated and there're too many lines in this cell.
+        // This also inserts a '...' string so users know that there's truncated stuff.
+        if let Some(lines) = row.max_height {
+            if cell_lines.len() > lines {
+                let _ = cell_lines.split_off(lines);
+                // Direct access. We know it's this long.
+                let last_line = cell_lines.get_mut(lines - 1).unwrap();
+                // Don't do anything if the collumn is smaller then 6 characters
+                let width = info.content_width() as usize;
+                if width >= 6 {
+                    // Truncate the line if '...' doesn't fit
+                    if last_line.len() >= width - 3 {
+                        let surplus = (last_line.len() + 3) - width;
+                        last_line.truncate(last_line.len() - surplus);
+                    }
+                    last_line.push_str("...");
                 }
-                cell_content.push(line);
             }
         }
 
-        temp_row_content.push(cell_content);
+        // Iterate over all generated lines of this cell and align them
+        let cell_lines = cell_lines
+            .iter()
+            .map(|line| align_line(line.to_string(), info, cell));
+
+        // Style the cell if necessary.
+        if table.should_style() {
+            let cell_lines = cell_lines.map(|line| style_line(line, cell));
+            temp_row_content.push(cell_lines.collect());
+        } else {
+            temp_row_content.push(cell_lines.collect());
+        }
     }
 
     // Right now, we have a different structure than desired.
@@ -127,12 +154,7 @@ pub fn format_row(
 /// This function tries to do this in a smart way, by taking the content's deliminator
 /// splitting it at these deliminators and reconnecting them until a line is full.
 /// Splitting of parts only occurs if the part doesn't fit in a single line by itself.
-pub fn split_line(
-    line: String,
-    info: &ColumnDisplayInfo,
-    cell: &Cell,
-    table: &Table,
-) -> Vec<String> {
+fn split_line(line: &str, info: &ColumnDisplayInfo) -> Vec<String> {
     let mut lines = Vec::new();
     let content_width = info.content_width();
 
@@ -217,19 +239,6 @@ pub fn split_line(
         lines.push(current_line);
     }
 
-    // Iterate over all generated lines of this cell and align them
-    // If cell styling should be applied, do this here as well.
-    lines = lines
-        .iter()
-        .map(|line| align_line(line.to_string(), info, cell))
-        .map(|line| {
-            if table.should_style() {
-                return style_line(line, cell);
-            }
-            line
-        })
-        .collect();
-
     lines
 }
 
@@ -237,7 +246,7 @@ pub fn split_line(
 /// In every case all lines will be exactly the same character length `info.width - padding long`
 /// This is needed, so we can simply insert it into the border frame later on.
 /// Padding is applied in this function as well.
-pub fn align_line(mut line: String, info: &ColumnDisplayInfo, cell: &Cell) -> String {
+fn align_line(mut line: String, info: &ColumnDisplayInfo, cell: &Cell) -> String {
     let content_width = info.content_width();
     let remaining = content_width - line.chars().count() as u16;
 
@@ -271,7 +280,7 @@ pub fn align_line(mut line: String, info: &ColumnDisplayInfo, cell: &Cell) -> St
 }
 
 /// Apply the column's padding to this line
-pub fn pad_line(line: String, info: &ColumnDisplayInfo) -> String {
+fn pad_line(line: String, info: &ColumnDisplayInfo) -> String {
     let mut padded_line = String::new();
 
     padded_line += &" ".repeat(info.padding.0 as usize);
@@ -281,7 +290,7 @@ pub fn pad_line(line: String, info: &ColumnDisplayInfo) -> String {
     padded_line
 }
 
-pub fn style_line(line: String, cell: &Cell) -> String {
+fn style_line(line: String, cell: &Cell) -> String {
     let mut content = style(line);
 
     // Apply text color


Test patch:
diff --git a/tests/combined_test.rs b/tests/combined_test.rs
index 7dbf8ea..461df5b 100644
--- a/tests/combined_test.rs
+++ b/tests/combined_test.rs
@@ -39,10 +39,10 @@ fn combined_features() {
     println!("{}", table.to_string());
     let expected = "
 ┌─────────────────────┬────────────────────────────┬───────────────────────────┐
-│\u{1b}[1m Header1             \u{1b}[0m┆\u{1b}[38;5;10m Header2                    \u{1b}[0m┆ Header3                   │
+│\u{1b}[1m Header1             \u{1b}[0m┆\u{1b}[38;5;10m Header2                    \u{1b}[39m┆ Header3                   │
 ╞═════════════════════╪════════════════════════════╪═══════════════════════════╡
-│\u{1b}[1m This is a bold text \u{1b}[0m┆\u{1b}[38;5;10m This is a green text       \u{1b}[0m┆\u{1b}[48;5;0m This one has black        \u{1b}[0m│
-│                     ┆                            ┆\u{1b}[48;5;0m background                \u{1b}[0m│
+│\u{1b}[1m This is a bold text \u{1b}[0m┆\u{1b}[38;5;10m This is a green text       \u{1b}[39m┆\u{1b}[48;5;0m This one has black        \u{1b}[49m│
+│                     ┆                            ┆\u{1b}[48;5;0m background                \u{1b}[49m│
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
 │\u{1b}[5m Blinky boi          \u{1b}[0m┆ This table\'s content is    ┆\u{1b}[48;5;0m\u{1b}[38;5;10m\u{1b}[1m\u{1b}[5m COMBINE ALL THE THINGS    \u{1b}[0m│
 │                     ┆ dynamically arranged. The  ┆                           │
diff --git a/tests/content_arrangement_test.rs b/tests/content_arrangement_test.rs
index 7b165b8..5f5da39 100644
--- a/tests/content_arrangement_test.rs
+++ b/tests/content_arrangement_test.rs
@@ -1,6 +1,6 @@
 use pretty_assertions::assert_eq;
 
-use comfy_table::{ContentArrangement, Table};
+use comfy_table::{ColumnConstraint, ContentArrangement, Row, Table};
 
 #[test]
 fn simple_dynamic_table() {
@@ -58,3 +58,55 @@ fn simple_dynamic_table() {
 +--------+-------+------+";
     assert_eq!("\n".to_string() + &table.to_string(), expected);
 }
+
+#[test]
+fn table_with_truncate() {
+    let mut table = Table::new();
+    let mut first_row: Row = Row::from(vec![
+        "This is a very long line with a lot of text",
+        "This is anotherverylongtextwithlongwords text",
+        "smol",
+    ]);
+    first_row.max_height(4);
+
+    let mut second_row = Row::from(vec![
+        "Now let's\nadd a really long line in the middle of the cell \n and add more multi line stuff",
+        "This is another text",
+        "smol",
+    ]);
+    second_row.max_height(4);
+
+    table
+        .set_header(&vec!["Header1", "Header2", "Head"])
+        .set_content_arrangement(ContentArrangement::Dynamic)
+        .set_table_width(35)
+        .add_row(first_row)
+        .add_row(second_row);
+
+    // The first column will be wider than 6 chars.
+    // The second column's content is wider than 6 chars. There should be a '...'.
+    let second_column = table.get_column_mut(1).unwrap();
+    second_column.set_constraint(ColumnConstraint::Width(8));
+
+    // The third column's content is less than 6 chars width. There shouldn't be a '...'.
+    let third_column = table.get_column_mut(2).unwrap();
+    third_column.set_constraint(ColumnConstraint::Width(7));
+
+    println!("{}", table.to_string());
+    let expected = "
++----------------+--------+-------+
+| Header1        | Header | Head  |
+|                | 2      |       |
++=================================+
+| This is a very | This   | smol  |
+| long line with | is ano |       |
+| a lot of text  | therve |       |
+|                | ryl... |       |
+|----------------+--------+-------|
+| Now let's      | This   | smol  |
+| add a really   | is ano |       |
+| long line in   | ther   |       |
+| the middle ... | text   |       |
++----------------+--------+-------+";
+    assert_eq!("\n".to_string() + &table.to_string(), expected);
+}
diff --git a/tests/property_test.rs b/tests/property_test.rs
index 0073324..bc2fdde 100644
--- a/tests/property_test.rs
+++ b/tests/property_test.rs
@@ -32,6 +32,16 @@ fn column_constraint() -> impl Strategy<Value = Option<ColumnConstraint>> {
     ]
 }
 
+fn max_height() -> impl Strategy<Value = Option<usize>> {
+    prop_oneof![
+        Just(None),
+        Just(Some(0)),
+        Just(Some(1)),
+        Just(Some(5)),
+        Just(Some(100))
+    ]
+}
+
 prop_compose! {
     fn dimensions()(columns in 1u16..10u16, rows in 1u16..10u16)
                     -> (u16, u16) {
@@ -72,10 +82,17 @@ fn columns_and_rows() -> impl Strategy<
 prop_compose! {
     fn table()
         (arrangement in content_arrangement(),
+        max_height in max_height(),
         table_width in 0..1000u16,
         (rows, constraints, cell_alignments, column_alignments) in columns_and_rows()) -> Table {
         let mut table = Table::new();
 
+        if let Some(height) = max_height {
+            for row in table.row_iter_mut() {
+                row.max_height(height);
+            }
+        }
+
         let mut cell_alignments = cell_alignments.iter();
         for row in rows.iter() {
             // Convert a vector of Strings to a vector of Cells and
diff --git a/tests/styling_test.rs b/tests/styling_test.rs
index 9bc635c..cb9986a 100644
--- a/tests/styling_test.rs
+++ b/tests/styling_test.rs
@@ -37,13 +37,13 @@ fn styled_table() {
     println!("{}", table.to_string());
     let expected = "
 ┌─────────────────────┬──────────────────────┬───────────────────────────────┐
-│\u{1b}[1m Header1             \u{1b}[0m┆\u{1b}[38;5;10m Header2              \u{1b}[0m┆\u{1b}[48;5;0m Header3                       \u{1b}[0m│
+│\u{1b}[1m Header1             \u{1b}[0m┆\u{1b}[38;5;10m Header2              \u{1b}[39m┆\u{1b}[48;5;0m Header3                       \u{1b}[49m│
 ╞═════════════════════╪══════════════════════╪═══════════════════════════════╡
-│\u{1b}[1m This is a bold text \u{1b}[0m┆\u{1b}[38;5;10m This is a green text \u{1b}[0m┆\u{1b}[48;5;0m This one has black background \u{1b}[0m│
+│\u{1b}[1m This is a bold text \u{1b}[0m┆\u{1b}[38;5;10m This is a green text \u{1b}[39m┆\u{1b}[48;5;0m This one has black background \u{1b}[49m│
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
-│\u{1b}[5m Blinking boiii      \u{1b}[0m┆\u{1b}[38;5;14m Now                  \u{1b}[0m┆\u{1b}[48;5;0m\u{1b}[38;5;10m\u{1b}[1m\u{1b}[5m COMBINE ALL THE THINGS        \u{1b}[0m│
-│                     ┆\u{1b}[38;5;14m add some             \u{1b}[0m┆                               │
-│                     ┆\u{1b}[38;5;14m multi line stuff     \u{1b}[0m┆                               │
+│\u{1b}[5m Blinking boiii      \u{1b}[0m┆\u{1b}[38;5;14m Now                  \u{1b}[39m┆\u{1b}[48;5;0m\u{1b}[38;5;10m\u{1b}[1m\u{1b}[5m COMBINE ALL THE THINGS        \u{1b}[0m│
+│                     ┆\u{1b}[38;5;14m add some             \u{1b}[39m┆                               │
+│                     ┆\u{1b}[38;5;14m multi line stuff     \u{1b}[39m┆                               │
 └─────────────────────┴──────────────────────┴───────────────────────────────┘";
     assert_eq!("\n".to_string() + &table.to_string(), expected);
 }