cjpais/Handy · error

failed to create separator

Error message

failed to create separator

What it means

The separator closure wraps PredefinedMenuItem::separator(app) with .expect("failed to create separator"). Separators are the cheapest muda items but go through the same native menu manager; because update_tray_menu creates ~5-8 of them per rebuild, a failing backend often surfaces here first. The panic aborts whatever thread rebuilds the tray, leaving the previous menu in place or crashing the app outright.

Source

Thrown at src-tauri/src/tray.rs:242

pub fn refresh_tray_icon(app: &AppHandle) {
    sync_tray(app);
}

/// Re-syncs the tray after something the menu depends on changed (model
/// list/selection/loaded state, language, settings).
pub fn update_tray_menu(app: &AppHandle) {
    sync_tray(app);
}

/// Records the current desired tray state and schedules one apply on the main
/// thread (or lets an already-pending apply pick it up). Never blocks on the
/// main thread.
///
/// The snapshot (settings, model list, loaded state) is computed on the
/// *calling* thread on purpose: the main-thread applier must not take manager
/// locks that a worker may hold across slow work (see #1716).
pub fn sync_tray(app: &AppHandle) {
    sync_tray_with(app, |_| {});
}

fn sync_tray_with(app: &AppHandle, update: impl FnOnce(&mut TrayInner)) {
    let Some(state) = app.try_state::<TrayState>() else {
        return;
    };

    // Record intent and claim a sequence number in one critical section, so
    // sequence order == the order in which state changes were requested.
    let (seq, icon_state) = {
        let mut inner = state.lock();
        update(&mut inner);
        inner.next_seq += 1;
        (inner.next_seq, inner.icon_state)
    };

    // Tray not built yet (early secure-input monitor callbacks). The intent
    // is kept and picked up by the first sync after the tray exists.

View on GitHub (pinned to fbd4e15fa1)

Solutions

  1. Ensure a StatusNotifierWatcher exists on the DBus session before the app starts (tray extension, waybar, plasma-systemtray)
  2. Replace the .expect in the separator closure with Result propagation so the caller can abort the rebuild gracefully
  3. Cache one separator creation failure and disable further tray rebuilds for the session instead of panicking repeatedly
  4. Delay the initial update_tray_menu call until the tray icon is confirmed visible

Example fix

// before
let separator = || PredefinedMenuItem::separator(app).expect("failed to create separator");

// after
let separator = || PredefinedMenuItem::separator(app).map_err(|e| {
    error!("Failed to create separator: {e}");
    e
});
// then propagate with ? in a update_tray_menu that returns Result<()>
Defensive patterns

Strategy: fallback

Try / catch

let separator = || PredefinedMenuItem::separator(app).map_err(|e| { error!("separator creation failed: {e}"); e });
// caller: let Some(sep) = separator() else { return; };

Prevention

When it happens

Trigger: Any rebuild of the tray (state change to Recording/Transcribing/Idle, language switch, secure-input warning toggle) on a system where the native/DBus menu backend cannot register items; the closure runs multiple times per menu build, so it multiplies the chance of hitting the broken backend.

Common situations: Linux desktops without a system-tray applet or StatusNotifierWatcher; X11 sessions where the tray DBus name vanished mid-session; running in environments where the app was started before the session bus was ready.

Related errors


AI-assisted analysis of cjpais/Handy@fbd4e15fa1 (2026-08-17). Data as JSON: /api/errors/877f52a04122167b. Report an issue: GitHub.