cli
cli copied to clipboard
Alternative to `utils::menu()`?
Semi-related to #151
More than once, when working on package UI, I've wished for a better version of utils::menu()
?
Do you think that could fit here? If so, I'll start to jot down a wish list as specifics come up.
Yes, definitely. Some requirements would be very helpful, thanks!
#119 is related.
OK, I will start adding thoughts incrementally.
Similar to rlang::abort()
's approach to structured error messages, there should be scaffolding for the usual elements needed when asking a question. Meaning:
HEADER where we presumably give some context and information to guide the user's choice
ACTUAL QUESTION?
1: pirates
2: ninjas
As it stands with menu()
, one has to print the HEADER and ACTUAL QUESTION separately, which introduces the possibility of the question appearing without these elements, in more complicated situations. Of course, this should never happen, but sometimes it does (for users or, more often, during development experiments).
I'm using rlang::inform()
more and more, directly and indirectly, which introduces the issue of standard output vs. standard error. Often there's some package- or function-level verbosity control in the picture as well.
It would be best if the front matter were handled holistically, with the rest of the menu-making elements, e.g. choices. This also seems to fit nicely with the semantic UI goals.
Yes, I know about the title
argument of menu()
. That's not very satisfying if you're using cli (or other, earlier methods) to create a nice-looking UI.
It feels like there should be a standard (probably optional) footer, explaining how to decline to make a choice, e.g. ESC or entering 0. Styled in a suitably subtle way.
It would be great to have a default choice that one could accept just by pressing "enter".
The "enter this" codes should? could? be customizable:
You are going to live on an island with precisely 1 fruit tree for the rest of your life.
Which do you prefer?
a: apple
b: banana
Concrete example where one might want to customize the selection entries:
Here it feels weird to type '9' to select issue 833. Why not just accept '833'? Maybe in this case, the short numbers are still better, but I figure it's still a decent motivating example.
This would be very helpful. Any chance that will be available on the next version of cli
?
Some things that I think would make menus more user-friendly but which may (probably?) be harder to implement consistently across all terminal types/environments. These would also go beyond just minor improvements to menu()
, and so may not make sense.
- Visual feedback in the list about which choice is currently selected (e.g. an * next to the default). Pressing 'enter' would select the marked option. Scanning back and forth to make sure the typed selection matches the row you really want slows user input
- Related to this, the ability to navigate the menu with arrow keys, like any non-terminal menu on a computer.
- An option to allow for selections without pressing enter? Probably a more specialized use case, but for many repeated inputs with <10 options (or <26 if you label with letters), where the cost of a typo isn't too large, this would also speed input
Presumably for terminals which can't have earlier parts overwritten, 1 and 2 would be disabled.
@CoryMcCartan Good ideas!
- can be implemented on every UI.
- can be only implemented in terminals that support moving the cursor around, i.e. not in RStudio, R.app, RGui, emacs, etc.
- the same applies here.
Given that few people would benefit from 2-3 I would not make them a priority for the first implementation.
Thank you, that makes sense! Just to be clear (and this may not change the doability / priority of it), for 2 the cursor itself wouldn't need to move, you'd just need to be able to listen for keypresses at the prompt & update the visual marker in 1 accordingly.
To update the visual marker you need to move the cursor to the marker first.
I was just looking into interactive terminal packages for R and came across this repo. Is there any news on menus?
Maybe to add inspiration; I love the way ESLint (a linter in the JS world), creates their interactive terminals:
https://sourcelevel.io/wp-content/uploads/eslint-init.gif
If there will be news on this, you'll see it in this issue. :)
Yeah, the JS ecosystem has a lot of tools that makes this easier, e.g. https://www.npmjs.com/package/enquirer and a whole terminal handling stack as well.
The menus are nice, but sadly they are not possible in RStudio, only in a real terminal, which makes them much less important. In the terminal it is not hard to implement them, here is a poc: https://github.com/gaborcsardi/ask
That is great! I think ask
should be part of cli
. Indeed your ask
package is what I was looking for initially. Thanks for sharing!
It'll be in cli at some point, but the fancy terminal stuff is not high priority because it only works in terminals.
I know that this is a bit off-topic; yet, I'm wondering about the underlying reason for why this interactive stuff works better in the terminal and worse in RStudio? So ultimately, that is something RStudio needs to address?
I don't think that will happen. The RStudio console is not a terminal, and it is very unlikely that it will turn into one. If we want menus, etc. in RStudio we could potentially use addins.
A small but useful feature is using is_interactive()
instead of interactive()
, and having some way to control what happens when run in a non-interactive environment.
This is what I've come up with:
cli_menu <- function(prompt, not_interactive, choices, quit = integer(), .envir = caller_env()) {
if (!is_interactive()) {
cli::cli_abort(c(prompt, not_interactive), .envir = .envir)
}
choices <- sapply(choices, cli::format_inline, .envir = .envir, USE.NAMES = FALSE)
choices <- paste0(seq_along(choices), " ", choices)
cli::cli_inform(
c(prompt, "What do you want to do?", choices),
.envir = .envir
)
repeat {
selected <- readline("Selection: ")
if (selected %in% c("0", seq_along(choices))) {
break
}
cli::cli_inform("Enter an item from the menu, or 0 to exit")
}
selected <- as.integer(selected)
if (selected %in% c(0, quit)) {
cli::cli_abort("Quiting...", call = NULL)
}
selected
}
Compared to my previous comment, the additional thing I've realised is that it's useful to mock readline
so you can simulate user input in tests. Obviously that won't once cli_menu
lives in cli, so I think it should include some specific ability to simulate user input, using a global option or similar. Maybe something like this?
cli_readline <- function(prompt) {
testing <- getOption("cli_prompt", character())
if (length(testing) > 0) {
selected <- testing[[1]]
cli::cli_inform(paste0(prompt, ": ", selected))
options(cli_prompt = testing[-1])
selected
} else {
readline("Selection: ")
}
}
Probably want some kind of helper like cli_readline_simulate()
that you could use inside (e.g.) a snapshot test. It'd just need to be a thin wrapper around withr::local_options()
.
Btw. don't call format_inline()
. It is not needed if you use cli_inform()
later, and leads to bug.
@gaborcsardi thanks; that was a remnant of an older approach.