diff --git a/NAMESPACE b/NAMESPACE index 37234fae4..4ba0a7b05 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -33,6 +33,8 @@ export(ansi_hide_cursor) export(ansi_html) export(ansi_html_style) export(ansi_nchar) +export(ansi_palette_show) +export(ansi_palettes) export(ansi_regex) export(ansi_show_cursor) export(ansi_simplify) @@ -213,6 +215,7 @@ export(symbol) export(test_that_cli) export(ticking) export(tree) +export(truecolor) export(utf8_graphemes) export(utf8_nchar) export(utf8_substr) diff --git a/NEWS.md b/NEWS.md index 703cd4fbb..829ce19db 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,8 @@ # cli (development version) +* Support for palettes, including a colorblind friendly palette. + See `?ansi_palettes` for details. + * True color support: `num_ansi_colors()` now detects terminals with 24 bit color support, and `make_ansi_style()` uses the exact RGB colors on these terminals. diff --git a/R/ansi-palette.R b/R/ansi-palette.R new file mode 100644 index 000000000..d059d234a --- /dev/null +++ b/R/ansi-palette.R @@ -0,0 +1,219 @@ + +get_palette_color <- function(style, colors = num_ansi_colors()) { + opt <- getOption("cli.palette") + if (is.null(opt) || colors < 256) return(style) + cache_palette_color(opt, style$palette, colors) +} + +palette_cache <- new.env(parent = emptyenv()) + +cache_palette_color <- function(pal, idx, colors = num_ansi_colors()) { + if (is_string(pal)) { + if (! pal %in% rownames(ansi_palettes)) { + stop("Cannot find cli ANSI palette '", pal, "'.") + } + pal <- ansi_palettes[pal, ] + } + + bg <- idx < 0 + idx <- abs(idx) + col <- pal[[idx]] + + colkey <- as.character(colors) + key <- paste0(col, bg) + if (key %in% names(palette_cache[[colkey]])) { + return(palette_cache[[colkey]][[key]]) + } + + val <- ansi_style_from_r_color( + col, + bg = bg, + colors, + grey = FALSE + ) + + if (is.null(palette_cache[[colkey]])) { + palette_cache[[colkey]] <- new.env(parent = emptyenv()) + } + palette_cache[[colkey]][[key]] <- val + + return(val) +} + +#' @details +#' `truecolor` is an integer constant for the number of 24 bit ANSI colors. +#' +#' @format `truecolor` is an integer scalar. +#' +#' @export +#' @rdname ansi_palettes + +truecolor <- as.integer(256 ^ 3) + +#' ANSI colors palettes +#' +#' If your platform supports at least 256 colors, then you can configure +#' the colors that cli uses for the eight base and the eight bright colors. +#' (I.e. the colors of [col_black()], [col_red()], and [col_br_black()], +#' [col_br_red()], etc. +#' +#' To customize the default palette, set the `cli.palette` option to the +#' name of a built-in palette (see `ansi_palettes()`), or the list of +#' 16 colors. Colors can be specified with RGB colors strings: +#' `#rrggbb` or R color names (see the output of [grDevices::colors()]). +#' +#' For example, you can put this in your R profile: +#' ```r +#' options(cli.palette = "vscode") +#' ``` +#' +#' It is currently not possible to configure the background colors +#' separately, these will be always the same as the foreground colors. +#' +#' If your platform only has 256 colors, then the colors specified in the +#' palette have to be interpolated. On true color platforms they RGB +#' values are used as-is. +#' +#' `ansi_palettes` is a data frame of the built-in palettes, each row +#' is one palette. +#' +#' `ansi_palette_show()` shows the colors of an ANSI palette on the screen. +#' +#' @format `ansi_palettes` is a data frame with one row for each palette, +#' and one column for each base ANSI color. `attr(ansi_palettes, "info")` +#' contains a list with information about each palette. +#' +#' @export +#' @examples +#' ansi_palettes +#' ansi_palette_show("dichro", colors = truecolor) + +ansi_palettes <- rbind( + read.table( + "tools/ansi-palettes.txt", + comment = ";", + stringsAsFactors = FALSE + ), + read.table( + "tools/ansi-iterm-palettes.txt", + comment = ";", + stringsAsFactors = FALSE + ) +) + +attr(ansi_palettes, "info") <- + list( + dichro = paste( + "Colorblind friendly palette, from", + "https://github.com/romainl/vim-dichromatic#dichromatic." + ), + vga = paste( + "Typical colors that are used when booting PCs and leaving them in", + "text mode, which used a 16-entry color table. The colors are", + "different in the EGA/VGA graphic modes.", + "From https://en.wikipedia.org/wiki/ANSI_escape_code#SGR." + ), + winxp = paste( + "Windows XP Console. Seen in Windows XP through Windows 8.1.", + "From https://en.wikipedia.org/wiki/ANSI_escape_code#SGR." + ), + vscode = paste( + "Visual Studio Debug console, 'Dark+' theme.", + "From https://en.wikipedia.org/wiki/ANSI_escape_code#SGR." + ), + win10 = paste0( + "Campbell theme, used as of Windows 10 version 1709. Also used", + "by PowerShell 6.", + "From https://en.wikipedia.org/wiki/ANSI_escape_code#SGR." + ), + macos = paste0( + "Terminal.app in macOS", + "From https://en.wikipedia.org/wiki/ANSI_escape_code#SGR." + ), + putty = paste0( + "From https://en.wikipedia.org/wiki/ANSI_escape_code#SGR." + ), + mirc = paste0( + "From https://en.wikipedia.org/wiki/ANSI_escape_code#SGR." + ), + xterm = paste0( + "From https://en.wikipedia.org/wiki/ANSI_escape_code#SGR." + ), + ubuntu = paste0( + "For virtual terminals, from /etc/vtrgb.", + "From https://en.wikipedia.org/wiki/ANSI_escape_code#SGR." + ), + eclipse = paste0( + "From https://en.wikipedia.org/wiki/ANSI_escape_code#SGR." + ), + iterm = "Built-in iTerm2 theme.", + "iterm-pastel" = "Built-In iTerm2 theme.", + "iterm-smoooooth" = "Built-In iTerm2 theme.", + "iterm-snazzy" = "From https://github.com/sindresorhus/iterm2-snazzy.", + "iterm-solarized" = "Built-In iTerm2 theme.", + "iterm-tango" = "Built-In iTerm2 theme." + ) + +#' @param palette The palette to show, in the same format as for the +#' `cli.palette` option, so it can be the name of a built-in palette, +#' of a list of 16 colors. +#' @param colors Number of ANSI colors to use the show the palette. If the +#' platform does not have sufficient support, the output might have +#' a lower color resolution. Without color support it will have no color +#' at all. +#' @param rows The number of colored rows to print. +#' @return `ansi_palette_show` returns a character vector, the rows that +#' are printed to the screen, invisibly. +#' +#' @export +#' @rdname ansi_palettes + +ansi_palette_show <- function(palette = NULL, colors = num_ansi_colors(), + rows = 4) { + opts <- options( + cli.palette = palette %||% getOption("cli.palette"), + cli.num_colors = colors + ) + on.exit(options(opts), add = TRUE) + + blk <- strrep(symbol$lower_block_8, 4) + blks <- c( + "blck" = col_black(blk), + "red " = col_red(blk), + "grn " = col_green(blk), + "yllw" = col_yellow(blk), + "blue" = col_blue(blk), + "mgnt" = col_magenta(blk), + "cyan" = col_cyan(blk), + "whte" = col_white(blk), + "blck" = col_br_black(blk), + "red " = col_br_red(blk), + "grn " = col_br_green(blk), + "yllw" = col_br_yellow(blk), + "blue" = col_br_blue(blk), + "mgnt" = col_br_magenta(blk), + "cyan" = col_br_cyan(blk), + "whte" = col_br_white(blk) + ) + + join <- function(x) { + paste0( + paste(x[1:8], collapse = " "), + " ", + paste(x[9:16], collapse = " ") + ) + } + + nms <- join(names(blks)) + str <- join(blks) + + out <- c( + paste(strrep(" ", 52), "bright variants"), + nms, + "", + rep(str, rows) + ) + + cat_line(out) + invisible(out) +} diff --git a/R/ansi.R b/R/ansi.R index c2256c350..9ca2899ad 100644 --- a/R/ansi.R +++ b/R/ansi.R @@ -1,5 +1,21 @@ -TRUE_COLORS <- as.integer(256^3) +palette_idx <- function(id) { + ifelse( + id < 38, + id - (30 - 1), + ifelse( + id < 48, + -(id - (40 - 1)), + ifelse( + id < 98, + id - (90 - 9), + -(id - (100 - 9)) + ))) +} + +palette_color <- function(x) { + c(x, palette = palette_idx(x[[1]])) +} ansi_builtin_styles <- list( reset = list(0, c(0, 22, 23, 24, 27, 28, 29, 39, 49)), @@ -11,42 +27,42 @@ ansi_builtin_styles <- list( hidden = list(8, 28), strikethrough = list(9, 29), - black = list(30, 39), - red = list(31, 39), - green = list(32, 39), - yellow = list(33, 39), - blue = list(34, 39), - magenta = list(35, 39), - cyan = list(36, 39), - white = list(37, 39), + black = palette_color(list(30, 39)), + red = palette_color(list(31, 39)), + green = palette_color(list(32, 39)), + yellow = palette_color(list(33, 39)), + blue = palette_color(list(34, 39)), + magenta = palette_color(list(35, 39)), + cyan = palette_color(list(36, 39)), + white = palette_color(list(37, 39)), silver = list(90, 39), - br_black = list(90, 39), - br_red = list(91, 39), - br_green = list(92, 39), - br_yellow = list(93, 39), - br_blue = list(94, 39), - br_magenta = list(95, 39), - br_cyan = list(96, 39), - br_white = list(97, 39), - - bg_black = list(40, 49), - bg_red = list(41, 49), - bg_green = list(42, 49), - bg_yellow = list(43, 49), - bg_blue = list(44, 49), - bg_magenta = list(45, 49), - bg_cyan = list(46, 49), - bg_white = list(47, 49), - - bg_br_black = list(100, 39), - bg_br_red = list(101, 39), - bg_br_green = list(102, 39), - bg_br_yellow = list(103, 39), - bg_br_blue = list(104, 39), - bg_br_magenta = list(105, 39), - bg_br_cyan = list(106, 39), - bg_br_white = list(107, 39), + br_black = palette_color(list(90, 39)), + br_red = palette_color(list(91, 39)), + br_green = palette_color(list(92, 39)), + br_yellow = palette_color(list(93, 39)), + br_blue = palette_color(list(94, 39)), + br_magenta = palette_color(list(95, 39)), + br_cyan = palette_color(list(96, 39)), + br_white = palette_color(list(97, 39)), + + bg_black = palette_color(list(40, 49)), + bg_red = palette_color(list(41, 49)), + bg_green = palette_color(list(42, 49)), + bg_yellow = palette_color(list(43, 49)), + bg_blue = palette_color(list(44, 49)), + bg_magenta = palette_color(list(45, 49)), + bg_cyan = palette_color(list(46, 49)), + bg_white = palette_color(list(47, 49)), + + bg_br_black = palette_color(list(100, 39)), + bg_br_red = palette_color(list(101, 39)), + bg_br_green = palette_color(list(102, 39)), + bg_br_yellow = palette_color(list(103, 39)), + bg_br_blue = palette_color(list(104, 39)), + bg_br_magenta = palette_color(list(105, 39)), + bg_br_cyan = palette_color(list(106, 39)), + bg_br_white = palette_color(list(107, 39)), # similar to reset, but only for a single property no_bold = list(c(0, 23, 24, 27, 28, 29, 39, 49), 22), @@ -97,19 +113,21 @@ ansi_style_str <- function(x) { paste0("\u001b[", x, "m", collapse = "") } -create_ansi_style_tag <- function(name, open, close) { +create_ansi_style_tag <- function(name, open, close, palette = NULL) { structure( - list(list(open = open, close = close)), + list(list(open = open, close = close, palette = palette)), names = name ) } create_ansi_style_fun <- function(styles) { fun <- eval(substitute(function(...) { - mystyles <- .styles txt <- paste0(...) - if (num_ansi_colors() > 1) { + nc <- num_ansi_colors() + if (nc > 1) { + mystyles <- .styles for (st in rev(mystyles)) { + if (!is.null(st$palette)) st <- get_palette_color(st, nc) txt <- paste0( st$open, gsub(st$close, st$open, txt, fixed = TRUE), @@ -129,7 +147,8 @@ create_ansi_style_fun <- function(styles) { create_ansi_style <- function(name, open = NULL, close = NULL) { open <- open %||% ansi_style_str(ansi_builtin_styles[[name]][[1]]) close <- close %||% ansi_style_str(ansi_builtin_styles[[name]][[2]]) - style <- create_ansi_style_tag(name, open, close) + palette <- ansi_builtin_styles[[name]]$palette + style <- create_ansi_style_tag(name, open, close, palette) create_ansi_style_fun(style) } @@ -263,7 +282,7 @@ ansi_style_8_from_rgb <- function(rgb, bg) { ansi_style_from_rgb <- function(rgb, bg, num_colors, grey) { if (num_colors < 256) { return(ansi_style_8_from_rgb(rgb, bg)) } - if (num_colors < TRUE_COLORS || grey) return(ansi256(rgb, bg, grey)) + if (num_colors < truecolor || grey) return(ansi256(rgb, bg, grey)) return(ansitrue(rgb, bg)) } diff --git a/R/ansiex.R b/R/ansiex.R index 533280467..aa971d1fe 100644 --- a/R/ansiex.R +++ b/R/ansiex.R @@ -817,19 +817,6 @@ ansi_html <- function(x, escape_reserved = TRUE, csi = c("drop", "keep")) { .Call(clic_ansi_html, x, csi == "keep") } -ansi_themes <- rbind( - read.table( - "tools/ansi-themes.txt", - comment = ";", - stringsAsFactors = FALSE - ), - read.table( - "tools/ansi-iterm-themes.txt", - comment = ";", - stringsAsFactors = FALSE - ) -) - #' CSS styles for the output of `ansi_html()` #' #' @@ -837,11 +824,11 @@ ansi_themes <- rbind( #' @param colors Whether or not to include colors. `FALSE` will not include #' colors, `TRUE` or `8` will include eight colors (plus their bright #' variants), `256` will include 256 colors. -#' @param theme Character scalar, theme to use for the first eight colors +#' @param palette Character scalar, palette to use for the first eight colors #' plus their bright variants. Terminals define these colors differently, -#' and cli includes a couple of examples. Sources of themes: +#' and cli includes a couple of examples. Sources of palettes: #' * https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit -#' * iTerm2 builtin themes +#' * iTerm2 builtin palettes #' * https://github.com/sindresorhus/iterm2-snazzy #' @return Named list of CSS declaration blocks, where the names are #' CSS selectors. It has a `format()` and `print()` methods, which you @@ -851,18 +838,18 @@ ansi_themes <- rbind( #' @export #' @examples #' ansi_html_style(colors = FALSE) -#' ansi_html_style(colors = 8, theme = "iterm-snazzy") +#' ansi_html_style(colors = 8, palette = "iterm-snazzy") -ansi_html_style <- function(colors = TRUE, theme = NULL) { - if (is.character(theme)) { - theme <- match.arg(theme) - theme <- as.list(ansi_themes[theme, ]) +ansi_html_style <- function(colors = TRUE, palette = NULL) { + if (is.character(palette)) { + palette <- match.arg(palette) + palette <- as.list(ansi_palettes[palette, ]) } stopifnot( isTRUE(colors) || identical(colors, FALSE) || (is_count(colors) && colors %in% c(8,256)), - is_string(theme) || is.list(theme) && length(theme) == 16 + is_string(palette) || is.list(palette) && length(palette) == 16 ) ret <- list( @@ -879,11 +866,11 @@ ansi_html_style <- function(colors = TRUE, theme = NULL) { if (!identical(colors, FALSE)) { fg <- structure( names = paste0(".ansi-color-", 0:15), - paste0("{ color: ", theme, " }") + paste0("{ color: ", palette, " }") ) bg <- structure( names = paste0(".ansi-bg-color-", 0:15), - paste0("{ background-color: ", theme, " }") + paste0("{ background-color: ", palette, " }") ) ret <- c(ret, fg, bg) } @@ -918,7 +905,7 @@ ansi_html_style <- function(colors = TRUE, theme = NULL) { } # This avoids duplication, but messes up the source ref of the function... -formals(ansi_html_style)$theme <- c("vscode", setdiff(rownames(ansi_themes), "vscode")) +formals(ansi_html_style)$palette <- c("vscode", setdiff(rownames(ansi_palettes), "vscode")) attr(body(ansi_html_style), "srcref") <- NULL attr(body(ansi_html_style), "wholeSrcref") <- NULL attr(body(ansi_html_style), "srcfile") <- NULL diff --git a/R/bullets.R b/R/bullets.R index 224cc4194..a0d22d5fc 100644 --- a/R/bullets.R +++ b/R/bullets.R @@ -14,7 +14,7 @@ #' a name create a `div` element of class `memo-item-empty`, and if the #' name is a single space character, the class is `memo-item-space`. #' -#' The builtin theme defines the following item types: +#' The built-in theme defines the following item types: #' * No name: Item without a prefix. #' * ` `: Indented item. #' * `*`: Item with a bullet. diff --git a/R/num-ansi-colors.R b/R/num-ansi-colors.R index cfd219c80..9cb65967c 100644 --- a/R/num-ansi-colors.R +++ b/R/num-ansi-colors.R @@ -161,7 +161,7 @@ detect_tty_colors <- function() { ct <- Sys.getenv("COLORTERM", NA_character_) if (!is.na(ct)) { if (ct == "truecolor" || ct == "24bit") { - return(TRUE_COLORS) + return(truecolor) } else { return(8L) } @@ -191,7 +191,7 @@ detect_tty_colors <- function() { # this is rather weird, but echo turns on color support :D system2("cmd", c("/c", "echo 1 >NUL")) # https://devblogs.microsoft.com/commandline/24-bit-color-in-the-windows-console/ - if (win10 >= 14931) return(TRUE_COLORS) else return(256L) + if (win10 >= 14931) return(truecolor) else return(256L) } if (os_type() == "windows") { diff --git a/R/spinner.R b/R/spinner.R index 5d7a39507..379732910 100644 --- a/R/spinner.R +++ b/R/spinner.R @@ -34,7 +34,7 @@ usethis::use_data(spinners, internal = TRUE) #' is used, which can be customized via the `cli.spinner_unicode`, #' `cli.spinner_ascii` and `cli.spinner` options. (The latter applies to #' both Unicode and ASCII displays. These options can be set to the name -#' of a builting spinner, or to a list that has an entry called `frames`, +#' of a built-in spinner, or to a list that has an entry called `frames`, #' a character vector of frames. #' @return A list with entries: `name`, `interval`: the suggested update #' interval in milliseconds and `frames`: the character vector of the diff --git a/_pkgdown.yml b/_pkgdown.yml index 2b6710d86..faab399c0 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -24,6 +24,8 @@ navbar: href: articles/progress.html - text: Advanced cli Progress Bars href: articles/progress-advanced.html + - text: cli color palettes + href: articles/palettes.html - text: News href: news/index.html @@ -37,6 +39,9 @@ articles: contents: - progress - progress-advanced +- title: Customization + contents: + - palettes reference: - title: Introduction @@ -123,6 +128,7 @@ reference: - title: Terminal Colors and Styles contents: + - ansi_palettes - num_ansi_colors - starts_with("bg_") - starts_with("col_") diff --git a/man/ansi_html_style.Rd b/man/ansi_html_style.Rd index 4c8dac3bb..40afb36ae 100644 --- a/man/ansi_html_style.Rd +++ b/man/ansi_html_style.Rd @@ -6,10 +6,9 @@ \usage{ ansi_html_style( colors = TRUE, - theme = c("vscode", "vga", "winxp", "win10", "macos", "putty", "mirc", "xterm", - "ubuntu", "eclipse", "iterm-dark", "iterm-light", "iterm-pastel", "iterm-smoooooth", - "iterm-snazzy", "iterm-sol-dark", "iterm-sol-light", "iterm-tango-dark", - "iterm-tango-light") + palette = c("vscode", "dichro", "vga", "winxp", "win10", "macos", "putty", "mirc", + "xterm", "ubuntu", "eclipse", "iterm", "iterm-pastel", "iterm-smoooooth", + "iterm-snazzy", "iterm-solarized", "iterm-tango") ) } \arguments{ @@ -17,12 +16,12 @@ ansi_html_style( colors, \code{TRUE} or \code{8} will include eight colors (plus their bright variants), \code{256} will include 256 colors.} -\item{theme}{Character scalar, theme to use for the first eight colors +\item{palette}{Character scalar, palette to use for the first eight colors plus their bright variants. Terminals define these colors differently, -and cli includes a couple of examples. Sources of themes: +and cli includes a couple of examples. Sources of palettes: \itemize{ \item https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit -\item iTerm2 builtin themes +\item iTerm2 builtin palettes \item https://github.com/sindresorhus/iterm2-snazzy }} } @@ -36,7 +35,7 @@ CSS styles for the output of \code{ansi_html()} } \examples{ ansi_html_style(colors = FALSE) -ansi_html_style(colors = 8, theme = "iterm-snazzy") +ansi_html_style(colors = 8, palette = "iterm-snazzy") } \seealso{ Other ANSI to HTML conversion: diff --git a/man/ansi_palettes.Rd b/man/ansi_palettes.Rd new file mode 100644 index 000000000..189bfcb72 --- /dev/null +++ b/man/ansi_palettes.Rd @@ -0,0 +1,72 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/ansi-palette.R +\docType{data} +\name{truecolor} +\alias{truecolor} +\alias{ansi_palettes} +\alias{ansi_palette_show} +\title{ANSI colors palettes} +\format{ +\code{truecolor} is an integer scalar. + +\code{ansi_palettes} is a data frame with one row for each palette, +and one column for each base ANSI color. \code{attr(ansi_palettes, "info")} +contains a list with information about each palette. +} +\usage{ +truecolor + +ansi_palettes + +ansi_palette_show(palette = NULL, colors = num_ansi_colors(), rows = 4) +} +\arguments{ +\item{palette}{The palette to show, in the same format as for the +\code{cli.palette} option, so it can be the name of a built-in palette, +of a list of 16 colors.} + +\item{colors}{Number of ANSI colors to use the show the palette. If the +platform does not have sufficient support, the output might have +a lower color resolution. Without color support it will have no color +at all.} + +\item{rows}{The number of colored rows to print.} +} +\value{ +\code{ansi_palette_show} returns a character vector, the rows that +are printed to the screen, invisibly. +} +\description{ +If your platform supports at least 256 colors, then you can configure +the colors that cli uses for the eight base and the eight bright colors. +(I.e. the colors of \code{\link[=col_black]{col_black()}}, \code{\link[=col_red]{col_red()}}, and \code{\link[=col_br_black]{col_br_black()}}, +\code{\link[=col_br_red]{col_br_red()}}, etc. +} +\details{ +\code{truecolor} is an integer constant for the number of 24 bit ANSI colors. + +To customize the default palette, set the \code{cli.palette} option to the +name of a built-in palette (see \code{ansi_palettes()}), or the list of +16 colors. Colors can be specified with RGB colors strings: +\verb{#rrggbb} or R color names (see the output of \code{\link[grDevices:colors]{grDevices::colors()}}). + +For example, you can put this in your R profile:\if{html}{\out{