-
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
Changes from 11 commits
99704cc
a9bb679
10654d2
e2ec764
485e0e4
163f92b
53e3f7f
d5c8219
0d747f8
f5bcd5e
71e9627
6e688e5
c2fb24b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -250,3 +250,80 @@ compute_n_show <- function(n, n_show_nominal = 5, n_fudge = 2) { | |
| n | ||
| } | ||
| } | ||
|
|
||
| # menu(), but based on readline() + cli and mockable --------------------------- | ||
| # https://github.com/r-lib/cli/issues/228 | ||
| # https://github.com/rstudio/rsconnect/blob/main/R/utils-cli.R | ||
|
|
||
| cli_menu <- function(header, | ||
| prompt, | ||
| choices, | ||
| not_interactive = choices, | ||
| exit = integer(), | ||
| .envir = caller_env(), | ||
| error_call = caller_env()) { | ||
| if (!is_interactive()) { | ||
| cli::cli_abort( | ||
| c(header, not_interactive), | ||
| .envir = .envir, | ||
| call = error_call | ||
| ) | ||
| } | ||
|
|
||
| choices <- paste0(cli::style_bold(seq_along(choices)), ": ", choices) | ||
| cli::cli_inform( | ||
| c(header, prompt, choices), | ||
| .envir = .envir | ||
| ) | ||
|
|
||
| repeat { | ||
| selected <- cli_readline("Selection: ") | ||
| if (selected %in% c("0", seq_along(choices))) { | ||
| break | ||
| } | ||
| cli::cli_inform( | ||
| "Enter a number between 1 and {length(choices)}, or enter 0 to exit." | ||
| ) | ||
| } | ||
|
|
||
| selected <- as.integer(selected) | ||
| if (selected %in% c(0, exit)) { | ||
| if (is_testing()) { | ||
| cli::cli_abort("Exiting...", call = NULL) | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Exiting not quitting |
||
| } else { | ||
| cli::cli_alert_danger("Exiting...") | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ditto |
||
| # simulate user pressing Ctrl + C | ||
| invokeRestart("abort") | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Omitted the rogue |
||
| } | ||
| } | ||
|
|
||
| selected | ||
| } | ||
|
|
||
| cli_readline <- function(prompt) { | ||
| local_input <- getOption("cli_input", character()) | ||
|
|
||
| # not convinced that we need to plan for multiple mocked inputs, but leaving | ||
| # this feature in for now | ||
| if (length(local_input) > 0) { | ||
| input <- local_input[[1]] | ||
| cli::cli_inform(paste0(prompt, input)) | ||
| options(cli_input = local_input[-1]) | ||
| input | ||
| } else { | ||
| readline(prompt) | ||
| } | ||
| } | ||
|
|
||
| local_user_input <- function(x, env = caller_env()) { | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Went with a |
||
| withr::local_options( | ||
| rlang_interactive = TRUE, | ||
| # trailing 0 prevents infinite loop if x only contains invalid choices | ||
| cli_input = c(x, "0"), | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Appending Maybe this makes both of us happy? 😅
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Or that could go in
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| .local_envir = env | ||
| ) | ||
| } | ||
|
|
||
| is_testing <- function() { | ||
| identical(Sys.getenv("TESTTHAT"), "true") | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -148,3 +148,98 @@ | |
| * g | ||
| * h | ||
|
|
||
| # cli_menu() basic usage | ||
|
|
||
| Code | ||
| cli_menu_with_mock(1) | ||
| Message | ||
| Found multiple thingies. | ||
| Which one do you want to use? | ||
| 1: label a | ||
| 2: label b | ||
| 3: label c | ||
| Selection: 1 | ||
| Output | ||
| [1] 1 | ||
|
|
||
| # cli_menu() invalid selection | ||
|
|
||
| Code | ||
| cli_menu_with_mock("nope") | ||
| Message | ||
| Found multiple thingies. | ||
| Which one do you want to use? | ||
| 1: label a | ||
| 2: label b | ||
| 3: label c | ||
| Selection: nope | ||
| Enter a number between 1 and 3, or enter 0 to exit. | ||
| Selection: 0 | ||
| Condition | ||
| Error: | ||
| ! Exiting... | ||
|
|
||
| # cli_menu(), request exit via 0 | ||
|
|
||
| Code | ||
| cli_menu_with_mock(0) | ||
| Message | ||
| Found multiple thingies. | ||
| Which one do you want to use? | ||
| 1: label a | ||
| 2: label b | ||
| 3: label c | ||
| Selection: 0 | ||
| Condition | ||
| Error: | ||
| ! Exiting... | ||
|
|
||
| # cli_menu(exit =) works | ||
|
|
||
| Code | ||
| cli_menu_with_mock(1) | ||
| Message | ||
| Hey we need to talk. | ||
| What do you want to do? | ||
| 1: Give up | ||
| 2: Some other thing | ||
| Selection: 1 | ||
| Condition | ||
| Error: | ||
| ! Exiting... | ||
|
|
||
| --- | ||
|
|
||
| Code | ||
| cli_menu_with_mock(2) | ||
| Message | ||
| Hey we need to talk. | ||
| What do you want to do? | ||
| 1: Give up | ||
| 2: Some other thing | ||
| Selection: 2 | ||
| Output | ||
| [1] 2 | ||
|
|
||
| # cli_menu() inline markup and environment passing | ||
|
|
||
| Code | ||
| cli_menu_with_mock(1) | ||
| Message | ||
| Hey we need to "talk". | ||
| What do you want to "do"? | ||
| 1: Send email to '[email protected]' | ||
| 2: Install the nifty package | ||
| Selection: 1 | ||
| Output | ||
| [1] 1 | ||
|
|
||
| # cli_menu() not_interactive, many strings, chained error | ||
|
|
||
| Code | ||
| wrapper_fun() | ||
| Condition | ||
| Error in `wrapper_fun()`: | ||
| ! Multiple things found. | ||
| i Use `thingy` to specify one of "thing 1", "thing 2", and "thing 3". | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -58,3 +58,94 @@ test_that("bulletize() works", { | |
| expect_snapshot(cli::cli_bullets(bulletize(letters[1:6], n_fudge = 0))) | ||
| expect_snapshot(cli::cli_bullets(bulletize(letters[1:8], n_fudge = 3))) | ||
| }) | ||
|
|
||
| # menu(), but based on readline() + cli and mockable --------------------------- | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This basically covers the |
||
|
|
||
| test_that("cli_menu() basic usage", { | ||
| cli_menu_with_mock <- function(x) { | ||
| local_user_input(x) | ||
| cli_menu( | ||
| "Found multiple thingies.", | ||
| "Which one do you want to use?", | ||
| glue("label {head(letters, 3)}") | ||
| ) | ||
| } | ||
|
|
||
| expect_snapshot(cli_menu_with_mock(1)) | ||
| }) | ||
|
|
||
| test_that("cli_menu() invalid selection", { | ||
| cli_menu_with_mock <- function(x) { | ||
| local_user_input(x) | ||
| cli_menu( | ||
| "Found multiple thingies.", | ||
| "Which one do you want to use?", | ||
| glue("label {head(letters, 3)}") | ||
| ) | ||
| } | ||
|
|
||
| expect_snapshot(cli_menu_with_mock("nope"), error = TRUE) | ||
| }) | ||
|
|
||
| test_that("cli_menu(), request exit via 0", { | ||
| cli_menu_with_mock <- function(x) { | ||
| local_user_input(x) | ||
| cli_menu( | ||
| "Found multiple thingies.", | ||
| "Which one do you want to use?", | ||
| glue("label {head(letters, 3)}") | ||
| ) | ||
| } | ||
|
|
||
| expect_snapshot(error = TRUE, cli_menu_with_mock(0)) | ||
| }) | ||
|
|
||
| test_that("cli_menu(exit =) works", { | ||
| cli_menu_with_mock <- function(x) { | ||
| local_user_input(x) | ||
| cli_menu( | ||
| header = "Hey we need to talk.", | ||
| prompt = "What do you want to do?", | ||
| choices = c( | ||
| "Give up", | ||
| "Some other thing" | ||
| ), | ||
| exit = 1 | ||
| ) | ||
| } | ||
|
|
||
| expect_snapshot(error = TRUE, cli_menu_with_mock(1)) | ||
| expect_snapshot(cli_menu_with_mock(2)) | ||
| }) | ||
|
|
||
| test_that("cli_menu() inline markup and environment passing", { | ||
| cli_menu_with_mock <- function(x) { | ||
| local_user_input(x) | ||
| verb <- "talk" | ||
| action <- "do" | ||
| pkg_name <- "nifty" | ||
| cli_menu( | ||
| header = "Hey we need to {.str {verb}}.", | ||
| prompt = "What do you want to {.str {action}}?", | ||
| choices = c( | ||
| "Send email to {.email [email protected]}", | ||
| "Install the {.pkg {pkg_name}} package" | ||
| ) | ||
| ) | ||
| } | ||
| expect_snapshot(cli_menu_with_mock(1)) | ||
| }) | ||
|
|
||
| test_that("cli_menu() not_interactive, many strings, chained error", { | ||
| wrapper_fun <- function() { | ||
| local_interactive(FALSE) | ||
| things <- glue("thing {1:3}") | ||
| cli_menu( | ||
| header = "Multiple things found.", | ||
| prompt = "Which one do you want to use?", | ||
| choices = things, | ||
| not_interactive = c(i = "Use {.arg thingy} to specify one of {.str {things}}.") | ||
| ) | ||
| } | ||
| expect_snapshot(wrapper_fun(), error = TRUE) | ||
| }) | ||
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 ofquit) to better match the typical vocabulary here (e.g. in the docs forutils::menu()) and to avoid any confusion with actuallly quitting R.