{cli}ckable hyperlinks in the R console

choosethis
cli
r
Author
Published

September 17, 2023

Text saying 'definitely not a virus' with a clickable link underneath that says 'not a virus'. The mouse hovers and has a popup saying 'virus!'

tl;dr

The {cli} R package can help build clickable hyperlinks in the R console, which you can use to execute functions. Of course, I used this for japes.

Error!

Have you noticed that tidyverse error messages are both helpful and pretty? This is in part due to Gabor’s {cli} package, which helps to style command-line output. Sometimes I make errors on purpose just to see these messages (that’s how I explain my mistakes to colleagues, anyway).

Have you noticed that sometimes the error message will include a link that, when clicked, will execute some code to help explore the bug? When you hover over the link, you get a popup in RStudio showing you a green ‘play’ arrow, the name and description of the function and the phrase ‘click to run’.

For example, if we ask {dplyr}’s select() to retain a column that doesn’t exist and then hover over the link in the error message:

That’s curious isn’t it? It also appears in other scenarios and sometimes even links to specific lines in specific scripts.

How?

{cli} functions like cli_text() accept {glue} strings that begin with a .run keyword and contain a Markdown hyperlink. Something like this: "{.run [function](package::function())}". The outcome is a link in the console with the text ‘function’ that will execute package::function() when clicked.

You can read more about hyperlinks in the {cli} docs. There’s some limitations, including that your terminal must be capable of supporting this type of hyperlink (RStudio is capable). Note also that links are ‘experimental’ in {cli}.

You might be wondering if a bad actor could exploit this to execute arbitrary code. As per the {cli} docs, there are several restrictions in place:

To make .run hyperlinks more secure, RStudio [will] not run code

  • that is not in the pkg::fun(args) form,
  • if args contains (, ) or ;,
  • if it calls a core package (base, stats, etc.),
  • if it calls a package that is not loaded, and it is not one of testthat, devtools, usethis, or rlang, which are explicitly allowed.

Note that this doesn’t stop nerd hobbyists like me from going off-piste.

Demos

I’ve made two quick sketches that use {cli}’s .run-enabled links. I’ve put these in the {choosethis} package1, which is on GitHub.

install.packages("remotes")  # if not yet installed
remotes::install_github("matt-dray/choosethis")

These nonserious demos are pretty minimal and only exist to prove the point. Maybe you can take these ideas and run with them?

1. A clickable text adventure

You can set up a narrative in the console that prompts the user for an action to advance the story. Clicking a link takes you down a story branch and prompts you with a new set of options.

A gif of the R console. The function 'begin' is run from the 'choosethis' package, which prints the message 'you reach a fork in the road'. Two clickable options are presented: go left or go right. The user clicks 'go right' and this automatically runs the 'right' function and outputs the text 'you died'.

In other words, a classic text adventure in with the flavour of a ‘Choose Your Own Adventure’ book.

This works by setting up a function for each option. Click a link and it will run that function, which itself will present more clickable options with their own underlying functions. And so on. In this demo, the chain starts when the user runs choosethis::begin().

Of course, the more complicated the story, the more functions there are to create and maintain.

2. An R GUI

Adriana suggested that .run could be used to create a sort-of ‘clickable R interface’ to do away with all that pesky typing and emulate superior statistical packages like SPSS, lol. She was definitely joking2, but like, you could do something like that, right?3

But this is… tricky. First there’s the limitations of .run itself, but the user prompts could also become overwhelming. For example, if you want to summarise a data.frame. Which columns should be summarised? Do you want the sum or mean or something else? Should NAs be ignored? And so on.

I made a tiny demo of this anyway. The user runs choosethis::ask_col_means() with a data.frame and they’re presented the names of any numeric columns as clickable links. The dataframe and selected column name are passed to choosethis::get_mean(), a bespoke function for calculating the summary.

Gif of the R console. The 'ask_col_mean' function is run from the 'choosethis' package. The Palmer penguins dataset is passed as the only argument. The user is then prompted with the message 'what column would you like the mean of' and it presents a number of clickable column names. 'bill_depth_mm' is clicked and the console outputs the answer. This is repeated for the column 'body_mass_g'.

Yes, due (I think) to the limitations of .run, we need a separate function in the package to calculate the mean for us (at least I think that’s the case). You can see how tedious it would be to wrap loads of potential summary functions using this approach.

3. Bonus: de-linkification

It feels a bit mean to write an exciting text adventure that people can’t play if their terminal doesn’t support hyperlinks. So you can check the user’s console and either provide them an executable link, or otherwise print the underlying expression to copy-paste.

To perform this check, you can use cli::ansi_has_hyperlink_support()4. I added the argument show_links to the choosethis::begin() function that defaults to this:

getOption("choosethis.show_links", cli::ansi_has_hyperlink_support())

By making this an option, the user can set options(choosethis.show_links = FALSE) to avoid seeing links even if their terminal supports them.

Hype man

So yeah, hyperlinks are ‘experimental’ in {cli} and—quite rightly—they’re limited to prevent nefarious activity. Everything I’ve created here might stop working tomorrow. And of course, the intent for links is to help people with errors, not mess around. But it is fun isn’t it?

I’d be interested to know if anyone is already using {cli}-enacted links in their packages, or if the ideas in {choosethis} spark some inspiration.

Environment

Session info
Last rendered: 2024-01-13 09:58:39 GMT
R version 4.3.1 (2023-06-16)
Platform: aarch64-apple-darwin20 (64-bit)
Running under: macOS Ventura 13.2.1

Matrix products: default
BLAS:   /Library/Frameworks/R.framework/Versions/4.3-arm64/Resources/lib/libRblas.0.dylib 
LAPACK: /Library/Frameworks/R.framework/Versions/4.3-arm64/Resources/lib/libRlapack.dylib;  LAPACK version 3.11.0

locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8

time zone: Europe/London
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

loaded via a namespace (and not attached):
 [1] htmlwidgets_1.6.2 compiler_4.3.1    fastmap_1.1.1     cli_3.6.2        
 [5] tools_4.3.1       htmltools_0.5.6.1 rstudioapi_0.15.0 yaml_2.3.8       
 [9] rmarkdown_2.25    knitr_1.45        jsonlite_1.8.7    xfun_0.41        
[13] digest_0.6.33     rlang_1.1.3       evaluate_0.23    

Footnotes

  1. Yes, a very clever pun on {usethis}, but infinitely less useful than that package.↩︎

  2. My lawyers have insisted I must clarify that, unequivocally, she definitely does not want R to be corrupted into an SPSS-like thing.↩︎

  3. Doesn’t mean you should, amirite.↩︎

  4. Thanks to Tim for suggesting a method for using options in this function.↩︎

Reuse

CC BY-NC-SA 4.0