Counterparts to R string manipulation functions that account for the effects of ANSI text formatting control sequences.
Many terminals will recognize special sequences of characters in
strings and change display behavior as a result. For example, on my
terminal the sequences "\033[3?m"
and
"\033[4?m"
, where "?"
is a digit in 1-7,
change the foreground and background colors of text respectively:
fansi <- "\033[30m\033[41mF\033[42mA\033[43mN\033[44mS\033[45mI\033[m"
This type of sequence is called an ANSI CSI SGR control sequence.
Most *nix terminals support them, and newer versions of Windows and
Rstudio consoles do too. You can check whether your display supports
them by running term_cap_test()
.
Whether the fansi
functions behave as expected depends
on many factors, including how your particular display handles Control
Sequences. See ?fansi
for details, particularly if you are
getting unexpected results.
ANSI control characters and sequences (Control Sequences hereafter) break the relationship between byte/character position in a string and display position. For example, to extract the “ANS” part of our colored “FANSI”, we would need to carefully compute the character positions:
With fansi
we can select directly based on display
position:
If you look closely you’ll notice that the text color for the
substr
version is wrong as the naïve string extraction
loses the initial"\033[37m"
that sets the foreground color.
Additionally, the color from the last letter bleeds out into the next
line.
fansi
Functionsfansi
provides counterparts to the following string
functions:
substr
(and substr<-
)strsplit
strtrim
strwrap
nchar
/ nzchar
trimws
These are drop-in replacements that behave (almost) identically to
the base counterparts, except for the Control Sequence
awareness. There are also utility functions such as
strip_ctl
to remove Control Sequences and
has_ctl
to detect whether strings contain them.
Much of fansi
is written in C so you should find
performance of the fansi
functions to be slightly slower
than the corresponding base functions, with the exception that
strwrap_ctl
is much faster. Operations involving
type = "width"
will be slower still. We have prioritized
convenience and safety over raw speed in the C code, but unless your
code is primarily engaged in string manipulation fansi
should be fast enough to avoid attention in benchmarking traces.
fansi
also includes improved versions of some of those
functions, such as substr2_ctl
which allows for width based
substrings. To illustrate, let’s create an emoji string made up of two
wide characters:
pizza.grin <- sprintf("\033[46m%s\033[m", strrep("\U1F355\U1F600", 10))
And a colorful background made up of one wide characters:
raw <- paste0("\033[45m", strrep("FANSI", 40))
wrapped <- strwrap2_ctl(raw, 41, wrap.always=TRUE)
When we inject the 2-wide emoji into the 1-wide background their widths are accounted for as shown by the result remaining rectangular:
starts <- c(18, 13, 8, 13, 18)
ends <- c(23, 28, 33, 28, 23)
substr2_ctl(wrapped, type='width', starts, ends) <- pizza.grin
fansi
width calculations use heuristics to account for
graphemes, including combining emoji:
emo <- c(
"\U1F468",
"\U1F468\U1F3FD",
"\U1F468\U1F3FD\u200D\U1F9B3",
"\U1F468\u200D\U1F469\u200D\U1F467\u200D\U1F466"
)
writeLines(
paste(
emo,
paste("base:", nchar(emo, type='width')),
paste("fansi:", nchar_ctl(emo, type='width'))
) )
## 👨 base: 2 fansi: 2
## 👨🏽 base: 4 fansi: 2
## 👨🏽🦳 base: 6 fansi: 2
## 👨👩👧👦 base: 8 fansi: 2
You can translate ANSI CSI SGR formatted strings into their HTML
counterparts with to_html
:
It is possible to set knitr
hooks such that R output
that contains ANSI CSI SGR is automatically converted to the HTML
formatted equivalent and displayed as intended. See the vignette
for details.
This package is available on CRAN:
install.packages('fansi')
It has no runtime dependencies.
For the development version use
remotes::install_github('brodieg/fansi@development')
or:
f.dl <- tempfile()
f.uz <- tempfile()
github.url <- 'https://github.com/brodieG/fansi/archive/development.zip'
download.file(github.url, f.dl)
unzip(f.dl, exdir=f.uz)
install.packages(file.path(f.uz, 'fansi-development'), repos=NULL, type='source')
unlink(c(f.dl, f.uz))
There is no guarantee that development versions are stable or even working. The master branch typically mirrors CRAN and should be stable.