Skip to content

fix(gui): avoid RefCell panic when applying theme on OS appearance change#337

Open
stefan-siebert wants to merge 1 commit into
AprilNEA:masterfrom
stefan-siebert:fix/appearance-observer-refcell-panic
Open

fix(gui): avoid RefCell panic when applying theme on OS appearance change#337
stefan-siebert wants to merge 1 commit into
AprilNEA:masterfrom
stefan-siebert:fix/appearance-observer-refcell-panic

Conversation

@stefan-siebert

Copy link
Copy Markdown

Problem

openlogi-gui panics on launch on Linux (both Wayland and X11) with:

thread 'main' panicked at .../gpui_linux/src/linux/wayland/client.rs:924:23:
RefCell already borrowed

(equivalently x11/client.rs:1529 on the X11 backend). The CLI is unaffected — only the GUI crashes, immediately, on essentially every launch. Reproduced against the shipped v0.6.18 build (gpui rev eb2223c).

Root cause

theme::apply_from_settings reads the OS appearance via cx.window_appearance(). On Linux that routes through the platform client's RefCell:

cx.window_appearance()
  → LinuxPlatform::window_appearance()
  → self.inner.with_common(...)        // gpui_linux platform.rs
  → self.0.borrow_mut().common         // gpui_linux .../client.rs  ← panics

apply_from_settings is invoked from the window-appearance observer wired up in open_main_window (main.rs). gpui fires that observer from inside its xdg-desktop-portal handler, which holds the platform client's RefCell mutably borrowed across the dispatch:

// gpui_linux .../wayland/client.rs — XDPEvent::WindowAppearance
let mut client = client.borrow_mut();          // borrows self.0 ...
for window in client.windows.values_mut() {
    window.set_appearance(appearance);          // ... fires our observer here
}

So when the portal reports the initial colour scheme at startup, the observer re-enters the platform via cx.window_appearance() while the same RefCell is already borrowed → panic. (Deferring the work doesn't help: gpui flushes the deferred effect before that borrow is released.)

This almost certainly never surfaced on macOS, where appearance dispatch doesn't hold a RefCell across the callback.

Fix

Read the OS appearance from the Window already in hand — a borrow-free field access (Window::appearance()) — instead of querying the platform. The platform query (cx.window_appearance()) is kept only for the no-window case (a live settings edit), which never runs inside the portal borrow.

Verification

  • Release build (matching the shipped v0.6.18 gpui rev eb2223c) compiles clean; cargo clippy -p openlogi-gui is clean.
  • Before: panicked on nearly every launch. After: 0 panics across repeated launches, and the window stays up.

🤖 Generated with Claude Code

…ange

`apply_from_settings` queried `cx.window_appearance()`, which on Linux
routes through the platform client's `RefCell` (`with_common`). It is
called from the window-appearance observer, and gpui fires that observer
from inside its xdg-desktop-portal handler while that same `RefCell` is
already mutably borrowed. Re-entering it there panics with "RefCell
already borrowed" on the wayland (and x11) backend, crashing the GUI on
launch when the portal reports the initial colour scheme.

Read the OS appearance from the `Window` already in hand instead — a
borrow-free field access (`Window::appearance()`) — and only fall back to
the platform query when no window is available (a settings edit, which
never runs inside the portal borrow).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jun 30, 2026

Copy link
Copy Markdown

Greptile Summary

This PR changes how the GUI reads OS appearance while applying themes.

  • Uses the current Window appearance when a window is available.
  • Keeps cx.window_appearance() for no-window settings updates.
  • Documents the Linux RefCell re-entry panic that this avoids.

Confidence Score: 5/5

This looks safe to merge after checking the System theme fallback behavior.

  • The Linux observer path avoids the re-entrant platform query that caused the crash.
  • The only follow-up is making sure Window::appearance() cannot hold a stale forced value when returning to System mode.

crates/openlogi-gui/src/theme.rs

Important Files Changed

Filename Overview
crates/openlogi-gui/src/theme.rs Updates apply_from_settings to prefer Window::appearance() when a window is available, while preserving the old platform query for settings edits without a window.

Fix All in Codex Fix All in Claude Code

Reviews (1): Last reviewed commit: "fix(gui): avoid RefCell panic when apply..." | Re-trigger Greptile

Comment on lines +153 to +155
let os_appearance = window
.as_ref()
.map_or_else(|| cx.window_appearance(), |w| w.appearance());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Window Appearance May Be Stale

When an existing window is passed while the user preference is System, this now resolves the theme from w.appearance() instead of the platform appearance. If that window value is a cached or forced per-window appearance after switching from Light or Dark back to System, the System branch can keep applying the old mode instead of following the OS.

Fix in Codex Fix in Claude Code

@stefan-siebert

Copy link
Copy Markdown
Author

Thanks for the review! On the P2 "Window Appearance May Be Stale" — I believe it's a false positive: the Light/DarkSystem transition can't reach the changed branch.

  • Every preference change goes through set_appearance / set_radius in windows/settings/appearance.rs (lines 81, 88, 522), all of which call apply_from_settings(None, cx). The None branch is unchanged — it still uses the platform query cx.window_appearance(). So switching back to System reads the live OS appearance, exactly as before.
  • The Some(window) branch is only reached from:
    • window build / first paint (main.rs:588, windows/mod.rs:85) — the preference is freshly loaded and the window's appearance is fresh from creation; and
    • the observe_window_appearance callback (main.rs:593, windows/mod.rs:88), which gpui fires from Window::appearance_changed. That sets self.appearance = platform_window.appearance() before invoking observers (gpui window.rs), so w.appearance() is guaranteed current there.
  • On Linux the per-window appearance is never force-overridden anyway: platform::os::set_app_appearance is a no-op there, and Light/Dark forcing is done via gpui-component's Theme::change, not by setting the platform window's appearance — so Window::appearance() always reflects the real OS scheme.

Net: no path resolves System from a stale forced window value, so I don't think a change is needed here. Happy to adjust if I've missed a call path.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants