ansi-width-0.1.0/.cargo_vcs_info.json0000644000000001360000000000100131010ustar { "git": { "sha1": "65caa0fd8794d47a2a97ead700e0ec7431acce9d" }, "path_in_vcs": "" }ansi-width-0.1.0/.github/workflows/ci.yml000064400000000000000000000121771046102023000164140ustar 00000000000000name: CI # Continuous Integration on: push: branches: - main pull_request: env: CARGO_TERM_COLOR: always jobs: test: name: Test Suite runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - run: cargo test --all-features --workspace rustfmt: name: Rustfmt runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable with: components: rustfmt - uses: Swatinem/rust-cache@v2 - name: Check formatting run: cargo fmt --all -- --check clippy: name: Clippy runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable with: components: clippy - uses: Swatinem/rust-cache@v2 - name: Clippy check run: cargo clippy --all-targets --all-features --workspace -- -D warnings docs: name: Docs runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - name: Check documentation env: RUSTDOCFLAGS: -D warnings run: cargo doc coverage: name: Code Coverage runs-on: ${{ matrix.job.os }} strategy: fail-fast: true matrix: job: - { os: ubuntu-latest , features: unix } - { os: macos-latest , features: macos } - { os: windows-latest , features: windows } steps: - uses: actions/checkout@v4 - name: Initialize workflow variables id: vars shell: bash run: | ## VARs setup outputs() { step_id="vars"; for var in "$@" ; do echo steps.${step_id}.outputs.${var}="${!var}"; echo "${var}=${!var}" >> $GITHUB_OUTPUT; done; } # toolchain TOOLCHAIN="nightly" ## default to "nightly" toolchain (required for certain required unstable compiler flags) ## !maint: refactor when stable channel has needed support # * specify gnu-type TOOLCHAIN for windows; `grcov` requires gnu-style code coverage data files case ${{ matrix.job.os }} in windows-*) TOOLCHAIN="$TOOLCHAIN-x86_64-pc-windows-gnu" ;; esac; # * use requested TOOLCHAIN if specified if [ -n "${{ matrix.job.toolchain }}" ]; then TOOLCHAIN="${{ matrix.job.toolchain }}" ; fi outputs TOOLCHAIN # target-specific options # * CARGO_FEATURES_OPTION CARGO_FEATURES_OPTION='--all -- --check' ; ## default to '--all-features' for code coverage # * CODECOV_FLAGS CODECOV_FLAGS=$( echo "${{ matrix.job.os }}" | sed 's/[^[:alnum:]]/_/g' ) outputs CODECOV_FLAGS - name: rust toolchain ~ install uses: dtolnay/rust-toolchain@nightly - name: Test run: cargo test ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} --no-fail-fast env: CARGO_INCREMENTAL: "0" RUSTC_WRAPPER: "" RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort" RUSTDOCFLAGS: "-Cpanic=abort" - name: "`grcov` ~ install" id: build_grcov shell: bash run: | git clone https://github.com/mozilla/grcov.git ~/grcov/ cd ~/grcov # Hardcode the version of crossbeam-epoch. See # https://github.com/uutils/coreutils/issues/3680 sed -i -e "s|tempfile =|crossbeam-epoch = \"=0.9.8\"\ntempfile =|" Cargo.toml cargo install --path . cd - # Uncomment when the upstream issue # https://github.com/mozilla/grcov/issues/849 is fixed # uses: actions-rs/install@v0.1 # with: # crate: grcov # version: latest # use-tool-cache: false - name: Generate coverage data (via `grcov`) id: coverage shell: bash run: | ## Generate coverage data COVERAGE_REPORT_DIR="target/debug" COVERAGE_REPORT_FILE="${COVERAGE_REPORT_DIR}/lcov.info" mkdir -p "${COVERAGE_REPORT_DIR}" # display coverage files grcov . --output-type files --ignore build.rs --ignore "vendor/*" --ignore "/*" --ignore "[a-zA-Z]:/*" --excl-br-line "^\s*((debug_)?assert(_eq|_ne)?!|#\[derive\()" | sort --unique # generate coverage report grcov . --output-type lcov --output-path "${COVERAGE_REPORT_FILE}" --branch --ignore build.rs --ignore "vendor/*" --ignore "/*" --ignore "[a-zA-Z]:/*" --excl-br-line "^\s*((debug_)?assert(_eq|_ne)?!|#\[derive\()" echo "name=report::${COVERAGE_REPORT_FILE}" >> $GITHUB_OUTPUT - name: Upload coverage results (to Codecov.io) uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} file: ${{ steps.coverage.outputs.report }} flags: ${{ steps.vars.outputs.CODECOV_FLAGS }} name: codecov-umbrella fail_ci_if_error: false ansi-width-0.1.0/.gitignore000064400000000000000000000000241046102023000136550ustar 00000000000000/target /Cargo.lock ansi-width-0.1.0/Cargo.toml0000644000000013550000000000100111030ustar # 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.70" name = "ansi-width" version = "0.1.0" authors = ["uutils developers"] description = "Calculate the width of a string when printed to the terminal" readme = "README.md" license = "MIT" [dependencies.unicode-width] version = "0.1.11" ansi-width-0.1.0/Cargo.toml.orig000064400000000000000000000005641046102023000145650ustar 00000000000000[package] name = "ansi-width" description = "Calculate the width of a string when printed to the terminal" license = "MIT" readme = "README.md" authors = ["uutils developers"] version = "0.1.0" edition = "2021" rust-version = "1.70" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] unicode-width = "0.1.11" ansi-width-0.1.0/LICENSE000064400000000000000000000020621046102023000126760ustar 00000000000000MIT License Copyright (c) 2023 uutils developers 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. ansi-width-0.1.0/README.md000064400000000000000000000075441046102023000131620ustar 00000000000000[![Crates.io](https://img.shields.io/crates/v/ansi-width.svg)](https://crates.io/crates/ansi-width) [![Discord](https://img.shields.io/badge/discord-join-7289DA.svg?logo=discord&longCache=true&style=flat)](https://discord.gg/wQVJbvJ) [![License](http://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/uutils/ansi-width/blob/main/LICENSE) [![dependency status](https://deps.rs/repo/github/uutils/ansi-width/status.svg)](https://deps.rs/repo/github/uutils/ansi-width) [![CodeCov](https://codecov.io/gh/uutils/ansi-width/branch/main/graph/badge.svg)](https://codecov.io/gh/uutils/ansi-width) # ANSI width Measure the width of a string when printed to the terminal For ASCII, this is identical to the length of the string in bytes. However, there are 2 special cases: - Many unicode characters (CJK, emoji, etc.) span multiple columns. - ANSI escape codes should be ignored. The first case is handled by the `unicode-width` crate. This function extends that crate by ignoring ANSI escape codes. ## Limitations - We cannot know the width of a `TAB` character in the terminal emulator. - Backspace is also treated as zero width. ## A Primer on ANSI escape codes (and how this crate works) ANSI codes are created using special character sequences in a string. These sequences start with the ESC character: `'\x1b'`, followed by some other character to determine the type of the escape code. That second character determines how long the sequence continues: - `ESC [`: until a character in the range `'\x40'..='\x7E'` is found. - `ESC ]`: until an `ST` is found. An `ST` is a String Terminator and is given by the sequence `ESC \` (or in Rust syntax `'\x1b\x5c'`). This is the subset of sequences that this library supports, since these are used by most applications that need this functionality. If you have a use case for other codes, please open an issue on the [GitHub repository](https://github.com/uutils/ansi-width). `ansi-width` does not parse the actual ANSI codes to improve performance, it can only skip the ANSI codes. ## Examples ```rust use ansi_width::ansi_width; // ASCII string assert_eq!(ansi_width("123456"), 6); // Accents assert_eq!(ansi_width("café"), 4); // Emoji (2 crab emoji) assert_eq!(ansi_width("🦀🦀"), 4); // CJK characters (“Nǐ hǎo” or “Hello” in Chinese) assert_eq!(ansi_width("你好"), 4); // ANSI colors assert_eq!(ansi_width("\u{1b}[31mRed\u{1b}[0m"), 3); // ANSI hyperlink assert_eq!( ansi_width("\x1b]8;;http://example.com\x1b\\This is a link\x1b]8;;\x1b\\"), 14 ); ``` ## Alternatives - [`str::len`](https://doc.rust-lang.org/std/primitive.str.html#method.len): Returns only the length in bytes and therefore only works for ASCII characters. - [`unicode-width`](https://crates.io/crates/unicode-width): Does not take ANSI characters into account by design (see [this issue](https://github.com/unicode-rs/unicode-width/issues/24)). This might be what you want if you don't care about ANSI codes. `unicode-width` is used internally by this crate as well. - [`textwrap::core::display_width`](https://docs.rs/textwrap/latest/textwrap/core/fn.display_width.html): Very similar functionality to this crate and it also supports hyperlinks since version 0.16.1. The advantage of this crate is that it does not require pulling in the rest of `textwrap`'s functionality (even though that functionality is excellent if you need it). - [`console::measure_text_width`](https://docs.rs/console/latest/console/fn.measure_text_width.html): Similar to `textwrap` and very well-tested. However, it constructs a new string internally without ANSI codes first and then measures the width of that. The parsing is more robust than this crate though. ## References The information above is based on: - - ansi-width-0.1.0/renovate.json000064400000000000000000000001531046102023000144060ustar 00000000000000{ "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "config:base" ] } ansi-width-0.1.0/src/lib.rs000064400000000000000000000063101046102023000135740ustar 00000000000000#![doc = include_str!("../README.md")] /// Character that starts escape codes const ESC: char = '\x1b'; /// Calculate the width of a string. /// /// See the [crate documentation](crate) for more information. pub fn ansi_width(s: &str) -> usize { let mut width = 0; let mut chars = s.chars(); // This lint is a false positive, because we use the iterator later, leading to // ownership issues if we follow the lint. #[allow(clippy::while_let_on_iterator)] while let Some(c) = chars.next() { // ESC starts escape sequences, so we need to take characters until the // end of the escape sequence. if c == ESC { let Some(c) = chars.next() else { break; }; match c { // String terminator character: ends other sequences // We probably won't encounter this but it's here for completeness. // Or for if we get passed invalid codes. '\\' => { // ignore } // Control Sequence Introducer: continue until `\x40-\x7C` '[' => while !matches!(chars.next(), Some('\x40'..='\x7C') | None) {}, // Operating System Command: continue until ST ']' => { let mut last = c; while let Some(new) = chars.next() { if new == '\x07' || (new == '\\' && last == ESC) { break; } last = new; } } // We don't know what character it is, best bet is to fall back to unicode width // The ESC is assumed to have 0 width in this case. _ => { width += unicode_width::UnicodeWidthChar::width(c).unwrap_or(0); } } } else { // If it's a normal character outside an escape sequence, use the // unicode width. width += unicode_width::UnicodeWidthChar::width(c).unwrap_or(0); } } width } #[cfg(test)] mod tests { use super::ansi_width; #[test] fn ascii() { assert_eq!(ansi_width(""), 0); assert_eq!(ansi_width("hello"), 5); assert_eq!(ansi_width("hello world"), 11); assert_eq!(ansi_width("WOW!"), 4); } #[test] fn c0_characters() { // Bell assert_eq!(ansi_width("\x07"), 0); // Backspace assert_eq!(ansi_width("\x08"), 0); // Tab assert_eq!(ansi_width("\t"), 0); } #[test] fn some_escape_codes() { // Simple assert_eq!(ansi_width("\u{1b}[34mHello\u{1b}[0m"), 5); // Red assert_eq!(ansi_width("\u{1b}[31mRed\u{1b}[0m"), 3); } #[test] fn hyperlink() { assert_eq!( ansi_width("\x1b]8;;http://example.com\x1b\\This is a link\x1b]8;;\x1b\\"), 14 ) } #[test] fn nonstandard_hyperlink() { // This hyperlink has a BEL character in the middle instead of `\x1b\\` assert_eq!( ansi_width("\x1b]8;;file://coreutils.md\x07coreutils.md\x1b]8;;\x07"), 12 ) } }