gitbutlerapp/gitbutler · error

theme::init() must be called before getting the theme

Error message

theme::init() must be called before getting the theme

What it means

The counterpart of the double-init panic: in non-test builds theme::get() reads the global OnceLock and panics unless theme::init already populated it. It fires when a code path renders styled output (TUI screens, tables, colored IDs) without the startup code having loaded the theme first — typically a new entry point that skips theme initialization or a library consumer calling but's rendering helpers without the CLI bootstrap.

Source

Thrown at crates/but/src/theme.rs:84

pub fn init(theme: Theme) {
    THEME
        .set(theme)
        .expect("theme may only be initialized once");
}

/// Return a reference to the global theme.
///
/// Panics if [`init`] has not been called yet.
pub fn get() -> &'static Theme {
    #[cfg(test)]
    {
        THEME.get_or_init(Theme::default)
    }
    #[cfg(not(test))]
    {
        THEME
            .get()
            .expect("theme::init() must be called before getting the theme")
    }
}

/// Load a theme from a JSON file.
///
/// Fields that are absent in the file keep their [`Theme::default`] values.
pub fn load(path: &Path) -> anyhow::Result<Theme> {
    let contents = std::fs::read_to_string(path)?;
    let theme: Theme = serde_json::from_str(&contents)?;
    Ok(theme)
}

/// Extension trait that lets us apply a [`Style`] to "paint" a string with raw ANSI escape codes.
///
/// ```ignore
/// use crate::theme::Paint;
/// let t = crate::theme::get();
/// writeln!(out, "{}", t.local_branch.paint(&name))?;

View on GitHub (pinned to 2497b8007a)

Solutions

  1. Call theme::init(theme::load(path).unwrap_or_default()) — or init with a preset — at the very start of the entry point, before any rendering can happen
  2. Make get() degrade gracefully: replace the expect with THEME.get_or_init(Theme::default) so a missed init falls back to the default palette instead of crashing
  3. Audit every entry point (CLI, TUI, MCP) for the init call when adding one
  4. In unit tests the cfg(test) branch already defaults; keep testable render paths under cfg(test)

Example fix

// before
THEME.get().expect("theme::init() must be called before getting the theme")

// after — missing init degrades to the default palette
THEME.get_or_init(Theme::default)
Defensive patterns

Strategy: validation

Validate before calling

// single bootstrap helper every entry point must call before rendering
fn bootstrap_theme(theme_path: Option<&std::path::Path>) {
    let theme = theme_path.map_or_else(Theme::default, |p| theme::load(p).unwrap_or_default());
    theme::init(theme); // must precede any theme::get() / styled output
}

Try / catch

// last-resort recovery if a code path skipped init: catch, init a default, retry once
let theme = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(theme::get)) {
    Ok(t) => t,
    Err(_) => { theme::init(Theme::default()); theme::get() }
};

Prevention

When it happens

Trigger: Invoking anything that calls theme::get() before theme::init: a newly added subcommand that prints styled output but forgets the init step; main() refactored so init is conditional (e.g. only in TUI mode) while another path renders early; embedding but crates in another app.

Common situations: Adding new commands or entry points; early error formatting that runs before init; using the crate as a library; version changes that move the init call deeper into startup.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@2497b8007a (2026-08-17). Data as JSON: /api/errors/60a0b8e4204f88e3. Report an issue: GitHub.