Repository: Nukesor/comfy-table
Pull request #108 (2023-05-26 14:03:56)

In-cell ansi style
Follow-up to #93


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 33134bd..814a75a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,17 @@ 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).
 
+## [6.2.0] - unreleased
+
+### Added
+
+- Add support for custom ansi styling inside of cells. This feature is hidden behind the feature flag `custom_styling`. Implemented by [blueforesticarus](https://github.com/blueforesticarus) in [#93](https://github.com/Nukesor/comfy-table/pull/93).
+- Add helper functions `add_row[s]_if`, which filtering of rows by a predicate. Implemented by [Techassi](https://github.com/Techassi) in [#106](https://github.com/Nukesor/comfy-table/pull/106).
+
+### Maintenance
+
+- Bump dependencies
+
 ## [6.1.4] - 2022-12-31
 
 ### Added
diff --git a/Cargo.toml b/Cargo.toml
index 2ca7263..98493c6 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -31,9 +31,15 @@ path = "examples/readme_table_no_tty.rs"
 name = "readme_table"
 path = "examples/readme_table.rs"
 
+[[example]]
+name = "inner_style"
+path = "examples/inner_style.rs"
+required-features = ["custom_styling"]
+
 [features]
 default = ["tty"]
 tty = ["crossterm"]
+custom_styling = ["console"]
 # This flag is for library debugging only!
 debug = []
 # This feature is used to expose internal functionality for integration testing.
@@ -44,6 +50,7 @@ crossterm = { version = "0.26", optional = true, default-features = false }
 strum = "0.24"
 strum_macros = "0.24"
 unicode-width = "0.1"
+console = { version = "0.15.1", optional = true }
 
 [dev-dependencies]
 criterion = "0.4"
diff --git a/examples/inner_style.rs b/examples/inner_style.rs
new file mode 100644
index 0000000..4b89e21
--- /dev/null
+++ b/examples/inner_style.rs
@@ -0,0 +1,36 @@
+use comfy_table::{Cell, ContentArrangement, Row, Table};
+
+fn main() {
+    let mut table = Table::new();
+    //table.load_preset(comfy_table::presets::NOTHING);
+    table.set_content_arrangement(ContentArrangement::Dynamic);
+    table.set_width(85);
+
+    let mut row = Row::new();
+    row.add_cell(Cell::new(format!(
+        "List of devices:\n{}",
+        console::style("Blockdevices\nCryptdevices").dim().blue()
+    )));
+    row.add_cell(Cell::new(""));
+
+    table.add_row(row);
+
+    let mut row = Row::new();
+    row.add_cell(Cell::new(format!(
+        "Block devices: \n/dev/{}\n/dev/{}",
+        console::style("sda1").bold().red(),
+        console::style("sda2").bold().red()
+    )));
+    row.add_cell(Cell::new("These are some block devices that were found."));
+    table.add_row(row);
+
+    let mut row = Row::new();
+    row.add_cell(Cell::new(format!(
+        "Crypt devices: \n/dev/mapper/{}",
+        console::style("cryptroot").bold().yellow()
+    )));
+    row.add_cell(Cell::new("This one seems to be encrypted."));
+    table.add_row(row);
+
+    println!("{}", table);
+}
diff --git a/src/cell.rs b/src/cell.rs
index e29731f..ac2b494 100644
--- a/src/cell.rs
+++ b/src/cell.rs
@@ -26,12 +26,16 @@ impl Cell {
     /// Create a new Cell
     #[allow(clippy::needless_pass_by_value)]
     pub fn new<T: ToString>(content: T) -> Self {
+        let content = content.to_string();
+        #[cfg_attr(not(feature = "custom_styling"), allow(unused_mut))]
+        let mut split_content: Vec<String> = content.split('\n').map(ToString::to_string).collect();
+
+        // Correct ansi codes so style is terminated and resumed around the split
+        #[cfg(feature = "custom_styling")]
+        crate::utils::formatting::content_split::fix_style_in_split_str(&mut split_content);
+
         Self {
-            content: content
-                .to_string()
-                .split('\n')
-                .map(ToString::to_string)
-                .collect(),
+            content: split_content,
             delimiter: None,
             alignment: None,
             #[cfg(feature = "tty")]
diff --git a/src/row.rs b/src/row.rs
index 8dd30d7..0611d0c 100644
--- a/src/row.rs
+++ b/src/row.rs
@@ -1,8 +1,9 @@
 use std::slice::Iter;
 
-use unicode_width::UnicodeWidthStr;
-
-use crate::cell::{Cell, Cells};
+use crate::{
+    cell::{Cell, Cells},
+    utils::formatting::content_split::measure_text_width,
+};
 
 /// Each row contains [Cells](crate::Cell) and can be added to a [Table](crate::Table).
 #[derive(Clone, Debug, Default)]
@@ -63,7 +64,7 @@ impl Row {
                 // Each entry represents the longest string width for a cell.
                 cell.content
                     .iter()
-                    .map(|string| string.width())
+                    .map(|string| measure_text_width(string))
                     .max()
                     .unwrap_or(0)
             })
diff --git a/src/utils/formatting/content_format.rs b/src/utils/formatting/content_format.rs
index 5988fe7..fbf35e0 100644
--- a/src/utils/formatting/content_format.rs
+++ b/src/utils/formatting/content_format.rs
@@ -2,7 +2,9 @@
 use crossterm::style::{style, Stylize};
 use unicode_width::UnicodeWidthStr;
 
+use super::content_split::measure_text_width;
 use super::content_split::split_line;
+
 use crate::cell::Cell;
 use crate::row::Row;
 use crate::style::CellAlignment;
@@ -87,7 +89,7 @@ pub fn format_row(
         // Iterate over each line and split it into multiple lines, if necessary.
         // Newlines added by the user will be preserved.
         for line in cell.content.iter() {
-            if line.width() > info.content_width.into() {
+            if measure_text_width(line) > info.content_width.into() {
                 let mut splitted = split_line(line, info, delimiter);
                 cell_lines.append(&mut splitted);
             } else {
@@ -106,6 +108,13 @@ pub fn format_row(
                     .get_mut(lines - 1)
                     .expect("We know it's this long.");
 
+                // we are truncate the line, so we might cuttoff a ansi code
+                #[cfg(feature = "custom_styling")]
+                {
+                    let stripped = console::strip_ansi_codes(last_line).to_string();
+                    *last_line = stripped;
+                }
+
                 // Don't do anything if the collumn is smaller then 6 characters
                 let width: usize = info.content_width.into();
                 if width >= 6 {
@@ -177,7 +186,7 @@ pub fn format_row(
 #[allow(unused_variables)]
 fn align_line(table: &Table, info: &ColumnDisplayInfo, cell: &Cell, mut line: String) -> String {
     let content_width = info.content_width;
-    let remaining: usize = usize::from(content_width).saturating_sub(line.width());
+    let remaining: usize = usize::from(content_width).saturating_sub(measure_text_width(&line));
 
     // Apply the styling before aligning the line, if the user requests it.
     // That way non-delimiter whitespaces won't have stuff like underlines.
diff --git a/src/utils/formatting/content_split/custom_styling.rs b/src/utils/formatting/content_split/custom_styling.rs
new file mode 100644
index 0000000..2dfbae2
--- /dev/null
+++ b/src/utils/formatting/content_split/custom_styling.rs
@@ -0,0 +1,178 @@
+use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
+
+const ANSI_RESET: &str = "\u{1b}[0m";
+
+/// Returns printed length of string, takes into account escape codes
+#[inline(always)]
+pub fn measure_text_width(s: &str) -> usize {
+    console::measure_text_width(s)
+}
+
+/// Split the line by the given deliminator without breaking ansi codes that contain the delimiter
+pub fn split_line_by_delimiter(line: &str, delimiter: char) -> Vec<String> {
+    let mut lines: Vec<String> = Vec::new();
+    let mut current_line = String::default();
+
+    // Iterate over line, spliting text with delimiter
+    let iter = console::AnsiCodeIterator::new(line);
+    for (str_slice, is_esc) in iter {
+        if is_esc {
+            current_line.push_str(str_slice);
+        } else {
+            let mut split = str_slice.split(delimiter);
+
+            // Text before first delimiter (if any) belongs to previous line
+            let first = split
+                .next()
+                .expect("split always produces at least one value");
+            current_line.push_str(first);
+
+            // Text after each delimiter goes to new line.
+            for text in split {
+                lines.push(current_line);
+                current_line = text.to_string();
+            }
+        }
+    }
+    lines.push(current_line);
+    fix_style_in_split_str(lines.as_mut());
+    lines
+}
+
+/// Splits a long word at a given character width. Inserting the needed ansi codes to preserve style.
+pub fn split_long_word(allowed_width: usize, word: &str) -> (String, String) {
+    // A buffer for the first half of the split str, which will take up at most `allowed_len` characters when printed to the terminal.
+    let mut head = String::with_capacity(word.len());
+    // A buffer for the second half of the split str
+    let mut tail = String::with_capacity(word.len());
+    // Tracks the len() of head
+    let mut head_len = 0;
+    // Tracks the len() of head, sans trailing ansi escape codes
+    let mut head_len_last = 0;
+    // Count of *non-trailing* escape codes in the buffer.
+    let mut escape_count_last = 0;
+    // A buffer for the escape codes that exist in the str before the split.
+    let mut escapes = Vec::new();
+
+    // Iterate over segments of the input string, each segment is either a singe escape code or block of text containing no escape codes.
+    // Add text and escape codes to the head buffer, keeping track of printable length and what ansi codes are active, untill there is no more room in allowed_width.
+    // If the str was split at a point with active escape-codes, add the ansi reset code to the end of head, and the list of active escape codes to the beginning of tail.
+    let mut iter = console::AnsiCodeIterator::new(word);
+    for (str_slice, is_esc) in iter.by_ref() {
+        if is_esc {
+            escapes.push(str_slice);
+            // If the code is reset, that means all current codes in the buffer can be ignored.
+            if str_slice == ANSI_RESET {
+                escapes.clear();
+            }
+        }
+
+        let slice_len = match is_esc {
+            true => 0,
+            false => str_slice.width(),
+        };
+
+        if head_len + slice_len <= allowed_width {
+            head.push_str(str_slice);
+            head_len += slice_len;
+
+            if !is_esc {
+                // allows popping unneeded escape codes later
+                head_len_last = head.len();
+                escape_count_last = escapes.len();
+            }
+        } else {
+            assert!(!is_esc);
+            let mut char_iter = str_slice.chars().peekable();
+            while let Some(c) = char_iter.peek() {
+                let character_width = c.width().unwrap_or(0);
+                if allowed_width < head_len + character_width {
+                    break;
+                }
+
+                head_len += character_width;
+                let c = char_iter.next().unwrap();
+                head.push(c);
+
+                // c is not escape code
+                head_len_last = head.len();
+                escape_count_last = escapes.len();
+            }
+
+            // cut off dangling escape codes since they should have no effect
+            head.truncate(head_len_last);
+            if escape_count_last != 0 {
+                head.push_str(ANSI_RESET);
+            }
+
+            for esc in escapes {
+                tail.push_str(esc);
+            }
+            let remaining: String = char_iter.collect();
+            tail.push_str(&remaining);
+            break;
+        }
+    }
+
+    iter.for_each(|s| tail.push_str(s.0));
+    (head, tail)
+}
+
+/// Fixes ansi escape codes in a split string
+/// 1. Adds reset code to the end of each substring if needed.
+/// 2. Keeps track of previous substring's escape codes and inserts them in later substrings to continue style
+pub fn fix_style_in_split_str(words: &mut [String]) {
+    let mut escapes: Vec<String> = Vec::new();
+
+    for word in words {
+        // before we modify the escape list, make a copy
+        let prepend = if !escapes.is_empty() {
+            Some(escapes.join(""))
+        } else {
+            None
+        };
+
+        // add escapes in word to escape list
+        let iter = console::AnsiCodeIterator::new(word)
+            .filter(|(_, is_esc)| *is_esc)
+            .map(|v| v.0);
+        for esc in iter {
+            if esc == ANSI_RESET {
+                escapes.clear()
+            } else {
+                escapes.push(esc.to_string())
+            }
+        }
+
+        // insert previous esc sequences at the beginning of the segment
+        if let Some(prepend) = prepend {
+            word.insert_str(0, &prepend);
+        }
+
+        // if there are active escape sequences, we need to append reset
+        if !escapes.is_empty() {
+            word.push_str(ANSI_RESET);
+        }
+    }
+}
+
+#[cfg(test)]
+mod test {
+    #[test]
+    fn ansi_aware_split_test() {
+        use super::split_line_by_delimiter;
+
+        let text = "\u{1b}[1m head [ middle [ tail \u{1b}[0m[ after";
+        let split = split_line_by_delimiter(text, '[');
+
+        assert_eq!(
+            split,
+            [
+                "\u{1b}[1m head \u{1b}[0m",
+                "\u{1b}[1m middle \u{1b}[0m",
+                "\u{1b}[1m tail \u{1b}[0m",
+                " after"
+            ]
+        )
+    }
+}
diff --git a/src/utils/formatting/content_split.rs b/src/utils/formatting/content_split/mod.rs
similarity index 76%
rename from src/utils/formatting/content_split.rs
rename to src/utils/formatting/content_split/mod.rs
index 0e57982..4c654d8 100644
--- a/src/utils/formatting/content_split.rs
+++ b/src/utils/formatting/content_split/mod.rs
@@ -1,7 +1,15 @@
-use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
-
 use crate::utils::ColumnDisplayInfo;
 
+#[cfg(feature = "custom_styling")]
+mod custom_styling;
+#[cfg(not(feature = "custom_styling"))]
+mod normal;
+
+#[cfg(feature = "custom_styling")]
+pub use custom_styling::*;
+#[cfg(not(feature = "custom_styling"))]
+pub use normal::*;
+
 /// Split a line if it's longer than the allowed columns (width - padding).
 ///
 /// This function tries to do this in a smart way, by splitting the content
@@ -17,19 +25,17 @@ pub fn split_line(line: &str, info: &ColumnDisplayInfo, delimiter: char) -> Vec<
     let content_width = usize::from(info.content_width);
 
     // Split the line by the given deliminator and turn the content into a stack.
-    // Reverse it, since we want to push/pop without reversing the text.
     // Also clone it and convert it into a Vec<String>. Otherwise we get some burrowing problems
     // due to early drops of borrowed values that need to be inserted into `Vec<&str>`
-    let mut elements = line
-        .split(delimiter)
-        .map(ToString::to_string)
-        .collect::<Vec<String>>();
+    let mut elements = split_line_by_delimiter(line, delimiter);
+
+    // Reverse it, since we want to push/pop without reversing the text.
     elements.reverse();
 
     let mut current_line = String::new();
     while let Some(next) = elements.pop() {
-        let current_length = current_line.width();
-        let next_length = next.width();
+        let current_length = measure_text_width(&current_line);
+        let next_length = measure_text_width(&next);
 
         // Some helper variables
         // The length of the current line when combining it with the next element
@@ -39,7 +45,7 @@ pub fn split_line(line: &str, info: &ColumnDisplayInfo, delimiter: char) -> Vec<
             added_length += 1;
         }
         // The remaining width for this column. If we are on a non-empty line, subtract 1 for the delimiter.
-        let mut remaining_width = content_width - current_line.width();
+        let mut remaining_width = content_width - current_length;
         if !current_line.is_empty() {
             remaining_width = remaining_width.saturating_sub(1);
         }
@@ -148,45 +154,10 @@ const MIN_FREE_CHARS: usize = 2;
 /// Otherwise, we simply return the current line and basically don't do anything.
 fn check_if_full(lines: &mut Vec<String>, content_width: usize, current_line: String) -> String {
     // Already complete the current line, if there isn't space for more than two chars
-    if current_line.width() > content_width.saturating_sub(MIN_FREE_CHARS) {
+    if measure_text_width(&current_line) > content_width.saturating_sub(MIN_FREE_CHARS) {
         lines.push(current_line);
         return String::new();
     }
 
     current_line
 }
-
-/// Splits a long word at a given character width.
-/// This needs some special logic, as we have to take multi-character UTF-8 symbols into account.
-/// When simply splitting at a certain char position, we might end up with a string that's has a
-/// wider display width than allowed.
-fn split_long_word(allowed_width: usize, word: &str) -> (String, String) {
-    let mut current_width = 0;
-    let mut splitted = String::new();
-
-    let mut char_iter = word.chars().peekable();
-    // Check if the string might be too long, one character at a time.
-    // Peek into the next char and check the exit condition.
-    // That is, pushing the next character would result in the string being too long.
-    while let Some(c) = char_iter.peek() {
-        if (current_width + c.width().unwrap_or(1)) > allowed_width {
-            break;
-        }
-
-        // We can unwrap, as we just checked that a suitable character is next in line.
-        let c = char_iter.next().unwrap();
-
-        // We default to 1 char, if the character length cannot be determined.
-        // The user has to live with this, if they decide to add control characters or some fancy
-        // stuff into their tables. This is considered undefined behavior and we try to handle this
-        // to the best of our capabilities.
-        let character_width = c.width().unwrap_or(1);
-
-        current_width += character_width;
-        splitted.push(c);
-    }
-
-    // Collect the remaining characters.
-    let remaining = char_iter.collect();
-    (splitted, remaining)
-}
diff --git a/src/utils/formatting/content_split/normal.rs b/src/utils/formatting/content_split/normal.rs
new file mode 100644
index 0000000..a030b60
--- /dev/null
+++ b/src/utils/formatting/content_split/normal.rs
@@ -0,0 +1,50 @@
+use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
+
+/// returns printed length of string
+/// if ansi feature enabled, takes into account escape codes
+#[inline(always)]
+pub fn measure_text_width(s: &str) -> usize {
+    s.width()
+}
+
+/// Split a line into its individual parts along the given delimiter.
+pub fn split_line_by_delimiter(line: &str, delimiter: char) -> Vec<String> {
+    line.split(delimiter)
+        .map(ToString::to_string)
+        .collect::<Vec<String>>()
+}
+
+/// Splits a long word at a given character width.
+/// This needs some special logic, as we have to take multi-character UTF-8 symbols into account.
+/// When simply splitting at a certain char position, we might end up with a string that's has a
+/// wider display width than allowed.
+pub fn split_long_word(allowed_width: usize, word: &str) -> (String, String) {
+    let mut current_width = 0;
+    let mut splitted = String::new();
+
+    let mut char_iter = word.chars().peekable();
+    // Check if the string might be too long, one character at a time.
+    // Peek into the next char and check the exit condition.
+    // That is, pushing the next character would result in the string being too long.
+    while let Some(c) = char_iter.peek() {
+        if (current_width + c.width().unwrap_or(1)) > allowed_width {
+            break;
+        }
+
+        // We can unwrap, as we just checked that a suitable character is next in line.
+        let c = char_iter.next().unwrap();
+
+        // We default to 1 char, if the character length cannot be determined.
+        // The user has to live with this, if they decide to add control characters or some fancy
+        // stuff into their tables. This is considered undefined behavior and we try to handle this
+        // to the best of our capabilities.
+        let character_width = c.width().unwrap_or(1);
+
+        current_width += character_width;
+        splitted.push(c);
+    }
+
+    // Collect the remaining characters.
+    let remaining = char_iter.collect();
+    (splitted, remaining)
+}


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index a73256e..faf755f 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -60,10 +60,18 @@ jobs:
         run: cargo build --target=${{ matrix.target }}
         if: ${{ !matrix.minimal_setup }}
 
+      - name: cargo test
+        run: cargo test --target=${{ matrix.target }} --tests --no-default-features
+        if: ${{ !matrix.minimal_setup }}
+
       - name: cargo test
         run: cargo test --target=${{ matrix.target }} --features=integration_test
         if: ${{ !matrix.minimal_setup }}
 
+      - name: cargo test with custom_styling
+        run: cargo test --target=${{ matrix.target }} --features=integration_test,custom_styling
+        if: ${{ !matrix.minimal_setup }}
+
       - name: cargo build without default features and without dev dependencies
         run: cargo build --release --target=${{ matrix.target }} --no-default-features
         if: ${{ matrix.minimal_setup }}
diff --git a/tests/all/inner_style_test.rs b/tests/all/inner_style_test.rs
new file mode 100644
index 0000000..9b757c6
--- /dev/null
+++ b/tests/all/inner_style_test.rs
@@ -0,0 +1,98 @@
+use comfy_table::{presets::UTF8_FULL, *};
+use pretty_assertions::assert_eq;
+
+fn get_preset_table() -> Table {
+    let mut table = Table::new();
+    table.load_preset(UTF8_FULL);
+    table.set_content_arrangement(ContentArrangement::Dynamic);
+    table.set_width(85);
+
+    let mut row = Row::new();
+    row.add_cell(Cell::new(format!(
+        "hello{}cell1",
+        console::style("123\n456").dim().blue()
+    )));
+    row.add_cell(Cell::new("cell2"));
+
+    table.add_row(row);
+
+    let mut row = Row::new();
+    row.add_cell(Cell::new(
+        format!(r"cell sys-devices-pci00:00-0000:000:07:00.1-usb2-2\x2d1-2\x2d1.3-2\x2d1.3:1.0-host2-target2:0:0-2:0:0:1-block-sdb{}", console::style(".device").bold().red())
+    ));
+    row.add_cell(Cell::new(
+        "cell4 asdfasfsad asdfasdf sad fas df asdf as df asdf    asdfasdfasdfasdfasdfasdfa dsfa sdf asdf asd f asdf as df sadf asd fas df "
+    ));
+    table.add_row(row);
+
+    let mut row = Row::new();
+    row.add_cell(Cell::new("cell5"));
+    row.add_cell(Cell::new("cell6"));
+    table.add_row(row);
+
+    table
+}
+
+#[test]
+fn styled_table() {
+    console::set_colors_enabled(true);
+    let mut table = get_preset_table();
+    table.force_no_tty().enforce_styling();
+    println!("{table}");
+    let expected = "
+┌─────────────────────────────────────────┬─────────────────────────────────────────┐
+│ hello\u{1b}[34m\u{1b}[2m123\u{1b}[0m                                ┆ cell2                                   │
+│ \u{1b}[34m\u{1b}[2m456\u{1b}[0mcell1                                ┆                                         │
+├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
+│ cell sys-devices-pci00:00-0000:000:07:0 ┆ cell4 asdfasfsad asdfasdf sad fas df    │
+│ 0.1-usb2-2\\x2d1-2\\x2d1.3-2\\x2d1.3:1.0-h ┆ asdf as df asdf                         │
+│ ost2-target2:0:0-2:0:0:1-block-sdb\u{1b}[31m\u{1b}[1m.devi\u{1b}[0m ┆ asdfasdfasdfasdfasdfasdfa dsfa sdf asdf │
+│ \u{1b}[31m\u{1b}[1mce\u{1b}[0m                                      ┆ asd f asdf as df sadf asd fas df        │
+├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
+│ cell5                                   ┆ cell6                                   │
+└─────────────────────────────────────────┴─────────────────────────────────────────┘";
+    assert_eq!("\n".to_string() + &table.to_string(), expected);
+}
+
+#[test]
+fn no_style_styled_table() {
+    console::set_colors_enabled(true);
+    let mut table = get_preset_table();
+    table.force_no_tty();
+
+    println!("{table}");
+    let expected = "
+┌─────────────────────────────────────────┬─────────────────────────────────────────┐
+│ hello\u{1b}[34m\u{1b}[2m123\u{1b}[0m                                ┆ cell2                                   │
+│ \u{1b}[34m\u{1b}[2m456\u{1b}[0mcell1                                ┆                                         │
+├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
+│ cell sys-devices-pci00:00-0000:000:07:0 ┆ cell4 asdfasfsad asdfasdf sad fas df    │
+│ 0.1-usb2-2\\x2d1-2\\x2d1.3-2\\x2d1.3:1.0-h ┆ asdf as df asdf                         │
+│ ost2-target2:0:0-2:0:0:1-block-sdb\u{1b}[31m\u{1b}[1m.devi\u{1b}[0m ┆ asdfasdfasdfasdfasdfasdfa dsfa sdf asdf │
+│ \u{1b}[31m\u{1b}[1mce\u{1b}[0m                                      ┆ asd f asdf as df sadf asd fas df        │
+├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
+│ cell5                                   ┆ cell6                                   │
+└─────────────────────────────────────────┴─────────────────────────────────────────┘";
+    assert_eq!("\n".to_string() + &table.to_string(), expected);
+}
+
+#[test]
+fn styled_text_only_table() {
+    console::set_colors_enabled(true);
+    let mut table = get_preset_table();
+    table.force_no_tty().enforce_styling().style_text_only();
+    println!("{table}");
+    let expected = "
+┌─────────────────────────────────────────┬─────────────────────────────────────────┐
+│ hello\u{1b}[34m\u{1b}[2m123\u{1b}[0m                                ┆ cell2                                   │
+│ \u{1b}[34m\u{1b}[2m456\u{1b}[0mcell1                                ┆                                         │
+├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
+│ cell sys-devices-pci00:00-0000:000:07:0 ┆ cell4 asdfasfsad asdfasdf sad fas df    │
+│ 0.1-usb2-2\\x2d1-2\\x2d1.3-2\\x2d1.3:1.0-h ┆ asdf as df asdf                         │
+│ ost2-target2:0:0-2:0:0:1-block-sdb\u{1b}[31m\u{1b}[1m.devi\u{1b}[0m ┆ asdfasdfasdfasdfasdfasdfa dsfa sdf asdf │
+│ \u{1b}[31m\u{1b}[1mce\u{1b}[0m                                      ┆ asd f asdf as df sadf asd fas df        │
+├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
+│ cell5                                   ┆ cell6                                   │
+└─────────────────────────────────────────┴─────────────────────────────────────────┘";
+    assert_eq!("\n".to_string() + &table.to_string(), expected);
+}
diff --git a/tests/all/mod.rs b/tests/all/mod.rs
index 8f2a8fc..53055d8 100644
--- a/tests/all/mod.rs
+++ b/tests/all/mod.rs
@@ -9,6 +9,8 @@ mod constraints_test;
 mod content_arrangement_test;
 mod custom_delimiter_test;
 mod hidden_test;
+#[cfg(feature = "custom_styling")]
+mod inner_style_test;
 mod modifiers_test;
 mod padding_test;
 mod presets_test;