You've already forked ansi-width
mirror of
https://github.com/uutils/ansi-width.git
synced 2026-06-10 16:13:22 -07:00
initial commit
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
/target
|
||||
/Cargo.lock
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
[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"
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT 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.
|
||||
@@ -0,0 +1,91 @@
|
||||
# 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
|
||||
|
||||
- [`String::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):
|
||||
Similar functionality to this crate, except that it does not support ANSI
|
||||
hyperlinks. Another 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:
|
||||
|
||||
- <https://en.wikipedia.org/wiki/ANSI_escape_code>
|
||||
- <https://www.ecma-international.org/wp-content/uploads/ECMA-48_5th_edition_june_1991.pdf>
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
|
||||
/// Character that starts escape codes
|
||||
const ESC: char = '\x1b';
|
||||
|
||||
/// Calculate the width of a string.
|
||||
///
|
||||
/// The 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 == '\\' && 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
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user