r-lib / cli

Tools for making beautiful & useful command line interfaces
https://cli.r-lib.org/
Other
648 stars 70 forks source link

Alternative to `utils::menu()`? #228

Open jennybc opened 3 years ago

jennybc commented 3 years ago

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.

gaborcsardi commented 3 years ago

Yes, definitely. Some requirements would be very helpful, thanks!

119 is related.

jennybc commented 3 years ago

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.

jennybc commented 3 years ago

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.

jennybc commented 3 years ago

It would be great to have a default choice that one could accept just by pressing "enter".

jennybc commented 3 years ago

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
jennybc commented 3 years ago

Concrete example where one might want to customize the selection entries:

Screen Shot 2021-03-19 at 12 13 07 PM

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.

danielvartan commented 3 years ago

This would be very helpful. Any chance that will be available on the next version of cli?

CoryMcCartan commented 3 years ago

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.

  1. 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
  2. Related to this, the ability to navigate the menu with arrow keys, like any non-terminal menu on a computer.
  3. 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.

gaborcsardi commented 3 years ago

@CoryMcCartan Good ideas!

  1. can be implemented on every UI.
  2. can be only implemented in terminals that support moving the cursor around, i.e. not in RStudio, R.app, RGui, emacs, etc.
  3. the same applies here.

Given that few people would benefit from 2-3 I would not make them a priority for the first implementation.

CoryMcCartan commented 3 years ago

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.

gaborcsardi commented 3 years ago

To update the visual marker you need to move the cursor to the marker first.

kalaschnik commented 2 years ago

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

gaborcsardi commented 2 years ago

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

kalaschnik commented 2 years ago

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!

gaborcsardi commented 2 years ago

It'll be in cli at some point, but the fancy terminal stuff is not high priority because it only works in terminals.

kalaschnik commented 2 years ago

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?

gaborcsardi commented 2 years ago

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.

hadley commented 1 year ago

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.

hadley commented 1 year ago

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: ")
  }
}
hadley commented 1 year ago

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().

gaborcsardi commented 1 year ago

Btw. don't call format_inline(). It is not needed if you use cli_inform() later, and leads to bug.

hadley commented 1 year ago

@gaborcsardi thanks; that was a remnant of an older approach.