Repository: zhiburt/tabled (rust)

Issue:
Support psql-like 'expanded display'
psql has an 'expanded display' mode, which converts the format to two columns, with the header in the first column and value in the second, and one row printed at a time:

```
peter@localhost testdb=> \a \t \x
Output format is aligned.
Tuples only is off.
Expanded display is on.
peter@localhost testdb=> SELECT * FROM my_table;
-[ RECORD 1 ]-
first  | 1
second | one
-[ RECORD 2 ]-
first  | 2
second | two
-[ RECORD 3 ]-
first  | 3
second | three
-[ RECORD 4 ]-
first  | 4
second | four
```

(example taken from the bottom of https://www.postgresql.org/docs/current/app-psql.html).

It would be great if something similar could be supported by this crate, as it helps a lot when rows have so many columns that they span multiple lines.

Thanks for the crate by the way, it's working wonderfully!

Pull request description:
Expaned display
closes #20 

Patch:
diff --git a/Cargo.toml b/Cargo.toml
index fcbf6bb..2016b9c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -25,7 +25,7 @@ color = ["papergrid/color", "ansi-cut"]
 
 [dependencies]
 tabled_derive = {path = "./tabled_derive"}
-papergrid = "0.1.22"
+papergrid = "0.1.23"
 ansi-cut = { version = "0.1.1", optional = true }
 
 [dev-dependencies]
diff --git a/examples/expanded_display.rs b/examples/expanded_display.rs
new file mode 100644
index 0000000..973fd7d
--- /dev/null
+++ b/examples/expanded_display.rs
@@ -0,0 +1,49 @@
+//! The example can be run by this command
+//! `cargo run --example expanded_display`
+
+use tabled::{display::ExpandedDisplay, Tabled};
+
+#[derive(Tabled)]
+struct Distribution {
+    name: &'static str,
+    #[field(display_with = "Self::display_based_on")]
+    based_on: Option<&'static str>,
+    is_active: bool,
+    is_cool: bool,
+}
+
+impl Distribution {
+    fn display_based_on(o: &Option<&'static str>) -> String {
+        match o {
+            &Some(s) => s.into(),
+            None => "Independent".into(),
+        }
+    }
+}
+
+fn main() {
+    let data = [
+        Distribution {
+            name: "Manjaro",
+            based_on: Some("Arch"),
+            is_cool: true,
+            is_active: true,
+        },
+        Distribution {
+            name: "Debian",
+            based_on: None,
+            is_cool: true,
+            is_active: true,
+        },
+        Distribution {
+            name: "Debian",
+            based_on: None,
+            is_cool: true,
+            is_active: true,
+        },
+    ];
+
+    let table = ExpandedDisplay::new(&data);
+
+    println!("{}", table);
+}
diff --git a/papergrid/Cargo.toml b/papergrid/Cargo.toml
index 5599054..5f3d879 100644
--- a/papergrid/Cargo.toml
+++ b/papergrid/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "papergrid"
-version = "0.1.22"
+version = "0.1.23"
 authors = ["Maxim Zhiburt <zhiburt@gmail.com>"]
 edition = "2018"
 description = "Papergrid is a library which provide an paper like table printing"
diff --git a/papergrid/src/lib.rs b/papergrid/src/lib.rs
index 93ccdc2..3804e23 100644
--- a/papergrid/src/lib.rs
+++ b/papergrid/src/lib.rs
@@ -719,12 +719,12 @@ fn write_option<D: Display>(f: &mut std::fmt::Formatter<'_>, text: Option<D>) ->
 }
 
 #[cfg(not(feature = "color"))]
-fn string_width(text: &str) -> usize {
+pub fn string_width(text: &str) -> usize {
     real_string_width(text)
 }
 
 #[cfg(feature = "color")]
-fn string_width(text: &str) -> usize {
+pub fn string_width(text: &str) -> usize {
     let b = strip_ansi_escapes::strip(text.as_bytes()).unwrap();
     let s = std::str::from_utf8(&b).unwrap();
     real_string_width(s)
diff --git a/src/display/expanded_display.rs b/src/display/expanded_display.rs
new file mode 100644
index 0000000..c85c964
--- /dev/null
+++ b/src/display/expanded_display.rs
@@ -0,0 +1,232 @@
+use crate::Tabled;
+
+/// ExpandedDisplay display data in a 'expanded display mode' from postgress.
+/// It may be usefull for a large data sets with a lot of fields.
+///
+/// See 'Examples' in https://www.postgresql.org/docs/current/app-psql.html.
+///
+/// It escapes strings to resolve a multi-line ones.
+/// Because of that `colors` may not be rendered.
+pub struct ExpandedDisplay {
+    format_record_splitter: Option<fn(usize) -> String>,
+    format_value: Option<Box<dyn Fn(&str) -> String>>,
+    fields: Vec<String>,
+    records: Vec<Vec<String>>,
+}
+
+impl ExpandedDisplay {
+    /// Creates a new instance of ExpandedDisplay
+    pub fn new<T: Tabled>(iter: impl IntoIterator<Item = T>) -> Self {
+        let data = iter.into_iter().map(|i| i.fields()).collect();
+        let header = T::headers();
+
+        Self {
+            records: data,
+            fields: header,
+            format_record_splitter: None,
+            format_value: None,
+        }
+    }
+
+    /// Sets a line format which will be used to split records.
+    ///
+    /// Default formating is "-[ RECORD {} ]-".
+    ///
+    /// At least one '\n' char will be printed at the end regardless if you set it or not.
+    pub fn header_template(&mut self, f: fn(usize) -> String) -> &mut Self {
+        self.format_record_splitter = Some(f);
+        self
+    }
+
+    /// Sets a value formatter.
+    ///
+    /// This method overrides others formatters like [ExpandedDisplay::truncate] and [ExpandedDisplay::wrap].
+    pub fn formatter(&mut self, f: impl Fn(&str) -> String + 'static) -> &mut Self {
+        self.format_value = Some(Box::new(f));
+        self
+    }
+
+    /// Sets max width of value.
+    /// The rest will be trunceted.
+    pub fn truncate(&mut self, max: usize, tail: impl AsRef<str>) -> &mut Self {
+        let tail = tail.as_ref().to_string();
+        self.format_value = Some(Box::new(move |s| {
+            let mut trucated = truncate(s, max);
+            if trucated.len() < s.len() {
+                trucated.push_str(&tail);
+            }
+
+            trucated
+        }));
+        self
+    }
+
+    /// Sets max width of value,
+    /// when limit is reached next chars will be placed on the next line.
+    pub fn wrap(&mut self, max: usize) -> &mut Self {
+        self.format_value = Some(Box::new(move |s| wrap(s, max)));
+        self
+    }
+}
+
+impl std::fmt::Display for ExpandedDisplay {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let format_value = |value: &String| match &self.format_value {
+            Some(f) => (f)(value),
+            None => value.to_string(),
+        };
+
+        // It's possible that field|header can be a multiline string so
+        // we escape it and trim \" chars.
+        let fields = self
+            .fields
+            .iter()
+            .map(|f| {
+                let escaped = format!("{:?}", f);
+                escaped
+                    .chars()
+                    .skip(1)
+                    .take(escaped.len() - 1 - 1)
+                    .collect::<String>()
+            })
+            .collect::<Vec<_>>();
+
+        let max_field_width = fields
+            .iter()
+            .map(|f| papergrid::string_width(f))
+            .max()
+            .unwrap_or_default();
+
+        let values = self
+            .records
+            .iter()
+            .map(|record| {
+                assert_eq!(record.len(), fields.len());
+
+                record.iter().map(format_value).collect::<Vec<_>>()
+            })
+            .collect::<Vec<_>>();
+
+        let max_values_length = values
+            .iter()
+            .map(|record| {
+                record
+                    .iter()
+                    .map(|v| v.lines().map(|l| papergrid::string_width(l)).max())
+                    .max()
+            })
+            .max()
+            .unwrap_or_default()
+            .unwrap_or_default()
+            .unwrap_or_default();
+
+        for (i, values) in values.into_iter().enumerate() {
+            match self.format_record_splitter {
+                Some(f_header) => {
+                    let header = (f_header)(i);
+                    writeln!(f, "{}", header)?;
+                }
+                None => {
+                    write_header_template(f, i, max_field_width, max_values_length)?;
+                }
+            }
+
+            for (value, field) in values.iter().zip(fields.iter()) {
+                write_record_line(f, field, value, max_field_width)?;
+            }
+        }
+
+        Ok(())
+    }
+}
+
+fn write_header_template(
+    f: &mut std::fmt::Formatter<'_>,
+    index: usize,
+    max_field_width: usize,
+    max_values_length: usize,
+) -> std::fmt::Result {
+    let mut template = format!("-[ RECORD {} ]-", index);
+    let default_template_length = template.len();
+
+    // 3 - is responsible for ' | ' formatting
+    let max_line_width = std::cmp::max(
+        max_field_width + 3 + max_values_length,
+        default_template_length,
+    );
+    let rest_to_print = max_line_width - default_template_length;
+    if rest_to_print > 0 {
+        // + 1 is a space after field name and we get a next pos so its +2
+        if max_field_width + 2 > default_template_length {
+            let part1 = (max_field_width + 1) - default_template_length;
+            let part2 = rest_to_print - part1 - 1;
+
+            template.extend(
+                std::iter::repeat('-')
+                    .take(part1)
+                    .chain(std::iter::once('+'))
+                    .chain(std::iter::repeat('-').take(part2)),
+            );
+        } else {
+            template.extend(std::iter::repeat('-').take(rest_to_print));
+        }
+    }
+
+    writeln!(f, "{}", template)?;
+
+    Ok(())
+}
+
+fn write_record_line(
+    f: &mut std::fmt::Formatter<'_>,
+    field: &str,
+    value: &str,
+    max_field_width: usize,
+) -> std::fmt::Result {
+    if value.is_empty() {
+        writeln!(f, "{:width$} | {}", field, value, width = max_field_width)?;
+        return Ok(());
+    }
+
+    for (i, line) in value.lines().enumerate() {
+        let field = if i == 0 { field } else { "" };
+        writeln!(f, "{:width$} | {}", field, line, width = max_field_width)?;
+    }
+    Ok(())
+}
+
+#[cfg(not(feature = "color"))]
+fn truncate(s: &str, max: usize) -> String {
+    s.chars()
+        .take(max)
+        .collect::<String>()
+        .escape_debug()
+        .to_string()
+}
+
+#[cfg(feature = "color")]
+fn truncate(s: &str, max: usize) -> String {
+    let max = std::cmp::min(s.chars().count(), max);
+    ansi_cut::AnsiCut::cut(&s, 0..max)
+}
+
+#[cfg(not(feature = "color"))]
+fn wrap(s: &str, max: usize) -> String {
+    s.chars()
+        .enumerate()
+        .flat_map(|(i, c)| {
+            if i != 0 && i % max == 0 {
+                Some('\n')
+            } else {
+                None
+            }
+            .into_iter()
+            .chain(std::iter::once(c))
+        })
+        .collect::<String>()
+}
+
+#[cfg(feature = "color")]
+fn wrap(s: &str, max: usize) -> String {
+    ansi_cut::chunks(s, max).join("\n")
+}
diff --git a/src/display/mod.rs b/src/display/mod.rs
new file mode 100644
index 0000000..afef587
--- /dev/null
+++ b/src/display/mod.rs
@@ -0,0 +1,4 @@
+/// A module which contains a different Views for a [crate::Table].
+mod expanded_display;
+
+pub use expanded_display::*;
diff --git a/src/lib.rs b/src/lib.rs
index 9b3a617..dd4f646 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -146,6 +146,7 @@ use std::fmt;
 
 mod alignment;
 mod disable;
+pub mod display;
 mod formating;
 mod indent;
 mod object;


Test patch:
diff --git a/tests/expanded_display_test.rs b/tests/expanded_display_test.rs
new file mode 100644
index 0000000..27fc5fd
--- /dev/null
+++ b/tests/expanded_display_test.rs
@@ -0,0 +1,803 @@
+use tabled::{display::ExpandedDisplay, Tabled};
+
+#[test]
+fn display() {
+    #[derive(Tabled)]
+    struct Linux {
+        id: u8,
+        destribution: &'static str,
+        link: &'static str,
+    }
+
+    let data = vec![
+        Linux {
+            id: 0,
+            destribution: "Fedora",
+            link: "https://getfedora.org/",
+        },
+        Linux {
+            id: 2,
+            destribution: "OpenSUSE",
+            link: "https://www.opensuse.org/",
+        },
+        Linux {
+            id: 3,
+            destribution: "Endeavouros",
+            link: "https://endeavouros.com/",
+        },
+    ];
+
+    let expected = concat!(
+        "-[ RECORD 0 ]---------------------------\n",
+        "id           | 0\n",
+        "destribution | Fedora\n",
+        "link         | https://getfedora.org/\n",
+        "-[ RECORD 1 ]---------------------------\n",
+        "id           | 2\n",
+        "destribution | OpenSUSE\n",
+        "link         | https://www.opensuse.org/\n",
+        "-[ RECORD 2 ]---------------------------\n",
+        "id           | 3\n",
+        "destribution | Endeavouros\n",
+        "link         | https://endeavouros.com/\n",
+    );
+
+    let table = ExpandedDisplay::new(&data).to_string();
+
+    assert_eq!(table, expected);
+}
+
+#[cfg(feature = "color")]
+#[test]
+fn display_colored() {
+    use owo_colors::{AnsiColors, OwoColorize};
+
+    #[derive(Tabled)]
+    struct Linux {
+        id: u8,
+        destribution: &'static str,
+        link: String,
+    }
+
+    let data = vec![
+        Linux {
+            id: 0,
+            destribution: "Fedora",
+            link: "https://getfedora.org/"
+                .red()
+                .on_color(AnsiColors::Blue)
+                .to_string(),
+        },
+        Linux {
+            id: 2,
+            destribution: "OpenSUSE",
+            link: "https://www.opensuse.org/"
+                .green()
+                .on_color(AnsiColors::Black)
+                .to_string(),
+        },
+        Linux {
+            id: 3,
+            destribution: "Endeavouros",
+            link: "https://endeavouros.com/".blue().underline().to_string(),
+        },
+    ];
+
+    let expected = concat!(
+        "-[ RECORD 0 ]---------------------------\n",
+        "id           | 0\n",
+        "destribution | Fedora\n",
+        "link         | \u{1b}[44m\u{1b}[31mhttps://getfedora.org/\u{1b}[0m\u{1b}[0m\n",
+        "-[ RECORD 1 ]---------------------------\n",
+        "id           | 2\n",
+        "destribution | OpenSUSE\nlink         | \u{1b}[40m\u{1b}[32mhttps://www.opensuse.org/\u{1b}[0m\u{1b}[0m\n",
+        "-[ RECORD 2 ]---------------------------\n",
+        "id           | 3\ndestribution | Endeavouros\n",
+        "link         | \u{1b}[4m\u{1b}[34mhttps://endeavouros.com/\u{1b}[0m\u{1b}[0m\n",
+    );
+
+    let table = ExpandedDisplay::new(&data).to_string();
+
+    println!("{}", table);
+
+    assert_eq!(table, expected);
+}
+
+#[test]
+fn display_empty() {
+    struct Type;
+
+    impl Tabled for Type {
+        fn fields(&self) -> Vec<String> {
+            Vec::new()
+        }
+
+        fn headers() -> Vec<String> {
+            Vec::new()
+        }
+    }
+
+    let table = ExpandedDisplay::new(&[Type]).to_string();
+
+    assert_eq!(table, "-[ RECORD 0 ]-\n");
+}
+
+#[test]
+fn display_dynamic_header_template() {
+    {
+        struct Type;
+
+        impl Tabled for Type {
+            fn fields(&self) -> Vec<String> {
+                vec!["He".to_string(), "123".to_string(), "asd".to_string()]
+            }
+
+            fn headers() -> Vec<String> {
+                vec!["1".to_string(), "2".to_string(), "3".to_string()]
+            }
+        }
+
+        let expected = concat!("-[ RECORD 0 ]-\n", "1 | He\n", "2 | 123\n", "3 | asd\n",);
+
+        let table = ExpandedDisplay::new(&[Type]).to_string();
+
+        assert_eq!(table, expected);
+    }
+    {
+        struct Type;
+
+        impl Tabled for Type {
+            fn fields(&self) -> Vec<String> {
+                vec!["He".to_string(), "123".to_string(), "asd".to_string()]
+            }
+
+            fn headers() -> Vec<String> {
+                vec!["11".to_string(), "2222222".to_string(), "3".to_string()]
+            }
+        }
+
+        let expected = concat!(
+            "-[ RECORD 0 ]-\n",
+            "11      | He\n",
+            "2222222 | 123\n",
+            "3       | asd\n",
+        );
+
+        let table = ExpandedDisplay::new(&[Type]).to_string();
+
+        assert_eq!(table, expected);
+    }
+    {
+        struct Type;
+
+        impl Tabled for Type {
+            fn fields(&self) -> Vec<String> {
+                vec!["HeheHehe".to_string(), "123".to_string(), "asd".to_string()]
+            }
+
+            fn headers() -> Vec<String> {
+                vec!["11".to_string(), "2222222".to_string(), "3".to_string()]
+            }
+        }
+
+        let expected = concat!(
+            "-[ RECORD 0 ]-----\n",
+            "11      | HeheHehe\n",
+            "2222222 | 123\n",
+            "3       | asd\n",
+        );
+
+        let table = ExpandedDisplay::new(&[Type]).to_string();
+
+        assert_eq!(table, expected);
+    }
+    {
+        struct Type;
+
+        impl Tabled for Type {
+            fn fields(&self) -> Vec<String> {
+                vec!["He".to_string(), "123".to_string(), "asd".to_string()]
+            }
+
+            fn headers() -> Vec<String> {
+                vec!["11111111111".to_string(), "2".to_string(), "3".to_string()]
+            }
+        }
+
+        let expected = concat!(
+            "-[ RECORD 0 ]----\n",
+            "11111111111 | He\n",
+            "2           | 123\n",
+            "3           | asd\n",
+        );
+
+        let table = ExpandedDisplay::new(&[Type]).to_string();
+
+        assert_eq!(table, expected);
+    }
+    {
+        struct Type;
+
+        impl Tabled for Type {
+            fn fields(&self) -> Vec<String> {
+                vec!["He".to_string(), "123".to_string(), "asd".to_string()]
+            }
+
+            fn headers() -> Vec<String> {
+                vec![
+                    "1111111111111".to_string(),
+                    "2".to_string(),
+                    "3".to_string(),
+                ]
+            }
+        }
+
+        let expected = concat!(
+            "-[ RECORD 0 ]-+----\n",
+            "1111111111111 | He\n",
+            "2             | 123\n",
+            "3             | asd\n",
+        );
+
+        let table = ExpandedDisplay::new(&[Type]).to_string();
+
+        assert_eq!(table, expected);
+    }
+    {
+        struct Type;
+
+        impl Tabled for Type {
+            fn fields(&self) -> Vec<String> {
+                vec!["He".to_string(), "123".to_string(), "asd".to_string()]
+            }
+
+            fn headers() -> Vec<String> {
+                vec![
+                    "11111111111111111111111111111".to_string(),
+                    "2".to_string(),
+                    "3".to_string(),
+                ]
+            }
+        }
+
+        let expected = concat!(
+            "-[ RECORD 0 ]-----------------+----\n",
+            "11111111111111111111111111111 | He\n",
+            "2                             | 123\n",
+            "3                             | asd\n",
+        );
+
+        let table = ExpandedDisplay::new(&[Type]).to_string();
+
+        assert_eq!(table, expected);
+    }
+    {
+        #[derive(Clone)]
+        struct Type;
+
+        impl Tabled for Type {
+            fn fields(&self) -> Vec<String> {
+                vec!["22".to_string()]
+            }
+
+            fn headers() -> Vec<String> {
+                vec!["11111111111".to_string()]
+            }
+        }
+
+        let expected = concat!(
+            "-[ RECORD 0 ]---\n",
+            "11111111111 | 22\n",
+            "-[ RECORD 1 ]---\n",
+            "11111111111 | 22\n",
+            "-[ RECORD 2 ]---\n",
+            "11111111111 | 22\n",
+            "-[ RECORD 3 ]---\n",
+            "11111111111 | 22\n",
+            "-[ RECORD 4 ]---\n",
+            "11111111111 | 22\n",
+            "-[ RECORD 5 ]---\n",
+            "11111111111 | 22\n",
+            "-[ RECORD 6 ]---\n",
+            "11111111111 | 22\n",
+            "-[ RECORD 7 ]---\n",
+            "11111111111 | 22\n",
+            "-[ RECORD 8 ]---\n",
+            "11111111111 | 22\n",
+            "-[ RECORD 9 ]---\n",
+            "11111111111 | 22\n",
+            "-[ RECORD 10 ]--\n",
+            "11111111111 | 22\n",
+        );
+
+        let table = ExpandedDisplay::new(std::iter::repeat(Type).take(11)).to_string();
+
+        assert_eq!(table, expected);
+    }
+}
+
+#[test]
+fn display_multiline_field() {
+    #[allow(dead_code)]
+    struct Linux {
+        id: u8,
+        destribution: &'static str,
+        link: &'static str,
+    }
+
+    impl Tabled for Linux {
+        fn fields(&self) -> Vec<String> {
+            vec![String::new(), String::new(), String::new()]
+        }
+
+        fn headers() -> Vec<String> {
+            vec![
+                "Hello\nWorld".to_string(),
+                "123".to_string(),
+                "asd".to_string(),
+            ]
+        }
+    }
+
+    let data = vec![Linux {
+        id: 0,
+        destribution: "Fedora",
+        link: "https://getfedora.org/",
+    }];
+
+    let expected = concat!(
+        "-[ RECORD 0 ]--\n",
+        "Hello\\nWorld | \n",
+        "123          | \n",
+        "asd          | \n",
+    );
+
+    let table = ExpandedDisplay::new(&data).to_string();
+
+    assert_eq!(table, expected);
+}
+
+#[test]
+fn display_multiline_record_value() {
+    #[allow(dead_code)]
+    struct Linux {
+        id: u8,
+        destribution: &'static str,
+        link: &'static str,
+    }
+
+    impl Tabled for Linux {
+        fn fields(&self) -> Vec<String> {
+            vec![
+                "Hello\nWorld".to_string(),
+                "123".to_string(),
+                "asd".to_string(),
+            ]
+        }
+
+        fn headers() -> Vec<String> {
+            vec!["1".to_string(), "2".to_string(), "3".to_string()]
+        }
+    }
+
+    let data = vec![Linux {
+        id: 0,
+        destribution: "Fedora",
+        link: "https://getfedora.org/",
+    }];
+
+    let expected = concat!(
+        "-[ RECORD 0 ]-\n",
+        "1 | Hello\n",
+        "  | World\n",
+        "2 | 123\n",
+        "3 | asd\n",
+    );
+
+    let table = ExpandedDisplay::new(&data).to_string();
+
+    assert_eq!(table, expected);
+}
+
+#[test]
+fn display_with_header_template() {
+    #[derive(Tabled)]
+    struct Linux {
+        id: u8,
+        destribution: &'static str,
+        link: &'static str,
+    }
+
+    let data = vec![
+        Linux {
+            id: 0,
+            destribution: "Fedora",
+            link: "https://getfedora.org/",
+        },
+        Linux {
+            id: 2,
+            destribution: "OpenSUSE",
+            link: "https://www.opensuse.org/",
+        },
+        Linux {
+            id: 3,
+            destribution: "Endeavouros",
+            link: "https://endeavouros.com/",
+        },
+    ];
+
+    let expected = concat!(
+        "=== Record => 0\n",
+        "id           | 0\n",
+        "destribution | Fedora\n",
+        "link         | https://getfedora.org/\n",
+        "=== Record => 1\n",
+        "id           | 2\n",
+        "destribution | OpenSUSE\n",
+        "link         | https://www.opensuse.org/\n",
+        "=== Record => 2\n",
+        "id           | 3\n",
+        "destribution | Endeavouros\n",
+        "link         | https://endeavouros.com/\n",
+    );
+
+    let table = ExpandedDisplay::new(&data)
+        .header_template(|i| format!("=== Record => {}", i))
+        .to_string();
+
+    assert_eq!(table, expected);
+}
+
+#[test]
+fn display_with_formatter() {
+    #[derive(Tabled)]
+    struct Linux {
+        id: u8,
+        destribution: &'static str,
+        link: &'static str,
+    }
+
+    let data = vec![
+        Linux {
+            id: 0,
+            destribution: "Fed\nora",
+            link: "https://getfedora.org/",
+        },
+        Linux {
+            id: 2,
+            destribution: "OpenSUSE",
+            link: "https://www.opensuse.org/",
+        },
+        Linux {
+            id: 3,
+            destribution: "Endeavouros",
+            link: "https://endeavouros.com/",
+        },
+    ];
+
+    let expected = concat!(
+        "-[ RECORD 0 ]----------------------------\n",
+        "id           | 0!\n",
+        "             | \n",
+        "destribution | Fed\n",
+        "             | ora!\n",
+        "             | \n",
+        "link         | https://getfedora.org/!\n",
+        "             | \n",
+        "-[ RECORD 1 ]----------------------------\n",
+        "id           | 2!\n",
+        "             | \n",
+        "destribution | OpenSUSE!\n",
+        "             | \n",
+        "link         | https://www.opensuse.org/!\n",
+        "             | \n",
+        "-[ RECORD 2 ]----------------------------\n",
+        "id           | 3!\n",
+        "             | \n",
+        "destribution | Endeavouros!\n",
+        "             | \n",
+        "link         | https://endeavouros.com/!\n",
+        "             | \n",
+    );
+
+    let table = ExpandedDisplay::new(&data)
+        .formatter(|s| format!("{}!\n\n", s))
+        .to_string();
+
+    assert_eq!(table, expected);
+}
+
+#[test]
+fn display_with_one_line_formatter() {
+    #[allow(dead_code)]
+    struct Linux {
+        id: u8,
+        destribution: &'static str,
+        link: &'static str,
+    }
+
+    impl Tabled for Linux {
+        fn fields(&self) -> Vec<String> {
+            vec![
+                "Hello\nWorld".to_string(),
+                "123".to_string(),
+                "asd".to_string(),
+            ]
+        }
+
+        fn headers() -> Vec<String> {
+            vec!["1".to_string(), "2".to_string(), "3".to_string()]
+        }
+    }
+
+    let data = vec![Linux {
+        id: 0,
+        destribution: "Fedora",
+        link: "https://getfedora.org/",
+    }];
+
+    let expected = concat!(
+        "-[ RECORD 0 ]---\n",
+        "1 | Hello\\nWorld\n",
+        "2 | 123\n",
+        "3 | asd\n",
+    );
+
+    let table = ExpandedDisplay::new(&data)
+        .formatter(|s| s.escape_debug().to_string())
+        .to_string();
+
+    assert_eq!(table, expected);
+}
+
+#[test]
+fn display_with_truncate() {
+    #[derive(Tabled)]
+    struct Linux {
+        id: u8,
+        destribution: &'static str,
+        link: &'static str,
+    }
+
+    let data = vec![
+        Linux {
+            id: 0,
+            destribution: "Fedora",
+            link: "https://getfedora.org/",
+        },
+        Linux {
+            id: 2,
+            destribution: "OpenSUSE",
+            link: "https://www.opensuse.org/",
+        },
+        Linux {
+            id: 3,
+            destribution: "Endeavouros",
+            link: "https://endeavouros.com/",
+        },
+    ];
+
+    let expected = concat!(
+        "-[ RECORD 0 ]-----\n",
+        "id           | 0\n",
+        "destribution | Fed\n",
+        "link         | htt\n",
+        "-[ RECORD 1 ]-----\n",
+        "id           | 2\n",
+        "destribution | Ope\n",
+        "link         | htt\n",
+        "-[ RECORD 2 ]-----\n",
+        "id           | 3\n",
+        "destribution | End\n",
+        "link         | htt\n",
+    );
+
+    let table = ExpandedDisplay::new(&data).truncate(3, "").to_string();
+
+    assert_eq!(table, expected);
+}
+
+#[test]
+fn display_with_truncate_with_tail() {
+    #[derive(Tabled)]
+    struct Linux {
+        id: u8,
+        destribution: &'static str,
+        link: &'static str,
+    }
+
+    let data = vec![
+        Linux {
+            id: 0,
+            destribution: "Fedora",
+            link: "https://getfedora.org/",
+        },
+        Linux {
+            id: 2,
+            destribution: "OpenSUSE",
+            link: "https://www.opensuse.org/",
+        },
+        Linux {
+            id: 3,
+            destribution: "Endeavouros",
+            link: "https://endeavouros.com/",
+        },
+    ];
+
+    let expected = concat!(
+        "-[ RECORD 0 ]--------\n",
+        "id           | 0\n",
+        "destribution | Fed...\n",
+        "link         | htt...\n",
+        "-[ RECORD 1 ]--------\n",
+        "id           | 2\n",
+        "destribution | Ope...\n",
+        "link         | htt...\n",
+        "-[ RECORD 2 ]--------\n",
+        "id           | 3\n",
+        "destribution | End...\n",
+        "link         | htt...\n",
+    );
+
+    let table = ExpandedDisplay::new(&data).truncate(3, "...").to_string();
+
+    assert_eq!(table, expected);
+}
+
+#[test]
+fn display_with_wrap() {
+    #[derive(Tabled)]
+    struct Linux {
+        id: u8,
+        destribution: &'static str,
+        link: &'static str,
+    }
+
+    let data = vec![
+        Linux {
+            id: 0,
+            destribution: "Fedora",
+            link: "https://getfedora.org/",
+        },
+        Linux {
+            id: 2,
+            destribution: "OpenSUSE",
+            link: "https://www.opensuse.org/",
+        },
+        Linux {
+            id: 3,
+            destribution: "Endeavouros",
+            link: "https://endeavouros.com/",
+        },
+    ];
+
+    let expected = concat!(
+        "-[ RECORD 0 ]-----\n",
+        "id           | 0\n",
+        "destribution | Fed\n",
+        "             | ora\n",
+        "link         | htt\n",
+        "             | ps:\n",
+        "             | //g\n",
+        "             | etf\n",
+        "             | edo\n",
+        "             | ra.\n",
+        "             | org\n",
+        "             | /\n",
+        "-[ RECORD 1 ]-----\n",
+        "id           | 2\n",
+        "destribution | Ope\n",
+        "             | nSU\n",
+        "             | SE\n",
+        "link         | htt\n",
+        "             | ps:\n",
+        "             | //w\n",
+        "             | ww.\n",
+        "             | ope\n",
+        "             | nsu\n",
+        "             | se.\n",
+        "             | org\n",
+        "             | /\n",
+        "-[ RECORD 2 ]-----\n",
+        "id           | 3\n",
+        "destribution | End\n",
+        "             | eav\n",
+        "             | our\n",
+        "             | os\n",
+        "link         | htt\n",
+        "             | ps:\n",
+        "             | //e\n",
+        "             | nde\n",
+        "             | avo\n",
+        "             | uro\n",
+        "             | s.c\n",
+        "             | om/\n",
+    );
+
+    let table = ExpandedDisplay::new(&data).wrap(3).to_string();
+
+    assert_eq!(table, expected);
+}
+
+#[cfg(feature = "color")]
+#[test]
+fn display_with_wrap_colored() {
+    use owo_colors::{AnsiColors, OwoColorize};
+
+    #[derive(Tabled)]
+    struct Linux {
+        id: u8,
+        destribution: &'static str,
+        link: String,
+    }
+
+    let data = vec![
+        Linux {
+            id: 0,
+            destribution: "Fedora",
+            link: "https://getfedora.org/".red().to_string(),
+        },
+        Linux {
+            id: 2,
+            destribution: "OpenSUSE",
+            link: "https://www.opensuse.org/".to_string(),
+        },
+        Linux {
+            id: 3,
+            destribution: "Endeavouros",
+            link: "https://endeavouros.com/"
+                .white()
+                .on_color(AnsiColors::Black)
+                .to_string(),
+        },
+    ];
+
+    let expected = concat!(
+        "-[ RECORD 0 ]-----\n",
+        "id           | 0\n",
+        "destribution | Fed",
+        "\n             | ora\n",
+        "link         | \u{1b}[31mhtt\u{1b}[0m\n",
+        "             | \u{1b}[31mps:\u{1b}[0m\n",
+        "             | \u{1b}[31m//g\u{1b}[0m\n",
+        "             | \u{1b}[31metf\u{1b}[0m\n",
+        "             | \u{1b}[31medo\u{1b}[0m\n",
+        "             | \u{1b}[31mra.\u{1b}[0m\n",
+        "             | \u{1b}[31morg\u{1b}[0m\n",
+        "             | \u{1b}[31m/\u{1b}[0m\n",
+        "-[ RECORD 1 ]-----\n",
+        "id           | 2\n",
+        "destribution | Ope\n",
+        "             | nSU\n",
+        "             | SE\n",
+        "link         | htt\n",
+        "             | ps:\n",
+        "             | //w\n",
+        "             | ww.\n",
+        "             | ope\n",
+        "             | nsu\n",
+        "             | se.\n",
+        "             | org\n",
+        "             | /\n",
+        "-[ RECORD 2 ]-----\n",
+        "id           | 3\n",
+        "destribution | End\n",
+        "             | eav\n",
+        "             | our\n",
+        "             | os\n",
+        "link         | \u{1b}[40m\u{1b}[37mhtt\u{1b}[0m\u{1b}[0m\n",
+        "             | \u{1b}[40m\u{1b}[37mps:\u{1b}[0m\u{1b}[0m\n",
+        "             | \u{1b}[40m\u{1b}[37m//e\u{1b}[0m\u{1b}[0m\n",
+        "             | \u{1b}[40m\u{1b}[37mnde\u{1b}[0m\u{1b}[0m\n",
+        "             | \u{1b}[40m\u{1b}[37mavo\u{1b}[0m\u{1b}[0m\n",
+        "             | \u{1b}[40m\u{1b}[37muro\u{1b}[0m\u{1b}[0m\n",
+        "             | \u{1b}[40m\u{1b}[37ms.c\u{1b}[0m\u{1b}[0m\n",
+        "             | \u{1b}[40m\u{1b}[37mom/\u{1b}[0m\u{1b}[0m\n",
+    );
+
+    let table = ExpandedDisplay::new(&data).wrap(3).to_string();
+
+    println!("{}", table);
+
+    assert_eq!(table, expected);
+}