-
Notifications
You must be signed in to change notification settings - Fork 85
Expand file tree
/
Copy pathsemantic-cli.Rmd
More file actions
628 lines (487 loc) · 17.5 KB
/
Copy pathsemantic-cli.Rmd
File metadata and controls
628 lines (487 loc) · 17.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
---
title: "Building a Semantic CLI"
author: "Gábor Csárdi"
description: >
Build a command line interface from predefined elements. Focus on the
content, and do not worry about the exact formatting.
date: "`r Sys.Date()`"
output:
rmarkdown::html_document:
toc: true
toc_depth: 2
editor_options:
markdown:
wrap: sentence
---
```{r}
#| include: false
#| cache: false
knitr::opts_chunk$set(
collapse = TRUE,
comment = "#>",
out.width = "100%",
cache = TRUE
)
asciicast::init_knitr_engine(
echo = TRUE,
echo_input = FALSE,
startup = quote({
library(cli)
set.seed(1) })
)
```
# Introduction
The cli package helps you build a command line interface (CLI) without
getting lost in details (colors, wrapping, spacing, etc.) of how each piece
of the output is formatted.
Instead, you can build command line output from semantic elements: lists,
alerts, quotes code blocks, headers, etc.
The formatting of each element is specified separately, in one or more
cli _themes_.
cli comes with a builtin theme, and if you are satisfied with that, then
you never need to worry about formatting.
A semantic cli is similar to how HTML and CSS work together to create a
web site.
In this introduction we will go over the functions that create semantic
CLI elements, and also some common features of them.
# Building a command line interface
```{r}
#| label: setup
library(cli)
```
To build a CLI, you can simply start using the `cli_*` functions to create various CLI elements.
Their exact formatting depends on the current theme, see 'Theming' below.
## Alerts
Alerts are typically short messages.
cli has four types of alerts (success, info, warning, danger) and also a generic alert type:
```{asciicast}
cli_alert_success("Updated database.")
```
```{asciicast}
cli_alert_info("Reopened database.")
```
```{asciicast}
cli_alert_warning("Cannot reach GitHub, using local database cache.")
```
```{asciicast}
cli_alert_danger("Failed to connect to database.")
```
```{asciicast}
cli_alert("A generic alert")
```
## Text
Text is automatically wrapped to the terminal width.
```{asciicast}
cli_text(cli:::lorem_ipsum())
```
Text may have ANSI style markup,
## Paragraphs
Paragraphs break the output.
The default theme inserts an empty line before and after paragraphs, but only a single empty line is added between two paragraphs.
```{asciicast}
fun <- function() {
cli_par()
cli_text("This is some text.")
cli_text("Some more text.")
cli_end()
cli_par()
cli_text("Already a new paragraph.")
cli_end()
}
fun()
```
`cli_end()` closes the latest open paragraph (or other open container).
## Auto-closing containers
If a paragraph (or other container, see 'Generic containers' later), is opened within a function, cli automatically closes it at the end of the function, by default.
So in the previous example the last `cli_end()` call is not needed.
Use `.auto_close = FALSE` in `cli_par()` to leave the paragraph open after its calling function returns.
## Headings
cli supports three levels of headings.
This is how they look in the default theme.
The default theme adds an empty line before headings, and an empty line after `cli_h1()` and `cli_h2()`.
```{asciicast}
cli_h1("Heading 1")
```
```{asciicast}
cli_h2("Heading 2")
```
```{asciicast}
cli_h3("Heading 3")
```
## Interpolation
All cli text is treated as a glue template, with special formatters available (see the 'Inline text formatting' Section):
```{asciicast}
size <- 123143123
dt <- 1.3454
cli_alert_info(c(
"Downloaded {prettyunits::pretty_bytes(size)} in ",
"{prettyunits::pretty_sec(dt)}"))
```
## Inline text formatting
To define inline markup, you can use the regular glue braces, and after the opening brace, supply the name of the markup formatter with a leading dot, e.g. for emphasized text, you use `.emph`.
Some examples are below, see `?"inline-markup"` for details.
```{asciicast}
fun <- function() {
cli_ul()
cli_li("{.emph Emphasized} text")
cli_li("{.strong Strong} importance")
cli_li("A piece of code: {.code sum(a) / length(a)}")
cli_li("A package name: {.pkg cli}")
cli_li("A function name: {.fn cli_text}")
cli_li("A keyboard key: press {.kbd ENTER}")
cli_li("A file name: {.file /usr/bin/env}")
cli_li("An email address: {.email bugs.bunny@acme.com}")
cli_li("A URL: {.url https://acme.com}")
cli_li("An environment variable: {.envvar R_LIBS}")
cli_li("Some {.field field}")
}
fun()
```
To combine inline markup and string interpolation, you need to add another set of braces:
```{asciicast}
dlurl <- "https://httpbin.org/status/404"
cli_alert_danger("Failed to download {.url {dlurl}}.")
```
`"val"` is a special inline style, that in the default theme calls `cli_format()` to tailor the conversion of values to strings.
The conversion can be themed, see "Theming" below.
```{asciicast}
cli_div(theme = list(.val = list(digits = 2)))
cli_text("Some random numbers: {.val {runif(4)}}.")
cli_end()
```
## Inline lists of items
When cli performs inline text formatting, it automatically collapses glue substitutions, after formatting.
This is handy to create lists of files, packages, etc.
```{asciicast}
pkgs <- c("pkg1", "pkg2", "pkg3")
cli_text("Packages: {pkgs}.")
```
```{asciicast}
pkgs <- c("pkg1", "pkg2", "pkg3")
cli_text("Packages: {.pkg {pkgs}}")
```
By default class names are collapsed differently:
```{asciicast}
x <- Sys.time()
cli_text("Hey {.var x} has class {.cls {class(x)}}")
```
## Non-breaking spaces
Use `\u00a0` to create a non-breaking space.
E.g. in here we insert some non-breaking spaces, and mark them with an `X`, so it is easy to see that there are no line breaks at a non-breaking space:
```{asciicast}
# Make some spaces non-breaking, and mark them with X
txt <- cli:::lorem_ipsum()
mch <- gregexpr(txt, pattern = " ", fixed = TRUE)
nbs <- runif(length(mch[[1]])) < 0.5
regmatches(txt, mch)[[1]] <- ifelse(nbs, "X\u00a0", " ")
cli_text(txt)
```
## Lists
cli has three types of list: ordered, unordered and definition lists, see `cli_ol()`, `cli_ul()` and `cli_dl()`:
```{asciicast}
cli_ol(c("item 1", "item 2", "item 3"))
```
```{asciicast}
cli_ul(c("item 1", "item 2", "item 3"))
```
```{asciicast}
cli_dl(c("item 1" = "description 1", "item 2" = "description 2",
"item 3" = "description 3"))
```
Item text is wrapped to the terminal width:
```{asciicast}
cli_ul(c("item 1" = cli:::lorem_paragraph(1, 50),
"item 2" = cli:::lorem_paragraph(1, 50)))
```
### Adding list items iteratively
Items can be added one by one:
```{asciicast}
fun <- function() {
lid <- cli_ul()
cli_li("Item 1")
cli_li("Item 2")
cli_li("Item 3")
cli_end(lid)
}
fun()
```
The `cli_ul()` call creates a list container, and because its items are not specified, it leaves the container open.
Then items can be added one by one.
(The last `cli_end()` is not necessary, because by default containers auto-close when their calling function exits.)
### Adding text to an item iteratively
`cli_li()` creates a new container for the list item, within the list container.
You can keep adding text to the item, until the container is closed via `cli_end()` or a new `cli_li()`, which closes the current item container, and creates another one for the new item:
```{asciicast}
fun <- function() {
cli_ul()
cli_li("First item")
cli_text("This is still the first item")
cli_li("This is the second item")
}
fun()
```
### Nested lists
To create nested lists, open nested containers:
```{asciicast}
fun <- function() {
cli_ol()
cli_li("Item 1")
ulid <- cli_ul()
cli_li("Subitem 1")
cli_li("Subitem 2")
cli_end(ulid)
cli_li("Item 2")
cli_end()
}
fun()
```
In `cli_end(olid)`, the `olid` is necessary, otherwise `cli_end()` would only close the container of the list item.
## Horizontal rules
`cli_rule()` creates a horizontal rule.
```{asciicast}
cli_rule(left = "Compiling {.pkg mypackage}")
```
You can use the usual inline markup in the labels of the rule.
The rule's appearance is specified in the current theme.
In particular:
- `before` is added before the rule.
- `after` is added after the rule.
- `color` is used for the color of the rule *and* the labels. (Use color within the label text for a different label color.)
- `background-color`: is the background color for the rule and the labels. (Again, you can use a different background color within the label itself.)
- `margin-top`, `margin-bottom` for empty space above and below the rule.
- `line-type` specifies the line type of the rule. See `?cli_rule` for line types.
## The status bar
cli supports creating a status bar in the last line of the console, if the terminal supports the carriage return control character to move the cursor to the beginning of the line.
This is supported in all terminals, in RStudio, Emacs, RGui, R.app, etc.
It is not supported if the output is a file, e.g. typically on CI systems.
`cli_status()` creates a new status bar, `cli_status_update()` updates a status bar, and `cli_status_clear()` clears it.
`cli_status()` returns an id, that can be used in `cli_status_update()` and `cli_status_clear()` to refer to the right status bar.
While it is possible to create multiple status bars, on a typical terminal only one of them can be shown at any time.
cli by default shows the one that was last created or updated.
While the status bar is active, cli can still produce output, as normal.
This output is created "above" the status bar, which is always kept in the last line of the screen.
See the following example:
```{asciicast}
#| label: status-bar
#| asciicast_at: "all"
#| asciicast_end_wait: 30
#| fig-alt: >
#| First the info message is shown. Then a dynamic status line is shown
#| that shows the number of downloaded and the number of downloading
#| files. After 5 files, this is replaced by the 'Already half-way!'
#| message and the status line is moved down. At the end the status
#| line is overwritten by the 'Downloads done.' message.
f <- function() {
cli_alert_info("About to start downloads.")
sb <- cli_status("{symbol$arrow_right} Downloading 10 files.")
for (i in 9:1) {
Sys.sleep(0.5)
if (i == 5) cli_alert_success("Already half-way!")
cli_status_update(id = sb,
"{symbol$arrow_right} Got {10-i} file{?s}, downloading {i}")
}
cli_status_clear(id = sb)
cli_alert_success("Downloads done.")
}
f()
```
# Theming
The looks of the various CLI elements can be changed via themes.
The cli package comes with a simple built-in theme, and new themes can be added as well.
## Tags, ids and classes
Similarly to HTML document, the elements of a CLI form a tree of nodes.
Each node has exactly one tag, at most one id, and optionally a set of classes.
E.g. `cli_par()` creates a node with a `<p>` tag, `cli_ol()` creates a node with an `<ol>` tag, etc.
Here is an example CLI tree.
It always starts with a `<body>` tag with id `"body"`, this is created automatically.
<body id="body">
<par>
<ol>
<it>
<span class="pkg">
A cli theme is a named list, where the names are selectors based on tag names, ids and classes, and the elements of the list are style declarations.
For example, the style of `<h1>` tags looks like this in the built-in theme:
```{asciicast}
builtin_theme()$h1
```
See also `?cli::themes` for the reference and `?cli::simple_theme` for an example theme.
## Generic containers
`cli_div()` is a generic container, that does not produce any output, but it can add a new theme.
This theme is removed when the `<div>` node is closed.
(Like other containers, `cli_div()` auto-closes when the calling function exits.)
```{asciicast}
fun <- function() {
cli_div(theme = list (.alert = list(color = "red")))
cli_alert("This will be red")
cli_end()
cli_alert("Back to normal color")
}
fun()
```
## Theming inline markup
The inline markup formatters always use a `<span>` tag, and add the name of the formatter as a class.
```{asciicast}
fun <- function() {
cli_div(theme = list(span.emph = list(color = "orange")))
cli_text("This is very {.emph important}")
cli_end()
cli_text("Back to the {.emph previous theme}")
}
fun()
```
In addition to adding inline markup explicitly, like `.emph` here, cli can use the class(es) of the substituted expression to style it automatically.
This can be configured as part of the theme, in the form of a mapping from the `class()` of the expression, to the name of the markup formatter.
For example, if we have a `filename` S3 class, we can make sure that it is always shown as a `.file` in the cli output:
```{asciicast}
fun <- function() {
cli_div(theme = list(body = list("class-map" = list("filename" = "file"))))
fns <- structure(c("file1", "file2", "file3"), class = "filename")
cli_text("Found some files: {fns}.")
cli_end()
}
fun()
```
# CLI messages
All `cli_*()` functions are implemented using standard R conditions.
For example a `cli_alert()` call emits an R condition with class `cli_message`.
These messages can be caught, muffled, transferred from a sub-process to the main R process.
When a cli function is called:
1. cli throws a `cli_message` condition.
2. If this condition is caught and muffled (via the `cli_message_handled` restart), then nothing else happens.
3. Otherwise the `cli.default_handler` option is checked and if this is a function, then it is called with the message.
4. If the `cli.default_handler` option is not set, or it is not a function, the default cli handler is called, which shows the text, alert, heading, etc. on the screen, using the standard R `message()` function.
```{asciicast}
tryCatch(cli_h1("Heading"), cli_message = function(x) x)
suppressMessages(cli_text("Not shown"))
```
# Sub-Processes
If `cli_*()` commands are invoked in a sub-process via `callr::r_session` (see <https://callr.r-lib.org>), and the `cli.message_class` option is set to `"callr_message"`, then cli messages are automatically copied to the main R process:
```{asciicast}
rs <- callr::r_session$new()
rs$run(function() {
options(cli.message_class = "callr_message")
cli::cli_text("This is sub-process {.emph {Sys.getpid()}} from {.pkg callr}")
Sys.getpid()
})
invisible(rs$close())
```
# Utility functions
## ANSI colors
cli has functions to create ANSI colored or styled output in the console.
`col_*` functions change the foreground color, `bg_*` functions change the background color, and `style_*` functions change the style of the text in some way.
These functions concatenate their arguments using `paste0()`, and add the `cli_ansi_string` class to their result:
```{asciicast}
cat(col_red("This ", "is ", "red."), sep = "\n")
```
Foreground colors:
```{asciicast}
cli_ul(c(
col_black("black"),
col_blue("blue"),
col_cyan("cyan"),
col_green("green"),
col_magenta("magenta"),
col_red("red"),
col_white("white"),
col_yellow("yellow"),
col_grey("grey")
))
```
Note that these might actually look different depending on your terminal theme.
Background colors:
```{asciicast}
cli_ul(c(
bg_black("black background"),
bg_blue("blue background"),
bg_cyan("cyan background"),
bg_green("green background"),
bg_magenta("magenta background"),
bg_red("red background"),
bg_white("white background"),
bg_yellow("yellow background")
))
```
Text styles:
```{asciicast}
cli_ul(c(
style_dim("dim style"),
style_blurred("blurred style"),
style_bold("bold style"),
style_hidden("hidden style"),
style_inverse("inverse style"),
style_italic("italic style"),
style_reset("reset style"),
style_strikethrough("strikethrough style"),
style_underline("underline style")
))
```
Not all `style_*` functions are supported by all terminals.
Colors, background colors and styles can be combined:
```{asciicast}
bg_white(style_bold(col_red("TITLE")))
```
`make_ansi_style()` can create custom colors, assuming your terminal supports them.
`combine_ansi_styles()` combines several styles into a function:
```{asciicast}
col_warn <- combine_ansi_styles(make_ansi_style("pink"), style_bold)
col_warn("This is a warning in pink!")
cat(col_warn("This is a warning in pink!"))
```
## Console capabilities
Query the console width:
```{asciicast}
console_width()
```
Query if the console supports ansi escapes:
```{asciicast}
is_ansi_tty()
```
Hide the cursor, if the console supports it (no-op otherwise):
```{r}
ansi_hide_cursor()
ansi_show_cursor()
```
See also `ansi_with_hidden_cursor()`.
Query if the console supports `\r`:
```{asciicast}
is_dynamic_tty()
```
Query if the console supports UTF-8 output:
```{asciicast}
is_utf8_output()
```
## Unicode characters
The `symbol` variable includes some Unicode characters that are often useful in CLI messages.
They automatically fall back to ASCII symbols if the platform does not support them.
You can use these symbols both with the semantic `cli_*()` functions and directly.
```{asciicast}
cli_text("{symbol$tick} no errors | {symbol$cross} 2 warnings")
```
Here is a list of all symbols:
```{asciicast}
list_symbols()
```
Most symbols were inspired by (and copied from) the awesome [figures](https://github.com/sindresorhus/figures) JavaScript project.
## Spinners
See `list_spinners()` and `get_spinner()`.
From the awesome [cli-spinners](https://github.com/sindresorhus/cli-spinners#readme) project.
```{asciicast}
list_spinners()
```
```{asciicast}
get_spinner("dots")
```
```{asciicast}
#| label: spinner-dots
#| fig-alt: "Animation of a spinner made out of Braille characters."
#| asciicast_at: "all"
ansi_with_hidden_cursor(demo_spinners("dots"))
```
```{asciicast}
#| label: spinner-clock
#| fig-alt: >
#| Animation of a spinner that is a Unicode clock glyph where the small
#| arm of the clock is spinning around.
#| asciicast_at: "all"
ansi_with_hidden_cursor(demo_spinners("clock"))
```