elkowar/eww · error
Failed to obtain toplevel window
Error message
Failed to obtain toplevel window
What it means
For systray items, widget.toplevel() returns None until the widget is realized and mapped into a window; eww expects it and additionally downcasts to gtk::Window. Panicking here means the tray icon widget had no toplevel GTK window at the moment `maintain` ran.
Solutions
- Defer the work until the widget is mapped: only call toplevel() inside a connect_map/realize handler, or skip the item if toplevel() is None and retry on the next maintain pass.
- Restart/reload the bar so the tray widget is realized before items are processed.
- Update eww — hardening this race (items arriving before the window maps) is a known class of systray bug worth reporting upstream.
- Replace both expects with logging and early-return so a single bad item cannot kill the daemon.
Example fix
// before
let window = widget
.toplevel().expect("Failed to obtain toplevel window")
.downcast::<Window>().expect("Failed to downcast window");
// after
let window = match widget.toplevel().and_then(|t| t.downcast::<Window>().ok()) {
Some(w) => w,
None => {
log::warn!("Systray item has no toplevel window yet; skipping");
return;
}
}; Defensive patterns
Strategy: type-guard
Validate before calling
// only proceed once the widget is realized and mapped
if !widget.is_realized() || widget.toplevel().is_none() { return; } Type guard
fn toplevel_window(widget: >k::Widget) -> Option<gtk::Window> {
widget.toplevel().and_then(|t| t.downcast::<gtk::Window>().ok())
} Try / catch
// replace expect with early return
let window = match toplevel_window(&widget) {
Some(w) => w,
None => { log::warn!("no toplevel window for tray item yet"); return; }
}; Prevention
- Initialize the systray only after the bar window is mapped (connect_map)
- Handle reload/recreate races by re-running maintain on map events
- Never .expect() on toplevel(); it is legitimately None pre-mapping
- Keep eww updated; this is a known race class in tray code
When it happens
Trigger: maintain() processing a StatusNotifier item while the containing widget is not yet realized/mapped — e.g. the tray is created before the bar window exists, the bar was re-created during a reload, or the item arrived during window teardown — so toplevel() yields None; a non-Window toplevel also fails the downcast.
Common situations: Eww bars reloaded or re-created while a systray item connects, tray widgets in popup/unmapped containers, and race conditions on startup where items register faster than the bar window maps.
Related errors
- Error, missing argument
- variables unexpectedly defined when creating window with id
- Error, trying to add multiple children to a…
- could not get default display
- Failed to initialize tokio runtime
AI-assisted analysis of elkowar/eww@48f5aa8b37 (2026-09-08).
Data as JSON: /api/errors/373316b0f6c0f2cc.
Report an issue: GitHub.
Appendix: source
Thrown at crates/eww/src/widgets/systray.rs:199
// TODO this is a lot of code duplication unfortunately, i'm not really sure how to
// refactor without making the borrow checker angry
// set status
match item.status().await? {
notifier_host::Status::Passive => widget.hide(),
notifier_host::Status::Active | notifier_host::Status::NeedsAttention => widget.show(),
}
// set title
widget.set_tooltip_text(Some(&item.sni.title().await?));
// set icon
let scale = icon.scale_factor();
load_icon_for_item(&icon, &item, *icon_size.borrow_and_update(), scale).await;
let item = Rc::new(item);
let window =
widget.toplevel().expect("Failed to obtain toplevel window").downcast::<Window>().expect("Failed to downcast window");
widget.add_events(gdk::EventMask::BUTTON_PRESS_MASK);
widget.connect_button_press_event(glib::clone!(@strong item => move |_, evt| {
let (x, y) = (evt.root().0 as i32 + window.x(), evt.root().1 as i32 + window.y());
let item_is_menu = run_async_task(async { item.sni.item_is_menu().await });
let have_item_is_menu = item_is_menu.is_ok();
let item_is_menu = item_is_menu.unwrap_or(false);
log::debug!(
"mouse click button={}, x={}, y={}, have_item_is_menu={}, item_is_menu={}",
evt.button(),
x,
y,
have_item_is_menu,
item_is_menu
);
let result = match (evt.button(), item_is_menu) {
(gdk::BUTTON_PRIMARY, false) => {
let result = run_async_task(async { item.sni.activate(x, y).await });View on GitHub (pinned to 48f5aa8b37)