├── src ├── utils │ ├── formatting │ │ ├── mod.rs │ │ ├── content_split │ │ │ ├── normal.rs │ │ │ ├── custom_styling.rs │ │ │ └── mod.rs │ │ └── borders.rs │ ├── arrangement │ │ ├── disabled.rs │ │ ├── helper.rs │ │ ├── mod.rs │ │ └── constraint.rs │ └── mod.rs ├── style │ ├── cell.rs │ ├── modifiers.rs │ ├── column.rs │ ├── mod.rs │ ├── table.rs │ ├── color.rs │ ├── presets.rs │ └── attribute.rs ├── lib.rs ├── row.rs ├── column.rs └── cell.rs ├── tests ├── all_tests.rs └── all │ ├── edge_cases.rs │ ├── mod.rs │ ├── modifiers_test.rs │ ├── padding_test.rs │ ├── counts.rs │ ├── alignment_test.rs │ ├── custom_delimiter_test.rs │ ├── combined_test.rs │ ├── simple_test.rs │ ├── utf_8_characters.rs │ ├── hidden_test.rs │ ├── styling_test.rs │ ├── inner_style_test.rs │ ├── presets_test.rs │ ├── truncation.rs │ ├── add_predicate.rs │ ├── content_arrangement_test.rs │ ├── constraints_test.rs │ └── property_test.proptest-regressions ├── .github ├── dependabot.yml ├── workflows │ ├── coverage.yml │ ├── lint.yml │ └── test.yml └── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── feature_request.yml ├── .taplo.toml ├── codecov.yml ├── .gitignore ├── FAQ.md ├── Justfile ├── examples ├── readme_table_no_tty.rs ├── inner_style.rs └── readme_table.rs ├── LICENSE ├── benches ├── build_large_table.rs └── build_tables.rs ├── Cargo.toml └── README.md /src/utils/formatting/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod borders; 2 | pub mod content_format; 3 | pub mod content_split; 4 | -------------------------------------------------------------------------------- /tests/all_tests.rs: -------------------------------------------------------------------------------- 1 | /// This module simply imports all tests. 2 | /// That way, they're processed faster, which is nice as proptesting takes quite a while. 3 | mod all; 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | 8 | - package-ecosystem: cargo 9 | directory: "/" 10 | schedule: 11 | interval: weekly 12 | -------------------------------------------------------------------------------- /.taplo.toml: -------------------------------------------------------------------------------- 1 | exclude = ["target"] 2 | 3 | [formatting] 4 | indent_string = " " 5 | reorder_arrays = true 6 | reorder_keys = true 7 | 8 | [[rule]] 9 | include = ["**/Cargo.toml"] 10 | keys = ["package"] 11 | 12 | [rule.formatting] 13 | reorder_keys = false 14 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "benches" 3 | - "examples" 4 | - "proptest_regressions" 5 | - "tests" 6 | - "CHANGELOG.md" 7 | - "Cargo.lock" 8 | - "Cargo.toml" 9 | - "FAQ.md" 10 | - "LICENSE" 11 | - "README.md" 12 | 13 | codecov: 14 | status: 15 | project: 16 | default: 17 | target: auto 18 | threshold: 2% 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | # Benchmark and profiling 13 | flamegraph.svg 14 | perf.data 15 | perf.data.old 16 | 17 | #Added by cargo 18 | /target 19 | -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | ## Why is my styling broken? Why doesn't styling work? 4 | 5 | `comfy-table` only supports styling via the internal styling functions on [Cell](https://docs.rs/comfy-table/5.0.0/comfy_table/struct.Cell.html#method.fg). 6 | 7 | Any styling from other libraries, even crossterm, will most likely not work as expected or break. 8 | 9 | It's impossible for `comfy-table` to know about any ANSI escape sequences it doesn't create itself. 10 | Hence, it's not possible to respect unknown styling, as ANSI styling doesn't work this way and doesn't support this. 11 | 12 | If you come up with a solution to this problem, feel free to create a PR. 13 | -------------------------------------------------------------------------------- /src/style/cell.rs: -------------------------------------------------------------------------------- 1 | /// This can be set on [columns](crate::Column::set_cell_alignment) and [cells](crate::Cell::set_alignment). 2 | /// 3 | /// Determines how content of cells should be aligned. 4 | /// 5 | /// ```text 6 | /// +----------------------+ 7 | /// | Header1 | 8 | /// +======================+ 9 | /// | Left | 10 | /// |----------------------+ 11 | /// | center | 12 | /// |----------------------+ 13 | /// | right | 14 | /// +----------------------+ 15 | /// ``` 16 | #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] 17 | pub enum CellAlignment { 18 | Left, 19 | Right, 20 | Center, 21 | } 22 | -------------------------------------------------------------------------------- /tests/all/edge_cases.rs: -------------------------------------------------------------------------------- 1 | use comfy_table::*; 2 | 3 | /// A gigantic table can generated, even if it's longer than the longest supported width. 4 | #[test] 5 | fn giant_table() { 6 | let mut table = Table::new(); 7 | table.set_header(["a".repeat(1_000_000)]); 8 | table.add_row(["a".repeat(1_000_000)]); 9 | 10 | table.to_string(); 11 | } 12 | 13 | /// No panic, even if there's a ridiculous amount of padding. 14 | #[test] 15 | fn max_padding() { 16 | let mut table = Table::new(); 17 | table.add_row(["test"]); 18 | let column = table.column_mut(0).unwrap(); 19 | 20 | column.set_padding((u16::MAX, u16::MAX)); 21 | 22 | table.to_string(); 23 | } 24 | -------------------------------------------------------------------------------- /Justfile: -------------------------------------------------------------------------------- 1 | # If you change anything in here, make sure to also adjust the lint CI job! 2 | lint: 3 | cargo fmt --all -- --check 4 | taplo format --check 5 | cargo clippy --tests --workspace -- -D warnings 6 | 7 | format: 8 | just ensure-command taplo 9 | cargo fmt 10 | taplo format 11 | 12 | 13 | # Ensures that one or more required commands are installed 14 | ensure-command +command: 15 | #!/usr/bin/env bash 16 | set -euo pipefail 17 | 18 | read -r -a commands <<< "{{ command }}" 19 | 20 | for cmd in "${commands[@]}"; do 21 | if ! command -v "$cmd" > /dev/null 2>&1 ; then 22 | printf "Couldn't find required executable '%s'\n" "$cmd" >&2 23 | exit 1 24 | fi 25 | done 26 | -------------------------------------------------------------------------------- /src/style/modifiers.rs: -------------------------------------------------------------------------------- 1 | /// A modifier, that when applied will convert the outer corners to round corners. 2 | /// ```text 3 | /// ╭───────┬───────╮ 4 | /// │ Hello │ there │ 5 | /// ╞═══════╪═══════╡ 6 | /// │ a ┆ b │ 7 | /// ├╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┤ 8 | /// │ c ┆ d │ 9 | /// ╰───────┴───────╯ 10 | /// ``` 11 | pub const UTF8_ROUND_CORNERS: &str = " ╭╮╰╯"; 12 | 13 | /// A modifier, that when applied will convert the inner borders to solid lines. 14 | /// ```text 15 | /// ╭───────┬───────╮ 16 | /// │ Hello │ there │ 17 | /// ╞═══════╪═══════╡ 18 | /// │ a │ b │ 19 | /// ├───────┼───────┤ 20 | /// │ c │ d │ 21 | /// ╰───────┴───────╯ 22 | /// ``` 23 | pub const UTF8_SOLID_INNER_BORDERS: &str = " │─ "; 24 | -------------------------------------------------------------------------------- /tests/all/mod.rs: -------------------------------------------------------------------------------- 1 | use comfy_table::Table; 2 | use unicode_width::UnicodeWidthStr; 3 | 4 | mod add_predicate; 5 | mod alignment_test; 6 | #[cfg(feature = "tty")] 7 | mod combined_test; 8 | mod constraints_test; 9 | mod content_arrangement_test; 10 | mod counts; 11 | mod custom_delimiter_test; 12 | mod edge_cases; 13 | mod hidden_test; 14 | #[cfg(feature = "custom_styling")] 15 | mod inner_style_test; 16 | mod modifiers_test; 17 | mod padding_test; 18 | mod presets_test; 19 | mod property_test; 20 | mod simple_test; 21 | #[cfg(feature = "tty")] 22 | mod styling_test; 23 | mod truncation; 24 | mod utf_8_characters; 25 | 26 | pub fn assert_table_line_width(table: &Table, count: usize) { 27 | for line in table.lines() { 28 | assert_eq!(line.width(), count); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![forbid(unsafe_code)] 2 | #![doc = include_str!("../README.md")] 3 | // The README code examples should be valid mini scripts to make them properly testable. 4 | #![allow(clippy::needless_doctest_main)] 5 | // Had a few false-positives on v1.81. Check lateron if they're still there. 6 | #![allow(clippy::manual_unwrap_or)] 7 | 8 | mod cell; 9 | mod column; 10 | mod row; 11 | mod style; 12 | mod table; 13 | #[cfg(feature = "_integration_test")] 14 | /// We publicly expose the internal [utils] module for our integration tests. 15 | /// There's some logic we need from inside here. 16 | /// The API inside of this isn't considered stable and shouldnt' be used. 17 | pub mod utils; 18 | #[cfg(not(feature = "_integration_test"))] 19 | mod utils; 20 | 21 | pub use crate::cell::{Cell, Cells}; 22 | pub use crate::column::Column; 23 | pub use crate::row::Row; 24 | pub use crate::table::{ColumnCellIter, Table}; 25 | pub use style::*; 26 | -------------------------------------------------------------------------------- /tests/all/modifiers_test.rs: -------------------------------------------------------------------------------- 1 | use pretty_assertions::assert_eq; 2 | 3 | use comfy_table::modifiers::*; 4 | use comfy_table::presets::*; 5 | use comfy_table::*; 6 | 7 | fn get_preset_table() -> Table { 8 | let mut table = Table::new(); 9 | table 10 | .set_header(vec!["Header1", "Header2", "Header3"]) 11 | .add_row(vec!["One One", "One Two", "One Three"]) 12 | .add_row(vec!["One One", "One Two", "One Three"]); 13 | 14 | table 15 | } 16 | 17 | #[test] 18 | fn utf8_round_corners() { 19 | let mut table = get_preset_table(); 20 | table 21 | .load_preset(UTF8_FULL) 22 | .apply_modifier(UTF8_ROUND_CORNERS); 23 | let expected = " 24 | ╭─────────┬─────────┬───────────╮ 25 | │ Header1 ┆ Header2 ┆ Header3 │ 26 | ╞═════════╪═════════╪═══════════╡ 27 | │ One One ┆ One Two ┆ One Three │ 28 | ├╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤ 29 | │ One One ┆ One Two ┆ One Three │ 30 | ╰─────────┴─────────┴───────────╯"; 31 | 32 | println!("{table}"); 33 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 34 | } 35 | -------------------------------------------------------------------------------- /examples/readme_table_no_tty.rs: -------------------------------------------------------------------------------- 1 | use comfy_table::presets::UTF8_FULL; 2 | use comfy_table::*; 3 | 4 | // This example works even with the `tty` feature disabled 5 | // You can try it out with `cargo run --example no_tty --no-default-features` 6 | 7 | fn main() { 8 | let mut table = Table::new(); 9 | table.load_preset(UTF8_FULL) 10 | .set_content_arrangement(ContentArrangement::Dynamic) 11 | .set_width(80) 12 | .set_header(vec![ 13 | Cell::new("Header1"), 14 | Cell::new("Header2"), 15 | Cell::new("Header3"), 16 | ]) 17 | .add_row(vec![ 18 | Cell::new("No bold text without tty"), 19 | Cell::new("No colored text without tty"), 20 | Cell::new("No custom background without tty"), 21 | ]) 22 | .add_row(vec![ 23 | Cell::new("Blinky boi"), 24 | Cell::new("This table's content is dynamically arranged. The table is exactly 80 characters wide.\nHere comes a reallylongwordthatshoulddynamicallywrap"), 25 | Cell::new("Done"), 26 | ]); 27 | 28 | println!("{table}"); 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Arne Beer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/utils/arrangement/disabled.rs: -------------------------------------------------------------------------------- 1 | use super::constraint; 2 | use super::helper::*; 3 | use super::{ColumnDisplayInfo, DisplayInfos}; 4 | use crate::Table; 5 | 6 | /// Dynamic arrangement is disabled. 7 | /// Apply all non-relative constraints, and set the width of all remaining columns to the 8 | /// respective max content width. 9 | pub fn arrange( 10 | table: &Table, 11 | infos: &mut DisplayInfos, 12 | visible_columns: usize, 13 | max_content_widths: &[u16], 14 | ) { 15 | for column in table.columns.iter() { 16 | if infos.contains_key(&column.index) { 17 | continue; 18 | } 19 | 20 | let mut width = max_content_widths[column.index]; 21 | 22 | // Reduce the width, if a column has longer content than the specified MaxWidth constraint. 23 | if let Some(max_width) = constraint::max(table, &column.constraint, visible_columns) { 24 | if max_width < width { 25 | width = absolute_width_with_padding(column, max_width); 26 | } 27 | } 28 | 29 | let info = ColumnDisplayInfo::new(column, width); 30 | infos.insert(column.index, info); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: Coverage 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: 7 | - '.github/workflows/coverage.yml' 8 | - '**.rs' 9 | - 'Cargo.toml' 10 | - 'Cargo.lock' 11 | pull_request: 12 | branches: [main] 13 | paths: 14 | - '.github/workflows/coverage.yml' 15 | - '**.rs' 16 | - 'Cargo.toml' 17 | - 'Cargo.lock' 18 | 19 | jobs: 20 | coverage: 21 | name: Create coverage statistics 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - name: Checkout code 26 | uses: actions/checkout@v4 27 | 28 | - name: Setup Rust toolchain 29 | uses: dtolnay/rust-toolchain@stable 30 | with: 31 | components: llvm-tools-preview 32 | 33 | - name: Install cargo-llvm-cov 34 | uses: taiki-e/install-action@cargo-llvm-cov 35 | 36 | - name: Generate code coverage 37 | run: cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info 38 | 39 | - name: Upload coverage to Codecov 40 | uses: codecov/codecov-action@v5 41 | with: 42 | files: lcov.info 43 | fail_ci_if_error: false 44 | -------------------------------------------------------------------------------- /examples/inner_style.rs: -------------------------------------------------------------------------------- 1 | use comfy_table::{Cell, ContentArrangement, Row, Table}; 2 | 3 | fn main() { 4 | let mut table = Table::new(); 5 | //table.load_preset(comfy_table::presets::NOTHING); 6 | table.set_content_arrangement(ContentArrangement::Dynamic); 7 | table.set_width(85); 8 | 9 | let mut row = Row::new(); 10 | row.add_cell(Cell::new(format!( 11 | "List of devices:\n{}", 12 | console::style("Blockdevices\nCryptdevices").dim().blue() 13 | ))); 14 | row.add_cell(Cell::new("")); 15 | 16 | table.add_row(row); 17 | 18 | let mut row = Row::new(); 19 | row.add_cell(Cell::new(format!( 20 | "Block devices: \n/dev/{}\n/dev/{}", 21 | console::style("sda1").bold().red(), 22 | console::style("sda2").bold().red() 23 | ))); 24 | row.add_cell(Cell::new("These are some block devices that were found.")); 25 | table.add_row(row); 26 | 27 | let mut row = Row::new(); 28 | row.add_cell(Cell::new(format!( 29 | "Crypt devices: \n/dev/mapper/{}", 30 | console::style("cryptroot").bold().yellow() 31 | ))); 32 | row.add_cell(Cell::new("This one seems to be encrypted.")); 33 | table.add_row(row); 34 | 35 | println!("{}", table); 36 | } 37 | -------------------------------------------------------------------------------- /examples/readme_table.rs: -------------------------------------------------------------------------------- 1 | use comfy_table::presets::UTF8_FULL; 2 | use comfy_table::*; 3 | 4 | fn main() { 5 | let mut table = Table::new(); 6 | table.load_preset(UTF8_FULL) 7 | .set_content_arrangement(ContentArrangement::Dynamic) 8 | .set_width(80) 9 | .set_header(vec![ 10 | Cell::new("Header1").add_attribute(Attribute::Bold), 11 | Cell::new("Header2").fg(Color::Green), 12 | Cell::new("Header3"), 13 | ]) 14 | .add_row(vec![ 15 | Cell::new("This is a bold text").add_attribute(Attribute::Bold), 16 | Cell::new("This is a green text").fg(Color::Green), 17 | Cell::new("This one has black background").bg(Color::Black), 18 | ]) 19 | .add_row(vec![ 20 | Cell::new("Blinky boi").add_attribute(Attribute::SlowBlink), 21 | Cell::new("This table's content is dynamically arranged. The table is exactly 80 characters wide.\nHere comes a reallylongwordthatshoulddynamicallywrap"), 22 | Cell::new("COMBINE ALL THE THINGS") 23 | .fg(Color::Green) 24 | .bg(Color::Black) 25 | .add_attributes(vec![ 26 | Attribute::Bold, 27 | Attribute::SlowBlink, 28 | ]) 29 | ]); 30 | 31 | println!("{table}"); 32 | } 33 | -------------------------------------------------------------------------------- /tests/all/padding_test.rs: -------------------------------------------------------------------------------- 1 | use pretty_assertions::assert_eq; 2 | 3 | use comfy_table::*; 4 | 5 | #[test] 6 | /// Columns can set a custom padding. 7 | /// Ensure these settings are working. 8 | fn custom_padding() { 9 | let mut table = Table::new(); 10 | table 11 | .set_header(vec![ 12 | Cell::new("Header1"), 13 | Cell::new("Header2"), 14 | Cell::new("Header3"), 15 | ]) 16 | .add_row(vec!["One One", "One Two", "One Three"]) 17 | .add_row(vec!["Two One", "Two Two", "Two Three"]) 18 | .add_row(vec!["Three One", "Three Two", "Three Three"]); 19 | 20 | let column = table.column_mut(0).unwrap(); 21 | column.set_padding((5, 5)); 22 | let column = table.column_mut(2).unwrap(); 23 | column.set_padding((0, 0)); 24 | 25 | println!("{table}"); 26 | let expected = " 27 | +-------------------+-----------+-----------+ 28 | | Header1 | Header2 |Header3 | 29 | +===========================================+ 30 | | One One | One Two |One Three | 31 | |-------------------+-----------+-----------| 32 | | Two One | Two Two |Two Three | 33 | |-------------------+-----------+-----------| 34 | | Three One | Three Two |Three Three| 35 | +-------------------+-----------+-----------+"; 36 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 37 | } 38 | -------------------------------------------------------------------------------- /benches/build_large_table.rs: -------------------------------------------------------------------------------- 1 | use criterion::{Criterion, criterion_group, criterion_main}; 2 | 3 | use comfy_table::presets::UTF8_FULL; 4 | use comfy_table::*; 5 | use rand::Rng; 6 | use rand::distr::Alphanumeric; 7 | 8 | /// Create a dynamic 10x500 Table with width 300 and unevenly distributed content. 9 | /// There are no constraint, the content simply has to be formatted to fit as good as possible into 10 | /// the given space. 11 | fn build_huge_table() { 12 | let mut table = Table::new(); 13 | table 14 | .load_preset(UTF8_FULL) 15 | .set_content_arrangement(ContentArrangement::DynamicFullWidth) 16 | .set_width(300) 17 | .set_header(vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); 18 | 19 | let mut rng = rand::rng(); 20 | for _ in 0..500 { 21 | let mut row = Vec::new(); 22 | for _ in 0..10 { 23 | let string_length = rng.random_range(2..100); 24 | let random_string: String = (&mut rng) 25 | .sample_iter(&Alphanumeric) 26 | .take(string_length) 27 | .map(char::from) 28 | .collect(); 29 | row.push(random_string); 30 | } 31 | table.add_row(row); 32 | } 33 | 34 | // Build the table. 35 | let _ = table.lines(); 36 | } 37 | 38 | pub fn build_tables(crit: &mut Criterion) { 39 | crit.bench_function("Huge table", |b| b.iter(build_huge_table)); 40 | } 41 | 42 | criterion_group!(benches, build_tables); 43 | criterion_main!(benches); 44 | -------------------------------------------------------------------------------- /tests/all/counts.rs: -------------------------------------------------------------------------------- 1 | use pretty_assertions::assert_eq; 2 | 3 | use comfy_table::*; 4 | 5 | #[test] 6 | fn test_col_count_header() { 7 | let mut table = Table::new(); 8 | 9 | table.set_header(vec!["Col 1", "Col 2", "Col 3"]); 10 | assert_eq!(table.column_count(), 3); 11 | 12 | table.set_header(vec!["Col 1", "Col 2", "Col 3", "Col 4"]); 13 | assert_eq!(table.column_count(), 4); 14 | 15 | table.set_header(vec!["Col I", "Col II"]); 16 | assert_eq!(table.column_count(), 4); 17 | } 18 | 19 | #[test] 20 | fn test_col_count_row() { 21 | let mut table = Table::new(); 22 | 23 | table.add_row(vec!["Foo", "Bar"]); 24 | assert_eq!(table.column_count(), 2); 25 | 26 | table.add_row(vec!["Bar", "Foo", "Baz"]); 27 | assert_eq!(table.column_count(), 3); 28 | } 29 | 30 | #[test] 31 | fn test_row_count() { 32 | let mut table = Table::new(); 33 | assert_eq!(table.row_count(), 0); 34 | 35 | table.add_row(vec!["Foo", "Bar"]); 36 | assert_eq!(table.row_count(), 1); 37 | 38 | table.add_row(vec!["Bar", "Foo", "Baz"]); 39 | assert_eq!(table.row_count(), 2); 40 | 41 | table.add_row_if(|_, _| false, vec!["Baz", "Bar", "Foo"]); 42 | assert_eq!(table.row_count(), 2); 43 | 44 | table.add_row_if(|_, _| true, vec!["Foo", "Baz", "Bar"]); 45 | assert_eq!(table.row_count(), 3); 46 | } 47 | 48 | #[test] 49 | fn test_is_empty() { 50 | let mut table = Table::new(); 51 | assert_eq!(table.is_empty(), true); 52 | 53 | table.add_row(vec!["Foo", "Bar"]); 54 | assert_eq!(table.is_empty(), false); 55 | } 56 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Code Style 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: 7 | - ".github/workflows/lint.yml" 8 | - "**.rs" 9 | - "Cargo.toml" 10 | - "Cargo.lock" 11 | pull_request: 12 | branches: [main] 13 | paths: 14 | - ".github/workflows/lint.yml" 15 | - "**.rs" 16 | - "Cargo.toml" 17 | - "Cargo.lock" 18 | 19 | jobs: 20 | test: 21 | name: Tests on ${{ matrix.os }} for ${{ matrix.toolchain }} 22 | runs-on: ${{ matrix.os }} 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | os: [ubuntu-latest, macos-latest, windows-latest] 27 | 28 | steps: 29 | - name: Checkout code 30 | uses: actions/checkout@v4 31 | 32 | - name: Setup Rust toolchain 33 | uses: dtolnay/rust-toolchain@stable 34 | with: 35 | components: rustfmt, clippy 36 | 37 | - name: cargo build 38 | run: cargo build 39 | 40 | - name: cargo fmt 41 | run: cargo fmt --all -- --check 42 | 43 | - name: cargo fmt 44 | run: cargo fmt --all -- --check 45 | 46 | - name: cargo clippy 47 | run: cargo clippy --tests -- -D warnings 48 | 49 | - name: cargo clippy without default features 50 | run: cargo clippy --no-default-features --tests -- -D warnings 51 | 52 | # Only run taplo on linux to save some time. 53 | # Also, taplo is broken on windows for some reason. 54 | - name: Install taplo-cli 55 | run: cargo install taplo-cli 56 | if: matrix.os == 'ubuntu-latest' 57 | 58 | - name: Run taplo check 59 | run: ~/.cargo/bin/taplo format --check 60 | if: matrix.os == 'ubuntu-latest' 61 | -------------------------------------------------------------------------------- /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod arrangement; 2 | pub mod formatting; 3 | 4 | use crate::style::{CellAlignment, ColumnConstraint}; 5 | use crate::{Column, Table}; 6 | 7 | use arrangement::arrange_content; 8 | use formatting::borders::draw_borders; 9 | use formatting::content_format::format_content; 10 | 11 | /// This struct is ONLY used when table.to_string() is called. 12 | /// It's purpose is to store intermediate results, information on how to 13 | /// arrange the table and other convenience variables. 14 | /// 15 | /// The idea is to have a place for all this intermediate stuff, without 16 | /// actually touching the Column struct. 17 | #[derive(Debug)] 18 | pub struct ColumnDisplayInfo { 19 | pub padding: (u16, u16), 20 | pub delimiter: Option, 21 | /// The actual allowed content width after arrangement 22 | pub content_width: u16, 23 | /// The content alignment of cells in this column 24 | pub cell_alignment: Option, 25 | is_hidden: bool, 26 | } 27 | 28 | impl ColumnDisplayInfo { 29 | pub fn new(column: &Column, mut content_width: u16) -> Self { 30 | // The min contend width may only be 1 31 | if content_width == 0 { 32 | content_width = 1; 33 | } 34 | Self { 35 | padding: column.padding, 36 | delimiter: column.delimiter, 37 | content_width, 38 | cell_alignment: column.cell_alignment, 39 | is_hidden: matches!(column.constraint, Some(ColumnConstraint::Hidden)), 40 | } 41 | } 42 | 43 | pub fn width(&self) -> u16 { 44 | self.content_width 45 | .saturating_add(self.padding.0) 46 | .saturating_add(self.padding.1) 47 | } 48 | } 49 | 50 | pub fn build_table(table: &Table) -> impl Iterator { 51 | let display_info = arrange_content(table); 52 | let content = format_content(table, &display_info); 53 | draw_borders(table, &content, &display_info).into_iter() 54 | } 55 | -------------------------------------------------------------------------------- /tests/all/alignment_test.rs: -------------------------------------------------------------------------------- 1 | use pretty_assertions::assert_eq; 2 | 3 | use comfy_table::*; 4 | 5 | #[test] 6 | /// Cell alignment can be specified on Columns and Cells 7 | /// Alignment settings on Cells overwrite the settings of Columns 8 | fn cell_alignment() { 9 | let mut table = Table::new(); 10 | table 11 | .set_header(vec!["Header1", "Header2", "Header3"]) 12 | .add_row(vec![ 13 | "Very long line Test", 14 | "Very long line Test", 15 | "Very long line Test", 16 | ]) 17 | .add_row(vec![ 18 | Cell::new("Right").set_alignment(CellAlignment::Right), 19 | Cell::new("Left").set_alignment(CellAlignment::Left), 20 | Cell::new("Center").set_alignment(CellAlignment::Center), 21 | ]) 22 | .add_row(vec!["Left", "Center", "Right"]); 23 | 24 | let alignment = [ 25 | CellAlignment::Left, 26 | CellAlignment::Center, 27 | CellAlignment::Right, 28 | ]; 29 | 30 | // Add the alignment to their respective column 31 | for (column_index, column) in table.column_iter_mut().enumerate() { 32 | let alignment = alignment.get(column_index).unwrap(); 33 | column.set_cell_alignment(*alignment); 34 | } 35 | 36 | println!("{table}"); 37 | let expected = " 38 | +---------------------+---------------------+---------------------+ 39 | | Header1 | Header2 | Header3 | 40 | +=================================================================+ 41 | | Very long line Test | Very long line Test | Very long line Test | 42 | |---------------------+---------------------+---------------------| 43 | | Right | Left | Center | 44 | |---------------------+---------------------+---------------------| 45 | | Left | Center | Right | 46 | +---------------------+---------------------+---------------------+"; 47 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 48 | } 49 | -------------------------------------------------------------------------------- /src/style/column.rs: -------------------------------------------------------------------------------- 1 | /// A Constraint can be added to a [columns](crate::Column). 2 | /// 3 | /// They allow some control over Column widths as well as the dynamic arrangement process. 4 | /// 5 | /// All percental boundaries will be ignored, if: 6 | /// - you aren't using one of ContentArrangement::{Dynamic, DynamicFullWidth} 7 | /// - the width of the table/terminal cannot be determined. 8 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 9 | pub enum ColumnConstraint { 10 | /// This will completely hide a column. 11 | Hidden, 12 | /// Force the column to be as long as it's content. 13 | /// Use with caution! This can easily mess up your table formatting, 14 | /// if a column's content is overly long. 15 | ContentWidth, 16 | /// Enforce a absolute width for a column. 17 | Absolute(Width), 18 | /// Specify a lower boundary, either fixed or as percentage of the total width. 19 | /// A column with this constraint will be at least as wide as specified. 20 | /// If the content isn't as long as that boundary, it will be padded. 21 | /// If the column has longer content and is allowed to grow, the column may take more space. 22 | LowerBoundary(Width), 23 | /// Specify a upper boundary, either fixed or as percentage of the total width. 24 | /// A column with this constraint will be at most as wide as specified. 25 | /// The column may be smaller than that width. 26 | UpperBoundary(Width), 27 | /// Specify both, an upper and a lower boundary. 28 | Boundaries { lower: Width, upper: Width }, 29 | } 30 | 31 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 32 | pub enum Width { 33 | /// A fixed amount of characters. 34 | Fixed(u16), 35 | /// A width equivalent to a certain percentage of the available width. 36 | /// Values above 100 will be automatically reduced to 100. 37 | /// 38 | /// **Warning:** This option will be ignored if: 39 | /// - you aren't using one of ContentArrangement::{Dynamic, DynamicFullWidth} 40 | /// - the width of the table/terminal cannot be determined. 41 | Percentage(u16), 42 | } 43 | -------------------------------------------------------------------------------- /src/utils/formatting/content_split/normal.rs: -------------------------------------------------------------------------------- 1 | use unicode_segmentation::UnicodeSegmentation; 2 | use unicode_width::UnicodeWidthStr; 3 | 4 | /// returns printed length of string 5 | /// if ansi feature enabled, takes into account escape codes 6 | #[inline(always)] 7 | pub fn measure_text_width(s: &str) -> usize { 8 | s.width() 9 | } 10 | 11 | /// Split a line into its individual parts along the given delimiter. 12 | pub fn split_line_by_delimiter(line: &str, delimiter: char) -> Vec { 13 | line.split(delimiter) 14 | .map(ToString::to_string) 15 | .collect::>() 16 | } 17 | 18 | /// Splits a long word at a given character width. 19 | /// This needs some special logic, as we have to take multi-character UTF-8 symbols into account. 20 | /// When simply splitting at a certain char position, we might end up with a string that's has a 21 | /// wider display width than allowed. 22 | pub fn split_long_word(allowed_width: usize, word: &str) -> (String, String) { 23 | let mut current_width = 0; 24 | let mut parts = String::new(); 25 | 26 | let mut graphmes = word.graphemes(true).peekable(); 27 | 28 | // Check if the string might be too long, one Unicode grapheme at a time. 29 | // Peek into the next grapheme and check the exit condition. 30 | // 31 | // This code uses graphemes to handle both zero-width joiner[0] UTF-8 chars, which 32 | // combine multiple UTF-8 chars into a single grapheme, and variant selectors [1], 33 | // which pick a certain variant of the preceding char. 34 | // 35 | // [0]: https://en.wikipedia.org/wiki/Zero-width_joiner 36 | // [1]: https://en.wikipedia.org/wiki/Variation_Selectors_(Unicode_block) 37 | while let Some(c) = graphmes.peek() { 38 | if (current_width + c.width()) > allowed_width { 39 | break; 40 | } 41 | 42 | // We can unwrap, as we just checked that a suitable grapheme is next in line. 43 | let c = graphmes.next().unwrap(); 44 | 45 | let character_width = c.width(); 46 | current_width += character_width; 47 | parts.push_str(c); 48 | } 49 | 50 | // Collect the remaining characters. 51 | let remaining = graphmes.collect(); 52 | (parts, remaining) 53 | } 54 | -------------------------------------------------------------------------------- /src/style/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(all(feature = "tty", not(feature = "reexport_crossterm")))] 2 | mod attribute; 3 | mod cell; 4 | #[cfg(all(feature = "tty", not(feature = "reexport_crossterm")))] 5 | mod color; 6 | mod column; 7 | /// Contains modifiers, that can be used to alter certain parts of a preset.\ 8 | /// For instance, the [UTF8_ROUND_CORNERS](modifiers::UTF8_ROUND_CORNERS) replaces all corners with round UTF8 box corners. 9 | pub mod modifiers; 10 | /// This module provides styling presets for tables.\ 11 | /// Every preset has an example preview. 12 | pub mod presets; 13 | mod table; 14 | 15 | pub use cell::CellAlignment; 16 | pub use column::{ColumnConstraint, Width}; 17 | #[cfg(feature = "tty")] 18 | pub use styling_enums::{Attribute, Color}; 19 | #[cfg(feature = "tty")] 20 | pub(crate) use styling_enums::{map_attribute, map_color}; 21 | pub use table::{ContentArrangement, TableComponent}; 22 | 23 | /// Convenience module to have cleaner and "identical" conditional re-exports for style enums. 24 | #[cfg(all(feature = "tty", not(feature = "reexport_crossterm")))] 25 | mod styling_enums { 26 | pub use super::attribute::*; 27 | pub use super::color::*; 28 | } 29 | 30 | /// Re-export the crossterm type directly instead of using the internal mirrored types. 31 | /// This result in possible ABI incompatibilities when using comfy_table and crossterm in the same 32 | /// project with different versions, but may also be very convenient for developers. 33 | #[cfg(all(feature = "tty", feature = "reexport_crossterm"))] 34 | mod styling_enums { 35 | /// Attributes used for styling cell content. Reexport of crossterm's [Attributes](crossterm::style::Attribute) enum. 36 | pub use crossterm::style::Attribute; 37 | /// Colors used for styling cell content. Reexport of crossterm's [Color](crossterm::style::Color) enum. 38 | pub use crossterm::style::Color; 39 | 40 | /// Convenience function to have the same mapping code for reexported types. 41 | #[inline] 42 | pub(crate) fn map_attribute(attribute: Attribute) -> Attribute { 43 | attribute 44 | } 45 | 46 | /// Convenience function to have the same mapping code for reexported types. 47 | #[inline] 48 | pub(crate) fn map_color(color: Color) -> Color { 49 | color 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 🐛 2 | description: Create a report to help improve the project 3 | labels: ["t: bug"] 4 | title: "[Bug]" 5 | 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | Please take the time to fill out all relevant the fields below. 11 | 12 | - type: textarea 13 | id: description-of-bug 14 | attributes: 15 | label: Describe the bug 16 | description: A clear and concise description of the bug. 17 | placeholder: | 18 | Description goes here :) 19 | validations: 20 | required: true 21 | 22 | - type: textarea 23 | id: steps-to-reproduce 24 | attributes: 25 | label: Steps to reproduce 26 | description: | 27 | Please add a code example on how to trigger the bug. 28 | Or even better, a link to a repository with a minimal reproducible setup to reproduce the bug. 29 | placeholder: | 30 | ``` 31 | use comfiest_table::*; 32 | ``` 33 | The import doesn't work! 34 | validations: 35 | required: true 36 | 37 | - type: textarea 38 | id: debug-output 39 | attributes: 40 | label: Logs (if applicable) 41 | description: | 42 | This is mostly important for crashes or panics. 43 | Logs helps me to debug a problem if the bug is something that's not clearly visible. 44 | placeholder: | 45 | ``` 46 | Some log output here 47 | ``` 48 | validations: 49 | required: false 50 | 51 | - type: input 52 | id: operating-system 53 | attributes: 54 | label: Operating system 55 | description: The operating system you're using. 56 | placeholder: iOS 8 / Windows 10 / Ubuntu 22.04 57 | validations: 58 | required: true 59 | 60 | - type: input 61 | id: version 62 | attributes: 63 | label: Comfy-table version 64 | description: The current version you're using. 65 | placeholder: v6.1.4 66 | validations: 67 | required: true 68 | 69 | - type: textarea 70 | id: additional-context 71 | attributes: 72 | label: Additional context 73 | description: Add any other context about the problem here. 74 | placeholder: | 75 | Anything else you want to add. 76 | validations: 77 | required: false 78 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "comfy-table" 3 | description = "An easy to use library for building beautiful tables with automatic content wrapping" 4 | version = "7.2.1" 5 | authors = ["Arne Beer "] 6 | homepage = "https://github.com/nukesor/comfy-table" 7 | repository = "https://github.com/nukesor/comfy-table" 8 | documentation = "https://docs.rs/comfy-table/" 9 | license = "MIT" 10 | keywords = ["table", "terminal", "unicode"] 11 | readme = "README.md" 12 | edition = "2024" 13 | 14 | [badges] 15 | maintenance = { status = "actively-developed" } 16 | 17 | [[bench]] 18 | harness = false 19 | name = "build_tables" 20 | 21 | [[bench]] 22 | harness = false 23 | name = "build_large_table" 24 | 25 | [[example]] 26 | name = "no_tty" 27 | path = "examples/readme_table_no_tty.rs" 28 | 29 | [[example]] 30 | name = "readme_table" 31 | path = "examples/readme_table.rs" 32 | 33 | [[example]] 34 | name = "inner_style" 35 | path = "examples/inner_style.rs" 36 | required-features = ["custom_styling"] 37 | 38 | [features] 39 | # For more info about these flags, please check the README. 40 | # Everything's explained over there. 41 | custom_styling = ["dep:ansi-str", "dep:console", "tty"] 42 | default = ["tty"] 43 | reexport_crossterm = ["tty"] 44 | tty = ["dep:crossterm"] 45 | # ---- DEVELOPMENT FLAGS ---- 46 | # This flag is for comfy-table development debugging! 47 | # You usually don't need this as a user of the library. 48 | _debug = [] 49 | # This feature is used to for integration testing of comfy_table. 50 | # It exposes normally unexposed internal functionality for easier testing. 51 | # DON'T USE. You opt in for breaking changes, as the internal API might change on minor/patch versions. 52 | _integration_test = [] 53 | 54 | [dependencies] 55 | unicode-segmentation = "1" 56 | unicode-width = "0.2" 57 | 58 | # Optional dependencies 59 | ansi-str = { version = "0.9", optional = true } 60 | console = { version = "0.16", optional = true } 61 | 62 | [dev-dependencies] 63 | criterion = "0.7" 64 | pretty_assertions = "1" 65 | proptest = "1" 66 | rand = "0.9" 67 | rstest = "0.26" 68 | 69 | # We don't need any of the default features for crossterm. 70 | # However, the windows build needs the windows feature enabled. 71 | [target.'cfg(not(windows))'.dependencies] 72 | crossterm = { version = "0.29", optional = true, default-features = false } 73 | [target.'cfg(windows)'.dependencies] 74 | crossterm = { version = "0.29", optional = true, default-features = false, features = [ 75 | "windows", 76 | ] } 77 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest an idea for this project 3 | labels: ["t: feature"] 4 | 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Please take the time to fill out all relevant the fields below. 10 | 11 | - type: textarea 12 | id: feature 13 | attributes: 14 | label: A detailed description of the feature you would like to see added. 15 | description: | 16 | Explain how that feature would look like and how it should behave. 17 | 18 | Also take a look at the [Contributing guide](https://github.com/Nukesor/comfy-table#contributing) to see if your ticket fits into the scope of this project :). 19 | placeholder: | 20 | It would be awesome, if all text would blink by default! 21 | 22 | Since not everybody might want this feature, it could also be hidden behind a feature flag. 23 | validations: 24 | required: true 25 | 26 | - type: textarea 27 | id: user-story 28 | attributes: 29 | label: Explain your usecase of the requested feature 30 | description: | 31 | I need to know what a feature is going to be used for, before I can decide if and how it's going to be implemented. 32 | 33 | The more information you provide, the better I understand your problem ;). 34 | placeholder: | 35 | I have a chronic condition that requires me to have text blinking all the time. 36 | 37 | Without this feature, I tend to get really unproductive. 38 | validations: 39 | required: true 40 | 41 | - type: textarea 42 | id: alternatives 43 | attributes: 44 | label: Alternatives 45 | description: | 46 | If your problem can be solved in multiple ways, I would like to hear the possible alternatives you've considered. 47 | 48 | Some problems really don't have any feasible alternatives, in that case don't bother answering this question :) 49 | placeholder: | 50 | I could add a wrapper around the project which takes any output and wraps it in ANSI escape codes. 51 | However, this is very cumbersome and not user-friendly. 52 | 53 | This is why I think this should be added to the project. 54 | validations: 55 | required: false 56 | 57 | - type: textarea 58 | id: additional-context 59 | attributes: 60 | label: Additional context 61 | description: Add any other context about the problem here. 62 | placeholder: | 63 | Anything else you want to add, such as sketches, example code, etc. 64 | validations: 65 | required: false 66 | -------------------------------------------------------------------------------- /tests/all/custom_delimiter_test.rs: -------------------------------------------------------------------------------- 1 | use pretty_assertions::assert_eq; 2 | 3 | use comfy_table::*; 4 | 5 | #[test] 6 | /// Create a table with a custom delimiter on Table, Column and Cell level. 7 | /// The first column should be split with the table's delimiter. 8 | /// The first cell of the second column should be split with the custom column delimiter 9 | /// The second cell of the second column should be split with the custom cell delimiter 10 | fn full_custom_delimiters() { 11 | let mut table = Table::new(); 12 | 13 | table 14 | .set_header(vec!["Header1", "Header2"]) 15 | .set_content_arrangement(ContentArrangement::Dynamic) 16 | .set_delimiter('-') 17 | .set_width(40) 18 | .add_row(vec![ 19 | "This shouldn't be split with any logic, since there's no matching delimiter", 20 | "Test-Test-Test-Test-Test-This_should_only_be_splitted_by_underscore_and not by space or hyphens", 21 | ]); 22 | 23 | // Give the bottom right cell a special delimiter 24 | table.add_row(vec![ 25 | Cell::new("Test_Test_Test_Test_Test_This-should-only-be-splitted-by-hyphens-not by space or underscore",), 26 | Cell::new( 27 | "Test-Test-Test-Test-Test-Test_Test_Test_Test_Test_Test_Test_This/should/only/be/splitted/by/backspace/and not by space or hyphens or anything else.", 28 | ) 29 | .set_delimiter('/'), 30 | ]); 31 | 32 | let column = table.column_mut(1).unwrap(); 33 | column.set_delimiter('_'); 34 | 35 | println!("{table}"); 36 | let expected = " 37 | +-------------------+------------------+ 38 | | Header1 | Header2 | 39 | +======================================+ 40 | | This shouldn't be | Test-Test-Test-T | 41 | | split with any l | est-Test-This | 42 | | ogic, since there | should_only_be | 43 | | 's no matching de | splitted_by | 44 | | limiter | underscore_and n | 45 | | | ot by space or h | 46 | | | yphens | 47 | |-------------------+------------------| 48 | | Test_Test_Test_Te | Test-Test-Test-T | 49 | | st_Test_This | est-Test-Test_Te | 50 | | should-only-be | st_Test_Test_Tes | 51 | | splitted-by | t_Test_Test_This | 52 | | hyphens-not by sp | should/only/be | 53 | | ace or underscore | splitted/by | 54 | | | backspace/and no | 55 | | | t by space or hy | 56 | | | phens or anythin | 57 | | | g else. | 58 | +-------------------+------------------+"; 59 | println!("{expected}"); 60 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 61 | } 62 | -------------------------------------------------------------------------------- /src/utils/arrangement/helper.rs: -------------------------------------------------------------------------------- 1 | use super::DisplayInfos; 2 | use crate::utils::formatting::borders::{ 3 | should_draw_left_border, should_draw_right_border, should_draw_vertical_lines, 4 | }; 5 | use crate::{Cell, Column, Table}; 6 | 7 | /// The ColumnDisplayInfo works with a fixed value for content width. 8 | /// However, if a column is supposed to get a absolute width, we have to make sure that 9 | /// the padding on top of the content width doesn't get larger than the specified absolute width. 10 | /// 11 | /// For this reason, we take the targeted width, subtract the column's padding and make sure that 12 | /// the content width is always a minimum of 1 13 | pub fn absolute_width_with_padding(column: &Column, width: u16) -> u16 { 14 | let mut content_width = width 15 | .saturating_sub(column.padding.0) 16 | .saturating_sub(column.padding.1); 17 | if content_width == 0 { 18 | content_width = 1; 19 | } 20 | 21 | content_width 22 | } 23 | 24 | /// Return the amount of visible columns 25 | pub fn count_visible_columns(columns: &[Column]) -> usize { 26 | columns.iter().filter(|column| !column.is_hidden()).count() 27 | } 28 | 29 | /// Return the amount of visible columns that haven't been checked yet. 30 | /// 31 | /// - `column_count` is the total amount of columns that are visible, calculated 32 | /// with [count_visible_columns]. 33 | /// - `infos` are all columns that have already been fixed in size or are hidden. 34 | pub fn count_remaining_columns(column_count: usize, infos: &DisplayInfos) -> usize { 35 | column_count - infos.iter().filter(|(_, info)| !info.is_hidden).count() 36 | } 37 | 38 | /// Return the amount of border columns, that will be visible in the final table output. 39 | pub fn count_border_columns(table: &Table, visible_columns: usize) -> usize { 40 | let mut lines = 0; 41 | // Remove space occupied by borders from remaining_width 42 | if should_draw_left_border(table) { 43 | lines += 1; 44 | } 45 | if should_draw_right_border(table) { 46 | lines += 1; 47 | } 48 | if should_draw_vertical_lines(table) { 49 | lines += visible_columns.saturating_sub(1); 50 | } 51 | 52 | lines 53 | } 54 | 55 | /// Get the delimiter for a Cell. 56 | /// Priority is in decreasing order: Cell -> Column -> Table. 57 | pub fn delimiter(table: &Table, column: &Column, cell: &Cell) -> char { 58 | // Determine, which delimiter should be used 59 | if let Some(delimiter) = cell.delimiter { 60 | delimiter 61 | } else if let Some(delimiter) = column.delimiter { 62 | delimiter 63 | } else if let Some(delimiter) = table.delimiter { 64 | delimiter 65 | } else { 66 | ' ' 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tests/all/combined_test.rs: -------------------------------------------------------------------------------- 1 | use comfy_table::presets::UTF8_FULL; 2 | use comfy_table::*; 3 | use pretty_assertions::assert_eq; 4 | 5 | fn get_preset_table() -> Table { 6 | let mut table = Table::new(); 7 | table.load_preset(UTF8_FULL) 8 | .set_content_arrangement(ContentArrangement::Dynamic) 9 | .set_width(80) 10 | .set_header(vec![ 11 | Cell::new("Header1").add_attribute(Attribute::Bold), 12 | Cell::new("Header2").fg(Color::Green), 13 | Cell::new("Header3"), 14 | ]) 15 | .add_row(vec![ 16 | Cell::new("This is a bold text").add_attribute(Attribute::Bold), 17 | Cell::new("This is a green text").fg(Color::Green), 18 | Cell::new("This one has black background").bg(Color::Black), 19 | ]) 20 | .add_row(vec![ 21 | Cell::new("Blinky boi").add_attribute(Attribute::SlowBlink), 22 | Cell::new("This table's content is dynamically arranged. The table is exactly 80 characters wide.\nHere comes a reallylongwordthatshoulddynamicallywrap"), 23 | Cell::new("COMBINE ALL THE THINGS") 24 | .fg(Color::Green) 25 | .bg(Color::Black) 26 | .add_attributes(vec![ 27 | Attribute::Bold, 28 | Attribute::SlowBlink, 29 | ]) 30 | ]); 31 | 32 | table 33 | } 34 | 35 | #[test] 36 | fn combined_features() { 37 | let mut table = get_preset_table(); 38 | table.force_no_tty().enforce_styling(); 39 | println!("{table}"); 40 | let expected = " 41 | ┌─────────────────────┬───────────────────────────────┬────────────────────────┐ 42 | │\u{1b}[1m Header1 \u{1b}[0m┆\u{1b}[38;5;10m Header2 \u{1b}[39m┆ Header3 │ 43 | ╞═════════════════════╪═══════════════════════════════╪════════════════════════╡ 44 | │\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│ 45 | │ ┆ ┆\u{1b}[48;5;0m background \u{1b}[49m│ 46 | ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ 47 | │\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│ 48 | │ ┆ dynamically arranged. The ┆ │ 49 | │ ┆ table is exactly 80 ┆ │ 50 | │ ┆ characters wide. ┆ │ 51 | │ ┆ Here comes a reallylongwordth ┆ │ 52 | │ ┆ atshoulddynamicallywrap ┆ │ 53 | └─────────────────────┴───────────────────────────────┴────────────────────────┘"; 54 | println!("{expected}"); 55 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 56 | } 57 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: 7 | - ".github/workflows/test.yml" 8 | - "**.rs" 9 | - "Cargo.toml" 10 | - "Cargo.lock" 11 | pull_request: 12 | branches: [main] 13 | paths: 14 | - ".github/workflows/test.yml" 15 | - "**.rs" 16 | - "Cargo.toml" 17 | - "Cargo.lock" 18 | 19 | jobs: 20 | test: 21 | name: Test target ${{ matrix.target }} on ${{ matrix.os }} for ${{ matrix.toolchain }} 22 | runs-on: ${{ matrix.os }} 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | target: 27 | - x86_64-unknown-linux-gnu 28 | - x86_64-pc-windows-msvc 29 | - x86_64-apple-darwin 30 | toolchain: [stable] 31 | include: 32 | - target: x86_64-unknown-linux-gnu 33 | os: ubuntu-latest 34 | minimal_setup: false 35 | - target: wasm32-wasip1 36 | os: ubuntu-latest 37 | minimal_setup: true 38 | toolchain: "stable" 39 | - target: x86_64-pc-windows-msvc 40 | os: windows-latest 41 | minimal_setup: false 42 | - target: x86_64-apple-darwin 43 | os: macos-latest 44 | minimal_setup: false 45 | 46 | # minimal_setup: This is needed for targets that don't support our dev dependencies. 47 | # It also excludes the default features, i.e. [tty]. 48 | # For instance, "wasm32-wasi" is such a target. 49 | steps: 50 | - name: Checkout code 51 | uses: actions/checkout@v4 52 | 53 | - name: Setup Rust toolchain 54 | uses: dtolnay/rust-toolchain@master 55 | with: 56 | targets: ${{ matrix.target }} 57 | toolchain: ${{ matrix.toolchain }} 58 | components: rustfmt, clippy 59 | 60 | - name: cargo build 61 | run: cargo build --target=${{ matrix.target }} 62 | if: ${{ !matrix.minimal_setup }} 63 | 64 | - name: cargo test 65 | run: cargo test --target=${{ matrix.target }} --features=_integration_test 66 | if: ${{ !matrix.minimal_setup }} 67 | 68 | - name: cargo test without default features 69 | run: cargo test --target=${{ matrix.target }} --tests --no-default-features 70 | if: ${{ !matrix.minimal_setup }} 71 | 72 | - name: cargo test with crossterm re-export 73 | run: cargo test --target=${{ matrix.target }} --features=_integration_test,reexport_crossterm 74 | if: ${{ !matrix.minimal_setup }} 75 | 76 | - name: cargo test with custom_styling 77 | run: cargo test --target=${{ matrix.target }} --features=_integration_test,custom_styling 78 | if: ${{ !matrix.minimal_setup }} 79 | 80 | - name: cargo build without default features and without dev dependencies 81 | run: cargo build --release --target=${{ matrix.target }} --no-default-features 82 | if: ${{ matrix.minimal_setup }} 83 | -------------------------------------------------------------------------------- /src/utils/arrangement/mod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use super::ColumnDisplayInfo; 4 | use crate::style::ContentArrangement; 5 | use crate::table::Table; 6 | 7 | pub mod constraint; 8 | mod disabled; 9 | mod dynamic; 10 | pub mod helper; 11 | 12 | type DisplayInfos = BTreeMap; 13 | 14 | /// Determine the width of each column depending on the content of the given table. 15 | /// The results uses Option, since users can choose to hide columns. 16 | pub fn arrange_content(table: &Table) -> Vec { 17 | let table_width = table.width().map(usize::from); 18 | let mut infos = BTreeMap::new(); 19 | 20 | let max_content_widths = table.column_max_content_widths(); 21 | 22 | // Check if we can already resolve some constraints. 23 | // This step also populates the ColumnDisplayInfo structs. 24 | let visible_columns = helper::count_visible_columns(&table.columns); 25 | for column in table.columns.iter() { 26 | if column.constraint.is_some() { 27 | constraint::evaluate( 28 | table, 29 | visible_columns, 30 | &mut infos, 31 | column, 32 | max_content_widths[column.index], 33 | ); 34 | } 35 | } 36 | #[cfg(feature = "_debug")] 37 | println!("After initial constraints: {infos:#?}"); 38 | 39 | // Fallback to `ContentArrangement::Disabled`, if we don't have any information 40 | // on how wide the table should be. 41 | let table_width = if let Some(table_width) = table_width { 42 | table_width 43 | } else { 44 | disabled::arrange(table, &mut infos, visible_columns, &max_content_widths); 45 | return infos.into_values().collect(); 46 | }; 47 | 48 | match &table.arrangement { 49 | ContentArrangement::Disabled => { 50 | disabled::arrange(table, &mut infos, visible_columns, &max_content_widths) 51 | } 52 | ContentArrangement::Dynamic | ContentArrangement::DynamicFullWidth => { 53 | dynamic::arrange(table, &mut infos, table_width, &max_content_widths); 54 | } 55 | } 56 | 57 | infos.into_values().collect() 58 | } 59 | 60 | #[cfg(test)] 61 | mod tests { 62 | use super::*; 63 | 64 | #[test] 65 | fn test_disabled_arrangement() { 66 | let mut table = Table::new(); 67 | table.set_header(vec!["head", "head", "head"]); 68 | table.add_row(vec!["__", "fivef", "sixsix"]); 69 | 70 | let display_infos = arrange_content(&table); 71 | 72 | // The width should be the width of the rows + padding 73 | let widths: Vec = display_infos.iter().map(ColumnDisplayInfo::width).collect(); 74 | assert_eq!(widths, vec![6, 7, 8]); 75 | } 76 | 77 | #[test] 78 | fn test_discover_columns() { 79 | let mut table = Table::new(); 80 | table.add_row(vec!["one", "two"]); 81 | 82 | // Get the first row and add a new cell, which would create a new column. 83 | let row = table.row_mut(0).unwrap(); 84 | row.add_cell("three".into()); 85 | 86 | // The table cannot know about the new cell yet, which is why we expect two columns. 87 | assert_eq!(table.columns.len(), 2); 88 | 89 | // After scanning for new columns however, it should show up. 90 | table.discover_columns(); 91 | assert_eq!(table.columns.len(), 3); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /tests/all/simple_test.rs: -------------------------------------------------------------------------------- 1 | use pretty_assertions::assert_eq; 2 | 3 | use comfy_table::*; 4 | 5 | #[test] 6 | fn simple_table() { 7 | let mut table = Table::new(); 8 | table 9 | .set_header(vec!["Header1", "Header2", "Header3"]) 10 | .add_row(vec![ 11 | "This is a text", 12 | "This is another text", 13 | "This is the third text", 14 | ]) 15 | .add_row(vec![ 16 | "This is another text", 17 | "Now\nadd some\nmulti line stuff", 18 | "This is awesome", 19 | ]); 20 | 21 | println!("{table}"); 22 | let expected = " 23 | +----------------------+----------------------+------------------------+ 24 | | Header1 | Header2 | Header3 | 25 | +======================================================================+ 26 | | This is a text | This is another text | This is the third text | 27 | |----------------------+----------------------+------------------------| 28 | | This is another text | Now | This is awesome | 29 | | | add some | | 30 | | | multi line stuff | | 31 | +----------------------+----------------------+------------------------+"; 32 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 33 | } 34 | 35 | #[test] 36 | fn missing_column_table() { 37 | let mut table = Table::new(); 38 | table 39 | .set_header(vec!["Header1", "Header2", "Header3"]) 40 | .add_row(vec!["One One", "One Two", "One Three"]) 41 | .add_row(vec!["Two One", "Two Two"]) 42 | .add_row(vec!["Three One"]); 43 | 44 | println!("{table}"); 45 | let expected = " 46 | +-----------+---------+-----------+ 47 | | Header1 | Header2 | Header3 | 48 | +=================================+ 49 | | One One | One Two | One Three | 50 | |-----------+---------+-----------| 51 | | Two One | Two Two | | 52 | |-----------+---------+-----------| 53 | | Three One | | | 54 | +-----------+---------+-----------+"; 55 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 56 | } 57 | 58 | #[test] 59 | fn single_column_table() { 60 | let mut table = Table::new(); 61 | table 62 | .set_header(vec!["Header1"]) 63 | .add_row(vec!["One One"]) 64 | .add_row(vec!["Two One"]) 65 | .add_row(vec!["Three One"]); 66 | 67 | println!("{table}"); 68 | let expected = " 69 | +-----------+ 70 | | Header1 | 71 | +===========+ 72 | | One One | 73 | |-----------| 74 | | Two One | 75 | |-----------| 76 | | Three One | 77 | +-----------+"; 78 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 79 | } 80 | 81 | #[test] 82 | fn lines() { 83 | let mut t = Table::new(); 84 | t.set_header(["heading 1", "heading 2", "heading 3"]); 85 | t.add_row(["test 1,1", "test 1,2", "test 1,3"]); 86 | t.add_row(["test 2,1", "test 2,2", "test 2,3"]); 87 | t.add_row(["test 3,1", "test 3,2", "test 3,3"]); 88 | 89 | let actual = t.lines(); 90 | let expected = &[ 91 | "+-----------+-----------+-----------+", 92 | "| heading 1 | heading 2 | heading 3 |", 93 | "+===================================+", 94 | "| test 1,1 | test 1,2 | test 1,3 |", 95 | "|-----------+-----------+-----------|", 96 | "| test 2,1 | test 2,2 | test 2,3 |", 97 | "|-----------+-----------+-----------|", 98 | "| test 3,1 | test 3,2 | test 3,3 |", 99 | "+-----------+-----------+-----------+", 100 | ]; 101 | 102 | assert_eq!(actual.collect::>(), expected); 103 | } 104 | -------------------------------------------------------------------------------- /tests/all/utf_8_characters.rs: -------------------------------------------------------------------------------- 1 | use pretty_assertions::assert_eq; 2 | 3 | use comfy_table::*; 4 | 5 | #[test] 6 | /// UTF-8 symbols that are longer than a single character are properly handled. 7 | /// This means, that comfy-table detects that they're longer than 1 character and styles/arranges 8 | /// the table accordingly. 9 | fn multi_character_utf8_symbols() { 10 | let mut table = Table::new(); 11 | table 12 | .set_header(vec!["Header1", "Header2", "Header3"]) 13 | .add_row(vec![ 14 | "This is a text", 15 | "This is another text", 16 | "This is the third text", 17 | ]) 18 | .add_row(vec![ 19 | "This is another text", 20 | "Now\nadd some\nmulti line stuff", 21 | "✅", 22 | ]); 23 | 24 | println!("{table}"); 25 | let expected = " 26 | +----------------------+----------------------+------------------------+ 27 | | Header1 | Header2 | Header3 | 28 | +======================================================================+ 29 | | This is a text | This is another text | This is the third text | 30 | |----------------------+----------------------+------------------------| 31 | | This is another text | Now | ✅ | 32 | | | add some | | 33 | | | multi line stuff | | 34 | +----------------------+----------------------+------------------------+"; 35 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 36 | } 37 | 38 | #[test] 39 | fn multi_character_utf8_word_splitting() { 40 | let mut table = Table::new(); 41 | table 42 | .set_width(8) 43 | .set_content_arrangement(ContentArrangement::Dynamic) 44 | .set_header(vec!["test"]) 45 | .add_row(vec!["abc✅def"]); 46 | 47 | println!("{table}"); 48 | let expected = " 49 | +------+ 50 | | test | 51 | +======+ 52 | | abc | 53 | | ✅de | 54 | | f | 55 | +------+"; 56 | println!("{expected}"); 57 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 58 | } 59 | 60 | #[test] 61 | fn multi_character_cjk_word_splitting() { 62 | let mut table = Table::new(); 63 | table 64 | .set_width(8) 65 | .set_content_arrangement(ContentArrangement::Dynamic) 66 | .set_header(vec!["test"]) 67 | .add_row(vec!["abc新年快乐edf"]); 68 | 69 | println!("{table}"); 70 | let expected = " 71 | +------+ 72 | | test | 73 | +======+ 74 | | abc | 75 | | 新年 | 76 | | 快乐 | 77 | | edf | 78 | +------+"; 79 | println!("{expected}"); 80 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 81 | } 82 | 83 | /// Handle emojis that'd joined via the "zero-width joiner" character U+200D and contain variant 84 | /// selectors. 85 | /// 86 | /// Those composite emojis should be handled as a single grapheme and thereby have their width 87 | /// calculated based on the grapheme length instead of the individual chars. 88 | /// 89 | /// This is also a regression test, as previously emojis were split in the middle of the joiner 90 | /// sequence, resulting in two different emojis on different lines. 91 | #[test] 92 | fn zwj_utf8_word_splitting() { 93 | let mut table = Table::new(); 94 | table 95 | .set_width(8) 96 | .set_content_arrangement(ContentArrangement::Dynamic) 97 | .set_header(vec!["test"]) 98 | .add_row(vec!["ab🙂‍↕️def"]); 99 | 100 | println!("{table}"); 101 | let expected = " 102 | +------+ 103 | | test | 104 | +======+ 105 | | ab🙂‍↕️ | 106 | | def | 107 | +------+"; 108 | println!("{expected}"); 109 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 110 | } 111 | -------------------------------------------------------------------------------- /src/style/table.rs: -------------------------------------------------------------------------------- 1 | /// Specify how comfy_table should arrange the content in your table. 2 | /// 3 | /// ``` 4 | /// use comfy_table::{Table, ContentArrangement}; 5 | /// 6 | /// let mut table = Table::new(); 7 | /// table.set_content_arrangement(ContentArrangement::Dynamic); 8 | /// ``` 9 | #[derive(Clone, Debug)] 10 | pub enum ContentArrangement { 11 | /// Don't do any content arrangement.\ 12 | /// Tables with this mode might become wider than your output and look ugly.\ 13 | /// Constraints on columns are still respected. 14 | Disabled, 15 | /// Dynamically determine the width of columns in regard to terminal width and content length.\ 16 | /// With this mode, the content in cells will wrap dynamically to get the best column layout 17 | /// for the given content.\ 18 | /// Constraints on columns are still respected. 19 | /// 20 | /// **Warning:** If terminal width cannot be determined and no table_width is set via 21 | /// [Table::set_width](crate::table::Table::set_width), 22 | /// this option won't work and [Disabled](ContentArrangement::Disabled) will be used as a fallback. 23 | Dynamic, 24 | /// This is mode is the same as the [ContentArrangement::Dynamic] arrangement, but it will always use as much 25 | /// space as it's given. Any surplus space will be distributed between all columns. 26 | DynamicFullWidth, 27 | } 28 | 29 | /// All configurable table components. 30 | /// A character can be assigned to each component via [Table::set_style](crate::table::Table::set_style). 31 | /// This is then used to draw character of the respective component to the commandline. 32 | /// 33 | /// I hope that most component names are self-explanatory. Just in case: 34 | /// BorderIntersections are Intersections, where rows/columns lines meet outer borders. 35 | /// E.g.: 36 | /// ```text 37 | /// --------- 38 | /// v | 39 | /// +---+---+---+ | 40 | /// | a | b | c | | 41 | /// +===+===+===+<-| 42 | /// | | | | | 43 | /// +---+---+---+<-- These "+" chars are Borderintersections. 44 | /// | | | | The inner "+" chars are MiddleIntersections 45 | /// +---+---+---+ 46 | /// ``` 47 | #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] 48 | pub enum TableComponent { 49 | LeftBorder, 50 | RightBorder, 51 | TopBorder, 52 | BottomBorder, 53 | LeftHeaderIntersection, 54 | HeaderLines, 55 | MiddleHeaderIntersections, 56 | RightHeaderIntersection, 57 | VerticalLines, 58 | HorizontalLines, 59 | MiddleIntersections, 60 | LeftBorderIntersections, 61 | RightBorderIntersections, 62 | TopBorderIntersections, 63 | BottomBorderIntersections, 64 | TopLeftCorner, 65 | TopRightCorner, 66 | BottomLeftCorner, 67 | BottomRightCorner, 68 | } 69 | 70 | impl TableComponent { 71 | const fn components() -> [TableComponent; 19] { 72 | [ 73 | TableComponent::LeftBorder, 74 | TableComponent::RightBorder, 75 | TableComponent::TopBorder, 76 | TableComponent::BottomBorder, 77 | TableComponent::LeftHeaderIntersection, 78 | TableComponent::HeaderLines, 79 | TableComponent::MiddleHeaderIntersections, 80 | TableComponent::RightHeaderIntersection, 81 | TableComponent::VerticalLines, 82 | TableComponent::HorizontalLines, 83 | TableComponent::MiddleIntersections, 84 | TableComponent::LeftBorderIntersections, 85 | TableComponent::RightBorderIntersections, 86 | TableComponent::TopBorderIntersections, 87 | TableComponent::BottomBorderIntersections, 88 | TableComponent::TopLeftCorner, 89 | TableComponent::TopRightCorner, 90 | TableComponent::BottomLeftCorner, 91 | TableComponent::BottomRightCorner, 92 | ] 93 | } 94 | 95 | pub fn iter() -> impl Iterator { 96 | TableComponent::components().into_iter() 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /tests/all/hidden_test.rs: -------------------------------------------------------------------------------- 1 | use comfy_table::*; 2 | use pretty_assertions::assert_eq; 3 | 4 | fn get_table() -> Table { 5 | let mut table = Table::new(); 6 | table 7 | .load_preset(presets::UTF8_FULL) 8 | .set_header(vec![ 9 | "hidden_header", 10 | "smol", 11 | "hidden_header", 12 | "Two_hidden_headers_in_a_row", 13 | "Header2", 14 | "Header3", 15 | "hidden_header", 16 | ]) 17 | .add_row(vec![ 18 | "start_hidden", 19 | "smol", 20 | "middle_hidden", 21 | "two_hidden_headers_in_a_row", 22 | "This is another text", 23 | "This is the third text", 24 | "end_hidden", 25 | ]) 26 | .add_row(vec![ 27 | "asdf", 28 | "smol", 29 | "asdf", 30 | "asdf", 31 | "Now\nadd some\nmulti line stuff", 32 | "This is awesome", 33 | "asdf", 34 | ]); 35 | 36 | // Hide the first, third and 6th column 37 | table 38 | .column_mut(0) 39 | .unwrap() 40 | .set_constraint(ColumnConstraint::Hidden); 41 | table 42 | .column_mut(2) 43 | .unwrap() 44 | .set_constraint(ColumnConstraint::Hidden); 45 | table 46 | .column_mut(3) 47 | .unwrap() 48 | .set_constraint(ColumnConstraint::Hidden); 49 | 50 | table 51 | .column_mut(6) 52 | .unwrap() 53 | .set_constraint(ColumnConstraint::Hidden); 54 | 55 | table 56 | } 57 | 58 | /// Make sure hidden columns won't be displayed 59 | #[test] 60 | fn hidden_columns() { 61 | let table = get_table(); 62 | println!("{table}"); 63 | let expected = " 64 | ┌──────┬──────────────────────┬────────────────────────┐ 65 | │ smol ┆ Header2 ┆ Header3 │ 66 | ╞══════╪══════════════════════╪════════════════════════╡ 67 | │ smol ┆ This is another text ┆ This is the third text │ 68 | ├╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ 69 | │ smol ┆ Now ┆ This is awesome │ 70 | │ ┆ add some ┆ │ 71 | │ ┆ multi line stuff ┆ │ 72 | └──────┴──────────────────────┴────────────────────────┘"; 73 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 74 | } 75 | 76 | /// Make sure dynamic adjustment still works with hidden columns 77 | #[test] 78 | fn hidden_columns_with_dynamic_adjustment() { 79 | let mut table = get_table(); 80 | table.set_width(25); 81 | table.set_content_arrangement(ContentArrangement::Dynamic); 82 | 83 | println!("{table}"); 84 | let expected = " 85 | ┌──────┬────────┬───────┐ 86 | │ smol ┆ Header ┆ Heade │ 87 | │ ┆ 2 ┆ r3 │ 88 | ╞══════╪════════╪═══════╡ 89 | │ smol ┆ This ┆ This │ 90 | │ ┆ is ano ┆ is │ 91 | │ ┆ ther ┆ the │ 92 | │ ┆ text ┆ third │ 93 | │ ┆ ┆ text │ 94 | ├╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┤ 95 | │ smol ┆ Now ┆ This │ 96 | │ ┆ add ┆ is │ 97 | │ ┆ some ┆ aweso │ 98 | │ ┆ multi ┆ me │ 99 | │ ┆ line ┆ │ 100 | │ ┆ stuff ┆ │ 101 | └──────┴────────┴───────┘"; 102 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 103 | } 104 | 105 | /// Nothing breaks, if all columns are hidden 106 | #[test] 107 | fn only_hidden_columns() { 108 | let mut table = get_table(); 109 | table.set_constraints(vec![ 110 | ColumnConstraint::Hidden, 111 | ColumnConstraint::Hidden, 112 | ColumnConstraint::Hidden, 113 | ColumnConstraint::Hidden, 114 | ColumnConstraint::Hidden, 115 | ColumnConstraint::Hidden, 116 | ]); 117 | 118 | println!("{table}"); 119 | let expected = " 120 | ┌┐ 121 | ╞╡ 122 | ├┤ 123 | └┘"; 124 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 125 | } 126 | -------------------------------------------------------------------------------- /benches/build_tables.rs: -------------------------------------------------------------------------------- 1 | use criterion::{Criterion, criterion_group, criterion_main}; 2 | 3 | use comfy_table::ColumnConstraint::*; 4 | use comfy_table::Width::*; 5 | use comfy_table::presets::UTF8_FULL; 6 | use comfy_table::*; 7 | 8 | /// Build the readme table 9 | #[cfg(feature = "tty")] 10 | fn build_readme_table() { 11 | let mut table = Table::new(); 12 | table.load_preset(UTF8_FULL) 13 | .set_content_arrangement(ContentArrangement::Dynamic) 14 | .set_width(80) 15 | .set_header(vec![ 16 | Cell::new("Header1").add_attribute(Attribute::Bold), 17 | Cell::new("Header2").fg(Color::Green), 18 | Cell::new("Header3"), 19 | ]) 20 | .add_row(vec![ 21 | Cell::new("This is a bold text").add_attribute(Attribute::Bold), 22 | Cell::new("This is a green text").fg(Color::Green), 23 | Cell::new("This one has black background").bg(Color::Black), 24 | ]) 25 | .add_row(vec![ 26 | Cell::new("Blinky boi").add_attribute(Attribute::SlowBlink), 27 | Cell::new("This table's content is dynamically arranged. The table is exactly 80 characters wide.\nHere comes a reallylongwordthatshoulddynamicallywrap"), 28 | Cell::new("COMBINE ALL THE THINGS") 29 | .fg(Color::Green) 30 | .bg(Color::Black) 31 | .add_attributes(vec![ 32 | Attribute::Bold, 33 | Attribute::SlowBlink, 34 | ]) 35 | ]); 36 | 37 | // Build the table. 38 | let _ = table.lines(); 39 | } 40 | 41 | #[cfg(not(feature = "tty"))] 42 | fn build_readme_table() { 43 | let mut table = Table::new(); 44 | table.load_preset(UTF8_FULL) 45 | .set_content_arrangement(ContentArrangement::Dynamic) 46 | .set_width(80) 47 | .set_header(vec![ 48 | Cell::new("Header1"), 49 | Cell::new("Header2"), 50 | Cell::new("Header3"), 51 | ]) 52 | .add_row(vec![ 53 | Cell::new("This is a bold text"), 54 | Cell::new("This is a green text"), 55 | Cell::new("This one has black background"), 56 | ]) 57 | .add_row(vec![ 58 | Cell::new("Blinky boi"), 59 | Cell::new("This table's content is dynamically arranged. The table is exactly 80 characters wide.\nHere comes a reallylongwordthatshoulddynamicallywrap"), 60 | Cell::new("COMBINE ALL THE THINGS"), 61 | ]); 62 | 63 | // Build the table. 64 | let _ = table.lines(); 65 | } 66 | 67 | /// Create a dynamic 10x10 Table with width 400 and unevenly distributed content. 68 | /// On top of that, most of the columns have some kind of constraint. 69 | fn build_big_table() { 70 | let mut table = Table::new(); 71 | table 72 | .load_preset(UTF8_FULL) 73 | .set_content_arrangement(ContentArrangement::DynamicFullWidth) 74 | .set_width(400) 75 | .set_header(vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); 76 | 77 | // Create a 10x10 grid 78 | for row_index in 0..10 { 79 | let mut row = Vec::new(); 80 | for column in 0..10 { 81 | row.push("SomeWord ".repeat((column + row_index * 2) % 10)); 82 | } 83 | table.add_row(row); 84 | } 85 | 86 | table.set_constraints(vec![ 87 | UpperBoundary(Fixed(20)), 88 | LowerBoundary(Fixed(40)), 89 | Absolute(Fixed(5)), 90 | Absolute(Percentage(3)), 91 | Absolute(Percentage(3)), 92 | Boundaries { 93 | lower: Fixed(30), 94 | upper: Percentage(10), 95 | }, 96 | ]); 97 | 98 | // Build the table. 99 | let _ = table.lines(); 100 | } 101 | 102 | pub fn build_tables(crit: &mut Criterion) { 103 | crit.bench_function("Readme table", |b| b.iter(build_readme_table)); 104 | 105 | crit.bench_function("Big table", |b| b.iter(build_big_table)); 106 | } 107 | 108 | criterion_group!(benches, build_tables); 109 | criterion_main!(benches); 110 | -------------------------------------------------------------------------------- /src/style/color.rs: -------------------------------------------------------------------------------- 1 | /// Represents a color. 2 | /// 3 | /// This type is a simplified re-implementation of crossterm's Color enum. 4 | /// See [crossterm::style::color](https://docs.rs/crossterm/latest/crossterm/style/enum.Color.html) 5 | /// 6 | /// # Platform-specific Notes 7 | /// 8 | /// The following list of 16 base colors are available for almost all terminals (Windows 7 and 8 included). 9 | /// 10 | /// | Light | Dark | 11 | /// | :--------- | :------------ | 12 | /// | `DarkGrey` | `Black` | 13 | /// | `Red` | `DarkRed` | 14 | /// | `Green` | `DarkGreen` | 15 | /// | `Yellow` | `DarkYellow` | 16 | /// | `Blue` | `DarkBlue` | 17 | /// | `Magenta` | `DarkMagenta` | 18 | /// | `Cyan` | `DarkCyan` | 19 | /// | `White` | `Grey` | 20 | /// 21 | /// Most UNIX terminals and Windows 10 consoles support additional colors. 22 | /// See [Color::Rgb] or [Color::AnsiValue] for more info. 23 | /// 24 | /// Usage: 25 | /// 26 | /// Check [crate::Cell::bg], [crate::Cell::fg] and on how to use it. 27 | #[derive(Copy, Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)] 28 | pub enum Color { 29 | /// Resets the terminal color. 30 | Reset, 31 | 32 | /// Black color. 33 | Black, 34 | 35 | /// Dark grey color. 36 | DarkGrey, 37 | 38 | /// Light red color. 39 | Red, 40 | 41 | /// Dark red color. 42 | DarkRed, 43 | 44 | /// Light green color. 45 | Green, 46 | 47 | /// Dark green color. 48 | DarkGreen, 49 | 50 | /// Light yellow color. 51 | Yellow, 52 | 53 | /// Dark yellow color. 54 | DarkYellow, 55 | 56 | /// Light blue color. 57 | Blue, 58 | 59 | /// Dark blue color. 60 | DarkBlue, 61 | 62 | /// Light magenta color. 63 | Magenta, 64 | 65 | /// Dark magenta color. 66 | DarkMagenta, 67 | 68 | /// Light cyan color. 69 | Cyan, 70 | 71 | /// Dark cyan color. 72 | DarkCyan, 73 | 74 | /// White color. 75 | White, 76 | 77 | /// Grey color. 78 | Grey, 79 | 80 | /// An RGB color. See [RGB color model](https://en.wikipedia.org/wiki/RGB_color_model) for more info. 81 | /// 82 | /// Most UNIX terminals and Windows 10 supported only. 83 | /// See [Platform-specific notes](enum.Color.html#platform-specific-notes) for more info. 84 | Rgb { r: u8, g: u8, b: u8 }, 85 | 86 | /// An ANSI color. See [256 colors - cheat sheet](https://jonasjacek.github.io/colors/) for more info. 87 | /// 88 | /// Most UNIX terminals and Windows 10 supported only. 89 | /// See [Platform-specific notes](enum.Color.html#platform-specific-notes) for more info. 90 | AnsiValue(u8), 91 | } 92 | 93 | /// Map the internal mirrored [Color] enum to the actually used [crossterm::style::Color]. 94 | pub(crate) fn map_color(color: Color) -> crossterm::style::Color { 95 | match color { 96 | Color::Reset => crossterm::style::Color::Reset, 97 | Color::Black => crossterm::style::Color::Black, 98 | Color::DarkGrey => crossterm::style::Color::DarkGrey, 99 | Color::Red => crossterm::style::Color::Red, 100 | Color::DarkRed => crossterm::style::Color::DarkRed, 101 | Color::Green => crossterm::style::Color::Green, 102 | Color::DarkGreen => crossterm::style::Color::DarkGreen, 103 | Color::Yellow => crossterm::style::Color::Yellow, 104 | Color::DarkYellow => crossterm::style::Color::DarkYellow, 105 | Color::Blue => crossterm::style::Color::Blue, 106 | Color::DarkBlue => crossterm::style::Color::DarkBlue, 107 | Color::Magenta => crossterm::style::Color::Magenta, 108 | Color::DarkMagenta => crossterm::style::Color::DarkMagenta, 109 | Color::Cyan => crossterm::style::Color::Cyan, 110 | Color::DarkCyan => crossterm::style::Color::DarkCyan, 111 | Color::White => crossterm::style::Color::White, 112 | Color::Grey => crossterm::style::Color::Grey, 113 | Color::Rgb { r, g, b } => crossterm::style::Color::Rgb { r, g, b }, 114 | Color::AnsiValue(value) => crossterm::style::Color::AnsiValue(value), 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/style/presets.rs: -------------------------------------------------------------------------------- 1 | /// The default style for tables. 2 | /// 3 | /// ```text 4 | /// +-------+-------+ 5 | /// | Hello | there | 6 | /// +===============+ 7 | /// | a | b | 8 | /// |-------+-------| 9 | /// | c | d | 10 | /// +-------+-------+ 11 | /// ``` 12 | pub const ASCII_FULL: &str = "||--+==+|-+||++++++"; 13 | 14 | /// Just like ASCII_FULL, but without dividers between rows. 15 | /// 16 | /// ```text 17 | /// +-------+-------+ 18 | /// | Hello | there | 19 | /// +===============+ 20 | /// | a | b | 21 | /// | c | d | 22 | /// +-------+-------+ 23 | pub const ASCII_FULL_CONDENSED: &str = "||--+==+| ++++++"; 24 | 25 | /// Just like ASCII_FULL, but without any borders. 26 | /// 27 | /// ```text 28 | /// Hello | there 29 | /// =============== 30 | /// a | b 31 | /// -------+------- 32 | /// c | d 33 | /// ``` 34 | pub const ASCII_NO_BORDERS: &str = " == |-+ "; 35 | 36 | /// Just like ASCII_FULL, but without vertical/horizontal middle lines. 37 | /// 38 | /// ```text 39 | /// +---------------+ 40 | /// | Hello there | 41 | /// +===============+ 42 | /// | a b | 43 | /// | | 44 | /// | c d | 45 | /// +---------------+ 46 | /// ``` 47 | pub const ASCII_BORDERS_ONLY: &str = "||--+==+ ||--++++"; 48 | 49 | /// Just like ASCII_BORDERS_ONLY, but without spacing between rows. 50 | /// 51 | /// ```text 52 | /// +---------------+ 53 | /// | Hello there | 54 | /// +===============+ 55 | /// | a b | 56 | /// | c d | 57 | /// +---------------+ 58 | /// ``` 59 | pub const ASCII_BORDERS_ONLY_CONDENSED: &str = "||--+==+ --++++"; 60 | 61 | /// Just like ASCII_FULL, but without vertical/horizontal middle lines and no side borders. 62 | /// 63 | /// ```text 64 | /// --------------- 65 | /// Hello there 66 | /// =============== 67 | /// a b 68 | /// --------------- 69 | /// c d 70 | /// --------------- 71 | /// ``` 72 | pub const ASCII_HORIZONTAL_ONLY: &str = " -- == -- -- "; 73 | 74 | /// Markdown like table styles. 75 | /// 76 | /// ```text 77 | /// | Hello | there | 78 | /// |-------|-------| 79 | /// | a | b | 80 | /// | c | d | 81 | /// ``` 82 | pub const ASCII_MARKDOWN: &str = "|| |-||| "; 83 | 84 | /// The UTF8 enabled version of the default style for tables.\ 85 | /// Quite beautiful isn't it? It's drawn with UTF8's box drawing characters. 86 | /// 87 | /// ```text 88 | /// ┌───────┬───────┐ 89 | /// │ Hello ┆ there │ 90 | /// ╞═══════╪═══════╡ 91 | /// │ a ┆ b │ 92 | /// ├╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┤ 93 | /// │ c ┆ d │ 94 | /// └───────┴───────┘ 95 | /// ``` 96 | pub const UTF8_FULL: &str = "││──╞═╪╡┆╌┼├┤┬┴┌┐└┘"; 97 | 98 | /// Default UTF8 style, but without dividers between rows. 99 | /// 100 | /// ```text 101 | /// ┌───────┬───────┐ 102 | /// │ Hello ┆ there │ 103 | /// ╞═══════╪═══════╡ 104 | /// │ a ┆ b │ 105 | /// │ c ┆ d │ 106 | /// └───────┴───────┘ 107 | /// ``` 108 | pub const UTF8_FULL_CONDENSED: &str = "││──╞═╪╡┆ ┬┴┌┐└┘"; 109 | 110 | /// Default UTF8 style, but without any borders. 111 | /// 112 | /// ```text 113 | /// Hello ┆ there 114 | /// ═══════╪═══════ 115 | /// a ┆ b 116 | /// ╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌ 117 | /// c ┆ d 118 | /// ``` 119 | pub const UTF8_NO_BORDERS: &str = " ═╪ ┆╌┼ "; 120 | 121 | /// Just like the UTF8_FULL style, but without vertical/horizontal middle lines. 122 | /// 123 | /// ```text 124 | /// ┌───────────────┐ 125 | /// │ Hello there │ 126 | /// ╞═══════════════╡ 127 | /// │ a b │ 128 | /// │ c d │ 129 | /// └───────────────┘ 130 | /// ``` 131 | pub const UTF8_BORDERS_ONLY: &str = "││──╞══╡ ──┌┐└┘"; 132 | 133 | /// Only display vertical lines. 134 | /// 135 | /// ```text 136 | /// ─────────────── 137 | /// Hello there 138 | /// ═══════════════ 139 | /// a b 140 | /// ─────────────── 141 | /// c d 142 | /// ─────────────── 143 | /// ``` 144 | pub const UTF8_HORIZONTAL_ONLY: &str = " ── ══ ── ── "; 145 | 146 | /// Don't draw any borders or other lines. 147 | /// Useful, if you want to simply organize some data without any cosmetics. 148 | /// 149 | /// ```text 150 | /// Hello there 151 | /// a b 152 | /// c d 153 | /// ``` 154 | pub const NOTHING: &str = " "; 155 | -------------------------------------------------------------------------------- /src/row.rs: -------------------------------------------------------------------------------- 1 | use std::slice::Iter; 2 | 3 | use crate::{ 4 | cell::{Cell, Cells}, 5 | utils::formatting::content_split::measure_text_width, 6 | }; 7 | 8 | /// Each row contains [Cells](crate::Cell) and can be added to a [Table](crate::Table). 9 | #[derive(Clone, Debug, Default)] 10 | pub struct Row { 11 | /// Index of the row. 12 | /// This will be set as soon as the row is added to the table. 13 | pub(crate) index: Option, 14 | pub(crate) cells: Vec, 15 | pub(crate) max_height: Option, 16 | } 17 | 18 | impl Row { 19 | pub fn new() -> Self { 20 | Self::default() 21 | } 22 | 23 | /// Add a cell to the row. 24 | /// 25 | /// **Attention:** 26 | /// If a row has already been added to a table and you add more cells to it 27 | /// than there're columns currently know to the [Table](crate::Table) struct, 28 | /// these columns won't be known to the table unless you call 29 | /// [crate::Table::discover_columns]. 30 | /// 31 | /// ```rust 32 | /// use comfy_table::{Row, Cell}; 33 | /// 34 | /// let mut row = Row::new(); 35 | /// row.add_cell(Cell::new("One")); 36 | /// ``` 37 | pub fn add_cell(&mut self, cell: Cell) -> &mut Self { 38 | self.cells.push(cell); 39 | 40 | self 41 | } 42 | 43 | /// Truncate content of cells which occupies more than X lines of space. 44 | /// 45 | /// ``` 46 | /// use comfy_table::{Row, Cell}; 47 | /// 48 | /// let mut row = Row::new(); 49 | /// row.max_height(5); 50 | /// ``` 51 | pub fn max_height(&mut self, lines: usize) -> &mut Self { 52 | self.max_height = Some(lines); 53 | 54 | self 55 | } 56 | 57 | /// Get the longest content width for all cells of this row 58 | pub(crate) fn max_content_widths(&self) -> Vec { 59 | // Iterate over all cells 60 | self.cells 61 | .iter() 62 | .map(|cell| { 63 | // Iterate over all content strings and return a vector of string widths. 64 | // Each entry represents the longest string width for a cell. 65 | cell.content 66 | .iter() 67 | .map(|string| measure_text_width(string)) 68 | .max() 69 | .unwrap_or(0) 70 | }) 71 | .collect() 72 | } 73 | 74 | /// Get the amount of cells on this row. 75 | pub fn cell_count(&self) -> usize { 76 | self.cells.len() 77 | } 78 | 79 | /// Returns an iterator over all cells of this row 80 | pub fn cell_iter(&self) -> Iter<'_, Cell> { 81 | self.cells.iter() 82 | } 83 | } 84 | 85 | /// Create a Row from any `Into`. \ 86 | /// [Cells] is a simple wrapper around a `Vec`. 87 | /// 88 | /// Check the [From] implementations on [Cell] for more information. 89 | /// 90 | /// ```rust 91 | /// use comfy_table::{Row, Cell}; 92 | /// 93 | /// let row = Row::from(vec!["One", "Two", "Three",]); 94 | /// let row = Row::from(vec![ 95 | /// Cell::new("One"), 96 | /// Cell::new("Two"), 97 | /// Cell::new("Three"), 98 | /// ]); 99 | /// let row = Row::from(vec![1, 2, 3, 4]); 100 | /// ``` 101 | impl> From for Row { 102 | fn from(cells: T) -> Self { 103 | Self { 104 | index: None, 105 | cells: cells.into().0, 106 | max_height: None, 107 | } 108 | } 109 | } 110 | 111 | #[cfg(test)] 112 | mod tests { 113 | use super::*; 114 | 115 | #[test] 116 | fn test_correct_max_content_width() { 117 | let row = Row::from(vec![ 118 | "", 119 | "four", 120 | "fivef", 121 | "sixsix", 122 | "11 but with\na newline", 123 | ]); 124 | 125 | let max_content_widths = row.max_content_widths(); 126 | 127 | assert_eq!(max_content_widths, vec![0, 4, 5, 6, 11]); 128 | } 129 | 130 | #[test] 131 | fn test_some_functions() { 132 | let cells = ["one", "two", "three"]; 133 | let mut row = Row::new(); 134 | for cell in cells.iter() { 135 | row.add_cell(Cell::new(cell)); 136 | } 137 | assert_eq!(row.cell_count(), cells.len()); 138 | 139 | let mut cell_content_iter = cells.iter(); 140 | for cell in row.cell_iter() { 141 | assert_eq!( 142 | cell.content(), 143 | cell_content_iter.next().unwrap().to_string() 144 | ); 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/column.rs: -------------------------------------------------------------------------------- 1 | use crate::style::{CellAlignment, ColumnConstraint}; 2 | 3 | /// A representation of a table's column. 4 | /// Useful for styling and specifying constraints how big a column should be. 5 | /// 6 | /// 1. Content padding for cells in this column 7 | /// 2. Constraints on how wide this column shall be 8 | /// 3. Default alignment for cells in this column 9 | /// 10 | /// Columns are generated when adding rows or a header to a table.\ 11 | /// As a result columns can only be modified after the table is populated by some data. 12 | /// 13 | /// ``` 14 | /// use comfy_table::{Width::*, CellAlignment, ColumnConstraint::*, Table}; 15 | /// 16 | /// let mut table = Table::new(); 17 | /// table.set_header(&vec!["one", "two"]); 18 | /// 19 | /// let mut column = table.column_mut(1).expect("This should be column two"); 20 | /// 21 | /// // Set the max width for all cells of this column to 20 characters. 22 | /// column.set_constraint(UpperBoundary(Fixed(20))); 23 | /// 24 | /// // Set the left padding to 5 spaces and the right padding to 1 space 25 | /// column.set_padding((5, 1)); 26 | /// 27 | /// // Align content in all cells of this column to the center of the cell. 28 | /// column.set_cell_alignment(CellAlignment::Center); 29 | /// ``` 30 | #[derive(Debug, Clone)] 31 | pub struct Column { 32 | /// The index of the column 33 | pub index: usize, 34 | /// Left/right padding for each cell of this column in spaces 35 | pub(crate) padding: (u16, u16), 36 | /// The delimiter which is used to split the text into consistent pieces. 37 | /// Default is ` `. 38 | pub(crate) delimiter: Option, 39 | /// Define the [CellAlignment] for all cells of this column 40 | pub(crate) cell_alignment: Option, 41 | pub(crate) constraint: Option, 42 | } 43 | 44 | impl Column { 45 | pub fn new(index: usize) -> Self { 46 | Self { 47 | index, 48 | padding: (1, 1), 49 | delimiter: None, 50 | constraint: None, 51 | cell_alignment: None, 52 | } 53 | } 54 | 55 | /// Set the padding for all cells of this column. 56 | /// 57 | /// Padding is provided in the form of (left, right).\ 58 | /// Default is `(1, 1)`. 59 | pub fn set_padding(&mut self, padding: (u16, u16)) -> &mut Self { 60 | self.padding = padding; 61 | 62 | self 63 | } 64 | 65 | /// Convenience helper that returns the total width of the combined padding. 66 | pub fn padding_width(&self) -> u16 { 67 | self.padding.0.saturating_add(self.padding.1) 68 | } 69 | 70 | /// Set the delimiter used to split text for this column's cells. 71 | /// 72 | /// A custom delimiter on a cell in will overwrite the column's delimiter. 73 | /// Normal text uses spaces (` `) as delimiters. This is necessary to help comfy-table 74 | /// understand the concept of _words_. 75 | pub fn set_delimiter(&mut self, delimiter: char) -> &mut Self { 76 | self.delimiter = Some(delimiter); 77 | 78 | self 79 | } 80 | 81 | /// Constraints allow to influence the auto-adjustment behavior of columns.\ 82 | /// This can be useful to counter undesired auto-adjustment of content in tables. 83 | pub fn set_constraint(&mut self, constraint: ColumnConstraint) -> &mut Self { 84 | self.constraint = Some(constraint); 85 | 86 | self 87 | } 88 | 89 | /// Get the constraint that is used for this column. 90 | pub fn constraint(&self) -> Option<&ColumnConstraint> { 91 | self.constraint.as_ref() 92 | } 93 | 94 | /// Remove any constraint on this column 95 | pub fn remove_constraint(&mut self) -> &mut Self { 96 | self.constraint = None; 97 | 98 | self 99 | } 100 | 101 | /// Returns weather the columns is hidden via [ColumnConstraint::Hidden]. 102 | pub fn is_hidden(&self) -> bool { 103 | matches!(self.constraint, Some(ColumnConstraint::Hidden)) 104 | } 105 | 106 | /// Set the alignment for content inside of cells for this column.\ 107 | /// **Note:** Alignment on a cell will always overwrite the column's setting. 108 | pub fn set_cell_alignment(&mut self, alignment: CellAlignment) { 109 | self.cell_alignment = Some(alignment); 110 | } 111 | } 112 | 113 | #[cfg(test)] 114 | mod tests { 115 | use super::*; 116 | 117 | #[test] 118 | fn test_column() { 119 | let mut column = Column::new(0); 120 | column.set_padding((0, 0)); 121 | 122 | column.set_constraint(ColumnConstraint::ContentWidth); 123 | assert_eq!(column.constraint(), Some(&ColumnConstraint::ContentWidth)); 124 | 125 | column.remove_constraint(); 126 | assert_eq!(column.constraint(), None); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /tests/all/styling_test.rs: -------------------------------------------------------------------------------- 1 | use pretty_assertions::assert_eq; 2 | 3 | use comfy_table::presets::UTF8_FULL; 4 | use comfy_table::*; 5 | 6 | fn get_preset_table() -> Table { 7 | let mut table = Table::new(); 8 | table 9 | .load_preset(UTF8_FULL) 10 | .set_header(vec![ 11 | Cell::new("Header1").add_attribute(Attribute::Bold), 12 | Cell::new("Header2").fg(Color::Green), 13 | Cell::new("Header3").bg(Color::Black), 14 | ]) 15 | .add_row(vec![ 16 | Cell::new("This is a bold text").add_attribute(Attribute::Bold), 17 | Cell::new("This is a green text").fg(Color::Green), 18 | Cell::new("This one has black background").bg(Color::Black), 19 | ]) 20 | .add_row(vec![ 21 | Cell::new("Blinking boiii").add_attribute(Attribute::SlowBlink), 22 | Cell::new("Now\nadd some\nmulti line stuff") 23 | .fg(Color::Cyan) 24 | .add_attribute(Attribute::Underlined), 25 | Cell::new("COMBINE ALL THE THINGS") 26 | .fg(Color::Green) 27 | .bg(Color::Black) 28 | .add_attribute(Attribute::Bold) 29 | .add_attribute(Attribute::SlowBlink), 30 | ]); 31 | 32 | table 33 | } 34 | 35 | #[test] 36 | fn styled_table() { 37 | let mut table = get_preset_table(); 38 | table.force_no_tty().enforce_styling(); 39 | println!("{table}"); 40 | let expected = " 41 | ┌─────────────────────┬──────────────────────┬───────────────────────────────┐ 42 | │\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│ 43 | ╞═════════════════════╪══════════════════════╪═══════════════════════════════╡ 44 | │\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│ 45 | ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ 46 | │\u{1b}[5m Blinking boiii \u{1b}[0m┆\u{1b}[38;5;14m\u{1b}[4m 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│ 47 | │ ┆\u{1b}[38;5;14m\u{1b}[4m add some \u{1b}[0m┆ │ 48 | │ ┆\u{1b}[38;5;14m\u{1b}[4m multi line stuff \u{1b}[0m┆ │ 49 | └─────────────────────┴──────────────────────┴───────────────────────────────┘"; 50 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 51 | } 52 | 53 | #[test] 54 | fn no_style_styled_table() { 55 | let mut table = get_preset_table(); 56 | table.force_no_tty(); 57 | 58 | println!("{table}"); 59 | let expected = " 60 | ┌─────────────────────┬──────────────────────┬───────────────────────────────┐ 61 | │ Header1 ┆ Header2 ┆ Header3 │ 62 | ╞═════════════════════╪══════════════════════╪═══════════════════════════════╡ 63 | │ This is a bold text ┆ This is a green text ┆ This one has black background │ 64 | ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ 65 | │ Blinking boiii ┆ Now ┆ COMBINE ALL THE THINGS │ 66 | │ ┆ add some ┆ │ 67 | │ ┆ multi line stuff ┆ │ 68 | └─────────────────────┴──────────────────────┴───────────────────────────────┘"; 69 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 70 | } 71 | 72 | #[test] 73 | fn styled_text_only_table() { 74 | let mut table = get_preset_table(); 75 | table.force_no_tty().enforce_styling().style_text_only(); 76 | println!("{table}"); 77 | let expected = " 78 | ┌─────────────────────┬──────────────────────┬───────────────────────────────┐ 79 | │ \u{1b}[1mHeader1\u{1b}[0m ┆ \u{1b}[38;5;10mHeader2\u{1b}[39m ┆ \u{1b}[48;5;0mHeader3\u{1b}[49m │ 80 | ╞═════════════════════╪══════════════════════╪═══════════════════════════════╡ 81 | │ \u{1b}[1mThis is a bold text\u{1b}[0m ┆ \u{1b}[38;5;10mThis is a green text\u{1b}[39m ┆ \u{1b}[48;5;0mThis one has black background\u{1b}[49m │ 82 | ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ 83 | │ \u{1b}[5mBlinking boiii\u{1b}[0m ┆ \u{1b}[38;5;14m\u{1b}[4mNow\u{1b}[0m ┆ \u{1b}[48;5;0m\u{1b}[38;5;10m\u{1b}[1m\u{1b}[5mCOMBINE ALL THE THINGS\u{1b}[0m │ 84 | │ ┆ \u{1b}[38;5;14m\u{1b}[4madd some\u{1b}[0m ┆ │ 85 | │ ┆ \u{1b}[38;5;14m\u{1b}[4mmulti line stuff\u{1b}[0m ┆ │ 86 | └─────────────────────┴──────────────────────┴───────────────────────────────┘"; 87 | 88 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 89 | } 90 | -------------------------------------------------------------------------------- /src/utils/arrangement/constraint.rs: -------------------------------------------------------------------------------- 1 | use super::helper::*; 2 | use super::{ColumnDisplayInfo, DisplayInfos}; 3 | use crate::style::{ColumnConstraint, ColumnConstraint::*, Width}; 4 | use crate::{Column, Table}; 5 | 6 | /// Look at given constraints of a column and check if some of them can be resolved at the very 7 | /// beginning. 8 | /// 9 | /// For example: 10 | /// - We get an absolute width. 11 | /// - MinWidth constraints on columns, whose content is garantueed to be smaller than the specified 12 | /// minimal width. 13 | /// - The Column is supposed to be hidden. 14 | pub fn evaluate( 15 | table: &Table, 16 | visible_columns: usize, 17 | infos: &mut DisplayInfos, 18 | column: &Column, 19 | max_content_width: u16, 20 | ) { 21 | match &column.constraint { 22 | Some(ContentWidth) => { 23 | let info = ColumnDisplayInfo::new(column, max_content_width); 24 | infos.insert(column.index, info); 25 | } 26 | Some(Absolute(width)) => { 27 | if let Some(width) = absolute_value_from_width(table, width, visible_columns) { 28 | // The column should get always get a fixed width. 29 | let width = absolute_width_with_padding(column, width); 30 | let info = ColumnDisplayInfo::new(column, width); 31 | infos.insert(column.index, info); 32 | } 33 | } 34 | Some(Hidden) => { 35 | let mut info = ColumnDisplayInfo::new(column, max_content_width); 36 | info.is_hidden = true; 37 | infos.insert(column.index, info); 38 | } 39 | _ => {} 40 | } 41 | 42 | if let Some(min_width) = min(table, &column.constraint, visible_columns) { 43 | // In case a min_width is specified, we may already fix the size of the column. 44 | // We do this, if we know that the content is smaller than the min size. 45 | let max_width = max_content_width + column.padding_width(); 46 | if max_width <= min_width { 47 | let width = absolute_width_with_padding(column, min_width); 48 | let info = ColumnDisplayInfo::new(column, width); 49 | infos.insert(column.index, info); 50 | } 51 | } 52 | } 53 | 54 | /// A little wrapper, which resolves possible lower boundary constraints to their actual value for 55 | /// the current table and terminal width. 56 | /// 57 | /// This returns the value of absolute characters that are allowed to be in this column. \ 58 | /// Lower boundaries with [Width::Fixed] just return their internal value. \ 59 | /// Lower boundaries with [Width::Percentage] return the percental amount of the current table 60 | /// width. 61 | pub fn min( 62 | table: &Table, 63 | constraint: &Option, 64 | visible_columns: usize, 65 | ) -> Option { 66 | let constraint = if let Some(constraint) = constraint { 67 | constraint 68 | } else { 69 | return None; 70 | }; 71 | 72 | match constraint { 73 | LowerBoundary(width) | Boundaries { lower: width, .. } => { 74 | absolute_value_from_width(table, width, visible_columns) 75 | } 76 | _ => None, 77 | } 78 | } 79 | 80 | /// A little wrapper, which resolves possible upper boundary constraints to their actual value for 81 | /// the current table and terminal width. 82 | /// 83 | /// This returns the value of absolute characters that are allowed to be in this column. \ 84 | /// Upper boundaries with [Width::Fixed] just return their internal value. \ 85 | /// Upper boundaries with [Width::Percentage] return the percental amount of the current table 86 | /// width. 87 | pub fn max( 88 | table: &Table, 89 | constraint: &Option, 90 | visible_columns: usize, 91 | ) -> Option { 92 | let constraint = if let Some(constraint) = constraint { 93 | constraint 94 | } else { 95 | return None; 96 | }; 97 | 98 | match constraint { 99 | UpperBoundary(width) | Boundaries { upper: width, .. } => { 100 | absolute_value_from_width(table, width, visible_columns) 101 | } 102 | _ => None, 103 | } 104 | } 105 | 106 | /// Resolve an absolute value from a given boundary 107 | pub fn absolute_value_from_width( 108 | table: &Table, 109 | width: &Width, 110 | visible_columns: usize, 111 | ) -> Option { 112 | match width { 113 | Width::Fixed(width) => Some(*width), 114 | Width::Percentage(percent) => { 115 | // Don't return a value, if we cannot determine the current table width. 116 | let table_width = table.width().map(usize::from)?; 117 | 118 | // Enforce at most 100% 119 | let percent = std::cmp::min(*percent, 100u16); 120 | 121 | // Subtract the borders from the table width. 122 | let width = table_width.saturating_sub(count_border_columns(table, visible_columns)); 123 | 124 | // Calculate the absolute value in actual columns. 125 | let width = (width * usize::from(percent) / 100) 126 | .try_into() 127 | .unwrap_or(u16::MAX); 128 | Some(width) 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /tests/all/inner_style_test.rs: -------------------------------------------------------------------------------- 1 | use comfy_table::{presets::UTF8_FULL, *}; 2 | use pretty_assertions::assert_eq; 3 | 4 | fn get_preset_table() -> Table { 5 | let mut table = Table::new(); 6 | table.load_preset(UTF8_FULL); 7 | table.set_content_arrangement(ContentArrangement::Dynamic); 8 | table.set_width(85); 9 | 10 | let mut row = Row::new(); 11 | row.add_cell(Cell::new(format!( 12 | "hello{}cell1", 13 | console::style("123\n456").dim().blue() 14 | ))); 15 | row.add_cell(Cell::new("cell2")); 16 | 17 | table.add_row(row); 18 | 19 | let mut row = Row::new(); 20 | row.add_cell(Cell::new( 21 | 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()) 22 | )); 23 | row.add_cell(Cell::new( 24 | "cell4 asdfasfsad asdfasdf sad fas df asdf as df asdf asdfasdfasdfasdfasdfasdfa dsfa sdf asdf asd f asdf as df sadf asd fas df " 25 | )); 26 | table.add_row(row); 27 | 28 | let mut row = Row::new(); 29 | row.add_cell(Cell::new("cell5")); 30 | row.add_cell(Cell::new("cell6")); 31 | table.add_row(row); 32 | 33 | table 34 | } 35 | 36 | #[test] 37 | fn styled_table() { 38 | console::set_colors_enabled(true); 39 | let mut table = get_preset_table(); 40 | table.force_no_tty().enforce_styling(); 41 | println!("{table}"); 42 | let expected = " 43 | ┌─────────────────────────────────────────┬─────────────────────────────────────────┐ 44 | │ hello\u{1b}[34m\u{1b}[2m123\u{1b}[0m ┆ cell2 │ 45 | │ \u{1b}[34m\u{1b}[2m456\u{1b}[0mcell1 ┆ │ 46 | ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ 47 | │ cell sys-devices-pci00:00-0000:000:07:0 ┆ cell4 asdfasfsad asdfasdf sad fas df │ 48 | │ 0.1-usb2-2\\x2d1-2\\x2d1.3-2\\x2d1.3:1.0-h ┆ asdf as df asdf │ 49 | │ ost2-target2:0:0-2:0:0:1-block-sdb\u{1b}[31m\u{1b}[1m.devi\u{1b}[0m ┆ asdfasdfasdfasdfasdfasdfa dsfa sdf asdf │ 50 | │ \u{1b}[31m\u{1b}[1mce\u{1b}[0m ┆ asd f asdf as df sadf asd fas df │ 51 | ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ 52 | │ cell5 ┆ cell6 │ 53 | └─────────────────────────────────────────┴─────────────────────────────────────────┘"; 54 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 55 | } 56 | 57 | #[test] 58 | fn no_style_styled_table() { 59 | console::set_colors_enabled(true); 60 | let mut table = get_preset_table(); 61 | table.force_no_tty(); 62 | 63 | println!("{table}"); 64 | let expected = " 65 | ┌─────────────────────────────────────────┬─────────────────────────────────────────┐ 66 | │ hello\u{1b}[34m\u{1b}[2m123\u{1b}[0m ┆ cell2 │ 67 | │ \u{1b}[34m\u{1b}[2m456\u{1b}[0mcell1 ┆ │ 68 | ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ 69 | │ cell sys-devices-pci00:00-0000:000:07:0 ┆ cell4 asdfasfsad asdfasdf sad fas df │ 70 | │ 0.1-usb2-2\\x2d1-2\\x2d1.3-2\\x2d1.3:1.0-h ┆ asdf as df asdf │ 71 | │ ost2-target2:0:0-2:0:0:1-block-sdb\u{1b}[31m\u{1b}[1m.devi\u{1b}[0m ┆ asdfasdfasdfasdfasdfasdfa dsfa sdf asdf │ 72 | │ \u{1b}[31m\u{1b}[1mce\u{1b}[0m ┆ asd f asdf as df sadf asd fas df │ 73 | ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ 74 | │ cell5 ┆ cell6 │ 75 | └─────────────────────────────────────────┴─────────────────────────────────────────┘"; 76 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 77 | } 78 | 79 | #[test] 80 | fn styled_text_only_table() { 81 | console::set_colors_enabled(true); 82 | let mut table = get_preset_table(); 83 | table.force_no_tty().enforce_styling().style_text_only(); 84 | println!("{table}"); 85 | let expected = " 86 | ┌─────────────────────────────────────────┬─────────────────────────────────────────┐ 87 | │ hello\u{1b}[34m\u{1b}[2m123\u{1b}[0m ┆ cell2 │ 88 | │ \u{1b}[34m\u{1b}[2m456\u{1b}[0mcell1 ┆ │ 89 | ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ 90 | │ cell sys-devices-pci00:00-0000:000:07:0 ┆ cell4 asdfasfsad asdfasdf sad fas df │ 91 | │ 0.1-usb2-2\\x2d1-2\\x2d1.3-2\\x2d1.3:1.0-h ┆ asdf as df asdf │ 92 | │ ost2-target2:0:0-2:0:0:1-block-sdb\u{1b}[31m\u{1b}[1m.devi\u{1b}[0m ┆ asdfasdfasdfasdfasdfasdfa dsfa sdf asdf │ 93 | │ \u{1b}[31m\u{1b}[1mce\u{1b}[0m ┆ asd f asdf as df sadf asd fas df │ 94 | ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ 95 | │ cell5 ┆ cell6 │ 96 | └─────────────────────────────────────────┴─────────────────────────────────────────┘"; 97 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 98 | } 99 | -------------------------------------------------------------------------------- /tests/all/presets_test.rs: -------------------------------------------------------------------------------- 1 | use pretty_assertions::assert_eq; 2 | 3 | use comfy_table::presets::*; 4 | use comfy_table::*; 5 | 6 | fn get_preset_table() -> Table { 7 | let mut table = Table::new(); 8 | table 9 | .set_header(vec!["Hello", "there"]) 10 | .add_row(vec!["a", "b"]) 11 | .add_row(vec!["c", "d"]); 12 | 13 | table 14 | } 15 | 16 | #[test] 17 | fn test_ascii_full() { 18 | let mut table = get_preset_table(); 19 | table.load_preset(ASCII_FULL); 20 | println!("{table}"); 21 | let expected = " 22 | +-------+-------+ 23 | | Hello | there | 24 | +===============+ 25 | | a | b | 26 | |-------+-------| 27 | | c | d | 28 | +-------+-------+"; 29 | println!("{expected}"); 30 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 31 | } 32 | 33 | #[test] 34 | fn test_ascii_full_condensed() { 35 | let mut table = get_preset_table(); 36 | table.load_preset(ASCII_FULL_CONDENSED); 37 | println!("{table}"); 38 | let expected = " 39 | +-------+-------+ 40 | | Hello | there | 41 | +===============+ 42 | | a | b | 43 | | c | d | 44 | +-------+-------+"; 45 | println!("{expected}"); 46 | assert_eq!(expected, "\n".to_string() + &table.trim_fmt()); 47 | } 48 | 49 | #[test] 50 | fn test_ascii_no_borders() { 51 | let mut table = get_preset_table(); 52 | table.load_preset(ASCII_NO_BORDERS); 53 | println!("{table}"); 54 | let expected = " 55 | Hello | there 56 | =============== 57 | a | b 58 | -------+------- 59 | c | d"; 60 | println!("{expected}"); 61 | assert_eq!(expected, "\n".to_string() + &table.trim_fmt()); 62 | } 63 | 64 | #[test] 65 | fn test_ascii_borders_only() { 66 | let mut table = get_preset_table(); 67 | table.load_preset(ASCII_BORDERS_ONLY); 68 | println!("{table}"); 69 | let expected = " 70 | +---------------+ 71 | | Hello there | 72 | +===============+ 73 | | a b | 74 | | | 75 | | c d | 76 | +---------------+"; 77 | println!("{expected}"); 78 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 79 | } 80 | 81 | #[test] 82 | fn test_ascii_borders_only_condensed() { 83 | let mut table = get_preset_table(); 84 | table.load_preset(ASCII_BORDERS_ONLY_CONDENSED); 85 | println!("{table}"); 86 | let expected = " 87 | +---------------+ 88 | | Hello there | 89 | +===============+ 90 | | a b | 91 | | c d | 92 | +---------------+"; 93 | println!("{expected}"); 94 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 95 | } 96 | 97 | #[test] 98 | fn test_ascii_horizontal_only() { 99 | let mut table = get_preset_table(); 100 | table.load_preset(ASCII_HORIZONTAL_ONLY); 101 | println!("{table}"); 102 | let expected = " 103 | --------------- 104 | Hello there 105 | =============== 106 | a b 107 | --------------- 108 | c d 109 | ---------------"; 110 | println!("{expected}"); 111 | assert_eq!(expected, "\n".to_string() + &table.trim_fmt()); 112 | } 113 | 114 | #[test] 115 | fn test_ascii_markdown() { 116 | let mut table = get_preset_table(); 117 | table.load_preset(ASCII_MARKDOWN); 118 | println!("{table}"); 119 | let expected = " 120 | | Hello | there | 121 | |-------|-------| 122 | | a | b | 123 | | c | d |"; 124 | println!("{expected}"); 125 | assert_eq!(expected, "\n".to_string() + &table.trim_fmt()); 126 | } 127 | 128 | #[test] 129 | fn test_utf8_full() { 130 | let mut table = get_preset_table(); 131 | table.load_preset(UTF8_FULL); 132 | println!("{table}"); 133 | let expected = " 134 | ┌───────┬───────┐ 135 | │ Hello ┆ there │ 136 | ╞═══════╪═══════╡ 137 | │ a ┆ b │ 138 | ├╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┤ 139 | │ c ┆ d │ 140 | └───────┴───────┘"; 141 | println!("{expected}"); 142 | assert_eq!(expected, "\n".to_string() + &table.trim_fmt()); 143 | } 144 | 145 | #[test] 146 | fn test_utf8_full_condensed() { 147 | let mut table = get_preset_table(); 148 | table.load_preset(UTF8_FULL_CONDENSED); 149 | println!("{table}"); 150 | let expected = " 151 | ┌───────┬───────┐ 152 | │ Hello ┆ there │ 153 | ╞═══════╪═══════╡ 154 | │ a ┆ b │ 155 | │ c ┆ d │ 156 | └───────┴───────┘"; 157 | println!("{expected}"); 158 | assert_eq!(expected, "\n".to_string() + &table.trim_fmt()); 159 | } 160 | 161 | #[test] 162 | fn test_utf8_no_borders() { 163 | let mut table = get_preset_table(); 164 | table.load_preset(UTF8_NO_BORDERS); 165 | println!("{table}"); 166 | let expected = " 167 | Hello ┆ there 168 | ═══════╪═══════ 169 | a ┆ b 170 | ╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌ 171 | c ┆ d"; 172 | println!("{expected}"); 173 | assert_eq!(expected, "\n".to_string() + &table.trim_fmt()); 174 | } 175 | 176 | #[test] 177 | fn test_utf8_horizontal_only() { 178 | let mut table = get_preset_table(); 179 | table.load_preset(UTF8_HORIZONTAL_ONLY); 180 | println!("{table}"); 181 | let expected = " 182 | ─────────────── 183 | Hello there 184 | ═══════════════ 185 | a b 186 | ─────────────── 187 | c d 188 | ───────────────"; 189 | println!("{expected}"); 190 | assert_eq!(expected, "\n".to_string() + &table.trim_fmt()); 191 | } 192 | 193 | #[test] 194 | fn test_nothing() { 195 | let mut table = get_preset_table(); 196 | table.load_preset(NOTHING); 197 | println!("{table}"); 198 | let expected = " 199 | Hello there 200 | a b 201 | c d"; 202 | println!("{expected}"); 203 | assert_eq!(expected, "\n".to_string() + &table.trim_fmt()); 204 | } 205 | 206 | #[test] 207 | fn test_nothing_without_padding() { 208 | let mut table = get_preset_table(); 209 | table.load_preset(NOTHING); 210 | let column = table.column_iter_mut().next().unwrap(); 211 | column.set_padding((0, 1)); 212 | println!("{table}"); 213 | let expected = " 214 | Hello there 215 | a b 216 | c d"; 217 | println!("{expected}"); 218 | assert_eq!(expected, "\n".to_string() + &table.trim_fmt()); 219 | } 220 | -------------------------------------------------------------------------------- /src/style/attribute.rs: -------------------------------------------------------------------------------- 1 | /// Represents an attribute. 2 | /// 3 | /// # Platform-specific Notes 4 | /// 5 | /// * Only UNIX and Windows 10 terminals do support text attributes. 6 | /// * Keep in mind that not all terminals support all attributes. 7 | /// * Crossterm implements almost all attributes listed in the 8 | /// [SGR parameters](https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters). 9 | /// 10 | /// | Attribute | Windows | UNIX | Notes | 11 | /// | :-- | :--: | :--: | :-- | 12 | /// | `Reset` | ✓ | ✓ | | 13 | /// | `Bold` | ✓ | ✓ | | 14 | /// | `Dim` | ✓ | ✓ | | 15 | /// | `Italic` | ? | ? | Not widely supported, sometimes treated as inverse. | 16 | /// | `Underlined` | ✓ | ✓ | | 17 | /// | `SlowBlink` | ? | ? | Not widely supported, sometimes treated as inverse. | 18 | /// | `RapidBlink` | ? | ? | Not widely supported. MS-DOS ANSI.SYS; 150+ per minute. | 19 | /// | `Reverse` | ✓ | ✓ | | 20 | /// | `Hidden` | ✓ | ✓ | Also known as Conceal. | 21 | /// | `Fraktur` | ✗ | ✓ | Legible characters, but marked for deletion. | 22 | /// | `DefaultForegroundColor` | ? | ? | Implementation specific (according to standard). | 23 | /// | `DefaultBackgroundColor` | ? | ? | Implementation specific (according to standard). | 24 | /// | `Framed` | ? | ? | Not widely supported. | 25 | /// | `Encircled` | ? | ? | This should turn on the encircled attribute. | 26 | /// | `OverLined` | ? | ? | This should draw a line at the top of the text. | 27 | /// 28 | /// Usage: 29 | /// 30 | /// Check [crate::Cell::add_attribute] on how to use it. 31 | #[derive(Copy, Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)] 32 | #[non_exhaustive] 33 | pub enum Attribute { 34 | /// Resets all the attributes. 35 | Reset, 36 | /// Increases the text intensity. 37 | Bold, 38 | /// Decreases the text intensity. 39 | Dim, 40 | /// Emphasises the text. 41 | Italic, 42 | /// Underlines the text. 43 | Underlined, 44 | 45 | // Other types of underlining 46 | /// Double underlines the text. 47 | DoubleUnderlined, 48 | /// Undercurls the text. 49 | Undercurled, 50 | /// Underdots the text. 51 | Underdotted, 52 | /// Underdashes the text. 53 | Underdashed, 54 | 55 | /// Makes the text blinking (< 150 per minute). 56 | SlowBlink, 57 | /// Makes the text blinking (>= 150 per minute). 58 | RapidBlink, 59 | /// Swaps foreground and background colors. 60 | Reverse, 61 | /// Hides the text (also known as Conceal). 62 | Hidden, 63 | /// Crosses the text. 64 | CrossedOut, 65 | /// Sets the [Fraktur](https://en.wikipedia.org/wiki/Fraktur) typeface. 66 | /// 67 | /// Mostly used for [mathematical alphanumeric symbols](https://en.wikipedia.org/wiki/Mathematical_Alphanumeric_Symbols). 68 | Fraktur, 69 | /// Turns off the `Bold` attribute. - Inconsistent - Prefer to use NormalIntensity 70 | NoBold, 71 | /// Switches the text back to normal intensity (no bold, italic). 72 | NormalIntensity, 73 | /// Turns off the `Italic` attribute. 74 | NoItalic, 75 | /// Turns off the `Underlined` attribute. 76 | NoUnderline, 77 | /// Turns off the text blinking (`SlowBlink` or `RapidBlink`). 78 | NoBlink, 79 | /// Turns off the `Reverse` attribute. 80 | NoReverse, 81 | /// Turns off the `Hidden` attribute. 82 | NoHidden, 83 | /// Turns off the `CrossedOut` attribute. 84 | NotCrossedOut, 85 | /// Makes the text framed. 86 | Framed, 87 | /// Makes the text encircled. 88 | Encircled, 89 | /// Draws a line at the top of the text. 90 | OverLined, 91 | /// Turns off the `Frame` and `Encircled` attributes. 92 | NotFramedOrEncircled, 93 | /// Turns off the `OverLined` attribute. 94 | NotOverLined, 95 | } 96 | 97 | /// Map the internal mirrored [Attribute] to the actually used [crossterm::style::Attribute] 98 | pub(crate) fn map_attribute(attribute: Attribute) -> crossterm::style::Attribute { 99 | match attribute { 100 | Attribute::Reset => crossterm::style::Attribute::Reset, 101 | Attribute::Bold => crossterm::style::Attribute::Bold, 102 | Attribute::Dim => crossterm::style::Attribute::Dim, 103 | Attribute::Italic => crossterm::style::Attribute::Italic, 104 | Attribute::Underlined => crossterm::style::Attribute::Underlined, 105 | Attribute::DoubleUnderlined => crossterm::style::Attribute::DoubleUnderlined, 106 | Attribute::Undercurled => crossterm::style::Attribute::Undercurled, 107 | Attribute::Underdotted => crossterm::style::Attribute::Underdotted, 108 | Attribute::Underdashed => crossterm::style::Attribute::Underdashed, 109 | Attribute::SlowBlink => crossterm::style::Attribute::SlowBlink, 110 | Attribute::RapidBlink => crossterm::style::Attribute::RapidBlink, 111 | Attribute::Reverse => crossterm::style::Attribute::Reverse, 112 | Attribute::Hidden => crossterm::style::Attribute::Hidden, 113 | Attribute::CrossedOut => crossterm::style::Attribute::CrossedOut, 114 | Attribute::Fraktur => crossterm::style::Attribute::Fraktur, 115 | Attribute::NoBold => crossterm::style::Attribute::NoBold, 116 | Attribute::NormalIntensity => crossterm::style::Attribute::NormalIntensity, 117 | Attribute::NoItalic => crossterm::style::Attribute::NoItalic, 118 | Attribute::NoUnderline => crossterm::style::Attribute::NoUnderline, 119 | Attribute::NoBlink => crossterm::style::Attribute::NoBlink, 120 | Attribute::NoReverse => crossterm::style::Attribute::NoReverse, 121 | Attribute::NoHidden => crossterm::style::Attribute::NoHidden, 122 | Attribute::NotCrossedOut => crossterm::style::Attribute::NotCrossedOut, 123 | Attribute::Framed => crossterm::style::Attribute::Framed, 124 | Attribute::Encircled => crossterm::style::Attribute::Encircled, 125 | Attribute::OverLined => crossterm::style::Attribute::OverLined, 126 | Attribute::NotFramedOrEncircled => crossterm::style::Attribute::NotFramedOrEncircled, 127 | Attribute::NotOverLined => crossterm::style::Attribute::NotOverLined, 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/cell.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "tty")] 2 | use crate::{Attribute, Color}; 3 | 4 | use crate::style::CellAlignment; 5 | 6 | /// A stylable table cell with content. 7 | #[derive(Clone, Debug, PartialEq, Eq, Hash)] 8 | pub struct Cell { 9 | /// The content is a list of strings.\ 10 | /// This is done to make working with newlines more easily.\ 11 | /// When creating a new [Cell], the given content is split by newline. 12 | pub(crate) content: Vec, 13 | /// The delimiter which is used to split the text into consistent pieces.\ 14 | /// The default is ` `. 15 | pub(crate) delimiter: Option, 16 | pub(crate) alignment: Option, 17 | #[cfg(feature = "tty")] 18 | pub(crate) fg: Option, 19 | #[cfg(feature = "tty")] 20 | pub(crate) bg: Option, 21 | #[cfg(feature = "tty")] 22 | pub(crate) attributes: Vec, 23 | } 24 | 25 | impl Cell { 26 | /// Create a new Cell 27 | #[allow(clippy::needless_pass_by_value)] 28 | pub fn new(content: T) -> Self { 29 | Self::new_owned(content.to_string()) 30 | } 31 | 32 | /// Create a new Cell from an owned String 33 | pub fn new_owned(content: String) -> Self { 34 | #[cfg_attr(not(feature = "custom_styling"), allow(unused_mut))] 35 | let mut split_content: Vec = content.split('\n').map(ToString::to_string).collect(); 36 | 37 | // Correct ansi codes so style is terminated and resumed around the split 38 | #[cfg(feature = "custom_styling")] 39 | crate::utils::formatting::content_split::fix_style_in_split_str(&mut split_content); 40 | 41 | Self { 42 | content: split_content, 43 | delimiter: None, 44 | alignment: None, 45 | #[cfg(feature = "tty")] 46 | fg: None, 47 | #[cfg(feature = "tty")] 48 | bg: None, 49 | #[cfg(feature = "tty")] 50 | attributes: Vec::new(), 51 | } 52 | } 53 | 54 | /// Return a copy of the content contained in this cell. 55 | pub fn content(&self) -> String { 56 | self.content.join("\n") 57 | } 58 | 59 | /// Set the delimiter used to split text for this cell. \ 60 | /// Normal text uses spaces (` `) as delimiters. This is necessary to help comfy-table 61 | /// understand the concept of _words_. 62 | #[must_use] 63 | pub fn set_delimiter(mut self, delimiter: char) -> Self { 64 | self.delimiter = Some(delimiter); 65 | 66 | self 67 | } 68 | 69 | /// Set the alignment of content for this cell. 70 | /// 71 | /// Setting this overwrites alignment settings of the 72 | /// [Column](crate::column::Column::set_cell_alignment) for this specific cell. 73 | /// ``` 74 | /// use comfy_table::CellAlignment; 75 | /// use comfy_table::Cell; 76 | /// 77 | /// let mut cell = Cell::new("Some content") 78 | /// .set_alignment(CellAlignment::Center); 79 | /// ``` 80 | #[must_use] 81 | pub fn set_alignment(mut self, alignment: CellAlignment) -> Self { 82 | self.alignment = Some(alignment); 83 | 84 | self 85 | } 86 | 87 | /// Set the foreground text color for this cell. 88 | /// 89 | /// Look at [Color](crate::Color) for a list of all possible Colors. 90 | /// ``` 91 | /// use comfy_table::Color; 92 | /// use comfy_table::Cell; 93 | /// 94 | /// let mut cell = Cell::new("Some content") 95 | /// .fg(Color::Red); 96 | /// ``` 97 | #[cfg(feature = "tty")] 98 | #[must_use] 99 | pub fn fg(mut self, color: Color) -> Self { 100 | self.fg = Some(color); 101 | 102 | self 103 | } 104 | 105 | /// Set the background color for this cell. 106 | /// 107 | /// Look at [Color](crate::Color) for a list of all possible Colors. 108 | /// ``` 109 | /// use comfy_table::Color; 110 | /// use comfy_table::Cell; 111 | /// 112 | /// let mut cell = Cell::new("Some content") 113 | /// .bg(Color::Red); 114 | /// ``` 115 | #[cfg(feature = "tty")] 116 | #[must_use] 117 | pub fn bg(mut self, color: Color) -> Self { 118 | self.bg = Some(color); 119 | 120 | self 121 | } 122 | 123 | /// Add a styling attribute to the content cell.\ 124 | /// Those can be **bold**, _italic_, blinking and many more. 125 | /// 126 | /// Look at [Attribute](crate::Attribute) for a list of all possible Colors. 127 | /// ``` 128 | /// use comfy_table::Attribute; 129 | /// use comfy_table::Cell; 130 | /// 131 | /// let mut cell = Cell::new("Some content") 132 | /// .add_attribute(Attribute::Bold); 133 | /// ``` 134 | #[cfg(feature = "tty")] 135 | #[must_use] 136 | pub fn add_attribute(mut self, attribute: Attribute) -> Self { 137 | self.attributes.push(attribute); 138 | 139 | self 140 | } 141 | 142 | /// Same as add_attribute, but you can pass a vector of [Attributes](Attribute) 143 | #[cfg(feature = "tty")] 144 | #[must_use] 145 | pub fn add_attributes(mut self, mut attribute: Vec) -> Self { 146 | self.attributes.append(&mut attribute); 147 | 148 | self 149 | } 150 | } 151 | 152 | /// Convert anything with [ToString] to a new [Cell]. 153 | /// 154 | /// ``` 155 | /// # use comfy_table::Cell; 156 | /// let cell: Cell = "content".into(); 157 | /// let cell: Cell = 5u32.into(); 158 | /// ``` 159 | impl From for Cell { 160 | fn from(content: T) -> Self { 161 | Self::new(content) 162 | } 163 | } 164 | 165 | /// A simple wrapper type for a `Vec`. 166 | /// 167 | /// This wrapper is needed to support generic conversions between iterables and `Vec`. 168 | /// Check the trait implementations for more docs. 169 | pub struct Cells(pub Vec); 170 | 171 | /// Allow the conversion of a type to a [Cells], which is a simple vector of cells. 172 | /// 173 | /// By default this is implemented for all Iterators over items implementing [ToString]. 174 | /// 175 | /// ``` 176 | /// use comfy_table::{Row, Cells}; 177 | /// 178 | /// let cells_string: Cells = vec!["One", "Two", "Three"].into(); 179 | /// let cells_integer: Cells = vec![1, 2, 3, 4].into(); 180 | /// ``` 181 | impl From for Cells 182 | where 183 | T: IntoIterator, 184 | T::Item: Into, 185 | { 186 | fn from(cells: T) -> Self { 187 | Self(cells.into_iter().map(Into::into).collect()) 188 | } 189 | } 190 | 191 | #[cfg(test)] 192 | mod tests { 193 | use super::*; 194 | 195 | #[test] 196 | fn test_column_generation() { 197 | let content = "This is\nsome multiline\nstring".to_string(); 198 | let cell = Cell::new(content.clone()); 199 | 200 | assert_eq!(cell.content(), content); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /tests/all/truncation.rs: -------------------------------------------------------------------------------- 1 | use pretty_assertions::assert_eq; 2 | 3 | use comfy_table::ColumnConstraint::*; 4 | use comfy_table::Width::*; 5 | use comfy_table::{ContentArrangement, Row, Table}; 6 | 7 | use crate::all::assert_table_line_width; 8 | 9 | /// Individual rows can be configured to have a max height. 10 | /// Everything beyond that line height should be truncated. 11 | #[test] 12 | fn table_with_truncate() { 13 | let mut table = Table::new(); 14 | let mut first_row: Row = Row::from(vec![ 15 | "This is a very long line with a lot of text", 16 | "This is anotherverylongtextwithlongwords text", 17 | "smol", 18 | ]); 19 | first_row.max_height(4); 20 | 21 | let mut second_row = Row::from(vec![ 22 | "Now let's\nadd a really long line in the middle of the cell \n and add more multi line stuff", 23 | "This is another text", 24 | "smol", 25 | ]); 26 | second_row.max_height(4); 27 | 28 | table 29 | .set_header(vec!["Header1", "Header2", "Head"]) 30 | .set_content_arrangement(ContentArrangement::Dynamic) 31 | .set_width(35) 32 | .add_row(first_row) 33 | .add_row(second_row); 34 | 35 | // The first column will be wider than 6 chars. 36 | // The second column's content is wider than 6 chars. There should be a '...'. 37 | let second_column = table.column_mut(1).unwrap(); 38 | second_column.set_constraint(Absolute(Fixed(8))); 39 | 40 | // The third column's content is less than 6 chars width. There shouldn't be a '...'. 41 | let third_column = table.column_mut(2).unwrap(); 42 | third_column.set_constraint(Absolute(Fixed(7))); 43 | 44 | println!("{table}"); 45 | let expected = " 46 | +----------------+--------+-------+ 47 | | Header1 | Header | Head | 48 | | | 2 | | 49 | +=================================+ 50 | | This is a very | This | smol | 51 | | long line with | is ano | | 52 | | a lot of text | therve | | 53 | | | ryl... | | 54 | |----------------+--------+-------| 55 | | Now let's | This | smol | 56 | | add a really | is ano | | 57 | | long line in | ther | | 58 | | the middle ... | text | | 59 | +----------------+--------+-------+"; 60 | println!("{expected}"); 61 | assert_table_line_width(&table, 35); 62 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 63 | } 64 | 65 | #[test] 66 | fn table_with_truncate_indicator() { 67 | let mut table = Table::new(); 68 | let mut first_row: Row = Row::from(vec![ 69 | "This is a very long line with a lot of text", 70 | "This is anotherverylongtextwithlongwords text", 71 | "smol", 72 | ]); 73 | first_row.max_height(4); 74 | 75 | let mut second_row = Row::from(vec![ 76 | "Now let's\nadd a really long line in the middle of the cell \n and add more multi line stuff", 77 | "This is another text", 78 | "smol", 79 | ]); 80 | second_row.max_height(4); 81 | 82 | table 83 | .set_header(vec!["Header1", "Header2", "Head"]) 84 | .set_content_arrangement(ContentArrangement::Dynamic) 85 | .set_truncation_indicator("…") 86 | .set_width(35) 87 | .add_row(first_row) 88 | .add_row(second_row); 89 | 90 | // The first column will be wider than 6 chars. 91 | // The second column's content is wider than 6 chars. There should be a '…'. 92 | let second_column = table.column_mut(1).unwrap(); 93 | second_column.set_constraint(Absolute(Fixed(8))); 94 | 95 | // The third column's content is less than 6 chars width. There shouldn't be a '…'. 96 | let third_column = table.column_mut(2).unwrap(); 97 | third_column.set_constraint(Absolute(Fixed(7))); 98 | 99 | println!("{table}"); 100 | let expected = " 101 | +----------------+--------+-------+ 102 | | Header1 | Header | Head | 103 | | | 2 | | 104 | +=================================+ 105 | | This is a very | This | smol | 106 | | long line with | is ano | | 107 | | a lot of text | therve | | 108 | | | rylon… | | 109 | |----------------+--------+-------| 110 | | Now let's | This | smol | 111 | | add a really | is ano | | 112 | | long line in | ther | | 113 | | the middle of… | text | | 114 | +----------------+--------+-------+"; 115 | println!("{expected}"); 116 | assert_table_line_width(&table, 35); 117 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 118 | } 119 | 120 | #[test] 121 | fn table_with_composite_utf8_strings() { 122 | let mut table = Table::new(); 123 | 124 | table 125 | .set_header(vec!["Header1"]) 126 | .set_width(20) 127 | .add_row(vec!["あいうえおかきくけこさしすせそたちつてと"]) 128 | .set_content_arrangement(comfy_table::ContentArrangement::Dynamic); 129 | 130 | for row in table.row_iter_mut() { 131 | row.max_height(1); // 2 -> also panics, 3 -> ok 132 | } 133 | 134 | println!("{table}"); 135 | let expected = " 136 | +------------------+ 137 | | Header1 | 138 | +==================+ 139 | | あいうえおか... | 140 | +------------------+"; 141 | println!("{expected}"); 142 | assert_table_line_width(&table, 20); 143 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 144 | } 145 | 146 | #[test] 147 | fn table_with_composite_utf8_strings_2_lines() { 148 | let mut table = Table::new(); 149 | 150 | table 151 | .set_header(vec!["Header1"]) 152 | .set_width(20) 153 | .add_row(vec!["あいうえおかきくけこさしすせそたちつてと"]) 154 | .set_content_arrangement(comfy_table::ContentArrangement::Dynamic); 155 | 156 | for row in table.row_iter_mut() { 157 | row.max_height(2); 158 | } 159 | 160 | println!("{table}"); 161 | let expected = " 162 | +------------------+ 163 | | Header1 | 164 | +==================+ 165 | | あいうえおかきく | 166 | | けこさしすせ... | 167 | +------------------+"; 168 | println!("{expected}"); 169 | assert_table_line_width(&table, 20); 170 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 171 | } 172 | 173 | #[test] 174 | fn table_with_composite_utf8_emojis() { 175 | let mut table = Table::new(); 176 | 177 | table 178 | .set_header(vec!["Header1"]) 179 | .set_width(15) 180 | .add_row(vec![ 181 | "🙂‍↕️🙂‍↕️🙂‍↕️🙂‍↕️🙂‍↕️🙂‍↕️last_line.into_bytes().truncate(truncate_at)", 182 | ]) 183 | .set_content_arrangement(comfy_table::ContentArrangement::Dynamic); 184 | 185 | for row in table.row_iter_mut() { 186 | row.max_height(1); // 2 -> also panics, 3 -> ok 187 | } 188 | 189 | println!("{table}"); 190 | let expected = " 191 | +-------------+ 192 | | Header1 | 193 | +=============+ 194 | | 🙂‍↕️🙂‍↕️🙂‍↕️🙂‍↕️... | 195 | +-------------+"; 196 | println!("{expected}"); 197 | assert_table_line_width(&table, 15); 198 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 199 | } 200 | -------------------------------------------------------------------------------- /src/utils/formatting/content_split/custom_styling.rs: -------------------------------------------------------------------------------- 1 | use ansi_str::AnsiStr; 2 | use unicode_segmentation::UnicodeSegmentation; 3 | use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; 4 | 5 | const ANSI_RESET: &str = "\u{1b}[0m"; 6 | 7 | /// Returns printed length of string, takes into account escape codes 8 | #[inline(always)] 9 | pub fn measure_text_width(s: &str) -> usize { 10 | s.ansi_strip().width() 11 | } 12 | 13 | /// Split the line by the given deliminator without breaking ansi codes that contain the delimiter 14 | pub fn split_line_by_delimiter(line: &str, delimiter: char) -> Vec { 15 | let mut lines: Vec = Vec::new(); 16 | let mut current_line = String::default(); 17 | 18 | // Iterate over line, splitting text with delimiter 19 | let iter = console::AnsiCodeIterator::new(line); 20 | for (str_slice, is_esc) in iter { 21 | if is_esc { 22 | current_line.push_str(str_slice); 23 | } else { 24 | let mut split = str_slice.split(delimiter); 25 | 26 | // Text before first delimiter (if any) belongs to previous line 27 | let first = split 28 | .next() 29 | .expect("split always produces at least one value"); 30 | current_line.push_str(first); 31 | 32 | // Text after each delimiter goes to new line. 33 | for text in split { 34 | lines.push(current_line); 35 | current_line = text.to_string(); 36 | } 37 | } 38 | } 39 | lines.push(current_line); 40 | fix_style_in_split_str(lines.as_mut()); 41 | lines 42 | } 43 | 44 | /// Splits a long word at a given character width. Inserting the needed ansi codes to preserve style. 45 | pub fn split_long_word(allowed_width: usize, word: &str) -> (String, String) { 46 | // A buffer for the first half of the split str, which will take up at most `allowed_len` characters when printed to the terminal. 47 | let mut head = String::with_capacity(word.len()); 48 | // A buffer for the second half of the split str 49 | let mut tail = String::with_capacity(word.len()); 50 | // Tracks the len() of head 51 | let mut head_len = 0; 52 | // Tracks the len() of head, sans trailing ansi escape codes 53 | let mut head_len_last = 0; 54 | // Count of *non-trailing* escape codes in the buffer. 55 | let mut escape_count_last = 0; 56 | // A buffer for the escape codes that exist in the str before the split. 57 | let mut escapes = Vec::new(); 58 | 59 | // Iterate over segments of the input string, each segment is either a singe escape code or block of text containing no escape codes. 60 | // Add text and escape codes to the head buffer, keeping track of printable length and what ansi codes are active, until there is no more room in allowed_width. 61 | // 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. 62 | let mut iter = console::AnsiCodeIterator::new(word); 63 | for (str_slice, is_esc) in iter.by_ref() { 64 | if is_esc { 65 | escapes.push(str_slice); 66 | // If the code is reset, that means all current codes in the buffer can be ignored. 67 | if str_slice == ANSI_RESET { 68 | escapes.clear(); 69 | } 70 | } 71 | 72 | let slice_len = match is_esc { 73 | true => 0, 74 | false => str_slice.width(), 75 | }; 76 | 77 | if head_len + slice_len <= allowed_width { 78 | head.push_str(str_slice); 79 | head_len += slice_len; 80 | 81 | if !is_esc { 82 | // allows popping unneeded escape codes later 83 | head_len_last = head.len(); 84 | escape_count_last = escapes.len(); 85 | } 86 | } else { 87 | assert!(!is_esc); 88 | let mut graphmes = str_slice.graphemes(true).peekable(); 89 | while let Some(c) = graphmes.peek() { 90 | let character_width = c.width(); 91 | if allowed_width < head_len + character_width { 92 | break; 93 | } 94 | 95 | head_len += character_width; 96 | let c = graphmes.next().unwrap(); 97 | head.push_str(c); 98 | 99 | // c is not escape code 100 | head_len_last = head.len(); 101 | escape_count_last = escapes.len(); 102 | } 103 | 104 | // cut off dangling escape codes since they should have no effect 105 | head.truncate(head_len_last); 106 | if escape_count_last != 0 { 107 | head.push_str(ANSI_RESET); 108 | } 109 | 110 | for esc in escapes { 111 | tail.push_str(esc); 112 | } 113 | let remaining: String = graphmes.collect(); 114 | tail.push_str(&remaining); 115 | break; 116 | } 117 | } 118 | 119 | iter.for_each(|s| tail.push_str(s.0)); 120 | (head, tail) 121 | } 122 | 123 | /// Fixes ansi escape codes in a split string 124 | /// 1. Adds reset code to the end of each substring if needed. 125 | /// 2. Keeps track of previous substring's escape codes and inserts them in later substrings to continue style 126 | pub fn fix_style_in_split_str(words: &mut [String]) { 127 | let mut escapes: Vec = Vec::new(); 128 | 129 | for word in words { 130 | // before we modify the escape list, make a copy 131 | let prepend = if !escapes.is_empty() { 132 | Some(escapes.join("")) 133 | } else { 134 | None 135 | }; 136 | 137 | // add escapes in word to escape list 138 | let iter = console::AnsiCodeIterator::new(word) 139 | .filter(|(_, is_esc)| *is_esc) 140 | .map(|v| v.0); 141 | for esc in iter { 142 | if esc == ANSI_RESET { 143 | escapes.clear() 144 | } else { 145 | escapes.push(esc.to_string()) 146 | } 147 | } 148 | 149 | // insert previous esc sequences at the beginning of the segment 150 | if let Some(prepend) = prepend { 151 | word.insert_str(0, &prepend); 152 | } 153 | 154 | // if there are active escape sequences, we need to append reset 155 | if !escapes.is_empty() { 156 | word.push_str(ANSI_RESET); 157 | } 158 | } 159 | } 160 | 161 | #[cfg(test)] 162 | mod test { 163 | #[test] 164 | fn ansi_aware_split_test() { 165 | use super::split_line_by_delimiter; 166 | 167 | let text = "\u{1b}[1m head [ middle [ tail \u{1b}[0m[ after"; 168 | let split = split_line_by_delimiter(text, '['); 169 | 170 | assert_eq!( 171 | split, 172 | [ 173 | "\u{1b}[1m head \u{1b}[0m", 174 | "\u{1b}[1m middle \u{1b}[0m", 175 | "\u{1b}[1m tail \u{1b}[0m", 176 | " after" 177 | ] 178 | ) 179 | } 180 | 181 | // TODO: Figure out why this fails with the custom_styling feature enabled. 182 | #[test] 183 | #[cfg(not(feature = "custom_styling"))] 184 | fn measure_text_width_osc8_test() { 185 | use super::measure_text_width; 186 | use unicode_width::UnicodeWidthStr; 187 | 188 | let text = "\x1b]8;;https://github.com\x1b\\This is a link\x1b]8;;\x1b"; 189 | let width = measure_text_width(text); 190 | 191 | assert_eq!(text.width(), 41); 192 | assert_eq!(width, 14); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/utils/formatting/content_split/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::ColumnDisplayInfo; 2 | 3 | #[cfg(feature = "custom_styling")] 4 | mod custom_styling; 5 | #[cfg(not(feature = "custom_styling"))] 6 | mod normal; 7 | 8 | #[cfg(feature = "custom_styling")] 9 | pub use custom_styling::*; 10 | #[cfg(not(feature = "custom_styling"))] 11 | pub use normal::*; 12 | 13 | /// Split a line if it's longer than the allowed columns (width - padding). 14 | /// 15 | /// This function tries to do this in a smart way, by splitting the content 16 | /// with a given delimiter at the very beginning. 17 | /// These "elements" then get added one-by-one to the lines, until a line is full. 18 | /// As soon as the line is full, we add it to the result set and start a new line. 19 | /// 20 | /// This is repeated until there are no more "elements". 21 | /// 22 | /// Mid-element splits only occurs if an element doesn't fit in a single line by itself. 23 | pub fn split_line(line: &str, info: &ColumnDisplayInfo, delimiter: char) -> Vec { 24 | let mut lines = Vec::new(); 25 | let content_width = usize::from(info.content_width); 26 | 27 | // Split the line by the given deliminator and turn the content into a stack. 28 | // Also clone it and convert it into a Vec. Otherwise, we get some burrowing problems 29 | // due to early drops of borrowed values that need to be inserted into `Vec<&str>` 30 | let mut elements = split_line_by_delimiter(line, delimiter); 31 | 32 | // Reverse it, since we want to push/pop without reversing the text. 33 | elements.reverse(); 34 | 35 | let mut current_line = String::new(); 36 | while let Some(next) = elements.pop() { 37 | let current_length = measure_text_width(¤t_line); 38 | let next_length = measure_text_width(&next); 39 | 40 | // Some helper variables 41 | // The length of the current line when combining it with the next element 42 | // Add 1 for the delimiter if we are on a non-empty line. 43 | let mut added_length = next_length + current_length; 44 | if !current_line.is_empty() { 45 | added_length += 1; 46 | } 47 | // The remaining width for this column. If we are on a non-empty line, subtract 1 for the delimiter. 48 | let mut remaining_width = content_width - current_length; 49 | if !current_line.is_empty() { 50 | remaining_width = remaining_width.saturating_sub(1); 51 | } 52 | 53 | // The next element fits into the current line 54 | if added_length <= content_width { 55 | // Only add delimiter, if we're not on a fresh line 56 | if !current_line.is_empty() { 57 | current_line.push(delimiter); 58 | } 59 | current_line += &next; 60 | 61 | // Already complete the current line, if there isn't space for more than two chars 62 | current_line = check_if_full(&mut lines, content_width, current_line); 63 | continue; 64 | } 65 | 66 | // The next element doesn't fit in the current line 67 | 68 | // Check, if there's enough space in the current line in case we decide to split the 69 | // element and only append a part of it to the current line. 70 | // If there isn't enough space, we simply push the current line, put the element back 71 | // on stack and start with a fresh line. 72 | if !current_line.is_empty() && remaining_width <= MIN_FREE_CHARS { 73 | elements.push(next); 74 | lines.push(current_line); 75 | current_line = String::new(); 76 | 77 | continue; 78 | } 79 | 80 | // Ok. There's still enough space to fit something in (more than MIN_FREE_CHARS characters) 81 | // There are two scenarios: 82 | // 83 | // 1. The word is too long for a single line. 84 | // In this case, we have to split the element anyway. Let's fill the remaining space on 85 | // the current line with, start a new line and push the remaining part on the stack. 86 | // 2. The word is short enough to fit as a whole into a line 87 | // In that case we simply push the current line and start a new one with the current element 88 | 89 | // Case 1 90 | // The element is longer than the specified content_width 91 | // Split the word, push the remaining string back on the stack 92 | if next_length > content_width { 93 | let new_line = current_line.is_empty(); 94 | 95 | // Only add delimiter, if we're not on a fresh line 96 | if !new_line { 97 | current_line.push(delimiter); 98 | } 99 | 100 | let (mut next, mut remaining) = split_long_word(remaining_width, &next); 101 | 102 | // This is an ugly hack, but it's needed for now. 103 | // 104 | // Scenario: The current column has to have a width of 1, and we work with a new line. 105 | // However, the next char is a multi-character UTF-8 symbol. 106 | // 107 | // Since a multi-character wide symbol doesn't fit into a 1-character column, 108 | // this code would loop endlessly. (There's no legitimate way to split that character.) 109 | // Hence, we have to live with the fact, that this line will look broken, as we put a 110 | // two-character wide symbol into it, despite the line being formatted for 1 character. 111 | if new_line && next.is_empty() { 112 | let mut chars = remaining.chars(); 113 | next.push(chars.next().unwrap()); 114 | remaining = chars.collect(); 115 | } 116 | 117 | current_line += &next; 118 | elements.push(remaining); 119 | 120 | // Push the finished line, and start a new one 121 | lines.push(current_line); 122 | current_line = String::new(); 123 | 124 | continue; 125 | } 126 | 127 | // Case 2 128 | // The element fits into a single line. 129 | // Push the current line and initialize the next line with the element. 130 | lines.push(current_line); 131 | current_line = next.to_string(); 132 | current_line = check_if_full(&mut lines, content_width, current_line); 133 | } 134 | 135 | if !current_line.is_empty() { 136 | lines.push(current_line); 137 | } 138 | 139 | lines 140 | } 141 | 142 | /// This is the minimum of available characters per line. 143 | /// It's used to check, whether another element can be added to the current line. 144 | /// Otherwise, the line will simply be left as it is, and we start with a new one. 145 | /// Two chars seems like a reasonable approach, since this would require next element to be 146 | /// a single char + delimiter. 147 | const MIN_FREE_CHARS: usize = 2; 148 | 149 | /// Check if the current line is too long and whether we should start a new one 150 | /// If it's too long, we add the current line to the list of lines and return a new [String]. 151 | /// Otherwise, we simply return the current line and basically don't do anything. 152 | fn check_if_full(lines: &mut Vec, content_width: usize, current_line: String) -> String { 153 | // Already complete the current line, if there isn't space for more than two chars 154 | if measure_text_width(¤t_line) > content_width.saturating_sub(MIN_FREE_CHARS) { 155 | lines.push(current_line); 156 | return String::new(); 157 | } 158 | 159 | current_line 160 | } 161 | 162 | #[cfg(test)] 163 | mod tests { 164 | use super::*; 165 | use unicode_width::UnicodeWidthStr; 166 | 167 | #[test] 168 | fn test_split_long_word() { 169 | let emoji = "🙂‍↕️"; // U+1F642 U+200D U+2195 U+FE0F head shaking vertically 170 | assert_eq!(emoji.len(), 13); 171 | assert_eq!(emoji.chars().count(), 4); 172 | assert_eq!(emoji.width(), 2); 173 | 174 | let (word, remaining) = split_long_word(emoji.width(), emoji); 175 | 176 | assert_eq!(word, "\u{1F642}\u{200D}\u{2195}\u{FE0F}"); 177 | assert_eq!(word.len(), 13); 178 | assert_eq!(word.chars().count(), 4); 179 | assert_eq!(word.width(), 2); 180 | 181 | assert!(remaining.is_empty()); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /tests/all/add_predicate.rs: -------------------------------------------------------------------------------- 1 | use pretty_assertions::assert_eq; 2 | 3 | use comfy_table::*; 4 | 5 | #[test] 6 | fn add_predicate_single_true() { 7 | let mut table = Table::new(); 8 | table 9 | .set_header(vec!["Header1", "Header2", "Header3"]) 10 | .add_row(vec![ 11 | "This is a text", 12 | "This is another text", 13 | "This is the third text", 14 | ]) 15 | .add_row_if( 16 | |_, _| true, 17 | &vec![ 18 | "This is another text", 19 | "Now\nadd some\nmulti line stuff", 20 | "This is awesome", 21 | ], 22 | ); 23 | 24 | println!("{table}"); 25 | let expected = " 26 | +----------------------+----------------------+------------------------+ 27 | | Header1 | Header2 | Header3 | 28 | +======================================================================+ 29 | | This is a text | This is another text | This is the third text | 30 | |----------------------+----------------------+------------------------| 31 | | This is another text | Now | This is awesome | 32 | | | add some | | 33 | | | multi line stuff | | 34 | +----------------------+----------------------+------------------------+"; 35 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 36 | } 37 | 38 | #[test] 39 | fn add_predicate_single_false() { 40 | let mut table = Table::new(); 41 | table 42 | .set_header(vec!["Header1", "Header2", "Header3"]) 43 | .add_row(vec![ 44 | "This is a text", 45 | "This is another text", 46 | "This is the third text", 47 | ]) 48 | .add_row_if( 49 | |_, _| false, 50 | &vec![ 51 | "This is another text", 52 | "Now\nadd some\nmulti line stuff", 53 | "This is awesome", 54 | ], 55 | ); 56 | 57 | println!("{table}"); 58 | let expected = " 59 | +----------------+----------------------+------------------------+ 60 | | Header1 | Header2 | Header3 | 61 | +================================================================+ 62 | | This is a text | This is another text | This is the third text | 63 | +----------------+----------------------+------------------------+"; 64 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 65 | } 66 | 67 | #[test] 68 | fn add_predicate_single_mixed() { 69 | let mut table = Table::new(); 70 | table 71 | .set_header(vec!["Header1", "Header2", "Header3"]) 72 | .add_row(vec![ 73 | "This is a text", 74 | "This is another text", 75 | "This is the third text", 76 | ]) 77 | .add_row_if( 78 | |_, _| false, 79 | &vec!["I won't get displayed", "Me neither", "Same here!"], 80 | ) 81 | .add_row_if( 82 | |_, _| true, 83 | &vec![ 84 | "This is another text", 85 | "Now\nadd some\nmulti line stuff", 86 | "This is awesome", 87 | ], 88 | ); 89 | 90 | println!("{table}"); 91 | let expected = " 92 | +----------------------+----------------------+------------------------+ 93 | | Header1 | Header2 | Header3 | 94 | +======================================================================+ 95 | | This is a text | This is another text | This is the third text | 96 | |----------------------+----------------------+------------------------| 97 | | This is another text | Now | This is awesome | 98 | | | add some | | 99 | | | multi line stuff | | 100 | +----------------------+----------------------+------------------------+"; 101 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 102 | } 103 | 104 | #[test] 105 | fn add_predicate_single_wrong_row_count() { 106 | let mut table = Table::new(); 107 | table 108 | .set_header(vec!["Header1", "Header2", "Header3"]) 109 | .add_row(vec![ 110 | "This is a text", 111 | "This is another text", 112 | "This is the third text", 113 | ]) 114 | .add_row_if( 115 | |_, row| row.len() == 2, 116 | &vec![ 117 | "This is another text", 118 | "Now\nadd some\nmulti line stuff", 119 | "This is awesome", 120 | ], 121 | ); 122 | 123 | println!("{table}"); 124 | let expected = " 125 | +----------------+----------------------+------------------------+ 126 | | Header1 | Header2 | Header3 | 127 | +================================================================+ 128 | | This is a text | This is another text | This is the third text | 129 | +----------------+----------------------+------------------------+"; 130 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 131 | } 132 | 133 | #[test] 134 | fn add_predicate_multi_true() { 135 | let mut table = Table::new(); 136 | let rows = vec![ 137 | Row::from(&vec![ 138 | "This is a text", 139 | "This is another text", 140 | "This is the third text", 141 | ]), 142 | Row::from(&vec![ 143 | "This is another text", 144 | "Now\nadd some\nmulti line stuff", 145 | "This is awesome", 146 | ]), 147 | ]; 148 | 149 | table 150 | .set_header(vec!["Header1", "Header2", "Header3"]) 151 | .add_rows_if(|_, _| true, rows); 152 | 153 | println!("{table}"); 154 | let expected = " 155 | +----------------------+----------------------+------------------------+ 156 | | Header1 | Header2 | Header3 | 157 | +======================================================================+ 158 | | This is a text | This is another text | This is the third text | 159 | |----------------------+----------------------+------------------------| 160 | | This is another text | Now | This is awesome | 161 | | | add some | | 162 | | | multi line stuff | | 163 | +----------------------+----------------------+------------------------+"; 164 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 165 | } 166 | 167 | #[test] 168 | fn add_predicate_multi_false() { 169 | let mut table = Table::new(); 170 | table 171 | .set_header(vec!["Header1", "Header2", "Header3"]) 172 | .add_row(vec![ 173 | "This is a text", 174 | "This is another text", 175 | "This is the third text", 176 | ]) 177 | .add_rows_if( 178 | |_, _| false, 179 | vec![Row::from(&vec![ 180 | "This is another text", 181 | "Now\nadd some\nmulti line stuff", 182 | "This is awesome", 183 | ])], 184 | ); 185 | 186 | println!("{table}"); 187 | let expected = " 188 | +----------------+----------------------+------------------------+ 189 | | Header1 | Header2 | Header3 | 190 | +================================================================+ 191 | | This is a text | This is another text | This is the third text | 192 | +----------------+----------------------+------------------------+"; 193 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 194 | } 195 | 196 | #[test] 197 | fn add_predicate_multi_mixed() { 198 | let mut table = Table::new(); 199 | let rows = vec![ 200 | Row::from(&vec![ 201 | "This is a text", 202 | "This is another text", 203 | "This is the third text", 204 | ]), 205 | Row::from(&vec![ 206 | "This is another text", 207 | "Now\nadd some\nmulti line stuff", 208 | "This is awesome", 209 | ]), 210 | ]; 211 | 212 | table 213 | .set_header(vec!["Header1", "Header2", "Header3"]) 214 | .add_rows_if(|_, _| true, rows) 215 | .add_rows_if( 216 | |_, _| false, 217 | vec![Row::from(&vec![ 218 | "I won't get displayed", 219 | "Me neither", 220 | "Same here!", 221 | ])], 222 | ); 223 | 224 | println!("{table}"); 225 | let expected = " 226 | +----------------------+----------------------+------------------------+ 227 | | Header1 | Header2 | Header3 | 228 | +======================================================================+ 229 | | This is a text | This is another text | This is the third text | 230 | |----------------------+----------------------+------------------------| 231 | | This is another text | Now | This is awesome | 232 | | | add some | | 233 | | | multi line stuff | | 234 | +----------------------+----------------------+------------------------+"; 235 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 236 | } 237 | 238 | #[test] 239 | fn add_predicate_multi_wrong_rows_count() { 240 | let mut table = Table::new(); 241 | table 242 | .set_header(vec!["Header1", "Header2", "Header3"]) 243 | .add_row(vec![ 244 | "This is a text", 245 | "This is another text", 246 | "This is the third text", 247 | ]) 248 | .add_rows_if( 249 | |_, rows| rows.len() == 2, 250 | vec![Row::from(&vec![ 251 | "This is another text", 252 | "Now\nadd some\nmulti line stuff", 253 | "This is awesome", 254 | ])], 255 | ); 256 | 257 | println!("{table}"); 258 | let expected = " 259 | +----------------+----------------------+------------------------+ 260 | | Header1 | Header2 | Header3 | 261 | +================================================================+ 262 | | This is a text | This is another text | This is the third text | 263 | +----------------+----------------------+------------------------+"; 264 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 265 | } 266 | -------------------------------------------------------------------------------- /src/utils/formatting/borders.rs: -------------------------------------------------------------------------------- 1 | use crate::style::TableComponent; 2 | use crate::table::Table; 3 | use crate::utils::ColumnDisplayInfo; 4 | 5 | pub(crate) fn draw_borders( 6 | table: &Table, 7 | rows: &[Vec>], 8 | display_info: &[ColumnDisplayInfo], 9 | ) -> Vec { 10 | // We know how many lines there should be. Initialize the vector with the rough correct amount. 11 | // We might over allocate a bit, but that's better than under allocating. 12 | let mut lines = if let Some(capacity) = rows.first().map(|lines| lines.len()) { 13 | // Lines * 2 -> Lines + delimiters 14 | // + 5 -> header delimiters + header + bottom/top borders 15 | Vec::with_capacity(capacity * 2 + 5) 16 | } else { 17 | Vec::new() 18 | }; 19 | 20 | if should_draw_top_border(table) { 21 | lines.push(draw_top_border(table, display_info)); 22 | } 23 | 24 | draw_rows(&mut lines, rows, table, display_info); 25 | 26 | if should_draw_bottom_border(table) { 27 | lines.push(draw_bottom_border(table, display_info)); 28 | } 29 | 30 | lines 31 | } 32 | 33 | fn draw_top_border(table: &Table, display_info: &[ColumnDisplayInfo]) -> String { 34 | let left_corner = table.style_or_default(TableComponent::TopLeftCorner); 35 | let top_border = table.style_or_default(TableComponent::TopBorder); 36 | let intersection = table.style_or_default(TableComponent::TopBorderIntersections); 37 | let right_corner = table.style_or_default(TableComponent::TopRightCorner); 38 | 39 | let mut line = String::new(); 40 | // We only need the top left corner, if we need to draw a left border 41 | if should_draw_left_border(table) { 42 | line += &left_corner; 43 | } 44 | 45 | // Build the top border line depending on the columns' width. 46 | // Also add the border intersections. 47 | let mut first = true; 48 | for info in display_info.iter() { 49 | // Only add something, if the column isn't hidden 50 | if !info.is_hidden { 51 | if !first { 52 | line += &intersection; 53 | } 54 | line += &top_border.repeat(info.width().into()); 55 | first = false; 56 | } 57 | } 58 | 59 | // We only need the top right corner, if we need to draw a right border 60 | if should_draw_right_border(table) { 61 | line += &right_corner; 62 | } 63 | 64 | line 65 | } 66 | 67 | fn draw_rows( 68 | lines: &mut Vec, 69 | rows: &[Vec>], 70 | table: &Table, 71 | display_info: &[ColumnDisplayInfo], 72 | ) { 73 | // Iterate over all rows 74 | let mut row_iter = rows.iter().enumerate().peekable(); 75 | while let Some((row_index, row)) = row_iter.next() { 76 | // Concatenate the line parts and insert the vertical borders if needed 77 | for line_parts in row.iter() { 78 | lines.push(embed_line(line_parts, table)); 79 | } 80 | 81 | // Draw the horizontal header line if desired, otherwise continue to the next iteration 82 | if row_index == 0 && table.header.is_some() { 83 | if should_draw_header(table) { 84 | lines.push(draw_horizontal_lines(table, display_info, true)); 85 | } 86 | continue; 87 | } 88 | 89 | // Draw a horizontal line, if we desired and if we aren't in the last row of the table. 90 | if row_iter.peek().is_some() && should_draw_horizontal_lines(table) { 91 | lines.push(draw_horizontal_lines(table, display_info, false)); 92 | } 93 | } 94 | } 95 | 96 | // Takes the parts of a single line, surrounds them with borders and adds vertical lines. 97 | fn embed_line(line_parts: &[String], table: &Table) -> String { 98 | let vertical_lines = table.style_or_default(TableComponent::VerticalLines); 99 | let left_border = table.style_or_default(TableComponent::LeftBorder); 100 | let right_border = table.style_or_default(TableComponent::RightBorder); 101 | 102 | let mut line = String::new(); 103 | if should_draw_left_border(table) { 104 | line += &left_border; 105 | } 106 | 107 | let mut part_iter = line_parts.iter().peekable(); 108 | while let Some(part) = part_iter.next() { 109 | line += part; 110 | if should_draw_vertical_lines(table) && part_iter.peek().is_some() { 111 | line += &vertical_lines; 112 | } else if should_draw_right_border(table) && part_iter.peek().is_none() { 113 | line += &right_border; 114 | } 115 | } 116 | 117 | line 118 | } 119 | 120 | // The horizontal line that separates between rows. 121 | fn draw_horizontal_lines( 122 | table: &Table, 123 | display_info: &[ColumnDisplayInfo], 124 | header: bool, 125 | ) -> String { 126 | // Styling depends on whether we're currently on the header line or not. 127 | let (left_intersection, horizontal_lines, middle_intersection, right_intersection) = if header { 128 | ( 129 | table.style_or_default(TableComponent::LeftHeaderIntersection), 130 | table.style_or_default(TableComponent::HeaderLines), 131 | table.style_or_default(TableComponent::MiddleHeaderIntersections), 132 | table.style_or_default(TableComponent::RightHeaderIntersection), 133 | ) 134 | } else { 135 | ( 136 | table.style_or_default(TableComponent::LeftBorderIntersections), 137 | table.style_or_default(TableComponent::HorizontalLines), 138 | table.style_or_default(TableComponent::MiddleIntersections), 139 | table.style_or_default(TableComponent::RightBorderIntersections), 140 | ) 141 | }; 142 | 143 | let mut line = String::new(); 144 | // We only need the bottom left corner, if we need to draw a left border 145 | if should_draw_left_border(table) { 146 | line += &left_intersection; 147 | } 148 | 149 | // Append the middle lines depending on the columns' widths. 150 | // Also add the middle intersections. 151 | let mut first = true; 152 | for info in display_info.iter() { 153 | // Only add something, if the column isn't hidden 154 | if !info.is_hidden { 155 | if !first { 156 | line += &middle_intersection; 157 | } 158 | line += &horizontal_lines.repeat(info.width().into()); 159 | first = false; 160 | } 161 | } 162 | 163 | // We only need the bottom right corner, if we need to draw a right border 164 | if should_draw_right_border(table) { 165 | line += &right_intersection; 166 | } 167 | 168 | line 169 | } 170 | 171 | fn draw_bottom_border(table: &Table, display_info: &[ColumnDisplayInfo]) -> String { 172 | let left_corner = table.style_or_default(TableComponent::BottomLeftCorner); 173 | let bottom_border = table.style_or_default(TableComponent::BottomBorder); 174 | let middle_intersection = table.style_or_default(TableComponent::BottomBorderIntersections); 175 | let right_corner = table.style_or_default(TableComponent::BottomRightCorner); 176 | 177 | let mut line = String::new(); 178 | // We only need the bottom left corner, if we need to draw a left border 179 | if should_draw_left_border(table) { 180 | line += &left_corner; 181 | } 182 | 183 | // Add the bottom border lines depending on column width 184 | // Also add the border intersections. 185 | let mut first = true; 186 | for info in display_info.iter() { 187 | // Only add something, if the column isn't hidden 188 | if !info.is_hidden { 189 | if !first { 190 | line += &middle_intersection; 191 | } 192 | line += &bottom_border.repeat(info.width().into()); 193 | first = false; 194 | } 195 | } 196 | 197 | // We only need the bottom right corner, if we need to draw a right border 198 | if should_draw_right_border(table) { 199 | line += &right_corner; 200 | } 201 | 202 | line 203 | } 204 | 205 | fn should_draw_top_border(table: &Table) -> bool { 206 | if table.style_exists(TableComponent::TopLeftCorner) 207 | || table.style_exists(TableComponent::TopBorder) 208 | || table.style_exists(TableComponent::TopBorderIntersections) 209 | || table.style_exists(TableComponent::TopRightCorner) 210 | { 211 | return true; 212 | } 213 | 214 | false 215 | } 216 | 217 | fn should_draw_bottom_border(table: &Table) -> bool { 218 | if table.style_exists(TableComponent::BottomLeftCorner) 219 | || table.style_exists(TableComponent::BottomBorder) 220 | || table.style_exists(TableComponent::BottomBorderIntersections) 221 | || table.style_exists(TableComponent::BottomRightCorner) 222 | { 223 | return true; 224 | } 225 | 226 | false 227 | } 228 | 229 | pub fn should_draw_left_border(table: &Table) -> bool { 230 | if table.style_exists(TableComponent::TopLeftCorner) 231 | || table.style_exists(TableComponent::LeftBorder) 232 | || table.style_exists(TableComponent::LeftBorderIntersections) 233 | || table.style_exists(TableComponent::LeftHeaderIntersection) 234 | || table.style_exists(TableComponent::BottomLeftCorner) 235 | { 236 | return true; 237 | } 238 | 239 | false 240 | } 241 | 242 | pub fn should_draw_right_border(table: &Table) -> bool { 243 | if table.style_exists(TableComponent::TopRightCorner) 244 | || table.style_exists(TableComponent::RightBorder) 245 | || table.style_exists(TableComponent::RightBorderIntersections) 246 | || table.style_exists(TableComponent::RightHeaderIntersection) 247 | || table.style_exists(TableComponent::BottomRightCorner) 248 | { 249 | return true; 250 | } 251 | 252 | false 253 | } 254 | 255 | fn should_draw_horizontal_lines(table: &Table) -> bool { 256 | if table.style_exists(TableComponent::LeftBorderIntersections) 257 | || table.style_exists(TableComponent::HorizontalLines) 258 | || table.style_exists(TableComponent::MiddleIntersections) 259 | || table.style_exists(TableComponent::RightBorderIntersections) 260 | { 261 | return true; 262 | } 263 | 264 | false 265 | } 266 | 267 | pub fn should_draw_vertical_lines(table: &Table) -> bool { 268 | if table.style_exists(TableComponent::TopBorderIntersections) 269 | || table.style_exists(TableComponent::MiddleHeaderIntersections) 270 | || table.style_exists(TableComponent::VerticalLines) 271 | || table.style_exists(TableComponent::MiddleIntersections) 272 | || table.style_exists(TableComponent::BottomBorderIntersections) 273 | { 274 | return true; 275 | } 276 | 277 | false 278 | } 279 | 280 | fn should_draw_header(table: &Table) -> bool { 281 | if table.style_exists(TableComponent::LeftHeaderIntersection) 282 | || table.style_exists(TableComponent::HeaderLines) 283 | || table.style_exists(TableComponent::MiddleHeaderIntersections) 284 | || table.style_exists(TableComponent::RightHeaderIntersection) 285 | { 286 | return true; 287 | } 288 | 289 | false 290 | } 291 | -------------------------------------------------------------------------------- /tests/all/content_arrangement_test.rs: -------------------------------------------------------------------------------- 1 | use comfy_table::ColumnConstraint; 2 | use comfy_table::Width; 3 | use pretty_assertions::assert_eq; 4 | 5 | use comfy_table::{ContentArrangement, Table}; 6 | 7 | use super::assert_table_line_width; 8 | 9 | /// Test the robustness of the dynamic table arrangement. 10 | #[test] 11 | fn simple_dynamic_table() { 12 | let mut table = Table::new(); 13 | table.set_header(vec!["Header1", "Header2", "Head"]) 14 | .set_content_arrangement(ContentArrangement::Dynamic) 15 | .set_width(25) 16 | .add_row(vec![ 17 | "This is a very long line with a lot of text", 18 | "This is anotherverylongtextwithlongwords text", 19 | "smol", 20 | ]) 21 | .add_row(vec![ 22 | "This is another text", 23 | "Now let's\nadd a really long line in the middle of the cell \n and add more multi line stuff", 24 | "smol", 25 | ]); 26 | 27 | println!("{table}"); 28 | let expected = " 29 | +--------+-------+------+ 30 | | Header | Heade | Head | 31 | | 1 | r2 | | 32 | +=======================+ 33 | | This | This | smol | 34 | | is a | is | | 35 | | very | anoth | | 36 | | long | erver | | 37 | | line | ylong | | 38 | | with a | textw | | 39 | | lot of | ithlo | | 40 | | text | ngwor | | 41 | | | ds | | 42 | | | text | | 43 | |--------+-------+------| 44 | | This | Now | smol | 45 | | is ano | let's | | 46 | | ther | add a | | 47 | | text | reall | | 48 | | | y | | 49 | | | long | | 50 | | | line | | 51 | | | in | | 52 | | | the | | 53 | | | middl | | 54 | | | e of | | 55 | | | the | | 56 | | | cell | | 57 | | | and | | 58 | | | add | | 59 | | | more | | 60 | | | multi | | 61 | | | line | | 62 | | | stuff | | 63 | +--------+-------+------+"; 64 | println!("{expected}"); 65 | assert_table_line_width(&table, 25); 66 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 67 | } 68 | 69 | /// This table checks the scenario, where a column has a big max_width, but a lot of the assigned 70 | /// space doesn't get used after splitting the lines. This happens mostly when there are 71 | /// many long words in a single column. 72 | /// The remaining space should rather be distributed to other cells. 73 | #[test] 74 | fn distribute_space_after_split() { 75 | let mut table = Table::new(); 76 | table 77 | .set_header(vec!["Header1", "Header2", "Head"]) 78 | .set_content_arrangement(ContentArrangement::Dynamic) 79 | .set_width(80) 80 | .add_row(vec![ 81 | "This is a very long line with a lot of text", 82 | "This is text with a anotherverylongtexttesttest", 83 | "smol", 84 | ]); 85 | 86 | println!("{table}"); 87 | let expected = " 88 | +-----------------------------------------+-----------------------------+------+ 89 | | Header1 | Header2 | Head | 90 | +==============================================================================+ 91 | | This is a very long line with a lot of | This is text with a | smol | 92 | | text | anotherverylongtexttesttest | | 93 | +-----------------------------------------+-----------------------------+------+"; 94 | println!("{expected}"); 95 | 96 | assert_table_line_width(&table, 80); 97 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 98 | } 99 | 100 | /// A single column get's split and a lot of the available isn't used afterward. 101 | /// The remaining space should be cut away, making the table more compact. 102 | #[test] 103 | fn unused_space_after_split() { 104 | let mut table = Table::new(); 105 | table 106 | .set_header(vec!["Header1"]) 107 | .set_content_arrangement(ContentArrangement::Dynamic) 108 | .set_width(30) 109 | .add_row(vec!["This is text with a anotherverylongtext"]); 110 | 111 | println!("{table}"); 112 | let expected = " 113 | +---------------------+ 114 | | Header1 | 115 | +=====================+ 116 | | This is text with a | 117 | | anotherverylongtext | 118 | +---------------------+"; 119 | println!("{expected}"); 120 | assert_table_line_width(&table, 23); 121 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 122 | } 123 | 124 | /// The full width of a table should be used, even if the space isn't used. 125 | #[test] 126 | fn dynamic_full_width_after_split() { 127 | let mut table = Table::new(); 128 | table 129 | .set_header(vec!["Header1"]) 130 | .set_content_arrangement(ContentArrangement::DynamicFullWidth) 131 | .set_width(50) 132 | .add_row(vec!["This is text with a anotherverylongtexttesttestaa"]); 133 | 134 | println!("{table}"); 135 | let expected = " 136 | +------------------------------------------------+ 137 | | Header1 | 138 | +================================================+ 139 | | This is text with a | 140 | | anotherverylongtexttesttestaa | 141 | +------------------------------------------------+"; 142 | println!("{expected}"); 143 | assert_table_line_width(&table, 50); 144 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 145 | } 146 | 147 | /// This table checks the scenario, where a column has a big max_width, but a lot of the assigned 148 | /// space isn't used after splitting the lines. 149 | /// The remaining space should rather distributed between all cells. 150 | #[test] 151 | fn dynamic_full_width() { 152 | let mut table = Table::new(); 153 | table 154 | .set_header(vec!["Header1", "Header2", "smol"]) 155 | .set_content_arrangement(ContentArrangement::DynamicFullWidth) 156 | .set_width(80) 157 | .add_row(vec!["This is a short line", "small", "smol"]); 158 | 159 | println!("{table}"); 160 | let expected = " 161 | +-----------------------------------+----------------------+-------------------+ 162 | | Header1 | Header2 | smol | 163 | +==============================================================================+ 164 | | This is a short line | small | smol | 165 | +-----------------------------------+----------------------+-------------------+"; 166 | println!("{expected}"); 167 | assert_table_line_width(&table, 80); 168 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 169 | } 170 | 171 | /// Test that a table is displayed in its full width, if the `table.width` is set to the exact 172 | /// width the table has, if it's fully expanded. 173 | /// 174 | /// The same should be the case for values that are larger than this width. 175 | #[test] 176 | fn dynamic_exact_width() { 177 | let header = vec!["a\n---\ni64", "b\n---\ni64", "b_squared\n---\nf64"]; 178 | let rows = vec![ 179 | vec!["1", "2", "4.0"], 180 | vec!["3", "4", "16.0"], 181 | vec!["5", "6", "36.0"], 182 | ]; 183 | 184 | for width in 25..40 { 185 | let mut table = Table::new(); 186 | let table = table 187 | .load_preset(comfy_table::presets::UTF8_FULL) 188 | .set_content_arrangement(ContentArrangement::Dynamic) 189 | .set_width(width); 190 | 191 | table.set_header(header.clone()).add_rows(rows.clone()); 192 | 193 | println!("{table}"); 194 | let expected = " 195 | ┌─────┬─────┬───────────┐ 196 | │ a ┆ b ┆ b_squared │ 197 | │ --- ┆ --- ┆ --- │ 198 | │ i64 ┆ i64 ┆ f64 │ 199 | ╞═════╪═════╪═══════════╡ 200 | │ 1 ┆ 2 ┆ 4.0 │ 201 | ├╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤ 202 | │ 3 ┆ 4 ┆ 16.0 │ 203 | ├╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤ 204 | │ 5 ┆ 6 ┆ 36.0 │ 205 | └─────┴─────┴───────────┘"; 206 | println!("{expected}"); 207 | assert_table_line_width(table, 25); 208 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 209 | } 210 | } 211 | 212 | /// Test that the formatting works as expected, if the table is slightly smaller than the max width 213 | /// of the table. 214 | #[test] 215 | fn dynamic_slightly_smaller() { 216 | let header = vec!["a\n---\ni64", "b\n---\ni64", "b_squared\n---\nf64"]; 217 | let rows = vec![ 218 | vec!["1", "2", "4.0"], 219 | vec!["3", "4", "16.0"], 220 | vec!["5", "6", "36.0"], 221 | ]; 222 | 223 | let mut table = Table::new(); 224 | let table = table 225 | .load_preset(comfy_table::presets::UTF8_FULL) 226 | .set_content_arrangement(ContentArrangement::Dynamic) 227 | .set_width(24); 228 | 229 | table.set_header(header.clone()).add_rows(rows.clone()); 230 | 231 | println!("{table}"); 232 | let expected = " 233 | ┌─────┬─────┬──────────┐ 234 | │ a ┆ b ┆ b_square │ 235 | │ --- ┆ --- ┆ d │ 236 | │ i64 ┆ i64 ┆ --- │ 237 | │ ┆ ┆ f64 │ 238 | ╞═════╪═════╪══════════╡ 239 | │ 1 ┆ 2 ┆ 4.0 │ 240 | ├╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤ 241 | │ 3 ┆ 4 ┆ 16.0 │ 242 | ├╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤ 243 | │ 5 ┆ 6 ┆ 36.0 │ 244 | └─────┴─────┴──────────┘"; 245 | println!("{expected}"); 246 | assert_table_line_width(table, 24); 247 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 248 | } 249 | 250 | /// This failed on a python integration test case in the polars project. 251 | /// This a regression test. 252 | #[test] 253 | fn polar_python_test_tbl_width_chars() { 254 | let header = vec![ 255 | "a really long col\n---\ni64", 256 | "b\n---\nstr", 257 | "this is 10\n---\ni64", 258 | ]; 259 | let rows = vec![ 260 | vec!["1", "", "4"], 261 | vec!["2", "this is a string value that will...", "5"], 262 | vec!["3", "null", "6"], 263 | ]; 264 | 265 | let mut table = Table::new(); 266 | let table = table 267 | .load_preset(comfy_table::presets::UTF8_FULL) 268 | .set_content_arrangement(ContentArrangement::Dynamic) 269 | .set_width(100) 270 | .set_header(header) 271 | .add_rows(rows) 272 | .set_constraints(vec![ 273 | ColumnConstraint::LowerBoundary(Width::Fixed(12)), 274 | ColumnConstraint::LowerBoundary(Width::Fixed(5)), 275 | ColumnConstraint::LowerBoundary(Width::Fixed(10)), 276 | ]); 277 | 278 | println!("{table}"); 279 | let expected = " 280 | ┌───────────────────┬─────────────────────────────────────┬────────────┐ 281 | │ a really long col ┆ b ┆ this is 10 │ 282 | │ --- ┆ --- ┆ --- │ 283 | │ i64 ┆ str ┆ i64 │ 284 | ╞═══════════════════╪═════════════════════════════════════╪════════════╡ 285 | │ 1 ┆ ┆ 4 │ 286 | ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┤ 287 | │ 2 ┆ this is a string value that will... ┆ 5 │ 288 | ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┤ 289 | │ 3 ┆ null ┆ 6 │ 290 | └───────────────────┴─────────────────────────────────────┴────────────┘"; 291 | println!("{expected}"); 292 | assert_table_line_width(table, 72); 293 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 294 | } 295 | -------------------------------------------------------------------------------- /tests/all/constraints_test.rs: -------------------------------------------------------------------------------- 1 | use comfy_table::ColumnConstraint::*; 2 | use comfy_table::Width::*; 3 | use comfy_table::*; 4 | use pretty_assertions::assert_eq; 5 | 6 | use super::assert_table_line_width; 7 | 8 | fn get_constraint_table() -> Table { 9 | let mut table = Table::new(); 10 | table 11 | .set_header(vec!["smol", "Header2", "Header3"]) 12 | .add_row(vec![ 13 | "smol", 14 | "This is another text", 15 | "This is the third text", 16 | ]) 17 | .add_row(vec![ 18 | "smol", 19 | "Now\nadd some\nmulti line stuff", 20 | "This is awesome", 21 | ]); 22 | 23 | table 24 | } 25 | 26 | #[test] 27 | /// Ensure max-, min- and fixed-width constraints are respected 28 | fn fixed_max_min_constraints() { 29 | let mut table = get_constraint_table(); 30 | 31 | table.set_constraints(vec![ 32 | LowerBoundary(Fixed(10)), 33 | UpperBoundary(Fixed(8)), 34 | Absolute(Fixed(10)), 35 | ]); 36 | 37 | println!("{table}"); 38 | let expected = " 39 | +----------+--------+----------+ 40 | | smol | Header | Header3 | 41 | | | 2 | | 42 | +==============================+ 43 | | smol | This | This is | 44 | | | is ano | the | 45 | | | ther | third | 46 | | | text | text | 47 | |----------+--------+----------| 48 | | smol | Now | This is | 49 | | | add | awesome | 50 | | | some | | 51 | | | multi | | 52 | | | line | | 53 | | | stuff | | 54 | +----------+--------+----------+"; 55 | println!("{expected}"); 56 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 57 | 58 | // Now try this again when using dynamic content arrangement 59 | // The table tries to arrange to 28 characters, 60 | // but constraints enforce a width of at least 10+10+2+1+4 = 27 61 | // min_width + max_width + middle_padding + middle_min_width + borders 62 | // Since the left and right column are fixed, the middle column should only get a width of 2 63 | table 64 | .set_content_arrangement(ContentArrangement::Dynamic) 65 | .set_width(28); 66 | 67 | println!("{table}"); 68 | let expected = " 69 | +----------+----+----------+ 70 | | smol | He | Header3 | 71 | | | ad | | 72 | | | er | | 73 | | | 2 | | 74 | +==========================+ 75 | | smol | Th | This is | 76 | | | is | the | 77 | | | is | third | 78 | | | an | text | 79 | | | ot | | 80 | | | he | | 81 | | | r | | 82 | | | te | | 83 | | | xt | | 84 | |----------+----+----------| 85 | | smol | No | This is | 86 | | | w | awesome | 87 | | | ad | | 88 | | | d | | 89 | | | so | | 90 | | | me | | 91 | | | mu | | 92 | | | lt | | 93 | | | i | | 94 | | | li | | 95 | | | ne | | 96 | | | st | | 97 | | | uf | | 98 | | | f | | 99 | +----------+----+----------+"; 100 | println!("{expected}"); 101 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 102 | } 103 | 104 | #[test] 105 | /// Max and Min constraints won't be considered, if they are unnecessary 106 | /// This is true for normal and dynamic arrangement tables. 107 | fn unnecessary_max_min_constraints() { 108 | let mut table = get_constraint_table(); 109 | 110 | table 111 | .set_width(80) 112 | .set_constraints(vec![LowerBoundary(Fixed(1)), UpperBoundary(Fixed(30))]); 113 | 114 | println!("{table}"); 115 | let expected = " 116 | +------+----------------------+------------------------+ 117 | | smol | Header2 | Header3 | 118 | +======================================================+ 119 | | smol | This is another text | This is the third text | 120 | |------+----------------------+------------------------| 121 | | smol | Now | This is awesome | 122 | | | add some | | 123 | | | multi line stuff | | 124 | +------+----------------------+------------------------+"; 125 | println!("{expected}"); 126 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 127 | 128 | // Now test for dynamic content arrangement 129 | table.set_content_arrangement(ContentArrangement::Dynamic); 130 | println!("{table}"); 131 | let expected = " 132 | +------+----------------------+------------------------+ 133 | | smol | Header2 | Header3 | 134 | +======================================================+ 135 | | smol | This is another text | This is the third text | 136 | |------+----------------------+------------------------| 137 | | smol | Now | This is awesome | 138 | | | add some | | 139 | | | multi line stuff | | 140 | +------+----------------------+------------------------+"; 141 | println!("{expected}"); 142 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 143 | } 144 | 145 | #[test] 146 | /// The user can specify constraints that result in bigger width than actually provided 147 | /// This is allowed, but results in a wider table than actually aimed for. 148 | /// Anyway we still try to fit everything as good as possible, which of course breaks stuff. 149 | fn constraints_bigger_than_table_width() { 150 | let mut table = get_constraint_table(); 151 | 152 | table 153 | .set_content_arrangement(ContentArrangement::Dynamic) 154 | .set_width(28) 155 | .set_constraints(vec![ 156 | UpperBoundary(Fixed(50)), 157 | LowerBoundary(Fixed(30)), 158 | ContentWidth, 159 | ]); 160 | 161 | println!("{table}"); 162 | let expected = " 163 | +---+------------------------------+------------------------+ 164 | | s | Header2 | Header3 | 165 | | m | | | 166 | | o | | | 167 | | l | | | 168 | +===========================================================+ 169 | | s | This is another text | This is the third text | 170 | | m | | | 171 | | o | | | 172 | | l | | | 173 | |---+------------------------------+------------------------| 174 | | s | Now | This is awesome | 175 | | m | add some | | 176 | | o | multi line stuff | | 177 | | l | | | 178 | +---+------------------------------+------------------------+"; 179 | println!("{expected}"); 180 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 181 | } 182 | 183 | #[test] 184 | /// Test correct usage of the Percentage constraint. 185 | /// Percentage allows to set a fixed width. 186 | fn percentage() { 187 | let mut table = get_constraint_table(); 188 | 189 | // Set a percentage of 20% for the first column. 190 | // The the rest should arrange accordingly. 191 | table 192 | .set_content_arrangement(ContentArrangement::Dynamic) 193 | .set_width(40) 194 | .set_constraints(vec![Absolute(Percentage(20))]); 195 | 196 | println!("{table}"); 197 | let expected = " 198 | +-------+---------------+--------------+ 199 | | smol | Header2 | Header3 | 200 | +======================================+ 201 | | smol | This is | This is the | 202 | | | another text | third text | 203 | |-------+---------------+--------------| 204 | | smol | Now | This is | 205 | | | add some | awesome | 206 | | | multi line | | 207 | | | stuff | | 208 | +-------+---------------+--------------+"; 209 | println!("{expected}"); 210 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 211 | } 212 | 213 | #[test] 214 | /// A single percentage constraint should be 100% at most. 215 | fn max_100_percentage() { 216 | let mut table = Table::new(); 217 | table 218 | .set_header(vec!["smol"]) 219 | .add_row(vec!["smol"]) 220 | .set_content_arrangement(ContentArrangement::Dynamic) 221 | .set_width(40) 222 | .set_constraints(vec![Absolute(Percentage(200))]); 223 | 224 | println!("{table}"); 225 | let expected = " 226 | +--------------------------------------+ 227 | | smol | 228 | +======================================+ 229 | | smol | 230 | +--------------------------------------+"; 231 | println!("{expected}"); 232 | assert_table_line_width(&table, 40); 233 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 234 | } 235 | 236 | #[test] 237 | fn percentage_second() { 238 | let mut table = get_constraint_table(); 239 | 240 | table 241 | .set_content_arrangement(ContentArrangement::Dynamic) 242 | .set_width(40) 243 | .set_constraints(vec![ 244 | LowerBoundary(Percentage(40)), 245 | UpperBoundary(Percentage(30)), 246 | Absolute(Percentage(30)), 247 | ]); 248 | 249 | println!("{table}"); 250 | let expected = " 251 | +--------------+----------+----------+ 252 | | smol | Header2 | Header3 | 253 | +====================================+ 254 | | smol | This is | This is | 255 | | | another | the | 256 | | | text | third | 257 | | | | text | 258 | |--------------+----------+----------| 259 | | smol | Now | This is | 260 | | | add some | awesome | 261 | | | multi | | 262 | | | line | | 263 | | | stuff | | 264 | +--------------+----------+----------+"; 265 | println!("{expected}"); 266 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 267 | } 268 | 269 | #[test] 270 | fn max_percentage() { 271 | let mut table = get_constraint_table(); 272 | 273 | table 274 | .set_content_arrangement(ContentArrangement::Dynamic) 275 | .set_width(40) 276 | .set_constraints(vec![ 277 | ContentWidth, 278 | UpperBoundary(Percentage(30)), 279 | Absolute(Percentage(30)), 280 | ]); 281 | 282 | println!("{table}"); 283 | let expected = " 284 | +------+----------+----------+ 285 | | smol | Header2 | Header3 | 286 | +============================+ 287 | | smol | This is | This is | 288 | | | another | the | 289 | | | text | third | 290 | | | | text | 291 | |------+----------+----------| 292 | | smol | Now | This is | 293 | | | add some | awesome | 294 | | | multi | | 295 | | | line | | 296 | | | stuff | | 297 | +------+----------+----------+"; 298 | println!("{expected}"); 299 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 300 | } 301 | 302 | #[test] 303 | /// Ensure that both min and max in [Boundaries] is respected 304 | fn min_max_boundary() { 305 | let mut table = get_constraint_table(); 306 | 307 | table 308 | .set_content_arrangement(ContentArrangement::Dynamic) 309 | .set_width(40) 310 | .set_constraints(vec![ 311 | Boundaries { 312 | lower: Percentage(50), 313 | upper: Fixed(2), 314 | }, 315 | Boundaries { 316 | lower: Fixed(15), 317 | upper: Percentage(50), 318 | }, 319 | Absolute(Percentage(30)), 320 | ]); 321 | 322 | println!("{table}"); 323 | let expected = " 324 | +------------------+---------------+----------+ 325 | | smol | Header2 | Header3 | 326 | +=============================================+ 327 | | smol | This is | This is | 328 | | | another text | the | 329 | | | | third | 330 | | | | text | 331 | |------------------+---------------+----------| 332 | | smol | Now | This is | 333 | | | add some | awesome | 334 | | | multi line | | 335 | | | stuff | | 336 | +------------------+---------------+----------+"; 337 | println!("{expected}"); 338 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 339 | } 340 | 341 | #[rstest::rstest] 342 | #[case(ContentArrangement::Dynamic)] 343 | #[case(ContentArrangement::Disabled)] 344 | /// Empty table with zero width constraint. 345 | fn empty_table(#[case] arrangement: ContentArrangement) { 346 | let mut table = Table::new(); 347 | table 348 | .add_row(vec![""]) 349 | .set_content_arrangement(arrangement) 350 | .set_constraints(vec![Absolute(Fixed(0))]); 351 | 352 | println!("{table}"); 353 | let expected = " 354 | +---+ 355 | | | 356 | +---+"; 357 | println!("{expected}"); 358 | assert_eq!(expected, "\n".to_string() + &table.to_string()); 359 | } 360 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Comfy-table 2 | 3 | [![GitHub Actions Workflow](https://github.com/Nukesor/comfy-table/actions/workflows/test.yml/badge.svg)](https://github.com/Nukesor/comfy-table/actions/workflows/test.yml) 4 | [![docs](https://docs.rs/comfy-table/badge.svg)](https://docs.rs/comfy-table/) 5 | [![license](http://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/nukesor/comfy-table/blob/main/LICENSE) 6 | [![Crates.io](https://img.shields.io/crates/v/comfy-table.svg)](https://crates.io/crates/comfy-table) 7 | [![codecov](https://codecov.io/gh/nukesor/comfy-table/branch/main/graph/badge.svg)](https://codecov.io/gh/nukesor/comfy-table) 8 | 9 | ![comfy-table](https://raw.githubusercontent.com/Nukesor/images/main/comfy_table.gif) 10 | 11 | 12 | 13 | Comfy-table is designed as a library for building beautiful terminal tables, while being easy to use. 14 | 15 | ## Table of Contents 16 | 17 | - [Features](#features) 18 | - [Examples](#examples) 19 | - [Feature Flags](#feature-flags) 20 | - [Contributing](#contributing) 21 | - [Usage of unsafe](#unsafe) 22 | - [Comparison with other libraries](#comparison-with-other-libraries) 23 | 24 | ## State of the Project 25 | 26 | Comfy-table can be considered "finished". 27 | It contains all major features that were planned and received a lot of additional features along the way! 28 | As far as I'm aware, there're no lingering bugs and the project has a lot of test coverage. 29 | 30 | "Finished" means: 31 | 32 | - Comfy-table still receives regular version bumps for its few dependencies. 33 | - Pull requests for wished for and approved features will be reviewed and eventually merged. 34 | - New releases are pushed when new features are added or when requested. 35 | 36 | ## Features 37 | 38 | - Dynamic arrangement of content depending on a given width. 39 | - ANSI content styling for terminals (Colors, Bold, Blinking, etc.). 40 | - Styling Presets and preset modifiers to get you started. 41 | - Pretty much every part of the table is customizable (borders, lines, padding, alignment). 42 | - Constraints on columns that allow some additional control over how to arrange content. 43 | - Cross platform (Linux, macOS, Windows). 44 | - It's fast enough. 45 | - Benchmarks show that a pretty big table with complex constraints is build in `470μs` or `~0.5ms`. 46 | - The table seen at the top of the readme takes `~30μs`. 47 | - These numbers are from a overclocked `i7-8700K` with a max single-core performance of 4.9GHz. 48 | - To run the benchmarks yourselves, install criterion via `cargo install cargo-criterion` and run `cargo criterion` afterwards. 49 | 50 | Comfy-table is written for the current `stable` Rust version. 51 | Older Rust versions may work but aren't officially supported. 52 | 53 | ## Examples 54 | 55 | ```rust 56 | use comfy_table::Table; 57 | 58 | fn main() { 59 | let mut table = Table::new(); 60 | table 61 | .set_header(vec!["Header1", "Header2", "Header3"]) 62 | .add_row(vec![ 63 | "This is a text", 64 | "This is another text", 65 | "This is the third text", 66 | ]) 67 | .add_row(vec![ 68 | "This is another text", 69 | "Now\nadd some\nmulti line stuff", 70 | "This is awesome", 71 | ]); 72 | 73 | println!("{table}"); 74 | } 75 | ``` 76 | 77 | Create a very basic table.\ 78 | This table will become as wide as your content. Nothing fancy happening here. 79 | 80 | ```text,ignore 81 | +----------------------+----------------------+------------------------+ 82 | | Header1 | Header2 | Header3 | 83 | +======================================================================+ 84 | | This is a text | This is another text | This is the third text | 85 | |----------------------+----------------------+------------------------| 86 | | This is another text | Now | This is awesome | 87 | | | add some | | 88 | | | multi line stuff | | 89 | +----------------------+----------------------+------------------------+ 90 | ``` 91 | 92 | ### More Features 93 | 94 | ```rust 95 | use comfy_table::modifiers::UTF8_ROUND_CORNERS; 96 | use comfy_table::presets::UTF8_FULL; 97 | use comfy_table::*; 98 | 99 | fn main() { 100 | let mut table = Table::new(); 101 | table 102 | .load_preset(UTF8_FULL) 103 | .apply_modifier(UTF8_ROUND_CORNERS) 104 | .set_content_arrangement(ContentArrangement::Dynamic) 105 | .set_width(40) 106 | .set_header(vec!["Header1", "Header2", "Header3"]) 107 | .add_row(vec![ 108 | Cell::new("Center aligned").set_alignment(CellAlignment::Center), 109 | Cell::new("This is another text"), 110 | Cell::new("This is the third text"), 111 | ]) 112 | .add_row(vec![ 113 | "This is another text", 114 | "Now\nadd some\nmulti line stuff", 115 | "This is awesome", 116 | ]); 117 | 118 | // Set the default alignment for the third column to right 119 | let column = table.column_mut(2).expect("Our table has three columns"); 120 | column.set_cell_alignment(CellAlignment::Right); 121 | 122 | println!("{table}"); 123 | } 124 | ``` 125 | 126 | Create a table with UTF8 styling, and apply a modifier that gives the table round corners.\ 127 | Additionally, the content will dynamically wrap to maintain a given table width.\ 128 | If the table width isn't explicitly set and the program runs in a terminal, the terminal size will be used. 129 | 130 | On top of this, we set the default alignment for the right column to `Right` and the alignment of the left top cell to `Center`. 131 | 132 | ```text,ignore 133 | ╭────────────┬────────────┬────────────╮ 134 | │ Header1 ┆ Header2 ┆ Header3 │ 135 | ╞════════════╪════════════╪════════════╡ 136 | │ This is a ┆ This is ┆ This is │ 137 | │ text ┆ another ┆ the third │ 138 | │ ┆ text ┆ text │ 139 | ├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┤ 140 | │ This is ┆ Now ┆ This is │ 141 | │ another ┆ add some ┆ awesome │ 142 | │ text ┆ multi line ┆ │ 143 | │ ┆ stuff ┆ │ 144 | ╰────────────┴────────────┴────────────╯ 145 | ``` 146 | 147 | ### Styling 148 | 149 | ```rust 150 | use comfy_table::presets::UTF8_FULL; 151 | use comfy_table::*; 152 | 153 | fn main() { 154 | let mut table = Table::new(); 155 | table.load_preset(UTF8_FULL) 156 | .set_content_arrangement(ContentArrangement::Dynamic) 157 | .set_width(80) 158 | .set_header(vec![ 159 | Cell::new("Header1").add_attribute(Attribute::Bold), 160 | Cell::new("Header2").fg(Color::Green), 161 | Cell::new("Header3"), 162 | ]) 163 | .add_row(vec![ 164 | Cell::new("This is a bold text").add_attribute(Attribute::Bold), 165 | Cell::new("This is a green text").fg(Color::Green), 166 | Cell::new("This one has black background").bg(Color::Black), 167 | ]) 168 | .add_row(vec![ 169 | Cell::new("Blinky boi").add_attribute(Attribute::SlowBlink), 170 | Cell::new("This table's content is dynamically arranged. The table is exactly 80 characters wide.\nHere comes a reallylongwordthatshoulddynamicallywrap"), 171 | Cell::new("COMBINE ALL THE THINGS") 172 | .fg(Color::Green) 173 | .bg(Color::Black) 174 | .add_attributes(vec![ 175 | Attribute::Bold, 176 | Attribute::SlowBlink, 177 | ]) 178 | ]); 179 | 180 | println!("{table}"); 181 | } 182 | ``` 183 | 184 | This code generates the table that can be seen at the top of this document. 185 | 186 | ### Code Examples 187 | 188 | A few examples can be found in the `example` folder. 189 | To test an example, run `cargo run --example $name`. E.g.: 190 | 191 | ```bash 192 | cargo run --example readme_table 193 | ``` 194 | 195 | If you're looking for more information, take a look at the [tests folder](https://github.com/Nukesor/comfy-table/tree/main/tests). 196 | There are tests for almost every feature including a visual view for each resulting table. 197 | 198 | ## Feature Flags 199 | 200 | ### `tty` (enabled) 201 | 202 | This flag enables support for terminals. In detail this means: 203 | 204 | - Automatic detection whether we're in a terminal environment. 205 | Only used when no explicit `Table::set_width` is provided. 206 | - Support for ANSI Escape Code styling for terminals. 207 | 208 | ### `custom_styling` (disabled) 209 | 210 | This flag enables support for custom styling of text inside of cells. 211 | 212 | - Text formatting still works, even if you roll your own ANSI escape sequences. 213 | - Rainbow text 214 | - Makes comfy-table 30-50% slower 215 | 216 | ### `reexport_crossterm` (disabled) 217 | 218 | With this flag, comfy-table re-exposes crossterm's [`Attribute`](https://docs.rs/crossterm/latest/crossterm/style/enum.Attribute.html) and [`Color`](https://docs.rs/crossterm/latest/crossterm/style/enum.Color.html) enum. 219 | By default, a mirrored type is exposed, which internally maps to the crossterm type. 220 | 221 | This feature is very convenient if you use both comfy-table and crossterm in your code and want to use crossterm's types for everything interchangeably. 222 | 223 | **BUT** if you enable this feature, you opt-in for breaking changes on minor/patch versions. 224 | Meaning, you have to update crossterm whenever you update comfy-table and you **cannot** update crossterm until comfy-table released a new version with that crossterm version. 225 | 226 | ## Contributing 227 | 228 | Comfy-table's main focus is on being minimalistic and reliable. 229 | A fixed set of features that just work for "normal" use-cases: 230 | 231 | - Normal tables (columns, rows, one cell per column/row). 232 | - Dynamic arrangement of content to a given width. 233 | - Some kind of manual intervention in the arrangement process. 234 | 235 | If you come up with an idea or an improvement that fits into the current scope of the project, feel free to create an issue :)! 236 | 237 | Some things however will most likely not be added to the project since they drastically increase the complexity of the library or cover very specific edge-cases. 238 | 239 | Such features are: 240 | 241 | - Nested tables 242 | - Cells that span over multiple columns/rows 243 | - CSV to table conversion and vice versa 244 | 245 | ## Unsafe 246 | 247 | Comfy-table doesn't allow `unsafe` code in its code-base. 248 | As it's a "simple" formatting library it also shouldn't be needed in the future. 249 | 250 | If one disables the `tty` feature flag, this is also true for all of its dependencies. 251 | 252 | However, when enabling `tty`, Comfy-table uses one unsafe function call in its dependencies. \ 253 | It can be circumvented by explicitly calling [Table::force_no_tty](https://docs.rs/comfy-table/latest/comfy_table/struct.Table.html#method.force_no_tty). 254 | 255 | 1. `crossterm::terminal::size`. This function is necessary to detect the current terminal width if we're on a tty. 256 | This is only called if no explicit width is provided via `Table::set_width`. 257 | 258 | 259 | This is another libc call which is used to communicate with `/dev/tty` via a file descriptor. 260 | 261 | ```rust,ignore 262 | ... 263 | if wrap_with_result(unsafe { ioctl(fd, TIOCGWINSZ.into(), &mut size) }).is_ok() { 264 | Ok((size.ws_col, size.ws_row)) 265 | } else { 266 | tput_size().ok_or_else(|| std::io::Error::last_os_error().into()) 267 | } 268 | ... 269 | ``` 270 | 271 | ## Comparison with other libraries 272 | 273 | The following are official statements of the other crate authors. 274 | [This ticket](https://github.com/Nukesor/comfy-table/issues/76) can be used as an entry to find all other sibling tickets in the other projects. 275 | 276 | ### Cli-table 277 | 278 | The main focus of [`cli-table`](https://crates.io/crates/cli-table) is to support all platforms and at the same time limit the dependencies to keep the compile times and crate size low. 279 | 280 | Currently, this crate only pulls two external dependencies (other than cli-table-derive): 281 | 282 | - termcolor 283 | - unicode-width 284 | 285 | With csv feature enabled, it also pulls csv crate as dependency. 286 | 287 | ### Term-table 288 | 289 | [`term-table`](https://crates.io/crates/term-table) is pretty basic in terms of features. 290 | My goal with the project is to provide a good set of tools for rendering CLI tables, while also allowing users to bring their own tools for things like colours. 291 | One thing that is unique to `term-table` (as far as I'm aware) is the ability to have different number of columns in each row of the table. 292 | 293 | ### Prettytables-rs 294 | 295 | [`prettytables-rs`](https://crates.io/crates/prettytable-rs) provides functionality for formatting and aligning tables. 296 | It his however abandoned since over three years and a [rustsec/advisory-db](https://github.com/rustsec/advisory-db/issues/1173) entry has been requested. 297 | 298 | ### Comfy-table 299 | 300 | One of [`comfy-table`](https://crates.io/crates/comfy-table)'s big foci is on providing a minimalistic, but rock-solid library for building text-based tables. 301 | This means that the code is very well tested, no usage of `unsafe` and `unwrap` is only used if we can be absolutely sure that it's safe. 302 | There're only one occurrence of `unsafe` in all of comfy-table's dependencies, to be exact inside the `tty` communication code, which can be explicitly disabled. 303 | 304 | The other focus is on dynamic-length content arrangement. 305 | This means that a lot of work went into building an algorithm that finds a (near) optimal table layout for any given text and terminal width. 306 | -------------------------------------------------------------------------------- /tests/all/property_test.proptest-regressions: -------------------------------------------------------------------------------- 1 | # Seeds for failure cases proptest has generated in the past. It is 2 | # automatically read and these particular cases re-run before any 3 | # novel cases are generated. 4 | # 5 | # It is recommended to check this file in to source control so that 6 | # everyone who runs the test benefits from these saved cases. 7 | cc 37316ce70e5b3d157e9129ce895f5955949777d1d2c735de414916a7a3ba9cfd # shrinks to table = Table { columns: [Column { index: 0, padding: (1, 1), cell_alignment: None, max_content_width: 9, constraint: Some(Percentage(0)) }], style: {LeftBorderIntersections: '├', LeftHeaderIntersection: '╞', HeaderLines: '═', HorizontalLines: '╌', TopBorderIntersections: '┬', TopBorder: '─', MiddleIntersections: '┼', RightBorderIntersections: '┤', BottomBorder: '─', RightHeaderIntersection: '╡', BottomBorderIntersections: '┴', TopLeftCorner: '╭', BottomLeftCorner: '╰', BottomRightCorner: '╯', LeftBorder: '│', RightBorder: '│', TopRightCorner: '╮', MiddleHeaderIntersections: '╪', VerticalLines: '┆'}, header: None, rows: [Row { index: Some(0), cells: [Cell { content: ["!00a!¡¡"], alignment: Some(Left), fg: None, bg: None, attributes: [] }] }], arrangement: Dynamic, no_tty: false, table_width: None, enforce_styling: false } 8 | cc f883b4cd24dac445a1392951228f1696ab71ea17ccfd6ca71f65e6c2415f4e0a # shrinks to table = Table { columns: [Column { index: 0, padding: (1, 1), cell_alignment: None, max_content_width: 52, constraint: Some(Percentage(14)) }, Column { index: 1, padding: (1, 1), cell_alignment: None, max_content_width: 57, constraint: Some(MaxPercentage(35)) }], style: {BottomBorderIntersections: '┴', HeaderLines: '═', MiddleIntersections: '┼', TopRightCorner: '╮', BottomRightCorner: '╯', LeftBorderIntersections: '├', LeftBorder: '│', HorizontalLines: '╌', RightBorder: '│', MiddleHeaderIntersections: '╪', BottomLeftCorner: '╰', TopBorder: '─', TopLeftCorner: '╭', BottomBorder: '─', VerticalLines: '┆', RightBorderIntersections: '┤', RightHeaderIntersection: '╡', LeftHeaderIntersection: '╞', TopBorderIntersections: '┬'}, header: None, rows: [Row { index: Some(0), cells: [Cell { content: [""], alignment: Some(Right), fg: None, bg: None, attributes: [] }, Cell { content: [""], alignment: Some(Left), fg: None, bg: None, attributes: [] }] }, Row { index: Some(1), cells: [Cell { content: [""], alignment: Some(Center), fg: None, bg: None, attributes: [] }, Cell { content: [""], alignment: Some(Left), fg: None, bg: None, attributes: [] }] }, Row { index: Some(2), cells: [Cell { content: [""], alignment: Some(Center), fg: None, bg: None, attributes: [] }, Cell { content: [""], alignment: Some(Left), fg: None, bg: None, attributes: [] }] }, Row { index: Some(3), cells: [Cell { content: ["\u{4dafa}\u{202e}.\u{4a790}\u{51669}\'ȺA=T\u{89ea2}B::<"], alignment: Some(Left), fg: None, bg: None, attributes: [] }, Cell { content: ["`\u{7f}?P5s\"4+🕴J\t𦃍"], alignment: Some(Left), fg: None, bg: None, attributes: [] }] }, Row { index: Some(4), cells: [Cell { content: ["y\u{b29b5}^Ѩ\u{85c12}/$\t%\u{9bfb2}=/LB\u{b6eb1}\u{6d4ad}.\r"], alignment: Some(Left), fg: None, bg: None, attributes: [] }, Cell { content: ["𡆵"], alignment: Some(Left), fg: None, bg: None, attributes: [] }] }, Row { index: Some(5), cells: [Cell { content: ["\u{87e3f}__\\*Y\u{b}\u{3e6a8}\t\u{fbf84}\u{ffddd}J¥&\u{0}9wp\r("], alignment: Some(Center), fg: None, bg: None, attributes: [] }, Cell { content: ["`\u{9b}~&`K\u{b7ddb}\u{1b}\u{1b}K�\u{635fe}\t\t\t.q\"\u{3aeb6}#\u{59273}hN\u{b4dc4}\u{acd64}\u{1b}\u{202e}:\u{2}s\u{79836}"], alignment: Some(Right), fg: None, bg: None, attributes: [] }] }, Row { index: Some(6), cells: [Cell { content: [":3)鮩>Ⱥ 1H1\u{4}\u{61abc}b\u{379f9}\\&1\u{1}\u{76f75}?🕴l2\u{feff}?㊙z\u{7f}.Y\u{202e}"], alignment: Some(Right), fg: None, bg: None, attributes: [] }, Cell { content: ["\u{0}Ѩ!\u{1b}P\u{f72b3}\u{0}\u{a873e}\u{fcf70}|\u{6}