A little ANSI trickery

Fun and games with asciicast and cli. There’s really not much to this one except a little bit of personal entertainment
R
Author
Published

June 14, 2023

On mastodon today I wrote cute little post showing what my R startup message currently looks like:

In the replies, I included the code showing how to generate the rainbow strip that appears at the bottom of the startup message. Calling this rainbow_strip() function from the R console will give you the result you’re looking for:

rainbow_strip <- function() {
  c("#e50000", "#ff8d00", "#ffee00", "#028121", "#004cff", "#770088") |>
    purrr::map(cli::make_ansi_style) |>
    purrr::walk(~ cat(.x(paste0(rep("\u2583", 6), collapse = ""))))
}

The code isn’t very complicated, but it does rely on a few tricks. The most important trick is the one that occurs on the second line, in which I use the cli package to define a set of six styles, each of which colours the text in one of the colours from the LGBTIQ+ pride flag.1 Each of these six styles is then applied to the UTF-8 character2 string "▃▃▃▃▃▃" and the results are concatenated in the output. It’s very pretty. But suppose I wanted to reproduce the style out output within a quarto document like this one. Alas, it does not work, because information about text colour is not preserved in the output when the document is rendered:

rainbow_strip()
▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃

Hm. Well that’s annoying, but hardly surprising to anyone familiar with R markdown or quarto. But just to be sure, let’s try it again, shall we?

rainbow_strip()
▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃                                            

Say what? It works the second time, but not the first? Peculiar. Someone – possibly me?3 – must be engaged in some trickery that isn’t obvious from first inspection.

The trick revealed

If you were to take a look at the source code for this quarto document, the thing you’d immediately notice is that while both of these two code chunks contain R code, one of them is explicitly an R chunk and the other is… not. Here’s the first one again, with the quarto code fencing shown:

```{r}
rainbow_strip()
```
▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃

The second one, however, is tagged as an “asciicast” code chunk. It still executes R code, but something extra is going on when the code runs, because now the output now preserves the text colour:

```{asciicast}
rainbow_strip()
```
▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃                                            

To be clear, there’s nothing special about my rainbow_strip() function here. You can see the same thing happening with any R function that produces coloured text in the output. For example, when printing a tibble at the R console you would normally expect to see the less important parts of the output printed in a muted grey colour. However, when we create a tibble in a quarto document the shading disappears:

# this is an R chunk
tibble::tibble(x = 1:3, y = letters[1:3])
# A tibble: 3 × 2
      x y    
  <int> <chr>
1     1 a    
2     2 b    
3     3 c    

The shading reappears when the same code is executed with the asciicast knitr engine:

# this is an asciicast chunk
tibble::tibble(x = 1:3, y = letters[1:3])
# A tibble: 3 × 2                                                               
      x y                                                                       
  <int> <chr>                                                                   
1     1 a                                                                       
2     2 b                                                                       
3     3 c                                                                       

The thing that makes this all work is the asciicast package. Typically, the asciicast package is used to create screencast from R code. I’ve used it before on this blog, actually. When I wrote the “pretty little CLIs” post about the cli package, I used asciicast to create all the animations that appear in the post. However, you can also use asciicast in conjunction with the knitr engine that powers the code execution in quarto.4 Here’s the code chunk I used to set it up for the current post:

```{r}
knitr::opts_chunk$set(
  collapse = FALSE,
  comment = "",
  out.width = "100%",
  cache = TRUE,
  asciicast_knitr_output = "html"
)

asciicast::init_knitr_engine(
  echo = TRUE,
  echo_input = FALSE,
  same_process = TRUE,
  startup = quote({
    library(cli)
    options(
      cli.num_colors = cli::truecolor,
      asciicast_theme = list(background = c(255, 255, 255))
    )
    set.seed(1)
  })
)
```

The first command sets the knitr options for R code chunks. The important line here is the one that sets asciicast_knitr_output = "html", which ensures that asciicast produces HTML output. If you don’t set this, the output from asciicast chunks will be rendered as images rather than HTML. The second line does what you might expect: calling asciicast::init_knitr_engine() initialises the asciicast knitr engine.

The trick explained

In case you don’t already know this stuff, it’s probably worth explaining why the text colour in R output usually vanishes when R code is executed within a quarto or R markdown document. In fact, when I wrote the “pretty little CLIs” post I talked about precisely this:

The R console is a terminal, and its behaviour doesn’t always translate nicely to HTML. Part of the magic of the rmarkdown package is that most of the time it is able to capture terminal output and translate it seamlessly into HTML, and we mere mortal users never notice how clever this is. However, when dealing with cli output, we run into cases where this breaks down and the law of leaky abstractions comes into play: text generated at the R console does not follow the same rules as text inserted into an HTML document, and R Markdown sometimes needs a little help when transforming one to the other.

As regards colour:

The colours and symbols used by cli, and supported in the R console, rely on ANSI escape codes, but those escape codes aren’t recognised in HTML

In that post I used the fansi package to write a knitr hook that translated the relevant ANSI characters into HTML, thereby preserving the colour information. In essence, the asciicast package allows me to do the same thing here.

Fun!

rainbow_flag <- function() {
  c("#e50000", "#ff8d00", "#ffee00", "#028121", "#004cff", "#770088") |>
    purrr::map(cli::make_ansi_style) |>
    purrr::walk(~ cat(.x(c(paste0(rep("\u2588", 18), collapse = ""), "\n"))))
}

rainbow_flag()
██████████████████                                                              
██████████████████                                                              
██████████████████                                                              
██████████████████                                                              
██████████████████                                                              
██████████████████                                                              

Footnotes

  1. In the post on Mastodon I used crayon::make_style () rather than cli::make_ansi_style(), but they do the same thing. After I wrote the post I remembered that the crayon package has been superseded by cli, so it’s generally better to use the cli version instead.↩︎

  2. In the source code I use "\u2583" to produce the UTF-8 block character "▃".↩︎

  3. Definitely me.↩︎

  4. I mean, that’s assuming you’re using knitr as the engine, which I am in this post. There’s nothing stopping you from using jupyter as the execution engine, which I’ve done in the past for python-focused posts.↩︎

Reuse

Citation

BibTeX citation:
@online{navarro2023,
  author = {Navarro, Danielle},
  title = {A Little {ANSI} Trickery},
  date = {2023-06-14},
  url = {https://blog.djnavarro.net/posts/2023-06-14_ansi-tricks/},
  langid = {en}
}
For attribution, please cite this work as:
Navarro, Danielle. 2023. “A Little ANSI Trickery.” June 14, 2023. https://blog.djnavarro.net/posts/2023-06-14_ansi-tricks/.