niri-wm/niri · error · anyhow::Error

error assigning the seat to libinput

Error message

error assigning the seat to libinput

What it means

niri calls libinput's udev_assign_seat(seat_name) at startup to hand its libseat session's seat to libinput, which lets libinput enumerate keyboards/pointers through udev. The underlying libinput function returns Err(()) when its internal udev context fails to enumerate devices for that seat (no udev access, unreadable /dev/input, or no seat associated with the session). niri wraps this as 'error assigning the seat to libinput' and aborts startup, because a compositor without input is unusable.

Source

Thrown at src/backend/tty.rs:442

        )?;
        let seat_name = session.seat();

        let udev_backend =
            UdevBackend::new(session.seat()).context("error creating a udev backend")?;
        let udev_dispatcher = Dispatcher::new(udev_backend, move |event, _, state: &mut State| {
            state.backend.tty().on_udev_event(&mut state.niri, event);
        });
        event_loop
            .register_dispatcher(udev_dispatcher.clone())
            .unwrap();

        let mut libinput = Libinput::new_with_udev(LibinputSessionInterface::from(session.clone()));
        unsafe { init_libinput_plugin_system(&libinput) };
        {
            let _span = tracy_client::span!("Libinput::udev_assign_seat");
            libinput.udev_assign_seat(&seat_name)
        }
        .map_err(|()| anyhow!("error assigning the seat to libinput"))?;

        // If the session is not active at startup (e.g. niri was launched from a different TTY),
        // suspend libinput now so that when ActivateSession fires, libinput.resume() performs a
        // full re-enumeration of input devices instead of being a no-op.
        if !session.is_active() {
            debug!("session is not active, starting libinput in paused state");
            libinput.suspend();
        }

        let input_backend = LibinputInputBackend::new(libinput.clone());
        event_loop
            .insert_source(input_backend, |mut event, _, state| {
                state.process_libinput_event(&mut event);
                state.process_input_event(event);
            })
            .unwrap();

        event_loop

View on GitHub (pinned to 606284464d)

Solutions

  1. Launch niri from within a registered seat session: log in on a TTY/graphical session managed by logind, or have seatd running (systemctl start seatd) with your user in the seatd group.
  2. Verify udev can enumerate input devices for the seat before launching: 'udevadm info --export-db | grep ID_INPUT_SEAT' should list your keyboards/pointers and 'ls /dev/input/event*' must be readable by your user.
  3. If using logind, confirm the session is active: 'loginctl show-session $XDG_SESSION_ID -p Active -p Type' should print Active=yes; if not, re-login through a real session (not su/sudo).
  4. In containers/VMs, expose /dev/input and /run/udev to the namespace, or run niri nested (inside an existing compositor) instead of on the tty backend.

Example fix

# before (fails: no seat available, libinput cannot enumerate)
ssh host niri
# after (launched from a real logind/seatd session)
loginctl  # confirm an active session for your user
# then start niri from that TTY or via a session-scoped service
niri
Defensive patterns

Strategy: try-catch

Validate before calling

// Rust (smithay/libinput): verify udev sees input devices for the seat before assigning it
let udev = udev::Udev::new().context("cannot open udev context")?;
let mut en = udev::Enumerator::new(&udev)?;
en.match_subsystem("input")?;
let visible = en.scan_devices()?.next().is_some();
anyhow::ensure!(visible, "udev sees no input devices; is this a registered seat session?");

Try / catch

let res = {
    let _span = tracy_client::span!("udev_assign_seat");
    libinput.udev_assign_seat(&seat_name)
};
if res.is_err() {
    // libinput only returns Err(()) with no detail — attach actionable context before bailing
    return Err(anyhow!("error assigning the seat to libinput: \
        ensure a logind/seatd session is active and /dev/input is readable"));
}

Prevention

When it happens

Trigger: Constructing Libinput::new_with_udev(LibinputSessionInterface::from(session)) and calling libinput.udev_assign_seat(&seat_name) while the process has no working seat/session: no seatd socket and no logind session (e.g. launched over SSH or from a bare root shell), a container/namespace without /run/udev and /dev/input, or udev permissions that deny the input subsystem to the user.

Common situations: Starting niri from SSH or cron instead of a logged-in seat session; seatd not running or the user not in the seatd group; a broken/missing systemd-logind session (pam_systemd disabled, session scope not active); running niri inside a container or minimal VM where udev never enumerates input devices; udev/hwdb out of sync after a partial system update.

Related errors


AI-assisted analysis of niri-wm/niri@606284464d (2026-08-16). Data as JSON: /api/errors/bbea66f452c42017. Report an issue: GitHub.