Open-Source Wikis

/

Starship

/

Subsystems

/

Formatter

starship/starship

Formatter

Starship's prompt is driven by a small DSL: format strings like "$directory[ $branch](red) $character". The DSL is parsed by pest using the grammar at src/formatter/spec.pest, then executed by StringFormatter (src/formatter/string_formatter.rs).

Grammar

expression = _{ SOI ~ value* ~ EOI }
value = _{ text | variable | textgroup | conditional }

variable = { "$" ~ (variable_name | variable_scope) }
variable_name = @{ ('a'..'z' | 'A'..'Z' | "_") ~ char* }
variable_scope = _{ "{" ~ variable_scoped_name ~ "}" }

text = { (string | escape)+ }
escape = _{ "\\" ~ escaped_char }
escaped_char = { "[" | "]" | "(" | ")" | "\\" | "$" }

textgroup = { "[" ~ format ~ "]" ~ "(" ~ style ~ ")" }
format = { value* }
style = { (variable | string)* }

conditional = { "(" ~ format ~ ")" }

In practice this gives the user four primitives:

  • Variable: $name or ${dotted.name} — substituted with the output of the named module or pseudo-variable. The ${...} form is needed for variables containing dots (e.g. ${custom.foo}).
  • Text group: [content](style) — applies style to content. style itself can use variables, e.g. [$dir]($style).
  • Conditional: (content) — content is rendered only if at least one variable inside expanded to a non-empty value. This is what makes (\($region\) ) in aws not render an empty () when the region is missing.
  • Escape: \[ \] \( \) \$ \\ — literals.

The parser produces an AST in src/formatter/model.rs, and parser.rs is the pest entry point.

StringFormatter execution

let s = StringFormatter::new("$directory$character")?
    .map_meta(|var, _| match var { "symbol" => Some(""), _ => None })
    .map_style(|var| match var { "style" => Some(Ok("bold red")), _ => None })
    .map(|var| match var { "version" => Some(Ok("1.0")), _ => None })
    .map_variables_to_segments(|var| /* expand $all */)
    .parse(None, Some(context))?;

Each map_* registers a closure that resolves a kind of variable. The chain is:

  1. map_meta — for compile-time-known constants and the implicit style variable. Returns Option<&'a str>.
  2. map_style — for style-only variables. Returns Option<Result<&'a str, _>>.
  3. map and map_no_escaping — for dynamic strings. Return Option<Result<String, _>>.
  4. map_variables_to_segments — for cases where one variable expands to many segments (used by git_status's individual states like $conflicted, by $all in print.rs, and by custom for shell-quoted output).
  5. parse — walks the AST, resolves variables, drops empty conditional groups, applies styles, and returns Vec<Segment>.

Variables that are referenced in the format string but not registered by any map_* resolve to None, which behaves as an empty string. Conditional groups that contain only unmapped variables collapse to nothing.

VariableHolder

src/formatter/model.rs defines VariableHolder::get_variables — a walk over the AST that returns the full set of variable names used. The render pipeline calls this before running any modules, so it can compute only what the format string actually needs.

VersionFormatter

src/formatter/version.rs is a separate, simpler formatter used by toolchain modules. It accepts a template like v${major}.${minor} and a raw version string, and produces the formatted output:

VersionFormatter::format_module_version("rust", "1.85.0", "v${raw}")
//=> "v1.85.0"

VersionFormatter::format_module_version("rust", "1.85.0", "${major}.${minor}")
//=> "1.85"

It strips a leading v, recognizes pre-release suffixes, and exposes ${major}, ${minor}, ${patch}, ${raw}. This is why almost every toolchain module's version_format defaults to "v${raw}" rather than rolling its own logic.

Style escaping

map_no_escaping is rare. Most variable substitution goes through map, which escapes the special DSL characters ([ ] ( ) $ \) in the produced string so that, e.g., a directory name containing [ doesn't accidentally start a text group. custom uses map_no_escaping because the user's command output may legitimately contain DSL syntax.

Formatter errors

StringFormatterError is mapped from pest parse errors (Pest) and from variable-resolution errors (Custom). On any error, the calling module logs at warn level and returns None. The user sees a blank module + a log line, never a panic.

If the user's format string is unparseable globally (e.g., unbalanced brackets), print::load_formatter_and_modules falls back to the literal string ">", ensuring the prompt still has a character.

Performance

The grammar is parsed once at start-up via lazy_static-style initialization in parser.rs. Hot-path execution avoids allocating intermediate Strings where possible, using &str references to the original format string and nu_ansi_term::AnsiString for styled output.

Entry points for modification

  • Add a new DSL primitive — extend spec.pest, the AST in model.rs, and the parser/walker in string_formatter.rs. This is invasive; consult git log -- src/formatter/spec.pest for prior changes.
  • Add a new variable kind to a module — register it via map, map_meta, map_style, or map_variables_to_segments in that module's module(context) function.
  • Add a new placeholder for VersionFormatter — extend the parser in src/formatter/version.rs.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Formatter – Starship wiki | Factory