gitbutlerapp/gitbutler · error

symbols must always be initialized

Error message

symbols must always be initialized

What it means

Theme::sym() returns the derived ThemeSymbols stored as an Option field, because symbols can only be constructed from the fully-built theme (default_for sets t.symbols = Some(ThemeSymbols::new(&t)) last). The field is #[serde(skip_serializing, skip_deserializing)] (theme.rs:356-363), so any Theme produced by deserialization — notably theme::load() reading a custom JSON theme file — has symbols == None, and the first sym() call panics. Every construction path that bypasses default_for (serde, struct literals) hits this.

Source

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

/// Helper — builds a bold + colored [`Style`].
const fn style_fg_bold(fg: Color) -> Style {
    Style::new().fg(fg).add_modifier(Modifier::BOLD)
}

impl Default for Theme {
    /// Produces the canonical color palette.
    fn default() -> Self {
        Self::default_for(ThemePreset::Dark)
    }
}

impl Theme {
    /// Get the symbols for this theme.
    pub fn sym(&self) -> &ThemeSymbols {
        self.symbols
            .as_ref()
            .expect("symbols must always be initialized")
    }

    /// Produces a specific default color palette.
    pub fn default_for(preset: ThemePreset) -> Self {
        let mut t = match preset {
            ThemePreset::Light => Self::default_light(),
            ThemePreset::Dark => Self::default_dark(),
        };
        t.symbols = Some(ThemeSymbols::new(&t));
        t
    }

    /// Load the syntax highlighting theme.
    pub fn load_syntax_highlighting_theme(&self) -> anyhow::Result<highlighting::Theme> {
        Ok(ThemeSet::load_from_reader(&mut std::io::Cursor::new(
            self.syntax_highlighting_theme_raw,
        ))?)
    }

View on GitHub (pinned to 2497b8007a)

Solutions

  1. Finish deserialized themes: in theme::load(), after serde_json::from_str, set theme.symbols = Some(ThemeSymbols::new(&theme)) (or add a pub fn finish() and call it wherever a Theme is built)
  2. If calling from outside the crate, construct via Theme::default_for(preset) and overlay JSON fields instead of deserializing a full Theme
  3. Make sym() degrade to a static default symbol table instead of expecting
  4. Add a regression test: load a JSON theme, then call sym()

Example fix

// before (theme::load)
let theme: Theme = serde_json::from_str(&contents)?;
Ok(theme)

// after — complete post-deserialization initialization
let mut theme: Theme = serde_json::from_str(&contents)?;
theme.symbols = Some(ThemeSymbols::new(&theme));
Ok(theme)
Defensive patterns

Strategy: try-catch

Validate before calling

// probe a custom-theme-loaded Theme before relying on it in a host process
let ok = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| theme.sym())).is_ok();
if !ok { /* fall back to Theme::default_for(ThemePreset::Dark) */ }

Try / catch

// degrade to the default palette's symbols instead of crashing the TUI
let theme = std::panic::catch_unwind(AssertUnwindSafe(|| theme::load(path)))
    .ok()
    .map(|mut t| { t.symbols = Some(ThemeSymbols::new(&t)); t }) // when accessible
    .unwrap_or_else(Theme::default);

Prevention

When it happens

Trigger: Loading a custom theme from a JSON file via theme::load() (theme.rs:93-97 returns the deserialized value without filling symbols) and then rendering anything that calls Theme::sym() — TUI symbols/icons on first draw. Also manually constructing a Theme literal in code without the final symbols step.

Common situations: A user-supplied theme file configured through but's config; tests/tools building a Theme via serde_json::from_*; partial theme JSON (absent fields keep defaults, but symbols is skipped entirely so it stays None regardless of file contents).

Related errors


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