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_loopView on GitHub (pinned to 606284464d)
Solutions
- 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.
- 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.
- 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).
- 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
- Always launch the compositor from a registered seat (logind session or seatd), never from SSH/cron/su.
- Add a preflight check to your startup script: 'udevadm trigger -s input && test -r /dev/input/event0'.
- Keep udev, libinput and systemd versions consistent; re-trigger udev ('udevadm control --reload') after rule updates.
- In containers, mount /dev/input and /run/udev or use the nested backend instead of tty.
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.