Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .Rbuildignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,4 @@
^[\.]?air\.toml$
^\.vscode$
^[.]cache$
^CLAUDE.md$"
74 changes: 74 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What this is

`cli` is an R package for building command line interfaces: semantic elements (headings, lists, alerts, paragraphs), CSS-like theming, ANSI colors/styles, progress bars, rich error/warning messages, and pluralization. It has both an R layer and a C layer (`src/`), and is a foundational dependency for much of the R ecosystem, so backward compatibility and correctness matter a lot.

## Development commands

This package has compiled C code, so you must recompile after editing anything in `src/`. Use the `uncovr` helpers (they handle compilation + instrumentation):

```r
uncovr::reload() # compile C code and (re)load the package
uncovr::test() # run the test suite (testthat, edition 3)
uncovr::document() # regenerate roxygen2 docs (man/*.Rd and NAMESPACE)
```

To run R CMD check (set `NOT_CRAN` so tests that are skipped on CRAN still run):

```r
withr::with_envvar(c(NOT_CRAN = "true"), rcmdcheck::rcmdcheck())
```

Running a single test file or a single test:

```r
uncovr::test(filter = "keypress") # run tests/testthat/test-keypress.R
```

Code is formatted with [air](https://posit-dev.github.io/air/) (see `air.toml`). A GitHub Action suggests formatting fixes on PRs.

## Architecture

### R / C split

The semantic CLI, theming, and most formatting logic live in R (`R/`). Performance-sensitive and OS-level primitives live in C (`src/`):

- ANSI/UTF-8/string-width handling (`ansi.c`, `utf8.c`, `width.c`-related, `charwidth.h`)
- the VT100 parser (`vt.c`, `vtparse*.c`) used to interpret/strip terminal control sequences
- the progress bar engine (`progress.c`, `progress-altrep.c`) — progress state is shared with R via an ALTREP
- keypress reading (`keypress*.c`, split into `keypress-unix.c` / `keypress-win.c`)
- hashing (`md5.c`, `sha1.c`, `sha256.c`, `xxhash*.c`) and `diff.c`, `glue.c`

C entry points are registered in `src/init.c` via `.Call`. `RCC(...)` registers functions that use the **cleancall** mechanism (`cleancall.c/.h`) for C-level resource cleanup; plain `R_CallMethodDef` entries (e.g. `cli_keypress`) are registered the normal way. When you add a C function callable from R, register it in `init.c`. Header `inst/include/cli/progress.h` is the public C API other packages link against — treat changes to it as part of the package's external contract.

### The "app" model

CLI output flows through a stack of **app** objects, not direct printing. `start_app()` / `stop_app()` / `default_app()` (in `R/app.R`) manage a global app stack in `cliappenv$stack`. An app (`R/cliapp.R`) is a closure-based object (via `new_class`) holding the active themes, container stack, and output connection. The user-facing `cli_*` functions (e.g. `cli_h1`, `cli_alert`, `cli_ul`) emit a *condition* (a `cliMessage`) that the default app formats and prints. The internal counterparts are named `clii_*` (app methods) and `clii__*` (lower-level helpers).

`cli({ ... })` (in `R/cli.R`) records multiple `cli_*` calls and emits them as one combined message, using the `cli.record` option and the `cli_recorded` registry. Themes are CSS-like selector/style rules matched against the container tree (`R/themes.R`, `R/simple-theme.R`, `R/containers.R`).

### Inline markup and glue

cli text supports interpreted string literals via glue, plus inline classes like `{.url ...}`, `{.file ...}`, `{.emph ...}`. Inline span handling is in `R/inline.R`; glue integration in `R/glue.R`; pluralization (`{?s}`, `{qty()}`) in `R/pluralize.R`.

### Loading & global state

`R/onload.R` sets up package-level mutable state in the `clienv` environment (PID, timers, progress/status registries, load time). Note the `.onLoad` cursor-restore finalizer and task callback. Timing is configurable via env vars (`CLI_TICK_TIME`, `CLI_SPEED_TIME`, `R_CLI_HIDE_CURSOR`).

## Testing conventions

- testthat edition 3 with snapshot tests. Snapshots live in `tests/testthat/_snaps/`. After an intentional output change, review `testthat::snapshot_review()` / accept with `testthat::snapshot_accept()`.
- `tests/testthat/setup.R` flushes gcov coverage data on teardown (`clic__gcov_flush`) and cleans `.gcda` files — this supports the coverage-instrumented test runs.
- `tests/testthat/helper.R` defines capture helpers central to testing output: `capture_msgs()`, `capture_cli_messages()` (catches `cliMessage` conditions), `capt()`, and `local_cli_config()`. Use these rather than asserting on raw printed output.
- `progresstest/` and `progresstestcpp/` are small embedded test packages exercising the C progress API from C and C++.
- Many tests are environment-sensitive (terminal width, number of ANSI colors, UTF-8 support, TTY detection). Tests pin these via `local_cli_config()` / options so they are reproducible off a real terminal.

## Documentation

- Roxygen2 (version 8.0.0) generates `man/` and `NAMESPACE` — never edit those by hand; edit the roxygen comments and run `uncovr::document()`.
- Many `.Rd` examples use **asciicast** ` ```{asciicast ...} ` code chunks (rendered to SVG for the website) rather than plain `\examples`. Match the surrounding style when adding examples.
- `README.md` is generated from `README.Rmd` (via `make` / `Makefile`) — edit the `.Rmd`.
- Update `NEWS.md` for user-facing changes.
6 changes: 6 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# cli (development version)

* `keypress()` improvements:
- `timeout` argument to wait at most a given number of seconds for a
key press.
- Blocking reads are now interruptible.
- Unicode characters (including emoji) are now read correctly on Windows.

* `ansi_strip()` now also removes generic OSC sequences such as the
`\033]0;...\a` window-title sequence emitted by `Rscript.exe` on
Windows.
Expand Down
30 changes: 24 additions & 6 deletions R/keypress.R
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,45 @@
#'
#' @param block Whether to wait for a key press, if there is none
#' available now.
#' @return The key pressed, a character scalar. For non-blocking reads
#' `NA` is returned if no keys are available.
#' @param timeout Maximum number of seconds to wait for a key press, if
#' `block` is `TRUE`. The default `Inf` waits indefinitely. If no key
#' is pressed before the timeout expires, `NA` is returned. Ignored
#' for non-blocking reads (`block = FALSE`). The wait is interruptible
#' regardless of the timeout.
#' @return The key pressed, a character scalar. `NA` is returned if no
#' key is available: for non-blocking reads, or when a blocking read
#' times out.
#'
#' @family keypress function
#' @export
#' @examplesIf FALSE
#' x <- keypress()
#' cat("You pressed key", x, "\n")
#'
#' # Wait at most five seconds for a key press
#' x <- keypress(timeout = 5)
#' if (is.na(x)) cat("No key pressed\n") else cat("You pressed key", x, "\n")

keypress <- function(block = TRUE) {
keypress <- function(block = TRUE, timeout = Inf) {
if (!has_keypress_support()) {
stop("Your platform/terminal does not support `keypress()`.")
}
block <- as.logical(block)
if (length(block) != 1) {
if (length(block) != 1 || is.na(block)) {
stop("'block' must be a logical scalar")
}
ret <- .Call(cli_keypress, block)
timeout <- as.double(timeout)
if (length(timeout) != 1 || is.na(timeout) || timeout < 0) {
stop("'timeout' must be a non-negative number of seconds")
}
ret <- call_with_cleanup(cli_keypress, block, timeout)
if (ret == "none") NA_character_ else ret
}

call_with_cleanup <- function(ptr, ...) {
.Call(cleancall_call, pairlist(ptr, ...), parent.frame())
}

#' Check if the current platform/terminal supports reading
#' single keys.
#'
Expand All @@ -51,7 +69,7 @@ keypress <- function(block = TRUE) {
#' * Others.
#'
#' @return Whether there is support for waiting for individual
#' keypressses.
#' keypresses.
#'
#' @family keypress function
#' @export
Expand Down
2 changes: 1 addition & 1 deletion man/ansi_html.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion man/ansi_palettes.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 20 additions & 3 deletions man/cli-config.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion man/has_keypress_support.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 15 additions & 4 deletions man/is_ansi_tty.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 15 additions & 4 deletions man/keypress.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion man/pluralize.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/init.c
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ static const R_CallMethodDef callMethods[] = {

{ "clic_vt_output", (DL_FUNC) clic_vt_output, 3 },

{ "cli_keypress", (DL_FUNC) cli_keypress, 1 },
{ "cli_keypress", (DL_FUNC) cli_keypress, 2 },

{ NULL, NULL, 0 }
};
Expand Down
12 changes: 12 additions & 0 deletions src/keypress-internal.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

#ifndef KEYPRESS_INTERNAL_H
#define KEYPRESS_INTERNAL_H

SEXP save_term_status(void);
SEXP restore_term_status(void);
SEXP set_term_echo(SEXP s_echo);

SEXP test_single_char(SEXP s_bytes);
SEXP test_function_key(SEXP s_bytes);

#endif
Loading
Loading