Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Alternative to utils::menu()? #228

Open
jennybc opened this issue Mar 12, 2021 · 22 comments
Open

Alternative to utils::menu()? #228

jennybc opened this issue Mar 12, 2021 · 22 comments
Labels
feature a feature request or enhancement

Comments

@jennybc
Copy link
Member

jennybc commented Mar 12, 2021

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
Copy link
Member

gaborcsardi commented Mar 13, 2021

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

#119 is related.

@jennybc
Copy link
Member Author

jennybc commented Mar 13, 2021

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
Copy link
Member Author

jennybc commented Mar 13, 2021

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
Copy link
Member Author

jennybc commented Mar 13, 2021

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

@jennybc
Copy link
Member Author

jennybc commented Mar 13, 2021

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
Copy link
Member Author

jennybc commented Mar 19, 2021

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.

@gaborcsardi gaborcsardi added the feature a feature request or enhancement label Mar 23, 2021
@gaborcsardi gaborcsardi added this to the 2.4.0 milestone Mar 24, 2021
@gaborcsardi gaborcsardi removed this from the 2.4.0 milestone Apr 4, 2021
@danielvartan
Copy link

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

@CoryMcCartan
Copy link

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
Copy link
Member

gaborcsardi commented Aug 23, 2021

@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
Copy link

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
Copy link
Member

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

@kalaschnik
Copy link

kalaschnik commented Dec 1, 2021

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
Copy link
Member

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
Copy link

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
Copy link
Member

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

@kalaschnik
Copy link

kalaschnik commented Jan 23, 2022

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
Copy link
Member

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
Copy link
Member

hadley commented Mar 2, 2023

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
Copy link
Member

hadley commented Mar 3, 2023

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
Copy link
Member

hadley commented Mar 7, 2023

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
Copy link
Member

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

@hadley
Copy link
Member

hadley commented Mar 7, 2023

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature a feature request or enhancement
Projects
None yet
Development

No branches or pull requests

6 participants