Skip to content

Commit 5c17616

Browse files
committed
Support ANSI-escaped color codes and hyperlinks.
1 parent fa70ca6 commit 5c17616

File tree

4 files changed

+32
-20
lines changed

4 files changed

+32
-20
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,5 @@ term = "^0.5"
3434
lazy_static = "1"
3535
atty = "^0.2"
3636
encode_unicode = "^0.3"
37+
regex = "1"
3738
csv = { version = "1", optional = true }

README.md

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

243+
## ANSI hyperlinks
244+
245+
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:
246+
247+
```rust
248+
"\u{1b}]8;;http://example.com\u{1b}\\example.com\u{1b}]8;;\u{1b}\\"
249+
```
250+
243251
## Slicing
244252

245253
Tables can be sliced into immutable borrowed subtables.

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ extern crate atty;
99
#[macro_use]
1010
extern crate lazy_static;
1111
extern crate encode_unicode;
12+
extern crate regex;
1213

1314
use std::io::{self, Write, Error};
1415
use std::fmt;

src/utils.rs

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

5+
use regex::Regex;
56
use unicode_width::UnicodeWidthStr;
67

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

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

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

10596
width - hidden
@@ -159,6 +150,17 @@ mod tests {
159150
assert_eq!(out.as_string(), "foo");
160151
}
161152

153+
#[test]
154+
fn ansi_escapes() {
155+
let mut out = StringWriter::new();
156+
print_align(&mut out, Alignment::LEFT, "\u{1b}[31;40mred\u{1b}[0m", ' ', 10, false).unwrap();
157+
assert_eq!(display_width(out.as_string()), 10);
158+
159+
let mut out = StringWriter::new();
160+
print_align(&mut out, Alignment::LEFT, "\u{1b}]8;;http://example.com\u{1b}\\example\u{1b}]8;;\u{1b}\\", ' ', 10, false).unwrap();
161+
assert_eq!(display_width(out.as_string()), 10);
162+
}
163+
162164
#[test]
163165
fn utf8_error() {
164166
let mut out = StringWriter::new();

0 commit comments

Comments
 (0)