Skip to content

feat(input): switch libinput to udev mode for hotplug support - #10

Merged
sanohiro merged 1 commit into
sanohiro:mainfrom
kay-ws:feature/libinput-udev-hotplug
Apr 22, 2026
Merged

feat(input): switch libinput to udev mode for hotplug support#10
sanohiro merged 1 commit into
sanohiro:mainfrom
kay-ws:feature/libinput-udev-hotplug

Conversation

@kay-ws

@kay-ws kay-ws commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Problem

bcon currently scans /dev/input/event* once at startup via
Libinput::new_from_path + path_add_device. Devices that appear
after startup are never picked up:

  • USB keyboards plugged in after bcon starts don't work.
  • USB mice plugged in after bcon starts don't work.
  • Touchpads connected after startup never get the touchpad
    configuration (tap-to-click, natural-scroll, disable-while-typing)
    applied.

Fix

Switch both constructors to udev mode:

let mut input = Libinput::new_with_udev(interface);
input.udev_assign_seat("seat0")?;

This makes libinput monitor udev for device add/remove and emit
DeviceEvent::Added / DeviceEvent::Removed. Existing devices are
also delivered as Added after udev_assign_seat, so the one-shot
scan loop is no longer needed.

A new Event::Device match arm in process_raw_events handles the
device lifecycle:

  • DeviceEvent::Added — call configure_touchpad on the new device,
    so hotplugged touchpads get the same tap-to-click / natural-scroll /
    disable-while-typing settings as boot-time ones.
  • DeviceEvent::Removed — clear held_keys and modifier state. If a
    keyboard is unplugged while keys are held down, the corresponding
    Released events never arrive; without cleanup those keys stay in
    held_keys and generate phantom key repeats in the repeat loop.
    suspend() already performs the same cleanup, so users are already
    accustomed to this transient-state-loss behavior.

MouseConfig is cloned into a new struct field so the Added handler
can apply touchpad config long after the constructor returns.

Both constructors are migrated:

  • EvdevKeyboard::new — direct device access path.
  • EvdevKeyboard::new_with_seat — libseat path.

No Cargo.toml changes: input = "0.8" already enables the udev
feature by default.

Testing

Verified on Arch Linux (kernel 6.19.11) with bcon@tty2.service.

1. Startup device enumeration via udev

Debug log on service start shows all existing devices delivered as
DeviceEvent::Added events:

evdev: libinput udev context initialized (seat0)
Input device added: "Power Button"
Input device added: "QEMU QEMU USB Tablet"
Input device added: "AT Translated Set 2 keyboard"
Input device added: "VirtualPS/2 VMware VMMouse"
Input device added: "VirtualPS/2 VMware VMMouse"

2. Hotplug: uinput device created after startup

To exercise the hotplug path in a QEMU guest where physical USB
hotplug isn't easily simulated, a uinput virtual keyboard was
created after bcon was already running, and ctrl+0 (font-reset
action) was injected through it:

Input device added: "<uinput virtual keyboard>"
EmojiAtlas: cache cleared due to font size change
Framebuffer created: id=43, 1280x800, stride=5120
Input device removed: "<uinput virtual keyboard>"

The EmojiAtlas: cache cleared line and the framebuffer redraw
confirm the font-reset action fired end-to-end through the new
hotplugged device — something the previous code path made impossible.

3. VT switch + suspend/resume

Ctrl+Alt+F1 → tty1 (~27 s) → Ctrl+Alt+F2 back to bcon. libinput
re-emits Removed + Added for all devices on resume, which re-applies
configure_touchpad cleanly:

VT release requested
Suspending libinput
VT released
... (27 s on tty1) ...
VT acquire
Resuming libinput
VT acquired
Input device removed: "Power Button"   / ... (5 devices)
Input device added:   "Power Button"   / ... (5 devices)

Pane tree and on-screen state are preserved across the switch.

4. Existing functionality — regression pass

  • Screen rendering, login prompt, PTY echo ✓
  • Keyboard and mouse input via physical input devices ✓
  • Japanese IME input (Ctrl+Space commit, preedit display) ✓
  • Pane split (Ctrl+Shift+O, Ctrl+Shift+Enter) ✓
  • Font scaling (Ctrl+Minus) with visible size change ✓

5. Unit tests

cargo test
...
test result: ok. 44 passed; 0 failed; 1 ignored

Not yet tested

  • Physical USB keyboard hotplug on real hardware (planned for
    follow-up; the test environment is QEMU/KVM which can't trivially
    simulate USB hotplug from the guest's point of view).
  • Stuck-key scenario where a keyboard is physically unplugged
    mid-keypress. The current simple cleanup (clear all held state on
    any device removal) is a conservative choice that may very briefly
    reset modifier state on uinput-originated short-lived devices too;
    a more surgical per-device cleanup could be added later if it
    becomes a problem in practice.

Replace Libinput::new_from_path + one-shot /dev/input/event* scan with
Libinput::new_with_udev + udev_assign_seat("seat0") so that devices
added after startup are picked up automatically:

- USB keyboards/mice plugged in after bcon starts now work
- uinput virtual devices (ydotool, bconmsg injection, etc.) reach bcon
- No more reliance on periodic restarts to rescan /dev/input

Device enumeration for existing devices and hotplug events flow through
a new Event::Device match arm in process_raw_events:

- DeviceEvent::Added applies touchpad config (tap-to-click, natural
  scroll, disable-while-typing) via the existing configure_touchpad
  helper, so hotplugged touchpads get the same settings as boot-time
  ones.
- DeviceEvent::Removed clears held_keys and modifier state. If a
  keyboard is unplugged while keys are held down, the corresponding
  Released events never arrive, which would otherwise stick keys and
  emit phantom key repeats. suspend() performs the same cleanup, so
  users already accept this behavior on transient state loss.

To support applying touchpad config in the Added handler, MouseConfig
is cloned into a new struct field.

Both constructors are migrated:
- EvdevKeyboard::new (direct device access)
- EvdevKeyboard::new_with_seat (libseat path)

No Cargo.toml changes — input = "0.8" already enables the udev feature
by default.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@sanohiro
sanohiro merged commit 8e758d4 into sanohiro:main Apr 22, 2026
1 check passed
@sanohiro

Copy link
Copy Markdown
Owner

Thanks for this! Switching from path to udev mode is the right call — the code is cleaner (net negative lines!) and hotplug support is a real usability win. The device removal cleanup for held keys is a nice touch too.

Really appreciate the thorough testing and clear write-up. Also thanks for #9 (Cargo.lock sync) — that saved downstream packagers.

sanohiro added a commit that referenced this pull request Apr 22, 2026
Switch libinput from path mode to udev mode for automatic device
hotplug detection. USB keyboards and mice plugged in after bcon
starts now work immediately. Touchpad configuration (tap-to-click,
natural scroll, disable-while-typing) is auto-applied to
hotplugged devices.

Also adds version bump procedure to CLAUDE.md (always sync
Cargo.lock with cargo update -p bcon).

Contributed by @kay-ws (#10).
@kay-ws
kay-ws deleted the feature/libinput-udev-hotplug branch April 22, 2026 23:12
@kay-ws

kay-ws commented Apr 23, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the quick merge and the v1.3.4 release! Upgraded the bcon-test
VM to 1.3.4-1 via pacman and re-verified the udev hotplug flow end-to-end:
uinput virtual device detection, existing keyboard/mouse/touchpad,
VT switch (Ctrl+Alt+F1/F2) round-trip, and bconmsg-driven action dispatch
all working correctly. The held-key cleanup and net-negative lines
feedback made my day.

The two items still marked "not yet tested" in the PR description
(physical USB hotplug on cathy-note, stuck-key regression under real
device pull-out) are on my list — I'll report back here if anything
surfaces.

(Separately, I've opened #11 to discuss AUR packaging for Arch users
— no urgency, just wanted to run it by you.)

@kay-ws

kay-ws commented Apr 23, 2026

Copy link
Copy Markdown
Contributor Author

Post-merge hardware verification on cachy-note (CachyOS, Intel UHD
620, linux-cachyos kernel 6.19.11, Logitech Unifying Receiver with
a K270 keyboard):

All three "not yet tested" items in the PR body now confirmed:

  1. USB keyboard hotplug (plug-in): With bcon running on tty2,
    inserted a Unifying dongle paired to a K270. libinput debug-events
    saw the DEVICE_ADDED event (event14 Logitech K270), and
    typed characters flowed through to the bcon PTY as expected
    (verified with abc and longer strings).

  2. USB keyboard unplug under held key (stuck-key regression):
    Held a on the USB keyboard (producing repeat in bcon), then
    physically yanked the dongle. The repeat stopped immediately;
    libinput debug-events showed a synthesized released event
    on event14 right before DEVICE_REMOVED — libinput's own
    defensive release, combined with the simple held_keys.clear()
    in the DeviceEvent::Removed handler, gave clean state cleanup.

  3. Internal keyboard after unplug: Immediately after the dongle
    was removed, the internal laptop keyboard worked normally —
    typing b produced a normal repeat sequence on the internal
    event2 path, no modifier / held-key residue from the USB side.

Additionally confirmed: VT release/acquire (Ctrl+Alt+F1 ↔ F2) cycle
remains clean across multiple round-trips, libinput suspend/resume
behaves as expected.

Nothing to report as a regression — the udev switch works end-to-end
on real hardware. Thanks again for the work that went into v1.3.4.

sanohiro added a commit that referenced this pull request May 7, 2026
New features (contributed by @kay-ws):

Config discovery:
- Layered XDG-style merge: built-in defaults → /etc/bcon/config.toml
  (system) → ~/.config/bcon/config.toml (user override)
- Tables merge recursively; scalars and arrays replace wholesale
- BCON_CONFIG env var for single-file debug bypass
- Config::default_template() API for distributors

CLI:
- --init-config now accepts explicit target: system, user, or path
- sudo bcon --init-config=system deterministically writes to /etc/
- Legacy --init-config=vim,jp still writes to ~/.config/ (backward compat)

Also includes:
- Arch Linux installation docs (#12)
- libinput udev hotplug support (#10)
- Lenient Kitty graphics base64 decode (#7)

Contributors: @kay-ws

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants