Skip to content

Commit 7b32d22

Browse files
committed
Support ANSI-escaped color codes and hyperlinks.
1 parent cf7dca3 commit 7b32d22

File tree

3 files changed

+31
-20
lines changed

3 files changed

+31
-20
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,5 @@ term = "0.6"
4040
lazy_static = "1"
4141
atty = "0.2"
4242
encode_unicode = "0.3"
43+
regex = "1"
4344
csv = { version = "1", optional = true }

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,14 @@ Uppercase letters stand for **bright** counterparts of the above colors:
241241
* **B** : Bright Blue
242242
* ... and so on ...
243243

244+
## ANSI hyperlinks
245+
246+
In most modern terminal emulators, it is possible to embed hyperlinks using ANSI escape codes. The following string field would display as a clickable link:
247+
248+
```rust
249+
"\u{1b}]8;;http://example.com\u{1b}\\example.com\u{1b}]8;;\u{1b}\\"
250+
```
251+
244252
## Slicing
245253

246254
Tables can be sliced into immutable borrowed subtables.

src/utils.rs

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::fmt;
33
use std::io::{Error, ErrorKind, Write};
44
use std::str;
55

6+
use regex::Regex;
67
use unicode_width::UnicodeWidthStr;
78

89
use super::format::Alignment;
@@ -77,30 +78,20 @@ pub fn print_align<T: Write + ?Sized>(out: &mut T,
7778
}
7879

7980
/// Return the display width of a unicode string.
80-
/// This functions takes ANSI-escaped color codes into account.
81+
/// This functions takes ANSI-escaped color codes and hyperlinks into account.
8182
pub fn display_width(text: &str) -> usize {
8283
let width = UnicodeWidthStr::width(text);
83-
let mut state = 0;
8484
let mut hidden = 0;
8585

86-
for c in text.chars() {
87-
state = match (state, c) {
88-
(0, '\u{1b}') => 1,
89-
(1, '[') => 2,
90-
(1, _) => 0,
91-
(2, 'm') => 3,
92-
_ => state,
93-
};
94-
95-
// We don't count escape characters as hidden as
96-
// UnicodeWidthStr::width already considers them.
97-
if state > 1 {
98-
hidden += 1;
99-
}
100-
101-
if state == 3 {
102-
state = 0;
103-
}
86+
lazy_static! {
87+
static ref COLOR_RE: Regex = Regex::new(r"\u{1b}(?P<colors>\[[^m]+?)m").unwrap();
88+
static ref HYPERLINK_RE: Regex = Regex::new(r"\u{1b}]8;;(?P<url>[^\u{1b}]+?)\u{1b}\\(?P<text>[^\u{1b}]+?)\u{1b}]8;;\u{1b}\\").unwrap();
89+
}
90+
for caps in COLOR_RE.captures_iter(text) {
91+
hidden += UnicodeWidthStr::width(&caps["colors"])
92+
}
93+
for caps in HYPERLINK_RE.captures_iter(text) {
94+
hidden += 10 + UnicodeWidthStr::width(&caps["url"])
10495
}
10596

10697
width - hidden
@@ -197,6 +188,17 @@ mod tests {
197188
assert_eq!(out.as_string(), "foo");
198189
}
199190

191+
#[test]
192+
fn ansi_escapes() {
193+
let mut out = StringWriter::new();
194+
print_align(&mut out, Alignment::LEFT, "\u{1b}[31;40mred\u{1b}[0m", ' ', 10, false).unwrap();
195+
assert_eq!(display_width(out.as_string()), 10);
196+
197+
let mut out = StringWriter::new();
198+
print_align(&mut out, Alignment::LEFT, "\u{1b}]8;;http://example.com\u{1b}\\example\u{1b}]8;;\u{1b}\\", ' ', 10, false).unwrap();
199+
assert_eq!(display_width(out.as_string()), 10);
200+
}
201+
200202
#[test]
201203
fn utf8_error() {
202204
let mut out = StringWriter::new();

0 commit comments

Comments
 (0)