Conversation
| prompt, | ||
| choices, | ||
| not_interactive = choices, | ||
| exit = integer(), |
There was a problem hiding this comment.
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.
| # guard against invalid mocked input | ||
| local_input <- getOption("cli_input", character()) |
There was a problem hiding this comment.
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.
| 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.
The second part of "avoiding an infinite loop".
There was a problem hiding this comment.
Can we do that all in one place? Or extract it out into a helper?
There was a problem hiding this comment.
OK I consolidated things, just to make it a bit neater, at least.
There was a problem hiding this comment.
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.
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.
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.
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) |
| if (is_testing()) { | ||
| cli::cli_abort("Exiting...", call = NULL) | ||
| } else { | ||
| cli::cli_alert_danger("Exiting...") |
| } else { | ||
| cli::cli_alert_danger("Exiting...") | ||
| # simulate user pressing Ctrl + C | ||
| invokeRestart("abort") |
There was a problem hiding this comment.
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.
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.
This basically covers the cli_menu() stuff 100%, except the lines you can only hit in interactive use.
hadley
left a comment
There was a problem hiding this comment.
Looks like some nice improvements!
| )) | ||
| 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.
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.
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 🤔
| 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.
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 <h.wickham@gmail.com>
| "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.
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.
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.
Or that could go in cli_readline()? I think I forgot how these functions were factored.
There was a problem hiding this comment.
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