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:
$nameor${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)— appliesstyletocontent.styleitself 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\) )inawsnot 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:
map_meta— for compile-time-known constants and the implicitstylevariable. ReturnsOption<&'a str>.map_style— for style-only variables. ReturnsOption<Result<&'a str, _>>.mapandmap_no_escaping— for dynamic strings. ReturnOption<Result<String, _>>.map_variables_to_segments— for cases where one variable expands to many segments (used bygit_status's individual states like$conflicted, by$allinprint.rs, and bycustomfor shell-quoted output).parse— walks the AST, resolves variables, drops empty conditional groups, applies styles, and returnsVec<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 inmodel.rs, and the parser/walker instring_formatter.rs. This is invasive; consultgit log -- src/formatter/spec.pestfor prior changes. - Add a new variable kind to a module — register it via
map,map_meta,map_style, ormap_variables_to_segmentsin that module'smodule(context)function. - Add a new placeholder for
VersionFormatter— extend the parser insrc/formatter/version.rs.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.