initial commit

This commit is contained in:
Terts Diepraam
2023-11-08 13:29:45 +01:00
commit cebd413470
5 changed files with 224 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
/target
/Cargo.lock
+14
View File
@@ -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"
+21
View File
@@ -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.
+91
View File
@@ -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
View File
@@ -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
)
}
}