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
- Initialize the theme once at process start, not per operation; move init into the single entry bootstrap
- In callers that may repeat, guard with std::sync::Once (see validationCode) so init runs at most once per process
- Expose/depend on a try_init(theme) -> bool (THEME.set(theme).is_ok()) and use it where repeated init is possible
- 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
- Call theme::init exactly once, at process start — never per command, request, or test iteration
- In test harnesses, prefer theme::get()'s cfg(test) default over calling init at all
- If embedding but entry points in one process, isolate them per process (as the real CLI does) or guard with Once
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
- theme::init() must be called before getting the theme
- symbols must always be initialized
- Failed to persist action: {e}
- Failed to list actions: {e}
- CliInstallCancelled
AI-assisted analysis of gitbutlerapp/gitbutler@2497b8007a (2026-08-17).
Data as JSON: /api/errors/37e607a799a4213b.
Report an issue: GitHub.