From d7da667f371f257a5d098e75d891ec8b8d568de9 Mon Sep 17 00:00:00 2001 From: Jermiah Joseph Date: Tue, 21 Apr 2026 17:56:34 -0400 Subject: [PATCH 1/5] feat: add duration and time_ago inliners --- R/simple-theme.R | 11 ++++++++++- R/themes.R | 9 +++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/R/simple-theme.R b/R/simple-theme.R index d903b0f7..2cbec899 100644 --- a/R/simple-theme.R +++ b/R/simple-theme.R @@ -144,7 +144,16 @@ simple_theme <- function(dark = getOption("cli.theme_dark", "auto")) { span.var = simple_theme_code(dark), span.envvar = simple_theme_code(dark), - span.timestamp = list(before = "[", after = "]", color = "grey") + span.timestamp = list(before = "[", after = "]", color = "grey"), + span.duration = list( + before = "[", + after = "]", + color = "grey", + transform = function(x) format_time$pretty_sec(as.numeric(x)) + ), + span.time_ago = list( + transform = function(x) format_time_ago$time_ago(as.POSIXct(as.numeric(x), origin = "1970-01-01")) + ) ) } diff --git a/R/themes.R b/R/themes.R index 26c94d18..b06622b9 100644 --- a/R/themes.R +++ b/R/themes.R @@ -267,6 +267,15 @@ builtin_theme <- function(dark = getOption("cli.theme_dark", "auto")) { ), span.num = list( transform = function(x) format_num$pretty_num(as.numeric(x)) + ), + span.duration = list( + before = "[", + after = "]", + color = "grey", + transform = function(x) format_time$pretty_sec(as.numeric(x)) + ), + span.time_ago = list( + transform = function(x) format_time_ago$time_ago(as.POSIXct(as.numeric(x), origin = "1970-01-01")) ) ) } From 88bc85ab928fc1db4128947d6941e1b8a743fd4e Mon Sep 17 00:00:00 2001 From: Jermiah Joseph Date: Wed, 22 Apr 2026 11:48:21 -0400 Subject: [PATCH 2/5] refactor(themes): drop before/after from .duration, add difftime normalisation - Remove bracket decoration from builtin_theme and simple_theme; users can add via cli.user_theme (consistent with .bytes/.num pattern) - Normalise difftime to seconds before pretty_sec to avoid unit mismatch - Simplify .time_ago transform (as.POSIXct handles numeric directly) - Expand tests: difftime normalisation + POSIXct/POSIXlt input variants --- R/simple-theme.R | 11 +---------- R/themes.R | 10 +++++----- tests/testthat/_snaps/inline-2.md | 33 +++++++++++++++++++++++++++++++ tests/testthat/test-inline-2.R | 32 ++++++++++++++++++++++++++++++ 4 files changed, 71 insertions(+), 15 deletions(-) diff --git a/R/simple-theme.R b/R/simple-theme.R index 2cbec899..d903b0f7 100644 --- a/R/simple-theme.R +++ b/R/simple-theme.R @@ -144,16 +144,7 @@ simple_theme <- function(dark = getOption("cli.theme_dark", "auto")) { span.var = simple_theme_code(dark), span.envvar = simple_theme_code(dark), - span.timestamp = list(before = "[", after = "]", color = "grey"), - span.duration = list( - before = "[", - after = "]", - color = "grey", - transform = function(x) format_time$pretty_sec(as.numeric(x)) - ), - span.time_ago = list( - transform = function(x) format_time_ago$time_ago(as.POSIXct(as.numeric(x), origin = "1970-01-01")) - ) + span.timestamp = list(before = "[", after = "]", color = "grey") ) } diff --git a/R/themes.R b/R/themes.R index b06622b9..71ada3a0 100644 --- a/R/themes.R +++ b/R/themes.R @@ -269,13 +269,13 @@ builtin_theme <- function(dark = getOption("cli.theme_dark", "auto")) { transform = function(x) format_num$pretty_num(as.numeric(x)) ), span.duration = list( - before = "[", - after = "]", - color = "grey", - transform = function(x) format_time$pretty_sec(as.numeric(x)) + transform = function(x) { + if (inherits(x, "difftime")) units(x) <- "secs" + format_time$pretty_sec(as.numeric(x)) + } ), span.time_ago = list( - transform = function(x) format_time_ago$time_ago(as.POSIXct(as.numeric(x), origin = "1970-01-01")) + transform = function(x) format_time_ago$time_ago(as.POSIXct(x, origin = "1970-01-01")) ) ) } diff --git a/tests/testthat/_snaps/inline-2.md b/tests/testthat/_snaps/inline-2.md index 4ffce625..db765c52 100644 --- a/tests/testthat/_snaps/inline-2.md +++ b/tests/testthat/_snaps/inline-2.md @@ -458,3 +458,36 @@ Output [1] "--- 10 k, 20 k, 30 k, and 40 k ---" +# .duration formats numeric seconds + + Code + format_inline("--- {.duration 0.042} ---") + Output + [1] "--- 42ms ---" + Code + format_inline("--- {.duration 90} ---") + Output + [1] "--- 1m 30s ---" + Code + format_inline("--- {.duration 3661} ---") + Output + [1] "--- 1h 1m 1s ---" + +# .time_ago accepts numeric, POSIXct, and POSIXlt inputs + + Code + t <- as.numeric(Sys.time() - 120) + format_inline("{.time_ago {t}}") + Output + [1] "2 minutes ago" + Code + t <- Sys.time() - 120 + format_inline("{.time_ago {t}}") + Output + [1] "2 minutes ago" + Code + t <- as.POSIXlt(Sys.time() - 120) + format_inline("{.time_ago {t}}") + Output + [1] "2 minutes ago" + diff --git a/tests/testthat/test-inline-2.R b/tests/testthat/test-inline-2.R index 0b9da477..db15e3ef 100644 --- a/tests/testthat/test-inline-2.R +++ b/tests/testthat/test-inline-2.R @@ -212,3 +212,35 @@ test_that(".num", { format_inline("--- {.num {1:4 * 10000}} ---") }) }) + +test_that(".duration formats numeric seconds", { + expect_snapshot({ + format_inline("--- {.duration 0.042} ---") + format_inline("--- {.duration 90} ---") + format_inline("--- {.duration 3661} ---") + }) +}) + +test_that(".duration normalises difftime units before formatting", { + # difftime stored in non-second units must not be treated as raw seconds + dt_mins <- as.difftime(1.5, units = "mins") # 90 seconds + expect_equal(format_inline("{.duration {dt_mins}}"), "1m 30s") + dt_hours <- as.difftime(1, units = "hours") # 3600 seconds + expect_equal(format_inline("{.duration {dt_hours}}"), "1h") +}) + +test_that(".time_ago accepts numeric, POSIXct, and POSIXlt inputs", { + expect_snapshot({ + # numeric (unix timestamp) + t <- as.numeric(Sys.time() - 120) + format_inline("{.time_ago {t}}") + + # POSIXct + t <- Sys.time() - 120 + format_inline("{.time_ago {t}}") + + # POSIXlt + t <- as.POSIXlt(Sys.time() - 120) + format_inline("{.time_ago {t}}") + }) +}) From 2c3d8e4eae7526a8dcd31d586a5b26e56a3d828d Mon Sep 17 00:00:00 2001 From: Jermiah Joseph Date: Wed, 22 Apr 2026 11:50:28 -0400 Subject: [PATCH 3/5] docs(NEWS): add .duration/.time_ago entry under dev version heading --- NEWS.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/NEWS.md b/NEWS.md index 2a00fd24..1165c962 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,8 @@ # cli (development version) +* New `{.duration}` and `{.time_ago}` inline styles to format durations + and relative times (@jjjermiah, #814). + # cli 3.6.6 * New `{.num}` and `{.bytes}` inline styles to format numbers From b8a2a2481ba65899a27e8eac4d39a977032f87d3 Mon Sep 17 00:00:00 2001 From: Jermiah Joseph Date: Wed, 22 Apr 2026 12:02:44 -0400 Subject: [PATCH 4/5] test: consolidate into one snapshot test per inline style --- tests/testthat/_snaps/inline-2.md | 12 ++++++++++-- tests/testthat/test-inline-2.R | 19 ++++++++----------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/tests/testthat/_snaps/inline-2.md b/tests/testthat/_snaps/inline-2.md index db765c52..2b55414a 100644 --- a/tests/testthat/_snaps/inline-2.md +++ b/tests/testthat/_snaps/inline-2.md @@ -458,7 +458,7 @@ Output [1] "--- 10 k, 20 k, 30 k, and 40 k ---" -# .duration formats numeric seconds +# .duration Code format_inline("--- {.duration 0.042} ---") @@ -472,8 +472,16 @@ format_inline("--- {.duration 3661} ---") Output [1] "--- 1h 1m 1s ---" + Code + format_inline("{.duration {as.difftime(1.5, units = 'mins')}}") + Output + [1] "1m 30s" + Code + format_inline("{.duration {as.difftime(1, units = 'hours')}}") + Output + [1] "1h" -# .time_ago accepts numeric, POSIXct, and POSIXlt inputs +# .time_ago Code t <- as.numeric(Sys.time() - 120) diff --git a/tests/testthat/test-inline-2.R b/tests/testthat/test-inline-2.R index db15e3ef..070762bb 100644 --- a/tests/testthat/test-inline-2.R +++ b/tests/testthat/test-inline-2.R @@ -213,25 +213,22 @@ test_that(".num", { }) }) -test_that(".duration formats numeric seconds", { +test_that(".duration", { expect_snapshot({ + # numeric seconds format_inline("--- {.duration 0.042} ---") format_inline("--- {.duration 90} ---") format_inline("--- {.duration 3661} ---") - }) -}) -test_that(".duration normalises difftime units before formatting", { - # difftime stored in non-second units must not be treated as raw seconds - dt_mins <- as.difftime(1.5, units = "mins") # 90 seconds - expect_equal(format_inline("{.duration {dt_mins}}"), "1m 30s") - dt_hours <- as.difftime(1, units = "hours") # 3600 seconds - expect_equal(format_inline("{.duration {dt_hours}}"), "1h") + # difftime: units normalised before formatting (not treated as raw seconds) + format_inline("{.duration {as.difftime(1.5, units = 'mins')}}") + format_inline("{.duration {as.difftime(1, units = 'hours')}}") + }) }) -test_that(".time_ago accepts numeric, POSIXct, and POSIXlt inputs", { +test_that(".time_ago", { expect_snapshot({ - # numeric (unix timestamp) + # numeric unix timestamp t <- as.numeric(Sys.time() - 120) format_inline("{.time_ago {t}}") From bacfc7c9f8eeb94feaa8bfb522d9f10dfdc057e3 Mon Sep 17 00:00:00 2001 From: Jermiah Joseph Date: Wed, 22 Apr 2026 12:04:38 -0400 Subject: [PATCH 5/5] Fix .duration difftime tests to use pre-computed variables and add days unit test --- tests/testthat/_snaps/inline-2.md | 8 +++++--- tests/testthat/test-inline-2.R | 7 +++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/tests/testthat/_snaps/inline-2.md b/tests/testthat/_snaps/inline-2.md index 2b55414a..7004ff24 100644 --- a/tests/testthat/_snaps/inline-2.md +++ b/tests/testthat/_snaps/inline-2.md @@ -473,13 +473,15 @@ Output [1] "--- 1h 1m 1s ---" Code - format_inline("{.duration {as.difftime(1.5, units = 'mins')}}") + dt_mins <- as.difftime(1.5, units = "mins") + format_inline("{.duration {dt_mins}}") Output [1] "1m 30s" Code - format_inline("{.duration {as.difftime(1, units = 'hours')}}") + dt_days <- as.difftime(30, units = "days") + format_inline("{.duration {dt_days}}") Output - [1] "1h" + [1] "30d" # .time_ago diff --git a/tests/testthat/test-inline-2.R b/tests/testthat/test-inline-2.R index 070762bb..67436653 100644 --- a/tests/testthat/test-inline-2.R +++ b/tests/testthat/test-inline-2.R @@ -221,8 +221,11 @@ test_that(".duration", { format_inline("--- {.duration 3661} ---") # difftime: units normalised before formatting (not treated as raw seconds) - format_inline("{.duration {as.difftime(1.5, units = 'mins')}}") - format_inline("{.duration {as.difftime(1, units = 'hours')}}") + dt_mins <- as.difftime(1.5, units = "mins") + format_inline("{.duration {dt_mins}}") + + dt_days <- as.difftime(30, units = "days") + format_inline("{.duration {dt_days}}") }) })