---
title: "Console Messaging with handyCli"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Console Messaging with handyCli}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment  = "#>"
)
```

```{r setup}
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.

```{r basic-messages}
msg_info("Loading configuration from disk")
msg_success("All 42 concept sets validated")
msg_warn("Column 'mappedSourceCode' is missing — using NA")
msg_danger("Connection pool exhausted (non-fatal, retrying)")
msg_process("Uploading results to the remote schema")
msg_bullet("concept_id 201826 — Type 2 Diabetes Mellitus")
msg_todo("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.

```{r layout}
msg_header("Step 1: Validate Input")
msg_info("Checking required columns")
msg_success("Validation passed")
msg_blank()
msg_header("Step 2: Build Cohort")
msg_process("Assembling concept set expressions")
msg_rule()
msg_info("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.

```{r msg-list}
domains <- c(
  Condition   = "201826, 442793",
  Drug        = "1503297, 40163554",
  Measurement = "3004501"
)
msg_list(domains, header = "Concept set domains")
```

### Key-value table

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

```{r msg-kv}
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)
```

---

## 4  Iteration patterns

### 4a  Logging inside a loop

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

```{r iteration-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()
}
```

### 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.

```{r iteration-safe}
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
    }
  )
}
```

### 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.

```{r verbose-pattern}
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")

# 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.

```{r timing-simple}
result <- msg_timed(
  expr  = Sys.sleep(0.05),
  msg   = "Sleeping"
)
```

### Timing an iteration

```{r timing-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
  })
)
```

---

## 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.

```{r msg-abort}
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))
)
```

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

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

```{r msg-warning}
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")
  }
)
```

### `msg_try()` on_error modes

```{r msg-try-modes}
# "warn"    — downgrade error to a styled warning
msg_try(stop("something went wrong"), on_error = "warn")

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

# "ignore"  — silently swallow the error
msg_try(stop("ignored error"),        on_error = "ignore")
msg_info("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.

```{r msg-debug-off}
# 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)")
```

```{r msg-debug-on}
options(pkg.debug = TRUE)
msg_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.

```{r progress-example, eval = FALSE}
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.

```{r spinner-example, eval = FALSE}
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.

```{r full-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)
```
