gitbutlerapp/gitbutler · error

theme may only be initialized once

Error message

theme may only be initialized once

What it means

The global TUI theme lives in a static OnceLock<Theme>; theme::init(theme) must run exactly once per process before anything reads it, and a second init call panics here. The real CLI inits once at startup, so this panic is almost always a same-process re-entry issue: test harnesses that run several but entry points in one process, or an embedded host (daemon/MCP-style) that re-initializes per session.

Source

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

const MONOKAI_THEME_LIGHT: &[u8] =
    include_bytes!("../assets/syntax-highlighting-themes/Monokai Extended Light.tmTheme");

/// The minimum number of change ID characters displayed for a commit, so that
/// short IDs remain visually distinctive.
pub(crate) const MIN_DISPLAYED_CHANGE_ID_CHARS: usize = 3;

/// Global theme instance, initialized once at startup.
static THEME: OnceLock<Theme> = OnceLock::new();

/// Initialize the global theme.
///
/// Must be called exactly once, before any call to [`get`].
/// Panics if called more than once.
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")
    }
}

View on GitHub (pinned to 2497b8007a)

Solutions

  1. Initialize the theme once at process start, not per operation; move init into the single entry bootstrap
  2. In callers that may repeat, guard with std::sync::Once (see validationCode) so init runs at most once per process
  3. Expose/depend on a try_init(theme) -> bool (THEME.set(theme).is_ok()) and use it where repeated init is possible
  4. In tests, rely on theme::get()'s cfg(test) defaulting (get_or_init(Theme::default)) instead of calling init

Example fix

// before — second call in the same process panics
theme::init(theme);

// after — idempotent guard at the call site (or make init itself try-init)
static THEME_INIT: std::sync::Once = std::sync::Once::new();
THEME_INIT.call_once(|| theme::init(theme));
Defensive patterns

Strategy: validation

Validate before calling

// idempotent bootstrap — safe to call from tests/embeddings repeatedly
static THEME_INIT: std::sync::Once = std::sync::Once::new();
THEME_INIT.call_once(|| {
    theme::init(theme::load(theme_path).unwrap_or_default());
});

Try / catch

// detect double-init without dying: keep the existing theme if already set
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| theme::init(theme)));

Prevention

When it happens

Trigger: Calling theme::init twice within one process: integration tests that invoke TUI commands repeatedly without process isolation; a library embedding that calls init per command; wrapping entry points in a loop in a long-lived host.

Common situations: Rust test suites where multiple tests each bootstrap the theme in the same process; plugin/embedding code around but crates; refactoring main() so init moved into a per-invocation helper.

Related errors


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