Claude Skill

cli

Comprehensive R package for command-line interface styling, semantic messaging, and user communication. Use this skill when working with R code that needs to: (1) Format console output with inline markup and colors, (2) Display errors, warnings, or messages with cli_abort/cli_war

LLM Mart · 0 points · 14 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download posit-dev-skills-r-lib_cli-b58a92e.zip · 34 KB
Part of posit-dev/skills — 23 skills

Install

skills CLI npx skills add https://github.com/posit-dev/skills/tree/main/r-lib/cli
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install posit-dev-skills@llmmart
Git git clone https://github.com/posit-dev/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole posit-dev/skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

CLI for R Packages

When to Use What

task: Display error with context and formatting use: cli_abort() with inline markup and bullet lists

task: Show warning with formatting use: cli_warn() with inline markup

task: Display informative message use: cli_inform() with inline markup

task: Show progress for counted operations use: cli_progress_bar() with total count

task: Show simple progress steps use: cli_progress_step() with status messages

task: Format code or function names use: {.code ...} or {.fn package::function}

task: Format file paths use: {.file path/to/file}

task: Format package names use: {.pkg packagename}

task: Format variable names use: {.var variable_name}

task: Format values use: {.val value}

task: Handle singular/plural text use: {?s} or {?y/ies} with pluralization

task: Create headers use: cli_h1(), cli_h2(), cli_h3()

task: Create alerts use: cli_alert_success(), cli_alert_danger(), cli_alert_warning(), cli_alert_info()

task: Create lists use: cli_ul(), cli_ol(), cli_dl() with cli_li()

Inline Markup Essentials

Use inline markup with {.class content} syntax to format text:

# Basic formatting
cli_text("Function {.fn mean} calculates averages")
cli_text("Install package {.pkg dplyr}")
cli_text("See file {.file ~/.Rprofile}")
cli_text("{.var x} must be numeric, not {.obj_type_of {x}}")
cli_text("Got value {.val {x}}")

# Code formatting
cli_text("Use {.code sum(x, na.rm = TRUE)}")

# Paths and arguments
cli_text("Reading from {.path /data/file.csv}")
cli_text("Set {.arg na.rm} to TRUE")

# Types and classes
cli_text("Object is {.cls data.frame}")

# Emphasis
cli_text("This is {.emph important}")
cli_text("This is {.strong critical}")

# Fields
cli_text("The {.field name} field is required")

Vector Collapsing

Vectors are automatically collapsed with commas and "and":

pkgs <- c("dplyr", "tidyr", "ggplot2")
cli_text("Installing packages: {.pkg {pkgs}}")
#> Installing packages: dplyr, tidyr, and ggplot2

files <- c("data.csv", "script.R")
cli_text("Found {length(files)} file{?s}: {.file {files}}")
#> Found 2 files: data.csv and script.R

Escaping Braces

Use double braces {{ and }} to escape literal braces:

cli_text("Use {{variable}} syntax in glue")
#> Use {variable} syntax in glue

For complete markup reference: See references/inline-markup.md for all 50+ inline classes, edge cases, nesting rules, and advanced patterns.

Pluralization Basics

Use {?} for pluralization with three patterns:

Single Alternative

nfile <- 1
cli_text("Found {nfile} file{?s}")
#> Found 1 file

nfile <- 3
cli_text("Found {nfile} file{?s}")
#> Found 3 files

Two Alternatives

ndir <- 1
cli_text("Found {ndir} director{?y/ies}")
#> Found 1 directory

ndir <- 5
cli_text("Found {ndir} director{?y/ies}")
#> Found 5 directories

Three Alternatives (zero/one/many)

nfile <- 0
cli_text("Found {nfile} file{?s}: {?no/the/the} file{?s}")
#> Found 0 files: no files

nfile <- 1
cli_text("Found {nfile} file{?s}: {?no/the/the} file{?s}")
#> Found 1 file: the file

nfile <- 3
cli_text("Found {nfile} file{?s}: {?no/the/the} file{?s}")
#> Found 3 files: the files

Helpers: qty() and no()

Use no() to display "no" instead of zero:

nfile <- 0
cli_text("Found {no(nfile)} file{?s}")
#> Found no files

Use qty() to set quantity explicitly:

nupd <- 3
ntotal <- 10
cli_text("{nupd}/{ntotal} {qty(nupd)} file{?s} {?needs/need} updates")
#> 3/10 files need updates

For advanced pluralization: See references/inline-markup.md for edge cases and complex patterns.

CLI Conditions: Core Patterns

Use cli conditions instead of base R for better formatting:

cli_abort() - Formatted Errors

# Before (base R)
stop("File not found: ", path)

# After (cli)
cli_abort("File {.file {path}} not found")

# With bullets for context
check_file <- function(path) {
  if (!file.exists(path)) {
    cli_abort(c(
      "File not found",
      "x" = "Cannot read {.file {path}}",
      "i" = "Check that the file exists"
    ))
  }
}

cli_warn() - Formatted Warnings

# Before (base R)
warning("Column ", col, " has missing values")

# After (cli)
cli_warn("Column {.field {col}} has missing values")

# With context
cli_warn(c(
  "Data quality issues detected",
  "!" = "Column {.field {col}} has {n_missing} missing value{?s}",
  "i" = "Consider using {.fn tidyr::drop_na}"
))

cli_inform() - Formatted Messages

# Before (base R)
message("Processing ", n, " files")

# After (cli)
cli_inform("Processing {n} file{?s}")

# With structure
cli_inform(c(
  "v" = "Successfully loaded {.pkg dplyr}",
  "i" = "Version {packageVersion('dplyr')}"
))

Bullet Types

  • "x" - Error/problem (red X)
  • "!" - Warning (yellow !)
  • "i" - Information (blue i)
  • "v" - Success (green checkmark)
  • "*" - Bullet point
  • ">" - Arrow/pointer

For advanced error design: See references/conditions.md for error design principles, rlang integration, testing strategies, and real-world patterns.

Basic Progress Indicators

Simple Progress Steps

process_data <- function() {
  cli_progress_step("Loading data")
  data <- load_data()

  cli_progress_step("Cleaning data")
  clean <- clean_data(data)

  cli_progress_step("Analyzing data")
  analyze(clean)
}

Basic Progress Bar

process_files <- function(files) {
  cli_progress_bar("Processing files", total = length(files))

  for (file in files) {
    process_file(file)
    cli_progress_update()
  }
}

Auto-Cleanup

Progress bars auto-close when the function exits:

process <- function() {
  cli_progress_bar("Working", total = 100)
  for (i in 1:100) {
    Sys.sleep(0.01)
    cli_progress_update()
  }
  # No need to call cli_progress_done() - auto-closes
}

For advanced progress: See references/progress.md for nested progress, custom formats, parallel processing, all progress variables, and Shiny integration.

Semantic CLI Elements

Headers

cli_h1("Main Section")
cli_h2("Subsection")
cli_h3("Detail")

Alerts

cli_alert_success("Operation completed successfully")
cli_alert_danger("Critical error occurred")
cli_alert_warning("Potential issue detected")
cli_alert_info("Additional information available")

Text and Code

# Regular text with markup
cli_text("This is formatted text with {.emph emphasis}")

# Code blocks
cli_code(c(
  "library(dplyr)",
  "mtcars %>% filter(mpg > 20)"
))

# Verbatim text (no formatting)
cli_verbatim("This is displayed exactly as-is: {not interpolated}")

Lists

# Unordered list
cli_ul()
cli_li("First item")
cli_li("Second item")
cli_end()

# Ordered list
cli_ol()
cli_li("First step")
cli_li("Second step")
cli_end()

# Definition list
cli_dl()
cli_li(c(name = "The name field"))
cli_li(c(email = "The email address"))
cli_end()

Common Workflows

Base R to CLI Migration

# Before: Base R error handling
validate_input <- function(x, y) {
  if (!is.numeric(x)) {
    stop("x must be numeric")
  }
  if (length(y) == 0) {
    stop("y cannot be empty")
  }
  if (length(x) != length(y)) {
    stop("x and y must have the same length")
  }
}

# After: CLI error handling
validate_input <- function(x, y) {
  if (!is.numeric(x)) {
    cli_abort(c(
      "{.arg x} must be numeric",
      "x" = "You supplied a {.cls {class(x)}} vector",
      "i" = "Use {.fn as.numeric} to convert"
    ))
  }

  if (length(y) == 0) {
    cli_abort(c(
      "{.arg y} cannot be empty",
      "i" = "Provide at least one element"
    ))
  }

  if (length(x) != length(y)) {
    cli_abort(c(
      "{.arg x} and {.arg y} must have the same length",
      "x" = "{.arg x} has length {length(x)}",
      "x" = "{.arg y} has length {length(y)}"
    ))
  }
}

Error Message with Rich Context

check_required_columns <- function(data, required_cols) {
  actual_cols <- names(data)
  missing_cols <- setdiff(required_cols, actual_cols)

  if (length(missing_cols) > 0) {
    cli_abort(c(
      "Required column{?s} missing from data",
      "x" = "Missing {length(missing_cols)} column{?s}: {.field {missing_cols}}",
      "i" = "Data has {length(actual_cols)} column{?s}: {.field {actual_cols}}",
      "i" = "Add the missing column{?s} or check for typos"
    ))
  }

  invisible(data)
}

Function with Progress Bar

process_files <- function(files, verbose = TRUE) {
  n <- length(files)

  if (verbose) {
    cli_progress_bar(
      format = "Processing {cli::pb_bar} {cli::pb_current}/{cli::pb_total} [{cli::pb_eta}]",
      total = n
    )
  }

  results <- vector("list", n)

  for (i in seq_along(files)) {
    results[[i]] <- process_file(files[[i]])

    if (verbose) {
      cli_progress_update()
    }
  }

  results
}

Resources & Advanced Topics

Reference Files

  • references/inline-markup.md - Complete catalog of inline classes organized by category, advanced patterns, nesting rules, and real-world examples

  • references/conditions.md - Advanced error design patterns, rlang integration, testing with testthat snapshots, migration guide, and anti-patterns

  • references/progress.md - Nested progress bars, custom formats, all progress variables, parallel processing, Shiny integration, and debugging

  • references/themes.md - Complete theming system with CSS-like selectors, container functions, color palettes, custom themes, and accessibility

  • references/ansi-operations.md - ANSI string operations (align, columns, nchar, etc.), hyperlinks, color detection, testing CLI output, and troubleshooting

External Resources

Related Packages

  • rlang - Condition handling and error objects integrate with cli
  • glue - String interpolation powers cli's {} syntax
  • testthat - Snapshot testing for cli output
Files (skills)
  • references
    • ansi-operations.md 19.3 KB
      # ANSI Operations and Advanced Features
      
      ## Table of Contents
      
      1. [ANSI String Operations](#ansi-string-operations)
         - [Character Counting](#character-counting)
         - [Text Alignment](#text-alignment)
         - [String Splitting](#string-splitting)
         - [Substring Operations](#substring-operations)
         - [Text Wrapping](#text-wrapping)
         - [Case Conversion](#case-conversion)
         - [Whitespace Handling](#whitespace-handling)
         - [Columnar Layout](#columnar-layout)
         - [Removing ANSI Codes](#removing-ansi-codes)
      2. [Hyperlinks](#hyperlinks)
      3. [Advanced Colors](#advanced-colors)
      4. [Symbol Sets](#symbol-sets)
      5. [Testing CLI Output](#testing-cli-output)
      6. [Performance](#performance)
      7. [Debugging](#debugging)
      
      ## ANSI String Operations
      
      All base R string functions break when applied to ANSI-formatted text because they count escape codes as characters. The cli package provides ANSI-aware versions of common string operations.
      
      ### Character Counting
      
      Use `ansi_nchar()` instead of `nchar()` to count visible characters:
      
      ```r
      # Problem with base R
      text <- col_red("hello")
      nchar(text)
      #> [1] 15  # Includes ANSI codes
      
      # Solution with cli
      ansi_nchar(text)
      #> [1] 5  # Counts only visible characters
      
      # Works with vectors
      texts <- c(col_blue("short"), col_green("longer text"))
      ansi_nchar(texts)
      #> [1]  5 11
      
      # type argument works like base nchar()
      ansi_nchar(text, type = "width")  # Display width
      ansi_nchar(text, type = "chars")  # Character count
      ansi_nchar(text, type = "bytes")  # Byte count
      ```
      
      ### Text Alignment
      
      Use `ansi_align()` to align formatted text while accounting for ANSI codes:
      
      ```r
      # Left alignment (default)
      texts <- c(col_red("Error"), col_yellow("Warning"), col_green("OK"))
      ansi_align(texts, width = 10)
      #> [1] "Error     " "Warning   " "OK        "
      
      # Right alignment
      ansi_align(texts, width = 10, align = "right")
      #> [1] "     Error" "   Warning" "        OK"
      
      # Center alignment
      ansi_align(texts, width = 10, align = "center")
      #> [1] "  Error   " " Warning  " "    OK    "
      
      # Practical example: aligned status messages
      statuses <- c(col_green("Success"), col_red("Failed"), col_yellow("Pending"))
      labels <- c("Database", "API", "Cache")
      paste0(labels, ": ", ansi_align(statuses, width = 10))
      #> [1] "Database: Success   " "API: Failed     " "Cache: Pending   "
      ```
      
      ### String Splitting
      
      Use `ansi_strsplit()` instead of `strsplit()`:
      
      ```r
      # Split colored text
      text <- paste0(col_red("hello"), " ", col_blue("world"))
      ansi_strsplit(text, " ")
      #> [[1]]
      #> [1] "\033[31mhello\033[39m" "\033[34mworld\033[39m"
      
      # Fixed strings
      ansi_strsplit(col_green("a-b-c"), "-", fixed = TRUE)
      #> [[1]]
      #> [1] "\033[32ma\033[39m" "\033[32mb\033[39m" "\033[32mc\033[39m"
      
      # Regular expressions
      text <- col_red("one123two456three")
      ansi_strsplit(text, "[0-9]+")
      #> [[1]]
      #> [1] "\033[31mone\033[39m"   "\033[31mtwo\033[39m"   "\033[31mthree\033[39m"
      ```
      
      ### Substring Operations
      
      Use `ansi_substr()` and `ansi_substring()` for extracting substrings:
      
      ```r
      # Extract substring
      text <- col_red("hello world")
      ansi_substr(text, 1, 5)
      #> [1] "\033[31mhello\033[39m"  # Preserves color
      
      ansi_substr(text, 7, 11)
      #> [1] "\033[31mworld\033[39m"
      
      # Negative indices work like base R
      ansi_substring(text, 7)  # From position 7 to end
      #> [1] "\033[31mworld\033[39m"
      
      # Substring replacement
      text <- col_blue("hello world")
      ansi_substr(text, 1, 5) <- "HELLO"
      text
      #> [1] "\033[34mHELLO world\033[39m"
      
      # Multiple strings
      texts <- c(col_red("abcdef"), col_green("123456"))
      ansi_substr(texts, 2, 4)
      #> [1] "\033[31mbcd\033[39m" "\033[32m234\033[39m"
      ```
      
      ### Text Wrapping
      
      Use `ansi_strwrap()` for wrapping formatted text:
      
      ```r
      # Basic wrapping
      long_text <- paste0(
        col_blue("This is a long piece of text "),
        col_red("that needs to be wrapped "),
        col_green("across multiple lines")
      )
      ansi_strwrap(long_text, width = 30)
      #> [1] "This is a long piece of text"
      #> [2] "that needs to be wrapped"
      #> [3] "across multiple lines"
      
      # Custom indent and exdent
      ansi_strwrap(long_text, width = 40, indent = 2, exdent = 4)
      #> First line indented by 2, subsequent by 4
      
      # Simplify option
      ansi_strwrap(c(col_red("short"), col_blue("text")), simplify = FALSE)
      #> Returns a list
      
      # Practical example: formatted help text
      help_text <- paste0(
        col_bold("--verbose"), " ",
        "Enable verbose output with detailed logging information"
      )
      cat(ansi_strwrap(help_text, width = 60), sep = "\n")
      ```
      
      ### Case Conversion
      
      Use `ansi_toupper()`, `ansi_tolower()`, and `ansi_chartr()`:
      
      ```r
      # Convert to uppercase
      text <- col_red("hello world")
      ansi_toupper(text)
      #> [1] "\033[31mHELLO WORLD\033[39m"
      
      # Convert to lowercase
      text <- col_blue("HELLO WORLD")
      ansi_tolower(text)
      #> [1] "\033[34mhello world\033[39m"
      
      # Character translation
      ansi_chartr("aeiou", "AEIOU", col_green("hello world"))
      #> [1] "\033[32mhEllO wOrld\033[39m"
      
      # Preserves all formatting
      text <- paste0(col_red(style_bold("error")), ": ", col_blue("message"))
      ansi_toupper(text)
      #> Colors and styles preserved
      ```
      
      ### Whitespace Handling
      
      Use `ansi_trimws()` to trim whitespace from ANSI strings:
      
      ```r
      # Trim both sides (default)
      text <- col_red("  hello world  ")
      ansi_trimws(text)
      #> [1] "\033[31mhello world\033[39m"
      
      # Trim left only
      ansi_trimws(text, which = "left")
      #> [1] "\033[31mhello world  \033[39m"
      
      # Trim right only
      ansi_trimws(text, which = "right")
      #> [1] "\033[31m  hello world\033[39m"
      
      # Custom whitespace definition
      ansi_trimws(col_blue("..hello.."), whitespace = ".")
      #> [1] "\033[34mhello\033[39m"
      ```
      
      ### Columnar Layout
      
      Use `ansi_columns()` to create column layouts with ANSI-formatted text:
      
      ```r
      # Simple two-column layout
      items <- paste0(col_blue(letters[1:10]), " = ", col_green(1:10))
      ansi_columns(items, width = 40)
      #> Arranges items in columns that fit in 40 characters
      
      # Custom number of columns
      ansi_columns(items, width = 60, fill = "cols")
      #> fill = "cols" fills by columns, "rows" fills by rows
      
      # Practical example: displaying options
      options <- paste0(
        col_yellow(sprintf("--option%d", 1:20)),
        " ",
        col_grey("Description text")
      )
      ansi_columns(options, width = 80)
      ```
      
      ### Removing ANSI Codes
      
      Use `ansi_strip()` to remove all ANSI formatting:
      
      ```r
      # Remove all ANSI codes
      text <- col_red(style_bold("error"))
      ansi_strip(text)
      #> [1] "error"
      
      # Works with complex formatting
      text <- paste0(
        col_blue("Status: "),
        col_green(style_underline("OK"))
      )
      ansi_strip(text)
      #> [1] "Status: OK"
      
      # Essential for testing (see Testing CLI Output section)
      expect_equal(ansi_strip(my_output()), "expected plain text")
      ```
      
      ## Hyperlinks
      
      Modern terminals support hyperlinks via ANSI escape codes. The cli package provides several link types through inline markup.
      
      ### Terminal Support Detection
      
      Check if the current terminal supports hyperlinks:
      
      ```r
      # Check for hyperlink support
      style_hyperlink_supported()
      #> [1] TRUE or FALSE
      
      # Hyperlinks are auto-disabled when not supported
      # Always safe to use in code
      ```
      
      ### Link Types Overview
      
      ```r
      # URL links - external websites
      cli_text("See {.url https://example.com}")
      
      # URL with custom text
      cli_text("Visit our {.href [website](https://example.com)}")
      
      # File links
      cli_text("Check {.file /path/to/file.R}")
      
      # File with line and column
      cli_text("Error at {.file /path/to/file.R:42:10}")
      
      # Function documentation
      cli_text("Use {.fun package::function}")
      
      # Help topic
      cli_text("See {.help topic}")
      
      # Topic in package
      cli_text("Read {.topic stats::lm}")
      
      # Vignette
      cli_text("Tutorial: {.vignette dplyr::introduction}")
      
      # Executable code
      cli_text("Try {.run code_to_execute()}")
      ```
      
      ### .url vs .href - When to Use Each
      
      **Use `.url` when:**
      - The URL is the meaningful content
      - Showing the full URL is important
      - The link destination should be visible
      
      ```r
      cli_text("Documentation: {.url https://cli.r-lib.org}")
      cli_text("API endpoint: {.url https://api.example.com/v1}")
      ```
      
      **Use `.href` when:**
      - The link text should differ from the URL
      - Creating natural prose with embedded links
      - The URL is long or technical
      
      ```r
      cli_text("Read the {.href [complete guide](https://very-long-url.com/path)}")
      cli_text("See {.href [issue #123](https://github.com/org/repo/issues/123)}")
      ```
      
      ### .file with Line and Column Syntax
      
      ```r
      # Just the file
      cli_text("Modified {.file script.R}")
      
      # File with line number
      cli_text("Error in {.file script.R:42}")
      
      # File with line and column
      cli_text("Syntax error at {.file script.R:42:10}")
      
      # Full path with line numbers
      cli_text("See {.file /home/user/project/R/utils.R:100:5}")
      
      # Practical example in error messages
      check_syntax <- function(file, line, col) {
        cli_abort(c(
          "Syntax error detected",
          "x" = "Unexpected token at {.file {file}:{line}:{col}}",
          "i" = "Check for matching braces"
        ))
      }
      ```
      
      ### Documentation Links
      
      ```r
      # Function help - opens help page
      cli_text("Calculate mean with {.fun mean}")
      cli_text("Join tables using {.fun dplyr::left_join}")
      
      # Help topic
      cli_text("Learn about {.help vectors}")
      
      # Topic from specific package
      cli_text("Read about {.topic dplyr::mutate}")
      
      # Vignette
      cli_text("Tutorial: {.vignette dplyr::window-functions}")
      cli_text("Guide: {.vignette tidyr::pivot}")
      
      # Practical example: suggesting functions
      suggest_function <- function() {
        cli_inform(c(
          "i" = "For string manipulation, try {.fun stringr::str_replace}",
          "i" = "See {.topic stringr::str_replace} for examples",
          "i" = "Learn more: {.vignette stringr::stringr}"
        ))
      }
      ```
      
      ### .run Links - Executable Code
      
      **Security Warning:** `.run` links execute arbitrary code when clicked. Use with extreme caution.
      
      ```r
      # Simple command
      cli_text("Install with {.run install.packages('cli')}")
      
      # Multiple statements
      cli_text("Setup: {.run source('setup.R'); init_project()}")
      
      # Display differs from execution
      cli_text("{.run [reset database](drop_all_tables(); rebuild())}")
      
      # Safe use cases only:
      # - Your own diagnostic commands
      # - Read-only operations
      # - Well-understood helper functions
      
      # NEVER use .run with:
      # - User-supplied input
      # - File system modifications from untrusted sources
      # - Network operations
      # - Data deletion
      ```
      
      ### Practical Hyperlink Examples
      
      ```r
      # Error with file link
      file_error <- function(path, line) {
        cli_abort(c(
          "Parse error in configuration file",
          "x" = "Invalid syntax at {.file {path}:{line}}",
          "i" = "Check the {.href [YAML specification](https://yaml.org/spec/)}"
        ))
      }
      
      # Function suggestion with documentation
      suggest_alternative <- function(old_fn, new_fn) {
        cli_warn(c(
          "{.fun {old_fn}} is deprecated",
          "!" = "Use {.fun {new_fn}} instead",
          "i" = "See {.topic {new_fn}} for details"
        ))
      }
      
      # Help message with links
      show_resources <- function() {
        cli_inform(c(
          "v" = "Package installed successfully",
          "i" = "Documentation: {.url https://pkg.example.com}",
          "i" = "Quick start: {.vignette mypkg::quickstart}",
          "i" = "Get help: {.run help('mypkg')}"
        ))
      }
      ```
      
      ## Advanced Colors
      
      ### Color Palette Customization
      
      Beyond basic colors, cli supports extensive palette customization:
      
      ```r
      # Create custom color palette
      my_palette <- list(
        error = "#FF5555",
        warn = "#FFB86C",
        success = "#50FA7B",
        info = "#8BE9FD"
      )
      
      # Apply custom theme
      my_theme <- list(
        span.error = list(color = my_palette$error),
        span.warn = list(color = my_palette$warn),
        span.success = list(color = my_palette$success),
        span.info = list(color = my_palette$info)
      )
      
      cli_div(theme = my_theme)
      ```
      
      ### Terminal Capability Detection
      
      ```r
      # Detect number of colors supported
      num_ansi_colors()
      #> Returns: 1 (no colors), 8, 256, or 16777216 (truecolor)
      
      # Check capabilities
      if (num_ansi_colors() >= 256) {
        # Use 256-color palette
        style_rgb(100, 150, 200)
      } else if (num_ansi_colors() >= 8) {
        # Fall back to 8-color palette
        col_blue()
      } else {
        # No colors, use plain text
        identity()
      }
      ```
      
      ### Color Mode Details
      
      **Truecolor (16.7M colors):**
      ```r
      # num_ansi_colors() returns 16777216
      # Full RGB color space available
      make_ansi_style("#FF5733")  # Hex colors
      style_rgb(255, 87, 51)       # RGB values
      ```
      
      **256-color mode:**
      ```r
      # num_ansi_colors() returns 256
      # 216 colors + 24 grayscale
      # Colors approximated from truecolor
      ```
      
      **8-color mode:**
      ```r
      # num_ansi_colors() returns 8
      # Basic ANSI colors only: black, red, green, yellow,
      # blue, magenta, cyan, white
      ```
      
      **No color (1):**
      ```r
      # num_ansi_colors() returns 1
      # All styling removed
      # Useful for piping to files or non-terminal output
      ```
      
      ### Custom Color Functions
      
      ```r
      # Define reusable color functions
      error_color <- make_ansi_style("#FF5555", bg = FALSE)
      success_color <- make_ansi_style("#50FA7B", bg = FALSE)
      highlight_bg <- make_ansi_style("#FFFF00", bg = TRUE)
      
      # Use in messages
      cli_text("Status: {error_color('Failed')}")
      cli_text("Result: {success_color('Success')}")
      cli_text("{highlight_bg('Important')}")
      
      # Combine styles
      emphasize <- combine_ansi_styles(
        make_ansi_style("#FF5555"),
        style_bold
      )
      cli_text("{emphasize('Critical warning')}")
      ```
      
      ## Symbol Sets
      
      The cli package provides Unicode symbols with automatic ASCII fallback.
      
      ### Symbol Behavior
      
      ```r
      # Unicode symbols in capable terminals
      symbol$tick           # ✔
      symbol$cross          # ✖
      symbol$arrow_right    # →
      symbol$ellipsis       # …
      symbol$warning        # ⚠
      
      # Automatic ASCII fallback when needed
      # Environment: NO_UNICODE=1 or incapable terminal
      symbol$tick           # v
      symbol$cross          # x
      symbol$arrow_right    # ->
      symbol$ellipsis       # ...
      symbol$warning        # !
      ```
      
      ### Available Symbols
      
      ```r
      # Status indicators
      symbol$tick            # ✔ or v
      symbol$cross           # ✖ or x
      symbol$circle_filled   # ● or (*)
      symbol$circle_dotted   # ◌ or ( )
      
      # Arrows
      symbol$arrow_right     # → or ->
      symbol$arrow_left      # ← or <-
      symbol$arrow_up        # ↑ or ^
      symbol$arrow_down      # ↓ or v
      
      # UI elements
      symbol$ellipsis        # … or ...
      symbol$continue        # ⋯ or ...
      symbol$warning         # ⚠ or !
      symbol$info            # ℹ or i
      
      # Pointers
      symbol$pointer         # ❯ or >
      symbol$radio_on        # ◉ or (*)
      symbol$radio_off       # ◯ or ( )
      
      # Box drawing (for tables/borders)
      symbol$line            # ─ or -
      symbol$double_line     # ═ or =
      ```
      
      ### Practical Symbol Usage
      
      ```r
      # Status messages
      cli_alert("Processing {symbol$ellipsis}")
      cli_text("{symbol$tick} Done")
      cli_text("{symbol$cross} Failed")
      
      # Lists with custom bullets
      cli_text("{symbol$pointer} Option 1")
      cli_text("{symbol$pointer} Option 2")
      
      # Progress indicators
      cli_text("Step 1 {symbol$arrow_right} Step 2 {symbol$arrow_right} Step 3")
      ```
      
      ## Testing CLI Output
      
      ### Basic Testing with ansi_strip()
      
      ```r
      test_that("function produces correct message", {
        # Capture output
        output <- capture.output({
          cli_alert_success("Operation complete")
        })
      
        # Strip ANSI codes for comparison
        plain <- ansi_strip(paste(output, collapse = "\n"))
      
        expect_match(plain, "Operation complete")
      })
      ```
      
      ### Snapshot Testing with testthat
      
      Best practice for testing CLI output:
      
      ```r
      test_that("error message is correct", {
        # Snapshot the entire formatted output
        expect_snapshot(error = TRUE, {
          my_function_that_errors()
        })
      })
      
      test_that("progress output is correct", {
        # Use local options for reproducible output
        withr::local_options(cli.width = 80)
      
        expect_snapshot({
          my_function_with_progress()
        })
      })
      ```
      
      ### Testing in Non-Interactive Mode
      
      ```r
      test_that("cli works non-interactively", {
        # Simulate non-interactive environment
        withr::local_options(cli.dynamic = FALSE)
      
        output <- capture.output({
          cli_progress_bar("Working", total = 100)
          for (i in 1:100) cli_progress_update()
        })
      
        # No progress bar in non-interactive mode
        expect_length(output, 0)
      })
      ```
      
      ### Using test_that_cli()
      
      Special testing helper for CLI output:
      
      ```r
      test_that_cli("formatted message appears", {
        # Automatically sets up CLI testing environment
        # - Fixed width (80 columns)
        # - ANSI colors enabled
        # - Unicode symbols enabled
      
        expect_snapshot({
          cli_h1("Header")
          cli_alert_success("Done")
        })
      })
      
      # Equivalent to:
      test_that("formatted message appears", {
        withr::local_options(
          cli.width = 80,
          cli.num_colors = 256,
          cli.unicode = TRUE
        )
      
        expect_snapshot({
          cli_h1("Header")
          cli_alert_success("Done")
        })
      })
      ```
      
      ### Transform for Reproducible Tests
      
      ```r
      test_that("output is stable", {
        expect_snapshot(
          my_function_with_timestamps(),
          transform = function(output) {
            # Remove variable elements
            output <- ansi_strip(output)
            output <- gsub("\\d{4}-\\d{2}-\\d{2}", "[DATE]", output)
            output <- gsub("\\d+\\.\\d+ seconds", "[TIME]", output)
            output
          }
        )
      })
      ```
      
      ## Performance
      
      ### When CLI Adds Overhead
      
      CLI has minimal overhead in most cases, but be aware of:
      
      **High-frequency operations:**
      ```r
      # Avoid CLI in tight loops
      for (i in 1:1000000) {
        cli_alert("Iteration {i}")  # Very slow!
      }
      
      # Instead: use progress bar
      cli_progress_bar("Processing", total = 1000000)
      for (i in 1:1000000) {
        # work
        if (i %% 1000 == 0) cli_progress_update()
      }
      ```
      
      **String interpolation cost:**
      ```r
      # Expensive: interpolation on every call
      for (i in 1:10000) {
        msg <- cli::cli_fmt(cli::cli_text("Value: {i}"))
      }
      
      # Cheaper: use sprintf or paste for simple cases
      for (i in 1:10000) {
        msg <- sprintf("Value: %d", i)
      }
      ```
      
      ### Optimization Strategies
      
      **Disable in production:**
      ```r
      # Option to disable all CLI output
      options(cli.default_handler = function(...) NULL)
      
      # Or use environment variable
      Sys.setenv(CLI_NO_OUTPUT = "true")
      ```
      
      **Batch updates:**
      ```r
      # Bad: update on every item
      cli_progress_bar("Processing", total = 1000000)
      for (i in 1:1000000) {
        process(i)
        cli_progress_update()  # Too frequent
      }
      
      # Good: batch updates
      cli_progress_bar("Processing", total = 1000000)
      for (i in 1:1000000) {
        process(i)
        if (i %% 100 == 0) cli_progress_update(inc = 100)
      }
      ```
      
      **Conditional verbosity:**
      ```r
      process_data <- function(data, verbose = TRUE) {
        if (verbose) {
          cli_progress_bar("Processing", total = nrow(data))
        }
      
        for (i in seq_len(nrow(data))) {
          # work
          if (verbose) cli_progress_update()
        }
      }
      ```
      
      ## Debugging
      
      ### CLI Internal State
      
      ```r
      # Check current CLI state
      cli::cli_status()
      #> Shows active containers, progress bars, themes
      
      # Debug theme application
      options(cli.debug = TRUE)
      cli_alert("Test")
      #> Shows theme resolution and style application
      ```
      
      ### Troubleshooting Techniques
      
      **Colors not appearing:**
      ```r
      # Check color support
      cli::num_ansi_colors()
      #> Should be > 1
      
      # Force colors on
      options(cli.num_colors = 256)
      
      # Check if output is to terminal
      isatty(stdout())
      #> Should be TRUE for interactive colors
      ```
      
      **Hyperlinks not working:**
      ```r
      # Check hyperlink support
      cli::style_hyperlink_supported()
      
      # Force enable for testing
      options(cli.hyperlink = TRUE)
      
      # Test with explicit hyperlink
      writeLines(style_hyperlink("test", "https://example.com"))
      ```
      
      **Unicode symbols showing as boxes:**
      ```r
      # Check Unicode support
      l10n_info()$`UTF-8`
      #> Should be TRUE
      
      # Force ASCII fallback
      options(cli.unicode = FALSE)
      
      # Or use environment variable
      Sys.setenv(CLI_NO_UNICODE = "true")
      ```
      
      **Progress bars not updating:**
      ```r
      # Check if output is buffered
      # Progress requires unbuffered output
      flush.console()  # Force output after updates
      
      # Check for interactive mode
      interactive()
      #> Should be TRUE for dynamic progress
      
      # Test with forced dynamic mode
      options(cli.dynamic = TRUE)
      ```
      
      **Debugging custom themes:**
      ```r
      # Validate theme structure
      my_theme <- list(
        span.error = list(color = "red", "font-weight" = "bold")
      )
      
      cli_div(theme = my_theme)
      cli_text("Test {.error message}")
      
      # Check resolved styles
      options(cli.debug = TRUE)
      # Shows which selectors match and final styles
      ```
      
    • conditions.md 18.4 KB
      # CLI Conditions Reference
      
      ## Table of Contents
      
      1. [CLI Conditions Overview](#cli-conditions-overview)
      2. [Error Design Principles](#error-design-principles)
      3. [cli_abort() Deep Dive](#cli_abort-deep-dive)
      4. [cli_warn() Patterns](#cli_warn-patterns)
      5. [cli_inform() Patterns](#cli_inform-patterns)
      6. [Testing CLI Conditions](#testing-cli-conditions)
      7. [Migration Guide](#migration-guide)
      8. [Real-World Examples](#real-world-examples)
      9. [Anti-Patterns](#anti-patterns)
      
      ## CLI Conditions Overview
      
      CLI conditions (cli_abort(), cli_warn(), cli_inform()) provide formatted alternatives to base R's stop(), warning(), and message(). They offer:
      
      **Key Benefits:**
      
      - **Inline markup** - Format code, paths, variables, and values with semantic meaning
      - **Structured output** - Use bullet lists to organize problem statements, context, and solutions
      - **Automatic styling** - Colors, icons, and formatting are applied consistently
      - **Better readability** - Multi-line messages are easier to scan and understand
      - **rlang integration** - Seamless integration with structured error handling via rlang
      
      **When to Use CLI Conditions:**
      
      ```r
      # Use cli_abort() for errors that stop execution
      cli_abort("Cannot proceed: {.file {path}} is missing")
      
      # Use cli_warn() for warnings about potential issues
      cli_warn("Column {.field {col}} has {n} missing value{?s}")
      
      # Use cli_inform() for informative messages
      cli_inform("Successfully processed {n} record{?s}")
      ```
      
      ## Error Design Principles
      
      Good error messages follow these principles:
      
      ### 1. Clear Problem Statement
      
      State what went wrong in plain language:
      
      ```r
      # Bad - Technical jargon
      cli_abort("NULL pointer in slot `data`")
      
      # Good - Clear statement
      cli_abort("Dataset is missing")
      ```
      
      ### 2. Actionable Solutions
      
      Tell users how to fix the problem:
      
      ```r
      validate_email <- function(email) {
        if (!grepl("@", email)) {
          cli_abort(c(
            "Invalid email address",
            "x" = "{.val {email}} is not a valid email",
            "i" = "Email must contain an @ symbol"
          ))
        }
      }
      ```
      
      ### 3. Context via Bullet Lists
      
      Use bullets to structure information hierarchically:
      
      ```r
      check_dimensions <- function(x, y) {
        if (length(x) != length(y)) {
          cli_abort(c(
            "Incompatible vector lengths",
            "x" = "{.arg x} has length {length(x)}",
            "x" = "{.arg y} has length {length(y)}",
            "i" = "Both vectors must have the same length"
          ))
        }
      }
      ```
      
      ### 4. Caller Information
      
      Use the `call` argument to show where the error occurred:
      
      ```r
      # Default - shows the function where cli_abort() is called
      validate <- function(x) {
        cli_abort("Invalid input")  # Error in: validate(x)
      }
      
      # Explicit - control the call shown in error
      validate <- function(x) {
        cli_abort("Invalid input", call = caller_env())
      }
      
      # Suppress - don't show any call
      validate <- function(x) {
        cli_abort("Invalid input", call = NULL)
      }
      ```
      
      ## cli_abort() Deep Dive
      
      ### Basic Usage
      
      ```r
      # Simple message
      cli_abort("Something went wrong")
      
      # With inline markup
      cli_abort("Cannot find file {.file {path}}")
      
      # With multiple elements
      cli_abort(c(
        "Operation failed",
        "i" = "Additional context here"
      ))
      ```
      
      ### Bullet Types
      
      Each bullet type has semantic meaning and visual styling:
      
      **`"x"` - Error/Problem (red X):**
      
      ```r
      cli_abort(c(
        "Validation failed",
        "x" = "File {.file data.csv} does not exist",
        "x" = "Directory {.path /tmp/output} is not writable"
      ))
      ```
      
      **`"i"` - Information (blue i):**
      
      ```r
      cli_abort(c(
        "Invalid argument type",
        "x" = "{.arg x} must be numeric",
        "i" = "You supplied a {.cls {class(x)}} object",
        "i" = "Use {.fn as.numeric} to convert"
      ))
      ```
      
      **`"v"` - Success context (green checkmark):**
      
      ```r
      cli_abort(c(
        "Partial operation failure",
        "v" = "Successfully processed {n_success} file{?s}",
        "x" = "Failed to process {n_failed} file{?s}",
        "i" = "See {.file error.log} for details"
      ))
      ```
      
      **`"*"` - Bullet point:**
      
      ```r
      cli_abort(c(
        "Invalid configuration",
        "x" = "Missing required fields in config file",
        "i" = "Required fields:",
        "*" = "{.field name}",
        "*" = "{.field version}",
        "*" = "{.field author}"
      ))
      ```
      
      **`">"` - Arrow/Pointer:**
      
      ```r
      cli_abort(c(
        "Database connection failed",
        "x" = "Cannot connect to {.val {host}}:{.val {port}}",
        "i" = "Troubleshooting steps:",
        ">" = "Check that the server is running",
        ">" = "Verify credentials in {.file .env}",
        ">" = "Ensure firewall allows port {.val {port}}"
      ))
      ```
      
      ### Named vs Unnamed Elements
      
      **Unnamed elements** are treated as headers or main messages:
      
      ```r
      cli_abort(c(
        "This is the main error message",
        "This is a second header line"
      ))
      ```
      
      **Named elements** get bullet icons:
      
      ```r
      cli_abort(c(
        "Main message",
        "x" = "Problem description",
        "i" = "Helpful information"
      ))
      ```
      
      ### rlang Integration
      
      CLI works seamlessly with rlang's structured error handling:
      
      **Error Classes:**
      
      ```r
      validate_user <- function(user) {
        if (is.null(user$id)) {
          cli_abort(
            "User ID is required",
            class = "validation_error"
          )
        }
      }
      
      # Catch specific error class
      tryCatch(
        validate_user(list()),
        validation_error = function(e) {
          # Handle validation errors specifically
        }
      )
      ```
      
      **Parent Errors (Error Chaining):**
      
      ```r
      load_data <- function(path) {
        tryCatch(
          read.csv(path),
          error = function(e) {
            cli_abort(
              c(
                "Failed to load data",
                "i" = "Attempted to read from {.file {path}}"
              ),
              parent = e
            )
          }
        )
      }
      ```
      
      **Multiple Error Classes:**
      
      ```r
      cli_abort(
        "Invalid input",
        class = c("invalid_input", "user_error")
      )
      ```
      
      ### Call Specification Patterns
      
      **Pattern 1: Default behavior (show internal function):**
      
      ```r
      helper <- function(x) {
        cli_abort("Invalid x")
      }
      
      my_function <- function(x) {
        helper(x)
      }
      
      # Error: in `helper()`
      my_function(NULL)
      ```
      
      **Pattern 2: Show caller's context:**
      
      ```r
      helper <- function(x, call = caller_env()) {
        cli_abort("Invalid x", call = call)
      }
      
      my_function <- function(x) {
        helper(x)
      }
      
      # Error: in `my_function()`
      my_function(NULL)
      ```
      
      **Pattern 3: Suppress call entirely:**
      
      ```r
      helper <- function(x) {
        cli_abort("Invalid x", call = NULL)
      }
      
      # Error: (no function context shown)
      helper(NULL)
      ```
      
      **Pattern 4: Custom call:**
      
      ```r
      validate <- function(x) {
        cli_abort("Invalid", call = quote(custom_function()))
      }
      
      # Error: in `custom_function()`
      ```
      
      ### Interpolation and Evaluation
      
      CLI evaluates expressions in the calling environment:
      
      ```r
      check_file <- function(path) {
        size <- file.size(path)
      
        cli_abort(c(
          "File too large",
          "x" = "{.file {path}} is {size} bytes",
          "i" = "Maximum size is {.val {1e6}} bytes"
        ))
      }
      ```
      
      **Escaping braces:**
      
      ```r
      # Use double braces to show literal braces
      cli_abort("Use {{variable}} syntax in glue")
      #> Error: Use {variable} syntax in glue
      ```
      
      ## cli_warn() Patterns
      
      Warnings indicate potential problems that don't stop execution:
      
      ### Basic Warnings
      
      ```r
      # Simple warning
      cli_warn("Deprecated function")
      
      # With context
      cli_warn(c(
        "Deprecated function",
        "!" = "{.fn old_function} is deprecated",
        "i" = "Use {.fn new_function} instead"
      ))
      ```
      
      ### Deprecation Warnings
      
      ```r
      old_function <- function(x) {
        cli_warn(c(
          "{.fn old_function} is deprecated",
          "i" = "Use {.fn new_function} instead",
          "i" = "See {.url https://example.com/migration} for migration guide"
        ))
      
        # Function implementation
      }
      ```
      
      ### Data Quality Warnings
      
      ```r
      clean_data <- function(data) {
        missing_counts <- sapply(data, function(x) sum(is.na(x)))
        cols_with_missing <- names(missing_counts[missing_counts > 0])
      
        if (length(cols_with_missing) > 0) {
          cli_warn(c(
            "Missing values detected",
            "!" = "Column{?s} with missing values: {.field {cols_with_missing}}",
            "i" = "Consider using {.fn tidyr::drop_na} or {.fn tidyr::fill}"
          ))
        }
      
        data
      }
      ```
      
      ### Configuration Warnings
      
      ```r
      load_config <- function(path) {
        config <- read_config(path)
      
        if (is.null(config$timeout)) {
          cli_warn(c(
            "Missing configuration value",
            "!" = "{.field timeout} not specified in {.file {path}}",
            "i" = "Using default value of {.val 30} seconds"
          ))
          config$timeout <- 30
        }
      
        config
      }
      ```
      
      ### Once Per Session Warnings
      
      ```r
      experimental_feature <- function() {
        cli_warn(
          c(
            "Experimental feature",
            "!" = "This function is experimental and may change",
            "i" = "Use at your own risk"
          ),
          .frequency = "once",
          .frequency_id = "experimental_feature_warning"
        )
      
        # Implementation
      }
      ```
      
      ## cli_inform() Patterns
      
      Informative messages provide feedback without indicating problems:
      
      ### Progress Updates
      
      ```r
      process_data <- function(data) {
        cli_inform("Starting data processing")
      
        # Processing steps...
      
        cli_inform(c(
          "v" = "Successfully processed {nrow(data)} row{?s}",
          "i" = "Output saved to {.file results.csv}"
        ))
      }
      ```
      
      ### Startup Messages
      
      ```r
      .onAttach <- function(libname, pkgname) {
        cli_inform(c(
          "v" = "Loaded {.pkg mypackage} version {packageVersion('mypackage')}",
          "i" = "Use {.fn ?mypackage} for help"
        ))
      }
      ```
      
      ### Verbose Mode Information
      
      ```r
      analyze <- function(data, verbose = TRUE) {
        if (verbose) {
          cli_inform("Analyzing {nrow(data)} observation{?s}")
        }
      
        result <- expensive_computation(data)
      
        if (verbose) {
          cli_inform(c(
            "v" = "Analysis complete",
            "i" = "Found {result$n_groups} group{?s}",
            "i" = "Mean value: {.val {round(result$mean, 2)}}"
          ))
        }
      
        result
      }
      ```
      
      ### Informative vs Progress
      
      **Use cli_inform() for:**
      - One-time status updates
      - Package startup messages
      - Final results or summaries
      - Debug/verbose output
      
      **Use cli_progress_*() for:**
      - Loops or iterations
      - Long-running operations
      - Operations with known total count
      - Real-time progress tracking
      
      ```r
      # Good - use inform for one-time messages
      process <- function(data) {
        cli_inform("Preprocessing data")
        data <- preprocess(data)
      
        # Good - use progress for iteration
        cli_progress_bar("Processing rows", total = nrow(data))
        for (i in seq_len(nrow(data))) {
          process_row(data[i, ])
          cli_progress_update()
        }
      
        cli_inform("v" = "Processing complete")
      }
      ```
      
      ## Testing CLI Conditions
      
      ### Snapshot Testing
      
      Use testthat's snapshot tests to verify condition messages:
      
      ```r
      test_that("validation errors are clear", {
        expect_snapshot(error = TRUE, {
          validate_email("")
          validate_email("not-an-email")
          validate_email("user@example.com@extra")
        })
      })
      ```
      
      Snapshot file (`tests/testthat/_snaps/validation.md`):
      
      ```md
      # validation errors are clear
      
          Code
            validate_email("")
          Error <rlang_error>
            Invalid email address
            x "" is not a valid email
            i Email must contain an @ symbol
      
          Code
            validate_email("not-an-email")
          Error <rlang_error>
            Invalid email address
            x "not-an-email" is not a valid email
            i Email must contain an @ symbol
      ```
      
      ### Testing Bullet Formatting
      
      ```r
      test_that("error messages show proper context", {
        expect_snapshot(error = TRUE, {
          check_dimensions(1:3, 1:5)
        })
      })
      ```
      
      ### Testing Pluralization
      
      ```r
      test_that("pluralization works in errors", {
        expect_snapshot(error = TRUE, {
          report_missing(c("file1.txt"))          # singular
          report_missing(c("file1.txt", "file2.txt"))  # plural
        })
      })
      ```
      
      ### Testing Warning Frequency
      
      ```r
      test_that("deprecation warning shown once per session", {
        # Clear warning registry
        assign("last_shown", NULL, envir = rlang::ns_env("cli"))
      
        # First call shows warning
        expect_warning(old_function(1), "deprecated")
      
        # Second call does not (with frequency = "once")
        expect_no_warning(old_function(2))
      })
      ```
      
      ### Testing Condition Classes
      
      ```r
      test_that("errors have correct classes", {
        expect_error(
          validate_user(list()),
          class = "validation_error"
        )
      
        err <- tryCatch(
          validate_user(list()),
          error = function(e) e
        )
      
        expect_s3_class(err, c("validation_error", "rlang_error"))
      })
      ```
      
      ### Mocking for Error Testing
      
      ```r
      test_that("handles missing file gracefully", {
        local_mocked_bindings(
          file.exists = function(path) FALSE
        )
      
        expect_snapshot(error = TRUE, {
          load_dataset("missing.csv")
        })
      })
      ```
      
      ## Migration Guide
      
      ### Base R to CLI: stop() to cli_abort()
      
      **Before:**
      
      ```r
      validate <- function(x, y) {
        if (!is.numeric(x)) {
          stop("x must be numeric")
        }
        if (length(x) != length(y)) {
          stop("x and y must have the same length")
        }
      }
      ```
      
      **After:**
      
      ```r
      validate <- function(x, y) {
        if (!is.numeric(x)) {
          cli_abort(c(
            "{.arg x} must be numeric",
            "x" = "You supplied a {.cls {class(x)}} object"
          ))
        }
      
        if (length(x) != length(y)) {
          cli_abort(c(
            "{.arg x} and {.arg y} must have the same length",
            "x" = "{.arg x} has length {length(x)}",
            "x" = "{.arg y} has length {length(y)}"
          ))
        }
      }
      ```
      
      ### Base R to CLI: warning() to cli_warn()
      
      **Before:**
      
      ```r
      process <- function(data) {
        if (any(is.na(data))) {
          warning("Data contains missing values")
        }
      }
      ```
      
      **After:**
      
      ```r
      process <- function(data) {
        if (any(is.na(data))) {
          n_missing <- sum(is.na(data))
          cli_warn(c(
            "Data contains missing values",
            "!" = "Found {n_missing} missing value{?s}",
            "i" = "Consider imputation or removal"
          ))
        }
      }
      ```
      
      ### Base R to CLI: message() to cli_inform()
      
      **Before:**
      
      ```r
      load_data <- function(path) {
        message("Loading data from ", path)
        data <- read.csv(path)
        message("Loaded ", nrow(data), " rows")
        data
      }
      ```
      
      **After:**
      
      ```r
      load_data <- function(path) {
        cli_inform("Loading data from {.file {path}}")
        data <- read.csv(path)
        cli_inform("v" = "Loaded {nrow(data)} row{?s}")
        data
      }
      ```
      
      ### sprintf() to Inline Markup
      
      **Before:**
      
      ```r
      stop(sprintf(
        "File '%s' not found. Expected path: %s",
        basename(path),
        dirname(path)
      ))
      ```
      
      **After:**
      
      ```r
      cli_abort(c(
        "File not found",
        "x" = "Cannot find {.file {basename(path)}}",
        "i" = "Expected location: {.path {dirname(path)}}"
      ))
      ```
      
      ### paste() Concatenation to Glue Syntax
      
      **Before:**
      
      ```r
      msg <- paste0(
        "Processing ",
        n,
        " files",
        if (n > 1) "s" else "",
        " from ",
        dirname
      )
      message(msg)
      ```
      
      **After:**
      
      ```r
      cli_inform("Processing {n} file{?s} from {.path {dirname}}")
      ```
      
      ## Real-World Examples
      
      ### usethis-Style Error Messages
      
      The usethis package provides excellent examples of clear, actionable errors:
      
      ```r
      use_github <- function() {
        if (!uses_git()) {
          cli_abort(c(
            "Cannot use GitHub without Git",
            "x" = "This project is not a Git repository",
            "i" = "Use {.fn usethis::use_git} to initialize Git first"
          ))
        }
      
        if (is.null(github_token())) {
          cli_abort(c(
            "GitHub token not found",
            "x" = "No GitHub personal access token (PAT) found",
            "i" = "Create a token at {.url https://github.com/settings/tokens}",
            "i" = "Store it with {.fn gitcreds::gitcreds_set}"
          ))
        }
      
        # Implementation
      }
      ```
      
      ### devtools-Style Validation
      
      ```r
      check_package <- function(path = ".") {
        errors <- character()
        warnings <- character()
      
        # Collect issues
        if (!file.exists(file.path(path, "DESCRIPTION"))) {
          errors <- c(errors, "Missing {.file DESCRIPTION} file")
        }
      
        if (!file.exists(file.path(path, "NAMESPACE"))) {
          warnings <- c(warnings, "Missing {.file NAMESPACE} file")
        }
      
        # Report
        if (length(errors) > 0) {
          cli_abort(c(
            "Package structure invalid",
            set_names(errors, rep("x", length(errors))),
            "i" = "Use {.fn usethis::create_package} to create proper structure"
          ))
        }
      
        if (length(warnings) > 0) {
          cli_warn(c(
            "Package structure issues",
            set_names(warnings, rep("!", length(warnings)))
          ))
        }
      }
      ```
      
      ### Database Connection with Rich Context
      
      ```r
      connect_db <- function(host, port, database, user, password) {
        tryCatch(
          {
            conn <- DBI::dbConnect(
              RPostgres::Postgres(),
              host = host,
              port = port,
              dbname = database,
              user = user,
              password = password
            )
      
            cli_inform("v" = "Connected to {.field {database}} at {.val {host}}")
            conn
          },
          error = function(e) {
            cli_abort(
              c(
                "Database connection failed",
                "x" = "Cannot connect to {.val {host}}:{.val {port}}",
                "i" = "Connection details:",
                "*" = "Host: {.val {host}}",
                "*" = "Port: {.val {port}}",
                "*" = "Database: {.val {database}}",
                "*" = "User: {.val {user}}",
                "i" = "Troubleshooting:",
                ">" = "Verify server is running",
                ">" = "Check firewall settings",
                ">" = "Confirm credentials"
              ),
              parent = e
            )
          }
        )
      }
      ```
      
      ## Anti-Patterns
      
      ### Don't Mix Base R and CLI
      
      **Bad:**
      
      ```r
      validate <- function(x) {
        if (!is.numeric(x)) {
          stop("x must be numeric")  # base R
        }
        if (length(x) == 0) {
          cli_abort("{.arg x} cannot be empty")  # cli
        }
      }
      ```
      
      **Good:**
      
      ```r
      validate <- function(x) {
        if (!is.numeric(x)) {
          cli_abort("{.arg x} must be numeric")
        }
        if (length(x) == 0) {
          cli_abort("{.arg x} cannot be empty")
        }
      }
      ```
      
      ### Don't Overuse Bullets
      
      **Bad - Too many bullets:**
      
      ```r
      cli_abort(c(
        "Error",
        "x" = "Problem 1",
        "i" = "Info 1",
        "x" = "Problem 2",
        "i" = "Info 2",
        "x" = "Problem 3",
        "i" = "Info 3",
        "x" = "Problem 4"
        # ... too much information
      ))
      ```
      
      **Good - Focused message:**
      
      ```r
      cli_abort(c(
        "Validation failed",
        "x" = "Found {n_errors} error{?s} in configuration",
        "i" = "See {.file validation.log} for details"
      ))
      ```
      
      ### Don't Repeat Information
      
      **Bad:**
      
      ```r
      cli_abort(c(
        "File data.csv not found",
        "x" = "Cannot read data.csv",
        "i" = "The file data.csv does not exist"
      ))
      ```
      
      **Good:**
      
      ```r
      cli_abort(c(
        "File not found",
        "x" = "Cannot read {.file data.csv}",
        "i" = "Check that the file exists in the working directory"
      ))
      ```
      
      ### Don't Use Technical Jargon
      
      **Bad:**
      
      ```r
      cli_abort("NULL pointer in slot `data` of S4 object")
      ```
      
      **Good:**
      
      ```r
      cli_abort(c(
        "Dataset is missing",
        "x" = "The {.field data} slot is empty",
        "i" = "Use {.fn set_data} to provide a dataset"
      ))
      ```
      
      ### Don't Forget Pluralization
      
      **Bad:**
      
      ```r
      cli_inform("Found {n} files")  # "Found 1 files" looks wrong
      ```
      
      **Good:**
      
      ```r
      cli_inform("Found {n} file{?s}")  # "Found 1 file", "Found 2 files"
      ```
      
      ### Don't Use Bare Errors in Package Code
      
      **Bad:**
      
      ```r
      # Package function with no context
      compute <- function(x) {
        cli_abort("Invalid input")
        # Which function failed? What's invalid?
      }
      ```
      
      **Good:**
      
      ```r
      compute <- function(x) {
        if (!is.numeric(x)) {
          cli_abort(
            c(
              "{.arg x} must be numeric",
              "x" = "You supplied a {.cls {class(x)}} object"
            ),
            call = caller_env()  # Show caller's context
          )
        }
      }
      ```
      
    • inline-markup.md 19.3 KB
      # CLI Inline Markup Reference
      
      ## Table of Contents
      
      1. [Introduction](#introduction)
      2. [Basic Syntax](#basic-syntax)
      3. [Code & Syntax Classes](#code--syntax-classes)
      4. [Files & Paths Classes](#files--paths-classes)
      5. [Communication Classes](#communication-classes)
      6. [Values & Data Classes](#values--data-classes)
      7. [Emphasis Classes](#emphasis-classes)
      8. [Documentation Classes](#documentation-classes)
      9. [Special Classes](#special-classes)
      10. [Vector Collapsing](#vector-collapsing)
      11. [Advanced Patterns](#advanced-patterns)
      12. [Pluralization](#pluralization)
      13. [Performance Considerations](#performance-considerations)
      14. [Quick Reference Table](#quick-reference-table)
      
      ## Introduction
      
      Read this file when you need to:
      
      - Look up the correct inline markup class for specific content types
      - Understand how to nest and combine markup classes
      - Learn vector collapsing behavior and customization
      - Master pluralization patterns beyond basic `{?s}`
      - Optimize performance for high-frequency messaging
      - Troubleshoot unexpected formatting behavior
      
      Inline markup classes format text within cli messages using `{.class content}` syntax. They integrate with glue string interpolation and work across all cli functions: `cli_text()`, `cli_abort()`, `cli_warn()`, `cli_inform()`, `cli_alert_*()`, etc.
      
      ## Basic Syntax
      
      Inline markup uses curly braces with a period-prefixed class name:
      
      ```r
      cli_text("Function {.fn mean} calculates {.field average}")
      #> Function `mean()` calculates average
      ```
      
      **Key syntax rules:**
      
      - Format: `{.class content}`
      - Content is interpolated if it contains `{}`
      - Double braces `{{` and `}}` escape literal braces
      - Whitespace inside `{}` is preserved in output
      - Classes can be nested (see [Advanced Patterns](#advanced-patterns))
      
      ## Code & Syntax Classes
      
      ### .code
      
      Generic code formatting for expressions, statements, or syntax:
      
      ```r
      cli_text("Use {.code sum(x, na.rm = TRUE)} to ignore NA values")
      cli_text("Set {.code options(width = 120)} in your .Rprofile")
      cli_text("The {.code return()} statement exits early")
      ```
      
      **When to use:**
      - Multi-token code expressions
      - Syntax examples
      - Configuration snippets
      - When other code classes are too specific
      
      ### .fn and .fun
      
      Function names with automatic parentheses:
      
      ```r
      cli_text("Call {.fn mean} to calculate average")
      #> Call `mean()` to calculate average
      
      cli_text("Use {.fun base::mean} for namespace clarity")
      #> Use `base::mean()` for namespace clarity
      ```
      
      **Automatic formatting:**
      - Adds `()` suffix automatically
      - Supports `package::function` notation
      - Both `.fn` and `.fun` are equivalent
      
      **When to use:**
      - Referring to functions by name
      - Suggesting which function to call
      - Error messages about function usage
      
      ### .arg
      
      Function argument names:
      
      ```r
      cli_abort(c(
        "{.arg x} must be numeric",
        "i" = "Set {.arg na.rm = TRUE} to handle missing values"
      ))
      ```
      
      **When to use:**
      - Parameter validation errors
      - Documenting function arguments
      - Explaining argument behavior
      
      ### .cls
      
      S3/S4/R6 class names:
      
      ```r
      cli_abort("Expected {.cls data.frame}, got {.cls {class(x)}}")
      cli_text("Object is {.cls tbl_df}, a tibble subclass")
      ```
      
      **When to use:**
      - Type checking errors
      - Documenting expected types
      - Explaining inheritance
      
      ### .type
      
      Broader type descriptions (base types, generic categories):
      
      ```r
      cli_abort("Input must be {.type integer}, not {.type {typeof(x)}}")
      cli_text("Coercing from {.type character} to {.type numeric}")
      ```
      
      **When to use:**
      - Base R types: integer, double, character, logical
      - Generic type categories
      - Type coercion messages
      
      ### .obj_type_friendly
      
      User-friendly type descriptions with indefinite articles:
      
      ```r
      x <- data.frame()
      cli_text("{.var x} must be a vector, not {.obj_type_friendly {x}}")
      #> `x` must be a vector, not a data frame
      
      y <- 1:10
      cli_text("You provided {.obj_type_friendly {y}}")
      #> You provided an integer vector
      ```
      
      **Automatic features:**
      - Adds "a"/"an" article automatically
      - Uses friendly names ("data frame" not "data.frame")
      - Handles pluralization for vectors
      
      ## Files & Paths Classes
      
      ### .file
      
      File names and paths with appropriate formatting:
      
      ```r
      cli_text("Reading {.file data/input.csv}")
      cli_warn("File {.file ~/.ssh/config} has insecure permissions")
      cli_inform("Created {.file output/results.xlsx}")
      ```
      
      **Best practices:**
      - Use for any file reference
      - Works with relative and absolute paths
      - Handles home directory expansion
      
      ### .path
      
      Directory paths and file system locations:
      
      ```r
      cli_text("Installing to {.path /usr/local/lib/R}")
      cli_text("Working directory: {.path {getwd()}}")
      cli_abort("Directory {.path {dir}} does not exist")
      ```
      
      **When to use .file vs .path:**
      - `.file` - Files, scripts, documents
      - `.path` - Directories, installation locations, system paths
      
      ## Communication Classes
      
      ### .email
      
      Email addresses:
      
      ```r
      cli_text("Contact maintainer at {.email user@example.com}")
      cli_inform("Send bug reports to {.email bugs@r-project.org}")
      ```
      
      **Features:**
      - Creates `mailto:` links in supported terminals
      - Formatted distinctly from regular text
      
      ### .url
      
      Web URLs and URIs:
      
      ```r
      cli_text("Visit {.url https://cli.r-lib.org} for documentation")
      cli_text("API endpoint: {.url https://api.example.com/v1}")
      ```
      
      **Features:**
      - Creates clickable hyperlinks in supported terminals
      - Formats protocol, domain, and path distinctly
      
      ### .href
      
      Custom hyperlinks with separate text and URL:
      
      ```r
      cli_text("See {.href [documentation](https://cli.r-lib.org)}")
      cli_text("Read {.href [vignette](vignette:cli::inline-markup)}")
      ```
      
      **Syntax:**
      - Format: `{.href [text](url)}`
      - Markdown-style link syntax
      - Text and URL can be styled differently
      
      **Link types:**
      - HTTP/HTTPS: `https://example.com`
      - Help topics: `help:topic`
      - Vignettes: `vignette:package::topic`
      
      ## Values & Data Classes
      
      ### .val
      
      Data values, constants, and literals:
      
      ```r
      n <- 42
      cli_text("Found {.val {n}} records")
      cli_text("Default timeout is {.val 30} seconds")
      cli_text("Status: {.val 'complete'}")
      ```
      
      **Automatic formatting:**
      - Quoted for character values
      - Unquoted for numeric values
      - Handles vectors with collapsing
      
      ### .var
      
      Variable names in code or data:
      
      ```r
      cli_abort("Variable {.var x} must be numeric")
      cli_text("Column {.var age} contains missing values")
      cli_inform("Using {.var Sepal.Length} as predictor")
      ```
      
      **When to use:**
      - Variable names in R code
      - Column names in data frames
      - Field names in objects
      
      ### .envvar
      
      Environment variable names:
      
      ```r
      cli_text("Set {.envvar R_LIBS_USER} to customize library location")
      cli_abort("{.envvar HOME} is not defined")
      cli_inform("Using {.envvar PATH}: {.val {Sys.getenv('PATH')}}")
      ```
      
      **When to use:**
      - System environment variables
      - R-specific environment variables
      - Configuration via environment
      
      ### .field
      
      Object fields, slots, or attributes:
      
      ```r
      cli_text("The {.field name} field is required")
      cli_abort("Invalid {.field status} value")
      cli_text("Access {.field @data} slot in S4 object")
      ```
      
      **When to use:**
      - Named list elements
      - Data frame columns (prefer `.var` for analysis context)
      - S4 slots
      - Object attributes
      
      ### .str
      
      String literals and text values:
      
      ```r
      cli_text("Message starts with {.str 'Error:'}")
      cli_text("Pattern {.str '^[0-9]+$'} matches digits")
      ```
      
      **When to use:**
      - Literal string values
      - Pattern strings
      - Format strings
      - When `.val` formatting is too generic
      
      ## Emphasis Classes
      
      ### .emph
      
      Emphasis for important concepts or terms:
      
      ```r
      cli_text("This function is {.emph deprecated}")
      cli_text("The file is {.emph locked} by another process")
      cli_inform("{.emph Note}: This may take several minutes")
      ```
      
      **Rendering:**
      - Typically italic or colored
      - Lighter emphasis than `.strong`
      
      ### .strong
      
      Strong emphasis for critical information:
      
      ```r
      cli_warn("{.strong Warning}: This action cannot be undone")
      cli_text("This parameter is {.strong required}")
      cli_abort("{.strong Error}: Database connection failed")
      ```
      
      **Rendering:**
      - Typically bold or brightly colored
      - Stronger emphasis than `.emph`
      
      ## Documentation Classes
      
      ### .help
      
      R help topic references:
      
      ```r
      cli_text("See {.help stats::lm} for details")
      cli_inform("More info: {.help base::sum}")
      ```
      
      **Features:**
      - Creates link to help topic in RStudio
      - Supports `package::topic` notation
      - Fallback to plain text in terminals
      
      ### .topic
      
      Generic topic or section references:
      
      ```r
      cli_text("See {.topic 'Error Handling'} section")
      cli_inform("Refer to {.topic 'Advanced Usage'}")
      ```
      
      **When to use:**
      - Internal documentation sections
      - Vignette sections
      - General topic references
      
      ### .vignette
      
      Vignette references:
      
      ```r
      cli_text("Read {.vignette cli::semantic-cli} for examples")
      cli_inform("See {.vignette dplyr::programming}")
      ```
      
      **Features:**
      - Links to package vignettes
      - Format: `package::vignette-name`
      
      ### .run
      
      Runnable R code examples:
      
      ```r
      cli_text("Try: {.run install.packages('cli')}")
      cli_inform("Debug with: {.run options(error = recover)}")
      ```
      
      **Features:**
      - Creates executable link in RStudio
      - Click to run code in console
      - Formatted as code in terminals
      
      ## Special Classes
      
      ### .kbd
      
      Keyboard keys and shortcuts:
      
      ```r
      cli_text("Press {.kbd Ctrl+C} to cancel")
      cli_text("Use {.kbd Enter} to confirm")
      cli_inform("Save with {.kbd Cmd+S} (Mac) or {.kbd Ctrl+S} (Windows)")
      ```
      
      **Rendering:**
      - Typically in keyboard key style
      - May show as boxed or distinct formatting
      
      ### .key
      
      Alternative to `.kbd` for key names:
      
      ```r
      cli_text("Press the {.key RETURN} key")
      cli_text("Hold {.key SHIFT} while clicking")
      ```
      
      ### .or
      
      Logical OR separator for alternatives:
      
      ```r
      cli_text("Use {.val 'yes'} {.or} {.val 'no'}")
      cli_abort("Type must be {.val 'auto'} {.or} {.val 'manual'}")
      ```
      
      **Rendering:**
      - Formats as " or " with appropriate styling
      - Maintains class formatting for surrounding elements
      
      ### .pkg
      
      Package names:
      
      ```r
      cli_text("Install {.pkg dplyr} for data manipulation")
      cli_inform("Loading {.pkg ggplot2}")
      cli_abort("{.pkg httr2} is required but not installed")
      ```
      
      **Features:**
      - Distinct package name formatting
      - May include CRAN/GitHub links in supported environments
      
      ### .dt
      
      Definition term in definition lists:
      
      ```r
      cli_dl(c(
        "{.dt name}" = "User's full name",
        "{.dt email}" = "Contact email address"
      ))
      ```
      
      **When to use:**
      - Definition list terms
      - Glossary entries
      - Key-value pair keys
      
      ### .dd
      
      Definition description in definition lists:
      
      ```r
      cli_dl(c(
        "name" = "{.dd User's full name}",
        "email" = "{.dd Contact email address}"
      ))
      ```
      
      **When to use:**
      - Definition list descriptions
      - Glossary definitions
      - Key-value pair values
      
      ## Vector Collapsing
      
      Vectors are automatically collapsed with appropriate separators:
      
      ### Default Behavior
      
      ```r
      pkgs <- c("dplyr", "tidyr", "ggplot2")
      cli_text("Loading packages: {.pkg {pkgs}}")
      #> Loading packages: dplyr, tidyr, and ggplot2
      
      files <- c("a.R", "b.R")
      cli_text("Modified: {.file {files}}")
      #> Modified: a.R and b.R
      
      single <- "data.csv"
      cli_text("Found: {.file {single}}")
      #> Found: data.csv
      ```
      
      **Rules:**
      - Length 1: no separators
      - Length 2: " and " separator
      - Length 3+: ", " separators with " and " before last item
      
      ### Collapsing Empty Vectors
      
      ```r
      empty <- character()
      cli_text("Files: {.file {empty}}")
      #> Files:
      
      cli_text("Files: {?none/one/some}: {.file {empty}}")
      #> Files: none:
      ```
      
      ### Custom Collapse Separators
      
      Control collapsing with glue transformers:
      
      ```r
      items <- c("apple", "banana", "cherry")
      
      # Custom separator
      cli_text("Items: {.val {items}}", .transformer = function(text, envir) {
        glue::glue_collapse(text, sep = " | ", last = " | ")
      })
      #> Items: 'apple' | 'banana' | 'cherry'
      
      # OR separator
      cli_text("Choose: {.val {items}}", .transformer = function(text, envir) {
        glue::glue_collapse(text, sep = ", ", last = " or ")
      })
      #> Choose: 'apple', 'banana' or 'cherry'
      ```
      
      ### Truncating Long Vectors
      
      ```r
      many <- letters[1:20]
      
      cli_text("Variables: {.var {many}}")
      #> Variables: a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, and t
      
      # Truncate with custom transformer
      cli_text("Variables: {.var {head(many, 5)}} and {length(many) - 5} more")
      #> Variables: a, b, c, d, e and 15 more
      ```
      
      ## Advanced Patterns
      
      ### Nested Markup
      
      Classes can be nested for combined formatting:
      
      ```r
      cli_text("Call {.fn {.pkg dplyr}::filter} to subset data")
      cli_abort("File {.file {.path {dir}}/data.csv} not found")
      cli_text("Set {.arg timeout} to {.val {default_timeout}} by default")
      ```
      
      **Nesting guidelines:**
      - Outer class determines primary formatting
      - Inner classes add semantic meaning
      - Keep nesting depth reasonable (2-3 levels max)
      
      ### Combining with Glue Expressions
      
      ```r
      n <- 5
      cli_text("Processing {n} file{?s} from {.path {getwd()}}")
      
      status <- "complete"
      cli_inform("Status: {.val {toupper(status)}}")
      
      pkg <- "dplyr"
      cli_text("Version: {.pkg {pkg}} {.val {packageVersion(pkg)}}")
      ```
      
      ### Custom Glue Transformers
      
      Full control over formatting with transformers:
      
      ```r
      # Highlight differences
      cli_text(
        "Expected {.val {expected}}, got {.val {actual}}",
        .transformer = function(text, envir) {
          # Custom logic here
          text
        }
      )
      
      # Add prefixes
      my_transformer <- function(text, envir) {
        if (startsWith(text, ".pkg")) {
          paste0("R package: ", glue::glue(text, .envir = envir))
        } else {
          glue::identity_transformer(text, envir)
        }
      }
      
      cli_text("Install {.pkg cli}", .transformer = my_transformer)
      ```
      
      ### Conditional Formatting
      
      ```r
      status <- "error"
      cli_text("Status: {.{if(status == 'error') 'strong' else 'emph'} {status}}")
      
      type <- class(x)
      cli_text("Type: {if(is.numeric(x)) .val else .cls} {type}")
      ```
      
      ### Multiple Classes on Same Content
      
      Combine semantic classes:
      
      ```r
      # Package function
      cli_text("{.fn {.pkg stats}::median}")
      
      # File in path
      cli_text("{.file {.path /usr/local}/script.R}")
      
      # Variable value
      cli_text("{.var x} = {.val {x}}")
      ```
      
      ### Working with Lists
      
      ```r
      config <- list(host = "localhost", port = 8080, ssl = TRUE)
      
      cli_dl(c(
        "{.field host}" = "{.val {config$host}}",
        "{.field port}" = "{.val {config$port}}",
        "{.field ssl}" = "{.val {config$ssl}}"
      ))
      ```
      
      ### Escaping Markup
      
      Prevent interpretation with double braces:
      
      ```r
      cli_text("In glue, use {{variable}} syntax")
      #> In glue, use {variable} syntax
      
      cli_text("Literal: {{.code not_markup}}")
      #> Literal: {.code not_markup}
      ```
      
      ## Pluralization
      
      Pluralization adapts text based on quantities using `{?}` syntax.
      
      ### Single Alternative Pattern
      
      Add "s" for plural:
      
      ```r
      n <- 1
      cli_text("{n} file{?s} found")
      #> 1 file found
      
      n <- 5
      cli_text("{n} file{?s} found")
      #> 5 files found
      ```
      
      ### Two Alternative Pattern
      
      Specify singular/plural forms:
      
      ```r
      n <- 1
      cli_text("{n} director{?y/ies}")
      #> 1 directory
      
      n <- 3
      cli_text("{n} director{?y/ies}")
      #> 3 directories
      ```
      
      **Common patterns:**
      - `{?y/ies}` - directory/directories
      - `{?/s}` - item/items
      - `{?is/are}` - is/are
      - `{?/es}` - box/boxes
      - `{?ex/ices}` - index/indices
      
      ### Three Alternative Pattern
      
      Handle zero/one/many:
      
      ```r
      n <- 0
      cli_text("{?No/One/Some} file{?s} {?is/is/are} ready")
      #> No files are ready
      
      n <- 1
      cli_text("{?No/One/Some} file{?s} {?is/is/are} ready")
      #> One file is ready
      
      n <- 5
      cli_text("{?No/One/Some} file{?s} {?is/is/are} ready")
      #> Some files are ready
      ```
      
      ### Setting Quantity with qty()
      
      Control pluralization explicitly:
      
      ```r
      updated <- 3
      total <- 10
      cli_text("{updated}/{total} {qty(updated)} file{?s} {?needs/need} update{?s}")
      #> 3/10 files need updates
      ```
      
      **When to use qty():**
      - When quantity appears elsewhere
      - Complex expressions with multiple numbers
      - Explicit pluralization control
      
      ### no() Helper
      
      Display "no" instead of 0:
      
      ```r
      n <- 0
      cli_text("Found {no(n)} error{?s}")
      #> Found no errors
      
      n <- 3
      cli_text("Found {no(n)} error{?s}")
      #> Found 3 errors
      ```
      
      ### Advanced Pluralization Patterns
      
      **Multiple quantities in one message:**
      
      ```r
      nerr <- 2
      nwarn <- 1
      cli_text(
        "{nerr} error{?s} and {nwarn} {qty(nwarn)} warning{?s} found"
      )
      #> 2 errors and 1 warning found
      ```
      
      **Conditional articles:**
      
      ```r
      n <- 1
      cli_text("Found {?a /}{n} file{?s}")
      #> Found a 1 file
      
      n <- 5
      cli_text("Found {?a /}{n} file{?s}")
      #> Found 5 files
      ```
      
      **Complex pluralization:**
      
      ```r
      nfile <- 3
      ndir <- 1
      cli_text(
        "{nfile} file{?s} in {ndir} {qty(ndir)} director{?y/ies}"
      )
      #> 3 files in 1 directory
      ```
      
      **Verb agreement:**
      
      ```r
      n <- 1
      cli_text("File {?was/were} modified")
      #> File was modified
      
      n <- 3
      cli_text("Files {?was/were} modified")
      #> Files were modified
      ```
      
      **Possessives:**
      
      ```r
      n <- 1
      cli_text("User{?'s/'s'} setting{?s}")
      #> User's setting
      
      n <- 3
      cli_text("Users{?'s/'s'} settings")
      #> Users' settings
      ```
      
      ## Performance Considerations
      
      ### High-Frequency Messages
      
      For loops with many iterations:
      
      ```r
      # Expensive - creates cli context each iteration
      for (i in 1:10000) {
        cli_text("Processing {.val {i}}")
      }
      
      # Better - batch messages
      if (i %% 1000 == 0) {
        cli_text("Processed {.val {i}} items")
      }
      
      # Best - use progress bar
      cli_progress_bar("Processing", total = 10000)
      for (i in 1:10000) {
        # work here
        cli_progress_update()
      }
      ```
      
      ### Expensive Interpolation
      
      Avoid expensive computations in markup:
      
      ```r
      # Expensive - computes every time
      cli_text("Processing {.file {slow_path_computation()}}")
      
      # Better - compute once
      path <- slow_path_computation()
      cli_text("Processing {.file {path}}")
      ```
      
      ### Conditional Messages
      
      Use conditions to avoid unnecessary formatting:
      
      ```r
      # Inefficient
      if (verbose) cli_text("Status: {.val {expensive_status_check()}}")
      
      # Better
      if (verbose) {
        status <- expensive_status_check()
        cli_text("Status: {.val {status}}")
      }
      ```
      
      ### Large Vectors
      
      Truncate large vectors before formatting:
      
      ```r
      # Problematic with huge vectors
      vars <- names(huge_dataframe)
      cli_text("Variables: {.var {vars}}")
      
      # Better
      n_vars <- length(vars)
      if (n_vars > 10) {
        cli_text("Variables: {.var {head(vars, 10)}} and {n_vars - 10} more")
      } else {
        cli_text("Variables: {.var {vars}}")
      }
      ```
      
      ## Quick Reference Table
      
      | Class | Use For | Example | Output |
      |-------|---------|---------|--------|
      | `.code` | Code expressions | `{.code sum(x)}` | `sum(x)` |
      | `.fn`, `.fun` | Functions | `{.fn mean}` | `mean()` |
      | `.arg` | Function arguments | `{.arg na.rm}` | `na.rm` |
      | `.cls` | Class names | `{.cls data.frame}` | data.frame |
      | `.type` | Base types | `{.type integer}` | integer |
      | `.obj_type_friendly` | Friendly types | `{.obj_type_friendly {x}}` | an integer vector |
      | `.file` | File names | `{.file script.R}` | script.R |
      | `.path` | Directory paths | `{.path /usr/local}` | /usr/local |
      | `.email` | Email addresses | `{.email user@example.com}` | user@example.com |
      | `.url` | Web URLs | `{.url https://example.com}` | https://example.com |
      | `.href` | Custom links | `{.href [text](url)}` | text (linked) |
      | `.val` | Data values | `{.val {x}}` | 42 or 'text' |
      | `.var` | Variable names | `{.var column}` | column |
      | `.envvar` | Environment vars | `{.envvar PATH}` | PATH |
      | `.field` | Object fields | `{.field name}` | name |
      | `.str` | String literals | `{.str 'pattern'}` | 'pattern' |
      | `.emph` | Emphasis | `{.emph important}` | *important* |
      | `.strong` | Strong emphasis | `{.strong critical}` | **critical** |
      | `.help` | Help topics | `{.help stats::lm}` | stats::lm (linked) |
      | `.topic` | Topic references | `{.topic 'Intro'}` | 'Intro' |
      | `.vignette` | Vignettes | `{.vignette pkg::name}` | pkg::name (linked) |
      | `.run` | Runnable code | `{.run code}` | code (executable) |
      | `.kbd`, `.key` | Keyboard keys | `{.kbd Ctrl+C}` | Ctrl+C |
      | `.or` | Logical OR | `{.val x} {.or} {.val y}` | x or y |
      | `.pkg` | Package names | `{.pkg dplyr}` | dplyr |
      | `.dt` | Definition term | `{.dt term}` | term (in definition list) |
      | `.dd` | Definition desc | `{.dd description}` | description (in definition list) |
      
    • progress.md 20.1 KB
      # Progress Indicators
      
      ## Table of Contents
      
      1. [Choosing Progress Style](#choosing-progress-style)
      2. [cli_progress_bar() Deep Dive](#cli_progress_bar-deep-dive)
      3. [cli_progress_step() Advanced](#cli_progress_step-advanced)
      4. [cli_progress_message()](#cli_progress_message)
      5. [Progress Variables Reference](#progress-variables-reference)
      6. [Format Strings](#format-strings)
      7. [Progress Styles](#progress-styles)
      8. [Advanced Scenarios](#advanced-scenarios)
      9. [Shiny Integration](#shiny-integration)
      10. [C-Level Progress](#c-level-progress)
      11. [Debugging Progress](#debugging-progress)
      12. [Performance Considerations](#performance-considerations)
      
      ## Choosing Progress Style
      
      cli offers three progress indicator functions, each suited for different scenarios:
      
      ### cli_progress_bar()
      
      Use when:
      - You know the total number of iterations upfront
      - Progress can be measured as a percentage
      - Operation involves loops or iterating over collections
      - Users need ETA and completion estimates
      
      ```r
      # Good use case
      process_files <- function(files) {
        cli_progress_bar("Processing", total = length(files))
        for (file in files) {
          process(file)
          cli_progress_update()
        }
      }
      ```
      
      ### cli_progress_step()
      
      Use when:
      - Progress happens in discrete named steps
      - Total work is unknown or variable
      - Steps have different durations
      - You want automatic success/failure indicators
      
      ```r
      # Good use case
      deploy_app <- function() {
        cli_progress_step("Building assets")
        build_assets()
      
        cli_progress_step("Running tests")
        run_tests()
      
        cli_progress_step("Deploying to server")
        deploy()
      }
      ```
      
      ### cli_progress_message()
      
      Use when:
      - Showing a simple status message
      - No quantifiable progress to track
      - Operation duration is uncertain
      - Want a spinner without step semantics
      
      ```r
      # Good use case
      cli_progress_message("Waiting for API response...")
      response <- long_running_api_call()
      ```
      
      ## cli_progress_bar() Deep Dive
      
      ### Basic Parameters
      
      ```r
      cli_progress_bar(
        name = NULL,           # Progress bar name/label
        status = NULL,         # Additional status text
        type = "iterator",     # Bar type: "iterator", "tasks", "download", "custom"
        total = NA,            # Total number of items (NA for unknown)
        format = NULL,         # Custom format string
        format_done = NULL,    # Format when complete
        format_failed = NULL,  # Format when failed
        clear = TRUE,          # Clear bar when done
        current = TRUE,        # Show current position
        auto_terminate = TRUE, # Auto-close when function exits
        .auto_close = TRUE     # Deprecated, use auto_terminate
      )
      ```
      
      ### Type Parameter Examples
      
      ```r
      # Iterator (default) - for loops
      cli_progress_bar("Processing items", total = 100, type = "iterator")
      
      # Tasks - for discrete tasks
      cli_progress_bar("Completing tasks", total = 5, type = "tasks")
      
      # Download - for file downloads
      cli_progress_bar("Downloading", total = file_size, type = "download")
      
      # Custom - for user-defined formats
      cli_progress_bar(
        format = "Working {cli::pb_bar} {cli::pb_percent}",
        total = 100,
        type = "custom"
      )
      ```
      
      ### Auto-Termination Behavior
      
      Progress bars automatically close when the calling function exits:
      
      ```r
      process_data <- function(data) {
        cli_progress_bar("Processing", total = nrow(data))
      
        for (i in seq_len(nrow(data))) {
          process_row(data[i, ])
          cli_progress_update()
        }
      
        # Bar automatically closes here when function returns
      }
      
      # Manual control if needed
      process_data_manual <- function(data) {
        id <- cli_progress_bar("Processing", total = nrow(data))
      
        for (i in seq_len(nrow(data))) {
          process_row(data[i, ])
          cli_progress_update(id = id)
        }
      
        cli_progress_done(id = id)  # Explicit close
      }
      ```
      
      ### Clearing Behavior
      
      Control whether progress bars remain visible after completion:
      
      ```r
      # Clear after completion (default)
      cli_progress_bar("Working", total = 100, clear = TRUE)
      for (i in 1:100) {
        Sys.sleep(0.01)
        cli_progress_update()
      }
      # Bar disappears when done
      
      # Keep visible after completion
      cli_progress_bar("Working", total = 100, clear = FALSE)
      for (i in 1:100) {
        Sys.sleep(0.01)
        cli_progress_update()
      }
      # Bar remains showing 100% completion
      ```
      
      ### Dynamic Updates
      
      Update progress with optional status messages:
      
      ```r
      process_files <- function(files) {
        cli_progress_bar("Processing files", total = length(files))
      
        for (i in seq_along(files)) {
          file <- files[[i]]
      
          # Update with status
          cli_progress_update(
            status = sprintf("Current: %s", basename(file))
          )
      
          process_file(file)
        }
      }
      
      # Update multiple items at once
      cli_progress_update(inc = 5)  # Increment by 5
      cli_progress_update(set = 50) # Set to specific value
      ```
      
      ## Format Strings
      
      ### Default Formats
      
      ```r
      # Iterator format (default)
      # "{cli::pb_spin} {cli::pb_name} {cli::pb_bar} {cli::pb_percent} | ETA: {cli::pb_eta}"
      
      # Download format
      # "Downloaded {cli::pb_current_bytes}/{cli::pb_total_bytes} ({cli::pb_rate_bytes}/s) | ETA: {cli::pb_eta}"
      
      # Tasks format
      # "{cli::pb_name} {cli::pb_current}/{cli::pb_total} | ETA: {cli::pb_eta}"
      ```
      
      ### Custom Format Examples
      
      ```r
      # Simple percentage only
      cli_progress_bar(
        "Working",
        total = 100,
        format = "{cli::pb_name} {cli::pb_percent}"
      )
      
      # With ETA and rate
      cli_progress_bar(
        "Processing",
        total = 1000,
        format = "{cli::pb_bar} {cli::pb_current}/{cli::pb_total} [{cli::pb_eta} @ {cli::pb_rate}]"
      )
      
      # With elapsed time
      cli_progress_bar(
        "Running",
        total = 50,
        format = "{cli::pb_spin} {cli::pb_name} {cli::pb_percent} | Elapsed: {cli::pb_elapsed}"
      )
      
      # Download-style with bytes
      cli_progress_bar(
        "Downloading data.zip",
        total = 1024^3,  # 1 GB
        format = paste0(
          "{cli::pb_current_bytes}/{cli::pb_total_bytes} ",
          "({cli::pb_percent}) | ",
          "{cli::pb_rate_bytes}/s | ",
          "ETA: {cli::pb_eta}"
        )
      )
      
      # Custom status display
      cli_progress_bar(
        format = "{cli::pb_bar} {cli::pb_status}"
      )
      ```
      
      ### Completion and Failure Formats
      
      ```r
      cli_progress_bar(
        "Processing",
        total = 100,
        format = "Working: {cli::pb_bar} {cli::pb_percent}",
        format_done = "Completed {cli::pb_total} items in {cli::pb_elapsed}",
        format_failed = "Failed after processing {cli::pb_current} items"
      )
      
      # Trigger failure
      for (i in 1:50) {
        if (i == 30) {
          cli_progress_done(result = "failed")
          break
        }
        cli_progress_update()
      }
      ```
      
      ## Progress Variables Reference
      
      ### Basic Progress Variables
      
      ```r
      # Current position
      cli::pb_current       # Current iteration number (e.g., 45)
      
      # Total work
      cli::pb_total         # Total iterations (e.g., 100)
      
      # Percentage
      cli::pb_percent       # Completion percentage (e.g., "45%")
      
      # Visual bar
      cli::pb_bar           # Progress bar visualization (e.g., "=========>    ")
      
      # Spinner
      cli::pb_spin          # Animated spinner character
      
      # Name and status
      cli::pb_name          # Progress bar name
      cli::pb_status        # Current status message
      ```
      
      ### Timing Variables
      
      ```r
      # Time elapsed
      cli::pb_elapsed       # Time since start (e.g., "2m 30s")
      cli::pb_elapsed_raw   # Elapsed time in seconds (e.g., 150.234)
      cli::pb_elapsed_clock # Clock time format (e.g., "02:30")
      
      # Time remaining
      cli::pb_eta           # Estimated time remaining (e.g., "1m 15s")
      cli::pb_eta_raw       # ETA in seconds (e.g., 75.0)
      cli::pb_eta_str       # ETA as string (e.g., "ETA: 1m 15s")
      
      # Rate
      cli::pb_rate          # Items per second (e.g., "0.6/s")
      cli::pb_rate_raw      # Raw rate value (e.g., 0.6)
      ```
      
      ### Byte-Based Variables
      
      For download/upload progress bars:
      
      ```r
      # Current bytes
      cli::pb_current_bytes # Formatted current (e.g., "45.2 MB")
      
      # Total bytes
      cli::pb_total_bytes   # Formatted total (e.g., "100 MB")
      
      # Rate
      cli::pb_rate_bytes    # Bytes per second (e.g., "2.1 MB/s")
      ```
      
      ### Advanced Variables
      
      ```r
      # Tick information
      cli::pb_tick          # Current tick number
      cli::pb_tick_rate     # Ticks per second
      
      # Timestamps
      cli::pb_start         # Start timestamp (POSIXct)
      cli::pb_timestamp     # Current timestamp (POSIXct)
      
      # Extra data
      cli::pb_extra         # User-defined extra data
      ```
      
      ## cli_progress_step() Advanced
      
      ### Basic Usage
      
      ```r
      deploy_package <- function() {
        cli_progress_step("Checking package")
        check_result <- check_package()
      
        cli_progress_step("Building package")
        build_result <- build_package()
      
        cli_progress_step("Uploading to CRAN")
        upload_result <- upload_package()
      }
      ```
      
      ### Spinner Customization
      
      ```r
      # Use different spinner style
      options(cli.spinner = "dots")
      cli_progress_step("Processing")
      
      # Available spinners
      options(cli.spinner = "line")      # Classic line spinner
      options(cli.spinner = "dots")      # Dots
      options(cli.spinner = "dots2")     # Alternative dots
      options(cli.spinner = "dots3")     # More dots
      options(cli.spinner = "dots12")    # 12-frame dots
      options(cli.spinner = "arrow")     # Rotating arrow
      options(cli.spinner = "bouncingBar") # Bouncing bar
      options(cli.spinner = "clock")     # Clock hands
      
      # Custom spinner frames
      options(cli.spinner = list(
        interval = 100,
        frames = c("◐", "◓", "◑", "◒")
      ))
      ```
      
      ### Multiple Concurrent Steps
      
      Progress steps can be nested:
      
      ```r
      process_projects <- function(projects) {
        cli_progress_step("Processing {length(projects)} projects")
      
        for (proj in projects) {
          cli_progress_step("Building {proj}")
          build_project(proj)
      
          cli_progress_step("Testing {proj}")
          test_project(proj)
      
          cli_progress_step("Deploying {proj}")
          deploy_project(proj)
        }
      }
      ```
      
      ### Dynamic Message Updates
      
      Update step messages while they're running:
      
      ```r
      process_with_details <- function(files) {
        id <- cli_progress_step("Processing files")
      
        for (i in seq_along(files)) {
          cli_progress_update(
            id = id,
            status = sprintf("File %d/%d: %s", i, length(files), basename(files[i]))
          )
      
          process_file(files[i])
        }
      
        cli_progress_done(id = id)
      }
      ```
      
      ### Success/Failure Termination
      
      ```r
      deploy_with_status <- function() {
        tryCatch({
          cli_progress_step("Building application")
          build_app()
      
          cli_progress_step("Running tests")
          test_result <- run_tests()
      
          if (!test_result$passed) {
            cli_progress_done(result = "failed", msg_failed = "Tests failed!")
            return(FALSE)
          }
      
          cli_progress_step("Deploying")
          deploy()
      
          cli_progress_done(result = "done", msg_done = "Deployment successful!")
          TRUE
      
        }, error = function(e) {
          cli_progress_done(result = "failed", msg_failed = "Deployment failed: {e$message}")
          FALSE
        })
      }
      ```
      
      ### Custom Success/Failure Messages
      
      ```r
      id <- cli_progress_step(
        "Processing data",
        msg_done = "Data processed successfully",
        msg_failed = "Data processing failed"
      )
      
      # Mark as done
      cli_progress_done(id = id, result = "done")
      
      # Or mark as failed
      cli_progress_done(id = id, result = "failed")
      ```
      
      ## cli_progress_message()
      
      Simple progress messages with automatic spinners:
      
      ```r
      # Basic usage
      cli_progress_message("Loading configuration...")
      config <- load_config()
      
      # With explicit cleanup
      id <- cli_progress_message("Waiting for server...")
      wait_for_server()
      cli_progress_done(id = id)
      
      # Updating message
      id <- cli_progress_message("Connecting...")
      cli_progress_update(id = id, status = "Authenticating...")
      cli_progress_update(id = id, status = "Connected!")
      cli_progress_done(id = id)
      ```
      
      ## Progress Styles
      
      cli includes several built-in progress bar styles:
      
      ```r
      # Classic style (default)
      options(cli.progress_bar_style = "classic")
      
      # Unicode style (requires Unicode support)
      options(cli.progress_bar_style = "unicode")
      
      # ASCII style (for limited terminals)
      options(cli.progress_bar_style = "ascii")
      
      # Custom style
      options(cli.progress_bar_style = list(
        complete = "=",
        incomplete = " ",
        current = ">",
        width = 40
      ))
      ```
      
      ## Advanced Scenarios
      
      ### Nested Progress Bars
      
      ```r
      process_datasets <- function(datasets) {
        cli_progress_bar("Processing datasets", total = length(datasets))
      
        for (dataset in datasets) {
          # Outer progress updates
          cli_progress_update()
      
          # Inner progress bar
          n_rows <- nrow(dataset)
          cli_progress_bar("Processing rows", total = n_rows)
      
          for (i in seq_len(n_rows)) {
            process_row(dataset[i, ])
            cli_progress_update()  # Updates inner bar
          }
          # Inner bar auto-closes
        }
        # Outer bar auto-closes
      }
      ```
      
      ### Progress with Parallel Code
      
      Progress bars in parallel contexts require special handling:
      
      ```r
      library(foreach)
      library(doParallel)
      
      # Not recommended - doesn't work well
      process_parallel_bad <- function(items) {
        cli_progress_bar("Processing", total = length(items))
      
        foreach(item = items) %dopar% {
          result <- process_item(item)
          cli_progress_update()  # Won't work across processes
          result
        }
      }
      
      # Better approach - update after each completion
      process_parallel_better <- function(items) {
        cli_progress_bar("Processing", total = length(items))
      
        results <- foreach(item = items) %dopar% {
          process_item(item)
        }
      
        # Update in main thread
        for (i in seq_along(results)) {
          cli_progress_update()
        }
      
        results
      }
      
      # Best approach - use progress updates between parallel batches
      process_parallel_best <- function(items, batch_size = 10) {
        batches <- split(items, ceiling(seq_along(items) / batch_size))
        cli_progress_bar("Processing", total = length(items))
      
        results <- list()
        for (batch in batches) {
          batch_results <- foreach(item = batch) %dopar% {
            process_item(item)
          }
          results <- c(results, batch_results)
          cli_progress_update(inc = length(batch))
        }
      
        results
      }
      ```
      
      ### Progress in Loops vs Mapping
      
      ```r
      # Traditional loop with progress
      process_loop <- function(items) {
        cli_progress_bar("Processing", total = length(items))
      
        results <- vector("list", length(items))
        for (i in seq_along(items)) {
          results[[i]] <- process_item(items[[i]])
          cli_progress_update()
        }
      
        results
      }
      
      # Using cli_progress_along()
      process_along <- function(items) {
        results <- lapply(cli_progress_along(items, "Processing"), function(i) {
          process_item(items[[i]])
        })
      
        results
      }
      
      # Using purrr with progress
      library(purrr)
      process_purrr <- function(items) {
        cli_progress_bar("Processing", total = length(items))
      
        map(items, function(item) {
          result <- process_item(item)
          cli_progress_update()
          result
        })
      }
      ```
      
      ### Progress Output vs Regular CLI Output
      
      Progress bars interact with other cli output:
      
      ```r
      process_with_messages <- function(items) {
        cli_progress_bar("Processing", total = length(items))
      
        for (i in seq_along(items)) {
          result <- process_item(items[[i]])
      
          # Regular cli messages work alongside progress
          if (result$warnings) {
            cli_alert_warning("Item {i} had warnings")
          }
      
          cli_progress_update()
        }
      
        cli_alert_success("Processing complete")
      }
      
      # Progress-aware output functions
      process_with_progress_output <- function(items) {
        cli_progress_bar("Processing", total = length(items))
      
        for (item in items) {
          # These respect progress bars
          cli_progress_output(cli_text("Processing {item}"))
          process_item(item)
          cli_progress_update()
        }
      }
      ```
      
      ### Unknown Total
      
      Progress bars can run without knowing the total:
      
      ```r
      process_stream <- function(conn) {
        cli_progress_bar("Processing stream", total = NA)
      
        while (length(line <- readLines(conn, n = 1)) > 0) {
          process_line(line)
          cli_progress_update()
        }
      
        # Shows spinner and count, no percentage
      }
      
      # Update total when it becomes known
      process_dynamic <- function() {
        cli_progress_bar("Discovering files", total = NA)
      
        files <- find_files()
      
        # Update total
        cli_progress_update(total = length(files))
      
        for (file in files) {
          process_file(file)
          cli_progress_update()
        }
      }
      ```
      
      ## Shiny Integration
      
      Progress indicators in Shiny applications require special consideration:
      
      ```r
      library(shiny)
      
      ui <- fluidPage(
        actionButton("process", "Process Data")
      )
      
      server <- function(input, output, session) {
        observeEvent(input$process, {
          # Use Shiny's progress API
          withProgress(message = "Processing", {
      
            # cli progress works but won't show in Shiny UI
            cli_progress_bar("Internal progress", total = 100)
      
            for (i in 1:100) {
              Sys.sleep(0.01)
      
              # Update Shiny progress
              incProgress(1/100)
      
              # cli progress (for logs)
              cli_progress_update()
            }
          })
        })
      }
      
      # Better: Use cli in Shiny with proper output
      server <- function(input, output, session) {
        observeEvent(input$process, {
          # Redirect cli output to console
          withProgress(message = "Processing", {
            # cli progress shows in R console, not Shiny UI
            process_data()
          })
        })
      }
      ```
      
      ## C-Level Progress
      
      For package developers using C/C++:
      
      ```r
      # In R
      process_with_c <- function(data) {
        cli_progress_bar("Processing in C", total = nrow(data))
        .Call(C_process_data, data, environment())
      }
      
      # In C code (using cli's C API)
      # #include <cli/progress.h>
      #
      # SEXP C_process_data(SEXP data, SEXP progress_env) {
      #   R_xlen_t n = Rf_xlength(data);
      #
      #   for (R_xlen_t i = 0; i < n; i++) {
      #     // Process data[i]
      #
      #     // Update progress from C
      #     cli_progress_update(progress_env);
      #   }
      #
      #   return R_NilValue;
      # }
      ```
      
      Note: C-level progress requires the cli package C headers and proper linking.
      
      ## Debugging Progress
      
      ### Common Issues
      
      ```r
      # Issue: Progress bar not updating
      # Problem: Not calling cli_progress_update()
      for (i in 1:100) {
        process(i)
        # Missing: cli_progress_update()
      }
      
      # Issue: Multiple progress bars interfering
      # Solution: Store IDs and update correct bar
      id1 <- cli_progress_bar("Task 1", total = 10)
      id2 <- cli_progress_bar("Task 2", total = 20)
      
      cli_progress_update(id = id1)
      cli_progress_update(id = id2)
      
      # Issue: Progress bar not visible
      # Check: Is stderr redirected? Is output captured?
      # Progress bars use stderr by default
      
      # Issue: Progress bar flickers
      # Problem: Output mixing with progress
      # Solution: Use cli_progress_output()
      cli_progress_output(cli_alert_info("Status update"))
      ```
      
      ### Testing Progress Bars
      
      ```r
      test_that("progress bar updates correctly", {
        # Mock progress to test without output
        mockery::stub(my_function, "cli_progress_bar", NULL)
        mockery::stub(my_function, "cli_progress_update", NULL)
      
        result <- my_function()
        expect_true(result)
      })
      
      # Test with captured output
      test_that("progress messages are correct", {
        output <- capture.output({
          process_data(test_data)
        })
      
        # Note: Progress bars may not appear in captured output
        # Consider testing function logic separately
      })
      ```
      
      ### Disabling Progress
      
      ```r
      # Disable all progress indicators
      options(cli.progress_show_after = Inf)
      
      # Or use show_after parameter
      cli_progress_bar("Working", total = 100, show_after = Inf)
      
      # Conditional progress
      process_data <- function(data, verbose = TRUE) {
        if (verbose) {
          cli_progress_bar("Processing", total = nrow(data))
        }
      
        for (i in seq_len(nrow(data))) {
          process_row(data[i, ])
          if (verbose) cli_progress_update()
        }
      }
      ```
      
      ## Performance Considerations
      
      ### Update Frequency
      
      ```r
      # Bad: Update too frequently (slows down loop)
      for (i in 1:1e6) {
        fast_operation(i)
        cli_progress_update()  # 1 million updates!
      }
      
      # Better: Update periodically
      cli_progress_bar("Processing", total = 1e6)
      for (i in 1:1e6) {
        fast_operation(i)
        if (i %% 1000 == 0) cli_progress_update(set = i)
      }
      
      # Best: Use show_after to delay display
      cli_progress_bar(
        "Processing",
        total = 1e6,
        show_after = 2  # Only show if takes >2 seconds
      )
      ```
      
      ### Overhead
      
      Progress bars add minimal overhead, but consider:
      
      ```r
      # Negligible overhead for slow operations
      process_files <- function(files) {
        cli_progress_bar("Processing", total = length(files))
        for (file in files) {
          slow_operation(file)  # 1 second per file
          cli_progress_update()  # <1ms overhead
        }
      }
      
      # Noticeable overhead for fast operations
      process_numbers <- function(n = 1e6) {
        cli_progress_bar("Processing", total = n)
        for (i in 1:n) {
          fast_operation(i)  # 1μs per operation
          cli_progress_update()  # 1ms overhead - 1000x slower!
        }
      }
      
      # Solution: Batch updates
      process_numbers_fast <- function(n = 1e6, update_every = 1000) {
        cli_progress_bar("Processing", total = n)
        for (i in 1:n) {
          fast_operation(i)
          if (i %% update_every == 0) {
            cli_progress_update(set = i)
          }
        }
      }
      ```
      
      ### Terminal Performance
      
      ```r
      # Progress bars can be slow on Windows
      # Use ASCII style for better performance
      if (.Platform$OS.type == "windows") {
        options(cli.progress_bar_style = "ascii")
      }
      
      # Disable Unicode for remote sessions
      if (Sys.getenv("SSH_CONNECTION") != "") {
        options(cli.unicode = FALSE)
      }
      ```
      
    • themes.md 13.4 KB
      # CLI Themes and Styling
      
      ## Table of Contents
      
      - [Theme Basics](#theme-basics)
      - [Container Functions](#container-functions)
      - [Selector Types](#selector-types)
      - [Theme Properties](#theme-properties)
      - [Built-in Themes](#built-in-themes)
      - [Custom Themes](#custom-themes)
      - [App and Package Themes](#app-and-package-themes)
      - [Color Palettes](#color-palettes)
      - [Accessibility](#accessibility)
      - [Debugging Themes](#debugging-themes)
      
      ## Theme Basics
      
      CLI uses a CSS-like theming system to style console output. Themes consist of selectors that match elements and properties that define their appearance.
      
      ### How Themes Work
      
      1. Elements are identified by selectors (like `.alert-success` or `.code`)
      2. Selectors are matched against the element hierarchy
      3. Properties are applied to matched elements
      4. Properties cascade through the element tree
      
      ### Basic Theme Structure
      
      ```r
      my_theme <- list(
        ".alert-success" = list(
          "color" = "green",
          "font-weight" = "bold"
        ),
        ".code" = list(
          "color" = "blue",
          "background-color" = "grey90"
        )
      )
      
      cli_div(theme = my_theme)
      cli_alert_success("Operation completed")
      cli_code("result <- compute()")
      cli_end()
      ```
      
      ## Container Functions
      
      Containers create themed regions and manage element hierarchy. They auto-close when the function exits or can be closed explicitly with `cli_end()`.
      
      ### General Containers
      
      **`cli_div()`** - Generic container for applying themes:
      
      ```r
      cli_div(theme = list(".emph" = list(color = "red")))
      cli_text("This is {.emph emphasized} text")
      cli_end()
      ```
      
      With classes:
      
      ```r
      cli_div(class = "my-section", theme = list(
        ".my-section" = list("margin-left" = 2),
        ".my-section .code" = list(color = "blue")
      ))
      cli_text("Code: {.code mean(x)}")
      cli_end()
      ```
      
      **`cli_par()`** - Paragraph container:
      
      ```r
      cli_par()
      cli_text("First line")
      cli_text("Second line")
      cli_end()
      ```
      
      ### List Containers
      
      **`cli_ul()` / `cli_ol()` / `cli_dl()`** - List containers:
      
      ```r
      # Unordered list
      cli_ul()
      cli_li("First item")
      cli_li("Second item")
      cli_end()
      
      # Ordered list
      cli_ol()
      cli_li("Step one")
      cli_li("Step two")
      cli_end()
      
      # Definition list
      cli_dl()
      cli_li(c(term = "Definition of term"))
      cli_end()
      ```
      
      ### Auto-closing Behavior
      
      Containers automatically close when the calling function exits:
      
      ```r
      my_function <- function() {
        cli_div(theme = list(".alert" = list(color = "red")))
        cli_alert("Alert message")
        # No need to call cli_end() - auto-closes here
      }
      ```
      
      Explicit closing with `cli_end()`:
      
      ```r
      id <- cli_div(theme = my_theme)
      cli_text("Themed content")
      cli_end(id)  # Close specific container
      ```
      
      ### Theme Scoping and Inheritance
      
      Themes inherit from parent containers and can be overridden:
      
      ```r
      # Outer theme
      cli_div(theme = list(".code" = list(color = "blue")))
      
      # Inner theme overrides
      cli_div(theme = list(".code" = list(color = "red")))
      cli_text("Code is {.code red} here")
      cli_end()
      
      cli_text("Code is {.code blue} here")
      cli_end()
      ```
      
      ## Selector Types
      
      ### Simple Selectors
      
      Match elements by class:
      
      ```r
      list(
        ".code" = list(color = "blue"),       # Matches {.code ...}
        ".file" = list(color = "magenta"),    # Matches {.file ...}
        ".pkg" = list(color = "cyan")         # Matches {.pkg ...}
      )
      ```
      
      ### Element Type Selectors
      
      Match by element type:
      
      ```r
      list(
        "h1" = list(color = "blue", "font-weight" = "bold"),
        "ul" = list("margin-left" = 2),
        "li" = list(before = "* ")
      )
      ```
      
      ### Descendant Selectors
      
      Match elements within other elements:
      
      ```r
      list(
        ".my-section .code" = list(color = "blue"),
        ".alert .emph" = list(color = "red")
      )
      ```
      
      ### Multiple Selectors
      
      Apply same styles to multiple selectors:
      
      ```r
      list(
        ".code, .fun, .fn" = list(color = "blue")
      )
      ```
      
      ### Pseudo-selectors
      
      Match specific states or positions:
      
      ```r
      list(
        "li:before" = list(content = "-> "),
        "ul li:first-child" = list("margin-top" = 0)
      )
      ```
      
      ## Theme Properties
      
      ### Color Properties
      
      **`color`** - Text color:
      
      ```r
      list(".alert" = list(color = "red"))
      ```
      
      **`background-color`** - Background color:
      
      ```r
      list(".code" = list("background-color" = "grey90"))
      ```
      
      Color formats:
      - Named colors: `"red"`, `"blue"`, `"green"`
      - ANSI colors: `"ansi_red"`, `"ansi_bright_blue"`
      - RGB hex: `"#FF5733"`
      - RGB function: `rgb(255, 87, 51)`
      
      ### Text Formatting
      
      **`font-weight`** - Text weight:
      
      ```r
      list(".strong" = list("font-weight" = "bold"))
      ```
      
      **`font-style`** - Text style:
      
      ```r
      list(".emph" = list("font-style" = "italic"))
      ```
      
      **`text-decoration`** - Text decoration:
      
      ```r
      list(".url" = list("text-decoration" = "underline"))
      ```
      
      ### Spacing Properties
      
      **`margin-left`** - Left margin (in characters):
      
      ```r
      list(".par" = list("margin-left" = 2))
      ```
      
      **`margin-right`** - Right margin:
      
      ```r
      list(".par" = list("margin-right" = 2))
      ```
      
      **`margin-top`** - Top margin (in lines):
      
      ```r
      list("h1" = list("margin-top" = 1, "margin-bottom" = 1))
      ```
      
      **`padding-left`** - Left padding:
      
      ```r
      list(".alert" = list("padding-left" = 2))
      ```
      
      ### Content Properties
      
      **`before`** - Content before element:
      
      ```r
      list(
        ".alert-success:before" = list(content = "[OK] "),
        "ul li:before" = list(content = "* ")
      )
      ```
      
      **`after`** - Content after element:
      
      ```r
      list(".code:after" = list(content = " }"))
      ```
      
      ### List Properties
      
      **`list-style-type`** - List marker style:
      
      ```r
      list(
        "ul" = list("list-style-type" = "bullet"),
        "ol" = list("list-style-type" = "decimal")
      )
      ```
      
      Values: `"bullet"`, `"circle"`, `"square"`, `"decimal"`, `"lower-alpha"`, `"upper-alpha"`
      
      **`start`** - Ordered list start number:
      
      ```r
      list("ol" = list(start = 5))
      ```
      
      ### Line Properties
      
      **`line-type`** - Line drawing style:
      
      ```r
      list(".rule" = list("line-type" = "double"))
      ```
      
      Values: `"single"`, `"double"`, `"bar1"` through `"bar8"`
      
      ### Format Control
      
      **`fmt`** - Custom format function:
      
      ```r
      list(
        ".timestamp" = list(
          fmt = function(x) format(Sys.time(), "%Y-%m-%d %H:%M:%S")
        )
      )
      ```
      
      **`transform`** - Transform function:
      
      ```r
      list(
        ".upper" = list(transform = toupper)
      )
      ```
      
      ## Built-in Themes
      
      ### Default Theme
      
      The standard cli theme with semantic colors and spacing:
      
      ```r
      # View built-in theme structure
      str(builtin_theme(), max.level = 2)
      ```
      
      Key elements:
      - Blue for code, functions, arguments
      - Magenta for files and paths
      - Cyan for packages
      - Green for success
      - Red for errors
      - Yellow for warnings
      
      ### Simple Theme
      
      A minimal theme without colors:
      
      ```r
      options(cli.theme = simple_theme())
      ```
      
      Useful for:
      - Terminals without color support
      - Logging to files
      - Screen readers
      - Testing
      
      ### Dark Theme
      
      Optimized for dark terminal backgrounds (included in default theme with automatic detection).
      
      ## Custom Themes
      
      ### Creating a Custom Theme
      
      Build themes incrementally:
      
      ```r
      my_theme <- list(
        # Headers
        "h1" = list(
          color = "blue",
          "font-weight" = "bold",
          "margin-top" = 1,
          "margin-bottom" = 1,
          before = "== ",
          after = " =="
        ),
      
        # Code elements
        ".code" = list(
          color = "cyan",
          "background-color" = "grey10"
        ),
      
        ".fn" = list(
          color = "blue",
          after = "()"
        ),
      
        # Alerts
        ".alert-success" = list(
          before = "[OK] ",
          color = "green"
        ),
      
        ".alert-danger" = list(
          before = "[ERROR] ",
          color = "red",
          "font-weight" = "bold"
        ),
      
        # Lists
        "ul li" = list(
          before = "• "
        ),
      
        "ol li" = list(
          "list-style-type" = "decimal"
        )
      )
      ```
      
      ### Extending Built-in Themes
      
      Merge your theme with the built-in theme:
      
      ```r
      my_theme <- utils::modifyList(
        builtin_theme(),
        list(
          ".code" = list(color = "magenta"),
          ".custom" = list(color = "cyan")
        )
      )
      
      cli_div(theme = my_theme)
      ```
      
      ### Theme Functions
      
      Create reusable theme functions:
      
      ```r
      create_brand_theme <- function(primary_color = "blue") {
        list(
          "h1" = list(color = primary_color, "font-weight" = "bold"),
          "h2" = list(color = primary_color),
          ".code" = list(color = primary_color),
          ".alert-success" = list(color = "green"),
          ".alert-danger" = list(color = "red")
        )
      }
      
      cli_div(theme = create_brand_theme("purple"))
      ```
      
      ## App and Package Themes
      
      ### Setting Package Theme
      
      Define a package-level theme in your `.onLoad()`:
      
      ```r
      .onLoad <- function(libname, pkgname) {
        my_theme <- list(
          ".code" = list(color = "blue"),
          ".pkg" = list(color = "cyan")
        )
      
        options(cli.theme = my_theme)
      }
      ```
      
      ### User Configuration
      
      Users can override package themes via options:
      
      ```r
      # In .Rprofile
      options(cli.theme = list(
        ".code" = list(color = "magenta")
      ))
      ```
      
      ### Conditional Themes
      
      Apply themes based on environment:
      
      ```r
      .onLoad <- function(libname, pkgname) {
        theme <- if (cli::num_ansi_colors() >= 256) {
          rich_theme()  # Full color theme
        } else {
          simple_theme()  # Basic theme
        }
      
        options(cli.theme = theme)
      }
      ```
      
      ### Theme Precedence
      
      Themes are applied in this order (highest to lowest):
      1. Inline theme in `cli_div(theme = ...)`
      2. User's `cli.theme` option
      3. Package's default theme
      4. Built-in theme
      
      ## Color Palettes
      
      ### Configuring Palettes
      
      Set the ANSI color palette with the `cli.palette` option:
      
      ```r
      options(cli.palette = "vscode")
      ```
      
      ### Built-in Palettes
      
      **`dichro`** - Dichromat-friendly palette:
      ```r
      options(cli.palette = "dichro")
      ```
      
      **`vscode`** - VS Code color scheme:
      ```r
      options(cli.palette = "vscode")
      ```
      
      **`iterm`** - iTerm2 default colors:
      ```r
      options(cli.palette = "iterm")
      ```
      
      ### Custom 16-Color Palettes
      
      Define custom ANSI colors:
      
      ```r
      my_palette <- c(
        # Normal colors (0-7)
        "#000000",  # black
        "#CD0000",  # red
        "#00CD00",  # green
        "#CDCD00",  # yellow
        "#0000EE",  # blue
        "#CD00CD",  # magenta
        "#00CDCD",  # cyan
        "#E5E5E5",  # white
      
        # Bright colors (8-15)
        "#7F7F7F",  # bright black (grey)
        "#FF0000",  # bright red
        "#00FF00",  # bright green
        "#FFFF00",  # bright yellow
        "#5C5CFF",  # bright blue
        "#FF00FF",  # bright magenta
        "#00FFFF",  # bright cyan
        "#FFFFFF"   # bright white
      )
      
      options(cli.palette = my_palette)
      ```
      
      ### Truecolor Support
      
      Check for truecolor support:
      
      ```r
      cli::num_ansi_colors()
      # Returns:
      #   1 - no color
      #   8 - 8 colors
      #   256 - 256 colors
      #   16777216 - truecolor (24-bit)
      ```
      
      Use truecolor when available:
      
      ```r
      if (cli::num_ansi_colors() >= 16777216) {
        # Use RGB hex colors
        list(".code" = list(color = "#6A9FB5"))
      } else {
        # Fall back to named colors
        list(".code" = list(color = "blue"))
      }
      ```
      
      ### Color Detection
      
      CLI automatically detects color support from:
      - `NO_COLOR` environment variable (disables color)
      - `TERM` environment variable
      - System capabilities
      - RStudio version
      
      Force color support:
      
      ```r
      options(cli.num_colors = 256)  # Force 256 colors
      ```
      
      Disable colors:
      
      ```r
      options(cli.num_colors = 1)
      # Or set environment variable
      Sys.setenv(NO_COLOR = "1")
      ```
      
      ## Accessibility
      
      ### Color Contrast
      
      Ensure sufficient contrast for readability:
      
      ```r
      # Good contrast
      list(
        ".code" = list(color = "blue"),        # Dark on light
        ".emph" = list(color = "ansi_red")     # High contrast
      )
      
      # Poor contrast (avoid)
      list(
        ".code" = list(color = "grey80"),      # Low contrast on white
        ".emph" = list(color = "#EEEEEE")      # Nearly invisible on white
      )
      ```
      
      ### Unicode Fallbacks
      
      Provide ASCII alternatives for Unicode symbols:
      
      ```r
      bullet <- if (cli::is_utf8_output()) "\u2022" else "*"
      
      list(
        "ul li:before" = list(content = bullet)
      )
      ```
      
      Built-in Unicode detection:
      
      ```r
      cli::is_utf8_output()  # TRUE if UTF-8 is supported
      ```
      
      ### Color-blind Friendly Themes
      
      Use the dichromat palette or ensure patterns work without color:
      
      ```r
      color_blind_theme <- list(
        ".alert-success" = list(
          before = "[OK] ",
          color = "green"
        ),
        ".alert-danger" = list(
          before = "[ERROR] ",
          color = "red",
          "font-weight" = "bold"
        )
      )
      ```
      
      Benefits:
      - Symbols provide meaning without color
      - Bold text adds emphasis
      - Works for colorblind users
      
      ### Screen Reader Compatibility
      
      Keep semantic meaning in text, not just styling:
      
      ```r
      # Good: Meaning is in text
      cli_alert_success("File saved successfully")
      
      # Poor: Meaning only in style
      cli_text("{.green File saved}")
      ```
      
      ## Debugging Themes
      
      ### Using cli_debug_doc()
      
      Visualize document structure and applied themes:
      
      ```r
      # Enable debug mode
      withr::local_options(cli.debug = TRUE)
      
      cli_div(class = "my-section")
      cli_h1("Header")
      cli_text("Text with {.code code}")
      cli_end()
      ```
      
      Debug output shows:
      - Element hierarchy
      - Applied selectors
      - Computed properties
      - Theme inheritance
      
      ### Inspecting Theme Application
      
      Check which theme properties are applied:
      
      ```r
      # Create a test container
      cli_div(theme = my_theme, class = "test")
      
      # Debug output will show:
      # - Matched selectors
      # - Applied properties
      # - Inherited values
      
      cli_text("Test content")
      cli_end()
      ```
      
      ### Theme Testing Strategy
      
      Test themes across different environments:
      
      ```r
      test_theme <- function(theme) {
        old_colors <- options(cli.num_colors = 256)
        on.exit(options(old_colors))
      
        cli_div(theme = theme)
        cli_h1("Header")
        cli_alert_success("Success message")
        cli_text("Code: {.code mean(x)}")
        cli_end()
      }
      
      # Test with different color depths
      test_colors <- c(1, 8, 256, 16777216)
      for (n in test_colors) {
        options(cli.num_colors = n)
        test_theme(my_theme)
      }
      ```
      
      ### Common Theme Issues
      
      **Colors not appearing:**
      - Check `cli::num_ansi_colors()` output
      - Verify terminal supports colors
      - Check for `NO_COLOR` environment variable
      
      **Spacing incorrect:**
      - Use `cli.debug = TRUE` to see computed margins
      - Check for inherited spacing properties
      - Verify units (characters vs. lines)
      
      **Selectors not matching:**
      - Use `cli.debug = TRUE` to see selector matching
      - Check selector syntax (spaces for descendants)
      - Verify class names match inline markup
      
  • SKILL.md 11 KB
    ---
    name: cli
    description: >
      Comprehensive R package for command-line interface styling, semantic messaging,
      and user communication. Use this skill when working with R code that needs to:
      (1) Format console output with inline markup and colors,
      (2) Display errors, warnings, or messages with cli_abort/cli_warn/cli_inform,
      (3) Show progress indicators for long-running operations,
      (4) Create semantic CLI elements (headers, lists, alerts, code blocks),
      (5) Apply themes and customize output styling,
      (6) Handle pluralization in user-facing text,
      (7) Work with ANSI strings, hyperlinks, or custom containers.
      Also use when migrating from base R message/warning/stop, debugging cli code,
      or improving existing cli usage.
    metadata:
      author: Garrick Aden-Buie (@gadenbuie)
      version: "1.0"
    license: MIT
    ---
    
    # CLI for R Packages
    
    ## When to Use What
    
    task: Display error with context and formatting
    use: `cli_abort()` with inline markup and bullet lists
    
    task: Show warning with formatting
    use: `cli_warn()` with inline markup
    
    task: Display informative message
    use: `cli_inform()` with inline markup
    
    task: Show progress for counted operations
    use: `cli_progress_bar()` with total count
    
    task: Show simple progress steps
    use: `cli_progress_step()` with status messages
    
    task: Format code or function names
    use: `{.code ...}` or `{.fn package::function}`
    
    task: Format file paths
    use: `{.file path/to/file}`
    
    task: Format package names
    use: `{.pkg packagename}`
    
    task: Format variable names
    use: `{.var variable_name}`
    
    task: Format values
    use: `{.val value}`
    
    task: Handle singular/plural text
    use: `{?s}` or `{?y/ies}` with pluralization
    
    task: Create headers
    use: `cli_h1()`, `cli_h2()`, `cli_h3()`
    
    task: Create alerts
    use: `cli_alert_success()`, `cli_alert_danger()`, `cli_alert_warning()`, `cli_alert_info()`
    
    task: Create lists
    use: `cli_ul()`, `cli_ol()`, `cli_dl()` with `cli_li()`
    
    ## Inline Markup Essentials
    
    Use inline markup with `{.class content}` syntax to format text:
    
    ```r
    # Basic formatting
    cli_text("Function {.fn mean} calculates averages")
    cli_text("Install package {.pkg dplyr}")
    cli_text("See file {.file ~/.Rprofile}")
    cli_text("{.var x} must be numeric, not {.obj_type_of {x}}")
    cli_text("Got value {.val {x}}")
    
    # Code formatting
    cli_text("Use {.code sum(x, na.rm = TRUE)}")
    
    # Paths and arguments
    cli_text("Reading from {.path /data/file.csv}")
    cli_text("Set {.arg na.rm} to TRUE")
    
    # Types and classes
    cli_text("Object is {.cls data.frame}")
    
    # Emphasis
    cli_text("This is {.emph important}")
    cli_text("This is {.strong critical}")
    
    # Fields
    cli_text("The {.field name} field is required")
    ```
    
    ### Vector Collapsing
    
    Vectors are automatically collapsed with commas and "and":
    
    ```r
    pkgs <- c("dplyr", "tidyr", "ggplot2")
    cli_text("Installing packages: {.pkg {pkgs}}")
    #> Installing packages: dplyr, tidyr, and ggplot2
    
    files <- c("data.csv", "script.R")
    cli_text("Found {length(files)} file{?s}: {.file {files}}")
    #> Found 2 files: data.csv and script.R
    ```
    
    ### Escaping Braces
    
    Use double braces `{{` and `}}` to escape literal braces:
    
    ```r
    cli_text("Use {{variable}} syntax in glue")
    #> Use {variable} syntax in glue
    ```
    
    **For complete markup reference**: See [references/inline-markup.md](references/inline-markup.md) for all 50+ inline classes, edge cases, nesting rules, and advanced patterns.
    
    ## Pluralization Basics
    
    Use `{?}` for pluralization with three patterns:
    
    ### Single Alternative
    
    ```r
    nfile <- 1
    cli_text("Found {nfile} file{?s}")
    #> Found 1 file
    
    nfile <- 3
    cli_text("Found {nfile} file{?s}")
    #> Found 3 files
    ```
    
    ### Two Alternatives
    
    ```r
    ndir <- 1
    cli_text("Found {ndir} director{?y/ies}")
    #> Found 1 directory
    
    ndir <- 5
    cli_text("Found {ndir} director{?y/ies}")
    #> Found 5 directories
    ```
    
    ### Three Alternatives (zero/one/many)
    
    ```r
    nfile <- 0
    cli_text("Found {nfile} file{?s}: {?no/the/the} file{?s}")
    #> Found 0 files: no files
    
    nfile <- 1
    cli_text("Found {nfile} file{?s}: {?no/the/the} file{?s}")
    #> Found 1 file: the file
    
    nfile <- 3
    cli_text("Found {nfile} file{?s}: {?no/the/the} file{?s}")
    #> Found 3 files: the files
    ```
    
    ### Helpers: qty() and no()
    
    Use `no()` to display "no" instead of zero:
    
    ```r
    nfile <- 0
    cli_text("Found {no(nfile)} file{?s}")
    #> Found no files
    ```
    
    Use `qty()` to set quantity explicitly:
    
    ```r
    nupd <- 3
    ntotal <- 10
    cli_text("{nupd}/{ntotal} {qty(nupd)} file{?s} {?needs/need} updates")
    #> 3/10 files need updates
    ```
    
    **For advanced pluralization**: See [references/inline-markup.md](references/inline-markup.md) for edge cases and complex patterns.
    
    ## CLI Conditions: Core Patterns
    
    Use cli conditions instead of base R for better formatting:
    
    ### cli_abort() - Formatted Errors
    
    ```r
    # Before (base R)
    stop("File not found: ", path)
    
    # After (cli)
    cli_abort("File {.file {path}} not found")
    
    # With bullets for context
    check_file <- function(path) {
      if (!file.exists(path)) {
        cli_abort(c(
          "File not found",
          "x" = "Cannot read {.file {path}}",
          "i" = "Check that the file exists"
        ))
      }
    }
    ```
    
    ### cli_warn() - Formatted Warnings
    
    ```r
    # Before (base R)
    warning("Column ", col, " has missing values")
    
    # After (cli)
    cli_warn("Column {.field {col}} has missing values")
    
    # With context
    cli_warn(c(
      "Data quality issues detected",
      "!" = "Column {.field {col}} has {n_missing} missing value{?s}",
      "i" = "Consider using {.fn tidyr::drop_na}"
    ))
    ```
    
    ### cli_inform() - Formatted Messages
    
    ```r
    # Before (base R)
    message("Processing ", n, " files")
    
    # After (cli)
    cli_inform("Processing {n} file{?s}")
    
    # With structure
    cli_inform(c(
      "v" = "Successfully loaded {.pkg dplyr}",
      "i" = "Version {packageVersion('dplyr')}"
    ))
    ```
    
    ### Bullet Types
    
    - `"x"` - Error/problem (red X)
    - `"!"` - Warning (yellow !)
    - `"i"` - Information (blue i)
    - `"v"` - Success (green checkmark)
    - `"*"` - Bullet point
    - `">"` - Arrow/pointer
    
    **For advanced error design**: See [references/conditions.md](references/conditions.md) for error design principles, rlang integration, testing strategies, and real-world patterns.
    
    ## Basic Progress Indicators
    
    ### Simple Progress Steps
    
    ```r
    process_data <- function() {
      cli_progress_step("Loading data")
      data <- load_data()
    
      cli_progress_step("Cleaning data")
      clean <- clean_data(data)
    
      cli_progress_step("Analyzing data")
      analyze(clean)
    }
    ```
    
    ### Basic Progress Bar
    
    ```r
    process_files <- function(files) {
      cli_progress_bar("Processing files", total = length(files))
    
      for (file in files) {
        process_file(file)
        cli_progress_update()
      }
    }
    ```
    
    ### Auto-Cleanup
    
    Progress bars auto-close when the function exits:
    
    ```r
    process <- function() {
      cli_progress_bar("Working", total = 100)
      for (i in 1:100) {
        Sys.sleep(0.01)
        cli_progress_update()
      }
      # No need to call cli_progress_done() - auto-closes
    }
    ```
    
    **For advanced progress**: See [references/progress.md](references/progress.md) for nested progress, custom formats, parallel processing, all progress variables, and Shiny integration.
    
    ## Semantic CLI Elements
    
    ### Headers
    
    ```r
    cli_h1("Main Section")
    cli_h2("Subsection")
    cli_h3("Detail")
    ```
    
    ### Alerts
    
    ```r
    cli_alert_success("Operation completed successfully")
    cli_alert_danger("Critical error occurred")
    cli_alert_warning("Potential issue detected")
    cli_alert_info("Additional information available")
    ```
    
    ### Text and Code
    
    ```r
    # Regular text with markup
    cli_text("This is formatted text with {.emph emphasis}")
    
    # Code blocks
    cli_code(c(
      "library(dplyr)",
      "mtcars %>% filter(mpg > 20)"
    ))
    
    # Verbatim text (no formatting)
    cli_verbatim("This is displayed exactly as-is: {not interpolated}")
    ```
    
    ### Lists
    
    ```r
    # Unordered list
    cli_ul()
    cli_li("First item")
    cli_li("Second item")
    cli_end()
    
    # Ordered list
    cli_ol()
    cli_li("First step")
    cli_li("Second step")
    cli_end()
    
    # Definition list
    cli_dl()
    cli_li(c(name = "The name field"))
    cli_li(c(email = "The email address"))
    cli_end()
    ```
    
    ## Common Workflows
    
    ### Base R to CLI Migration
    
    ```r
    # Before: Base R error handling
    validate_input <- function(x, y) {
      if (!is.numeric(x)) {
        stop("x must be numeric")
      }
      if (length(y) == 0) {
        stop("y cannot be empty")
      }
      if (length(x) != length(y)) {
        stop("x and y must have the same length")
      }
    }
    
    # After: CLI error handling
    validate_input <- function(x, y) {
      if (!is.numeric(x)) {
        cli_abort(c(
          "{.arg x} must be numeric",
          "x" = "You supplied a {.cls {class(x)}} vector",
          "i" = "Use {.fn as.numeric} to convert"
        ))
      }
    
      if (length(y) == 0) {
        cli_abort(c(
          "{.arg y} cannot be empty",
          "i" = "Provide at least one element"
        ))
      }
    
      if (length(x) != length(y)) {
        cli_abort(c(
          "{.arg x} and {.arg y} must have the same length",
          "x" = "{.arg x} has length {length(x)}",
          "x" = "{.arg y} has length {length(y)}"
        ))
      }
    }
    ```
    
    ### Error Message with Rich Context
    
    ```r
    check_required_columns <- function(data, required_cols) {
      actual_cols <- names(data)
      missing_cols <- setdiff(required_cols, actual_cols)
    
      if (length(missing_cols) > 0) {
        cli_abort(c(
          "Required column{?s} missing from data",
          "x" = "Missing {length(missing_cols)} column{?s}: {.field {missing_cols}}",
          "i" = "Data has {length(actual_cols)} column{?s}: {.field {actual_cols}}",
          "i" = "Add the missing column{?s} or check for typos"
        ))
      }
    
      invisible(data)
    }
    ```
    
    ### Function with Progress Bar
    
    ```r
    process_files <- function(files, verbose = TRUE) {
      n <- length(files)
    
      if (verbose) {
        cli_progress_bar(
          format = "Processing {cli::pb_bar} {cli::pb_current}/{cli::pb_total} [{cli::pb_eta}]",
          total = n
        )
      }
    
      results <- vector("list", n)
    
      for (i in seq_along(files)) {
        results[[i]] <- process_file(files[[i]])
    
        if (verbose) {
          cli_progress_update()
        }
      }
    
      results
    }
    ```
    
    ## Resources & Advanced Topics
    
    ### Reference Files
    
    - **[references/inline-markup.md](references/inline-markup.md)** - Complete catalog of inline classes organized by category, advanced patterns, nesting rules, and real-world examples
    
    - **[references/conditions.md](references/conditions.md)** - Advanced error design patterns, rlang integration, testing with testthat snapshots, migration guide, and anti-patterns
    
    - **[references/progress.md](references/progress.md)** - Nested progress bars, custom formats, all progress variables, parallel processing, Shiny integration, and debugging
    
    - **[references/themes.md](references/themes.md)** - Complete theming system with CSS-like selectors, container functions, color palettes, custom themes, and accessibility
    
    - **[references/ansi-operations.md](references/ansi-operations.md)** - ANSI string operations (align, columns, nchar, etc.), hyperlinks, color detection, testing CLI output, and troubleshooting
    
    ### External Resources
    
    - [cli package documentation](https://cli.r-lib.org)
    - [cli GitHub repository](https://github.com/r-lib/cli)
    - [Building a semantic CLI (article)](https://cli.r-lib.org/articles/semantic-cli.html)
    
    ### Related Packages
    
    - **rlang** - Condition handling and error objects integrate with cli
    - **glue** - String interpolation powers cli's `{}` syntax
    - **testthat** - Snapshot testing for cli output
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related