R/use_rstudio_secondary_repo.R

Defines functions use_rstudio_secondary_repo

Documented in use_rstudio_secondary_repo

#' Set RStudio Secondary Repository
#'
#' Updates the secondary repositories in `rstudio-prefs.json`.
#'
#' @param ... a series of named secondary repositories, e.g.
#'   `ropensci = "https://ropensci.r-universe.dev"`. Pass `NULL` to remove a
#'   repository, e.g. `ropensci = NULL`. If a URL is passed under a new name,
#'   the old name is removed.
#'
#' @return Invisibly returns the updated `cran_mirror` preference as a named
#'   list on success, or `NULL` if no updates were made (no changes, user
#'   aborted, or not in an interactive session).
#'
#' @author Daniel D. Sjoberg (2021-2022)
#' @author S.A. van der Wulp (since 2026)
#'
#' @examplesIf interactive()
#' # Add a repository
#' use_rstudio_secondary_repo(
#'   ropensci = "https://ropensci.r-universe.dev",
#'   username = "https://username.r-universe.dev"
#' )
#'
#' # Remove a repository
#' use_rstudio_secondary_repo(ropensci = NULL)
#'
#' @export
use_rstudio_secondary_repo <- function(...) {
  # check whether fn may be used -----------------------------------------------
  check_min_rstudio_version("1.3")
  if (!interactive()) {
    "{.code use_rstudio_secondary_repo()} must be run interactively." %>%
      cli::cli_alert_danger()
    return(invisible())
  }

  # save lists of existing and updated repos -----------------------------------
  user_passed_updated_repos <- rlang::dots_list(...)
  if (!rlang::is_named(user_passed_updated_repos)) {
    rlang::abort("Each argument must be named.")
  }

  list_current_cran_mirror <-
    rstudioapi::readRStudioPreference("cran_mirror", default = NULL)

  # if no secondary repos exist, create the default structure for them ---------
  if (is.null(list_current_cran_mirror)) {
    list_current_cran_mirror <-
      list("name" = "Global (CDN)",
           "host" = "RStudio",
           "url" = "https://cran.rstudio.com/",
           "repos" = "",
           "country" = "us",
           "secondary" = "")
  }

  # parse the secondary repo string --------------------------------------------
  current_repos <-
    repo_string_as_named_list(list_current_cran_mirror$secondary)

  # adjust lists ---------------------------------------------------------------
  user_passed_updated_repos <-
    union(
      # if one of the new repos has the same value but new name as a previous
      # then add a new NULL value to the updates list
      current_repos[unlist(current_repos) %in% unlist(user_passed_updated_repos)] %>% names(),
      # set any existing named repos with same name as passed list to NULL
      current_repos[names(current_repos) %in% names(user_passed_updated_repos)] %>% names()
    ) %>%
    purrr::compact() %>%
    {stats::setNames(rep_len(list(NULL), length.out = length(.)), .)} %>%
    # add user-defined repos to the list
    purrr::list_modify(!!!user_passed_updated_repos)

  # add names from user passed repos to current that don't exist yet (with NULL value)
  current_repos[names(user_passed_updated_repos)] <-
    current_repos[names(user_passed_updated_repos)]

  # print updates that will be made --------------------------------------------
  any_update <- pretty_print_updates(current_repos, user_passed_updated_repos)

  # if no updates, abort function execution
  if (!any_update) {
    return(invisible(NULL))
  }

  # ask user to abort or not
  if (!startsWith(tolower(readline("Would you like to continue? [y/n] ")), "y")) {
    return(invisible(NULL))
  }

  # create final list of repos -------------------------------------------------
  list_current_cran_mirror$secondary <-
    utils::modifyList(current_repos, user_passed_updated_repos) %>%
    purrr::imap_chr(~paste0(.y, "|", .x)) %>%
    paste(collapse = "|")

  # update preferences ---------------------------------------------------------
  rstudioapi::writeRStudioPreference(
    name = "cran_mirror",
    value = list_current_cran_mirror
  )
  return(invisible(list_current_cran_mirror))
}


#' Convert secondary repo string to named list
#'
#' The secondary repo string uses `|` to separate the repo names and their
#' values, as well as two different repos, e.g.
#' `'ropensci|https://ropensci.r-universe.dev|username|https://username.r-universe.dev'`.
#'
#' @param x secondary repository string from
#' `"rstudio-prefs.json"` --> `"cran_mirror"` --> `"secondary"`
#'
#' @return named list
#'
#' @author Daniel D. Sjoberg (2021-2022)
#' @author S.A. van der Wulp (since 2026)
#'
#' @examples
#' repo_string_as_named_list(
#'   'ropensci|https://ropensci.r-universe.dev|username|https://username.r-universe.dev'
#' )
#'
#' @export
repo_string_as_named_list <- function(x) {
  if (is.null(x)) return(list())

  # split string by |
  xx <- strsplit(x, "|", fixed = TRUE) %>% unlist()

  # add empty URL for unpaired repository name
  if (length(xx) %% 2L != 0L) xx <- c(xx, "")

  # use every other element as the value and the name and return as list
  xx[seq_along(xx) %% 2L == 0L] %>%
    stats::setNames(xx[seq_along(xx) %% 2L == 1L]) %>%
    as.list()
}

Try the rstudio.prefs package in your browser

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

rstudio.prefs documentation built on Sept. 26, 2026, 5:06 p.m.