starship/starship
Patterns and conventions
This page captures the recurring patterns you will see across Starship's source — how modules are structured, how config is wired, how tests are written, how external commands are invoked. Following them keeps your PR consistent with the rest of the codebase.
Module shape
Every prompt module in src/modules/<name>.rs follows the same template:
use super::{Context, Module, ModuleConfig};
use crate::configs::php::PhpConfig;
use crate::formatter::StringFormatter;
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
let mut module = context.new_module("php");
let config: PhpConfig = PhpConfig::try_load(module.config);
// 1. Detect: should this module render at all?
let is_php_project = context
.try_begin_scan()?
.set_files(&config.detect_files)
.set_extensions(&config.detect_extensions)
.set_folders(&config.detect_folders)
.is_match();
if !is_php_project {
return None;
}
// 2. Compute: gather the data the format string needs.
// ...
// 3. Render: parse the format string and substitute variables.
let parsed = StringFormatter::new(config.format).and_then(|formatter| {
formatter
.map_meta(|var, _| match var {
"symbol" => Some(config.symbol),
_ => None,
})
.map_style(|var| match var {
"style" => Some(Ok(config.style)),
_ => None,
})
.map(|var| match var {
"version" => /* ... */,
_ => None,
})
.parse(None, Some(context))
});
module.set_segments(match parsed {
Ok(segments) => segments,
Err(error) => {
log::warn!("Error in module `php`:\n{error}");
return None;
}
});
Some(module)
}The three phases — detect → compute → render — are explicit in almost every module. Detection should be cheap (file/extension/folder scanning, env var lookups) so the module bails out fast when it does not apply.
Configuration structs
Every module has a sibling <Name>Config struct in src/configs/<name>.rs with this shape:
#[derive(Clone, Deserialize, Serialize)]
#[cfg_attr(
feature = "config-schema",
derive(schemars::JsonSchema),
schemars(deny_unknown_fields)
)]
#[serde(default)]
pub struct PhpConfig<'a> {
pub format: &'a str,
pub symbol: &'a str,
pub style: &'a str,
pub disabled: bool,
pub detect_extensions: Vec<&'a str>,
pub detect_files: Vec<&'a str>,
pub detect_folders: Vec<&'a str>,
pub version_format: &'a str,
}
impl Default for PhpConfig<'_> {
fn default() -> Self {
Self { /* sensible defaults */ }
}
}Conventions:
- Borrowed
&'a strfor any string field (formats, symbols, styles, file lists). Strings are owned by the parsed TOML and live for the life of the prompt. #[serde(default)]at the struct level so partial tables in TOML merge onto defaults.#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema), schemars(deny_unknown_fields))]to feed the JSON Schema generator. Doc comments (/// ...) on struct fields become schema descriptions.disabled: boolis supported on every standard module. TheContextmethodis_module_disabled_in_configshort-circuits computation.version_formatdefaults to"v${raw}"and is processed byVersionFormatter— do not roll your own.
When you add a new module, you must also wire it into FullConfig in src/configs/mod.rs, ALL_MODULES in src/module.rs, the mod declarations in src/configs/mod.rs and src/modules/mod.rs, the dispatch match in modules::handle, the description in modules::description, and PROMPT_ORDER in src/configs/starship_root.rs. The Contributing guide has the full checklist.
Reading the environment
Always go through Context, never std::env or std::process::Command directly. Modules use:
let editor = context.get_env("EDITOR");
let home = context.get_home();
let cmd = context.exec_cmd("php", &["--version"])?;Context::exec_cmd is mockable in tests via ModuleRenderer::cmd, and Context::get_env is mockable via ModuleRenderer::env. Context::get_home returns a PathBuf honoring the test root. If you must spawn a process directly, use crate::utils::create_command (which sets predictable defaults like UTF-8 locale on Windows). Never call std::process::Command::new from inside a module.
For absolute paths (system files like /etc/..., /run/..., etc.), use crate::utils::context_path(context, "/abs/path"). In tests this rewrites the root to a tempdir, so tests do not depend on the real file system.
Detecting projects
The Context::try_begin_scan() builder is the fast path:
let is_match = context.try_begin_scan()?
.set_files(&["Cargo.toml"])
.set_extensions(&["rs"])
.set_folders(&["src"])
.is_match();It uses a single cached DirContents per Context (lazy-initialized via OnceLock), so multiple modules that scan the same directory share the work.
For env-var-only detection (e.g., conda activates by CONDA_DEFAULT_ENV), use context.detect_env_vars(&config.detect_env_vars).
For Git state, use context.get_repo() which returns a cached Result<Repo, _>. The Repo exposes open() which returns a gix::Repository; do not open the repo yourself.
Format string parsing
Module rendering should always go through StringFormatter::new(config.format):
map_meta— handles compile-time-known constants (likesymbolfrom config) and the implicitstylevariable.map_style— producesOk(&str)style strings for variables that act as style placeholders.map/map_no_escaping— handles dynamic content that needs string substitution.map_variables_to_segments— for cases where one variable expands to multiple segments (used bygit_status,$all, etc.).
Parsing returns Result<Vec<Segment>, StringFormatterError>. On error, log a warn! with the module name and return None. Don't unwrap.
ANSI styling
All color/style work flows through:
nu_ansi_term::Style(re-exported viacrate::config::Style) — the underlying ANSI primitive.Style::from_config(s)insrc/config.rs— parses the user's space-separated style tokens, with palette resolution.Segment::ansi_string(prev_style)— emits anAnsiString, which is then collected bynu_ansi_term::AnsiStringsto strip duplicate escapes.
Don't write ANSI escape sequences by hand. Always go through styles.
Logging
Use the log crate macros (log::warn!, log::debug!, log::trace!). At runtime they go through StarshipLogger which writes to a session log file under ${STARSHIP_CACHE:-$HOME/.cache}/starship/. The user controls the level with STARSHIP_LOG.
For tests, init_logger in src/test/mod.rs sets the log file to /dev/null/nul so logging never blocks.
Errors and failure modes
- A module that does not apply returns
None. That is normal and not an error. - A module that fails (bad regex, unparseable format, command timeout) should
log::warn!and returnNone. The user gets a silent skip plus a log entry; the rest of the prompt still renders. - Never
panic!from inside a module, including.unwrap()on user-controlled input.
Tests
Use ModuleRenderer from src/test/mod.rs:
#[cfg(test)]
mod tests {
use super::*;
use crate::test::ModuleRenderer;
use nu_ansi_term::Color;
#[test]
fn folder_without_php_files() -> std::io::Result<()> {
let dir = tempfile::tempdir()?;
let actual = ModuleRenderer::new("php").path(dir.path()).collect();
assert_eq!(actual, None);
dir.close()
}
}Patterns:
fixture_repo()insrc/test/mod.rsreturns atempfile::TempDircontaining an extracted git fixture bundle. Always.close()it.- Always
sync_all()after creating files inside atempdir(Windows is picky about visibility). - Mock commands with
.cmd("php --version", Some(CommandOutput { stdout: ..., stderr: ... })). - Mock env with
.env("PATH", "..."). - Tests must be hermetic. No reliance on the host having
pythoninstalled, the user's home being a git repo, or anything else mutable.
CI runs --include-ignored, so any test depending on real binaries should be marked #[ignore] and CI sets up the dependency in .github/workflows/workflow.yml.
File and module layout
| Path | Holds |
|---|---|
src/main.rs |
The clap CLI and dispatch |
src/lib.rs |
Public crate roots and the rayon thread pool helper |
src/print.rs |
prompt, module, explain, timings entry points |
src/context.rs |
Context, Repo, Properties, Shell, Target |
src/config.rs |
StarshipConfig (TOML loader), Style, ModuleConfig trait |
src/configs/mod.rs |
FullConfig aggregating every per-module config |
src/configs/<name>.rs |
One per-module config struct + Default impl |
src/configs/starship_root.rs |
Root config, PROMPT_ORDER, default profiles |
src/modules/mod.rs |
mod declarations, handle() dispatch, descriptions |
src/modules/<name>.rs |
One module's logic |
src/modules/utils/ |
Shared module helpers (e.g. directory truncation) |
src/formatter/ |
The format-string DSL: pest grammar, StringFormatter, VersionFormatter |
src/segment.rs |
Segment, TextSegment, FillSegment |
src/init/mod.rs |
The two-phase init logic |
src/init/starship.<shell> |
Per-shell init template |
src/utils/mod.rs |
create_command, exec_cmd, context_path, ANSI wrapping |
src/utils/serde.rs |
Custom serde value deserializer with "Unknown key" warnings |
src/test/mod.rs |
ModuleRenderer, default_context, fixtures |
src/test/fixtures/ |
Bundled git/hg repos for tests |
docs/public/presets/toml/ |
Built-in presets compiled in via build.rs |
.github/config-schema.json |
Generated JSON Schema for starship.toml |
Style nits
#![warn(clippy::disallowed_methods)]is set insrc/main.rsandsrc/lib.rs. The disallowed list lives inclippy.toml— typically methods that have a context-aware Starship wrapper (std::env::var,std::process::Command::new, etc.).use_selfis set towarninCargo.toml. PreferSelf::default()overMyConfig::default()insideimplblocks.- Format strings live in
Default::default()impls; do not concatenate. - Module descriptions in
modules::descriptionare full sentences, capitalized, no trailing period (mostly — seepackagefor the historical odd one out).
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.