# Console Messaging with handyCli

    library(tidyOhdsiSolutions)

    # Disable ANSI colour codes so rendered HTML output is clean
    options(pkg.no_color = TRUE)

## Overview

`handyCli` is a zero-dependency console messaging system built on base R. It provides styled, consistently prefixed messages for every severity level, structured output helpers (`msg_list()`, `msg_kv()`), progress and spinner indicators, a timing wrapper, and safe error-handling utilities.

All functions write to `message()` (stderr) so they are silenceable with `suppressMessages()` and do not pollute `stdout()` or function return values.

## 1 Basic message levels

Each message type carries a distinct prefix symbol.

    msg_info("Loading configuration from disk")
    #> i Loading configuration from disk
    msg_success("All 42 concept sets validated")
    #> v All 42 concept sets validated
    msg_warn("Column 'mappedSourceCode' is missing — using NA")
    #> ! Column 'mappedSourceCode' is missing — using NA
    msg_danger("Connection pool exhausted (non-fatal, retrying)")
    #> x Connection pool exhausted (non-fatal, retrying)
    msg_process("Uploading results to the remote schema")
    #> -> Uploading results to the remote schema
    msg_bullet("concept_id 201826 — Type 2 Diabetes Mellitus")
    #> * concept_id 201826 — Type 2 Diabetes Mellitus
    msg_todo("Verify descendant flag for hypertension concept set")
    #> - Verify descendant flag for hypertension concept set

## 2 Section structure: headers, rules, and blank lines

Use `msg_header()` and `msg_rule()` to visually group output in long-running pipelines.

    msg_header("Step 1: Validate Input")
    #> --------------------------- Step 1: Validate Input ---------------------------
    msg_info("Checking required columns")
    #> i Checking required columns
    msg_success("Validation passed")
    #> v Validation passed
    msg_blank()
    #>
    msg_header("Step 2: Build Cohort")
    #> ---------------------------- Step 2: Build Cohort ----------------------------
    msg_process("Assembling concept set expressions")
    #> -> Assembling concept set expressions
    msg_rule()
    #> --------------------------------------------------------------------------------
    msg_info("Pipeline complete")
    #> i Pipeline complete

## 3 Structured output: lists and key-value tables

### Named list

`msg_list()` is useful for displaying a set of labelled items, such as cohort components or domain breakdowns.

    domains <- c(
      Condition   = "201826, 442793",
      Drug        = "1503297, 40163554",
      Measurement = "3004501"
    )
    msg_list(domains, header = "Concept set domains")
    #> i Concept set domains
    #>   > Condition: 201826, 442793
    #>   > Drug: 1503297, 40163554
    #>   > Measurement: 3004501

### Key-value table

`msg_kv()` aligns keys and values into two columns — handy for configuration summaries or run metadata.

    run_info <- list(
      Package   = "tidyOhdsiSolutions",
      Version   = as.character(packageVersion("tidyOhdsiSolutions")),
      R_version = paste0(R.version$major, ".", R.version$minor),
      Date      = format(Sys.Date())
    )
    msg_kv(run_info)
    #>   Package    tidyOhdsiSolutions
    #>   Version    0.1.0
    #>   R_version  4.5.3
    #>   Date       2026-04-08

## 4 Iteration patterns

### 4a Logging inside a loop

Combine `msg_header()` and message functions to annotate each iteration of a processing loop.

    concept_sets <- list(
      diabetes     = c(201826L, 442793L),
      hypertension = c(320128L),
      obesity      = c(433736L, 4215968L)
    )

    for (nm in names(concept_sets)) {
      msg_header(nm)
      msg_info("Concepts: ", paste(concept_sets[[nm]], collapse = ", "))
      msg_success("Processed ", length(concept_sets[[nm]]), " concept(s)")
      msg_blank()
    }
    #> ---------------------------------- diabetes ----------------------------------
    #> i Concepts: 201826, 442793
    #> v Processed 2 concept(s)
    #>
    #> -------------------------------- hypertension --------------------------------
    #> i Concepts: 320128
    #> v Processed 1 concept(s)
    #>
    #> ---------------------------------- obesity -----------------------------------
    #> i Concepts: 433736, 4215968
    #> v Processed 2 concept(s)
    #>

### 4b Safe iteration with `msg_try()`

`msg_try()` wraps an expression so errors and warnings are caught and styled without stopping the loop. The `on_error` argument controls the behaviour: `"warn"` downgrades errors to styled warnings; `"ignore"` silences them.

    sources <- list(
      schema_a = list(valid = TRUE,  rows = 1200L),
      schema_b = list(valid = FALSE, rows = 0L),
      schema_c = list(valid = TRUE,  rows = 850L)
    )

    results <- vector("list", length(sources))
    names(results) <- names(sources)

    for (nm in names(sources)) {
      results[[nm]] <- msg_try(
        on_error = "warn",
        expr = {
          src <- sources[[nm]]
          if (!src$valid) stop("Schema '", nm, "' failed validation")
          msg_success(nm, ": ", src$rows, " rows loaded")
          src$rows
        }
      )
    }
    #> v schema_a: 1200 rows loaded
    #> ! Schema 'schema_b' failed validation
    #> v schema_c: 850 rows loaded

### 4c Verbose mode — conditional logging inside helpers

`msg_verbose()` only emits output when `getOption("pkg.verbose")` is `TRUE` (or the `verbose` argument is set explicitly). This lets callers opt in/out without modifying the function body.

    process_file <- function(path, verbose = TRUE) {
      msg_verbose("Opening: ", path, verbose = verbose)
      # ... processing ...
      msg_verbose("Done:    ", path, verbose = verbose)
      invisible(path)
    }

    # Verbose on (default)
    process_file("data/concepts.csv")
    #> i Opening: data/concepts.csv
    #> i Done:    data/concepts.csv

    # Verbose off
    process_file("data/concepts.csv", verbose = FALSE)

## 5 Timing expressions with `msg_timed()`

`msg_timed()` evaluates an expression, prints a labelled elapsed-time message, and returns the result invisibly. It is composable — the timed block can be any R expression, including a whole pipeline.

    result <- msg_timed(
      expr  = Sys.sleep(0.05),
      msg   = "Sleeping"
    )
    #> i Sleeping: 0.08s

### Timing an iteration

    concept_ids <- as.list(c(201826L, 442793L, 320128L, 433736L))

    processed <- msg_timed(
      msg  = "Total batch time",
      expr = lapply(concept_ids, function(id) {
        msg_info("Processing concept_id ", id)
        id * 2L           # stand-in for real work
      })
    )
    #> i Processing concept_id 201826
    #> i Processing concept_id 442793
    #> i Processing concept_id 320128
    #> i Processing concept_id 433736
    #> i Total batch time: 0.00s

## 6 Error and warning handling

### Raising styled errors with `msg_abort()`

`msg_abort()` throws a proper R error condition so it integrates with `tryCatch()` and `withCallingHandlers()` like any other error.

    validate_schema <- function(x) {
      if (!is.data.frame(x)) {
        msg_abort("Expected a data.frame, got: ", class(x)[1])
      }
      invisible(x)
    }

    # Catch the error and show its message
    tryCatch(
      validate_schema("not a data frame"),
      error = function(e) msg_danger("Caught: ", conditionMessage(e))
    )
    #> x Caught: x Expected a data.frame, got: character

### Raising styled warnings with `msg_warning()`

`msg_warning()` emits a proper R warning while also printing a styled console message.

    withCallingHandlers(
      {
        msg_warning("Deprecated argument 'schema' — use 'cdm_schema' instead")
        msg_info("Continuing with default")
      },
      warning = function(w) {
        # Muffle so it does not print twice
        invokeRestart("muffleWarning")
      }
    )
    #> i Continuing with default

### `msg_try()` on_error modes

    # "warn"    — downgrade error to a styled warning
    msg_try(stop("something went wrong"), on_error = "warn")
    #> ! something went wrong

    # "message" — emit as a styled danger message, no stop
    msg_try(stop("non-critical failure"), on_error = "message")
    #> x non-critical failure

    # "ignore"  — silently swallow the error
    msg_try(stop("ignored error"),        on_error = "ignore")
    msg_info("Execution continued after all three")
    #> i Execution continued after all three

## 7 Debug messages

`msg_debug()` is a no-op unless `options(pkg.debug = TRUE)` is set, making it safe to leave in production code.

    # Default: pkg.debug = FALSE, so nothing is printed
    msg_debug("SQL query: SELECT * FROM concept WHERE ...")
    msg_info("(no debug output above — pkg.debug is FALSE)")
    #> i (no debug output above — pkg.debug is FALSE)

    options(pkg.debug = TRUE)
    msg_debug("SQL query: SELECT * FROM concept WHERE ...")
    #> [DEBUG] SQL query: SELECT * FROM concept WHERE ...
    options(pkg.debug = FALSE)   # reset

## 8 Progress bar (interactive sessions)

`msg_progress()` is designed for interactive terminal use. In rendered documents the `\r` cursor updates do not display, so the code below is shown but not evaluated.

    files <- paste0("file_", seq_len(8), ".csv")
    pb    <- msg_progress(length(files), prefix = "Loading")

    for (f in files) {
      Sys.sleep(0.1)   # simulated I/O
      pb$tick()
    }
    pb$done("All files loaded")

## 9 Spinner (interactive sessions)

Similarly, the animated spinner is for interactive use only.

    sp <- msg_spinner("Querying vocabulary server")

    for (i in seq_len(30)) {
      Sys.sleep(0.05)
      sp$spin()
    }
    sp$done("Query complete")

## 10 Putting it all together — annotated pipeline

The example below combines several `handyCli` helpers to produce readable console output for a multi-step pipeline.

    run_pipeline <- function(concept_sets, verbose = TRUE) {

      msg_header("tidyOhdsiSolutions Pipeline")
      msg_kv(list(
        Steps       = as.character(length(concept_sets)),
        Verbose     = as.character(verbose)
      ))
      msg_blank()

      results <- vector("list", length(concept_sets))
      names(results) <- names(concept_sets)

      for (nm in names(concept_sets)) {
        msg_process("Processing: ", nm)

        results[[nm]] <- msg_try(on_error = "warn", expr = {
          ids <- concept_sets[[nm]]
          if (length(ids) == 0L) stop("'", nm, "' has no concept IDs")
          msg_verbose("  concept IDs: ", paste(ids, collapse = ", "),
                      verbose = verbose)
          ids
        })
      }

      msg_blank()
      msg_rule()

      succeeded <- sum(!vapply(results, is.null, logical(1L)))
      msg_success(succeeded, " / ", length(concept_sets), " concept sets processed")

      invisible(results)
    }

    concept_sets <- list(
      diabetes     = c(201826L, 442793L),
      hypertension = c(320128L),
      empty_set    = integer(0)      # will trigger a warning
    )

    out <- run_pipeline(concept_sets, verbose = TRUE)
    #> ------------------------ tidyOhdsiSolutions Pipeline -------------------------
    #>   Steps    3
    #>   Verbose  TRUE
    #>
    #> -> Processing: diabetes
    #> i   concept IDs: 201826, 442793
    #> -> Processing: hypertension
    #> i   concept IDs: 320128
    #> -> Processing: empty_set
    #> ! 'empty_set' has no concept IDs
    #>
    #> --------------------------------------------------------------------------------
    #> v 2 / 3 concept sets processed