{"record":{"id":"bbea66f452c42017","repo":"niri-wm/niri","slug":"error-assigning-the-seat-to-libinput","errorCode":null,"errorMessage":"error assigning the seat to libinput","messagePattern":"error assigning the seat to libinput","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"src/backend/tty.rs","lineNumber":442,"sourceCode":"        )?;\n        let seat_name = session.seat();\n\n        let udev_backend =\n            UdevBackend::new(session.seat()).context(\"error creating a udev backend\")?;\n        let udev_dispatcher = Dispatcher::new(udev_backend, move |event, _, state: &mut State| {\n            state.backend.tty().on_udev_event(&mut state.niri, event);\n        });\n        event_loop\n            .register_dispatcher(udev_dispatcher.clone())\n            .unwrap();\n\n        let mut libinput = Libinput::new_with_udev(LibinputSessionInterface::from(session.clone()));\n        unsafe { init_libinput_plugin_system(&libinput) };\n        {\n            let _span = tracy_client::span!(\"Libinput::udev_assign_seat\");\n            libinput.udev_assign_seat(&seat_name)\n        }\n        .map_err(|()| anyhow!(\"error assigning the seat to libinput\"))?;\n\n        // If the session is not active at startup (e.g. niri was launched from a different TTY),\n        // suspend libinput now so that when ActivateSession fires, libinput.resume() performs a\n        // full re-enumeration of input devices instead of being a no-op.\n        if !session.is_active() {\n            debug!(\"session is not active, starting libinput in paused state\");\n            libinput.suspend();\n        }\n\n        let input_backend = LibinputInputBackend::new(libinput.clone());\n        event_loop\n            .insert_source(input_backend, |mut event, _, state| {\n                state.process_libinput_event(&mut event);\n                state.process_input_event(event);\n            })\n            .unwrap();\n\n        event_loop","sourceCodeStart":424,"sourceCodeEnd":460,"githubUrl":"https://github.com/niri-wm/niri/blob/606284464d4a99bb35710fee68192bc71085ee7c/src/backend/tty.rs#L424-L460","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"# before (fails: no seat available, libinput cannot enumerate)\nssh host niri\n# after (launched from a real logind/seatd session)\nloginctl  # confirm an active session for your user\n# then start niri from that TTY or via a session-scoped service\nniri","handlingStrategy":"try-catch","validationCode":"// Rust (smithay/libinput): verify udev sees input devices for the seat before assigning it\nlet udev = udev::Udev::new().context(\"cannot open udev context\")?;\nlet mut en = udev::Enumerator::new(&udev)?;\nen.match_subsystem(\"input\")?;\nlet visible = en.scan_devices()?.next().is_some();\nanyhow::ensure!(visible, \"udev sees no input devices; is this a registered seat session?\");","typeGuard":null,"tryCatchPattern":"let res = {\n    let _span = tracy_client::span!(\"udev_assign_seat\");\n    libinput.udev_assign_seat(&seat_name)\n};\nif res.is_err() {\n    // libinput only returns Err(()) with no detail — attach actionable context before bailing\n    return Err(anyhow!(\"error assigning the seat to libinput: \\\n        ensure a logind/seatd session is active and /dev/input is readable\"));\n}","preventionTips":["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."],"tags":["libinput","udev","seat","session","startup","permissions","smithay"],"backgroundTag":"input-device-enumeration-failed","analyzedSha":"606284464d4a99bb35710fee68192bc71085ee7c","analyzedAt":"2026-08-16T21:48:00.228Z","schemaVersion":2},"datasetVersion":"2026-08-18T00:17:09.346Z"}