Open-Source Wikis

/

Starship

/

How to contribute

/

Patterns and conventions

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 str for 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: bool is supported on every standard module. The Context method is_module_disabled_in_config short-circuits computation.
  • version_format defaults to "v${raw}" and is processed by VersionFormatter — 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 (like symbol from config) and the implicit style variable.
  • map_style — produces Ok(&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 by git_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 via crate::config::Style) — the underlying ANSI primitive.
  • Style::from_config(s) in src/config.rs — parses the user's space-separated style tokens, with palette resolution.
  • Segment::ansi_string(prev_style) — emits an AnsiString, which is then collected by nu_ansi_term::AnsiStrings to 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 return None. 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() in src/test/mod.rs returns a tempfile::TempDir containing an extracted git fixture bundle. Always .close() it.
  • Always sync_all() after creating files inside a tempdir (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 python installed, 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 in src/main.rs and src/lib.rs. The disallowed list lives in clippy.toml — typically methods that have a context-aware Starship wrapper (std::env::var, std::process::Command::new, etc.).
  • use_self is set to warn in Cargo.toml. Prefer Self::default() over MyConfig::default() inside impl blocks.
  • Format strings live in Default::default() impls; do not concatenate.
  • Module descriptions in modules::description are full sentences, capitalized, no trailing period (mostly — see package for 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.

Patterns and conventions – Starship wiki | Factory