Skip to contents

Everything logtree_theme() can change, and the reference tables for looking it up. The Get started guide covers the parts you need on day one; this is the rest.

Every example below renders the same little tree, so the styling change is the only difference:

demo <- function() {
  load_config <- function() {
    log_step("Load config")
    log_info("reading config.yml")
    log_success("validated 12 parameters")
  }
  pipeline <- function() {
    log_step("Pipeline")
    load_config()
    log_warn("using a stale cache")
  }
  logtree_reset()
  pipeline()
}

The five presets

for (preset in c("unicode", "ascii", "emoji", "minimal", "ci")) {
  cat("\n", preset, "\n", sep = "")
  logtree_theme(preset)
  demo()
}
#> 
#> unicode
#>  Pipeline
#> ├─  Load config
#> │  ├─  reading config.yml
#> │  ├─  validated 12 parameters
#> │  └─  Done  0.00s
#> ├─  using a stale cache
#> └─  Done  0.01s
#> 
#> ascii
#> > Pipeline
#> |- > Load config
#> |  |- i reading config.yml
#> |  |- + validated 12 parameters
#> |  |- + Done  0.00s
#> |- ! using a stale cache
#> |- ! Done  0.00s
#> 
#> emoji
#> 🔹 Pipeline
#> ├─ 🔹 Load config
#> │  ├─ 💡 reading config.yml
#> │  ├─ ✅ validated 12 parameters
#> │  └─ ✅ Done  0.00s
#> ├─ ⚠️ using a stale cache
#> └─ ⚠️ Done  0.00s
#> 
#> minimal
#>  Pipeline
#>    Load config
#>     · reading config.yml
#>      validated 12 parameters
#>      0.00s
#>   ! using a stale cache
#>   ! 0.00s
#> 
#> ci
#> [step]  Pipeline
#> |- [step]  Load config
#> |  |- [info]  reading config.yml
#> |  |- [ok]    validated 12 parameters
#> |  \- [done]  0.00s
#> |- [warn]  using a stale cache
#> \- [warn]  0.00s
logtree_theme("unicode")

"minimal" drops the connectors entirely – branch, corner and pipe are empty strings, so a level costs nothing but its indentation. The trade is worth knowing: info, debug and interrupted all render the middle dot and are told apart by colour alone.

"ci" is built for log capture rather than a terminal. The bracketed words are different lengths on purpose; each declares its true width, so the message column still lines up.

Overriding slots

overrides is a named list keyed by slot, each element holding only the fields to change. Everything else is kept from the active theme.

logtree_theme("unicode", overrides = list(
  success = list(glyph = "*", color = c("green", "bold")),
  group   = list(bracket = TRUE)
))
demo()
#>  Pipeline
#> ├─  Load config
#> │  ├─  reading config.yml
#> │  ├─ * validated 12 parameters
#> │  └─  Done  0.00s
#> ├─  using a stale cache
#> └─  Done  0.00s
logtree_theme("unicode")

The tick on a log_success() line and the tick on a step’s own close line are two different slots – success and done – that merely ship the same glyph in every preset. Restyle one and the other stays as it was:

logtree_theme("unicode", overrides = list(
  done = list(glyph = "=", color = "silver")
))
demo()
#>  Pipeline
#> ├─  Load config
#> │  ├─  reading config.yml
#> │  ├─  validated 12 parameters
#> │  └─ = Done  0.00s
#> ├─  using a stale cache
#> └─  Done  0.00s
logtree_theme("unicode")

Accepted slots

Slot Applies to Fields it accepts
step open / running step glyph glyph, width, color
info log_info() leaf glyph, width, color
debug log_debug() leaf glyph, width, color
success log_success() leaf glyph, width, color
done a step’s own close line on a clean close glyph, width, color, text
warning log_warn() / elevated step glyph glyph, width, color, text
error log_error() / elevated step glyph glyph, width, color, text
interrupted abnormal-exit (dimmed) glyph glyph, width, color, text
group group header marker glyph, color, bracket
elapsed the elapsed-time column on every close line show, min, color, slow, slow_color
trace the optional call-site column show, format, color, capture
timestamp the optional wall-clock column format, color
branch child connector glyph, color
corner close-line connector glyph, color
pipe vertical rail glyph, color
crumb logtree_summary() breadcrumb separator glyph, color, path_color
summary logtree_summary() divider above the digest gap, rule, line

Accepted fields

Field Type Accepted values
glyph character(1) Any string, including "".
width integer(1) Rendered display width of the glyph (1 normal, 2 emoji/wide). Sets column alignment; status slots only.
color character / NULL / list One or more cli styles, or NULL. Named ("red", "cyan", …), bright ("br_red"), backgrounds ("bg_blue"), styles ("bold", "dim", "italic"), or hex ("#ff8800"). A vector combines them. On trace, a named list styles the parts separately: location, fn, base.
text character(1) Close-line status slots only (done, warning, error, interrupted). The word a close line prints before its time; "" drops it. Expands {label} and {elapsed}.
show logical(1) / character On elapsed, FALSE drops the time column. On trace, FALSE (default) is off, TRUE marks every line that can carry a call site, "problems" is shorthand for c("warning", "error", "interrupted"), and a status vector names exactly what to mark.
format character(1) On trace, a template over {fn}, {file}, {line}; default "{file}:{line} {fn}()". On timestamp, a strftime format; NULL (the default) is off.
capture logical(1) trace only. TRUE records call sites even where show prints none. Default FALSE; it only ever adds.
min numeric(1) elapsed only. Hide times below this many seconds.
slow numeric(1) / NULL elapsed only. Times at or over this count as slow.
slow_color character / NULL elapsed only. Styles a slow time in place of color.
bracket logical(1) group only. TRUE wraps the header name in < >.
path_color character / NULL crumb only. Styles the breadcrumb’s path nodes.
gap integer(1) summary only. Blank lines above the digest.
rule logical(1) / character(1) summary only. The divider drawn above the digest.
line integer(1) / character(1) summary only. The rule’s line type.

Close lines and elapsed time

The word a step prints when it closes is the text field of a theme slot. It is read from the closing status’s own slot, falling back to done’s and then to the built-in "Done" – so one call renames every close line, and a narrower one renames only the outcomes that went wrong. Two placeholders expand: {label} (the step’s own label, or a group’s name) and {elapsed}.

logtree_theme("unicode", overrides = list(
  done    = list(text = "{label} ok"),
  warning = list(text = "{label} finished with warnings")
))
demo()
#>  Pipeline
#> ├─  Load config
#> │  ├─  reading config.yml
#> │  ├─  validated 12 parameters
#> │  └─  Load config ok  0.00s
#> ├─  using a stale cache
#> └─  Pipeline finished with warnings  0.00s
logtree_theme("unicode")

text governs close lines only – a log_error() leaf keeps its own message – and text = "" drops the word entirely, leaving the glyph and the time.

The time itself is the elapsed slot. min hides anything faster than a threshold, which is how you silence 0.00s noise on trivial steps, while slow and slow_color flag the ones worth noticing:

logtree_theme("unicode", overrides = list(
  elapsed = list(min = 0.001, color = "silver", slow = 1, slow_color = "red")
))
demo()
#>  Pipeline
#> ├─  Load config
#> │  ├─  reading config.yml
#> │  ├─  validated 12 parameters
#> │  └─  Done
#> ├─  using a stale cache
#> └─  Done  0.00s
logtree_theme("unicode")

Nothing in demo() runs long enough to trip that slow = 1, so every time above stayed silver. With a step that does, the time turns red – and only the time, since slow_color replaces color on that one field and leaves the glyph and the close-line word alone:

slow_demo <- function() {
  build_index <- function() {
    log_step("Build index")
    Sys.sleep(0.4)
    log_success("18,204 documents")
  }
  pipeline <- function() {
    log_step("Pipeline")
    log_info("reading config.yml")
    build_index()
  }
  logtree_reset()
  pipeline()
}

logtree_theme("unicode", overrides = list(
  elapsed = list(min = 0.001, color = "silver", slow = 0.25, slow_color = "red")
))
slow_demo()
#>  Pipeline
#> ├─  reading config.yml
#> ├─  Build index
#> │  ├─  18,204 documents
#> │  └─  Done  0.40s
#> └─  Done  0.40s
logtree_theme("unicode")

Both close lines are flagged, because a step’s elapsed time includes everything it called: a slow child makes its parents slow too. Reading down a tree for the deepest red line is how you find where the time actually went. slow = NULL – the default in every preset – turns the comparison off entirely.

show = FALSE drops the column outright. The with_logging() run summary line takes the slot’s colouring but never its hiding rules, since Run complete in <time> would read as an unfinished sentence without its time.

The timestamp column

timestamp puts a wall-clock column in front of every line. It ships format = NULL in every preset, so it costs nothing until asked for.

logtree_theme(list(timestamp = list(format = "%H:%M:%S")))
demo()
#> 14:29:12  Pipeline
#> 14:29:12 ├─  Load config
#> 14:29:12 │  ├─  reading config.yml
#> 14:29:12 │  ├─  validated 12 parameters
#> 14:29:12 │  └─  Done  0.00s
#> 14:29:12 ├─  using a stale cache
#> 14:29:12 └─  Done  0.00s
logtree_theme(list(timestamp = list(format = NULL)))

"%H:%M:%S" is the interactive choice; "%Y-%m-%d %H:%M:%S" is what a saved log wants, since a file outlives the day it was written.

In the coloured presets the column ships "silver" – supporting detail that stays out of the way:

A logtree tree with a silver wall-clock timestamp column in front of every line

color takes the same values as any other slot:

logtree_theme(list(timestamp = list(format = "%H:%M:%S",
                                    color = c("bold", "blue"))))
demo()
#> 14:29:12  Pipeline
#> 14:29:12 ├─  Load config
#> 14:29:12 │  ├─  reading config.yml
#> 14:29:12 │  ├─  validated 12 parameters
#> 14:29:12 │  └─  Done  0.00s
#> 14:29:12 ├─  using a stale cache
#> 14:29:12 └─  Done  0.00s
logtree_theme(list(timestamp = list(format = NULL, color = "silver")))

Three properties worth knowing:

  • The column is padded to a fixed width measured from a rendered sample, not from the format string, so a format whose width varies with the value cannot shear the tree from one line to the next.
  • It counts against the wrapping budget like any other column, and a wrapped message’s continuation rows carry a blank column rather than a repeated time.
  • The digest is never stamped: it replays events that already happened, so the time it was printed would be the wrong answer.

A file sink pins its own column with logtree_sink_file(timestamp = ), and a "json" sink always carries an ISO-8601 ts regardless.

The call-site column

trace annotates a line with where the log call came from – file.R:line fn(), in a column of its own after the message. It ships show = FALSE in every preset, since capturing a call site costs a frame walk per logged line.

A logtree tree with a call-site column showing each line's file, line number and enclosing function

That figure is a rendered capture rather than a live chunk, and the reason shapes every example below it: {file} and {line} are read from source references, which knitted chunks do not carry. Run from a script or an interactive session these examples would each print a location too; here they can only print fn().

Choosing which lines are marked

show names the statuses worth a location. Marking everything is rarely what you want – a location on a line you were never going to investigate is a column of noise.

show = Marks
FALSE (the default) nothing – and nothing is captured either, unless capture asks for it
TRUE every line that can carry one
"problems" shorthand for c("warning", "error", "interrupted")
a status vector exactly those, from running, info, debug, success, warning, error, interrupted

"running" is the odd one: it names a step’s open line rather than a leaf, which is how you annotate where each step was started from.

logtree_theme("unicode", overrides = list(
  trace = list(show = c("running", "warning"), format = "{fn}()")
))
demo()
#>  Pipeline  pipeline()
#> ├─  Load config  load_config()
#> │  ├─  reading config.yml
#> │  ├─  validated 12 parameters
#> │  └─  Done  0.00s
#> ├─  using a stale cache  pipeline()
#> └─  Done  0.01s
logtree_theme("unicode")

Two rules are not settings. An ordinary close line never carries a call site whatever you name – its site is its own open line’s, a few rows up, as the Done lines above show. A close line that unwound is the exception, under "interrupted": there is no accompanying leaf to hang the location on, and it is the one line where you most want it.

logtree_theme("unicode", overrides = list(
  trace = list(show = "problems", format = "{fn}()")
))

risky <- function() {
  log_step("Risky")
  stop("boom")
}

logtree_reset()
try(risky(), silent = TRUE)
#>  Risky
#> └─  Done  0.00s  risky()
logtree_theme("unicode")

The template

format is a template over {fn}, {file} and {line}, defaulting to "{file}:{line} {fn}()". Literal text between the placeholders is kept, so the column can be made to read as a sentence rather than a coordinate:

logtree_theme("unicode", overrides = list(
  trace = list(show = "problems", format = "logged in {fn}()")
))
demo()
#>  Pipeline
#> ├─  Load config
#> │  ├─  reading config.yml
#> │  ├─  validated 12 parameters
#> │  └─  Done  0.00s
#> ├─  using a stale cache  logged in pipeline()
#> └─  Done  0.00s
logtree_theme("unicode")

The template is split on whitespace, and a run whose placeholders are all unavailable is dropped whole rather than rendered as NA. That is what makes the default degrade cleanly to pipeline() in these chunks – and it is worth knowing before writing a format like "{file}:{line}", which has nothing left to fall back to and renders as an empty column wherever source references are missing.

Styling the parts

trace$color takes either a character vector – styling the whole column – or a named list styling its parts separately: location for a {file}/{line} run, fn for the function name, and base for everything else, which in the default format is the ().

logtree_theme("unicode", overrides = list(
  trace = list(show = "problems", color = list(fn = "magenta"))
))
demo()
#>  Pipeline
#> ├─  Load config
#> │  ├─  reading config.yml
#> │  ├─  validated 12 parameters
#> │  └─  Done  0.00s
#> ├─  using a stale cache  pipeline()
#> └─  Done  0.00s
logtree_theme("unicode")

The coloured presets ship exactly that shape – base = "dim", location = c("dim", "silver"), fn = c("dim", "cyan") – which is how the whole column stays faint while its two halves still read apart, as in the figure above. A plain vector is shorthand for setting base alone.

A run mentioning {file}/{line} is treated as one unit, separator included, and carries a single terminal hyperlink, so a click anywhere on it opens the file at that line. The escape has no printable width, and terminals without OSC 8 support drop it, so nothing about the layout changes either way.

Recording without printing

Capture and rendering are separate stages, and only rendering can be decided after the run: by the time a digest is printed the frame stack that answered “where?” is gone. capture = TRUE records a call site on every line whatever show prints, which is what a quiet console with an annotated digest – or a JSON sink carrying locations the terminal never showed – is built from.

logtree_theme("unicode", overrides = list(
  trace = list(show = FALSE, capture = TRUE, format = "{fn}()")
))
demo()
#>  Pipeline
#> ├─  Load config
#> │  ├─  reading config.yml
#> │  ├─  validated 12 parameters
#> │  └─  Done  0.00s
#> ├─  using a stale cache
#> └─  Done  0.00s
logtree_summary(trace = TRUE)
#> 
#> ── Summary: 1 warning ──────────────────────────────────────────────────────────
#>  Pipelineusing a stale cache  pipeline()
logtree_theme("unicode")

logtree_summary(trace = ) swaps the slot’s show for that one call, so a digest can name locations the tree kept quiet. It can only ever narrow what was captured, though: with capture = FALSE and show = FALSE for the run, no trace = here can invent what was never recorded.

A sink asks in its own right, and registering one with trace = is itself enough to switch capture on – a sink cannot render a column that was never populated:

logtree_sink_file(path, format = "text", trace = TRUE)   # its own column
logtree_sink_file(path, format = "json", trace = TRUE)   # fn/file/line fields

Neither disturbs the console: a run can print no call sites at all while the file beside it records every one.

Density and spacing

Four arguments control horizontal space. They are scalars carried on the theme, not override slots: set them through logtree_theme()’s own arguments, and the next preset swap clears them.

Argument The gap it sets
compact the per-level tree column: "medium" drops the trailing gap after each connector, "tight" also slims the connectors to one character
connector_gap a leaf or close line’s own connector, to its status glyph
glyph_gap the status glyph, to the message text
wrap the column budget a rendered line is capped at
logtree_theme("unicode", compact = "medium")
demo()
#>  Pipeline
#> ├─ Load config
#> │ ├─ reading config.yml
#> │ ├─ validated 12 parameters
#> │ └─ Done  0.00s
#> ├─ using a stale cache
#> └─ Done  0.00s

logtree_theme("unicode", compact = "tight", glyph_gap = 0)
demo()
#> Pipeline
#> Load config
#> │├reading config.yml
#> │├validated 12 parameters
#> │└Done  0.00s
#> using a stale cache
#> Done  0.00s

logtree_theme("unicode")

connector_gap is unset by default and tracks the tree’s own column gap, so every preset renders as it always has. Setting it lets the two diverge – which is what it is for: compact = "tight" keeps every rail column flush while leaf and close glyphs still get air.

logtree_theme("unicode", compact = "tight", connector_gap = 1)
demo()
#>  Pipeline
#>  Load config
#> │├  reading config.yml
#> │├  validated 12 parameters
#> │└  Done  0.00s
#>   using a stale cache
#>   Done  0.00s
logtree_theme("unicode")

wrap = TRUE follows cli::console_width(), measured at render time; a number pins a fixed width. A token with no break opportunity – a long path, a URL – is split by display width rather than left to overflow, and a budget narrower than the tree is deep degrades to no wrapping rather than to an unusable one-column line.

logtree_theme("unicode", wrap = 50)
long <- function() {
  log_step("Deploy")
  log_info("uploading layers to registry.example.internal, 412 MB across 14 layers")
}
logtree_reset(); long()
#>  Deploy
#> ├─  uploading layers to
#>   registry.example.internal, 412 MB across 14
#>   layers
#> └─  Done  0.01s
logtree_theme("unicode")

None of the four reaches file sinks, which render through the ascii preset with its built-in spacing.

Recipes

A branded palette

Hex colours work anywhere a cli style does, so a house palette is a single overrides call.

logtree_theme("unicode", overrides = list(
  step    = list(color = "#7c3aed"),
  info    = list(color = "#0ea5e9"),
  success = list(color = "#16a34a"),
  warning = list(color = "#f59e0b"),
  error   = list(color = "#dc2626"),
  done    = list(color = "#16a34a"),
  branch  = list(color = "#94a3b8"),
  pipe    = list(color = "#94a3b8"),
  corner  = list(color = "#94a3b8")
))
demo()
#>  Pipeline
#> ├─  Load config
#> │  ├─  reading config.yml
#> │  ├─  validated 12 parameters
#> │  └─  Done  0.00s
#> ├─  using a stale cache
#> └─  Done  0.00s
logtree_theme("unicode")

A quiet tree

Drop the elapsed column, drop the close-line word, tighten the spacing: what is left is structure and messages.

logtree_theme("unicode",
  compact = "medium",
  overrides = list(
    elapsed = list(show = FALSE),
    done    = list(text = "")
  )
)
demo()
#>  Pipeline
#> ├─ Load config
#> │ ├─ reading config.yml
#> │ ├─ validated 12 parameters
#> │ └─
#> ├─ using a stale cache
#> └─
logtree_theme("unicode")

A theme that survives log capture

Start from "ci" rather than restyling "unicode": it is already colourless and pure-ASCII. Add a timestamp, since a captured log is read after the fact.

logtree_theme("ci", overrides = list(
  timestamp = list(format = "%Y-%m-%d %H:%M:%S")
))
demo()
#> 2026-08-11 14:29:13 [step]  Pipeline
#> 2026-08-11 14:29:13 |- [step]  Load config
#> 2026-08-11 14:29:13 |  |- [info]  reading config.yml
#> 2026-08-11 14:29:13 |  |- [ok]    validated 12 parameters
#> 2026-08-11 14:29:13 |  \- [done]  0.00s
#> 2026-08-11 14:29:13 |- [warn]  using a stale cache
#> 2026-08-11 14:29:13 \- [warn]  0.00s
logtree_theme("unicode")

Emoji, with the alignment kept

Emoji cells are two columns wide, which is why every glyph declares its width rather than having it measured – nchar() cannot size them reliably. Set both when you substitute one:

logtree_theme("unicode", overrides = list(
  success = list(glyph = "\U0001f7e2", width = 2L),
  warning = list(glyph = "\U0001f7e1", width = 2L),
  error   = list(glyph = "\U0001f534", width = 2L)
))
demo()
#>  Pipeline
#> ├─  Load config
#> │  ├─  reading config.yml
#> │  ├─ 🟢 validated 12 parameters
#> │  └─  Done  0.00s
#> ├─ 🟡 using a stale cache
#> └─ 🟡 Done  0.00s
logtree_theme("unicode")

Get the width wrong and the message column shears by a character on every line that uses that glyph – the one failure mode worth testing after a custom preset.