cli icon indicating copy to clipboard operation
cli copied to clipboard

Alternative to `utils::menu()`?

Open jennybc opened this issue 3 years ago • 22 comments

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.

jennybc avatar Mar 12 '21 23:03 jennybc

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

#119 is related.

gaborcsardi avatar Mar 13 '21 06:03 gaborcsardi

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 avatar Mar 13 '21 15:03 jennybc

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 avatar Mar 13 '21 15:03 jennybc

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

jennybc avatar Mar 13 '21 15:03 jennybc

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 avatar Mar 13 '21 16:03 jennybc

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.

jennybc avatar Mar 19 '21 19:03 jennybc

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

danielvartan avatar Aug 07 '21 01:08 danielvartan

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.

CoryMcCartan avatar Aug 23 '21 18:08 CoryMcCartan

@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.

gaborcsardi avatar Aug 23 '21 19:08 gaborcsardi

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.

CoryMcCartan avatar Aug 23 '21 20:08 CoryMcCartan

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

gaborcsardi avatar Aug 24 '21 12:08 gaborcsardi

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

kalaschnik avatar Dec 01 '21 15:12 kalaschnik

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

gaborcsardi avatar Dec 01 '21 17:12 gaborcsardi

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!

kalaschnik avatar Dec 01 '21 18:12 kalaschnik

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

gaborcsardi avatar Dec 01 '21 19:12 gaborcsardi

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?

kalaschnik avatar Jan 23 '22 07:01 kalaschnik

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.

gaborcsardi avatar Jan 23 '22 09:01 gaborcsardi

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 avatar Mar 02 '23 23:03 hadley

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 avatar Mar 03 '23 14:03 hadley

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

hadley avatar Mar 07 '23 14:03 hadley

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

gaborcsardi avatar Mar 07 '23 14:03 gaborcsardi

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

hadley avatar Mar 07 '23 15:03 hadley