R/select.R

Defines functions select

Documented in select

#' Single Selection Menu
#'
#' Interactive menu for selecting a single item from a list.
#' Uses arrow keys (or j/k) to navigate and Enter to select.
#'
#' @param choices Character vector of choices to display
#' @param prompt Prompt message to display
#' @param selected Pre-selected item (index or value)
#' @param return_index Return index instead of value (default: FALSE)
#' @param max_visible Maximum number of items to display at once (default: 10).
#'   Set to NULL to show all items.
#'
#' @return Selected item as character or index, or NULL if cancelled
#' @export
#'
#' @examples
#' if (interactive()) {
#'   choice <- select(c("Yes", "No", "Maybe"))
#'   index <- select(c("First", "Second", "Third"), return_index = TRUE)
#'
#'   # With scrolling for long lists
#'   choice <- select(1:100, max_visible = 10)
#' }
select <- function(choices,
                   prompt = "Select an item:",
                   selected = NULL,
                   return_index = FALSE,
                   max_visible = 10L) {
  # Validate inputs
  validate_choices(choices)

  # Determine initial cursor position
  cursor_pos <- normalize_selected(selected, choices, multiple = FALSE)
  if (is.null(cursor_pos)) cursor_pos <- 1L

  n_choices <- length(choices)

  # Check if running in interactive mode
  if (!interactive()) {
    cli::cli_warn("Not running in interactive mode. Returning first choice.")
    return(if (return_index) 1L else choices[1])
  }

  # Initialize window offset for scrolling
  window_offset <- 1L

  # Adjust initial window to show cursor
  if (!is.null(max_visible) && max_visible < n_choices) {
    # Center cursor in window if possible
    ideal_offset <- cursor_pos - as.integer(max_visible / 2)
    window_offset <- max(1L, min(ideal_offset, n_choices - max_visible + 1L))
  }

  # Display prompt
  cat("\n")
  cli::cli_text(prompt)
  cat("\n")

  # Main interaction loop
  repeat {
    # Render menu
    menu_lines <- render_menu(
      choices = choices,
      cursor_pos = cursor_pos,
      selected_indices = NULL,
      type = "select",
      window_offset = window_offset,
      max_visible = max_visible
    )

    n_lines <- length(menu_lines)

    # Get user input
    key <- get_keypress()

    # Clear previous menu
    clear_lines(n_lines)

    # Handle key press
    if (key %in% c("up", "k")) {
      cursor_pos <- if (cursor_pos > 1) cursor_pos - 1L else n_choices

      # Adjust window if cursor moved outside visible range
      if (!is.null(max_visible) && max_visible < n_choices) {
        if (cursor_pos < window_offset) {
          window_offset <- cursor_pos
        }
        # Handle wrap-around from top to bottom
        if (cursor_pos == n_choices && window_offset != max(1L, n_choices - max_visible + 1L)) {
          window_offset <- max(1L, n_choices - max_visible + 1L)
        }
      }
    } else if (key %in% c("down", "j")) {
      cursor_pos <- if (cursor_pos < n_choices) cursor_pos + 1L else 1L

      # Adjust window if cursor moved outside visible range
      if (!is.null(max_visible) && max_visible < n_choices) {
        visible_end <- min(window_offset + max_visible - 1L, n_choices)
        if (cursor_pos > visible_end) {
          window_offset <- cursor_pos - max_visible + 1L
        }
        # Handle wrap-around from bottom to top
        if (cursor_pos == 1L) {
          window_offset <- 1L
        }
      }
    } else if (key == "enter") {
      break
    } else if (key == "esc") {
      cat("\n")
      cli::cli_alert_info("Selection cancelled")
      return(NULL)
    }
  }

  cat("\n")
  cli::cli_alert_success("Selected: {.val {choices[cursor_pos]}}")

  if (return_index) {
    return(cursor_pos)
  } else {
    return(choices[cursor_pos])
  }
}

Try the climenu package in your browser

Any scripts or data that you put into this service are public.

climenu documentation built on Feb. 6, 2026, 5:08 p.m.