vignettes/articles/ui-cli-conversion.Rmd
ui-cli-conversion.Rmd
In a hidden chunk here, I’m “exporting” some unexported internal helpers, so that I can use them and talk about them. For similar reasons, I attach glue above, so that certain glue functions work here, without explicitly namespacing them.
The block styles exist to produce bulleted output with a specific symbol, using a specific color.
> f <- function() { + ui_todo("ui_todo(): red bullet") + ui_done("ui_done(): green check") + ui_oops("ui_oops(): red x") + ui_info("ui_info(): yellow i") + ui_line("ui_line(): (no symbol)") + } > f() • ui_todo(): red bullet ✔ ui_done(): green check ✖ ui_oops(): red x ℹ ui_info(): yellow i ui_line(): (no symbol)
Another important feature is that all of this output can be turned
off package-wide via the usethis.quiet
option.
> withr::with_options(
+ list(usethis.quiet = TRUE),
+ ui_info("You won't see this message.")
+ )
> withr::with_options(
+ list(usethis.quiet = FALSE), # this is the default
+ ui_info("But you will see this one.")
+ )
ℹ But you will see this one.
These styles are very close to what can be done with
cli::cli_bullets()
and the way it responds to the names of
its input text
.
> cli::cli_bullets(c( + "noindent", + " " = "indent", + "*" = "bullet", + ">" = "arrow", + "v" = "success", + "x" = "danger", + "!" = "warning", + "i" = "info" + )) noindent indent • bullet → arrow ✔ success ✖ danger ! warning ℹ info
A direct translation would look something like this:
Legacy ui_*()
|
cli_bullets() shortcode |
tweaks needed |
---|---|---|
ui_todo() |
* |
blue (default) -> red |
ui_done() |
v |
perfect match |
ui_oops() |
x |
perfect match |
ui_info() |
i |
blue (default) -> yellow |
ui_line() |
(unnamed) | sort of a perfect match? although sometimes ui_line()
is used just to get a blank line |
The overall conversion plan is to switch to a new function,
ui_bullets()
, which is a wrapper around
cli::cli_bullets()
, that adds a few features:
usethis.quiet
option is TRUE
.> ui_bullets(c( + "v" = "A great success!", + "_" = "Something you need to do.", + "x" = "Bad news.", + "i" = "The more you know.", + " " = "I'm just here for the indentation.", + "No indentation at all. Not used much in usethis." + )) ✔ A great success! ☐ Something you need to do. ✖ Bad news. ℹ The more you know. I'm just here for the indentation. No indentation at all. Not used much in usethis.
Summary of what I’ve done for todo’s:
_
(instead of *
), which seems to be the best
single ascii character that evokes a place to check something off.cli::symbol$checkbox_off
as the symbol (instead of
a generic bullet). I guess it will continue to be red.In terms of the block styles, that just leaves
ui_code_block()
, which is pretty different.
ui_code_block()
is used to put some code on the screen and
optionally place it on the clipboard. I have created a new function,
ui_code_snippet()
that is built around
cli::code_block()
. Main observations:
cli::code_block(language = "R")
applies syntax
highlighting and hyperlinks (e.g. to function help topics) to R code,
which is cool. Therefore the language
argument is also
exposed in ui_code_snippet()
, defaulting to
"R"
. Use ""
for anything that’s not R code:
> ui_code_snippet("#include <blah.h>", language = "") #include <blah.h> <simpleWarning in system2(util_test[1], util_test[-1], stdout = TRUE, st derr = TRUE): running command ''xsel' --clipboard --output 2>&1' had sta tus 1> <simpleWarning in system2(util_test[1], util_test[-1], stdout = TRUE, st derr = TRUE): running command ''xsel' --clipboard --output 2>&1' had sta tus 1>
ui_code_snippet()
takes a scalar glue-type template
string or a vector of lines. Note that the two calls below produce the
same output.
warnPartialMatchDollar = TRUE, warnPartialMatchAttr = TRUE ) <simpleWarning in system2(util_test[1], util_test[-1], stdout = TRUE, st derr = TRUE): running command ''xsel' --clipboard --output 2>&1' had sta tus 1> <simpleWarning in system2(util_test[1], util_test[-1], stdout = TRUE, st derr = TRUE): running command ''xsel' --clipboard --output 2>&1' had sta tus 1> > # produces same result as > ui_code_snippet(c( + "options(", + " warnPartialMatchArgs = TRUE,", + " warnPartialMatchDollar = TRUE,", + " warnPartialMatchAttr = TRUE", + ")")) options( warnPartialMatchArgs = TRUE, warnPartialMatchDollar = TRUE, warnPartialMatchAttr = TRUE ) <simpleWarning in system2(util_test[1], util_test[-1], stdout = TRUE, st derr = TRUE): running command ''xsel' --clipboard --output 2>&1' had sta tus 1> <simpleWarning in system2(util_test[1], util_test[-1], stdout = TRUE, st derr = TRUE): running command ''xsel' --clipboard --output 2>&1' had sta tus 1>
ui_code_snippet()
does glue interpolation, by default,
before calling cli::cli_code()
, which means you have to
think about your use of {
and }
. If you want
literal {
or }
:
interpolate = FALSE
, if you don’t need
interpolation.{{
or
}}
..open
and .close
as arguments to
ui_code_snippet()
.The block style functions all route through some unexported utility functions.
is_quiet()
just consults the usethis.quiet
option and implements the default of FALSE
.
ui_bullet()
is an intermediate helper used by
ui_todo()
, ui_done()
, ui_oops()
and ui_info()
. It does some hygiene related to indentation
(using the indent()
utility function), then calls
ui_inform()
. ui_line()
and
ui_code()
both call ui_inform()
directly.
ui_inform()
is just a wrapper around
rlang::inform()
that is guarded by a call to
is_quiet()
Other than is_quiet()
, which will continue to play the
same role, I anticipate that we no longer need these utilities
(indent()
, ui_bullet()
,
ui_inform()
). Updates from the future:
indent()
turns out to still be useful in
ui_code_snippet()
, so I’ve inlined it there, to avoid any
reliance on definitions in ui-legacy.R.ui_bullet()
has been renamed to
ui_legacy_bullet()
for auto-completion happiness with the
new ui_bullets()
.Let’s cover ui_silence()
while we’re here, which
is exported. It’s just a withr::with_*()
function
for executing code
with
usethis.quiet = TRUE
.
ui_silence <- function(code) {
withr::with_options(list(usethis.quiet = TRUE), code)
}
usethis has its own inline styles (mostly) for use inside functions
like ui_todo()
:
> new_val <- "oxnard" > x <- glue("{ui_field('name')} set to {ui_value(new_val)}") > dput(x) structure("\033[32mname\033[39m set to \033[34m'oxnard'\033[39m", class = c("glue", "character")) > ui_done(x) ✔ name set to 'oxnard'
The inline styles enact some combination of:
crayon::green(x)
c("a", "b", "c")
to “a, b, c”encodeString(x, quote = "'")
ui_path()
is special because it potentially modifies the
input before styling it. ui_path()
first makes the path
relative to a specific base (by default, the active project root) and,
if the path is a directory, it also ensures there is a trailing
/
.
ui_unset()
is a special purpose helper used when we need
to report that something is unknown, not configured, nonexistent,
etc.
> x <- glue("Your super secret password is {ui_unset()}.") > dput(x) structure("Your super secret password is \033[90m<unset>\033[39m.", clas s = c("glue", "character")) > ui_info(x) ℹ Your super secret password is <unset>.
In general, we can move towards cli’s native inline-markup: https://cli.r-lib.org/reference/inline-markup.html
Here’s the general conversion plan:
ui_field()
becomes {.field blah}
. In
usethis_theme()
, I tweak the .field
style to
apply single quotes if color is not available, which is what
ui_field()
has always done.
ui_value()
becomes
{.val value}
.
ui_path()
is connected to
{.path path/to/thing}
, but, as explained above,
ui_path()
also does more. Therefore, I abstracted the “path
math” into an internal helper, ui_path_impl()
, which is
aliased to pth()
for compactness. Here’s a typical
conversion:
# using legacy functions
ui_done("Setting {ui_field('LazyData')} to \\
{ui_value('true')} in {ui_path('DESCRIPTION')}")
# using new cli-based ui
ui_bullets(c(
"v" = "Setting {.field LazyData} to {.val true} in {.path {pth('DESCRIPTION')}}."
))
It would be nice to create a custom inline class,
e.g. {.ui_path {some_path}}
, which I have done in, e.g.,
googledrive. But it’s not easy to do this while still inheriting
cli’s file:
hyperlink behaviour, which is very
desirable. So that leads to the somewhat clunky, verbose pattern above,
but it gives a nice result.
ui_code()
gets replaced by various inline styles,
depending on what the actual goal is, such as:
{.code some_text}
{.arg some_argument}
{.cls some_class}
{.fun devtools::build_readme}
{.help some_function}
{.run usethis::usethis_function()}
{.topic some_topic}
{.var some_variable}
ui_unset()
is replaced by ui_special()
,
which you’ll see more of below. Currently the intended grey color
doesn’t show up when I render this document using solarized-dark and so
far I can’t get to the bottom of that :( Why isn’t it the same grey as
“[Copied to clipboard]” in ui_code_snippet()
, which does
work?
I’m moving from ui_stop()
:
ui_stop <- function(x, .envir = parent.frame()) {
x <- glue_collapse(x, "\n")
x <- glue(x, .envir = .envir)
cnd <- structure(
class = c("usethis_error", "error", "condition"),
list(message = x)
)
stop(cnd)
}
to ui_abort()
:
ui_abort <- function(message, ..., class = NULL, .envir = parent.frame()) {
cli::cli_div(theme = usethis_theme())
# bullet naming gymnastics, see below
cli::cli_abort(
message,
class = c(class, "usethis_error"),
.envir = .envir,
...
)
}
The main point of ui_abort()
is to use to
cli_abort()
(and to continue applying the
"usethis_error"
class).
I also use ui_abort()
to apply different default bullet
naming/styling. Starting with "x"
and then defaulting to
"i"
seems to fit best with usethis’s existing errors.
> block_start = "# <<<" > block_end = "# >>>" > ui_abort(c( + "Invalid block specification.", + "Must start with {.code {block_start}} and end with {.code {block_en d}}." + )) Error: ✖ Invalid block specification. ℹ Must start with `# <<<` and end with `# >>>`. Run `rlang::last_trace()` to see where the error occurred.
Any bullets that are explicitly given are honored.
atever.")) Error: ✔ It's weird to give a green check in an error, but whatever. Run `rlang::last_trace()` to see where the error occurred. > ui_abort(c( + "!" = "Things are not ideal.", + ">" = "Look at me!" + )) Error: ! Things are not ideal. → Look at me! Run `rlang::last_trace()` to see where the error occurred.
rlang::abort()
and cli::cli_abort()
start
with "!"
by default, then use "*"
and
" "
, respectively.
The legacy functions also include ui_warn()
. It has very
little usage and, instead of converting it, I’ve eliminated its use
altogether in favor of a "!"
bullet:
> ui_bullets(c("!" = "The guy she told you not to worry about."))
! The guy she told you not to worry about.
Sidebar: Now that I’m looking at a lot of the new errors with
ui_abort()
I realize that usethis also needs to be passing
the call
argument along. I’m going to leave that for a
future, separate effort.
This is a small clump of functions that support sitrep-type output.
hd_line()
unexported and, apparently, unused! now
removed
kv_line()
unexported, so has new cli
implementation
ui_unset()
exported and succeeded by
ui_special()
kv_line()
stands for “key-value line”. Here’s what it
used to be:
> kv_line_legacy <- function(key, value, .envir = parent.frame()) { + value <- if (is.null(value)) ui_unset() else ui_value(value) + key <- glue(key, .envir = .envir) + ui_inform(glue("{cli::symbol$bullet} {key}: {value}")) + } > > url <- "https://github.com/r-lib/usethis.git" > remote <- "origin" > kv_line_legacy("URL for the {ui_value(remote)} remote", url) • URL for the 'origin' remote: 'https://github.com/r-lib/usethis.git' > > host <- "github.com" > kv_line_legacy("Personal access token for {ui_value(host)}", NULL) • Personal access token for 'github.com': <unset>
Key features:
key
and value
, because you’re
much more likely to use interpolation and styling in key
than value
.value
is
NULL
."*"
bullet name/style to over all result.I won’t show the updated source for kv_line()
but here
is some usage to show what it’s capable of:
> noun <- "thingy" > value <- "VALUE" > kv_line("Let's reveal {.field {noun}}", "whatever") • Let's reveal thingy: "whatever" > > kv_line("URL for the {.val {remote}} remote", I("{.url {url}}")) • URL for the "origin" remote: <https://github.com/r-lib/usethis.git> > > kv_line("Personal access token for {.val {host}}", NULL) • Personal access token for "github.com": <unset> > > kv_line("Personal access token for {.val {host}}", ui_special("discove red")) • Personal access token for "github.com": <discovered>
ui_special()
is the successor to
ui_unset()
.
There’s currently no drop-in substitute for ui_yeah()
and ui_nope()
in cli. Related issues: https://github.com/r-lib/cli/issues/228, https://github.com/r-lib/cli/issues/488. Therefore, in
the meantime, ui_yeah()
and ui_nope()
are
not-quite-superseded for external users.
However, internally, I’ve switched to the unexported functions
ui_yep()
and ui_nah()
that are lightly
modified versions of ui_yeah()
and ui_nope()
that use cli for styling.
if (ui_nope("
Current branch ({ui_value(actual)}) is not repo's default \\
branch ({ui_value(default_branch)}).{details}")) {
ui_abort("Cancelling. Not on desired branch.")
}
I’ve been adding a period to the end of messages, as a general rule.
In terms of whitespace and indentation, I’ve settled on some conventions. The overall goal is to get the right user-facing output (obviously), while making it as easy as possible to predict what that’s going to look like when you’re writing the code.
ui_bullets(c(
"i" = "Downloading into {.path {pth(destdir)}}.",
"_" = "Prefer a different location? Cancel, try again, and specify
{.arg destdir}."
))
...
ui_bullets(c("x" = "Things are very wrong."))
Key points:
Put ui_bullets(c(
on its own line, then all of the
bullet items, followed by ))
on its own line. Sometimes I
make an exception for a bullet list with exactly one, short bullet
item.
Use hard line breaks inside bullet text to comply with
surrounding line length. In subsequent lines, use indentation to get you
just past the opening "
. This extraneous white space is
later rationalized by cli, which handles wrapping.
Surround bullet names like x
and i
with
quotes, even though you don’t have to, because it’s required for other
names, such as !
or _
and it’s better to be
consistent.
Here’s another style I like that applies to
ui_abort()
, where there’s just one, unnamed bullet, but the
call doesn’t fit on one line.
pr <- list(pr_number = 13)
ui_abort("
The repo or branch where PR #{pr$pr_number} originates seems to have been
deleted.")