gitbutlerapp/gitbutler · info
symbols must always be initialized
Error message
symbols must always be initialized
What it means
Theme::sym() unwraps the private field symbols: Option<ThemeSymbols>. Every sanctioned constructor populates it: default_for() assigns t.symbols = Some(ThemeSymbols::new(&t)), Default delegates there, and the field is #[serde(skip_serializing, skip_deserializing)] while the container-level #[serde(default)] restores it from Theme::default(), so JSON-loaded themes also carry Some. The expect documents an internal invariant: a panic means a Theme was constructed bypassing these constructors.
Solutions
- Construct themes only via Theme::default(), Theme::default_for(preset), or serde deserialization
- When adding a constructor, finish it with t.symbols = Some(ThemeSymbols::new(&t)) exactly like default_for does
- Long term: apply the refactor noted in the source comment - split symbols out of the deserialized struct so the Option disappears
Example fix
// before let mut t = default_dark(); // ... symbols still None here -> t.sym() would panic // after let mut t = default_dark(); t.symbols = Some(ThemeSymbols::new(&t)); // invariant restored before sym() is reachable
Defensive patterns
Strategy: validation
Validate before calling
// Only sanctioned constructors keep the invariant; route all creation through them: let theme = but::theme::Theme::default_for(preset); // symbols initialized // or deserialize - container-level #[serde(default)] restores symbols let theme: but::theme::Theme = serde_json::from_str(json)?;
Prevention
- Never build Theme by struct literal or partial copy outside default_for/default_light/dark
- End any new constructor with t.symbols = Some(ThemeSymbols::new(&t))
- If refactoring Theme, split symbols into a separate struct to delete the Option entirely
When it happens
Trigger: Only in-crate construction can leave symbols None - e.g. using default_light()/default_dark() output before default_for() fixes it up, or new code building/rebuilding a Theme struct literal that drops the field. Calling .sym() on such a value panics.
Common situations: Contributors adding new Theme constructors or partially cloning/rebuilding Theme values. Not triggerable by user theme files: serde round-trips keep symbols initialized.
Related errors
- anchor is always present in the order at this point
- BUG: Sensitive data cannot be serialized - it needs to be…
- prefiltered
- statically known
- theme::init() must be called before getting the theme
AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20).
Data as JSON: /api/errors/da4d146463590174.
Report an issue: GitHub.
Appendix: 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 caf1f223d3)