r-lib / cli
1

2
#' Update the status bar
3
#'
4
#' The status bar is the last line of the terminal. cli apps can use this
5
#' to show status information, progress bars, etc. The status bar is kept
6
#' intact by all semantic cli output.
7
#'
8
#' Use [cli_status_clear()] to clear the status bar.
9
#'
10
#' Often status messages are associated with processes. E.g. the app starts
11
#' downloading a large file, so it sets the status bar accordingly. Once the
12
#' download is done (or failed), the app typically updates the status bar
13
#' again. cli automates much of this, via the `msg_done`, `msg_failed`, and
14
#' `.auto_result` arguments. See examples below.
15
#'
16
#' @param msg The text to show, a character vector. It will be
17
#'   collapsed into a single string, and the first line is kept and cut to
18
#'   [console_width()]. The message is often associated with the start of
19
#'   a calculation.
20
#' @param msg_done The message to use when the message is cleared, when
21
#'   the calculation finishes successfully. If `.auto_close` is `TRUE`
22
#'   and `.auto_result` is `"done"`, then this is printed automatically
23
#'   then the calling function (or `.envir`) finishes.
24
#' @param msg_failed The message to use when the message is cleared, when
25
#'   the calculation finishes unsuccessfully. If `.auto_close` is `TRUE`
26
#'   and `.auto_result` is `"failed"`, then this is printed automatically
27
#'   then the calling function (or `.envir`) finishes.
28
#' @param .keep What to do when this status bar is cleared. If `TRUE` then
29
#'   the content of this status bar is kept, as regular cli output (the
30
#'   screen is scrolled up if needed). If `FALSE`, then this status bar
31
#'   is deleted.
32
#' @param .auto_close Whether to clear the status bar when the calling
33
#'   function finishes (or ‘.envir’ is removed from the stack, if
34
#'   specified).
35
#' @param .envir Environment to evaluate the glue expressions in. It is
36
#'   also used to auto-clear the status bar if `.auto_close` is `TRUE.
37
#' @param .auto_result What to do when auto-closing the status bar.
38
#' @return The id of the new status bar container element, invisibly.
39
#'
40
#' @seealso [cli_process_start] for a higher level interface to the
41
#'   status bar, that adds automatic styling.
42
#' @family status bar
43
#' @export
44

45
cli_status <- function(msg, msg_done = paste(msg, "... done"),
46
                       msg_failed = paste(msg, "... failed"),
47
                       .keep = FALSE, .auto_close = TRUE,
48
                       .envir = parent.frame(),
49
                       .auto_result = c("clear", "done", "failed")) {
50 1
  cli__message(
51 1
    "status",
52 1
    list(
53 1
      id = NULL,
54 1
      msg = glue_cmd(msg, .envir = .envir),
55 1
      msg_done = glue_cmd(msg_done, .envir = .envir),
56 1
      msg_failed = glue_cmd(msg_failed, .envir = .envir),
57 1
      keep = .keep,
58 1
      auto_result = match.arg(.auto_result)
59
    ),
60 1
    .auto_close = .auto_close,
61 1
    .envir = .envir
62
  )
63
}
64

65
#' Clear the status bar
66
#'
67
#' @param id Id of the status bar container to clear. If `id` is not the id
68
#'   of the current status bar (because it was overwritten by another
69
#'   status bar container), then the status bar is not cleared. If `NULL`
70
#'   (the default) then the status bar is always cleared.
71
#' @param result Whether to show a message for success or failure or just
72
#'   clear the status bar.
73
#' @param msg_done If not `NULL`, then the message to use for successful
74
#'   process termination. This overrides the message given when the status
75
#'   bar was created.
76
#' @param msg_failed If not `NULL`, then the message to use for failed
77
#'   process termination. This overrides the message give when the status
78
#'   bar was created.
79
#' @inheritParams cli_status
80
#'
81
#' @family status bar
82
#' @export
83

84
cli_status_clear <- function(id = NULL, result = c("clear", "done", "failed"),
85
                             msg_done = NULL, msg_failed = NULL,
86
                             .envir = parent.frame()) {
87

88 1
  cli__message(
89 1
    "status_clear",
90 1
    list(
91 1
      id = id %||% NA_character_,
92 1
      result = match.arg(result),
93 1
      msg_done = if (!is.null(msg_done)) glue_cmd(msg_done, .envir = .envir),
94 1
      msg_failed = if (!is.null(msg_failed)) glue_cmd(msg_failed, .envir = .envir)
95
    )
96
  )
97
}
98

99
#' Update the status bar
100
#'
101
#' @param msg Text to update the status bar with. `NULL` if you don't want
102
#'   to change it.
103
#' @param msg_done Updated "done" message. `NULL` if you don't want to
104
#'   change it.
105
#' @param msg_failed Updated "failed" message. `NULL` if you don't want to
106
#'   change it.
107
#' @param id Id of the status bar to update. Defaults to the current
108
#'   status bar container.
109
#' @param .envir Environment to evaluate the glue expressions in.
110
#' @return Id of the status bar container.
111
#'
112
#' @family status bar
113
#' @export
114

115
cli_status_update <- function(id = NULL, msg = NULL, msg_done = NULL,
116
                              msg_failed = NULL, .envir = parent.frame()) {
117 1
  cli__message(
118 1
    "status_update",
119 1
    list(
120 1
      msg = if (!is.null(msg)) glue_cmd(msg, .envir = .envir),
121 1
      msg_done = if (!is.null(msg_done)) glue_cmd(msg_done, .envir = .envir),
122 1
      msg_failed = if (!is.null(msg_failed)) glue_cmd(msg_failed, .envir = .envir),
123 1
      id = id %||% NA_character_
124
    )
125
  )
126
}
127

128
#' Indicate the start and termination of some computation in the status bar
129
#'
130
#' Typically you call `cli_process_start()` to start the process, and then
131
#' `cli_process_done()` when it is done. If an error happens before
132
#' `cli_process_done()` is called, then cli automatically shows the message
133
#' for unsuccessful termination.
134
#'
135
#' If you handle the errors of the process or computation, then you can do
136
#' the opposite: call `cli_process_start()` with `on_exit = "done"`, and
137
#' in the error handler call `cli_process_failed()`. cli will automatically
138
#' call `cli_process_done()` on successful termination, when the calling
139
#' function finishes.
140
#'
141
#' See examples below.
142
#'
143
#' @param msg The message to show to indicate the start of the process or
144
#'   computation. It will be collapsed into a single string, and the first
145
#'   line is kept and cut to [console_width()].
146
#' @param msg_done The message to use for successful termination.
147
#' @param msg_failed The message to use for unsuccessful termination.
148
#' @param on_exit Whether this process should fail or terminate
149
#'   successfully when the calling function (or the environment in `.envir`)
150
#'   exits.
151
#' @param msg_class The style class to add to the message. Use an empty
152
#'   string to suppress styling.
153
#' @param done_class The style class to add to the successful termination
154
#'   message. Use an empty string to suppress styling.a
155
#' @param failed_class The style class to add to the unsuccessful
156
#'   termination message. Use an empty string to suppress styling.a
157
#' @inheritParams cli_status
158
#' @return Id of the status bar container.
159
#'
160
#' @family status bar
161
#' @export
162
#' @examples
163
#'
164
#' ## Failure by default
165
#' fun <- function() {
166
#'   cli_process_start("Calculating")
167
#'   if (interactive()) Sys.sleep(1)
168
#'   if (runif(1) < 0.5) stop("Failed")
169
#'   cli_process_done()
170
#' }
171
#' tryCatch(fun(), error = function(err) err)
172
#'
173
#' ## Success by default
174
#' fun2 <- function() {
175
#'   cli_process_start("Calculating", on_exit = "done")
176
#'   tryCatch({
177
#'     if (interactive()) Sys.sleep(1)
178
#'     if (runif(1) < 0.5) stop("Failed")
179
#'   }, error = function(err) cli_process_failed())
180
#' }
181
#' fun2()
182

183
cli_process_start <- function(msg, msg_done = paste(msg, "... done"),
184
                              msg_failed = paste(msg, "... failed"),
185
                              on_exit = c("failed", "done"),
186
                              msg_class = "alert-info",
187
                              done_class = "alert-success",
188
                              failed_class = "alert-danger",
189
                              .auto_close = TRUE, .envir = parent.frame()) {
190

191
  # Force the defaults, because we might modify msg
192 1
  msg_done
193 1
  msg_failed
194

195 1
  if (length(msg_class) > 0 && msg_class != "") {
196 1
    msg <- paste0("{.", msg_class, " ", msg, "}")
197
  }
198 1
  if (length(done_class) > 0 && done_class != "") {
199 1
    msg_done <- paste0("{.", done_class, " ", msg_done, "}")
200
  }
201 1
  if (length(failed_class) > 0 && failed_class != "") {
202 1
    msg_failed <- paste0("{.", failed_class, " ", msg_failed, "}")
203
  }
204

205 1
  cli_status(msg, msg_done, msg_failed, .auto_close = .auto_close,
206 1
             .envir = .envir, .auto_result = match.arg(on_exit))
207
}
208

209
#' @param id Id of the status bar container to clear. If `id` is not the id
210
#'   of the current status bar (because it was overwritten by another
211
#'   status bar container), then the status bar is not cleared. If `NULL`
212
#'   (the default) then the status bar is always cleared.
213
#'
214
#' @rdname cli_process_start
215
#' @export
216

217
cli_process_done <- function(id = NULL, msg_done = NULL,
218
                             .envir = parent.frame(),
219
                             done_class = "alert-success") {
220

221 0
  if (!is.null(msg_done) && length(done_class) > 0 && done_class != "") {
222 0
    msg_done <- paste0("{.", done_class, " ", msg_done, "}")
223
  }
224 0
  cli_status_clear(id, result = "done", msg_done = msg_done, .envir = .envir)
225
}
226

227
#' @rdname cli_process_start
228
#' @export
229

230
cli_process_failed <- function(id = NULL, msg = NULL, msg_failed = NULL,
231
                               .envir = parent.frame(),
232
                               failed_class = "alert-danger") {
233 0
  if (!is.null(msg_failed) && length(failed_class) > 0 &&
234 0
      failed_class != "") {
235 0
    msg_failed <- paste0("{.", failed_class, " ", msg_failed, "}")
236
  }
237 0
  cli_status_clear(
238 0
    id,
239 0
    result = "failed",
240 0
    msg_failed = msg_failed,
241 0
    .envir = .envir
242
  )
243
}
244

245
# -----------------------------------------------------------------------
246

247
clii_status <- function(app, id, msg, msg_done, msg_failed, keep,
248
                        auto_result) {
249

250 1
  app$status_bar[[id]] <- list(
251 1
    content = "",
252 1
    msg_done = msg_done,
253 1
    msg_failed = msg_failed,
254 1
    keep = keep,
255 1
    auto_result = auto_result
256
  )
257 1
  clii_status_update(app, id, msg, msg_done = NULL, msg_failed = NULL)
258
}
259

260
clii_status_clear <- function(app, id, result, msg_done, msg_failed) {
261
  ## If NA then the most recent one
262 1
  if (is.na(id)) id <- names(app$status_bar)[1]
263

264
  ## If no active status bar, then ignore
265 0
  if (is.na(id)) return(invisible())
266 1
  if (! id %in% names(app$status_bar)) return(invisible())
267

268 1
  if (result == "done") {
269 1
    msg <- msg_done %||% app$status_bar[[id]]$msg_done
270 1
    clii_status_update(app, id, msg, NULL, NULL)
271 1
    app$status_bar[[id]]$keep <- TRUE
272 1
  } else if (result == "failed") {
273 1
    msg <- msg_failed %||% app$status_bar[[id]]$msg_failed
274 1
    clii_status_update(app, id, msg, NULL, NULL)
275 1
    app$status_bar[[id]]$keep <- TRUE
276
  }
277

278 1
  if (names(app$status_bar)[1] == id) {
279
    ## This is the active one
280 1
    if (app$status_bar[[id]]$keep) {
281
      ## Keep? Just emit it
282 1
      app$cat("\n")
283

284
    } else {
285
      ## Not keep? Remove it
286 1
      clii__clear_status_bar(app)
287
    }
288

289
  } else {
290 0
    if (app$status_bar[[id]]$keep) {
291
      ## Keep?
292 0
      clii__clear_status_bar(app)
293 0
      app$cat(paste0(app$status_bar[[id]]$content, "\n"))
294 0
      app$cat(paste0(app$status_bar[[1]]$content))
295

296
    } else {
297
      ## Not keep? Nothing to output
298
    }
299
  }
300

301
  ## Remove
302 1
  app$status_bar[[id]] <- NULL
303

304
  ## Switch to the previous one
305 1
  if (length(app$status_bar)) app$cat(paste0(app$status_bar[[1]]$content))
306
}
307

308
clii_status_update <- function(app, id, msg, msg_done, msg_failed) {
309
  ## If NA then the most recent one
310 0
  if (is.na(id)) id <- names(app$status_bar)[1]
311

312
  ## If no active status bar, then ignore
313 0
  if (is.na(id)) return(invisible())
314

315
  ## Update messages
316 0
  if (!is.null(msg_done)) app$status_bar[[id]]$msg_done <- msg_done
317 0
  if (!is.null(msg_failed)) app$status_bar[[id]]$msg_failed <- msg_failed
318

319
  ## Do we have a new message?
320 0
  if (is.null(msg)) return(invisible())
321

322
  ## Do we need to clear the current content?
323 1
  current <- paste0("", app$status_bar[[1]]$content)
324

325
  ## Format the line
326 1
  content <- ""
327 1
  fmsg <- app$inline(msg)
328 1
  cfmsg <- strwrap2_fixed(fmsg, width = app$get_width(), strip.spaces = FALSE)
329 1
  content <- strsplit(cfmsg, "\r?\n")[[1]][1]
330 0
  if (is.na(content)) content <- ""
331

332
  ## Update status bar, put it in front
333 1
  app$status_bar[[id]]$content <- content
334 1
  app$status_bar <- c(
335 1
    app$status_bar[id],
336 1
    app$status_bar[setdiff(names(app$status_bar), id)])
337

338
  ## New content, if it is an ANSI terminal we'll overwrite and clear
339
  ## until the end of the line. Otherwise we add some space characters
340
  ## to the content to make sure we clear up residual content.
341 1
  output <- get_real_output(app$output)
342 1
  if (is_ansi_tty(output)) {
343 0
    app$cat(paste0("\r", content, ANSI_EL))
344
  } else {
345 1
    nsp <- max(nchar_fixed(current) - nchar_fixed(content), 0)
346 1
    app$cat(paste0("\r", content, strrep(" ", nsp)))
347
  }
348
}
349

350
clii__clear_status_bar <- function(app) {
351 1
  output <- get_real_output(app$output)
352 1
  if (is_ansi_tty(output)) {
353 0
    app$cat(paste0("\r", ANSI_EL))
354
  } else {
355 1
    text <- app$status_bar[[1]]$content
356 1
    len <- nchar_fixed(text, type = "width")
357 1
    app$cat(paste0("\r", strrep(" ", len), "\r"))
358
  }
359
}

Read our documentation on viewing source code .

Loading