Skip to content

Commit c2db1e9

Browse files
authored
Merge pull request #824 from r-lib/feature/keypress-features
Port improvements from keypress package
2 parents c6e6ac4 + e889178 commit c2db1e9

18 files changed

Lines changed: 477 additions & 61 deletions

.Rbuildignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,4 @@
3838
^[\.]?air\.toml$
3939
^\.vscode$
4040
^[.]cache$
41+
^CLAUDE.md$"

CLAUDE.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## What this is
6+
7+
`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.
8+
9+
## Development commands
10+
11+
This package has compiled C code, so you must recompile after editing anything in `src/`. Use the `uncovr` helpers (they handle compilation + instrumentation):
12+
13+
```r
14+
uncovr::reload() # compile C code and (re)load the package
15+
uncovr::test() # run the test suite (testthat, edition 3)
16+
uncovr::document() # regenerate roxygen2 docs (man/*.Rd and NAMESPACE)
17+
```
18+
19+
To run R CMD check (set `NOT_CRAN` so tests that are skipped on CRAN still run):
20+
21+
```r
22+
withr::with_envvar(c(NOT_CRAN = "true"), rcmdcheck::rcmdcheck())
23+
```
24+
25+
Running a single test file or a single test:
26+
27+
```r
28+
uncovr::test(filter = "keypress") # run tests/testthat/test-keypress.R
29+
```
30+
31+
Code is formatted with [air](https://posit-dev.github.io/air/) (see `air.toml`). A GitHub Action suggests formatting fixes on PRs.
32+
33+
## Architecture
34+
35+
### R / C split
36+
37+
The semantic CLI, theming, and most formatting logic live in R (`R/`). Performance-sensitive and OS-level primitives live in C (`src/`):
38+
39+
- ANSI/UTF-8/string-width handling (`ansi.c`, `utf8.c`, `width.c`-related, `charwidth.h`)
40+
- the VT100 parser (`vt.c`, `vtparse*.c`) used to interpret/strip terminal control sequences
41+
- the progress bar engine (`progress.c`, `progress-altrep.c`) — progress state is shared with R via an ALTREP
42+
- keypress reading (`keypress*.c`, split into `keypress-unix.c` / `keypress-win.c`)
43+
- hashing (`md5.c`, `sha1.c`, `sha256.c`, `xxhash*.c`) and `diff.c`, `glue.c`
44+
45+
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.
46+
47+
### The "app" model
48+
49+
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).
50+
51+
`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`).
52+
53+
### Inline markup and glue
54+
55+
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`.
56+
57+
### Loading & global state
58+
59+
`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`).
60+
61+
## Testing conventions
62+
63+
- 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()`.
64+
- `tests/testthat/setup.R` flushes gcov coverage data on teardown (`clic__gcov_flush`) and cleans `.gcda` files — this supports the coverage-instrumented test runs.
65+
- `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.
66+
- `progresstest/` and `progresstestcpp/` are small embedded test packages exercising the C progress API from C and C++.
67+
- 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.
68+
69+
## Documentation
70+
71+
- Roxygen2 (version 8.0.0) generates `man/` and `NAMESPACE` — never edit those by hand; edit the roxygen comments and run `uncovr::document()`.
72+
- 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.
73+
- `README.md` is generated from `README.Rmd` (via `make` / `Makefile`) — edit the `.Rmd`.
74+
- Update `NEWS.md` for user-facing changes.

NEWS.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# cli (development version)
22

3+
* `keypress()` improvements:
4+
- `timeout` argument to wait at most a given number of seconds for a
5+
key press.
6+
- Blocking reads are now interruptible.
7+
- Unicode characters (including emoji) are now read correctly on Windows.
8+
39
* `ansi_strip()` now also removes generic OSC sequences such as the
410
`\033]0;...\a` window-title sequence emitted by `Rscript.exe` on
511
Windows.

R/keypress.R

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,27 +14,45 @@
1414
#'
1515
#' @param block Whether to wait for a key press, if there is none
1616
#' available now.
17-
#' @return The key pressed, a character scalar. For non-blocking reads
18-
#' `NA` is returned if no keys are available.
17+
#' @param timeout Maximum number of seconds to wait for a key press, if
18+
#' `block` is `TRUE`. The default `Inf` waits indefinitely. If no key
19+
#' is pressed before the timeout expires, `NA` is returned. Ignored
20+
#' for non-blocking reads (`block = FALSE`). The wait is interruptible
21+
#' regardless of the timeout.
22+
#' @return The key pressed, a character scalar. `NA` is returned if no
23+
#' key is available: for non-blocking reads, or when a blocking read
24+
#' times out.
1925
#'
2026
#' @family keypress function
2127
#' @export
2228
#' @examplesIf FALSE
2329
#' x <- keypress()
2430
#' cat("You pressed key", x, "\n")
31+
#'
32+
#' # Wait at most five seconds for a key press
33+
#' x <- keypress(timeout = 5)
34+
#' if (is.na(x)) cat("No key pressed\n") else cat("You pressed key", x, "\n")
2535

26-
keypress <- function(block = TRUE) {
36+
keypress <- function(block = TRUE, timeout = Inf) {
2737
if (!has_keypress_support()) {
2838
stop("Your platform/terminal does not support `keypress()`.")
2939
}
3040
block <- as.logical(block)
31-
if (length(block) != 1) {
41+
if (length(block) != 1 || is.na(block)) {
3242
stop("'block' must be a logical scalar")
3343
}
34-
ret <- .Call(cli_keypress, block)
44+
timeout <- as.double(timeout)
45+
if (length(timeout) != 1 || is.na(timeout) || timeout < 0) {
46+
stop("'timeout' must be a non-negative number of seconds")
47+
}
48+
ret <- call_with_cleanup(cli_keypress, block, timeout)
3549
if (ret == "none") NA_character_ else ret
3650
}
3751

52+
call_with_cleanup <- function(ptr, ...) {
53+
.Call(cleancall_call, pairlist(ptr, ...), parent.frame())
54+
}
55+
3856
#' Check if the current platform/terminal supports reading
3957
#' single keys.
4058
#'
@@ -51,7 +69,7 @@ keypress <- function(block = TRUE) {
5169
#' * Others.
5270
#'
5371
#' @return Whether there is support for waiting for individual
54-
#' keypressses.
72+
#' keypresses.
5573
#'
5674
#' @family keypress function
5775
#' @export

man/ansi_html.Rd

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/ansi_palettes.Rd

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/cli-config.Rd

Lines changed: 20 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/has_keypress_support.Rd

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/is_ansi_tty.Rd

Lines changed: 15 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/keypress.Rd

Lines changed: 15 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)