-
Notifications
You must be signed in to change notification settings - Fork 39
Introduce cli_menu() and friends
#242
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
Conversation
| prompt, | ||
| choices, | ||
| not_interactive = choices, | ||
| exit = integer(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I called this exit (instead of quit) to better match the typical vocabulary here (e.g. in the docs for utils::menu()) and to avoid any confusion with actuallly quitting R.
R/utils-ui.R
Outdated
| # guard against invalid mocked input | ||
| local_input <- getOption("cli_input", character()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was able to get into an infinite loop without this. Of course it only happens when one enters invalid mocked input, but still seemed worth accounting for.
R/utils-ui.R
Outdated
| if (length(local_input) > 0) { | ||
| cli::cli_abort( | ||
| c(x = "Internal error: mocked input is invalid."), | ||
| .envir = .envir, | ||
| call = error_call | ||
| ) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The second part of "avoiding an infinite loop".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we do that all in one place? Or extract it out into a helper?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK I consolidated things, just to make it a bit neater, at least.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does it error if it’s length greater than zero? The original intent was to be able to supply multiple values that were used in turn.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Over in rsconnect, if I add this test, it causes the test suite to just hang:
test_that("cli_menu() works with multiple inputs", {
simulate_user_input(3)
expect_snapshot(
cli_menu(
"Let's talk",
"Are you OK?",
choices = c("Yes", "No")
)
)
})
I guess I haven't found an organic need to supply multiple values. But I have managed to supply an invalid one. So it seems nice for development purposes to handle that case.
I can try to think about how to preserve both.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you convinced that multiple mocked inputs is important?
It feels like we could just make multiple calls to simulate_user_input() (rsconnect) or local_user_input() (here in gargle) in such a test and let the functions be simpler.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I realize I can pursue the simplification that's most natural here. I'm mostly talking about what we might eventually want in cli or in some standalone file for general use.
| selected <- as.integer(selected) | ||
| if (selected %in% c(0, exit)) { | ||
| if (is_testing()) { | ||
| cli::cli_abort("Exiting...", call = NULL) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exiting not quitting
| if (is_testing()) { | ||
| cli::cli_abort("Exiting...", call = NULL) | ||
| } else { | ||
| cli::cli_alert_danger("Exiting...") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto
| } else { | ||
| cli::cli_alert_danger("Exiting...") | ||
| # simulate user pressing Ctrl + C | ||
| invokeRestart("abort") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Omitted the rogue cnd that the rsconnect version has here (presumably a think-o).
| } | ||
| } | ||
|
|
||
| local_user_input <- function(x, env = caller_env()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Went with a local_*()-style name
| expect_snapshot(cli::cli_bullets(bulletize(letters[1:8], n_fudge = 3))) | ||
| }) | ||
|
|
||
| # menu(), but based on readline() + cli and mockable --------------------------- |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This basically covers the cli_menu() stuff 100%, except the lines you can only hit in interactive use.
gaborcsardi
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
hadley
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like some nice improvements!
R/oauth-cache.R
Outdated
| )) | ||
| choice <- utils::menu(emails) | ||
| c( | ||
| "Select a pre-authorised account or enter '0' to obtain a new token.", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we add an explicit bullet like "Obtain a new token" instead of using 0? (I don't think this code will work now because cli_menu() will exit to the top-level if the user enters 0)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this code will work now because
cli_menu()will exit to the top-level if the user enters 0
Yes, this is true. I will deal with that 🤔
R/utils-ui.R
Outdated
| if (length(local_input) > 0) { | ||
| cli::cli_abort( | ||
| c(x = "Internal error: mocked input is invalid."), | ||
| .envir = .envir, | ||
| call = error_call | ||
| ) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we do that all in one place? Or extract it out into a helper?
Sure it's inefficient to re-read the option more than once but this truly does not matter.
Co-authored-by: Hadley Wickham <[email protected]>
R/oauth-cache.R
Outdated
| "Select a pre-authorised account or enter '0' to obtain a new token.", | ||
| "Press Esc/Ctrl + C to cancel." | ||
| "Enter '1' to start a new auth process or select a pre-authorized account.", | ||
| "Press Esc or Ctrl + C to cancel." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe you could omit this line now that 0 has its typical behaviour?
| withr::local_options( | ||
| rlang_interactive = TRUE, | ||
| # trailing 0 prevents infinite loop if x only contains invalid choices | ||
| cli_input = c(x, "0"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Appending "0" here means you can't get stuck in an infinite loop, which means I can also delete the fussy check inside the repeat { ... } in cli_menu().
Maybe this makes both of us happy? 😅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or that could go in cli_readline()? I think I forgot how these functions were factored.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, that's an interesting idea. I've mentally moved on to other projects in gargle, but I think this conversation and the state of these functions here can still inform whatever a more official version looks like.
|
I found a solution I like to the infinite loop problem, which is to append
I also added a test for this behaviour. |

This is functionally important for gargle, because
cli_menu()is ultimately implemented on top ofreadline(), notutils::menu(). And Jupyter has shimmedreadline()to work even though, at the time of writing,base::interactive()returnsFALSEin Jupyter.Therefore this PR makes gargle's interactive choices work in Jupyter notebooks, which notably covers Google Colab.
This is part (but not all) of making user auth "just work" on Colab (#140).
I have adapted
cli_menu()from https://github.com/rstudio/rsconnect/blob/main/R/utils-cli.R. I think most of my changes might be generally positive and could be considered when/if something like this goes into cli itself (r-lib/cli#228). I also added tests.@hadley @gaborcsardi I don't really need feedback on this (though it's welcome). This is more of an FYI request for review, to continue the discussion around r-lib/cli#228. I'll make a few comments inline.
Links to the Jupyter +
readline()story:IRkernel/IRkernel#452
https://github.com/IRkernel/IRkernel/blob/1eddb304b246c14b62949abd946e8d4ca5080d25/R/execution.r#L131-L137
https://github.com/IRkernel/IRkernel/blob/1eddb304b246c14b62949abd946e8d4ca5080d25/R/execution.r#L271-272