comfy-table-7.1.4/.cargo_vcs_info.json0000644000000001360000000000100132470ustar { "git": { "sha1": "28b07986effa5278d5bc8bbbcb7a6b9f2d44b77f" }, "path_in_vcs": "" }comfy-table-7.1.4/.github/ISSUE_TEMPLATE/bug_report.yml000064400000000000000000000037001046102023000204550ustar 00000000000000name: Bug Report ๐Ÿ› description: Create a report to help improve the project labels: ["t: bug"] title: "[Bug]" body: - type: markdown attributes: value: | Please take the time to fill out all relevant the fields below. - type: textarea id: description-of-bug attributes: label: Describe the bug description: A clear and concise description of the bug. placeholder: | Description goes here :) validations: required: true - type: textarea id: steps-to-reproduce attributes: label: Steps to reproduce description: | Please add a code example on how to trigger the bug. Or even better, a link to a repository with a minimal reproducible setup to reproduce the bug. placeholder: | ``` use comfiest_table::*; ``` The import doesn't work! validations: required: true - type: textarea id: debug-output attributes: label: Logs (if applicable) description: | This is mostly important for crashes or panics. Logs helps me to debug a problem if the bug is something that's not clearly visible. placeholder: | ``` Some log output here ``` validations: required: false - type: input id: operating-system attributes: label: Operating system description: The operating system you're using. placeholder: iOS 8 / Windows 10 / Ubuntu 22.04 validations: required: true - type: input id: version attributes: label: Comfy-table version description: The current version you're using. placeholder: v6.1.4 validations: required: true - type: textarea id: additional-context attributes: label: Additional context description: Add any other context about the problem here. placeholder: | Anything else you want to add. validations: required: false comfy-table-7.1.4/.github/ISSUE_TEMPLATE/feature_request.yml000064400000000000000000000044421046102023000215140ustar 00000000000000name: Feature request description: Suggest an idea for this project labels: ["t: feature"] body: - type: markdown attributes: value: | Please take the time to fill out all relevant the fields below. - type: textarea id: feature attributes: label: A detailed description of the feature you would like to see added. description: | Explain how that feature would look like and how it should behave. 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 :). placeholder: | It would be awesome, if all text would blink by default! Since not everybody might want this feature, it could also be hidden behind a feature flag. validations: required: true - type: textarea id: user-story attributes: label: Explain your usecase of the requested feature description: | 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. The more information you provide, the better I understand your problem ;). placeholder: | I have a chronic condition that requires me to have text blinking all the time. Without this feature, I tend to get really unproductive. validations: required: true - type: textarea id: alternatives attributes: label: Alternatives description: | If your problem can be solved in multiple ways, I would like to hear the possible alternatives you've considered. Some problems really don't have any feasible alternatives, in that case don't bother answering this question :) placeholder: | I could add a wrapper around the project which takes any output and wraps it in ANSI escape codes. However, this is very cumbersome and not user-friendly. This is why I think this should be added to the project. validations: required: false - type: textarea id: additional-context attributes: label: Additional context description: Add any other context about the problem here. placeholder: | Anything else you want to add, such as sketches, example code, etc. validations: required: false comfy-table-7.1.4/.github/dependabot.yml000064400000000000000000000002671046102023000162340ustar 00000000000000version: 2 updates: - package-ecosystem: github-actions directory: "/" schedule: interval: daily - package-ecosystem: cargo directory: "/" schedule: interval: weekly comfy-table-7.1.4/.github/workflows/coverage.yml000064400000000000000000000017111046102023000177520ustar 00000000000000name: Coverage on: push: branches: [main] paths: - '.github/workflows/coverage.yml' - '**.rs' - 'Cargo.toml' - 'Cargo.lock' pull_request: branches: [main] paths: - '.github/workflows/coverage.yml' - '**.rs' - 'Cargo.toml' - 'Cargo.lock' jobs: coverage: name: Create coverage statistics runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Rust toolchain uses: dtolnay/rust-toolchain@stable with: components: llvm-tools-preview - name: Install cargo-llvm-cov uses: taiki-e/install-action@cargo-llvm-cov - name: Generate code coverage run: cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 with: files: lcov.info fail_ci_if_error: false comfy-table-7.1.4/.github/workflows/lint.yml000064400000000000000000000026461046102023000171350ustar 00000000000000name: Code Style on: push: branches: [main] paths: - ".github/workflows/lint.yml" - "**.rs" - "Cargo.toml" - "Cargo.lock" pull_request: branches: [main] paths: - ".github/workflows/lint.yml" - "**.rs" - "Cargo.toml" - "Cargo.lock" jobs: test: name: Tests on ${{ matrix.os }} for ${{ matrix.toolchain }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Rust toolchain uses: dtolnay/rust-toolchain@stable with: components: rustfmt, clippy - name: cargo build run: cargo build - name: cargo fmt run: cargo fmt --all -- --check - name: cargo fmt run: cargo fmt --all -- --check - name: cargo clippy run: cargo clippy --tests -- -D warnings - name: cargo clippy without default features run: cargo clippy --no-default-features --tests -- -D warnings # Only run taplo on linux to save some time. # Also, taplo is broken on windows for some reason. - name: Install taplo-cli run: cargo install taplo-cli if: matrix.os == 'ubuntu-latest' - name: Run taplo check run: ~/.cargo/bin/taplo format --check if: matrix.os == 'ubuntu-latest' comfy-table-7.1.4/.github/workflows/test.yml000064400000000000000000000053301046102023000171370ustar 00000000000000name: Tests on: push: branches: [main] paths: - ".github/workflows/test.yml" - "**.rs" - "Cargo.toml" - "Cargo.lock" pull_request: branches: [main] paths: - ".github/workflows/test.yml" - "**.rs" - "Cargo.toml" - "Cargo.lock" jobs: test: name: Test target ${{ matrix.target }} on ${{ matrix.os }} for ${{ matrix.toolchain }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: target: - x86_64-unknown-linux-gnu - x86_64-pc-windows-msvc - x86_64-apple-darwin toolchain: [stable, "1.74"] include: - target: x86_64-unknown-linux-gnu os: ubuntu-latest minimal_setup: false - target: wasm32-wasi os: ubuntu-latest minimal_setup: true toolchain: "1.74" - target: wasm32-wasip1 os: ubuntu-latest minimal_setup: true toolchain: "stable" - target: x86_64-pc-windows-msvc os: windows-latest minimal_setup: false - target: x86_64-apple-darwin os: macos-latest minimal_setup: false # minimal_setup: This is needed for targets that don't support our dev dependencies. # It also excludes the default features, i.e. [tty]. # For instance, "wasm32-wasi" is such a target. steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Rust toolchain uses: dtolnay/rust-toolchain@master with: targets: ${{ matrix.target }} toolchain: ${{ matrix.toolchain }} components: rustfmt, clippy - name: cargo build run: cargo build --target=${{ matrix.target }} if: ${{ !matrix.minimal_setup }} - name: cargo test run: cargo test --target=${{ matrix.target }} --features=integration_test if: ${{ !matrix.minimal_setup }} - name: cargo test without default features run: cargo test --target=${{ matrix.target }} --tests --no-default-features if: ${{ !matrix.minimal_setup }} - name: cargo test with crossterm re-export run: cargo test --target=${{ matrix.target }} --features=integration_test,reexport_crossterm if: ${{ !matrix.minimal_setup }} - name: cargo test with custom_styling run: cargo test --target=${{ matrix.target }} --features=integration_test,custom_styling if: ${{ !matrix.minimal_setup }} - name: cargo build without default features and without dev dependencies run: cargo build --release --target=${{ matrix.target }} --no-default-features if: ${{ matrix.minimal_setup }} comfy-table-7.1.4/.gitignore000064400000000000000000000006331046102023000140310ustar 00000000000000# Generated by Cargo # will have compiled files and executables /target/ # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk # Benchmark and profiling flamegraph.svg perf.data perf.data.old #Added by cargo /target comfy-table-7.1.4/.taplo.toml000064400000000000000000000002571046102023000141350ustar 00000000000000[formatting] indent_string = " " reorder_arrays = true reorder_keys = true [[rule]] include = ["**/Cargo.toml"] keys = ["package"] [rule.formatting] reorder_keys = false comfy-table-7.1.4/CHANGELOG.md000064400000000000000000000360101046102023000136500ustar 00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [7.1.4] - 2025-02-07 ### Fix - Handle UTF-8 graphemes when truncating cells. [#167](https://github.com/Nukesor/comfy-table/pull/167) - Respect UTF-8 zero-width joiner and variation selection characters when splitting words. [#168](https://github.com/Nukesor/comfy-table/pull/168) by [tisonkun](https://github.com/tisonkun) ### Change - Remove strum dependency. [#169](https://github.com/Nukesor/comfy-table/pull/169) by [tisonkun](https://github.com/tisonkun) - Introduce the `unicode-segmentation` library in the scope of #167 and #168. - The new changes for correct UTF-8 handling have a performance hit of up to ~67%. However, this will most likely unnoticable for most people. The benchmark table with 10 columns and 500 rows slowed down from 15ms to 25ms. For "normal" tables, the performance hit is negligible. ## Chore - Bump ansi-str ## [7.1.3] - 2024-11-24 ### Fix - Bump `crossterm` to `0.28` on Windows as well ## [7.1.2] - 2024-11-24 ### Chore - Bump `crossterm` to `0.28` - Bump `unicode-width` to `0.2` ## [7.1.1] - 2024-04-05 ## Fix - Fix string width calculation with ANSI escape sequences by switching to the `ansi-str` crate. The previous implementation didn't respect OSC 8 hyperlink style ANSI sequences. Implemented by [dsully](https://github.com/dsully) in [#137](https://github.com/Nukesor/comfy-table/pull/137). - `custom_styling` feature now requires `tty` feature as it should. ## [7.1.0] - 2023-10-21 ### Added - Add helper methods `(col,row)_count` and `is_empty`. The first set of methods return the number of columns and rows respectively. The method `is_empty` returns if the table is empty (contains no data rows). Implemented by [Techassi](https://github.com/Techassi) in [#119](https://github.com/Nukesor/comfy-table/pull/119). ### Chore - Bump crossterm dependency ## [7.0.1] - 2023-06-16 ## Fix - Fix a panic when working with extreme paddings, where `(padding.left + padding.right) > u16::MAX`. - Fix a panic when working with extremely long content, where `(content_width + padding) > u16::MAX`. - Properly enforce lower boundary constraints. Previously, "normal" columns were allocated before lower boundaries were respected. This could lead to scenarios, where the table would grow beyond the specified size, when there was a lower boundary. - Fix calculation of column widths for empty columns. The minimum content width for a column is `1` char, but the `column_max_content_widths` function on the table returned a `0` width for fully empty columns. This resulted in tables becoming larger than specified if there were any empty columns. ## Misc - Extend property tests, which lead to the discovery some bugs. ## [7.0.0] - 2023-06-06 ### Breaking - The `Color` and `Attribute` enum are no longer re-exported from crossterm by default. Previously, when updating comfy-table, crossterm needed to be upgraded as well, since the compile would otherwise fail due to type incompatibilities. To fix this, these enums are now mirrored and internally mapped to their crossterm equivalents, which allows us to safely bump crossterm whenever a new version is released. This change will only affect you if your projects explicitly use crossterm and comfy-table at the same time **and** feed crossterm's native types into comfy-table. If one wants the old behavior for convenience reasons, this can be enabled via a feature flag. However, **this is also a opt-in to potential breaking changes on minor/patch versions**. - Bump minimum version to v1.64 ### Added - `reexport_crossterm` feature flag to enable old crossterm re-export. ## [6.2.0] - 2023-05-26 ### Added - Add support for custom ansi styling inside of cells. This feature is hidden behind the feature flag `custom_styling`. Implemented by [blueforesticarus](https://github.com/blueforesticarus) in [#93](https://github.com/Nukesor/comfy-table/pull/93). - Add helper functions `add_row[s]_if`, which filtering of rows by a predicate. Implemented by [Techassi](https://github.com/Techassi) in [#106](https://github.com/Nukesor/comfy-table/pull/106). ### Maintenance - Bump dependencies ## [6.1.4] - 2022-12-31 ### Added - New preset `ASCII_FULL_CONDENSED` [#97](https://github.com/Nukesor/comfy-table/pull/97) ## [6.1.3] - 2022-11-21 ### Fixed - Disable unneeded crossterm `bracketed-paste` feature. ## [6.1.2] - 2022-10-27 ### Fixed - `Table::row_iter` no longer requires a `&mut self`, but only `&self`. ## [6.1.1] - 2022-10-22 ### Fixed - Fixed an issue where dynamic arrangement failed when setting the table to the exact width of the content [#90](https://github.com/Nukesor/comfy-table/issues/90). - The header size is now properly respected in the final optimization step [#90](https://github.com/Nukesor/comfy-table/issues/90). Previously, this wasn't the case and lead to weird formatting behavior when both of the following were true - Dynamic content adjustment was active. - The table didn't fit into the the available space. - The header of a row was longer than its content. - Fix wrong LowerBoundary calculation. This was introduced in commit bee764d, when this logic was refactored. [#90](https://github.com/Nukesor/comfy-table/issues/90). - `Table::column_iter` no longer requires a `&mut self`, but only `&self`. ### Added - Expose current ContentArrangement for table via `table.content_arrangement`. ## [6.1.0] - 2022-08-28 ### Added - Add `Table::add_rows` to add multiple rows at the same time. ### Misc - Update crossterm to `v0.24` ## [6.0.0] - 2022-05-31 ### Added - Add `Table::style_text_only()`, which prevents non-delimiter whitespaces in cells to be styled. - Add the `Table::discover_columns` function and add info on when to use it to `Row::add_cell`. ### Breaking Changes - Renaming of several functions to be Rust idiomatic: - `Cell::get_content` -> `Cell::content` - `Column::get_padding_width` -> `Column::padding_width` - `Column::get_constraint` -> `Column::constraint` - `Table::get_header` -> `Table::header` - `Table::get_table_width` -> `Table::width` - `Table::set_table_width` -> `Table::set_width` - `Table::set_style` -> `Table::style` - `Table::get_column` -> `Table::column` - `Table::get_column_mut` -> `Table::column_mut` - `Table::get_row` -> `Table::row` - `Table::get_row_mut` -> `Table::row_mut` - `Column::get_max_width` and `Column::get_max_content_width` have been removed as we cannot guarantee that these numbers are always correct. Use `Table::column_max_content_widths` instead ### Changed - `Table::column_max_content_widths` now performs a full scan of the table's content when called. - Don't include `Table::is_tty`, `Table::force_no_tty` and `Table::should_style` if `tty` feature isn't enabled. ## [5.0.1] - 2022-02-18 ### Updates - All dependencies have been bumped. ## [5.0.0] - 2021-11-07 ### Updates - All dependencies have been bumped. ### Added - Add option to use `stderr` for `is_tty` check [#25](https://github.com/Nukesor/comfy-table/pull/47). ### Breaking - Remove `ASCII_HORIZONTAL_BORDERS_ONLY` in favor of `ASCII_HORIZONTAL_ONLY`. - Remove `UTF8_HORIZONTAL_BORDERS_ONLY` in favor of `UTF8_HORIZONTAL_ONLY`. ## [4.1.1] - 2021-08-11 ### Added - `tty` feature flag, which enables tty related functionality. This includes styling and terminal-width detection. The `tty` feature flag is enabled by default. Implemented by [roee88](https://github.com/roee88) in [#47](https://github.com/Nukesor/comfy-table/pull/47). ## [4.1.0] - 2021-08-09 ### Added - Add `modifiers::UTF8_SOLID_INNER_BORDERS`, which makes the inner borders solid lines: `โ”‚โ”€` by [ModProg](https://github.com/ModProg) for [#39](https://github.com/Nukesor/comfy-table/issues/39). - Add `presets::ASCII_BORDERS_ONLY_CONDENSED`, which is `ASCII_BORDERS_ONLY` but without spacing between rows [#43](https://github.com/Nukesor/comfy-table/issues/43). ### Fixed - Several preset examples weren't correct. - Multi-character UTF8 symbols are now handled correctly in most situations. Table-layout might still break for 1-character columns. - Mid-word splitting now takes multi-character utf8 characters into account. ### Changed - Rename `ASCII_HORIZONTAL_BORDERS_ONLY` to `ASCII_HORIZONTAL_ONLY`. Old imports will still work until v5.0. - Rename `UTF8_HORIZONTAL_BORDERS_ONLY` to `UTF8_HORIZONTAL_ONLY`. Old imports will still work until v5.0. ## [4.0.1] - 2021-07-08 ### Fixed - Some docstrings on the `ColumnConstraint` and `Width` enum were wrong. ## [4.0.0] - 2021-07-03 ### Added - Add `Table::lines`, which returns an iterator over all lines of the final table output by [dmaahs2017](https://github.com/dmaahs2017) for [#35](https://github.com/Nukesor/comfy-table/issues/35). - Add `ColumnConstraints::Boundaries{lower: Width, upper: Width}` to specify both an upper and a lower boundary. ### Fixed - Fixed percentages sometimes weren't correctly calculated, due to not taking border columns into account. - Return `None` for `Table::get_table_width`, if getting the terminal size somehow failed. - Fixed a lot of possible, but highly unlikely number conversion overflow issues. - Run space optimization under all circumstances. - Percentage constraints with values of >100 will now be capped to 100. - The MinConstraint would be ignored when: - The content was larger than the min constraint - There was less space available than specified in the constraint. ### Changed - The way ColumnConstraints are initialized has been changed. There is now ``` enum ColumnConstraints { ..., /// Enforce a absolute width for a column. Absolute(Width), /// Specify a lower boundary, either fixed or as percentage of the total width. LowerBoundary(Width), /// Specify a upper boundary, either fixed or as percentage of the total width. UpperBoundary(Width), } pub enum Width { /// Specify a min amount of characters per line for a column. Fixed(u16), /// Set a a minimum percentage in respect to table_width for this column. /// Values above 100 will be automatically reduced to 100. /// **Warning:** This option will be ignored, if the width cannot be determined! Percentage(u16), } ``` Instead of the old ``` enum ColumnConstraints { ..., Width(u16), MinWidth(u16), MaxWidth(u16), Percentage(u16), MinPercentage(u16), MaxPercentage(u16), } ``` ## [3.0.0] - 2021-06-13 ### Breaking changes - Remove most custom traits and replace them with std's generic `From` trait. \ Check the docs on the trait implementations for Cell, Row and Cells - Add the `Cells` type, to allow super generic `Iterator -> Row` conversions. ## [2.1.0] - 2021-01-26 ### Added - `DynamicFullWidth` arrangement. This mode is basically the same as the `Dynamic` arrangement mode, but it will always use the full available width, even if there isn't enough content to fill the space. ## [2.0.0] - 2021-01-16 ### Added **Dynamic arrangement** A new logic to optimize space usage after splitting content has been added.\ If there is a lot of unused space after the content has been arranged, this space will now be redistributed to the remaining columns. Or it will be removed if there are no other columns. **This is considered a breaking change, since this can result in different table layouts!!** This process is far from perfect, but the behavior is better than before. Old behavior: ``` +-----------------------------------+-----------------------------------+------+ | Header1 | Header2 | Head | +==============================================================================+ | This is a very long line with a | This is text with a | smol | | lot of text | anotherverylongtexttesttest | | +-----------------------------------+-----------------------------------+------+ ``` New behavior: ``` +-----------------------------------------+-----------------------------+------+ | Header1 | Header2 | Head | +==============================================================================+ | This is a very long line with a lot of | This is text with a | smol | | text | anotherverylongtexttesttest | | +-----------------------------------------+-----------------------------+------+ ``` Old behavior: ``` +------------------------------------------------+ | Header1 | +================================================+ | This is text with a | | anotherverylongtexttesttestaa | +------------------------------------------------+ ``` New behavior: ``` +-------------------------------+ | Header1 | +===============================+ | This is text with a | | anotherverylongtexttesttestaa | +-------------------------------+ ``` ## [1.6.0] - 2021-01-16 ### Added - Add the `NOTHING` preset, which prints no borders or lines at all. ## [1.5.0] - 2020-12-29 ### Added - Add `table::trim_fmt`, which trims all trailing whitespaces on tables with no right border. ## [1.4.0] - 2020-12-06 ### Added - Allow to set custom delimiters on tables, columns and cells. ### Changed - Expose all important traits. I.e. `ToRow`, `ToCell` and `ToCells`. ## [1.3.0] - 2020-11-20 ### Added - New ColumnConstraint for hiding columns ## [1.2.0] - 2020-10-27 ### Added - Add the option to set a max-height on rows. Long content will be truncated. ## [1.1.1] - 2020-08-23 ### Changed - A simple update of all dependencies. ## [1.1.0] - 2020-08-23 ### Changed - Move `is_tty` logic from `atty` to `crossterm`. - Remove `skeptic`, since it fails in CI and bloats compile time. Compile time is reduced by ca. 40%. ## [1.0.0] - 2020-07-07 ### Changed - The project has been in use for quite some time and seems to be quite stable! - Use cargo's `example` functionality for examples. ## [0.1.1] - 2020-05-24 ### Added - `Column::get_max_width()`, which returns the character count of the widest line in this column including padding. - `current_style_as_preset` method for convenient conversion of a style into a preset - New Markdown like table style prefix. Thanks to [joeydumont](https://github.com/joeydumont). ## [0.1.0] - 2020-03-21 ### Added - Better test coverage ### Fixed - Fixed a bug with broken percentage constraints for super wide tables. ## [0.0.7] - 2020-02-09 ### Added - Introduce proptest ### Fixed - Fix wrong calculation due to bytes count instead of char count - Fix panics caused by wrong string splits ## [0.0.6] - 2020-01-31 ### Changed - Crossterm 0.15 update - Simplify the project's module structure ## [0.0.5] - 2020-01-29 ### Added - Add `Column::remove_constraint()` - Preset `UTF8_NO_BORDERS` - Preset `UTF8_HORIZONTAL_BORDERS_ONLY` ## [0.0.4] - 2020-01-27 ### Added - Add skeptic tests - Add code coverage - A lot more tests ### Changed - Removed `Hidden` Constraint comfy-table-7.1.4/Cargo.lock0000644000001034130000000000100112240ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "anes" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "ansi-str" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "060de1453b69f46304b28274f382132f4e72c55637cf362920926a70d090890d" dependencies = [ "ansitok", ] [[package]] name = "ansitok" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0a8acea8c2f1c60f0a92a8cd26bf96ca97db56f10bbcab238bbe0cceba659ee" dependencies = [ "nom", "vte", ] [[package]] name = "anstyle" version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" [[package]] name = "arrayvec" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "autocfg" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "bit-set" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" dependencies = [ "bit-vec", ] [[package]] name = "bit-vec" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" [[package]] name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "bumpalo" version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cast" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "ciborium" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" dependencies = [ "ciborium-io", "ciborium-ll", "serde", ] [[package]] name = "ciborium-io" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" [[package]] name = "ciborium-ll" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" dependencies = [ "ciborium-io", "half", ] [[package]] name = "clap" version = "4.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fbb260a053428790f3de475e304ff84cdbc4face759ea7a3e64c1edd938a7fc" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" version = "4.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64b17d7ea74e9f833c7dbf2cbe4fb12ff26783eda4782a8975b72f895c9b4d99" dependencies = [ "anstyle", "clap_lex", ] [[package]] name = "clap_lex" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" [[package]] name = "comfy-table" version = "7.1.4" dependencies = [ "ansi-str", "console", "criterion", "crossterm", "pretty_assertions", "proptest", "rand 0.9.0", "rstest", "unicode-segmentation", "unicode-width 0.2.0", ] [[package]] name = "console" version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" dependencies = [ "encode_unicode", "lazy_static", "libc", "unicode-width 0.1.13", "windows-sys 0.52.0", ] [[package]] name = "criterion" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" dependencies = [ "anes", "cast", "ciborium", "clap", "criterion-plot", "is-terminal", "itertools", "num-traits", "once_cell", "oorandom", "plotters", "rayon", "regex", "serde", "serde_derive", "serde_json", "tinytemplate", "walkdir", ] [[package]] name = "criterion-plot" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" dependencies = [ "cast", "itertools", ] [[package]] name = "crossbeam-deque" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" dependencies = [ "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crossterm" version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ "bitflags", "crossterm_winapi", "parking_lot", "rustix", "winapi", ] [[package]] name = "crossterm_winapi" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" dependencies = [ "winapi", ] [[package]] name = "crunchy" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" [[package]] name = "diff" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" [[package]] name = "either" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "encode_unicode" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "fastrand" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "futures-core" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-macro" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "futures-task" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-timer" version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-core", "futures-macro", "futures-task", "pin-project-lite", "pin-utils", "slab", ] [[package]] name = "getrandom" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", "wasi 0.11.0+wasi-snapshot-preview1", ] [[package]] name = "getrandom" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" dependencies = [ "cfg-if", "libc", "wasi 0.13.3+wasi-0.2.2", "windows-targets", ] [[package]] name = "glob" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "half" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" dependencies = [ "cfg-if", "crunchy", ] [[package]] name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "hermit-abi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "indexmap" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" dependencies = [ "equivalent", "hashbrown", ] [[package]] name = "is-terminal" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" dependencies = [ "hermit-abi", "libc", "windows-sys 0.52.0", ] [[package]] name = "itertools" version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" dependencies = [ "either", ] [[package]] name = "itoa" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "js-sys" version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libm" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "linux-raw-sys" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "lock_api" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", ] [[package]] name = "log" version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "nom" version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", "minimal-lexical", ] [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", "libm", ] [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "oorandom" version = "11.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" [[package]] name = "parking_lot" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", "windows-targets", ] [[package]] name = "pin-project-lite" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "plotters" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a15b6eccb8484002195a3e44fe65a4ce8e93a625797a063735536fd59cb01cf3" dependencies = [ "num-traits", "plotters-backend", "plotters-svg", "wasm-bindgen", "web-sys", ] [[package]] name = "plotters-backend" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "414cec62c6634ae900ea1c56128dfe87cf63e7caece0852ec76aba307cebadb7" [[package]] name = "plotters-svg" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81b30686a7d9c3e010b84284bdd26a29f2138574f52f5eb6f794fc0ad924e705" dependencies = [ "plotters-backend", ] [[package]] name = "ppv-lite86" version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" dependencies = [ "zerocopy 0.7.35", ] [[package]] name = "pretty_assertions" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" dependencies = [ "diff", "yansi", ] [[package]] name = "proc-macro-crate" version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" dependencies = [ "toml_edit", ] [[package]] name = "proc-macro2" version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] [[package]] name = "proptest" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c2511913b88df1637da85cc8d96ec8e43a3f8bb8ccb71ee1ac240d6f3df58d" dependencies = [ "bit-set", "bit-vec", "bitflags", "lazy_static", "num-traits", "rand 0.8.5", "rand_chacha 0.3.1", "rand_xorshift", "regex-syntax", "rusty-fork", "tempfile", "unarray", ] [[package]] name = "quick-error" version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha 0.3.1", "rand_core 0.6.4", ] [[package]] name = "rand" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.0", "zerocopy 0.8.17", ] [[package]] name = "rand_chacha" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core 0.6.4", ] [[package]] name = "rand_chacha" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", "rand_core 0.9.0", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom 0.2.15", ] [[package]] name = "rand_core" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff" dependencies = [ "getrandom 0.3.1", "zerocopy 0.8.17", ] [[package]] name = "rand_xorshift" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" dependencies = [ "rand_core 0.6.4", ] [[package]] name = "rayon" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ "either", "rayon-core", ] [[package]] name = "rayon-core" version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" dependencies = [ "crossbeam-deque", "crossbeam-utils", ] [[package]] name = "redox_syscall" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" dependencies = [ "bitflags", ] [[package]] name = "regex" version = "1.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "relative-path" version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" [[package]] name = "rstest" version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03e905296805ab93e13c1ec3a03f4b6c4f35e9498a3d5fa96dc626d22c03cd89" dependencies = [ "futures-timer", "futures-util", "rstest_macros", "rustc_version", ] [[package]] name = "rstest_macros" version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef0053bbffce09062bee4bcc499b0fbe7a57b879f1efe088d6d8d4c7adcdef9b" dependencies = [ "cfg-if", "glob", "proc-macro-crate", "proc-macro2", "quote", "regex", "relative-path", "rustc_version", "syn", "unicode-ident", ] [[package]] name = "rustc_version" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ "semver", ] [[package]] name = "rustix" version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", "windows-sys 0.52.0", ] [[package]] name = "rusty-fork" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" dependencies = [ "fnv", "quick-error", "tempfile", "wait-timeout", ] [[package]] name = "ryu" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "same-file" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ "winapi-util", ] [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "semver" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.122" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da" dependencies = [ "itoa", "memchr", "ryu", "serde", ] [[package]] name = "slab" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] [[package]] name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "syn" version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tempfile" version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8fcd239983515c23a32fb82099f97d0b11b8c72f654ed659363a95c3dad7a53" dependencies = [ "cfg-if", "fastrand", "once_cell", "rustix", "windows-sys 0.52.0", ] [[package]] name = "tinytemplate" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" dependencies = [ "serde", "serde_json", ] [[package]] name = "toml_datetime" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" [[package]] name = "toml_edit" version = "0.22.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b072cee73c449a636ffd6f32bd8de3a9f7119139aff882f44943ce2986dc5cf" dependencies = [ "indexmap", "toml_datetime", "winnow", ] [[package]] name = "unarray" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" [[package]] name = "unicode-ident" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" [[package]] name = "unicode-segmentation" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-width" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" [[package]] name = "unicode-width" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" [[package]] name = "vte" version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077" dependencies = [ "arrayvec", "memchr", ] [[package]] name = "wait-timeout" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" dependencies = [ "libc", ] [[package]] name = "walkdir" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", ] [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasi" version = "0.13.3+wasi-0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" dependencies = [ "wit-bindgen-rt", ] [[package]] name = "wasm-bindgen" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "web-sys" version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets", ] [[package]] name = "windows-sys" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_gnullvm", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" dependencies = [ "memchr", ] [[package]] name = "wit-bindgen-rt" version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" dependencies = [ "bitflags", ] [[package]] name = "yansi" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" [[package]] name = "zerocopy" version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "byteorder", "zerocopy-derive 0.7.35", ] [[package]] name = "zerocopy" version = "0.8.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa91407dacce3a68c56de03abe2760159582b846c6a4acd2f456618087f12713" dependencies = [ "zerocopy-derive 0.8.17", ] [[package]] name = "zerocopy-derive" version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "zerocopy-derive" version = "0.8.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06718a168365cad3d5ff0bb133aad346959a2074bd4a85c121255a11304a8626" dependencies = [ "proc-macro2", "quote", "syn", ] comfy-table-7.1.4/Cargo.toml0000644000000046770000000000100112630ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.64" name = "comfy-table" version = "7.1.4" authors = ["Arne Beer "] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "An easy to use library for building beautiful tables with automatic content wrapping" homepage = "https://github.com/nukesor/comfy-table" documentation = "https://docs.rs/comfy-table/" readme = "README.md" keywords = [ "table", "terminal", "unicode", ] license = "MIT" repository = "https://github.com/nukesor/comfy-table" [lib] name = "comfy_table" path = "src/lib.rs" [[example]] name = "inner_style" path = "examples/inner_style.rs" required-features = ["custom_styling"] [[example]] name = "no_tty" path = "examples/readme_table_no_tty.rs" [[example]] name = "readme_table" path = "examples/readme_table.rs" [[test]] name = "all_tests" path = "tests/all_tests.rs" [[bench]] name = "build_large_table" path = "benches/build_large_table.rs" harness = false [[bench]] name = "build_tables" path = "benches/build_tables.rs" harness = false [dependencies.ansi-str] version = "0.9" optional = true [dependencies.console] version = "0.15" optional = true [dependencies.unicode-segmentation] version = "1" [dependencies.unicode-width] version = "0.2" [dev-dependencies.criterion] version = "0.5" [dev-dependencies.pretty_assertions] version = "1" [dev-dependencies.proptest] version = "1" [dev-dependencies.rand] version = "0.9" [dev-dependencies.rstest] version = "0.24" [features] custom_styling = [ "dep:ansi-str", "dep:console", "tty", ] debug = [] default = ["tty"] integration_test = [] reexport_crossterm = ["tty"] tty = ["dep:crossterm"] [target."cfg(not(windows))".dependencies.crossterm] version = "0.28" optional = true default-features = false [target."cfg(windows)".dependencies.crossterm] version = "0.28" features = ["windows"] optional = true default-features = false [badges.maintenance] status = "actively-developed" comfy-table-7.1.4/Cargo.toml.orig000064400000000000000000000042351046102023000147320ustar 00000000000000[package] name = "comfy-table" description = "An easy to use library for building beautiful tables with automatic content wrapping" version = "7.1.4" authors = ["Arne Beer "] homepage = "https://github.com/nukesor/comfy-table" repository = "https://github.com/nukesor/comfy-table" documentation = "https://docs.rs/comfy-table/" license = "MIT" keywords = ["table", "terminal", "unicode"] readme = "README.md" rust-version = "1.64" edition = "2021" [badges] maintenance = { status = "actively-developed" } [[bench]] harness = false name = "build_tables" [[bench]] harness = false name = "build_large_table" [[example]] name = "no_tty" path = "examples/readme_table_no_tty.rs" [[example]] name = "readme_table" path = "examples/readme_table.rs" [[example]] name = "inner_style" path = "examples/inner_style.rs" required-features = ["custom_styling"] [features] # For more info about these flags, please check the README. # Everything's explained over there. custom_styling = ["dep:ansi-str", "dep:console", "tty"] default = ["tty"] reexport_crossterm = ["tty"] tty = ["dep:crossterm"] # ---- DEVELOPMENT FLAGS ---- # This flag is for comfy-table development debugging! # You usually don't need this as a user of the library. debug = [] # This feature is used to for integration testing of comfy_table. # It exposes normally unexposed internal functionality for easier testing. # DON'T USE. You opt in for breaking changes, as the internal API might change on minor/patch versions. integration_test = [] [dependencies] unicode-segmentation = "1" unicode-width = "0.2" # Optional dependencies ansi-str = { version = "0.9", optional = true } console = { version = "0.15", optional = true } [dev-dependencies] criterion = "0.5" pretty_assertions = "1" proptest = "1" rand = "0.9" rstest = "0.24" # We don't need any of the default features for crossterm. # However, the windows build needs the windows feature enabled. [target.'cfg(not(windows))'.dependencies] crossterm = { version = "0.28", optional = true, default-features = false } [target.'cfg(windows)'.dependencies] crossterm = { version = "0.28", optional = true, default-features = false, features = [ "windows", ] } comfy-table-7.1.4/FAQ.md000064400000000000000000000011431046102023000127670ustar 00000000000000# FAQ ## Why is my styling broken? Why doesn't styling work? `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). Any styling from other libraries, even crossterm, will most likely not work as expected or break. It's impossible for `comfy-table` to know about any ANSI escape sequences it doesn't create itself. Hence, it's not possible to respect unknown styling, as ANSI styling doesn't work this way and doesn't support this. If you come up with a solution to this problem, feel free to create a PR. comfy-table-7.1.4/Justfile000064400000000000000000000012471046102023000135530ustar 00000000000000# If you change anything in here, make sure to also adjust the lint CI job! lint: just ensure-command cargo-nextest cargo fmt --all -- --check taplo format --check cargo clippy --tests --workspace -- -D warnings format: just ensure-command taplo cargo fmt taplo format # Ensures that one or more required commands are installed ensure-command +command: #!/usr/bin/env bash set -euo pipefail read -r -a commands <<< "{{ command }}" for cmd in "${commands[@]}"; do if ! command -v "$cmd" > /dev/null 2>&1 ; then printf "Couldn't find required executable '%s'\n" "$cmd" >&2 exit 1 fi done comfy-table-7.1.4/LICENSE000064400000000000000000000020521046102023000130430ustar 00000000000000MIT License Copyright (c) 2019 Arne Beer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. comfy-table-7.1.4/README.md000064400000000000000000000304521046102023000133220ustar 00000000000000# Comfy-table [![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) [![docs](https://docs.rs/comfy-table/badge.svg)](https://docs.rs/comfy-table/) [![license](http://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/nukesor/comfy-table/blob/main/LICENSE) [![Crates.io](https://img.shields.io/crates/v/comfy-table.svg)](https://crates.io/crates/comfy-table) [![codecov](https://codecov.io/gh/nukesor/comfy-table/branch/main/graph/badge.svg)](https://codecov.io/gh/nukesor/comfy-table) ![comfy-table](https://raw.githubusercontent.com/Nukesor/images/main/comfy_table.gif) Comfy-table is designed as a library for building beautiful terminal tables, while being easy to use. ## Table of Contents - [Features](#features) - [Examples](#examples) - [Feature Flags](#feature-flags) - [Contributing](#contributing) - [Usage of unsafe](#unsafe) - [Comparison with other libraries](#comparison-with-other-libraries) ## Features - Dynamic arrangement of content depending on a given width. - ANSI content styling for terminals (Colors, Bold, Blinking, etc.). - Styling Presets and preset modifiers to get you started. - Pretty much every part of the table is customizable (borders, lines, padding, alignment). - Constraints on columns that allow some additional control over how to arrange content. - Cross plattform (Linux, macOS, Windows). - It's fast enough. - Benchmarks show that a pretty big table with complex constraints is build in `470ฮผs` or `~0.5ms`. - The table seen at the top of the readme takes `~30ฮผs`. - These numbers are from a overclocked `i7-8700K` with a max single-core performance of 4.9GHz. - To run the benchmarks yourselves, install criterion via `cargo install cargo-criterion` and run `cargo criterion` afterwards. Comfy-table is written for the current `stable` Rust version. Older Rust versions may work but aren't officially supported. ## Examples ```rust use comfy_table::Table; fn main() { let mut table = Table::new(); table .set_header(vec!["Header1", "Header2", "Header3"]) .add_row(vec![ "This is a text", "This is another text", "This is the third text", ]) .add_row(vec![ "This is another text", "Now\nadd some\nmulti line stuff", "This is awesome", ]); println!("{table}"); } ``` Create a very basic table.\ This table will become as wide as your content. Nothing fancy happening here. ```text,ignore +----------------------+----------------------+------------------------+ | Header1 | Header2 | Header3 | +======================================================================+ | This is a text | This is another text | This is the third text | |----------------------+----------------------+------------------------| | This is another text | Now | This is awesome | | | add some | | | | multi line stuff | | +----------------------+----------------------+------------------------+ ``` ### More Features ```rust use comfy_table::modifiers::UTF8_ROUND_CORNERS; use comfy_table::presets::UTF8_FULL; use comfy_table::*; fn main() { let mut table = Table::new(); table .load_preset(UTF8_FULL) .apply_modifier(UTF8_ROUND_CORNERS) .set_content_arrangement(ContentArrangement::Dynamic) .set_width(40) .set_header(vec!["Header1", "Header2", "Header3"]) .add_row(vec![ Cell::new("Center aligned").set_alignment(CellAlignment::Center), Cell::new("This is another text"), Cell::new("This is the third text"), ]) .add_row(vec![ "This is another text", "Now\nadd some\nmulti line stuff", "This is awesome", ]); // Set the default alignment for the third column to right let column = table.column_mut(2).expect("Our table has three columns"); column.set_cell_alignment(CellAlignment::Right); println!("{table}"); } ``` Create a table with UTF8 styling, and apply a modifier that gives the table round corners.\ Additionally, the content will dynamically wrap to maintain a given table width.\ If the table width isn't explicitely set and the program runs in a terminal, the terminal size will be used. 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`. ```text,ignore โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ โ”‚ Header1 โ”† Header2 โ”† Header3 โ”‚ โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ก โ”‚ This is a โ”† This is โ”† This is โ”‚ โ”‚ text โ”† another โ”† the third โ”‚ โ”‚ โ”† text โ”† text โ”‚ โ”œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ผโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ผโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ค โ”‚ This is โ”† Now โ”† This is โ”‚ โ”‚ another โ”† add some โ”† awesome โ”‚ โ”‚ text โ”† multi line โ”† โ”‚ โ”‚ โ”† stuff โ”† โ”‚ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ ``` ### Styling ```rust use comfy_table::presets::UTF8_FULL; use comfy_table::*; fn main() { let mut table = Table::new(); table.load_preset(UTF8_FULL) .set_content_arrangement(ContentArrangement::Dynamic) .set_width(80) .set_header(vec![ Cell::new("Header1").add_attribute(Attribute::Bold), Cell::new("Header2").fg(Color::Green), Cell::new("Header3"), ]) .add_row(vec![ Cell::new("This is a bold text").add_attribute(Attribute::Bold), Cell::new("This is a green text").fg(Color::Green), Cell::new("This one has black background").bg(Color::Black), ]) .add_row(vec![ Cell::new("Blinky boi").add_attribute(Attribute::SlowBlink), Cell::new("This table's content is dynamically arranged. The table is exactly 80 characters wide.\nHere comes a reallylongwordthatshoulddynamicallywrap"), Cell::new("COMBINE ALL THE THINGS") .fg(Color::Green) .bg(Color::Black) .add_attributes(vec![ Attribute::Bold, Attribute::SlowBlink, ]) ]); println!("{table}"); } ``` This code generates the table that can be seen at the top of this document. ### Code Examples A few examples can be found in the `example` folder. To test an example, run `cargo run --example $name`. E.g.: ```bash cargo run --example readme_table ``` If you're looking for more information, take a look at the [tests folder](https://github.com/Nukesor/comfy-table/tree/main/tests). There are tests for almost every feature including a visual view for each resulting table. ## Feature Flags ### `tty` (enabled) This flag enables support for terminals. In detail this means: - Automatic detection whether we're in a terminal environment. Only used when no explicit `Table::set_width` is provided. - Support for ANSI Escape Code styling for terminals. ### `custom_styling` (disabled) This flag enables support for custom styling of text inside of cells. - Text formatting still works, even if you roll your own ANSI escape sequences. - Rainbow text - Makes comfy-table 30-50% slower ### `reexport_crossterm` (disabled) 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. By default, a mirrored type is exposed, which internally maps to the crossterm type. 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. **BUT** if you enable this feature, you opt-in for breaking changes on minor/patch versions. 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. ## Contributing Comfy-table's main focus is on being minimalistic and reliable. A fixed set of features that just work for "normal" use-cases: - Normal tables (columns, rows, one cell per column/row). - Dynamic arrangement of content to a given width. - Some kind of manual intervention in the arrangement process. 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 :)! 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. Such features are: - Nested tables - Cells that span over multiple columns/rows - CSV to table conversion and vice versa ## Unsafe Comfy-table doesn't allow `unsafe` code in its code-base. As it's a "simple" formatting library it also shouldn't be needed in the future. If one disables the `tty` feature flag, this is also true for all of its dependencies. However, when enabling `tty`, Comfy-table uses one unsafe function call in its dependencies. \ It can be circumvented by explicitely calling [Table::force_no_tty](https://docs.rs/comfy-table/latest/comfy_table/struct.Table.html#method.force_no_tty). 1. `crossterm::terminal::size`. This function is necessary to detect the current terminal width if we're on a tty. This is only called if no explicit width is provided via `Table::set_width`. This is another libc call which is used to communicate with `/dev/tty` via a file descriptor. ```rust,ignore ... if wrap_with_result(unsafe { ioctl(fd, TIOCGWINSZ.into(), &mut size) }).is_ok() { Ok((size.ws_col, size.ws_row)) } else { tput_size().ok_or_else(|| std::io::Error::last_os_error().into()) } ... ``` ## Comparison with other libraries The following are official statements of the other crate authors. [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. ### Cli-table 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. Currently, this crate only pulls two external dependencies (other than cli-table-derive): - termcolor - unicode-width With csv feature enabled, it also pulls csv crate as dependency. ### Term-table [`term-table`](https://crates.io/crates/term-table) is pretty basic in terms of features. 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. 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. ### Prettytables-rs [`prettytables-rs`](https://crates.io/crates/prettytable-rs) provides functionality for formatting and aligning tables. 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. ### Comfy-table 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. 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. 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. The other focus is on dynamic-length content arrangement. 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. comfy-table-7.1.4/benches/build_large_table.rs000064400000000000000000000024321046102023000174350ustar 00000000000000use criterion::{criterion_group, criterion_main, Criterion}; use comfy_table::presets::UTF8_FULL; use comfy_table::*; use rand::distr::Alphanumeric; use rand::Rng; /// Create a dynamic 10x500 Table with width 300 and unevenly distributed content. /// There are no constraint, the content simply has to be formatted to fit as good as possible into /// the given space. fn build_huge_table() { let mut table = Table::new(); table .load_preset(UTF8_FULL) .set_content_arrangement(ContentArrangement::DynamicFullWidth) .set_width(300) .set_header(vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); let mut rng = rand::rng(); for _ in 0..500 { let mut row = Vec::new(); for _ in 0..10 { let string_length = rng.random_range(2..100); let random_string: String = (&mut rng) .sample_iter(&Alphanumeric) .take(string_length) .map(char::from) .collect(); row.push(random_string); } table.add_row(row); } // Build the table. let _ = table.lines(); } pub fn build_tables(crit: &mut Criterion) { crit.bench_function("Huge table", |b| b.iter(build_huge_table)); } criterion_group!(benches, build_tables); criterion_main!(benches); comfy-table-7.1.4/benches/build_tables.rs000064400000000000000000000067131046102023000164540ustar 00000000000000use criterion::{criterion_group, criterion_main, Criterion}; use comfy_table::presets::UTF8_FULL; use comfy_table::ColumnConstraint::*; use comfy_table::Width::*; use comfy_table::*; /// Build the readme table #[cfg(feature = "tty")] fn build_readme_table() { let mut table = Table::new(); table.load_preset(UTF8_FULL) .set_content_arrangement(ContentArrangement::Dynamic) .set_width(80) .set_header(vec![ Cell::new("Header1").add_attribute(Attribute::Bold), Cell::new("Header2").fg(Color::Green), Cell::new("Header3"), ]) .add_row(vec![ Cell::new("This is a bold text").add_attribute(Attribute::Bold), Cell::new("This is a green text").fg(Color::Green), Cell::new("This one has black background").bg(Color::Black), ]) .add_row(vec![ Cell::new("Blinky boi").add_attribute(Attribute::SlowBlink), Cell::new("This table's content is dynamically arranged. The table is exactly 80 characters wide.\nHere comes a reallylongwordthatshoulddynamicallywrap"), Cell::new("COMBINE ALL THE THINGS") .fg(Color::Green) .bg(Color::Black) .add_attributes(vec![ Attribute::Bold, Attribute::SlowBlink, ]) ]); // Build the table. let _ = table.lines(); } #[cfg(not(feature = "tty"))] fn build_readme_table() { let mut table = Table::new(); table.load_preset(UTF8_FULL) .set_content_arrangement(ContentArrangement::Dynamic) .set_width(80) .set_header(vec![ Cell::new("Header1"), Cell::new("Header2"), Cell::new("Header3"), ]) .add_row(vec![ Cell::new("This is a bold text"), Cell::new("This is a green text"), Cell::new("This one has black background"), ]) .add_row(vec![ Cell::new("Blinky boi"), Cell::new("This table's content is dynamically arranged. The table is exactly 80 characters wide.\nHere comes a reallylongwordthatshoulddynamicallywrap"), Cell::new("COMBINE ALL THE THINGS"), ]); // Build the table. let _ = table.lines(); } /// Create a dynamic 10x10 Table with width 400 and unevenly distributed content. /// On top of that, most of the columns have some kind of constraint. fn build_big_table() { let mut table = Table::new(); table .load_preset(UTF8_FULL) .set_content_arrangement(ContentArrangement::DynamicFullWidth) .set_width(400) .set_header(vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); // Create a 10x10 grid for row_index in 0..10 { let mut row = Vec::new(); for column in 0..10 { row.push("SomeWord ".repeat((column + row_index * 2) % 10)); } table.add_row(row); } table.set_constraints(vec![ UpperBoundary(Fixed(20)), LowerBoundary(Fixed(40)), Absolute(Fixed(5)), Absolute(Percentage(3)), Absolute(Percentage(3)), Boundaries { lower: Fixed(30), upper: Percentage(10), }, ]); // Build the table. let _ = table.lines(); } pub fn build_tables(crit: &mut Criterion) { crit.bench_function("Readme table", |b| b.iter(build_readme_table)); crit.bench_function("Big table", |b| b.iter(build_big_table)); } criterion_group!(benches, build_tables); criterion_main!(benches); comfy-table-7.1.4/codecov.yml000064400000000000000000000004071046102023000142050ustar 00000000000000ignore: - "benches" - "examples" - "proptest_regressions" - "tests" - "CHANGELOG.md" - "Cargo.lock" - "Cargo.toml" - "FAQ.md" - "LICENSE" - "README.md" codecov: status: project: default: target: auto threshold: 2% comfy-table-7.1.4/examples/inner_style.rs000064400000000000000000000020721046102023000165570ustar 00000000000000use comfy_table::{Cell, ContentArrangement, Row, Table}; fn main() { let mut table = Table::new(); //table.load_preset(comfy_table::presets::NOTHING); table.set_content_arrangement(ContentArrangement::Dynamic); table.set_width(85); let mut row = Row::new(); row.add_cell(Cell::new(format!( "List of devices:\n{}", console::style("Blockdevices\nCryptdevices").dim().blue() ))); row.add_cell(Cell::new("")); table.add_row(row); let mut row = Row::new(); row.add_cell(Cell::new(format!( "Block devices: \n/dev/{}\n/dev/{}", console::style("sda1").bold().red(), console::style("sda2").bold().red() ))); row.add_cell(Cell::new("These are some block devices that were found.")); table.add_row(row); let mut row = Row::new(); row.add_cell(Cell::new(format!( "Crypt devices: \n/dev/mapper/{}", console::style("cryptroot").bold().yellow() ))); row.add_cell(Cell::new("This one seems to be encrypted.")); table.add_row(row); println!("{}", table); } comfy-table-7.1.4/examples/readme_table.rs000064400000000000000000000022441046102023000166310ustar 00000000000000use comfy_table::presets::UTF8_FULL; use comfy_table::*; fn main() { let mut table = Table::new(); table.load_preset(UTF8_FULL) .set_content_arrangement(ContentArrangement::Dynamic) .set_width(80) .set_header(vec![ Cell::new("Header1").add_attribute(Attribute::Bold), Cell::new("Header2").fg(Color::Green), Cell::new("Header3"), ]) .add_row(vec![ Cell::new("This is a bold text").add_attribute(Attribute::Bold), Cell::new("This is a green text").fg(Color::Green), Cell::new("This one has black background").bg(Color::Black), ]) .add_row(vec![ Cell::new("Blinky boi").add_attribute(Attribute::SlowBlink), Cell::new("This table's content is dynamically arranged. The table is exactly 80 characters wide.\nHere comes a reallylongwordthatshoulddynamicallywrap"), Cell::new("COMBINE ALL THE THINGS") .fg(Color::Green) .bg(Color::Black) .add_attributes(vec![ Attribute::Bold, Attribute::SlowBlink, ]) ]); println!("{table}"); } comfy-table-7.1.4/examples/readme_table_no_tty.rs000064400000000000000000000017431046102023000202300ustar 00000000000000use comfy_table::presets::UTF8_FULL; use comfy_table::*; // This example works even with the `tty` feature disabled // You can try it out with `cargo run --example no_tty --no-default-features` fn main() { let mut table = Table::new(); table.load_preset(UTF8_FULL) .set_content_arrangement(ContentArrangement::Dynamic) .set_width(80) .set_header(vec![ Cell::new("Header1"), Cell::new("Header2"), Cell::new("Header3"), ]) .add_row(vec![ Cell::new("No bold text without tty"), Cell::new("No colored text without tty"), Cell::new("No custom background without tty"), ]) .add_row(vec![ Cell::new("Blinky boi"), Cell::new("This table's content is dynamically arranged. The table is exactly 80 characters wide.\nHere comes a reallylongwordthatshoulddynamicallywrap"), Cell::new("Done"), ]); println!("{table}"); } comfy-table-7.1.4/src/cell.rs000064400000000000000000000132161046102023000141160ustar 00000000000000#[cfg(feature = "tty")] use crate::{Attribute, Color}; use crate::style::CellAlignment; /// A stylable table cell with content. #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct Cell { /// The content is a list of strings.\ /// This is done to make working with newlines more easily.\ /// When creating a new [Cell], the given content is split by newline. pub(crate) content: Vec, /// The delimiter which is used to split the text into consistent pieces.\ /// The default is ` `. pub(crate) delimiter: Option, pub(crate) alignment: Option, #[cfg(feature = "tty")] pub(crate) fg: Option, #[cfg(feature = "tty")] pub(crate) bg: Option, #[cfg(feature = "tty")] pub(crate) attributes: Vec, } impl Cell { /// Create a new Cell #[allow(clippy::needless_pass_by_value)] pub fn new(content: T) -> Self { let content = content.to_string(); #[cfg_attr(not(feature = "custom_styling"), allow(unused_mut))] let mut split_content: Vec = content.split('\n').map(ToString::to_string).collect(); // Correct ansi codes so style is terminated and resumed around the split #[cfg(feature = "custom_styling")] crate::utils::formatting::content_split::fix_style_in_split_str(&mut split_content); Self { content: split_content, delimiter: None, alignment: None, #[cfg(feature = "tty")] fg: None, #[cfg(feature = "tty")] bg: None, #[cfg(feature = "tty")] attributes: Vec::new(), } } /// Return a copy of the content contained in this cell. pub fn content(&self) -> String { self.content.join("\n") } /// Set the delimiter used to split text for this cell. \ /// Normal text uses spaces (` `) as delimiters. This is necessary to help comfy-table /// understand the concept of _words_. #[must_use] pub fn set_delimiter(mut self, delimiter: char) -> Self { self.delimiter = Some(delimiter); self } /// Set the alignment of content for this cell. /// /// Setting this overwrites alignment settings of the /// [Column](crate::column::Column::set_cell_alignment) for this specific cell. /// ``` /// use comfy_table::CellAlignment; /// use comfy_table::Cell; /// /// let mut cell = Cell::new("Some content") /// .set_alignment(CellAlignment::Center); /// ``` #[must_use] pub fn set_alignment(mut self, alignment: CellAlignment) -> Self { self.alignment = Some(alignment); self } /// Set the foreground text color for this cell. /// /// Look at [Color](crate::Color) for a list of all possible Colors. /// ``` /// use comfy_table::Color; /// use comfy_table::Cell; /// /// let mut cell = Cell::new("Some content") /// .fg(Color::Red); /// ``` #[cfg(feature = "tty")] #[must_use] pub fn fg(mut self, color: Color) -> Self { self.fg = Some(color); self } /// Set the background color for this cell. /// /// Look at [Color](crate::Color) for a list of all possible Colors. /// ``` /// use comfy_table::Color; /// use comfy_table::Cell; /// /// let mut cell = Cell::new("Some content") /// .bg(Color::Red); /// ``` #[cfg(feature = "tty")] #[must_use] pub fn bg(mut self, color: Color) -> Self { self.bg = Some(color); self } /// Add a styling attribute to the content cell.\ /// Those can be **bold**, _italic_, blinking and many more. /// /// Look at [Attribute](crate::Attribute) for a list of all possible Colors. /// ``` /// use comfy_table::Attribute; /// use comfy_table::Cell; /// /// let mut cell = Cell::new("Some content") /// .add_attribute(Attribute::Bold); /// ``` #[cfg(feature = "tty")] #[must_use] pub fn add_attribute(mut self, attribute: Attribute) -> Self { self.attributes.push(attribute); self } /// Same as add_attribute, but you can pass a vector of [Attributes](Attribute) #[cfg(feature = "tty")] #[must_use] pub fn add_attributes(mut self, mut attribute: Vec) -> Self { self.attributes.append(&mut attribute); self } } /// Convert anything with [ToString] to a new [Cell]. /// /// ``` /// # use comfy_table::Cell; /// let cell: Cell = "content".into(); /// let cell: Cell = 5u32.into(); /// ``` impl From for Cell { fn from(content: T) -> Self { Self::new(content) } } /// A simple wrapper type for a `Vec`. /// /// This wrapper is needed to support generic conversions between iterables and `Vec`. /// Check the trait implementations for more docs. pub struct Cells(pub Vec); /// Allow the conversion of a type to a [Cells], which is a simple vector of cells. /// /// By default this is implemented for all Iterators over items implementing [ToString]. /// /// ``` /// use comfy_table::{Row, Cells}; /// /// let cells_string: Cells = vec!["One", "Two", "Three"].into(); /// let cells_integer: Cells = vec![1, 2, 3, 4].into(); /// ``` impl From for Cells where T: IntoIterator, T::Item: Into, { fn from(cells: T) -> Self { Self(cells.into_iter().map(Into::into).collect()) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_column_generation() { let content = "This is\nsome multiline\nstring".to_string(); let cell = Cell::new(content.clone()); assert_eq!(cell.content(), content); } } comfy-table-7.1.4/src/column.rs000064400000000000000000000101771046102023000144770ustar 00000000000000use crate::style::{CellAlignment, ColumnConstraint}; /// A representation of a table's column. /// Useful for styling and specifying constraints how big a column should be. /// /// 1. Content padding for cells in this column /// 2. Constraints on how wide this column shall be /// 3. Default alignment for cells in this column /// /// Columns are generated when adding rows or a header to a table.\ /// As a result columns can only be modified after the table is populated by some data. /// /// ``` /// use comfy_table::{Width::*, CellAlignment, ColumnConstraint::*, Table}; /// /// let mut table = Table::new(); /// table.set_header(&vec!["one", "two"]); /// /// let mut column = table.column_mut(1).expect("This should be column two"); /// /// // Set the max width for all cells of this column to 20 characters. /// column.set_constraint(UpperBoundary(Fixed(20))); /// /// // Set the left padding to 5 spaces and the right padding to 1 space /// column.set_padding((5, 1)); /// /// // Align content in all cells of this column to the center of the cell. /// column.set_cell_alignment(CellAlignment::Center); /// ``` #[derive(Debug, Clone)] pub struct Column { /// The index of the column pub index: usize, /// Left/right padding for each cell of this column in spaces pub(crate) padding: (u16, u16), /// The delimiter which is used to split the text into consistent pieces. /// Default is ` `. pub(crate) delimiter: Option, /// Define the [CellAlignment] for all cells of this column pub(crate) cell_alignment: Option, pub(crate) constraint: Option, } impl Column { pub fn new(index: usize) -> Self { Self { index, padding: (1, 1), delimiter: None, constraint: None, cell_alignment: None, } } /// Set the padding for all cells of this column. /// /// Padding is provided in the form of (left, right).\ /// Default is `(1, 1)`. pub fn set_padding(&mut self, padding: (u16, u16)) -> &mut Self { self.padding = padding; self } /// Convenience helper that returns the total width of the combined padding. pub fn padding_width(&self) -> u16 { self.padding.0.saturating_add(self.padding.1) } /// Set the delimiter used to split text for this column's cells. /// /// A custom delimiter on a cell in will overwrite the column's delimiter. /// Normal text uses spaces (` `) as delimiters. This is necessary to help comfy-table /// understand the concept of _words_. pub fn set_delimiter(&mut self, delimiter: char) -> &mut Self { self.delimiter = Some(delimiter); self } /// Constraints allow to influence the auto-adjustment behavior of columns.\ /// This can be useful to counter undesired auto-adjustment of content in tables. pub fn set_constraint(&mut self, constraint: ColumnConstraint) -> &mut Self { self.constraint = Some(constraint); self } /// Get the constraint that is used for this column. pub fn constraint(&self) -> Option<&ColumnConstraint> { self.constraint.as_ref() } /// Remove any constraint on this column pub fn remove_constraint(&mut self) -> &mut Self { self.constraint = None; self } /// Returns weather the columns is hidden via [ColumnConstraint::Hidden]. pub fn is_hidden(&self) -> bool { matches!(self.constraint, Some(ColumnConstraint::Hidden)) } /// Set the alignment for content inside of cells for this column.\ /// **Note:** Alignment on a cell will always overwrite the column's setting. pub fn set_cell_alignment(&mut self, alignment: CellAlignment) { self.cell_alignment = Some(alignment); } } #[cfg(test)] mod tests { use super::*; #[test] fn test_column() { let mut column = Column::new(0); column.set_padding((0, 0)); column.set_constraint(ColumnConstraint::ContentWidth); assert_eq!(column.constraint(), Some(&ColumnConstraint::ContentWidth)); column.remove_constraint(); assert_eq!(column.constraint(), None); } } comfy-table-7.1.4/src/lib.rs000064400000000000000000000014701046102023000137440ustar 00000000000000#![forbid(unsafe_code)] #![doc = include_str!("../README.md")] // The README code examples should be valid mini scripts to make them properly testable. #![allow(clippy::needless_doctest_main)] // Had a few false-positives on v1.81. Check lateron if they're still there. #![allow(clippy::manual_unwrap_or)] mod cell; mod column; mod row; mod style; mod table; #[cfg(feature = "integration_test")] /// We publicly expose the internal [utils] module for our integration tests. /// There's some logic we need from inside here. /// The API inside of this isn't considered stable and shouldnt' be used. pub mod utils; #[cfg(not(feature = "integration_test"))] mod utils; pub use crate::cell::{Cell, Cells}; pub use crate::column::Column; pub use crate::row::Row; pub use crate::table::{ColumnCellIter, Table}; pub use style::*; comfy-table-7.1.4/src/row.rs000064400000000000000000000074411046102023000140110ustar 00000000000000use std::slice::Iter; use crate::{ cell::{Cell, Cells}, utils::formatting::content_split::measure_text_width, }; /// Each row contains [Cells](crate::Cell) and can be added to a [Table](crate::Table). #[derive(Clone, Debug, Default)] pub struct Row { /// Index of the row. /// This will be set as soon as the row is added to the table. pub(crate) index: Option, pub(crate) cells: Vec, pub(crate) max_height: Option, } impl Row { pub fn new() -> Self { Self::default() } /// Add a cell to the row. /// /// **Attention:** /// If a row has already been added to a table and you add more cells to it /// than there're columns currently know to the [Table](crate::Table) struct, /// these columns won't be known to the table unless you call /// [crate::Table::discover_columns]. /// /// ```rust /// use comfy_table::{Row, Cell}; /// /// let mut row = Row::new(); /// row.add_cell(Cell::new("One")); /// ``` pub fn add_cell(&mut self, cell: Cell) -> &mut Self { self.cells.push(cell); self } /// Truncate content of cells which occupies more than X lines of space. /// /// ``` /// use comfy_table::{Row, Cell}; /// /// let mut row = Row::new(); /// row.max_height(5); /// ``` pub fn max_height(&mut self, lines: usize) -> &mut Self { self.max_height = Some(lines); self } /// Get the longest content width for all cells of this row pub(crate) fn max_content_widths(&self) -> Vec { // Iterate over all cells self.cells .iter() .map(|cell| { // Iterate over all content strings and return a vector of string widths. // Each entry represents the longest string width for a cell. cell.content .iter() .map(|string| measure_text_width(string)) .max() .unwrap_or(0) }) .collect() } /// Get the amount of cells on this row. pub fn cell_count(&self) -> usize { self.cells.len() } /// Returns an iterator over all cells of this row pub fn cell_iter(&self) -> Iter { self.cells.iter() } } /// Create a Row from any `Into`. \ /// [Cells] is a simple wrapper around a `Vec`. /// /// Check the [From] implementations on [Cell] for more information. /// /// ```rust /// use comfy_table::{Row, Cell}; /// /// let row = Row::from(vec!["One", "Two", "Three",]); /// let row = Row::from(vec![ /// Cell::new("One"), /// Cell::new("Two"), /// Cell::new("Three"), /// ]); /// let row = Row::from(vec![1, 2, 3, 4]); /// ``` impl> From for Row { fn from(cells: T) -> Self { Self { index: None, cells: cells.into().0, max_height: None, } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_correct_max_content_width() { let row = Row::from(vec![ "", "four", "fivef", "sixsix", "11 but with\na newline", ]); let max_content_widths = row.max_content_widths(); assert_eq!(max_content_widths, vec![0, 4, 5, 6, 11]); } #[test] fn test_some_functions() { let cells = ["one", "two", "three"]; let mut row = Row::new(); for cell in cells.iter() { row.add_cell(Cell::new(cell)); } assert_eq!(row.cell_count(), cells.len()); let mut cell_content_iter = cells.iter(); for cell in row.cell_iter() { assert_eq!( cell.content(), cell_content_iter.next().unwrap().to_string() ); } } } comfy-table-7.1.4/src/style/attribute.rs000064400000000000000000000127641046102023000163510ustar 00000000000000/// Represents an attribute. /// /// # Platform-specific Notes /// /// * Only UNIX and Windows 10 terminals do support text attributes. /// * Keep in mind that not all terminals support all attributes. /// * Crossterm implements almost all attributes listed in the /// [SGR parameters](https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters). /// /// | Attribute | Windows | UNIX | Notes | /// | :-- | :--: | :--: | :-- | /// | `Reset` | โœ“ | โœ“ | | /// | `Bold` | โœ“ | โœ“ | | /// | `Dim` | โœ“ | โœ“ | | /// | `Italic` | ? | ? | Not widely supported, sometimes treated as inverse. | /// | `Underlined` | โœ“ | โœ“ | | /// | `SlowBlink` | ? | ? | Not widely supported, sometimes treated as inverse. | /// | `RapidBlink` | ? | ? | Not widely supported. MS-DOS ANSI.SYS; 150+ per minute. | /// | `Reverse` | โœ“ | โœ“ | | /// | `Hidden` | โœ“ | โœ“ | Also known as Conceal. | /// | `Fraktur` | โœ— | โœ“ | Legible characters, but marked for deletion. | /// | `DefaultForegroundColor` | ? | ? | Implementation specific (according to standard). | /// | `DefaultBackgroundColor` | ? | ? | Implementation specific (according to standard). | /// | `Framed` | ? | ? | Not widely supported. | /// | `Encircled` | ? | ? | This should turn on the encircled attribute. | /// | `OverLined` | ? | ? | This should draw a line at the top of the text. | /// /// Usage: /// /// Check [crate::Cell::add_attribute] on how to use it. #[derive(Copy, Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)] #[non_exhaustive] pub enum Attribute { /// Resets all the attributes. Reset, /// Increases the text intensity. Bold, /// Decreases the text intensity. Dim, /// Emphasises the text. Italic, /// Underlines the text. Underlined, // Other types of underlining /// Double underlines the text. DoubleUnderlined, /// Undercurls the text. Undercurled, /// Underdots the text. Underdotted, /// Underdashes the text. Underdashed, /// Makes the text blinking (< 150 per minute). SlowBlink, /// Makes the text blinking (>= 150 per minute). RapidBlink, /// Swaps foreground and background colors. Reverse, /// Hides the text (also known as Conceal). Hidden, /// Crosses the text. CrossedOut, /// Sets the [Fraktur](https://en.wikipedia.org/wiki/Fraktur) typeface. /// /// Mostly used for [mathematical alphanumeric symbols](https://en.wikipedia.org/wiki/Mathematical_Alphanumeric_Symbols). Fraktur, /// Turns off the `Bold` attribute. - Inconsistent - Prefer to use NormalIntensity NoBold, /// Switches the text back to normal intensity (no bold, italic). NormalIntensity, /// Turns off the `Italic` attribute. NoItalic, /// Turns off the `Underlined` attribute. NoUnderline, /// Turns off the text blinking (`SlowBlink` or `RapidBlink`). NoBlink, /// Turns off the `Reverse` attribute. NoReverse, /// Turns off the `Hidden` attribute. NoHidden, /// Turns off the `CrossedOut` attribute. NotCrossedOut, /// Makes the text framed. Framed, /// Makes the text encircled. Encircled, /// Draws a line at the top of the text. OverLined, /// Turns off the `Frame` and `Encircled` attributes. NotFramedOrEncircled, /// Turns off the `OverLined` attribute. NotOverLined, } /// Map the internal mirrored [Attribute] to the actually used [crossterm::style::Attribute] pub(crate) fn map_attribute(attribute: Attribute) -> crossterm::style::Attribute { match attribute { Attribute::Reset => crossterm::style::Attribute::Reset, Attribute::Bold => crossterm::style::Attribute::Bold, Attribute::Dim => crossterm::style::Attribute::Dim, Attribute::Italic => crossterm::style::Attribute::Italic, Attribute::Underlined => crossterm::style::Attribute::Underlined, Attribute::DoubleUnderlined => crossterm::style::Attribute::DoubleUnderlined, Attribute::Undercurled => crossterm::style::Attribute::Undercurled, Attribute::Underdotted => crossterm::style::Attribute::Underdotted, Attribute::Underdashed => crossterm::style::Attribute::Underdashed, Attribute::SlowBlink => crossterm::style::Attribute::SlowBlink, Attribute::RapidBlink => crossterm::style::Attribute::RapidBlink, Attribute::Reverse => crossterm::style::Attribute::Reverse, Attribute::Hidden => crossterm::style::Attribute::Hidden, Attribute::CrossedOut => crossterm::style::Attribute::CrossedOut, Attribute::Fraktur => crossterm::style::Attribute::Fraktur, Attribute::NoBold => crossterm::style::Attribute::NoBold, Attribute::NormalIntensity => crossterm::style::Attribute::NormalIntensity, Attribute::NoItalic => crossterm::style::Attribute::NoItalic, Attribute::NoUnderline => crossterm::style::Attribute::NoUnderline, Attribute::NoBlink => crossterm::style::Attribute::NoBlink, Attribute::NoReverse => crossterm::style::Attribute::NoReverse, Attribute::NoHidden => crossterm::style::Attribute::NoHidden, Attribute::NotCrossedOut => crossterm::style::Attribute::NotCrossedOut, Attribute::Framed => crossterm::style::Attribute::Framed, Attribute::Encircled => crossterm::style::Attribute::Encircled, Attribute::OverLined => crossterm::style::Attribute::OverLined, Attribute::NotFramedOrEncircled => crossterm::style::Attribute::NotFramedOrEncircled, Attribute::NotOverLined => crossterm::style::Attribute::NotOverLined, } } comfy-table-7.1.4/src/style/cell.rs000064400000000000000000000010641046102023000152540ustar 00000000000000/// This can be set on [columns](crate::Column::set_cell_alignment) and [cells](crate::Cell::set_alignment). /// /// Determines how content of cells should be aligned. /// /// ```text /// +----------------------+ /// | Header1 | /// +======================+ /// | Left | /// |----------------------+ /// | center | /// |----------------------+ /// | right | /// +----------------------+ /// ``` #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub enum CellAlignment { Left, Right, Center, } comfy-table-7.1.4/src/style/color.rs000064400000000000000000000070311046102023000154530ustar 00000000000000/// Represents a color. /// /// This type is a simplified re-implementation of crossterm's Color enum. /// See [crossterm::style::color](https://docs.rs/crossterm/latest/crossterm/style/enum.Color.html) /// /// # Platform-specific Notes /// /// The following list of 16 base colors are available for almost all terminals (Windows 7 and 8 included). /// /// | Light | Dark | /// | :--------- | :------------ | /// | `DarkGrey` | `Black` | /// | `Red` | `DarkRed` | /// | `Green` | `DarkGreen` | /// | `Yellow` | `DarkYellow` | /// | `Blue` | `DarkBlue` | /// | `Magenta` | `DarkMagenta` | /// | `Cyan` | `DarkCyan` | /// | `White` | `Grey` | /// /// Most UNIX terminals and Windows 10 consoles support additional colors. /// See [Color::Rgb] or [Color::AnsiValue] for more info. /// /// Usage: /// /// Check [crate::Cell::bg], [crate::Cell::fg] and on how to use it. #[derive(Copy, Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)] pub enum Color { /// Resets the terminal color. Reset, /// Black color. Black, /// Dark grey color. DarkGrey, /// Light red color. Red, /// Dark red color. DarkRed, /// Light green color. Green, /// Dark green color. DarkGreen, /// Light yellow color. Yellow, /// Dark yellow color. DarkYellow, /// Light blue color. Blue, /// Dark blue color. DarkBlue, /// Light magenta color. Magenta, /// Dark magenta color. DarkMagenta, /// Light cyan color. Cyan, /// Dark cyan color. DarkCyan, /// White color. White, /// Grey color. Grey, /// An RGB color. See [RGB color model](https://en.wikipedia.org/wiki/RGB_color_model) for more info. /// /// Most UNIX terminals and Windows 10 supported only. /// See [Platform-specific notes](enum.Color.html#platform-specific-notes) for more info. Rgb { r: u8, g: u8, b: u8 }, /// An ANSI color. See [256 colors - cheat sheet](https://jonasjacek.github.io/colors/) for more info. /// /// Most UNIX terminals and Windows 10 supported only. /// See [Platform-specific notes](enum.Color.html#platform-specific-notes) for more info. AnsiValue(u8), } /// Map the internal mirrored [Color] enum to the actually used [crossterm::style::Color]. pub(crate) fn map_color(color: Color) -> crossterm::style::Color { match color { Color::Reset => crossterm::style::Color::Reset, Color::Black => crossterm::style::Color::Black, Color::DarkGrey => crossterm::style::Color::DarkGrey, Color::Red => crossterm::style::Color::Red, Color::DarkRed => crossterm::style::Color::DarkRed, Color::Green => crossterm::style::Color::Green, Color::DarkGreen => crossterm::style::Color::DarkGreen, Color::Yellow => crossterm::style::Color::Yellow, Color::DarkYellow => crossterm::style::Color::DarkYellow, Color::Blue => crossterm::style::Color::Blue, Color::DarkBlue => crossterm::style::Color::DarkBlue, Color::Magenta => crossterm::style::Color::Magenta, Color::DarkMagenta => crossterm::style::Color::DarkMagenta, Color::Cyan => crossterm::style::Color::Cyan, Color::DarkCyan => crossterm::style::Color::DarkCyan, Color::White => crossterm::style::Color::White, Color::Grey => crossterm::style::Color::Grey, Color::Rgb { r, g, b } => crossterm::style::Color::Rgb { r, g, b }, Color::AnsiValue(value) => crossterm::style::Color::AnsiValue(value), } } comfy-table-7.1.4/src/style/column.rs000064400000000000000000000036001046102023000156300ustar 00000000000000/// A Constraint can be added to a [columns](crate::Column). /// /// They allow some control over Column widths as well as the dynamic arrangement process. /// /// All percental boundaries will be ignored, if: /// - you aren't using one of ContentArrangement::{Dynamic, DynamicFullWidth} /// - the width of the table/terminal cannot be determined. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum ColumnConstraint { /// This will completely hide a column. Hidden, /// Force the column to be as long as it's content. /// Use with caution! This can easily mess up your table formatting, /// if a column's content is overly long. ContentWidth, /// Enforce a absolute width for a column. Absolute(Width), /// Specify a lower boundary, either fixed or as percentage of the total width. /// A column with this constraint will be at least as wide as specified. /// If the content isn't as long as that boundary, it will be padded. /// If the column has longer content and is allowed to grow, the column may take more space. LowerBoundary(Width), /// Specify a upper boundary, either fixed or as percentage of the total width. /// A column with this constraint will be at most as wide as specified. /// The column may be smaller than that width. UpperBoundary(Width), /// Specify both, an upper and a lower boundary. Boundaries { lower: Width, upper: Width }, } #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum Width { /// A fixed amount of characters. Fixed(u16), /// A width equivalent to a certain percentage of the available width. /// Values above 100 will be automatically reduced to 100. /// /// **Warning:** This option will be ignored if: /// - you aren't using one of ContentArrangement::{Dynamic, DynamicFullWidth} /// - the width of the table/terminal cannot be determined. Percentage(u16), } comfy-table-7.1.4/src/style/mod.rs000064400000000000000000000040241046102023000151130ustar 00000000000000#[cfg(all(feature = "tty", not(feature = "reexport_crossterm")))] mod attribute; mod cell; #[cfg(all(feature = "tty", not(feature = "reexport_crossterm")))] mod color; mod column; /// Contains modifiers, that can be used to alter certain parts of a preset.\ /// For instance, the [UTF8_ROUND_CORNERS](modifiers::UTF8_ROUND_CORNERS) replaces all corners with round UTF8 box corners. pub mod modifiers; /// This module provides styling presets for tables.\ /// Every preset has an example preview. pub mod presets; mod table; pub use cell::CellAlignment; pub use column::{ColumnConstraint, Width}; #[cfg(feature = "tty")] pub(crate) use styling_enums::{map_attribute, map_color}; #[cfg(feature = "tty")] pub use styling_enums::{Attribute, Color}; pub use table::{ContentArrangement, TableComponent}; /// Convenience module to have cleaner and "identical" conditional re-exports for style enums. #[cfg(all(feature = "tty", not(feature = "reexport_crossterm")))] mod styling_enums { pub use super::attribute::*; pub use super::color::*; } /// Re-export the crossterm type directly instead of using the internal mirrored types. /// This result in possible ABI incompatibilities when using comfy_table and crossterm in the same /// project with different versions, but may also be very convenient for developers. #[cfg(all(feature = "tty", feature = "reexport_crossterm"))] mod styling_enums { /// Attributes used for styling cell content. Reexport of crossterm's [Attributes](crossterm::style::Attribute) enum. pub use crossterm::style::Attribute; /// Colors used for styling cell content. Reexport of crossterm's [Color](crossterm::style::Color) enum. pub use crossterm::style::Color; /// Convenience function to have the same mapping code for reexported types. #[inline] pub(crate) fn map_attribute(attribute: Attribute) -> Attribute { attribute } /// Convenience function to have the same mapping code for reexported types. #[inline] pub(crate) fn map_color(color: Color) -> Color { color } } comfy-table-7.1.4/src/style/modifiers.rs000064400000000000000000000016771046102023000163300ustar 00000000000000/// A modifier, that when applied will convert the outer corners to round corners. /// ```text /// โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ /// โ”‚ Hello โ”‚ there โ”‚ /// โ•žโ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•ก /// โ”‚ a โ”† b โ”‚ /// โ”œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ผโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ค /// โ”‚ c โ”† d โ”‚ /// โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ /// ``` pub const UTF8_ROUND_CORNERS: &str = " โ•ญโ•ฎโ•ฐโ•ฏ"; /// A modifier, that when applied will convert the inner borders to solid lines. /// ```text /// โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ /// โ”‚ Hello โ”‚ there โ”‚ /// โ•žโ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•ก /// โ”‚ a โ”‚ b โ”‚ /// โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค /// โ”‚ c โ”‚ d โ”‚ /// โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ /// ``` pub const UTF8_SOLID_INNER_BORDERS: &str = " โ”‚โ”€ "; comfy-table-7.1.4/src/style/presets.rs000064400000000000000000000102001046102023000160120ustar 00000000000000/// The default style for tables. /// /// ```text /// +-------+-------+ /// | Hello | there | /// +===============+ /// | a | b | /// |-------+-------| /// | c | d | /// +-------+-------+ /// ``` pub const ASCII_FULL: &str = "||--+==+|-+||++++++"; /// Just like ASCII_FULL, but without dividers between rows. /// /// ```text /// +-------+-------+ /// | Hello | there | /// +===============+ /// | a | b | /// | c | d | /// +-------+-------+ pub const ASCII_FULL_CONDENSED: &str = "||--+==+| ++++++"; /// Just like ASCII_FULL, but without any borders. /// /// ```text /// Hello | there /// =============== /// a | b /// -------+------- /// c | d /// ``` pub const ASCII_NO_BORDERS: &str = " == |-+ "; /// Just like ASCII_FULL, but without vertical/horizontal middle lines. /// /// ```text /// +---------------+ /// | Hello there | /// +===============+ /// | a b | /// | | /// | c d | /// +---------------+ /// ``` pub const ASCII_BORDERS_ONLY: &str = "||--+==+ ||--++++"; /// Just like ASCII_BORDERS_ONLY, but without spacing between rows. /// /// ```text /// +---------------+ /// | Hello there | /// +===============+ /// | a b | /// | c d | /// +---------------+ /// ``` pub const ASCII_BORDERS_ONLY_CONDENSED: &str = "||--+==+ --++++"; /// Just like ASCII_FULL, but without vertical/horizontal middle lines and no side borders. /// /// ```text /// --------------- /// Hello there /// =============== /// a b /// --------------- /// c d /// --------------- /// ``` pub const ASCII_HORIZONTAL_ONLY: &str = " -- == -- -- "; /// Markdown like table styles. /// /// ```text /// | Hello | there | /// |-------|-------| /// | a | b | /// | c | d | /// ``` pub const ASCII_MARKDOWN: &str = "|| |-||| "; /// The UTF8 enabled version of the default style for tables.\ /// Quite beautiful isn't it? It's drawn with UTF8's box drawing characters. /// /// ```text /// โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” /// โ”‚ Hello โ”† there โ”‚ /// โ•žโ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•ก /// โ”‚ a โ”† b โ”‚ /// โ”œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ผโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ค /// โ”‚ c โ”† d โ”‚ /// โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ /// ``` pub const UTF8_FULL: &str = "โ”‚โ”‚โ”€โ”€โ•žโ•โ•ชโ•กโ”†โ•Œโ”ผโ”œโ”คโ”ฌโ”ดโ”Œโ”โ””โ”˜"; /// Default UTF8 style, but without dividers between rows. /// /// ```text /// โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” /// โ”‚ Hello โ”† there โ”‚ /// โ•žโ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•ก /// โ”‚ a โ”† b โ”‚ /// โ”‚ c โ”† d โ”‚ /// โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ /// ``` pub const UTF8_FULL_CONDENSED: &str = "โ”‚โ”‚โ”€โ”€โ•žโ•โ•ชโ•กโ”† โ”ฌโ”ดโ”Œโ”โ””โ”˜"; /// Default UTF8 style, but without any borders. /// /// ```text /// Hello โ”† there /// โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ• /// a โ”† b /// โ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ผโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œ /// c โ”† d /// ``` pub const UTF8_NO_BORDERS: &str = " โ•โ•ช โ”†โ•Œโ”ผ "; /// Just like the UTF8_FULL style, but without vertical/horizontal middle lines. /// /// ```text /// โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” /// โ”‚ Hello there โ”‚ /// โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ก /// โ”‚ a b โ”‚ /// โ”‚ c d โ”‚ /// โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ /// ``` pub const UTF8_BORDERS_ONLY: &str = "โ”‚โ”‚โ”€โ”€โ•žโ•โ•โ•ก โ”€โ”€โ”Œโ”โ””โ”˜"; /// Only display vertical lines. /// /// ```text /// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ /// Hello there /// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• /// a b /// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ /// c d /// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ /// ``` pub const UTF8_HORIZONTAL_ONLY: &str = " โ”€โ”€ โ•โ• โ”€โ”€ โ”€โ”€ "; /// Don't draw any borders or other lines. /// Useful, if you want to simply organize some data without any cosmetics. /// /// ```text /// Hello there /// a b /// c d /// ``` pub const NOTHING: &str = " "; comfy-table-7.1.4/src/style/table.rs000064400000000000000000000070241046102023000154260ustar 00000000000000/// Specify how comfy_table should arrange the content in your table. /// /// ``` /// use comfy_table::{Table, ContentArrangement}; /// /// let mut table = Table::new(); /// table.set_content_arrangement(ContentArrangement::Dynamic); /// ``` #[derive(Clone, Debug)] pub enum ContentArrangement { /// Don't do any content arrangement.\ /// Tables with this mode might become wider than your output and look ugly.\ /// Constraints on columns are still respected. Disabled, /// Dynamically determine the width of columns in regard to terminal width and content length.\ /// With this mode, the content in cells will wrap dynamically to get the the best column layout /// for the given content.\ /// Constraints on columns are still respected. /// /// **Warning:** If terminal width cannot be determined and no table_width is set via /// [Table::set_width](crate::table::Table::set_width), /// this option won't work and [Disabled](ContentArrangement::Disabled) will be used as a fallback. Dynamic, /// This is mode is the same as the [ContentArrangement::Dynamic] arrangement, but it will always use as much /// space as it's given. Any surplus space will be distributed between all columns. DynamicFullWidth, } /// All configurable table components. /// A character can be assigned to each component via [Table::set_style](crate::table::Table::set_style). /// This is then used to draw character of the respective component to the commandline. /// /// I hope that most component names are self-explanatory. Just in case: /// BorderIntersections are Intersections, where rows/columns lines meet outer borders. /// E.g.: /// ```text /// --------- /// v | /// +---+---+---+ | /// | a | b | c | | /// +===+===+===+<-| /// | | | | | /// +---+---+---+<-- These "+" chars are Borderintersections. /// | | | | The inner "+" chars are MiddleIntersections /// +---+---+---+ /// ``` #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] pub enum TableComponent { LeftBorder, RightBorder, TopBorder, BottomBorder, LeftHeaderIntersection, HeaderLines, MiddleHeaderIntersections, RightHeaderIntersection, VerticalLines, HorizontalLines, MiddleIntersections, LeftBorderIntersections, RightBorderIntersections, TopBorderIntersections, BottomBorderIntersections, TopLeftCorner, TopRightCorner, BottomLeftCorner, BottomRightCorner, } impl TableComponent { const fn components() -> [TableComponent; 19] { [ TableComponent::LeftBorder, TableComponent::RightBorder, TableComponent::TopBorder, TableComponent::BottomBorder, TableComponent::LeftHeaderIntersection, TableComponent::HeaderLines, TableComponent::MiddleHeaderIntersections, TableComponent::RightHeaderIntersection, TableComponent::VerticalLines, TableComponent::HorizontalLines, TableComponent::MiddleIntersections, TableComponent::LeftBorderIntersections, TableComponent::RightBorderIntersections, TableComponent::TopBorderIntersections, TableComponent::BottomBorderIntersections, TableComponent::TopLeftCorner, TableComponent::TopRightCorner, TableComponent::BottomLeftCorner, TableComponent::BottomRightCorner, ] } pub fn iter() -> impl Iterator { TableComponent::components().into_iter() } } comfy-table-7.1.4/src/table.rs000064400000000000000000000710621046102023000142710ustar 00000000000000use std::collections::HashMap; use std::fmt; use std::iter::IntoIterator; use std::slice::{Iter, IterMut}; #[cfg(feature = "tty")] use crossterm::terminal; #[cfg(feature = "tty")] use crossterm::tty::IsTty; use crate::cell::Cell; use crate::column::Column; use crate::row::Row; use crate::style::presets::ASCII_FULL; use crate::style::{ColumnConstraint, ContentArrangement, TableComponent}; use crate::utils::build_table; /// This is the main interface for building a table. /// Each table consists of [Rows](Row), which in turn contain [Cells](crate::cell::Cell). /// /// There also exists a representation of a [Column]. /// Columns are automatically created when adding rows to a table. #[derive(Debug, Clone)] pub struct Table { pub(crate) columns: Vec, style: HashMap, pub(crate) header: Option, pub(crate) rows: Vec, pub(crate) arrangement: ContentArrangement, pub(crate) delimiter: Option, pub(crate) truncation_indicator: String, #[cfg(feature = "tty")] no_tty: bool, #[cfg(feature = "tty")] use_stderr: bool, width: Option, #[cfg(feature = "tty")] enforce_styling: bool, /// Define whether everything in a cells should be styled, including whitespaces /// or whether only the text should be styled. #[cfg(feature = "tty")] pub(crate) style_text_only: bool, } impl fmt::Display for Table { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.lines().collect::>().join("\n")) } } impl Default for Table { fn default() -> Self { Self::new() } } impl Table { /// Create a new table with default ASCII styling. pub fn new() -> Self { let mut table = Self { columns: Vec::new(), header: None, rows: Vec::new(), arrangement: ContentArrangement::Disabled, delimiter: None, truncation_indicator: "...".to_string(), #[cfg(feature = "tty")] no_tty: false, #[cfg(feature = "tty")] use_stderr: false, width: None, style: HashMap::new(), #[cfg(feature = "tty")] enforce_styling: false, #[cfg(feature = "tty")] style_text_only: false, }; table.load_preset(ASCII_FULL); table } /// This is an alternative `fmt` function, which simply removes any trailing whitespaces. /// Trailing whitespaces often occur, when using tables without a right border. pub fn trim_fmt(&self) -> String { self.lines() .map(|line| line.trim_end().to_string()) .collect::>() .join("\n") } /// This is an alternative to `fmt`, but rather returns an iterator to each line, rather than /// one String separated by newlines. pub fn lines(&self) -> impl Iterator { build_table(self) } /// Set the header row of the table. This is usually the title of each column.\ /// There'll be no header unless you explicitly set it with this function. /// /// ``` /// use comfy_table::{Table, Row}; /// /// let mut table = Table::new(); /// let header = Row::from(vec!["Header One", "Header Two"]); /// table.set_header(header); /// ``` pub fn set_header>(&mut self, row: T) -> &mut Self { let row = row.into(); self.autogenerate_columns(&row); self.header = Some(row); self } pub fn header(&self) -> Option<&Row> { self.header.as_ref() } /// Returns the number of currently present columns. /// /// ``` /// use comfy_table::Table; /// /// let mut table = Table::new(); /// table.set_header(vec!["Col 1", "Col 2", "Col 3"]); /// /// assert_eq!(table.column_count(), 3); /// ``` pub fn column_count(&mut self) -> usize { self.discover_columns(); self.columns.len() } /// Add a new row to the table. /// /// ``` /// use comfy_table::{Table, Row}; /// /// let mut table = Table::new(); /// table.add_row(vec!["One", "Two"]); /// ``` pub fn add_row>(&mut self, row: T) -> &mut Self { let mut row = row.into(); self.autogenerate_columns(&row); row.index = Some(self.rows.len()); self.rows.push(row); self } /// Add a new row to the table if the predicate evaluates to `true`. /// /// ``` /// use comfy_table::{Table, Row}; /// /// let mut table = Table::new(); /// table.add_row_if(|index, row| true, vec!["One", "Two"]); /// ``` pub fn add_row_if(&mut self, predicate: P, row: T) -> &mut Self where P: Fn(usize, &T) -> bool, T: Into, { if predicate(self.rows.len(), &row) { return self.add_row(row); } self } /// Add multiple rows to the table. /// /// ``` /// use comfy_table::{Table, Row}; /// /// let mut table = Table::new(); /// let rows = vec![ /// vec!["One", "Two"], /// vec!["Three", "Four"] /// ]; /// table.add_rows(rows); /// ``` pub fn add_rows(&mut self, rows: I) -> &mut Self where I: IntoIterator, I::Item: Into, { for row in rows.into_iter() { let mut row = row.into(); self.autogenerate_columns(&row); row.index = Some(self.rows.len()); self.rows.push(row); } self } /// Add multiple rows to the table if the predicate evaluates to `true`. /// /// ``` /// use comfy_table::{Table, Row}; /// /// let mut table = Table::new(); /// let rows = vec![ /// vec!["One", "Two"], /// vec!["Three", "Four"] /// ]; /// table.add_rows_if(|index, rows| true, rows); /// ``` pub fn add_rows_if(&mut self, predicate: P, rows: I) -> &mut Self where P: Fn(usize, &I) -> bool, I: IntoIterator, I::Item: Into, { if predicate(self.rows.len(), &rows) { return self.add_rows(rows); } self } /// Returns the number of currently present rows. /// /// ``` /// use comfy_table::Table; /// /// let mut table = Table::new(); /// table.add_row(vec!["One", "Two"]); /// /// assert_eq!(table.row_count(), 1); /// ``` pub fn row_count(&self) -> usize { self.rows.len() } /// Returns if the table is empty (contains no data rows). /// /// ``` /// use comfy_table::Table; /// /// let mut table = Table::new(); /// assert!(table.is_empty()); /// /// table.add_row(vec!["One", "Two"]); /// assert!(!table.is_empty()); /// ``` pub fn is_empty(&self) -> bool { self.rows.is_empty() } /// Enforce a max width that should be used in combination with [dynamic content arrangement](ContentArrangement::Dynamic).\ /// This is usually not necessary, if you plan to output your table to a tty, /// since the terminal width can be automatically determined. pub fn set_width(&mut self, width: u16) -> &mut Self { self.width = Some(width); self } /// Get the expected width of the table. /// /// This will be `Some(width)`, if the terminal width can be detected or if the table width is set via [set_width](Table::set_width). /// /// If neither is not possible, `None` will be returned.\ /// This implies that both the [Dynamic](ContentArrangement::Dynamic) mode and the [Percentage](crate::style::Width::Percentage) constraint won't work. #[cfg(feature = "tty")] pub fn width(&self) -> Option { if let Some(width) = self.width { Some(width) } else if self.is_tty() { if let Ok((width, _)) = terminal::size() { Some(width) } else { None } } else { None } } #[cfg(not(feature = "tty"))] pub fn width(&self) -> Option { self.width } /// Specify how Comfy Table should arrange the content in your table. /// /// ``` /// use comfy_table::{Table, ContentArrangement}; /// /// let mut table = Table::new(); /// table.set_content_arrangement(ContentArrangement::Dynamic); /// ``` pub fn set_content_arrangement(&mut self, arrangement: ContentArrangement) -> &mut Self { self.arrangement = arrangement; self } /// Get the current content arrangement of the table. pub fn content_arrangement(&self) -> ContentArrangement { self.arrangement.clone() } /// Set the delimiter used to split text in all cells. /// /// A custom delimiter on a cell in will overwrite the column's delimiter.\ /// Normal text uses spaces (` `) as delimiters. This is necessary to help comfy-table /// understand the concept of _words_. pub fn set_delimiter(&mut self, delimiter: char) -> &mut Self { self.delimiter = Some(delimiter); self } /// Set the truncation indicator for cells that are too long to be displayed. /// /// Set it to "โ€ฆ" for example to use an ellipsis that only takes up one character. pub fn set_truncation_indicator(&mut self, indicator: &str) -> &mut Self { self.truncation_indicator = indicator.to_string(); self } /// In case you are sure you don't want export tables to a tty or you experience /// problems with tty specific code, you can enforce a non_tty mode. /// /// This disables: /// /// - width lookup from the current tty /// - Styling and attributes on cells (unless you use [Table::enforce_styling]) /// /// If you use the [dynamic content arrangement](ContentArrangement::Dynamic), /// you need to set the width of your desired table manually with [set_width](Table::set_width). #[cfg(feature = "tty")] pub fn force_no_tty(&mut self) -> &mut Self { self.no_tty = true; self } /// Use this function to check whether `stderr` is a tty. /// /// The default is `stdout`. #[cfg(feature = "tty")] pub fn use_stderr(&mut self) -> &mut Self { self.use_stderr = true; self } /// Returns whether the table will be handled as if it's printed to a tty. /// /// By default, comfy-table looks at `stdout` and checks whether it's a tty. /// This behavior can be changed via [Table::force_no_tty] and [Table::use_stderr]. #[cfg(feature = "tty")] pub fn is_tty(&self) -> bool { if self.no_tty { return false; } if self.use_stderr { ::std::io::stderr().is_tty() } else { ::std::io::stdout().is_tty() } } /// Enforce terminal styling. /// /// Only useful if you forcefully disabled tty, but still want those fancy terminal styles. /// /// ``` /// use comfy_table::Table; /// /// let mut table = Table::new(); /// table.force_no_tty() /// .enforce_styling(); /// ``` #[cfg(feature = "tty")] pub fn enforce_styling(&mut self) -> &mut Self { self.enforce_styling = true; self } /// Returns whether the content of this table should be styled with the current settings and /// environment. #[cfg(feature = "tty")] pub fn should_style(&self) -> bool { if self.enforce_styling { return true; } self.is_tty() } /// By default, the whole content of a cells will be styled. /// Calling this function disables this behavior for all cells, resulting in /// only the text of cells being styled. #[cfg(feature = "tty")] pub fn style_text_only(&mut self) { self.style_text_only = true; } /// Convenience method to set a [ColumnConstraint] for all columns at once. /// Constraints are used to influence the way the columns will be arranged. /// Check out their docs for more information. /// /// **Attention:** /// This function should be called after at least one row (or the headers) has been added to the table. /// Before that, the columns won't initialized. /// /// If more constraints are passed than there are columns, any superfluous constraints will be ignored. /// ``` /// use comfy_table::{Width::*, CellAlignment, ColumnConstraint::*, ContentArrangement, Table}; /// /// let mut table = Table::new(); /// table.add_row(&vec!["one", "two", "three"]) /// .set_content_arrangement(ContentArrangement::Dynamic) /// .set_constraints(vec![ /// UpperBoundary(Fixed(15)), /// LowerBoundary(Fixed(20)), /// ]); /// ``` pub fn set_constraints>( &mut self, constraints: T, ) -> &mut Self { let mut constraints = constraints.into_iter(); for column in self.column_iter_mut() { if let Some(constraint) = constraints.next() { column.set_constraint(constraint); } else { break; } } self } /// This function creates a TableStyle from a given preset string.\ /// Preset strings can be found in `styling::presets::*`. /// /// You can also write your own preset strings and use them with this function. /// There's the convenience method [Table::current_style_as_preset], which prints you a preset /// string from your current style configuration. \ /// The function expects the to-be-drawn characters to be in the same order as in the [TableComponent] enum. /// /// If the string isn't long enough, the default [ASCII_FULL] style will be used for all remaining components. /// /// If the string is too long, remaining charaacters will be simply ignored. pub fn load_preset(&mut self, preset: &str) -> &mut Self { let mut components = TableComponent::iter(); for character in preset.chars() { if let Some(component) = components.next() { // White spaces mean "don't draw this" in presets // If we want to override the default preset, we need to remove // this component from the HashMap in case we find a whitespace. if character == ' ' { self.remove_style(component); continue; } self.set_style(component, character); } else { break; } } self } /// Returns the current style as a preset string. /// /// A pure convenience method, so you're not force to fiddle with those preset strings yourself. /// /// ``` /// use comfy_table::Table; /// use comfy_table::presets::UTF8_FULL; /// /// let mut table = Table::new(); /// table.load_preset(UTF8_FULL); /// /// assert_eq!(UTF8_FULL, table.current_style_as_preset()) /// ``` pub fn current_style_as_preset(&mut self) -> String { let components = TableComponent::iter(); let mut preset_string = String::new(); for component in components { match self.style(component) { None => preset_string.push(' '), Some(character) => preset_string.push(character), } } preset_string } /// Modify a preset with a modifier string from [modifiers](crate::style::modifiers). /// /// For instance, the [UTF8_ROUND_CORNERS](crate::style::modifiers::UTF8_ROUND_CORNERS) modifies all corners to be round UTF8 box corners. /// /// ``` /// use comfy_table::Table; /// use comfy_table::presets::UTF8_FULL; /// use comfy_table::modifiers::UTF8_ROUND_CORNERS; /// /// let mut table = Table::new(); /// table.load_preset(UTF8_FULL); /// table.apply_modifier(UTF8_ROUND_CORNERS); /// ``` pub fn apply_modifier(&mut self, modifier: &str) -> &mut Self { let mut components = TableComponent::iter(); for character in modifier.chars() { // Skip spaces while applying modifiers. if character == ' ' { components.next(); continue; } if let Some(component) = components.next() { self.set_style(component, character); } else { break; } } self } /// Define the char that will be used to draw a specific component.\ /// Look at [TableComponent] to see all stylable components /// /// If `None` is supplied, the element won't be displayed.\ /// In case of a e.g. *BorderIntersection a whitespace will be used as placeholder, /// unless related borders and and corners are set to `None` as well. /// /// For example, if `TopBorderIntersections` is `None` the first row would look like this: /// /// ```text /// +------ ------+ /// | this | test | /// ``` /// /// If in addition `TopLeftCorner`,`TopBorder` and `TopRightCorner` would be `None` as well, /// the first line wouldn't be displayed at all. /// /// ``` /// use comfy_table::Table; /// use comfy_table::presets::UTF8_FULL; /// use comfy_table::TableComponent::*; /// /// let mut table = Table::new(); /// // Load the UTF8_FULL preset /// table.load_preset(UTF8_FULL); /// // Set all outer corners to round UTF8 corners /// // This is basically the same as the UTF8_ROUND_CORNERS modifier /// table.set_style(TopLeftCorner, 'โ•ญ'); /// table.set_style(TopRightCorner, 'โ•ฎ'); /// table.set_style(BottomLeftCorner, 'โ•ฐ'); /// table.set_style(BottomRightCorner, 'โ•ฏ'); /// ``` pub fn set_style(&mut self, component: TableComponent, character: char) -> &mut Self { self.style.insert(component, character); self } /// Get a copy of the char that's currently used for drawing this component. /// ``` /// use comfy_table::Table; /// use comfy_table::TableComponent::*; /// /// let mut table = Table::new(); /// assert_eq!(table.style(TopLeftCorner), Some('+')); /// ``` pub fn style(&mut self, component: TableComponent) -> Option { self.style.get(&component).copied() } /// Remove the style for a specific component of the table.\ /// By default, a space will be used as a placeholder instead.\ /// Though, if for instance all components of the left border are removed, the left border won't be displayed. pub fn remove_style(&mut self, component: TableComponent) -> &mut Self { self.style.remove(&component); self } /// Get a reference to a specific column. pub fn column(&self, index: usize) -> Option<&Column> { self.columns.get(index) } /// Get a mutable reference to a specific column. pub fn column_mut(&mut self, index: usize) -> Option<&mut Column> { self.columns.get_mut(index) } /// Iterator over all columns pub fn column_iter(&self) -> Iter { self.columns.iter() } /// Get a mutable iterator over all columns. /// /// ``` /// use comfy_table::{Width::*, ColumnConstraint::*, Table}; /// /// let mut table = Table::new(); /// table.add_row(&vec!["First", "Second", "Third"]); /// /// // Add a ColumnConstraint to each column (left->right) /// // first -> min width of 10 /// // second -> max width of 8 /// // third -> fixed width of 10 /// let constraints = vec![ /// LowerBoundary(Fixed(10)), /// UpperBoundary(Fixed(8)), /// Absolute(Fixed(10)), /// ]; /// /// // Add the constraints to their respective column /// for (column_index, column) in table.column_iter_mut().enumerate() { /// let constraint = constraints.get(column_index).unwrap(); /// column.set_constraint(*constraint); /// } /// ``` pub fn column_iter_mut(&mut self) -> IterMut { self.columns.iter_mut() } /// Get a mutable iterator over cells of a column. /// The iterator returns a nested `Option>`, since there might be /// rows that are missing this specific Cell. /// /// ``` /// use comfy_table::Table; /// let mut table = Table::new(); /// table.add_row(&vec!["First", "Second"]); /// table.add_row(&vec!["Third"]); /// table.add_row(&vec!["Fourth", "Fifth"]); /// /// // Create an iterator over the second column /// let mut cell_iter = table.column_cells_iter(1); /// assert_eq!(cell_iter.next().unwrap().unwrap().content(), "Second"); /// assert!(cell_iter.next().unwrap().is_none()); /// assert_eq!(cell_iter.next().unwrap().unwrap().content(), "Fifth"); /// assert!(cell_iter.next().is_none()); /// ``` pub fn column_cells_iter(&self, column_index: usize) -> ColumnCellIter { ColumnCellIter { rows: &self.rows, column_index, row_index: 0, } } /// Get a mutable iterator over cells of a column, including the header cell. /// The header cell will be the very first cell returned. /// The iterator returns a nested `Option>`, since there might be /// rows that are missing this specific Cell. /// /// ``` /// use comfy_table::Table; /// let mut table = Table::new(); /// table.set_header(&vec!["A", "B"]); /// table.add_row(&vec!["First", "Second"]); /// table.add_row(&vec!["Third"]); /// table.add_row(&vec!["Fourth", "Fifth"]); /// /// // Create an iterator over the second column /// let mut cell_iter = table.column_cells_with_header_iter(1); /// assert_eq!(cell_iter.next().unwrap().unwrap().content(), "B"); /// assert_eq!(cell_iter.next().unwrap().unwrap().content(), "Second"); /// assert!(cell_iter.next().unwrap().is_none()); /// assert_eq!(cell_iter.next().unwrap().unwrap().content(), "Fifth"); /// assert!(cell_iter.next().is_none()); /// ``` pub fn column_cells_with_header_iter(&self, column_index: usize) -> ColumnCellsWithHeaderIter { ColumnCellsWithHeaderIter { header_checked: false, header: &self.header, rows: &self.rows, column_index, row_index: 0, } } /// Reference to a specific row pub fn row(&self, index: usize) -> Option<&Row> { self.rows.get(index) } /// Mutable reference to a specific row pub fn row_mut(&mut self, index: usize) -> Option<&mut Row> { self.rows.get_mut(index) } /// Iterator over all rows pub fn row_iter(&self) -> Iter { self.rows.iter() } /// Get a mutable iterator over all rows. /// /// ``` /// use comfy_table::Table; /// let mut table = Table::new(); /// table.add_row(&vec!["First", "Second", "Third"]); /// /// // Add the constraints to their respective row /// for row in table.row_iter_mut() { /// row.max_height(5); /// } /// assert!(table.row_iter_mut().len() == 1); /// ``` pub fn row_iter_mut(&mut self) -> IterMut { self.rows.iter_mut() } /// Return a vector representing the maximum amount of characters in any line of this column.\ /// /// **Attention** This scans the whole current content of the table. pub fn column_max_content_widths(&self) -> Vec { fn set_max_content_widths(max_widths: &mut [u16], row: &Row) { // Get the max width for each cell of the row let row_max_widths = row.max_content_widths(); for (index, width) in row_max_widths.iter().enumerate() { let mut width = (*width).try_into().unwrap_or(u16::MAX); // A column's content is at least 1 char wide. width = std::cmp::max(1, width); // Set a new max, if the current cell is the longest for that column. let current_max = max_widths[index]; if current_max < width { max_widths[index] = width; } } } // The vector that'll contain the max widths per column. let mut max_widths = vec![0; self.columns.len()]; if let Some(header) = &self.header { set_max_content_widths(&mut max_widths, header); } // Iterate through all rows of the table. for row in self.rows.iter() { set_max_content_widths(&mut max_widths, row); } max_widths } pub(crate) fn style_or_default(&self, component: TableComponent) -> String { match self.style.get(&component) { None => " ".to_string(), Some(character) => character.to_string(), } } pub(crate) fn style_exists(&self, component: TableComponent) -> bool { self.style.contains_key(&component) } /// Autogenerate new columns, if a row is added with more cells than existing columns. fn autogenerate_columns(&mut self, row: &Row) { if row.cell_count() > self.columns.len() { for index in self.columns.len()..row.cell_count() { self.columns.push(Column::new(index)); } } } /// Calling this might be necessary if you add new cells to rows that're already added to the /// table. /// /// If more cells than're currently know to the table are added to that row, /// the table cannot know about these, since new [Column]s are only /// automatically detected when a new row is added. /// /// To make sure everything works as expected, just call this function if you're adding cells /// to rows that're already added to the table. pub fn discover_columns(&mut self) { for row in self.rows.iter() { if row.cell_count() > self.columns.len() { for index in self.columns.len()..row.cell_count() { self.columns.push(Column::new(index)); } } } } } /// An iterator over cells of a specific column. /// A dedicated struct is necessary, as data is usually handled by rows and thereby stored in /// `Table::rows`. This type is returned by [Table::column_cells_iter]. pub struct ColumnCellIter<'a> { rows: &'a [Row], column_index: usize, row_index: usize, } impl<'a> Iterator for ColumnCellIter<'a> { type Item = Option<&'a Cell>; fn next(&mut self) -> Option> { // Check if there's a next row if let Some(row) = self.rows.get(self.row_index) { self.row_index += 1; // Return the cell (if it exists). return Some(row.cells.get(self.column_index)); } None } } /// An iterator over cells of a specific column. /// A dedicated struct is necessary, as data is usually handled by rows and thereby stored in /// `Table::rows`. This type is returned by [Table::column_cells_iter]. pub struct ColumnCellsWithHeaderIter<'a> { header_checked: bool, header: &'a Option, rows: &'a [Row], column_index: usize, row_index: usize, } impl<'a> Iterator for ColumnCellsWithHeaderIter<'a> { type Item = Option<&'a Cell>; fn next(&mut self) -> Option> { // Get the header as the first cell if !self.header_checked { self.header_checked = true; return match self.header { Some(header) => { // Return the cell (if it exists). Some(header.cells.get(self.column_index)) } None => Some(None), }; } // Check if there's a next row if let Some(row) = self.rows.get(self.row_index) { self.row_index += 1; // Return the cell (if it exists). return Some(row.cells.get(self.column_index)); } None } } #[cfg(test)] mod tests { use super::*; #[test] fn test_column_generation() { let mut table = Table::new(); table.set_header(vec!["thr", "four", "fivef"]); // When adding a new row, columns are automatically generated assert_eq!(table.columns.len(), 3); // The max content width is also correctly set for each column assert_eq!(table.column_max_content_widths(), vec![3, 4, 5]); // When adding a new row, the max content width is updated accordingly table.add_row(vec!["four", "fivef", "very long text with 23"]); assert_eq!(table.column_max_content_widths(), vec![4, 5, 22]); // Now add a row that has column lines. The max content width shouldn't change table.add_row(vec!["", "", "shorter"]); assert_eq!(table.column_max_content_widths(), vec![4, 5, 22]); println!("{table}"); } } comfy-table-7.1.4/src/utils/arrangement/constraint.rs000064400000000000000000000110321046102023000210200ustar 00000000000000use super::helper::*; use super::{ColumnDisplayInfo, DisplayInfos}; use crate::style::{ColumnConstraint, ColumnConstraint::*, Width}; use crate::{Column, Table}; /// Look at given constraints of a column and check if some of them can be resolved at the very /// beginning. /// /// For example: /// - We get an absolute width. /// - MinWidth constraints on columns, whose content is garantueed to be smaller than the specified /// minimal width. /// - The Column is supposed to be hidden. pub fn evaluate( table: &Table, visible_columns: usize, infos: &mut DisplayInfos, column: &Column, max_content_width: u16, ) { match &column.constraint { Some(ContentWidth) => { let info = ColumnDisplayInfo::new(column, max_content_width); infos.insert(column.index, info); } Some(Absolute(width)) => { if let Some(width) = absolute_value_from_width(table, width, visible_columns) { // The column should get always get a fixed width. let width = absolute_width_with_padding(column, width); let info = ColumnDisplayInfo::new(column, width); infos.insert(column.index, info); } } Some(Hidden) => { let mut info = ColumnDisplayInfo::new(column, max_content_width); info.is_hidden = true; infos.insert(column.index, info); } _ => {} } if let Some(min_width) = min(table, &column.constraint, visible_columns) { // In case a min_width is specified, we may already fix the size of the column. // We do this, if we know that the content is smaller than the min size. let max_width = max_content_width + column.padding_width(); if max_width <= min_width { let width = absolute_width_with_padding(column, min_width); let info = ColumnDisplayInfo::new(column, width); infos.insert(column.index, info); } } } /// A little wrapper, which resolves possible lower boundary constraints to their actual value for /// the current table and terminal width. /// /// This returns the value of absolute characters that are allowed to be in this column. \ /// Lower boundaries with [Width::Fixed] just return their internal value. \ /// Lower boundaries with [Width::Percentage] return the percental amount of the current table /// width. pub fn min( table: &Table, constraint: &Option, visible_columns: usize, ) -> Option { let constraint = if let Some(constraint) = constraint { constraint } else { return None; }; match constraint { LowerBoundary(width) | Boundaries { lower: width, .. } => { absolute_value_from_width(table, width, visible_columns) } _ => None, } } /// A little wrapper, which resolves possible upper boundary constraints to their actual value for /// the current table and terminal width. /// /// This returns the value of absolute characters that are allowed to be in this column. \ /// Upper boundaries with [Width::Fixed] just return their internal value. \ /// Upper boundaries with [Width::Percentage] return the percental amount of the current table /// width. pub fn max( table: &Table, constraint: &Option, visible_columns: usize, ) -> Option { let constraint = if let Some(constraint) = constraint { constraint } else { return None; }; match constraint { UpperBoundary(width) | Boundaries { upper: width, .. } => { absolute_value_from_width(table, width, visible_columns) } _ => None, } } /// Resolve an absolute value from a given boundary pub fn absolute_value_from_width( table: &Table, width: &Width, visible_columns: usize, ) -> Option { match width { Width::Fixed(width) => Some(*width), Width::Percentage(percent) => { // Don't return a value, if we cannot determine the current table width. let table_width = table.width().map(usize::from)?; // Enforce at most 100% let percent = std::cmp::min(*percent, 100u16); // Subtract the borders from the table width. let width = table_width.saturating_sub(count_border_columns(table, visible_columns)); // Calculate the absolute value in actual columns. let width = (width * usize::from(percent) / 100) .try_into() .unwrap_or(u16::MAX); Some(width) } } } comfy-table-7.1.4/src/utils/arrangement/disabled.rs000064400000000000000000000017671046102023000204210ustar 00000000000000use super::constraint; use super::helper::*; use super::{ColumnDisplayInfo, DisplayInfos}; use crate::Table; /// Dynamic arrangement is disabled. /// Apply all non-relative constraints, and set the width of all remaining columns to the /// respective max content width. pub fn arrange( table: &Table, infos: &mut DisplayInfos, visible_columns: usize, max_content_widths: &[u16], ) { for column in table.columns.iter() { if infos.contains_key(&column.index) { continue; } let mut width = max_content_widths[column.index]; // Reduce the width, if a column has longer content than the specified MaxWidth constraint. if let Some(max_width) = constraint::max(table, &column.constraint, visible_columns) { if max_width < width { width = absolute_width_with_padding(column, max_width); } } let info = ColumnDisplayInfo::new(column, width); infos.insert(column.index, info); } } comfy-table-7.1.4/src/utils/arrangement/dynamic.rs000064400000000000000000000516311046102023000202710ustar 00000000000000use unicode_width::UnicodeWidthStr; use super::constraint; use super::helper::*; use super::{ColumnDisplayInfo, DisplayInfos}; use crate::style::*; use crate::utils::formatting::content_split::split_line; use crate::{Column, Table}; /// Try to find the best fit for a given content and table_width /// /// 1. Determine the amount of available space after applying fixed columns, padding, and borders. /// 2. Now that we know how much space we have to work with, we have to check again for /// LowerBoundary constraints. If there are any columns that have a higher LowerBoundary, /// we have to fix that column to this size. /// 3. Check if there are any columns that require less space than the average /// remaining space for the remaining columns. (This includes the MaxWidth constraint). /// 4. Take those columns, fix their size and add the surplus in space to the remaining space. /// 5. Repeat step 2-3 until no columns with smaller size than average remaining space are left. /// 6. At this point, the remaining spaces is equally distributed between all columns. /// It get's a little tricky now. Check the documentation of [optimize_space_after_split] /// for more information. /// 7. Divide the remaining space in relatively equal chunks. /// /// This breaks when: /// /// 1. A user assigns fixed sizes to a few columns, which are larger than the terminal when combined. /// 2. A user provides more than 100% column width across a few columns. pub fn arrange( table: &Table, infos: &mut DisplayInfos, table_width: usize, max_content_widths: &[u16], ) { let visible_columns = count_visible_columns(&table.columns); // Step 1 // Find out how much space there is left. let mut remaining_width: usize = available_content_width(table, infos, visible_columns, table_width); let mut remaining_columns = count_remaining_columns(visible_columns, infos); #[cfg(feature = "debug")] println!( "dynamic::arrange: Table width: {table_width}, Start remaining width {remaining_width}" ); #[cfg(feature = "debug")] println!("dynamic::arrange: Max content widths: {max_content_widths:#?}"); // Step 2. // // Iterate through all undecided columns and enforce LowerBoundary constraints, if they're // bigger than the current average space. if remaining_columns > 0 { (remaining_width, remaining_columns) = enforce_lower_boundary_constraints( table, infos, remaining_width, remaining_columns, visible_columns, ); } // Step 3-5. // Find all columns that require less space than the average. // Returns the remaining available width and the amount of remaining columns that need handling let (mut remaining_width, mut remaining_columns) = find_columns_that_fit_into_average( table, infos, remaining_width, remaining_columns, visible_columns, max_content_widths, ); #[cfg(feature = "debug")] { println!("After less than average: {infos:#?}"); println!("Remaining width {remaining_width}, column {remaining_columns}"); } // Step 6 // All remaining columns should get an equal amount of remaining space. // However, we check if we can save some space after the content has been split. // // We only do this if there are remaining columns. if remaining_columns > 0 { // This is where Step 5 happens. (remaining_width, remaining_columns) = optimize_space_after_split( table, &table.columns, infos, remaining_width, remaining_columns, ); } #[cfg(feature = "debug")] { println!("dynamic::arrange: After optimize: {infos:#?}",); println!("dynamic::arrange: Remaining width {remaining_width}, column {remaining_columns}",); } // Early exit and one branch of Part 7. // // All columns have been successfully assigned a width. // However, in case the user specified that the full terminal width should always be fully // utilized, we have to equally distribute the remaining space across all columns. if remaining_columns == 0 { if remaining_width > 0 && matches!(table.arrangement, ContentArrangement::DynamicFullWidth) { use_full_width(infos, remaining_width); #[cfg(feature = "debug")] println!("dynamic::arrange: After full width: {infos:#?}"); } return; } // Step 7. Equally distribute the remaining_width to all remaining columns // If we have less than one space per remaining column, give at least one space per column if remaining_width < remaining_columns { remaining_width = remaining_columns; } distribute_remaining_space(&table.columns, infos, remaining_width, remaining_columns); #[cfg(feature = "debug")] println!("dynamic::arrange: After distribute: {infos:#?}"); } /// Step 1 /// /// This function calculates the amount of remaining space that can be distributed between /// all remaining columns. /// /// Take the current terminal width and /// - Subtract borders /// - Subtract padding /// - Subtract columns that already have a fixed width. /// /// This value is converted to a i32 to handle negative values in case we work with a very small /// terminal. fn available_content_width( table: &Table, infos: &DisplayInfos, visible_columns: usize, mut width: usize, ) -> usize { let border_count = count_border_columns(table, visible_columns); width = width.saturating_sub(border_count); // Subtract all paddings from the remaining width. for column in table.columns.iter() { if infos.contains_key(&column.index) { continue; } // Remove the fixed padding for each column let (left, right) = column.padding; width = width.saturating_sub((left + right).into()); } // Remove all already fixed sizes from the remaining_width. for info in infos.values() { if info.is_hidden { continue; } width = width.saturating_sub(info.width().into()); } width } /// Step 2-4 /// This function is part of the column width calculation process. /// It checks if there are columns that take less space than there's currently available in average /// for each column. /// /// The algorithm is a while loop with a nested for loop. /// 1. We iterate over all columns and check if there are columns that take less space. /// 2. If we find one or more such columns, we fix their width and add the surplus space to the /// remaining space. Due to this step, the average space per column increased. Now some other /// column might be fixed in width as well. /// 3. Do step 1 and 2, as long as there are columns left and as long as we find columns /// that take up less space than the current remaining average. /// /// Parameters: /// - `table_width`: The absolute amount of available space. /// - `remaining_width`: This is the amount of space that isn't yet reserved by any other column. /// We need this to determine the average space each column has left. /// Any columns that needs less than this average receives a fixed width. /// The leftover space can then be used for the other columns. /// - `visible_columns`: All visible columns that should be displayed. /// /// Returns: /// `(remaining_width: usize, remaining_columns: u16)` fn find_columns_that_fit_into_average( table: &Table, infos: &mut DisplayInfos, mut remaining_width: usize, mut remaining_columns: usize, visible_columns: usize, max_content_widths: &[u16], ) -> (usize, usize) { let mut found_smaller = true; while found_smaller { found_smaller = false; // There are no columns left to check. Proceed to the next step if remaining_columns == 0 { break; } let mut average_space = remaining_width / remaining_columns; // We have no space left, the terminal is either tiny or the other columns are huge. if average_space == 0 { break; } for column in table.columns.iter() { // Ignore hidden columns // We already checked this column, skip it if infos.contains_key(&column.index) { continue; } let max_column_width = max_content_widths[column.index]; // The column has a MaxWidth Constraint. // we can fix the column to this max_width and mark it as checked if these // two conditions are met: // - The average remaining space is bigger then the MaxWidth constraint. // - The actual max content of the column is bigger than the MaxWidth constraint. if let Some(max_width) = constraint::max(table, &column.constraint, visible_columns) { // Max/Min constraints always include padding! let average_space_with_padding = average_space + usize::from(column.padding_width()); let width_with_padding = max_column_width + column.padding_width(); // Check that both conditions mentioned above are met. if usize::from(max_width) <= average_space_with_padding && width_with_padding >= max_width { // Save the calculated info, this column has been handled. let width = absolute_width_with_padding(column, max_width); let info = ColumnDisplayInfo::new(column, width); infos.insert(column.index, info); #[cfg(feature = "debug")] println!( "dynamic::find_columns_that_fit_into_average: Fixed column {} via MaxWidth constraint with size {}, as it's bigger than average {}", column.index, width, average_space ); // Continue with new recalculated width remaining_width = remaining_width.saturating_sub(width.into()); remaining_columns -= 1; if remaining_columns == 0 { break; } average_space = remaining_width / remaining_columns; found_smaller = true; continue; } } // The column has a smaller or equal max_content_width than the average space. // Fix the width to max_content_width and mark it as checked if usize::from(max_column_width) <= average_space { let info = ColumnDisplayInfo::new(column, max_column_width); infos.insert(column.index, info); #[cfg(feature = "debug")] println!( "dynamic::find_columns_that_fit_into_average: Fixed column {} with size {}, as it's smaller than average {}", column.index, max_column_width, average_space ); // Continue with new recalculated width remaining_width = remaining_width.saturating_sub(max_column_width.into()); remaining_columns -= 1; if remaining_columns == 0 { break; } average_space = remaining_width / remaining_columns; found_smaller = true; } } } (remaining_width, remaining_columns) } /// Step 5 /// /// Determine, whether there are any columns that are allowed to occupy more width than the current /// `average_space` via a [LowerBoundary] constraint. /// /// These columns will then get fixed to the width specified in the [LowerBoundary] constraint. /// /// I.e. if a column has to have at least 10 characters, but the average width left for a column is /// only 6, we fix the column to this 10 character minimum! fn enforce_lower_boundary_constraints( table: &Table, infos: &mut DisplayInfos, mut remaining_width: usize, mut remaining_columns: usize, visible_columns: usize, ) -> (usize, usize) { let mut average_space = remaining_width / remaining_columns; for column in table.columns.iter() { // Ignore hidden columns // We already checked this column, skip it if infos.contains_key(&column.index) { continue; } // Check whether the column has a LowerBoundary constraint. let min_width = if let Some(min_width) = constraint::min(table, &column.constraint, visible_columns) { min_width } else { continue; }; // Only proceed if the average spaces is smaller than the specified lower boundary. if average_space >= min_width.into() { continue; } // This column would get smaller than the specified lower boundary. // Fix its width!!! let width = absolute_width_with_padding(column, min_width); let info = ColumnDisplayInfo::new(column, width); infos.insert(column.index, info); #[cfg(feature = "debug")] println!( "dynamic::enforce_lower_boundary_constraints: Fixed column {} to min constraint width {}", column.index, width ); // Continue with new recalculated width remaining_width = remaining_width.saturating_sub(width.into()); remaining_columns -= 1; if remaining_columns == 0 { break; } average_space = remaining_width / remaining_columns; continue; } (remaining_width, remaining_columns) } /// Step 5. /// /// Some Column's are too big and need to be split. /// We're now going to simulate how this might look like. /// The reason for this is the way we're splitting, which is to prefer a split at a delimiter. /// This can lead to a column needing less space than it was initially assigned. /// /// Example: /// A column is allowed to have a width of 10 characters. /// A cell's content looks like this `sometest sometest`, which is 17 chars wide. /// After splitting at the default delimiter (space), it looks like this: /// ```text /// sometest /// sometest /// ``` /// Even though the column required 17 spaces beforehand, it can now be shrunk to 8 chars width. /// /// By doing this for each column, we can save a lot of space in some edge-cases. fn optimize_space_after_split( table: &Table, columns: &[Column], infos: &mut DisplayInfos, mut remaining_width: usize, mut remaining_columns: usize, ) -> (usize, usize) { let mut found_smaller = true; // Calculate the average space that remains for each column. let mut average_space = remaining_width / remaining_columns; #[cfg(feature = "debug")] println!( "dynamic::optimize_space_after_split: Start with average_space {}", average_space ); // Do this as long as we find a smaller column while found_smaller { found_smaller = false; for column in columns.iter() { // We already checked this column, skip it if infos.contains_key(&column.index) { continue; } let longest_line = longest_line_after_split(average_space, column, table); #[cfg(feature = "debug")] println!( "dynamic::optimize_space_after_split: Longest line after split for column {} is {}", column.index, longest_line ); // If there's a considerable amount space left after splitting, we freeze the column and // set its content width to the calculated post-split width. let remaining_space = average_space.saturating_sub(longest_line); if remaining_space >= 3 { let info = ColumnDisplayInfo::new(column, longest_line.try_into().unwrap_or(u16::MAX)); infos.insert(column.index, info); remaining_width = remaining_width.saturating_sub(longest_line); remaining_columns -= 1; if remaining_columns == 0 { break; } average_space = remaining_width / remaining_columns; #[cfg(feature = "debug")] println!( "dynamic::optimize_space_after_split: average_space is now {}", average_space ); found_smaller = true; } } } (remaining_width, remaining_columns) } /// Part of Step 5. /// /// This function simulates the split of a Column's content and returns the longest /// existing line after the split. /// /// A lot of this logic is duplicated from the [utils::format::format_row] function. fn longest_line_after_split(average_space: usize, column: &Column, table: &Table) -> usize { // Collect all resulting lines of the column in a single vector. // That way we can easily determine the longest line afterwards. let mut column_lines = Vec::new(); // Iterate for cell in table.column_cells_with_header_iter(column.index) { // Only look at rows that actually contain this cell. let cell = match cell { Some(cell) => cell, None => continue, }; let delimiter = delimiter(table, column, cell); // Create a temporary ColumnDisplayInfo with the average space as width. // That way we can simulate how the split text will look like. let info = ColumnDisplayInfo::new(column, average_space.try_into().unwrap_or(u16::MAX)); // Iterate over each line and split it into multiple lines, if necessary. // Newlines added by the user will be preserved. for line in cell.content.iter() { if line.width() > average_space { let mut parts = split_line(line, &info, delimiter); #[cfg(feature = "debug")] println!( "dynamic::longest_line_after_split: Splitting line with width {}. Original:\n {}\nSplitted:\n {:?}", line.width(), line, parts ); column_lines.append(&mut parts); } else { column_lines.push(line.into()); } } } // Get the longest line, default to length 0 if no lines exist. column_lines .iter() .map(|line| line.width()) .max() .unwrap_or(0) } /// Step 6 - First branch /// /// At this point of time, all columns have been assigned some kind of width! /// The user wants to utilize the full width of the terminal and there's space left. /// /// Equally distribute the remaining space between all columns. fn use_full_width(infos: &mut DisplayInfos, remaining_width: usize) { let visible_columns = infos.iter().filter(|(_, info)| !info.is_hidden).count(); if visible_columns == 0 { return; } // Calculate the amount of average remaining space per column. // Since we do integer division, there is most likely a little bit of non equally-divisible space. // We then try to distribute it as fair as possible (from left to right). let average_space = remaining_width / visible_columns; let mut excess = remaining_width - (average_space * visible_columns); for (_, info) in infos.iter_mut() { // Ignore hidden columns if info.is_hidden { continue; } // Distribute the non-divisible excess from left-to right until nothing is left. let width = if excess > 0 { excess -= 1; (average_space + 1).try_into().unwrap_or(u16::MAX) } else { average_space.try_into().unwrap_or(u16::MAX) }; info.content_width += width; } } /// Step 6 - Second branch /// /// Not all columns have a determined width yet -> The content still doesn't fully fit into the /// given width. /// /// This function now equally distributes the remaining width between the remaining columns. fn distribute_remaining_space( columns: &[Column], infos: &mut DisplayInfos, remaining_width: usize, remaining_columns: usize, ) { // Calculate the amount of average remaining space per column. // Since we do integer division, there is most likely a little bit of non equally-divisible space. // We then try to distribute it as fair as possible (from left to right). let average_space = remaining_width / remaining_columns; let mut excess = remaining_width - (average_space * remaining_columns); for column in columns.iter() { // Ignore hidden columns if infos.contains_key(&column.index) { continue; } // Distribute the non-divisible excess from left-to right until nothing is left. let width = if excess > 0 { excess -= 1; (average_space + 1).try_into().unwrap_or(u16::MAX) } else { average_space.try_into().unwrap_or(u16::MAX) }; let info = ColumnDisplayInfo::new(column, width); infos.insert(column.index, info); } } comfy-table-7.1.4/src/utils/arrangement/helper.rs000064400000000000000000000046001046102023000201160ustar 00000000000000use super::DisplayInfos; use crate::utils::formatting::borders::{ should_draw_left_border, should_draw_right_border, should_draw_vertical_lines, }; use crate::{Cell, Column, Table}; /// The ColumnDisplayInfo works with a fixed value for content width. /// However, if a column is supposed to get a absolute width, we have to make sure that /// the padding on top of the content width doesn't get larger than the specified absolute width. /// /// For this reason, we take the targeted width, subtract the column's padding and make sure that /// the content width is always a minimum of 1 pub fn absolute_width_with_padding(column: &Column, width: u16) -> u16 { let mut content_width = width .saturating_sub(column.padding.0) .saturating_sub(column.padding.1); if content_width == 0 { content_width = 1; } content_width } /// Return the amount of visible columns pub fn count_visible_columns(columns: &[Column]) -> usize { columns.iter().filter(|column| !column.is_hidden()).count() } /// Return the amount of visible columns that haven't been checked yet. /// /// - `column_count` is the total amount of columns that are visible, calculated /// with [count_visible_columns]. /// - `infos` are all columns that have already been fixed in size or are hidden. pub fn count_remaining_columns(column_count: usize, infos: &DisplayInfos) -> usize { column_count - infos.iter().filter(|(_, info)| !info.is_hidden).count() } /// Return the amount of border columns, that will be visible in the final table output. pub fn count_border_columns(table: &Table, visible_columns: usize) -> usize { let mut lines = 0; // Remove space occupied by borders from remaining_width if should_draw_left_border(table) { lines += 1; } if should_draw_right_border(table) { lines += 1; } if should_draw_vertical_lines(table) { lines += visible_columns.saturating_sub(1); } lines } /// Get the delimiter for a Cell. /// Priority is in decreasing order: Cell -> Column -> Table. pub fn delimiter(table: &Table, column: &Column, cell: &Cell) -> char { // Determine, which delimiter should be used if let Some(delimiter) = cell.delimiter { delimiter } else if let Some(delimiter) = column.delimiter { delimiter } else if let Some(delimiter) = table.delimiter { delimiter } else { ' ' } } comfy-table-7.1.4/src/utils/arrangement/mod.rs000064400000000000000000000057511046102023000174260ustar 00000000000000use std::collections::BTreeMap; use super::ColumnDisplayInfo; use crate::style::ContentArrangement; use crate::table::Table; pub mod constraint; mod disabled; mod dynamic; pub mod helper; type DisplayInfos = BTreeMap; /// Determine the width of each column depending on the content of the given table. /// The results uses Option, since users can choose to hide columns. pub fn arrange_content(table: &Table) -> Vec { let table_width = table.width().map(usize::from); let mut infos = BTreeMap::new(); let max_content_widths = table.column_max_content_widths(); // Check if we can already resolve some constraints. // This step also populates the ColumnDisplayInfo structs. let visible_columns = helper::count_visible_columns(&table.columns); for column in table.columns.iter() { if column.constraint.is_some() { constraint::evaluate( table, visible_columns, &mut infos, column, max_content_widths[column.index], ); } } #[cfg(feature = "debug")] println!("After initial constraints: {infos:#?}"); // Fallback to `ContentArrangement::Disabled`, if we don't have any information // on how wide the table should be. let table_width = if let Some(table_width) = table_width { table_width } else { disabled::arrange(table, &mut infos, visible_columns, &max_content_widths); return infos.into_values().collect(); }; match &table.arrangement { ContentArrangement::Disabled => { disabled::arrange(table, &mut infos, visible_columns, &max_content_widths) } ContentArrangement::Dynamic | ContentArrangement::DynamicFullWidth => { dynamic::arrange(table, &mut infos, table_width, &max_content_widths); } } infos.into_values().collect() } #[cfg(test)] mod tests { use super::*; #[test] fn test_disabled_arrangement() { let mut table = Table::new(); table.set_header(vec!["head", "head", "head"]); table.add_row(vec!["__", "fivef", "sixsix"]); let display_infos = arrange_content(&table); // The width should be the width of the rows + padding let widths: Vec = display_infos.iter().map(ColumnDisplayInfo::width).collect(); assert_eq!(widths, vec![6, 7, 8]); } #[test] fn test_discover_columns() { let mut table = Table::new(); table.add_row(vec!["one", "two"]); // Get the first row and add a new cell, which would create a new column. let row = table.row_mut(0).unwrap(); row.add_cell("three".into()); // The table cannot know about the new cell yet, which is why we expect two columns. assert_eq!(table.columns.len(), 2); // After scanning for new columns however, it should show up. table.discover_columns(); assert_eq!(table.columns.len(), 3); } } comfy-table-7.1.4/src/utils/formatting/borders.rs000064400000000000000000000232321046102023000201500ustar 00000000000000use crate::style::TableComponent; use crate::table::Table; use crate::utils::ColumnDisplayInfo; pub(crate) fn draw_borders( table: &Table, rows: &[Vec>], display_info: &[ColumnDisplayInfo], ) -> Vec { // We know how many lines there should be. Initialize the vector with the rough correct amount. // We might over allocate a bit, but that's better than under allocating. let mut lines = if let Some(capacity) = rows.first().map(|lines| lines.len()) { // Lines * 2 -> Lines + delimiters // + 5 -> header delimiters + header + bottom/top borders Vec::with_capacity(capacity * 2 + 5) } else { Vec::new() }; if should_draw_top_border(table) { lines.push(draw_top_border(table, display_info)); } draw_rows(&mut lines, rows, table, display_info); if should_draw_bottom_border(table) { lines.push(draw_bottom_border(table, display_info)); } lines } fn draw_top_border(table: &Table, display_info: &[ColumnDisplayInfo]) -> String { let left_corner = table.style_or_default(TableComponent::TopLeftCorner); let top_border = table.style_or_default(TableComponent::TopBorder); let intersection = table.style_or_default(TableComponent::TopBorderIntersections); let right_corner = table.style_or_default(TableComponent::TopRightCorner); let mut line = String::new(); // We only need the top left corner, if we need to draw a left border if should_draw_left_border(table) { line += &left_corner; } // Build the top border line depending on the columns' width. // Also add the border intersections. let mut first = true; for info in display_info.iter() { // Only add something, if the column isn't hidden if !info.is_hidden { if !first { line += &intersection; } line += &top_border.repeat(info.width().into()); first = false; } } // We only need the top right corner, if we need to draw a right border if should_draw_right_border(table) { line += &right_corner; } line } fn draw_rows( lines: &mut Vec, rows: &[Vec>], table: &Table, display_info: &[ColumnDisplayInfo], ) { // Iterate over all rows let mut row_iter = rows.iter().enumerate().peekable(); while let Some((row_index, row)) = row_iter.next() { // Concatenate the line parts and insert the vertical borders if needed for line_parts in row.iter() { lines.push(embed_line(line_parts, table)); } // Draw the horizontal header line if desired, otherwise continue to the next iteration if row_index == 0 && table.header.is_some() { if should_draw_header(table) { lines.push(draw_horizontal_lines(table, display_info, true)); } continue; } // Draw a horizontal line, if we desired and if we aren't in the last row of the table. if row_iter.peek().is_some() && should_draw_horizontal_lines(table) { lines.push(draw_horizontal_lines(table, display_info, false)); } } } // Takes the parts of a single line, surrounds them with borders and adds vertical lines. fn embed_line(line_parts: &[String], table: &Table) -> String { let vertical_lines = table.style_or_default(TableComponent::VerticalLines); let left_border = table.style_or_default(TableComponent::LeftBorder); let right_border = table.style_or_default(TableComponent::RightBorder); let mut line = String::new(); if should_draw_left_border(table) { line += &left_border; } let mut part_iter = line_parts.iter().peekable(); while let Some(part) = part_iter.next() { line += part; if should_draw_vertical_lines(table) && part_iter.peek().is_some() { line += &vertical_lines; } else if should_draw_right_border(table) && part_iter.peek().is_none() { line += &right_border; } } line } // The horizontal line that separates between rows. fn draw_horizontal_lines( table: &Table, display_info: &[ColumnDisplayInfo], header: bool, ) -> String { // Styling depends on whether we're currently on the header line or not. let (left_intersection, horizontal_lines, middle_intersection, right_intersection) = if header { ( table.style_or_default(TableComponent::LeftHeaderIntersection), table.style_or_default(TableComponent::HeaderLines), table.style_or_default(TableComponent::MiddleHeaderIntersections), table.style_or_default(TableComponent::RightHeaderIntersection), ) } else { ( table.style_or_default(TableComponent::LeftBorderIntersections), table.style_or_default(TableComponent::HorizontalLines), table.style_or_default(TableComponent::MiddleIntersections), table.style_or_default(TableComponent::RightBorderIntersections), ) }; let mut line = String::new(); // We only need the bottom left corner, if we need to draw a left border if should_draw_left_border(table) { line += &left_intersection; } // Append the middle lines depending on the columns' widths. // Also add the middle intersections. let mut first = true; for info in display_info.iter() { // Only add something, if the column isn't hidden if !info.is_hidden { if !first { line += &middle_intersection; } line += &horizontal_lines.repeat(info.width().into()); first = false; } } // We only need the bottom right corner, if we need to draw a right border if should_draw_right_border(table) { line += &right_intersection; } line } fn draw_bottom_border(table: &Table, display_info: &[ColumnDisplayInfo]) -> String { let left_corner = table.style_or_default(TableComponent::BottomLeftCorner); let bottom_border = table.style_or_default(TableComponent::BottomBorder); let middle_intersection = table.style_or_default(TableComponent::BottomBorderIntersections); let right_corner = table.style_or_default(TableComponent::BottomRightCorner); let mut line = String::new(); // We only need the bottom left corner, if we need to draw a left border if should_draw_left_border(table) { line += &left_corner; } // Add the bottom border lines depending on column width // Also add the border intersections. let mut first = true; for info in display_info.iter() { // Only add something, if the column isn't hidden if !info.is_hidden { if !first { line += &middle_intersection; } line += &bottom_border.repeat(info.width().into()); first = false; } } // We only need the bottom right corner, if we need to draw a right border if should_draw_right_border(table) { line += &right_corner; } line } fn should_draw_top_border(table: &Table) -> bool { if table.style_exists(TableComponent::TopLeftCorner) || table.style_exists(TableComponent::TopBorder) || table.style_exists(TableComponent::TopBorderIntersections) || table.style_exists(TableComponent::TopRightCorner) { return true; } false } fn should_draw_bottom_border(table: &Table) -> bool { if table.style_exists(TableComponent::BottomLeftCorner) || table.style_exists(TableComponent::BottomBorder) || table.style_exists(TableComponent::BottomBorderIntersections) || table.style_exists(TableComponent::BottomRightCorner) { return true; } false } pub fn should_draw_left_border(table: &Table) -> bool { if table.style_exists(TableComponent::TopLeftCorner) || table.style_exists(TableComponent::LeftBorder) || table.style_exists(TableComponent::LeftBorderIntersections) || table.style_exists(TableComponent::LeftHeaderIntersection) || table.style_exists(TableComponent::BottomLeftCorner) { return true; } false } pub fn should_draw_right_border(table: &Table) -> bool { if table.style_exists(TableComponent::TopRightCorner) || table.style_exists(TableComponent::RightBorder) || table.style_exists(TableComponent::RightBorderIntersections) || table.style_exists(TableComponent::RightHeaderIntersection) || table.style_exists(TableComponent::BottomRightCorner) { return true; } false } fn should_draw_horizontal_lines(table: &Table) -> bool { if table.style_exists(TableComponent::LeftBorderIntersections) || table.style_exists(TableComponent::HorizontalLines) || table.style_exists(TableComponent::MiddleIntersections) || table.style_exists(TableComponent::RightBorderIntersections) { return true; } false } pub fn should_draw_vertical_lines(table: &Table) -> bool { if table.style_exists(TableComponent::TopBorderIntersections) || table.style_exists(TableComponent::MiddleHeaderIntersections) || table.style_exists(TableComponent::VerticalLines) || table.style_exists(TableComponent::MiddleIntersections) || table.style_exists(TableComponent::BottomBorderIntersections) { return true; } false } fn should_draw_header(table: &Table) -> bool { if table.style_exists(TableComponent::LeftHeaderIntersection) || table.style_exists(TableComponent::HeaderLines) || table.style_exists(TableComponent::MiddleHeaderIntersections) || table.style_exists(TableComponent::RightHeaderIntersection) { return true; } false } comfy-table-7.1.4/src/utils/formatting/content_format.rs000064400000000000000000000336321046102023000215370ustar 00000000000000#[cfg(feature = "tty")] use crossterm::style::{style, Stylize}; use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthStr; use super::content_split::measure_text_width; use super::content_split::split_line; use crate::cell::Cell; use crate::row::Row; use crate::style::CellAlignment; #[cfg(feature = "tty")] use crate::style::{map_attribute, map_color}; use crate::table::Table; use crate::utils::ColumnDisplayInfo; pub fn delimiter(cell: &Cell, info: &ColumnDisplayInfo, table: &Table) -> char { // Determine, which delimiter should be used if let Some(delimiter) = cell.delimiter { delimiter } else if let Some(delimiter) = info.delimiter { delimiter } else if let Some(delimiter) = table.delimiter { delimiter } else { ' ' } } /// Returns the formatted content of the table. /// The content is organized in the following structure /// /// tc stands for table content and represents the returned value /// ``` text /// column1 column2 /// row1 tc[0][0][0] tc[0][0][1] <-line1 /// tc[0][1][0] tc[0][1][1] <-line2 /// tc[0][2][0] tc[0][2][1] <-line3 /// /// row2 tc[1][0][0] tc[1][0][1] <-line1 /// tc[1][1][0] tc[1][1][1] <-line2 /// tc[1][2][0] tc[1][2][1] <-line3 /// ``` /// /// The strings for each row will be padded and aligned according to their respective column. pub fn format_content(table: &Table, display_info: &[ColumnDisplayInfo]) -> Vec>> { // The content of the whole table let mut table_content = Vec::with_capacity(table.rows.len() + 1); // Format table header if it exists if let Some(header) = table.header() { table_content.push(format_row(header, display_info, table)); } for row in table.rows.iter() { table_content.push(format_row(row, display_info, table)); } table_content } pub fn format_row( row: &Row, display_infos: &[ColumnDisplayInfo], table: &Table, ) -> Vec> { // The content of this specific row let mut temp_row_content = Vec::with_capacity(display_infos.len()); let mut cell_iter = row.cells.iter(); // Now iterate over all cells and handle them according to their alignment for info in display_infos.iter() { if info.is_hidden { cell_iter.next(); continue; } // Each cell is divided into several lines divided by newline // Every line that's too long will be split into multiple lines let mut cell_lines = Vec::new(); // Check if the row has as many cells as the table has columns. // If that's not the case, create a new cell with empty spaces. let cell = if let Some(cell) = cell_iter.next() { cell } else { cell_lines.push(" ".repeat(info.width().into())); temp_row_content.push(cell_lines); continue; }; // The delimiter is configurable, determine which one should be used for this cell. let delimiter = delimiter(cell, info, table); // Iterate over each line and split it into multiple lines if necessary. // Newlines added by the user will be preserved. for line in cell.content.iter() { if measure_text_width(line) > info.content_width.into() { let mut parts = split_line(line, info, delimiter); cell_lines.append(&mut parts); } else { cell_lines.push(line.into()); } } // Remove all unneeded lines of this cell, if the row's height is capped to a certain // amount of lines and there're too many lines in this cell. // This then truncates and inserts a '...' string at the end of the last line to indicate // that the cell has been truncated. if let Some(lines) = row.max_height { if cell_lines.len() > lines { // We already have to many lines. Cut off the surplus lines. let _ = cell_lines.split_off(lines); // Directly access the last line. let last_line = cell_lines .get_mut(lines - 1) .expect("We know it's this long."); // Truncate any ansi codes, as the following cutoff might break ansi code // otherwise anyway. This could be handled smarter, but it's simple and just works. #[cfg(feature = "custom_styling")] { let stripped = console::strip_ansi_codes(last_line).to_string(); *last_line = stripped; } let max_width: usize = info.content_width.into(); let indicator_width = table.truncation_indicator.width(); let mut truncate_at = 0; // Start the accumulated_width with the indicator_width, which is the minimum width // we may show anyway. let mut accumulated_width = indicator_width; let mut full_string_fits = false; // Leave these print statements in here in case we ever have to debug this annoying // stuff again. //println!("\nSTART:"); //println!("\nMax width: {max_width}, Indicator width: {indicator_width}"); //println!("Full line hex: {last_line}"); //println!( // "Full line hex: {}", // last_line // .as_bytes() // .iter() // .map(|byte| format!("{byte:02x}")) // .collect::>() // .join(", ") //); // Iterate through the UTF-8 graphemes. // Check the `split_long_word` inline function docs to see why we're using // graphemes. // **Note:** The `index` here is the **byte** index. So we cannot just // String::truncate afterwards. We have to convert to a byte vector to perform // the truncation first. let mut grapheme_iter = last_line.grapheme_indices(true).peekable(); while let Some((index, grapheme)) = grapheme_iter.next() { // Leave these print statements in here in case we ever have to debug this // annoying stuff again //println!( // "Current index: {index}, Next grapheme: {grapheme} (width: {})", // grapheme.width() //); //println!( // "Next grapheme hex: {}", // grapheme // .as_bytes() // .iter() // .map(|byte| format!("{byte:02x}")) // .collect::>() // .join(", ") //); // Immediately save where to truncate in case this grapheme doesn't fit. // The index is just before the current grapheme actually starts. truncate_at = index; // Check if the next grapheme would break the boundary of the allowed line // length. let new_width = accumulated_width + grapheme.width(); //println!( // "Next width: {new_width}/{max_width} ({accumulated_width} + {})", // grapheme.width() //); if new_width > max_width { //println!( // "Breaking: {:?}", // accumulated_width + grapheme.width() > max_width //); break; } // The grapheme seems to fit. Save the index and check the next one. accumulated_width += grapheme.width(); // This is a special case. // We reached the last char, meaning that full last line + the indicator fit. if grapheme_iter.peek().is_none() { full_string_fits = true } } // Only do any truncation logic if the line doesn't fit. if !full_string_fits { // Truncate the string at the byte index just behind the last valid grapheme // and overwrite the last line with the new truncated string. let mut last_line_bytes = last_line.clone().into_bytes(); last_line_bytes.truncate(truncate_at); let new_last_line = String::from_utf8(last_line_bytes) .expect("We cut at an exact char boundary"); *last_line = new_last_line; } // Push the truncation indicator. last_line.push_str(&table.truncation_indicator); } } // Iterate over all generated lines of this cell and align them let cell_lines = cell_lines .iter() .map(|line| align_line(table, info, cell, line.to_string())); temp_row_content.push(cell_lines.collect()); } // Right now, we have a different structure than desired. // The content is organized by `row->cell->line`. // We want to remove the cell from our datastructure, since this makes the next step a lot easier. // In the end it should look like this: `row->lines->column`. // To achieve this, we calculate the max amount of lines for the current row. // Afterwards, we iterate over each cell and convert the current structure to the desired one. // This step basically transforms this: // tc[0][0][0] tc[0][1][0] // tc[0][0][1] tc[0][1][1] // tc[0][0][2] This part of the line is missing // // to this: // tc[0][0][0] tc[0][0][1] // tc[0][1][0] tc[0][1][1] // tc[0][2][0] tc[0][2][1] <- Now filled with placeholder (spaces) let max_lines = temp_row_content.iter().map(Vec::len).max().unwrap_or(0); let mut row_content = Vec::with_capacity(max_lines * display_infos.len()); // Each column should have `max_lines` for this row. // Cells content with fewer lines will simply be topped up with empty strings. for index in 0..max_lines { let mut line = Vec::with_capacity(display_infos.len()); let mut cell_iter = temp_row_content.iter(); for info in display_infos.iter() { if info.is_hidden { continue; } let cell = cell_iter.next().unwrap(); match cell.get(index) { // The current cell has content for this line. Append it Some(content) => line.push(content.clone()), // The current cell doesn't have content for this line. // Fill with a placeholder (empty spaces) None => line.push(" ".repeat(info.width().into())), } } row_content.push(line); } row_content } /// Apply the alignment for a column. Alignment can be either Left/Right/Center. /// In every case all lines will be exactly the same character length `info.width - padding long` /// This is needed, so we can simply insert it into the border frame later on. /// Padding is applied in this function as well. #[allow(unused_variables)] fn align_line(table: &Table, info: &ColumnDisplayInfo, cell: &Cell, mut line: String) -> String { let content_width = info.content_width; let remaining: usize = usize::from(content_width).saturating_sub(measure_text_width(&line)); // Apply the styling before aligning the line, if the user requests it. // That way non-delimiter whitespaces won't have stuff like underlines. #[cfg(feature = "tty")] if table.should_style() && table.style_text_only { line = style_line(line, cell); } // Determine the alignment of the column cells. // Cell settings overwrite the columns Alignment settings. // Default is Left let alignment = if let Some(alignment) = cell.alignment { alignment } else if let Some(alignment) = info.cell_alignment { alignment } else { CellAlignment::Left }; // Apply left/right/both side padding depending on the alignment of the column match alignment { CellAlignment::Left => { line += &" ".repeat(remaining); } CellAlignment::Right => { line = " ".repeat(remaining) + &line; } CellAlignment::Center => { let left_padding = (remaining as f32 / 2f32).ceil() as usize; let right_padding = (remaining as f32 / 2f32).floor() as usize; line = " ".repeat(left_padding) + &line + &" ".repeat(right_padding); } } line = pad_line(&line, info); #[cfg(feature = "tty")] if table.should_style() && !table.style_text_only { return style_line(line, cell); } line } /// Apply the column's padding to this line fn pad_line(line: &str, info: &ColumnDisplayInfo) -> String { let mut padded_line = String::new(); padded_line += &" ".repeat(info.padding.0.into()); padded_line += line; padded_line += &" ".repeat(info.padding.1.into()); padded_line } #[cfg(feature = "tty")] fn style_line(line: String, cell: &Cell) -> String { // Just return the line, if there's no need to style. if cell.fg.is_none() && cell.bg.is_none() && cell.attributes.is_empty() { return line; } let mut content = style(line); // Apply text color if let Some(color) = cell.fg { content = content.with(map_color(color)); } // Apply background color if let Some(color) = cell.bg { content = content.on(map_color(color)); } for attribute in cell.attributes.iter() { content = content.attribute(map_attribute(*attribute)); } content.to_string() } comfy-table-7.1.4/src/utils/formatting/content_split/custom_styling.rs000064400000000000000000000152151046102023000244620ustar 00000000000000use ansi_str::AnsiStr; use unicode_segmentation::UnicodeSegmentation; use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; const ANSI_RESET: &str = "\u{1b}[0m"; /// Returns printed length of string, takes into account escape codes #[inline(always)] pub fn measure_text_width(s: &str) -> usize { s.ansi_strip().width() } /// Split the line by the given deliminator without breaking ansi codes that contain the delimiter pub fn split_line_by_delimiter(line: &str, delimiter: char) -> Vec { let mut lines: Vec = Vec::new(); let mut current_line = String::default(); // Iterate over line, splitting text with delimiter let iter = console::AnsiCodeIterator::new(line); for (str_slice, is_esc) in iter { if is_esc { current_line.push_str(str_slice); } else { let mut split = str_slice.split(delimiter); // Text before first delimiter (if any) belongs to previous line let first = split .next() .expect("split always produces at least one value"); current_line.push_str(first); // Text after each delimiter goes to new line. for text in split { lines.push(current_line); current_line = text.to_string(); } } } lines.push(current_line); fix_style_in_split_str(lines.as_mut()); lines } /// Splits a long word at a given character width. Inserting the needed ansi codes to preserve style. pub fn split_long_word(allowed_width: usize, word: &str) -> (String, String) { // A buffer for the first half of the split str, which will take up at most `allowed_len` characters when printed to the terminal. let mut head = String::with_capacity(word.len()); // A buffer for the second half of the split str let mut tail = String::with_capacity(word.len()); // Tracks the len() of head let mut head_len = 0; // Tracks the len() of head, sans trailing ansi escape codes let mut head_len_last = 0; // Count of *non-trailing* escape codes in the buffer. let mut escape_count_last = 0; // A buffer for the escape codes that exist in the str before the split. let mut escapes = Vec::new(); // Iterate over segments of the input string, each segment is either a singe escape code or block of text containing no escape codes. // Add text and escape codes to the head buffer, keeping track of printable length and what ansi codes are active, until there is no more room in allowed_width. // If the str was split at a point with active escape-codes, add the ansi reset code to the end of head, and the list of active escape codes to the beginning of tail. let mut iter = console::AnsiCodeIterator::new(word); for (str_slice, is_esc) in iter.by_ref() { if is_esc { escapes.push(str_slice); // If the code is reset, that means all current codes in the buffer can be ignored. if str_slice == ANSI_RESET { escapes.clear(); } } let slice_len = match is_esc { true => 0, false => str_slice.width(), }; if head_len + slice_len <= allowed_width { head.push_str(str_slice); head_len += slice_len; if !is_esc { // allows popping unneeded escape codes later head_len_last = head.len(); escape_count_last = escapes.len(); } } else { assert!(!is_esc); let mut graphmes = str_slice.graphemes(true).peekable(); while let Some(c) = graphmes.peek() { let character_width = c.width(); if allowed_width < head_len + character_width { break; } head_len += character_width; let c = graphmes.next().unwrap(); head.push_str(c); // c is not escape code head_len_last = head.len(); escape_count_last = escapes.len(); } // cut off dangling escape codes since they should have no effect head.truncate(head_len_last); if escape_count_last != 0 { head.push_str(ANSI_RESET); } for esc in escapes { tail.push_str(esc); } let remaining: String = graphmes.collect(); tail.push_str(&remaining); break; } } iter.for_each(|s| tail.push_str(s.0)); (head, tail) } /// Fixes ansi escape codes in a split string /// 1. Adds reset code to the end of each substring if needed. /// 2. Keeps track of previous substring's escape codes and inserts them in later substrings to continue style pub fn fix_style_in_split_str(words: &mut [String]) { let mut escapes: Vec = Vec::new(); for word in words { // before we modify the escape list, make a copy let prepend = if !escapes.is_empty() { Some(escapes.join("")) } else { None }; // add escapes in word to escape list let iter = console::AnsiCodeIterator::new(word) .filter(|(_, is_esc)| *is_esc) .map(|v| v.0); for esc in iter { if esc == ANSI_RESET { escapes.clear() } else { escapes.push(esc.to_string()) } } // insert previous esc sequences at the beginning of the segment if let Some(prepend) = prepend { word.insert_str(0, &prepend); } // if there are active escape sequences, we need to append reset if !escapes.is_empty() { word.push_str(ANSI_RESET); } } } #[cfg(test)] mod test { #[test] fn ansi_aware_split_test() { use super::split_line_by_delimiter; let text = "\u{1b}[1m head [ middle [ tail \u{1b}[0m[ after"; let split = split_line_by_delimiter(text, '['); assert_eq!( split, [ "\u{1b}[1m head \u{1b}[0m", "\u{1b}[1m middle \u{1b}[0m", "\u{1b}[1m tail \u{1b}[0m", " after" ] ) } // TODO: Figure out why this fails with the custom_styling feature enabled. #[test] #[cfg(not(feature = "custom_styling"))] fn measure_text_width_osc8_test() { use super::measure_text_width; use unicode_width::UnicodeWidthStr; let text = "\x1b]8;;https://github.com\x1b\\This is a link\x1b]8;;\x1b"; let width = measure_text_width(text); assert_eq!(text.width(), 41); assert_eq!(width, 14); } } comfy-table-7.1.4/src/utils/formatting/content_split/mod.rs000064400000000000000000000165411046102023000221610ustar 00000000000000use crate::utils::ColumnDisplayInfo; #[cfg(feature = "custom_styling")] mod custom_styling; #[cfg(not(feature = "custom_styling"))] mod normal; #[cfg(feature = "custom_styling")] pub use custom_styling::*; #[cfg(not(feature = "custom_styling"))] pub use normal::*; /// Split a line if it's longer than the allowed columns (width - padding). /// /// This function tries to do this in a smart way, by splitting the content /// with a given delimiter at the very beginning. /// These "elements" then get added one-by-one to the lines, until a line is full. /// As soon as the line is full, we add it to the result set and start a new line. /// /// This is repeated until there are no more "elements". /// /// Mid-element splits only occurs if an element doesn't fit in a single line by itself. pub fn split_line(line: &str, info: &ColumnDisplayInfo, delimiter: char) -> Vec { let mut lines = Vec::new(); let content_width = usize::from(info.content_width); // Split the line by the given deliminator and turn the content into a stack. // Also clone it and convert it into a Vec. Otherwise, we get some burrowing problems // due to early drops of borrowed values that need to be inserted into `Vec<&str>` let mut elements = split_line_by_delimiter(line, delimiter); // Reverse it, since we want to push/pop without reversing the text. elements.reverse(); let mut current_line = String::new(); while let Some(next) = elements.pop() { let current_length = measure_text_width(¤t_line); let next_length = measure_text_width(&next); // Some helper variables // The length of the current line when combining it with the next element // Add 1 for the delimiter if we are on a non-empty line. let mut added_length = next_length + current_length; if !current_line.is_empty() { added_length += 1; } // The remaining width for this column. If we are on a non-empty line, subtract 1 for the delimiter. let mut remaining_width = content_width - current_length; if !current_line.is_empty() { remaining_width = remaining_width.saturating_sub(1); } // The next element fits into the current line if added_length <= content_width { // Only add delimiter, if we're not on a fresh line if !current_line.is_empty() { current_line.push(delimiter); } current_line += &next; // Already complete the current line, if there isn't space for more than two chars current_line = check_if_full(&mut lines, content_width, current_line); continue; } // The next element doesn't fit in the current line // Check, if there's enough space in the current line in case we decide to split the // element and only append a part of it to the current line. // If there isn't enough space, we simply push the current line, put the element back // on stack and start with a fresh line. if !current_line.is_empty() && remaining_width <= MIN_FREE_CHARS { elements.push(next); lines.push(current_line); current_line = String::new(); continue; } // Ok. There's still enough space to fit something in (more than MIN_FREE_CHARS characters) // There are two scenarios: // // 1. The word is too long for a single line. // In this case, we have to split the element anyway. Let's fill the remaining space on // the current line with, start a new line and push the remaining part on the stack. // 2. The word is short enough to fit as a whole into a line // In that case we simply push the current line and start a new one with the current element // Case 1 // The element is longer than the specified content_width // Split the word, push the remaining string back on the stack if next_length > content_width { let new_line = current_line.is_empty(); // Only add delimiter, if we're not on a fresh line if !new_line { current_line.push(delimiter); } let (mut next, mut remaining) = split_long_word(remaining_width, &next); // This is an ugly hack, but it's needed for now. // // Scenario: The current column has to have a width of 1, and we work with a new line. // However, the next char is a multi-character UTF-8 symbol. // // Since a multi-character wide symbol doesn't fit into a 1-character column, // this code would loop endlessly. (There's no legitimate way to split that character.) // Hence, we have to live with the fact, that this line will look broken, as we put a // two-character wide symbol into it, despite the line being formatted for 1 character. if new_line && next.is_empty() { let mut chars = remaining.chars(); next.push(chars.next().unwrap()); remaining = chars.collect(); } current_line += &next; elements.push(remaining); // Push the finished line, and start a new one lines.push(current_line); current_line = String::new(); continue; } // Case 2 // The element fits into a single line. // Push the current line and initialize the next line with the element. lines.push(current_line); current_line = next.to_string(); current_line = check_if_full(&mut lines, content_width, current_line); } if !current_line.is_empty() { lines.push(current_line); } lines } /// This is the minimum of available characters per line. /// It's used to check, whether another element can be added to the current line. /// Otherwise, the line will simply be left as it is, and we start with a new one. /// Two chars seems like a reasonable approach, since this would require next element to be /// a single char + delimiter. const MIN_FREE_CHARS: usize = 2; /// Check if the current line is too long and whether we should start a new one /// If it's too long, we add the current line to the list of lines and return a new [String]. /// Otherwise, we simply return the current line and basically don't do anything. fn check_if_full(lines: &mut Vec, content_width: usize, current_line: String) -> String { // Already complete the current line, if there isn't space for more than two chars if measure_text_width(¤t_line) > content_width.saturating_sub(MIN_FREE_CHARS) { lines.push(current_line); return String::new(); } current_line } #[cfg(test)] mod tests { use super::*; use unicode_width::UnicodeWidthStr; #[test] fn test_split_long_word() { let emoji = "๐Ÿ™‚โ€โ†•๏ธ"; // U+1F642 U+200D U+2195 U+FE0F head shaking vertically assert_eq!(emoji.len(), 13); assert_eq!(emoji.chars().count(), 4); assert_eq!(emoji.width(), 2); let (word, remaining) = split_long_word(emoji.width(), emoji); assert_eq!(word, "\u{1F642}\u{200D}\u{2195}\u{FE0F}"); assert_eq!(word.len(), 13); assert_eq!(word.chars().count(), 4); assert_eq!(word.width(), 2); assert!(remaining.is_empty()); } } comfy-table-7.1.4/src/utils/formatting/content_split/normal.rs000064400000000000000000000037241046102023000226710ustar 00000000000000use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthStr; /// returns printed length of string /// if ansi feature enabled, takes into account escape codes #[inline(always)] pub fn measure_text_width(s: &str) -> usize { s.width() } /// Split a line into its individual parts along the given delimiter. pub fn split_line_by_delimiter(line: &str, delimiter: char) -> Vec { line.split(delimiter) .map(ToString::to_string) .collect::>() } /// Splits a long word at a given character width. /// This needs some special logic, as we have to take multi-character UTF-8 symbols into account. /// When simply splitting at a certain char position, we might end up with a string that's has a /// wider display width than allowed. pub fn split_long_word(allowed_width: usize, word: &str) -> (String, String) { let mut current_width = 0; let mut parts = String::new(); let mut graphmes = word.graphemes(true).peekable(); // Check if the string might be too long, one Unicode grapheme at a time. // Peek into the next grapheme and check the exit condition. // // This code uses graphemes to handle both zero-width joiner[0] UTF-8 chars, which // combine multiple UTF-8 chars into a single grapheme, and variant selectors [1], // which pick a certain variant of the preceding char. // // [0]: https://en.wikipedia.org/wiki/Zero-width_joiner // [1]: https://en.wikipedia.org/wiki/Variation_Selectors_(Unicode_block) while let Some(c) = graphmes.peek() { if (current_width + c.width()) > allowed_width { break; } // We can unwrap, as we just checked that a suitable grapheme is next in line. let c = graphmes.next().unwrap(); let character_width = c.width(); current_width += character_width; parts.push_str(c); } // Collect the remaining characters. let remaining = graphmes.collect(); (parts, remaining) } comfy-table-7.1.4/src/utils/formatting/mod.rs000064400000000000000000000001001046102023000172540ustar 00000000000000pub mod borders; pub mod content_format; pub mod content_split; comfy-table-7.1.4/src/utils/mod.rs000064400000000000000000000033271046102023000151200ustar 00000000000000pub mod arrangement; pub mod formatting; use crate::style::{CellAlignment, ColumnConstraint}; use crate::{Column, Table}; use arrangement::arrange_content; use formatting::borders::draw_borders; use formatting::content_format::format_content; /// This struct is ONLY used when table.to_string() is called. /// It's purpose is to store intermediate results, information on how to /// arrange the table and other convenience variables. /// /// The idea is to have a place for all this intermediate stuff, without /// actually touching the Column struct. #[derive(Debug)] pub struct ColumnDisplayInfo { pub padding: (u16, u16), pub delimiter: Option, /// The actual allowed content width after arrangement pub content_width: u16, /// The content alignment of cells in this column pub cell_alignment: Option, is_hidden: bool, } impl ColumnDisplayInfo { pub fn new(column: &Column, mut content_width: u16) -> Self { // The min contend width may only be 1 if content_width == 0 { content_width = 1; } Self { padding: column.padding, delimiter: column.delimiter, content_width, cell_alignment: column.cell_alignment, is_hidden: matches!(column.constraint, Some(ColumnConstraint::Hidden)), } } pub fn width(&self) -> u16 { self.content_width .saturating_add(self.padding.0) .saturating_add(self.padding.1) } } pub fn build_table(table: &Table) -> impl Iterator { let display_info = arrange_content(table); let content = format_content(table, &display_info); draw_borders(table, &content, &display_info).into_iter() } comfy-table-7.1.4/tests/all/add_predicate.rs000064400000000000000000000222571046102023000170770ustar 00000000000000use pretty_assertions::assert_eq; use comfy_table::*; #[test] fn add_predicate_single_true() { let mut table = Table::new(); table .set_header(vec!["Header1", "Header2", "Header3"]) .add_row(vec![ "This is a text", "This is another text", "This is the third text", ]) .add_row_if( |_, _| true, &vec![ "This is another text", "Now\nadd some\nmulti line stuff", "This is awesome", ], ); println!("{table}"); let expected = " +----------------------+----------------------+------------------------+ | Header1 | Header2 | Header3 | +======================================================================+ | This is a text | This is another text | This is the third text | |----------------------+----------------------+------------------------| | This is another text | Now | This is awesome | | | add some | | | | multi line stuff | | +----------------------+----------------------+------------------------+"; assert_eq!(expected, "\n".to_string() + &table.to_string()); } #[test] fn add_predicate_single_false() { let mut table = Table::new(); table .set_header(vec!["Header1", "Header2", "Header3"]) .add_row(vec![ "This is a text", "This is another text", "This is the third text", ]) .add_row_if( |_, _| false, &vec![ "This is another text", "Now\nadd some\nmulti line stuff", "This is awesome", ], ); println!("{table}"); let expected = " +----------------+----------------------+------------------------+ | Header1 | Header2 | Header3 | +================================================================+ | This is a text | This is another text | This is the third text | +----------------+----------------------+------------------------+"; assert_eq!(expected, "\n".to_string() + &table.to_string()); } #[test] fn add_predicate_single_mixed() { let mut table = Table::new(); table .set_header(vec!["Header1", "Header2", "Header3"]) .add_row(vec![ "This is a text", "This is another text", "This is the third text", ]) .add_row_if( |_, _| false, &vec!["I won't get displayed", "Me neither", "Same here!"], ) .add_row_if( |_, _| true, &vec![ "This is another text", "Now\nadd some\nmulti line stuff", "This is awesome", ], ); println!("{table}"); let expected = " +----------------------+----------------------+------------------------+ | Header1 | Header2 | Header3 | +======================================================================+ | This is a text | This is another text | This is the third text | |----------------------+----------------------+------------------------| | This is another text | Now | This is awesome | | | add some | | | | multi line stuff | | +----------------------+----------------------+------------------------+"; assert_eq!(expected, "\n".to_string() + &table.to_string()); } #[test] fn add_predicate_single_wrong_row_count() { let mut table = Table::new(); table .set_header(vec!["Header1", "Header2", "Header3"]) .add_row(vec![ "This is a text", "This is another text", "This is the third text", ]) .add_row_if( |_, row| row.len() == 2, &vec![ "This is another text", "Now\nadd some\nmulti line stuff", "This is awesome", ], ); println!("{table}"); let expected = " +----------------+----------------------+------------------------+ | Header1 | Header2 | Header3 | +================================================================+ | This is a text | This is another text | This is the third text | +----------------+----------------------+------------------------+"; assert_eq!(expected, "\n".to_string() + &table.to_string()); } #[test] fn add_predicate_multi_true() { let mut table = Table::new(); let rows = vec![ Row::from(&vec![ "This is a text", "This is another text", "This is the third text", ]), Row::from(&vec![ "This is another text", "Now\nadd some\nmulti line stuff", "This is awesome", ]), ]; table .set_header(vec!["Header1", "Header2", "Header3"]) .add_rows_if(|_, _| true, rows); println!("{table}"); let expected = " +----------------------+----------------------+------------------------+ | Header1 | Header2 | Header3 | +======================================================================+ | This is a text | This is another text | This is the third text | |----------------------+----------------------+------------------------| | This is another text | Now | This is awesome | | | add some | | | | multi line stuff | | +----------------------+----------------------+------------------------+"; assert_eq!(expected, "\n".to_string() + &table.to_string()); } #[test] fn add_predicate_multi_false() { let mut table = Table::new(); table .set_header(vec!["Header1", "Header2", "Header3"]) .add_row(vec![ "This is a text", "This is another text", "This is the third text", ]) .add_rows_if( |_, _| false, vec![Row::from(&vec![ "This is another text", "Now\nadd some\nmulti line stuff", "This is awesome", ])], ); println!("{table}"); let expected = " +----------------+----------------------+------------------------+ | Header1 | Header2 | Header3 | +================================================================+ | This is a text | This is another text | This is the third text | +----------------+----------------------+------------------------+"; assert_eq!(expected, "\n".to_string() + &table.to_string()); } #[test] fn add_predicate_multi_mixed() { let mut table = Table::new(); let rows = vec![ Row::from(&vec![ "This is a text", "This is another text", "This is the third text", ]), Row::from(&vec![ "This is another text", "Now\nadd some\nmulti line stuff", "This is awesome", ]), ]; table .set_header(vec!["Header1", "Header2", "Header3"]) .add_rows_if(|_, _| true, rows) .add_rows_if( |_, _| false, vec![Row::from(&vec![ "I won't get displayed", "Me neither", "Same here!", ])], ); println!("{table}"); let expected = " +----------------------+----------------------+------------------------+ | Header1 | Header2 | Header3 | +======================================================================+ | This is a text | This is another text | This is the third text | |----------------------+----------------------+------------------------| | This is another text | Now | This is awesome | | | add some | | | | multi line stuff | | +----------------------+----------------------+------------------------+"; assert_eq!(expected, "\n".to_string() + &table.to_string()); } #[test] fn add_predicate_multi_wrong_rows_count() { let mut table = Table::new(); table .set_header(vec!["Header1", "Header2", "Header3"]) .add_row(vec![ "This is a text", "This is another text", "This is the third text", ]) .add_rows_if( |_, rows| rows.len() == 2, vec![Row::from(&vec![ "This is another text", "Now\nadd some\nmulti line stuff", "This is awesome", ])], ); println!("{table}"); let expected = " +----------------+----------------------+------------------------+ | Header1 | Header2 | Header3 | +================================================================+ | This is a text | This is another text | This is the third text | +----------------+----------------------+------------------------+"; assert_eq!(expected, "\n".to_string() + &table.to_string()); } comfy-table-7.1.4/tests/all/alignment_test.rs000064400000000000000000000034441046102023000173410ustar 00000000000000use pretty_assertions::assert_eq; use comfy_table::*; #[test] /// Cell alignment can be specified on Columns and Cells /// Alignment settings on Cells overwrite the settings of Columns fn cell_alignment() { let mut table = Table::new(); table .set_header(vec!["Header1", "Header2", "Header3"]) .add_row(vec![ "Very long line Test", "Very long line Test", "Very long line Test", ]) .add_row(vec![ Cell::new("Right").set_alignment(CellAlignment::Right), Cell::new("Left").set_alignment(CellAlignment::Left), Cell::new("Center").set_alignment(CellAlignment::Center), ]) .add_row(vec!["Left", "Center", "Right"]); let alignment = [ CellAlignment::Left, CellAlignment::Center, CellAlignment::Right, ]; // Add the alignment to their respective column for (column_index, column) in table.column_iter_mut().enumerate() { let alignment = alignment.get(column_index).unwrap(); column.set_cell_alignment(*alignment); } println!("{table}"); let expected = " +---------------------+---------------------+---------------------+ | Header1 | Header2 | Header3 | +=================================================================+ | Very long line Test | Very long line Test | Very long line Test | |---------------------+---------------------+---------------------| | Right | Left | Center | |---------------------+---------------------+---------------------| | Left | Center | Right | +---------------------+---------------------+---------------------+"; assert_eq!(expected, "\n".to_string() + &table.to_string()); } comfy-table-7.1.4/tests/all/combined_test.rs000064400000000000000000000066071046102023000171470ustar 00000000000000use comfy_table::presets::UTF8_FULL; use comfy_table::*; use pretty_assertions::assert_eq; fn get_preset_table() -> Table { let mut table = Table::new(); table.load_preset(UTF8_FULL) .set_content_arrangement(ContentArrangement::Dynamic) .set_width(80) .set_header(vec![ Cell::new("Header1").add_attribute(Attribute::Bold), Cell::new("Header2").fg(Color::Green), Cell::new("Header3"), ]) .add_row(vec![ Cell::new("This is a bold text").add_attribute(Attribute::Bold), Cell::new("This is a green text").fg(Color::Green), Cell::new("This one has black background").bg(Color::Black), ]) .add_row(vec![ Cell::new("Blinky boi").add_attribute(Attribute::SlowBlink), Cell::new("This table's content is dynamically arranged. The table is exactly 80 characters wide.\nHere comes a reallylongwordthatshoulddynamicallywrap"), Cell::new("COMBINE ALL THE THINGS") .fg(Color::Green) .bg(Color::Black) .add_attributes(vec![ Attribute::Bold, Attribute::SlowBlink, ]) ]); table } #[test] fn combined_features() { let mut table = get_preset_table(); table.force_no_tty().enforce_styling(); println!("{table}"); let expected = " โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚\u{1b}[1m Header1 \u{1b}[0mโ”†\u{1b}[38;5;10m Header2 \u{1b}[39mโ”† Header3 โ”‚ โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ก โ”‚\u{1b}[1m This is a bold text \u{1b}[0mโ”†\u{1b}[38;5;10m This is a green text \u{1b}[39mโ”†\u{1b}[48;5;0m This one has black \u{1b}[49mโ”‚ โ”‚ โ”† โ”†\u{1b}[48;5;0m background \u{1b}[49mโ”‚ โ”œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ผโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ผโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ค โ”‚\u{1b}[5m Blinky boi \u{1b}[0mโ”† This table\'s content is โ”†\u{1b}[48;5;0m\u{1b}[38;5;10m\u{1b}[1m\u{1b}[5m COMBINE ALL THE THINGS \u{1b}[0mโ”‚ โ”‚ โ”† dynamically arranged. The โ”† โ”‚ โ”‚ โ”† table is exactly 80 โ”† โ”‚ โ”‚ โ”† characters wide. โ”† โ”‚ โ”‚ โ”† Here comes a reallylongwordth โ”† โ”‚ โ”‚ โ”† atshoulddynamicallywrap โ”† โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜"; println!("{expected}"); assert_eq!(expected, "\n".to_string() + &table.to_string()); } comfy-table-7.1.4/tests/all/constraints_test.rs000064400000000000000000000273461046102023000177410ustar 00000000000000use comfy_table::ColumnConstraint::*; use comfy_table::Width::*; use comfy_table::*; use pretty_assertions::assert_eq; use super::assert_table_line_width; fn get_constraint_table() -> Table { let mut table = Table::new(); table .set_header(vec!["smol", "Header2", "Header3"]) .add_row(vec![ "smol", "This is another text", "This is the third text", ]) .add_row(vec![ "smol", "Now\nadd some\nmulti line stuff", "This is awesome", ]); table } #[test] /// Ensure max-, min- and fixed-width constraints are respected fn fixed_max_min_constraints() { let mut table = get_constraint_table(); table.set_constraints(vec![ LowerBoundary(Fixed(10)), UpperBoundary(Fixed(8)), Absolute(Fixed(10)), ]); println!("{table}"); let expected = " +----------+--------+----------+ | smol | Header | Header3 | | | 2 | | +==============================+ | smol | This | This is | | | is ano | the | | | ther | third | | | text | text | |----------+--------+----------| | smol | Now | This is | | | add | awesome | | | some | | | | multi | | | | line | | | | stuff | | +----------+--------+----------+"; println!("{expected}"); assert_eq!(expected, "\n".to_string() + &table.to_string()); // Now try this again when using dynamic content arrangement // The table tries to arrange to 28 characters, // but constraints enforce a width of at least 10+10+2+1+4 = 27 // min_width + max_width + middle_padding + middle_min_width + borders // Since the left and right column are fixed, the middle column should only get a width of 2 table .set_content_arrangement(ContentArrangement::Dynamic) .set_width(28); println!("{table}"); let expected = " +----------+----+----------+ | smol | He | Header3 | | | ad | | | | er | | | | 2 | | +==========================+ | smol | Th | This is | | | is | the | | | is | third | | | an | text | | | ot | | | | he | | | | r | | | | te | | | | xt | | |----------+----+----------| | smol | No | This is | | | w | awesome | | | ad | | | | d | | | | so | | | | me | | | | mu | | | | lt | | | | i | | | | li | | | | ne | | | | st | | | | uf | | | | f | | +----------+----+----------+"; println!("{expected}"); assert_eq!(expected, "\n".to_string() + &table.to_string()); } #[test] /// Max and Min constraints won't be considered, if they are unnecessary /// This is true for normal and dynamic arrangement tables. fn unnecessary_max_min_constraints() { let mut table = get_constraint_table(); table.set_constraints(vec![LowerBoundary(Fixed(1)), UpperBoundary(Fixed(30))]); println!("{table}"); let expected = " +------+----------------------+------------------------+ | smol | Header2 | Header3 | +======================================================+ | smol | This is another text | This is the third text | |------+----------------------+------------------------| | smol | Now | This is awesome | | | add some | | | | multi line stuff | | +------+----------------------+------------------------+"; println!("{expected}"); assert_eq!(expected, "\n".to_string() + &table.to_string()); // Now test for dynamic content arrangement table.set_content_arrangement(ContentArrangement::Dynamic); println!("{table}"); let expected = " +------+----------------------+------------------------+ | smol | Header2 | Header3 | +======================================================+ | smol | This is another text | This is the third text | |------+----------------------+------------------------| | smol | Now | This is awesome | | | add some | | | | multi line stuff | | +------+----------------------+------------------------+"; println!("{expected}"); assert_eq!(expected, "\n".to_string() + &table.to_string()); } #[test] /// The user can specify constraints that result in bigger width than actually provided /// This is allowed, but results in a wider table than actually aimed for. /// Anyway we still try to fit everything as good as possible, which of course breaks stuff. fn constraints_bigger_than_table_width() { let mut table = get_constraint_table(); table .set_content_arrangement(ContentArrangement::Dynamic) .set_width(28) .set_constraints(vec![ UpperBoundary(Fixed(50)), LowerBoundary(Fixed(30)), ContentWidth, ]); println!("{table}"); let expected = " +---+------------------------------+------------------------+ | s | Header2 | Header3 | | m | | | | o | | | | l | | | +===========================================================+ | s | This is another text | This is the third text | | m | | | | o | | | | l | | | |---+------------------------------+------------------------| | s | Now | This is awesome | | m | add some | | | o | multi line stuff | | | l | | | +---+------------------------------+------------------------+"; println!("{expected}"); assert_eq!(expected, "\n".to_string() + &table.to_string()); } #[test] /// Test correct usage of the Percentage constraint. /// Percentage allows to set a fixed width. fn percentage() { let mut table = get_constraint_table(); // Set a percentage of 20% for the first column. // The the rest should arrange accordingly. table .set_content_arrangement(ContentArrangement::Dynamic) .set_width(40) .set_constraints(vec![Absolute(Percentage(20))]); println!("{table}"); let expected = " +-------+---------------+--------------+ | smol | Header2 | Header3 | +======================================+ | smol | This is | This is the | | | another text | third text | |-------+---------------+--------------| | smol | Now | This is | | | add some | awesome | | | multi line | | | | stuff | | +-------+---------------+--------------+"; println!("{expected}"); assert_eq!(expected, "\n".to_string() + &table.to_string()); } #[test] /// A single percentage constraint should be 100% at most. fn max_100_percentage() { let mut table = Table::new(); table .set_header(vec!["smol"]) .add_row(vec!["smol"]) .set_content_arrangement(ContentArrangement::Dynamic) .set_width(40) .set_constraints(vec![Absolute(Percentage(200))]); println!("{table}"); let expected = " +--------------------------------------+ | smol | +======================================+ | smol | +--------------------------------------+"; println!("{expected}"); assert_table_line_width(&table, 40); assert_eq!(expected, "\n".to_string() + &table.to_string()); } #[test] fn percentage_second() { let mut table = get_constraint_table(); table .set_content_arrangement(ContentArrangement::Dynamic) .set_width(40) .set_constraints(vec![ LowerBoundary(Percentage(40)), UpperBoundary(Percentage(30)), Absolute(Percentage(30)), ]); println!("{table}"); let expected = " +--------------+----------+----------+ | smol | Header2 | Header3 | +====================================+ | smol | This is | This is | | | another | the | | | text | third | | | | text | |--------------+----------+----------| | smol | Now | This is | | | add some | awesome | | | multi | | | | line | | | | stuff | | +--------------+----------+----------+"; println!("{expected}"); assert_eq!(expected, "\n".to_string() + &table.to_string()); } #[test] fn max_percentage() { let mut table = get_constraint_table(); table .set_content_arrangement(ContentArrangement::Dynamic) .set_width(40) .set_constraints(vec![ ContentWidth, UpperBoundary(Percentage(30)), Absolute(Percentage(30)), ]); println!("{table}"); let expected = " +------+----------+----------+ | smol | Header2 | Header3 | +============================+ | smol | This is | This is | | | another | the | | | text | third | | | | text | |------+----------+----------| | smol | Now | This is | | | add some | awesome | | | multi | | | | line | | | | stuff | | +------+----------+----------+"; println!("{expected}"); assert_eq!(expected, "\n".to_string() + &table.to_string()); } #[test] /// Ensure that both min and max in [Boundaries] is respected fn min_max_boundary() { let mut table = get_constraint_table(); table .set_content_arrangement(ContentArrangement::Dynamic) .set_width(40) .set_constraints(vec![ Boundaries { lower: Percentage(50), upper: Fixed(2), }, Boundaries { lower: Fixed(15), upper: Percentage(50), }, Absolute(Percentage(30)), ]); println!("{table}"); let expected = " +------------------+---------------+----------+ | smol | Header2 | Header3 | +=============================================+ | smol | This is | This is | | | another text | the | | | | third | | | | text | |------------------+---------------+----------| | smol | Now | This is | | | add some | awesome | | | multi line | | | | stuff | | +------------------+---------------+----------+"; println!("{expected}"); assert_eq!(expected, "\n".to_string() + &table.to_string()); } #[rstest::rstest] #[case(ContentArrangement::Dynamic)] #[case(ContentArrangement::Disabled)] /// Empty table with zero width constraint. fn empty_table(#[case] arrangement: ContentArrangement) { let mut table = Table::new(); table .add_row(vec![""]) .set_content_arrangement(arrangement) .set_constraints(vec![Absolute(Fixed(0))]); println!("{table}"); let expected = " +---+ | | +---+"; println!("{expected}"); assert_eq!(expected, "\n".to_string() + &table.to_string()); } comfy-table-7.1.4/tests/all/content_arrangement_test.rs000064400000000000000000000265661046102023000214320ustar 00000000000000use comfy_table::ColumnConstraint; use comfy_table::Width; use pretty_assertions::assert_eq; use comfy_table::{ContentArrangement, Table}; use super::assert_table_line_width; /// Test the robustness of the dynamic table arrangement. #[test] fn simple_dynamic_table() { let mut table = Table::new(); table.set_header(vec!["Header1", "Header2", "Head"]) .set_content_arrangement(ContentArrangement::Dynamic) .set_width(25) .add_row(vec![ "This is a very long line with a lot of text", "This is anotherverylongtextwithlongwords text", "smol", ]) .add_row(vec![ "This is another text", "Now let's\nadd a really long line in the middle of the cell \n and add more multi line stuff", "smol", ]); println!("{table}"); let expected = " +--------+-------+------+ | Header | Heade | Head | | 1 | r2 | | +=======================+ | This | This | smol | | is a | is | | | very | anoth | | | long | erver | | | line | ylong | | | with a | textw | | | lot of | ithlo | | | text | ngwor | | | | ds | | | | text | | |--------+-------+------| | This | Now | smol | | is ano | let's | | | ther | add a | | | text | reall | | | | y | | | | long | | | | line | | | | in | | | | the | | | | middl | | | | e of | | | | the | | | | cell | | | | and | | | | add | | | | more | | | | multi | | | | line | | | | stuff | | +--------+-------+------+"; println!("{expected}"); assert_table_line_width(&table, 25); assert_eq!(expected, "\n".to_string() + &table.to_string()); } /// This table checks the scenario, where a column has a big max_width, but a lot of the assigned /// space doesn't get used after splitting the lines. This happens mostly when there are /// many long words in a single column. /// The remaining space should rather be distributed to other cells. #[test] fn distribute_space_after_split() { let mut table = Table::new(); table .set_header(vec!["Header1", "Header2", "Head"]) .set_content_arrangement(ContentArrangement::Dynamic) .set_width(80) .add_row(vec![ "This is a very long line with a lot of text", "This is text with a anotherverylongtexttesttest", "smol", ]); println!("{table}"); let expected = " +-----------------------------------------+-----------------------------+------+ | Header1 | Header2 | Head | +==============================================================================+ | This is a very long line with a lot of | This is text with a | smol | | text | anotherverylongtexttesttest | | +-----------------------------------------+-----------------------------+------+"; println!("{expected}"); assert_table_line_width(&table, 80); assert_eq!(expected, "\n".to_string() + &table.to_string()); } /// A single column get's split and a lot of the available isn't used afterward. /// The remaining space should be cut away, making the table more compact. #[test] fn unused_space_after_split() { let mut table = Table::new(); table .set_header(vec!["Header1"]) .set_content_arrangement(ContentArrangement::Dynamic) .set_width(30) .add_row(vec!["This is text with a anotherverylongtext"]); println!("{table}"); let expected = " +---------------------+ | Header1 | +=====================+ | This is text with a | | anotherverylongtext | +---------------------+"; println!("{expected}"); assert_table_line_width(&table, 23); assert_eq!(expected, "\n".to_string() + &table.to_string()); } /// The full width of a table should be used, even if the space isn't used. #[test] fn dynamic_full_width_after_split() { let mut table = Table::new(); table .set_header(vec!["Header1"]) .set_content_arrangement(ContentArrangement::DynamicFullWidth) .set_width(50) .add_row(vec!["This is text with a anotherverylongtexttesttestaa"]); println!("{table}"); let expected = " +------------------------------------------------+ | Header1 | +================================================+ | This is text with a | | anotherverylongtexttesttestaa | +------------------------------------------------+"; println!("{expected}"); assert_table_line_width(&table, 50); assert_eq!(expected, "\n".to_string() + &table.to_string()); } /// This table checks the scenario, where a column has a big max_width, but a lot of the assigned /// space isn't used after splitting the lines. /// The remaining space should rather distributed between all cells. #[test] fn dynamic_full_width() { let mut table = Table::new(); table .set_header(vec!["Header1", "Header2", "smol"]) .set_content_arrangement(ContentArrangement::DynamicFullWidth) .set_width(80) .add_row(vec!["This is a short line", "small", "smol"]); println!("{table}"); let expected = " +-----------------------------------+----------------------+-------------------+ | Header1 | Header2 | smol | +==============================================================================+ | This is a short line | small | smol | +-----------------------------------+----------------------+-------------------+"; println!("{expected}"); assert_table_line_width(&table, 80); assert_eq!(expected, "\n".to_string() + &table.to_string()); } /// Test that a table is displayed in its full width, if the `table.width` is set to the exact /// width the table has, if it's fully expanded. /// /// The same should be the case for values that are larger than this width. #[test] fn dynamic_exact_width() { let header = vec!["a\n---\ni64", "b\n---\ni64", "b_squared\n---\nf64"]; let rows = vec![ vec!["1", "2", "4.0"], vec!["3", "4", "16.0"], vec!["5", "6", "36.0"], ]; for width in 25..40 { let mut table = Table::new(); let table = table .load_preset(comfy_table::presets::UTF8_FULL) .set_content_arrangement(ContentArrangement::Dynamic) .set_width(width); table.set_header(header.clone()).add_rows(rows.clone()); println!("{table}"); let expected = " โ”Œโ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ a โ”† b โ”† b_squared โ”‚ โ”‚ --- โ”† --- โ”† --- โ”‚ โ”‚ i64 โ”† i64 โ”† f64 โ”‚ โ•žโ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ก โ”‚ 1 โ”† 2 โ”† 4.0 โ”‚ โ”œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ผโ•Œโ•Œโ•Œโ•Œโ•Œโ”ผโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ค โ”‚ 3 โ”† 4 โ”† 16.0 โ”‚ โ”œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ผโ•Œโ•Œโ•Œโ•Œโ•Œโ”ผโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ค โ”‚ 5 โ”† 6 โ”† 36.0 โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜"; println!("{expected}"); assert_table_line_width(table, 25); assert_eq!(expected, "\n".to_string() + &table.to_string()); } } /// Test that the formatting works as expected, if the table is slightly smaller than the max width /// of the table. #[test] fn dynamic_slightly_smaller() { let header = vec!["a\n---\ni64", "b\n---\ni64", "b_squared\n---\nf64"]; let rows = vec![ vec!["1", "2", "4.0"], vec!["3", "4", "16.0"], vec!["5", "6", "36.0"], ]; let mut table = Table::new(); let table = table .load_preset(comfy_table::presets::UTF8_FULL) .set_content_arrangement(ContentArrangement::Dynamic) .set_width(24); table.set_header(header.clone()).add_rows(rows.clone()); println!("{table}"); let expected = " โ”Œโ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ a โ”† b โ”† b_square โ”‚ โ”‚ --- โ”† --- โ”† d โ”‚ โ”‚ i64 โ”† i64 โ”† --- โ”‚ โ”‚ โ”† โ”† f64 โ”‚ โ•žโ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ก โ”‚ 1 โ”† 2 โ”† 4.0 โ”‚ โ”œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ผโ•Œโ•Œโ•Œโ•Œโ•Œโ”ผโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ค โ”‚ 3 โ”† 4 โ”† 16.0 โ”‚ โ”œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ผโ•Œโ•Œโ•Œโ•Œโ•Œโ”ผโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ค โ”‚ 5 โ”† 6 โ”† 36.0 โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜"; println!("{expected}"); assert_table_line_width(table, 24); assert_eq!(expected, "\n".to_string() + &table.to_string()); } /// This failed on a python integration test case in the polars project. /// This a regression test. #[test] fn polar_python_test_tbl_width_chars() { let header = vec![ "a really long col\n---\ni64", "b\n---\nstr", "this is 10\n---\ni64", ]; let rows = vec![ vec!["1", "", "4"], vec!["2", "this is a string value that will...", "5"], vec!["3", "null", "6"], ]; let mut table = Table::new(); let table = table .load_preset(comfy_table::presets::UTF8_FULL) .set_content_arrangement(ContentArrangement::Dynamic) .set_width(100) .set_header(header) .add_rows(rows) .set_constraints(vec![ ColumnConstraint::LowerBoundary(Width::Fixed(12)), ColumnConstraint::LowerBoundary(Width::Fixed(5)), ColumnConstraint::LowerBoundary(Width::Fixed(10)), ]); println!("{table}"); let expected = " โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ a really long col โ”† b โ”† this is 10 โ”‚ โ”‚ --- โ”† --- โ”† --- โ”‚ โ”‚ i64 โ”† str โ”† i64 โ”‚ โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ก โ”‚ 1 โ”† โ”† 4 โ”‚ โ”œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ผโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ผโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ค โ”‚ 2 โ”† this is a string value that will... โ”† 5 โ”‚ โ”œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ผโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ผโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ค โ”‚ 3 โ”† null โ”† 6 โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜"; println!("{expected}"); assert_table_line_width(table, 72); assert_eq!(expected, "\n".to_string() + &table.to_string()); } comfy-table-7.1.4/tests/all/counts.rs000064400000000000000000000024471046102023000156410ustar 00000000000000use pretty_assertions::assert_eq; use comfy_table::*; #[test] fn test_col_count_header() { let mut table = Table::new(); table.set_header(vec!["Col 1", "Col 2", "Col 3"]); assert_eq!(table.column_count(), 3); table.set_header(vec!["Col 1", "Col 2", "Col 3", "Col 4"]); assert_eq!(table.column_count(), 4); table.set_header(vec!["Col I", "Col II"]); assert_eq!(table.column_count(), 4); } #[test] fn test_col_count_row() { let mut table = Table::new(); table.add_row(vec!["Foo", "Bar"]); assert_eq!(table.column_count(), 2); table.add_row(vec!["Bar", "Foo", "Baz"]); assert_eq!(table.column_count(), 3); } #[test] fn test_row_count() { let mut table = Table::new(); assert_eq!(table.row_count(), 0); table.add_row(vec!["Foo", "Bar"]); assert_eq!(table.row_count(), 1); table.add_row(vec!["Bar", "Foo", "Baz"]); assert_eq!(table.row_count(), 2); table.add_row_if(|_, _| false, vec!["Baz", "Bar", "Foo"]); assert_eq!(table.row_count(), 2); table.add_row_if(|_, _| true, vec!["Foo", "Baz", "Bar"]); assert_eq!(table.row_count(), 3); } #[test] fn test_is_empty() { let mut table = Table::new(); assert_eq!(table.is_empty(), true); table.add_row(vec!["Foo", "Bar"]); assert_eq!(table.is_empty(), false); } comfy-table-7.1.4/tests/all/custom_delimiter_test.rs000064400000000000000000000045431046102023000207340ustar 00000000000000use pretty_assertions::assert_eq; use comfy_table::*; #[test] /// Create a table with a custom delimiter on Table, Column and Cell level. /// The first column should be split with the table's delimiter. /// The first cell of the second column should be split with the custom column delimiter /// The second cell of the second column should be split with the custom cell delimiter fn full_custom_delimiters() { let mut table = Table::new(); table .set_header(vec!["Header1", "Header2"]) .set_content_arrangement(ContentArrangement::Dynamic) .set_delimiter('-') .set_width(40) .add_row(vec![ "This shouldn't be split with any logic, since there's no matching delimiter", "Test-Test-Test-Test-Test-This_should_only_be_splitted_by_underscore_and not by space or hyphens", ]); // Give the bottom right cell a special delimiter table.add_row(vec![ Cell::new("Test_Test_Test_Test_Test_This-should-only-be-splitted-by-hyphens-not by space or underscore",), Cell::new( "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.", ) .set_delimiter('/'), ]); let column = table.column_mut(1).unwrap(); column.set_delimiter('_'); println!("{table}"); let expected = " +-------------------+------------------+ | Header1 | Header2 | +======================================+ | This shouldn't be | Test-Test-Test-T | | split with any l | est-Test-This | | ogic, since there | should_only_be | | 's no matching de | splitted_by | | limiter | underscore_and n | | | ot by space or h | | | yphens | |-------------------+------------------| | Test_Test_Test_Te | Test-Test-Test-T | | st_Test_This | est-Test-Test_Te | | should-only-be | st_Test_Test_Tes | | splitted-by | t_Test_Test_This | | hyphens-not by sp | should/only/be | | ace or underscore | splitted/by | | | backspace/and no | | | t by space or hy | | | phens or anythin | | | g else. | +-------------------+------------------+"; println!("{expected}"); assert_eq!(expected, "\n".to_string() + &table.to_string()); } comfy-table-7.1.4/tests/all/edge_cases.rs000064400000000000000000000010621046102023000164000ustar 00000000000000use comfy_table::*; /// A gigantic table can generated, even if it's longer than the longest supported width. #[test] fn giant_table() { let mut table = Table::new(); table.set_header(["a".repeat(1_000_000)]); table.add_row(["a".repeat(1_000_000)]); table.to_string(); } /// No panic, even if there's a ridiculous amount of padding. #[test] fn max_padding() { let mut table = Table::new(); table.add_row(["test"]); let column = table.column_mut(0).unwrap(); column.set_padding((u16::MAX, u16::MAX)); table.to_string(); } comfy-table-7.1.4/tests/all/hidden_test.rs000064400000000000000000000101041046102023000166050ustar 00000000000000use comfy_table::*; use pretty_assertions::assert_eq; fn get_table() -> Table { let mut table = Table::new(); table .load_preset(presets::UTF8_FULL) .set_header(vec![ "hidden_header", "smol", "hidden_header", "Two_hidden_headers_in_a_row", "Header2", "Header3", "hidden_header", ]) .add_row(vec![ "start_hidden", "smol", "middle_hidden", "two_hidden_headers_in_a_row", "This is another text", "This is the third text", "end_hidden", ]) .add_row(vec![ "asdf", "smol", "asdf", "asdf", "Now\nadd some\nmulti line stuff", "This is awesome", "asdf", ]); // Hide the first, third and 6th column table .column_mut(0) .unwrap() .set_constraint(ColumnConstraint::Hidden); table .column_mut(2) .unwrap() .set_constraint(ColumnConstraint::Hidden); table .column_mut(3) .unwrap() .set_constraint(ColumnConstraint::Hidden); table .column_mut(6) .unwrap() .set_constraint(ColumnConstraint::Hidden); table } /// Make sure hidden columns won't be displayed #[test] fn hidden_columns() { let table = get_table(); println!("{table}"); let expected = " โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ smol โ”† Header2 โ”† Header3 โ”‚ โ•žโ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ก โ”‚ smol โ”† This is another text โ”† This is the third text โ”‚ โ”œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ผโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ผโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ค โ”‚ smol โ”† Now โ”† This is awesome โ”‚ โ”‚ โ”† add some โ”† โ”‚ โ”‚ โ”† multi line stuff โ”† โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜"; assert_eq!(expected, "\n".to_string() + &table.to_string()); } /// Make sure dynamic adjustment still works with hidden columns #[test] fn hidden_columns_with_dynamic_adjustment() { let mut table = get_table(); table.set_width(25); table.set_content_arrangement(ContentArrangement::Dynamic); println!("{table}"); let expected = " โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ smol โ”† Header โ”† Heade โ”‚ โ”‚ โ”† 2 โ”† r3 โ”‚ โ•žโ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•ก โ”‚ smol โ”† This โ”† This โ”‚ โ”‚ โ”† is ano โ”† is โ”‚ โ”‚ โ”† ther โ”† the โ”‚ โ”‚ โ”† text โ”† third โ”‚ โ”‚ โ”† โ”† text โ”‚ โ”œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ผโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ผโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ค โ”‚ smol โ”† Now โ”† This โ”‚ โ”‚ โ”† add โ”† is โ”‚ โ”‚ โ”† some โ”† aweso โ”‚ โ”‚ โ”† multi โ”† me โ”‚ โ”‚ โ”† line โ”† โ”‚ โ”‚ โ”† stuff โ”† โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜"; assert_eq!(expected, "\n".to_string() + &table.to_string()); } /// Nothing breaks, if all columns are hidden #[test] fn only_hidden_columns() { let mut table = get_table(); table.set_constraints(vec![ ColumnConstraint::Hidden, ColumnConstraint::Hidden, ColumnConstraint::Hidden, ColumnConstraint::Hidden, ColumnConstraint::Hidden, ColumnConstraint::Hidden, ]); println!("{table}"); let expected = " โ”Œโ” โ•žโ•ก โ”œโ”ค โ””โ”˜"; assert_eq!(expected, "\n".to_string() + &table.to_string()); } comfy-table-7.1.4/tests/all/inner_style_test.rs000064400000000000000000000161021046102023000177110ustar 00000000000000use comfy_table::{presets::UTF8_FULL, *}; use pretty_assertions::assert_eq; fn get_preset_table() -> Table { let mut table = Table::new(); table.load_preset(UTF8_FULL); table.set_content_arrangement(ContentArrangement::Dynamic); table.set_width(85); let mut row = Row::new(); row.add_cell(Cell::new(format!( "hello{}cell1", console::style("123\n456").dim().blue() ))); row.add_cell(Cell::new("cell2")); table.add_row(row); let mut row = Row::new(); row.add_cell(Cell::new( format!(r"cell sys-devices-pci00:00-0000:000:07:00.1-usb2-2\x2d1-2\x2d1.3-2\x2d1.3:1.0-host2-target2:0:0-2:0:0:1-block-sdb{}", console::style(".device").bold().red()) )); row.add_cell(Cell::new( "cell4 asdfasfsad asdfasdf sad fas df asdf as df asdf asdfasdfasdfasdfasdfasdfa dsfa sdf asdf asd f asdf as df sadf asd fas df " )); table.add_row(row); let mut row = Row::new(); row.add_cell(Cell::new("cell5")); row.add_cell(Cell::new("cell6")); table.add_row(row); table } #[test] fn styled_table() { console::set_colors_enabled(true); let mut table = get_preset_table(); table.force_no_tty().enforce_styling(); println!("{table}"); let expected = " โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ hello\u{1b}[34m\u{1b}[2m123\u{1b}[0m โ”† cell2 โ”‚ โ”‚ \u{1b}[34m\u{1b}[2m456\u{1b}[0mcell1 โ”† โ”‚ โ”œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ผโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ค โ”‚ cell sys-devices-pci00:00-0000:000:07:0 โ”† cell4 asdfasfsad asdfasdf sad fas df โ”‚ โ”‚ 0.1-usb2-2\\x2d1-2\\x2d1.3-2\\x2d1.3:1.0-h โ”† asdf as df asdf โ”‚ โ”‚ ost2-target2:0:0-2:0:0:1-block-sdb\u{1b}[31m\u{1b}[1m.devi\u{1b}[0m โ”† asdfasdfasdfasdfasdfasdfa dsfa sdf asdf โ”‚ โ”‚ \u{1b}[31m\u{1b}[1mce\u{1b}[0m โ”† asd f asdf as df sadf asd fas df โ”‚ โ”œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ผโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ค โ”‚ cell5 โ”† cell6 โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜"; assert_eq!(expected, "\n".to_string() + &table.to_string()); } #[test] fn no_style_styled_table() { console::set_colors_enabled(true); let mut table = get_preset_table(); table.force_no_tty(); println!("{table}"); let expected = " โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ hello\u{1b}[34m\u{1b}[2m123\u{1b}[0m โ”† cell2 โ”‚ โ”‚ \u{1b}[34m\u{1b}[2m456\u{1b}[0mcell1 โ”† โ”‚ โ”œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ผโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ค โ”‚ cell sys-devices-pci00:00-0000:000:07:0 โ”† cell4 asdfasfsad asdfasdf sad fas df โ”‚ โ”‚ 0.1-usb2-2\\x2d1-2\\x2d1.3-2\\x2d1.3:1.0-h โ”† asdf as df asdf โ”‚ โ”‚ ost2-target2:0:0-2:0:0:1-block-sdb\u{1b}[31m\u{1b}[1m.devi\u{1b}[0m โ”† asdfasdfasdfasdfasdfasdfa dsfa sdf asdf โ”‚ โ”‚ \u{1b}[31m\u{1b}[1mce\u{1b}[0m โ”† asd f asdf as df sadf asd fas df โ”‚ โ”œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ผโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ค โ”‚ cell5 โ”† cell6 โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜"; assert_eq!(expected, "\n".to_string() + &table.to_string()); } #[test] fn styled_text_only_table() { console::set_colors_enabled(true); let mut table = get_preset_table(); table.force_no_tty().enforce_styling().style_text_only(); println!("{table}"); let expected = " โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ hello\u{1b}[34m\u{1b}[2m123\u{1b}[0m โ”† cell2 โ”‚ โ”‚ \u{1b}[34m\u{1b}[2m456\u{1b}[0mcell1 โ”† โ”‚ โ”œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ผโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ค โ”‚ cell sys-devices-pci00:00-0000:000:07:0 โ”† cell4 asdfasfsad asdfasdf sad fas df โ”‚ โ”‚ 0.1-usb2-2\\x2d1-2\\x2d1.3-2\\x2d1.3:1.0-h โ”† asdf as df asdf โ”‚ โ”‚ ost2-target2:0:0-2:0:0:1-block-sdb\u{1b}[31m\u{1b}[1m.devi\u{1b}[0m โ”† asdfasdfasdfasdfasdfasdfa dsfa sdf asdf โ”‚ โ”‚ \u{1b}[31m\u{1b}[1mce\u{1b}[0m โ”† asd f asdf as df sadf asd fas df โ”‚ โ”œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ผโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ค โ”‚ cell5 โ”† cell6 โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜"; assert_eq!(expected, "\n".to_string() + &table.to_string()); } comfy-table-7.1.4/tests/all/mod.rs000064400000000000000000000012001046102023000150670ustar 00000000000000use comfy_table::Table; use unicode_width::UnicodeWidthStr; mod add_predicate; mod alignment_test; #[cfg(feature = "tty")] mod combined_test; mod constraints_test; mod content_arrangement_test; mod counts; mod custom_delimiter_test; mod edge_cases; mod hidden_test; #[cfg(feature = "custom_styling")] mod inner_style_test; mod modifiers_test; mod padding_test; mod presets_test; mod property_test; mod simple_test; #[cfg(feature = "tty")] mod styling_test; mod truncation; mod utf_8_characters; pub fn assert_table_line_width(table: &Table, count: usize) { for line in table.lines() { assert_eq!(line.width(), count); } } comfy-table-7.1.4/tests/all/modifiers_test.rs000064400000000000000000000022421046102023000173370ustar 00000000000000use pretty_assertions::assert_eq; use comfy_table::modifiers::*; use comfy_table::presets::*; use comfy_table::*; fn get_preset_table() -> Table { let mut table = Table::new(); table .set_header(vec!["Header1", "Header2", "Header3"]) .add_row(vec!["One One", "One Two", "One Three"]) .add_row(vec!["One One", "One Two", "One Three"]); table } #[test] fn utf8_round_corners() { let mut table = get_preset_table(); table .load_preset(UTF8_FULL) .apply_modifier(UTF8_ROUND_CORNERS); let expected = " โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ โ”‚ Header1 โ”† Header2 โ”† Header3 โ”‚ โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ก โ”‚ One One โ”† One Two โ”† One Three โ”‚ โ”œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ผโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ผโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ค โ”‚ One One โ”† One Two โ”† One Three โ”‚ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ"; println!("{table}"); assert_eq!(expected, "\n".to_string() + &table.to_string()); } comfy-table-7.1.4/tests/all/padding_test.rs000064400000000000000000000023001046102023000167570ustar 00000000000000use pretty_assertions::assert_eq; use comfy_table::*; #[test] /// Columns can set a custom padding. /// Ensure these settings are working. fn custom_padding() { let mut table = Table::new(); table .set_header(vec![ Cell::new("Header1"), Cell::new("Header2"), Cell::new("Header3"), ]) .add_row(vec!["One One", "One Two", "One Three"]) .add_row(vec!["Two One", "Two Two", "Two Three"]) .add_row(vec!["Three One", "Three Two", "Three Three"]); let column = table.column_mut(0).unwrap(); column.set_padding((5, 5)); let column = table.column_mut(2).unwrap(); column.set_padding((0, 0)); println!("{table}"); let expected = " +-------------------+-----------+-----------+ | Header1 | Header2 |Header3 | +===========================================+ | One One | One Two |One Three | |-------------------+-----------+-----------| | Two One | Two Two |Two Three | |-------------------+-----------+-----------| | Three One | Three Two |Three Three| +-------------------+-----------+-----------+"; assert_eq!(expected, "\n".to_string() + &table.to_string()); } comfy-table-7.1.4/tests/all/presets_test.rs000064400000000000000000000116571046102023000170550ustar 00000000000000use pretty_assertions::assert_eq; use comfy_table::presets::*; use comfy_table::*; fn get_preset_table() -> Table { let mut table = Table::new(); table .set_header(vec!["Hello", "there"]) .add_row(vec!["a", "b"]) .add_row(vec!["c", "d"]); table } #[test] fn test_ascii_full() { let mut table = get_preset_table(); table.load_preset(ASCII_FULL); println!("{table}"); let expected = " +-------+-------+ | Hello | there | +===============+ | a | b | |-------+-------| | c | d | +-------+-------+"; println!("{expected}"); assert_eq!(expected, "\n".to_string() + &table.to_string()); } #[test] fn test_ascii_full_condensed() { let mut table = get_preset_table(); table.load_preset(ASCII_FULL_CONDENSED); println!("{table}"); let expected = " +-------+-------+ | Hello | there | +===============+ | a | b | | c | d | +-------+-------+"; println!("{expected}"); assert_eq!(expected, "\n".to_string() + &table.trim_fmt()); } #[test] fn test_ascii_no_borders() { let mut table = get_preset_table(); table.load_preset(ASCII_NO_BORDERS); println!("{table}"); let expected = " Hello | there =============== a | b -------+------- c | d"; println!("{expected}"); assert_eq!(expected, "\n".to_string() + &table.trim_fmt()); } #[test] fn test_ascii_borders_only() { let mut table = get_preset_table(); table.load_preset(ASCII_BORDERS_ONLY); println!("{table}"); let expected = " +---------------+ | Hello there | +===============+ | a b | | | | c d | +---------------+"; println!("{expected}"); assert_eq!(expected, "\n".to_string() + &table.to_string()); } #[test] fn test_ascii_borders_only_condensed() { let mut table = get_preset_table(); table.load_preset(ASCII_BORDERS_ONLY_CONDENSED); println!("{table}"); let expected = " +---------------+ | Hello there | +===============+ | a b | | c d | +---------------+"; println!("{expected}"); assert_eq!(expected, "\n".to_string() + &table.to_string()); } #[test] fn test_ascii_horizontal_only() { let mut table = get_preset_table(); table.load_preset(ASCII_HORIZONTAL_ONLY); println!("{table}"); let expected = " --------------- Hello there =============== a b --------------- c d ---------------"; println!("{expected}"); assert_eq!(expected, "\n".to_string() + &table.trim_fmt()); } #[test] fn test_ascii_markdown() { let mut table = get_preset_table(); table.load_preset(ASCII_MARKDOWN); println!("{table}"); let expected = " | Hello | there | |-------|-------| | a | b | | c | d |"; println!("{expected}"); assert_eq!(expected, "\n".to_string() + &table.trim_fmt()); } #[test] fn test_utf8_full() { let mut table = get_preset_table(); table.load_preset(UTF8_FULL); println!("{table}"); let expected = " โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Hello โ”† there โ”‚ โ•žโ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•ก โ”‚ a โ”† b โ”‚ โ”œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ผโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ค โ”‚ c โ”† d โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜"; println!("{expected}"); assert_eq!(expected, "\n".to_string() + &table.trim_fmt()); } #[test] fn test_utf8_full_condensed() { let mut table = get_preset_table(); table.load_preset(UTF8_FULL_CONDENSED); println!("{table}"); let expected = " โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Hello โ”† there โ”‚ โ•žโ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•ก โ”‚ a โ”† b โ”‚ โ”‚ c โ”† d โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜"; println!("{expected}"); assert_eq!(expected, "\n".to_string() + &table.trim_fmt()); } #[test] fn test_utf8_no_borders() { let mut table = get_preset_table(); table.load_preset(UTF8_NO_BORDERS); println!("{table}"); let expected = " Hello โ”† there โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ• a โ”† b โ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ผโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œ c โ”† d"; println!("{expected}"); assert_eq!(expected, "\n".to_string() + &table.trim_fmt()); } #[test] fn test_utf8_horizontal_only() { let mut table = get_preset_table(); table.load_preset(UTF8_HORIZONTAL_ONLY); println!("{table}"); let expected = " โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Hello there โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• a b โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ c d โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€"; println!("{expected}"); assert_eq!(expected, "\n".to_string() + &table.trim_fmt()); } #[test] fn test_nothing() { let mut table = get_preset_table(); table.load_preset(NOTHING); println!("{table}"); let expected = " Hello there a b c d"; println!("{expected}"); assert_eq!(expected, "\n".to_string() + &table.trim_fmt()); } comfy-table-7.1.4/tests/all/property_test.proptest-regressions000064400000000000000000000346111046102023000230440ustar 00000000000000# Seeds for failure cases proptest has generated in the past. It is # automatically read and these particular cases re-run before any # novel cases are generated. # # It is recommended to check this file in to source control so that # everyone who runs the test benefits from these saved cases. 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 } 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} = formatted.split_terminator('\n').map(|line| line.to_owned()).collect(); let mut line_iter = lines.iter(); // ----- Table width check ------ // Get the length of the very first line. // We're lateron going to ensure, that all lines have the same length. let line_length = if let Some(line) = line_iter.next() { line.trim().len() } else { 0 }; // Make sure all lines have the same length for line in line_iter { if line.len() != line_length { return build_error(&formatted, "Each line of a printed table has to have the same length!"); } } // Make sure that the table is within its width, if arrangement isn't enabled. // This is a bit tricky. // A table can be larger than the specified width, if the user forces it to be larger. #[cfg(feature = "integration_test")] { let current_arrangement = table.content_arrangement(); match current_arrangement { ContentArrangement::Disabled => (), _ => { let expected_max = determine_max_table_width(&table); // A line can be a bit longer than u16::MAX due to formatting and borders. let actual: u16 = line_length.try_into().unwrap_or(u16::MAX); if actual > expected_max { return build_error( &formatted, &format!("Expected table to be smaller than line length!\n\ Actual: {actual}, Expected max: {expected_max}\n\ Arrangement: {current_arrangement:?}" )); } } } } #[cfg(feature = "integration_test")] // Only run this test, if the `integration_test` is enabled. // Without this flag, we don't have access to some util functions in comfy_table, that // aren't exposed by default. enforce_constraints(&table, formatted, lines)? } } fn build_error(table: &str, context: &str) -> Result<(), TestCaseError> { Err(TestCaseError::Fail( format!("\n{context}:\n{table}\n").into(), )) } /// The user can actually force a table to be longer than the specified `table.width()` /// by specifying [ColumnConstraint]s. #[cfg(feature = "integration_test")] fn determine_max_table_width(table: &Table) -> u16 { use comfy_table::utils::arrangement::helper::count_border_columns; let table_width = table.width().unwrap(); // The max value that will be enforced by constraints. // We start with `2` for the side borders. let visible_columns = table .column_iter() .filter(|column| !column.is_hidden()) .count(); // Initialize the value for the min width enforced by constraints. // Borders may exist, but they are not included in constraints, which is why we have to // explicitly add them. let mut constraint_min_width: u16 = count_border_columns(table, visible_columns) .try_into() .unwrap_or(u16::MAX); // Get the max content widths for each column. // This is necessary for the `ContentWidth` constraint. let max_content_widths = table.column_max_content_widths(); // Calculate the enforced widths by any constraints. for (index, column) in table.column_iter().enumerate() { if let Some(constraint) = column.constraint() { match constraint { ColumnConstraint::ContentWidth => { constraint_min_width = constraint_min_width .saturating_add(max_content_widths[index]) .saturating_add(column.padding_width()); } ColumnConstraint::Absolute(width) => { constraint_min_width = constraint_min_width .saturating_add(absolute_width(table, width)) .saturating_add(column.padding_width()); } ColumnConstraint::LowerBoundary(width) | ColumnConstraint::Boundaries { lower: width, .. } => { constraint_min_width = constraint_min_width .saturating_add(absolute_width(table, width)) .saturating_add(column.padding_width()); } ColumnConstraint::Hidden => {} _ => { // Add the padding and the min-width of `1` for this column constraint_min_width = constraint_min_width .saturating_add(column.padding_width()) .saturating_add(1); } } } else { // Add the padding + 1 space for all columns without constraints. constraint_min_width = constraint_min_width .saturating_add(column.padding_width()) .saturating_add(1); } } std::cmp::max(table_width, constraint_min_width) } /// Enforce that Column constraints are enforced as expected in `Dynamic` mode. #[cfg(feature = "integration_test")] fn enforce_constraints( table: &Table, formatted: String, lines: Vec, ) -> Result<(), TestCaseError> { let content_arrangement = table.content_arrangement(); // Don't run the following for disabled or full-width arrangement. // These constraints kind of mess with all kinds of assertions we can make, which is why we // skip them. match content_arrangement { ContentArrangement::Dynamic => (), _ => return Ok(()), } // Extract the constraints for each table // Also remove hidden columns let constraints: Vec> = table .column_iter() .map(|col| col.constraint().cloned()) .filter(|constraint| !matches!(constraint, Some(ColumnConstraint::Hidden))) .collect(); let line_iter = lines.iter(); for line in line_iter { // Split the line along the column delimiter. // This allows us to ensure that each column is inside its constraints. let line_parts: Vec = line.split('|').map(|col| col.to_string()).collect(); // Skip the line if there're fewer vertical delimiters than columns + borders. // If that's the case, we're currently looking at a border or a delimiter line. if line_parts.len() < (constraints.len() + 2) { continue; } // The left and right borders will produce empty strings, let's filter those out. let line_parts: Vec = line_parts .into_iter() .filter(|part| !part.is_empty()) .collect(); for (index, (part, constraint)) in line_parts.iter().zip(constraints.iter()).enumerate() { let constraint = match constraint { Some(constraint) => constraint, // No constraint, we're good to go. None => continue, }; // Get the actual length of the part. let actual = part.len(); match constraint { ColumnConstraint::Hidden => panic!("This shouldn't happen"), // No need to check, if the column can be as wide as the content. ColumnConstraint::ContentWidth => continue, // Absolute width is defined. ColumnConstraint::Absolute(absolute) => { let mut expected = absolute_width(table, absolute); // The minimal amount of chars per column (with default padding) // is 3 chars. 2 padding + 1 char content. if expected < 3 { expected = 3; } if actual != expected.into() { return build_error( &formatted, &format!( "Column {index} for should have absolute width of {expected}.\n\ Actual width is {actual}.\n\ {absolute:?} for line '{line}', part '{part}'" ), ); } } ColumnConstraint::LowerBoundary(lower) => { let expected_lower = absolute_width(table, lower); if actual < expected_lower.into() { return build_error( &formatted, &format!( "Column {index} has a lower bound of {expected_lower}.\n\ Actual width is {actual}.\n\ {lower:?} for line '{line}', part '{part}'" ), ); } } ColumnConstraint::UpperBoundary(upper) => { let mut expected_upper = absolute_width(table, upper); // The minimal amount of chars per column (with default padding) // is 3 chars. 2 padding + 1 char content. if expected_upper < 3 { expected_upper = 3; } if actual > expected_upper.into() { return build_error( &formatted, &format!( "Column {index} has a upper bound of {expected_upper}.\n\ Actual width is {actual}.\n\ {upper:?} for line '{line}', part '{part}'" ), ); } } ColumnConstraint::Boundaries { lower, upper } => { let expected_lower = absolute_width(table, lower); let mut expected_upper = absolute_width(table, upper); // The minimal amount of chars per column (with default padding) // is 3 chars. 2 padding + 1 char content. if expected_upper < 3 { expected_upper = 3; } if actual < expected_lower.into() { return build_error( &formatted, &format!( "Column {index} has a lower bound of {expected_lower}.\n\ Actual width is {actual}.\n\ {lower:?} for line '{line}', part '{part}'" ), ); } if actual > expected_upper.into() { return build_error( &formatted, &format!( "Column {index} has a upper bound of {expected_upper}.\n\ Actual width is {actual}.\n\ {upper:?} for line '{line}', part '{part}'" ), ); } } } } } Ok(()) } /// Resolve an absolute value from a given boundary #[cfg(feature = "integration_test")] pub fn absolute_width(table: &Table, width: &Width) -> u16 { use comfy_table::utils::arrangement::constraint::absolute_value_from_width; let visible_columns = table .column_iter() .filter(|column| !column.is_hidden()) .count(); let computed_width = absolute_value_from_width(table, width, visible_columns) .expect("Expected table to have a width"); std::cmp::max(1, computed_width) } comfy-table-7.1.4/tests/all/simple_test.rs000064400000000000000000000062761046102023000166620ustar 00000000000000use pretty_assertions::assert_eq; use comfy_table::*; #[test] fn simple_table() { let mut table = Table::new(); table .set_header(vec!["Header1", "Header2", "Header3"]) .add_row(vec![ "This is a text", "This is another text", "This is the third text", ]) .add_row(vec![ "This is another text", "Now\nadd some\nmulti line stuff", "This is awesome", ]); println!("{table}"); let expected = " +----------------------+----------------------+------------------------+ | Header1 | Header2 | Header3 | +======================================================================+ | This is a text | This is another text | This is the third text | |----------------------+----------------------+------------------------| | This is another text | Now | This is awesome | | | add some | | | | multi line stuff | | +----------------------+----------------------+------------------------+"; assert_eq!(expected, "\n".to_string() + &table.to_string()); } #[test] fn missing_column_table() { let mut table = Table::new(); table .set_header(vec!["Header1", "Header2", "Header3"]) .add_row(vec!["One One", "One Two", "One Three"]) .add_row(vec!["Two One", "Two Two"]) .add_row(vec!["Three One"]); println!("{table}"); let expected = " +-----------+---------+-----------+ | Header1 | Header2 | Header3 | +=================================+ | One One | One Two | One Three | |-----------+---------+-----------| | Two One | Two Two | | |-----------+---------+-----------| | Three One | | | +-----------+---------+-----------+"; assert_eq!(expected, "\n".to_string() + &table.to_string()); } #[test] fn single_column_table() { let mut table = Table::new(); table .set_header(vec!["Header1"]) .add_row(vec!["One One"]) .add_row(vec!["Two One"]) .add_row(vec!["Three One"]); println!("{table}"); let expected = " +-----------+ | Header1 | +===========+ | One One | |-----------| | Two One | |-----------| | Three One | +-----------+"; assert_eq!(expected, "\n".to_string() + &table.to_string()); } #[test] fn lines() { let mut t = Table::new(); t.set_header(["heading 1", "heading 2", "heading 3"]); t.add_row(["test 1,1", "test 1,2", "test 1,3"]); t.add_row(["test 2,1", "test 2,2", "test 2,3"]); t.add_row(["test 3,1", "test 3,2", "test 3,3"]); let actual = t.lines(); let expected = &[ "+-----------+-----------+-----------+", "| heading 1 | heading 2 | heading 3 |", "+===================================+", "| test 1,1 | test 1,2 | test 1,3 |", "|-----------+-----------+-----------|", "| test 2,1 | test 2,2 | test 2,3 |", "|-----------+-----------+-----------|", "| test 3,1 | test 3,2 | test 3,3 |", "+-----------+-----------+-----------+", ]; assert_eq!(actual.collect::>(), expected); } comfy-table-7.1.4/tests/all/styling_test.rs000064400000000000000000000146721046102023000170610ustar 00000000000000use pretty_assertions::assert_eq; use comfy_table::presets::UTF8_FULL; use comfy_table::*; fn get_preset_table() -> Table { let mut table = Table::new(); table .load_preset(UTF8_FULL) .set_header(vec![ Cell::new("Header1").add_attribute(Attribute::Bold), Cell::new("Header2").fg(Color::Green), Cell::new("Header3").bg(Color::Black), ]) .add_row(vec![ Cell::new("This is a bold text").add_attribute(Attribute::Bold), Cell::new("This is a green text").fg(Color::Green), Cell::new("This one has black background").bg(Color::Black), ]) .add_row(vec![ Cell::new("Blinking boiii").add_attribute(Attribute::SlowBlink), Cell::new("Now\nadd some\nmulti line stuff") .fg(Color::Cyan) .add_attribute(Attribute::Underlined), Cell::new("COMBINE ALL THE THINGS") .fg(Color::Green) .bg(Color::Black) .add_attribute(Attribute::Bold) .add_attribute(Attribute::SlowBlink), ]); table } #[test] fn styled_table() { let mut table = get_preset_table(); table.force_no_tty().enforce_styling(); println!("{table}"); let expected = " โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚\u{1b}[1m Header1 \u{1b}[0mโ”†\u{1b}[38;5;10m Header2 \u{1b}[39mโ”†\u{1b}[48;5;0m Header3 \u{1b}[49mโ”‚ โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ก โ”‚\u{1b}[1m This is a bold text \u{1b}[0mโ”†\u{1b}[38;5;10m This is a green text \u{1b}[39mโ”†\u{1b}[48;5;0m This one has black background \u{1b}[49mโ”‚ โ”œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ผโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ผโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ค โ”‚\u{1b}[5m Blinking boiii \u{1b}[0mโ”†\u{1b}[38;5;14m\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โ”‚ โ”‚ โ”†\u{1b}[38;5;14m\u{1b}[4m add some \u{1b}[0mโ”† โ”‚ โ”‚ โ”†\u{1b}[38;5;14m\u{1b}[4m multi line stuff \u{1b}[0mโ”† โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜"; assert_eq!(expected, "\n".to_string() + &table.to_string()); } #[test] fn no_style_styled_table() { let mut table = get_preset_table(); table.force_no_tty(); println!("{table}"); let expected = " โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Header1 โ”† Header2 โ”† Header3 โ”‚ โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ก โ”‚ This is a bold text โ”† This is a green text โ”† This one has black background โ”‚ โ”œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ผโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ผโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ค โ”‚ Blinking boiii โ”† Now โ”† COMBINE ALL THE THINGS โ”‚ โ”‚ โ”† add some โ”† โ”‚ โ”‚ โ”† multi line stuff โ”† โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜"; assert_eq!(expected, "\n".to_string() + &table.to_string()); } #[test] fn styled_text_only_table() { let mut table = get_preset_table(); table.force_no_tty().enforce_styling().style_text_only(); println!("{table}"); let expected = " โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ \u{1b}[1mHeader1\u{1b}[0m โ”† \u{1b}[38;5;10mHeader2\u{1b}[39m โ”† \u{1b}[48;5;0mHeader3\u{1b}[49m โ”‚ โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ก โ”‚ \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 โ”‚ โ”œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ผโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ผโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ•Œโ”ค โ”‚ \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 โ”‚ โ”‚ โ”† \u{1b}[38;5;14m\u{1b}[4madd some\u{1b}[0m โ”† โ”‚ โ”‚ โ”† \u{1b}[38;5;14m\u{1b}[4mmulti line stuff\u{1b}[0m โ”† โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜"; assert_eq!(expected, "\n".to_string() + &table.to_string()); } comfy-table-7.1.4/tests/all/truncation.rs000064400000000000000000000142621046102023000165120ustar 00000000000000use pretty_assertions::assert_eq; use comfy_table::ColumnConstraint::*; use comfy_table::Width::*; use comfy_table::{ContentArrangement, Row, Table}; use crate::all::assert_table_line_width; /// Individual rows can be configured to have a max height. /// Everything beyond that line height should be truncated. #[test] fn table_with_truncate() { let mut table = Table::new(); let mut first_row: Row = Row::from(vec![ "This is a very long line with a lot of text", "This is anotherverylongtextwithlongwords text", "smol", ]); first_row.max_height(4); let mut second_row = Row::from(vec![ "Now let's\nadd a really long line in the middle of the cell \n and add more multi line stuff", "This is another text", "smol", ]); second_row.max_height(4); table .set_header(vec!["Header1", "Header2", "Head"]) .set_content_arrangement(ContentArrangement::Dynamic) .set_width(35) .add_row(first_row) .add_row(second_row); // The first column will be wider than 6 chars. // The second column's content is wider than 6 chars. There should be a '...'. let second_column = table.column_mut(1).unwrap(); second_column.set_constraint(Absolute(Fixed(8))); // The third column's content is less than 6 chars width. There shouldn't be a '...'. let third_column = table.column_mut(2).unwrap(); third_column.set_constraint(Absolute(Fixed(7))); println!("{table}"); let expected = " +----------------+--------+-------+ | Header1 | Header | Head | | | 2 | | +=================================+ | This is a very | This | smol | | long line with | is ano | | | a lot of text | therve | | | | ryl... | | |----------------+--------+-------| | Now let's | This | smol | | add a really | is ano | | | long line in | ther | | | the middle ... | text | | +----------------+--------+-------+"; println!("{expected}"); assert_table_line_width(&table, 35); assert_eq!(expected, "\n".to_string() + &table.to_string()); } #[test] fn table_with_truncate_indicator() { let mut table = Table::new(); let mut first_row: Row = Row::from(vec![ "This is a very long line with a lot of text", "This is anotherverylongtextwithlongwords text", "smol", ]); first_row.max_height(4); let mut second_row = Row::from(vec![ "Now let's\nadd a really long line in the middle of the cell \n and add more multi line stuff", "This is another text", "smol", ]); second_row.max_height(4); table .set_header(vec!["Header1", "Header2", "Head"]) .set_content_arrangement(ContentArrangement::Dynamic) .set_truncation_indicator("โ€ฆ") .set_width(35) .add_row(first_row) .add_row(second_row); // The first column will be wider than 6 chars. // The second column's content is wider than 6 chars. There should be a 'โ€ฆ'. let second_column = table.column_mut(1).unwrap(); second_column.set_constraint(Absolute(Fixed(8))); // The third column's content is less than 6 chars width. There shouldn't be a 'โ€ฆ'. let third_column = table.column_mut(2).unwrap(); third_column.set_constraint(Absolute(Fixed(7))); println!("{table}"); let expected = " +----------------+--------+-------+ | Header1 | Header | Head | | | 2 | | +=================================+ | This is a very | This | smol | | long line with | is ano | | | a lot of text | therve | | | | rylonโ€ฆ | | |----------------+--------+-------| | Now let's | This | smol | | add a really | is ano | | | long line in | ther | | | the middle ofโ€ฆ | text | | +----------------+--------+-------+"; println!("{expected}"); assert_table_line_width(&table, 35); assert_eq!(expected, "\n".to_string() + &table.to_string()); } #[test] fn table_with_composite_utf8_strings() { let mut table = Table::new(); table .set_header(vec!["Header1"]) .set_width(20) .add_row(vec!["ใ‚ใ„ใ†ใˆใŠใ‹ใใใ‘ใ“ใ•ใ—ใ™ใ›ใใŸใกใคใฆใจ"]) .set_content_arrangement(comfy_table::ContentArrangement::Dynamic); for row in table.row_iter_mut() { row.max_height(1); // 2 -> also panics, 3 -> ok } println!("{table}"); let expected = " +------------------+ | Header1 | +==================+ | ใ‚ใ„ใ†ใˆใŠใ‹... | +------------------+"; println!("{expected}"); assert_table_line_width(&table, 20); assert_eq!(expected, "\n".to_string() + &table.to_string()); } #[test] fn table_with_composite_utf8_strings_2_lines() { let mut table = Table::new(); table .set_header(vec!["Header1"]) .set_width(20) .add_row(vec!["ใ‚ใ„ใ†ใˆใŠใ‹ใใใ‘ใ“ใ•ใ—ใ™ใ›ใใŸใกใคใฆใจ"]) .set_content_arrangement(comfy_table::ContentArrangement::Dynamic); for row in table.row_iter_mut() { row.max_height(2); } println!("{table}"); let expected = " +------------------+ | Header1 | +==================+ | ใ‚ใ„ใ†ใˆใŠใ‹ใใ | | ใ‘ใ“ใ•ใ—ใ™ใ›... | +------------------+"; println!("{expected}"); assert_table_line_width(&table, 20); assert_eq!(expected, "\n".to_string() + &table.to_string()); } #[test] fn table_with_composite_utf8_emojis() { let mut table = Table::new(); table .set_header(vec!["Header1"]) .set_width(15) .add_row(vec![ "๐Ÿ™‚โ€โ†•๏ธ๐Ÿ™‚โ€โ†•๏ธ๐Ÿ™‚โ€โ†•๏ธ๐Ÿ™‚โ€โ†•๏ธ๐Ÿ™‚โ€โ†•๏ธ๐Ÿ™‚โ€โ†•๏ธlast_line.into_bytes().truncate(truncate_at)", ]) .set_content_arrangement(comfy_table::ContentArrangement::Dynamic); for row in table.row_iter_mut() { row.max_height(1); // 2 -> also panics, 3 -> ok } println!("{table}"); let expected = " +-------------+ | Header1 | +=============+ | ๐Ÿ™‚โ€โ†•๏ธ๐Ÿ™‚โ€โ†•๏ธ๐Ÿ™‚โ€โ†•๏ธ๐Ÿ™‚โ€โ†•๏ธ... | +-------------+"; println!("{expected}"); assert_table_line_width(&table, 15); assert_eq!(expected, "\n".to_string() + &table.to_string()); } comfy-table-7.1.4/tests/all/utf_8_characters.rs000064400000000000000000000063541046102023000175530ustar 00000000000000use pretty_assertions::assert_eq; use comfy_table::*; #[test] /// UTF-8 symbols that are longer than a single character are properly handled. /// This means, that comfy-table detects that they're longer than 1 character and styles/arranges /// the table accordingly. fn multi_character_utf8_symbols() { let mut table = Table::new(); table .set_header(vec!["Header1", "Header2", "Header3"]) .add_row(vec![ "This is a text", "This is another text", "This is the third text", ]) .add_row(vec![ "This is another text", "Now\nadd some\nmulti line stuff", "โœ…", ]); println!("{table}"); let expected = " +----------------------+----------------------+------------------------+ | Header1 | Header2 | Header3 | +======================================================================+ | This is a text | This is another text | This is the third text | |----------------------+----------------------+------------------------| | This is another text | Now | โœ… | | | add some | | | | multi line stuff | | +----------------------+----------------------+------------------------+"; assert_eq!(expected, "\n".to_string() + &table.to_string()); } #[test] fn multi_character_utf8_word_splitting() { let mut table = Table::new(); table .set_width(8) .set_content_arrangement(ContentArrangement::Dynamic) .set_header(vec!["test"]) .add_row(vec!["abcโœ…def"]); println!("{table}"); let expected = " +------+ | test | +======+ | abc | | โœ…de | | f | +------+"; println!("{expected}"); assert_eq!(expected, "\n".to_string() + &table.to_string()); } #[test] fn multi_character_cjk_word_splitting() { let mut table = Table::new(); table .set_width(8) .set_content_arrangement(ContentArrangement::Dynamic) .set_header(vec!["test"]) .add_row(vec!["abcๆ–ฐๅนดๅฟซไนedf"]); println!("{table}"); let expected = " +------+ | test | +======+ | abc | | ๆ–ฐๅนด | | ๅฟซไน | | edf | +------+"; println!("{expected}"); assert_eq!(expected, "\n".to_string() + &table.to_string()); } /// Handle emojis that'd joined via the "zero-width joiner" character U+200D and contain variant /// selectors. /// /// Those composite emojis should be handled as a single grapheme and thereby have their width /// calculated based on the grapheme length instead of the individual chars. /// /// This is also a regression test, as previously emojis were split in the middle of the joiner /// sequence, resulting in two different emojis on different lines. #[test] fn zwj_utf8_word_splitting() { let mut table = Table::new(); table .set_width(8) .set_content_arrangement(ContentArrangement::Dynamic) .set_header(vec!["test"]) .add_row(vec!["ab๐Ÿ™‚โ€โ†•๏ธdef"]); println!("{table}"); let expected = " +------+ | test | +======+ | ab๐Ÿ™‚โ€โ†•๏ธ | | def | +------+"; println!("{expected}"); assert_eq!(expected, "\n".to_string() + &table.to_string()); } comfy-table-7.1.4/tests/all_tests.rs000064400000000000000000000002151046102023000155370ustar 00000000000000/// This module simply imports all tests. /// That way, they're processed faster, which is nice as proptesting takes quite a while. mod all;