[GH-ISSUE #14] First-class NixOS support for vuinputd #12

Open
opened 2026-10-30 21:32:28 +00:00 by bsakaguchi · 11 comments
Owner

Originally created by @joleuger on GitHub (May 6, 2026).
Original GitHub issue: https://github.com/joleuger/vuinputd/issues/14

First-class NixOS support for vuinputd

Motivation

vuinputd is evolving into a flexible userspace input virtualization daemon, and NixOS is a particularly strong target platform due to:

  • declarative system configuration
  • strong integration with systemd
  • reproducible builds and environments
  • common usage in homelab / desktop virtualization setups

Currently, NixOS support is work-in-progress and requires manual setup. The goal is to make vuinputd a first-class citizen on NixOS with a clean, idiomatic module.

Reference draft documentation:
https://github.com/joleuger/vuinputd/blob/main/docs/USAGE-NIXOS.md


Goals

  • Add vuinputd to nixpkgs (if not already present or incomplete)
  • Provide a NixOS module
  • udev integration
  • Support for both:
  • Documentation:
    • nixos-specific usage guide
    • minimal working examples
    • advanced setups (multi-device, virtualization, containers)
    • test automation for new releases

Existing Work

Some community configurations already exist:

These can serve as inspiration but are not yet standardized.


Call for Feedback

Looking for input on:

  • NixOS module design patterns for device daemons
  • Existing similar services to take inspiration from
  • Real-world use cases to validate the design
  • ...

PRs, ideas, and experiments are very welcome 🙂

Originally created by @joleuger on GitHub (May 6, 2026). Original GitHub issue: https://github.com/joleuger/vuinputd/issues/14 ## First-class NixOS support for `vuinputd` ### Motivation `vuinputd` is evolving into a flexible userspace input virtualization daemon, and NixOS is a particularly strong target platform due to: * declarative system configuration * strong integration with systemd * reproducible builds and environments * common usage in homelab / desktop virtualization setups Currently, NixOS support is **work-in-progress** and requires manual setup. The goal is to make `vuinputd` a **first-class citizen on NixOS** with a clean, idiomatic module. Reference draft documentation: https://github.com/joleuger/vuinputd/blob/main/docs/USAGE-NIXOS.md --- ### Goals * [ ] Add `vuinputd` to nixpkgs (if not already present or incomplete) * [ ] Provide a NixOS module * [ ] udev integration * [ ] Support for both: * [ ] Documentation: * nixos-specific usage guide * minimal working examples * advanced setups (multi-device, virtualization, containers) * test automation for new releases --- ### Existing Work Some community configurations already exist: * https://github.com/ShaneTRS/nixos-config/ * https://github.com/girl-pp-ua/nixos-infra/ These can serve as inspiration but are not yet standardized. --- ### Call for Feedback Looking for input on: * NixOS module design patterns for device daemons * Existing similar services to take inspiration from * Real-world use cases to validate the design * ... PRs, ideas, and experiments are very welcome 🙂
Author
Owner

@joleuger commented on GitHub (May 6, 2026):

Hi @griffi-gh , @ShaneTRS , @Markus328 ,

from past issues I know that you’re using NixOS and already have working setups for vuinputd.

I’ve started putting together a NixOS usage guide and referenced your repositories here:
https://github.com/joleuger/vuinputd/blob/main/docs/USAGE-NIXOS.md

I hope that’s okay with you—please let me know if you’d prefer not to be listed.

More generally, I’d really be interested in your experience:

  • what parts of your setup felt awkward or non-idiomatic on NixOS?
  • what would “first-class” support look like for you?
  • anything you had to work around that a proper module should solve?
  • any sharp edges that the documentation should mention

Even brief feedback or pointers are very helpful.

Thanks!

<!-- gh-comment-id:4392291596 --> @joleuger commented on GitHub (May 6, 2026): Hi @griffi-gh , @ShaneTRS , @Markus328 , from past issues I know that you’re using NixOS and already have working setups for `vuinputd`. I’ve started putting together a NixOS usage guide and referenced your repositories here: https://github.com/joleuger/vuinputd/blob/main/docs/USAGE-NIXOS.md I hope that’s okay with you—please let me know if you’d prefer not to be listed. More generally, I’d really be interested in your experience: * what parts of your setup felt awkward or non-idiomatic on NixOS? * what would “first-class” support look like for you? * anything you had to work around that a proper module should solve? * any sharp edges that the documentation should mention Even brief feedback or pointers are very helpful. Thanks!
Author
Owner

@griffi-gh commented on GitHub (May 6, 2026):

I'd say isolation was the most awkward part, i couldn't get it running under anything but root + no systemd isolation

would've been nice to figure out a way to run it with more isolation/with less potential blast radius

<!-- gh-comment-id:4392837365 --> @griffi-gh commented on GitHub (May 6, 2026): I'd say isolation was the most awkward part, i couldn't get it running under anything but root + no systemd isolation would've been nice to figure out a way to run it with more isolation/with less potential blast radius
Author
Owner

@Markus328 commented on GitHub (May 7, 2026):

The most awkward for me was some strange behavior with permission on /dev/vuinput. I've created two systemd services for vuinput to work correctly here

<!-- gh-comment-id:4393898501 --> @Markus328 commented on GitHub (May 7, 2026): The most awkward for me was some strange behavior with permission on /dev/vuinput. I've created two systemd services for vuinput to work correctly here
Author
Owner

@Markus328 commented on GitHub (May 7, 2026):

  systemd.services.vuinputd = {
    enable = true;
    wantedBy = ["multi-user.target"];
    unitConfig = {
      Description = "Virtual input (/dev/vuinput) daemon";
    };
    serviceConfig = {
      Type = "exec";
      ExecStartPre = pkgs.writeShellScript "mount-tmpfs-dev-input" ''
        mkdir -p /run/vuinputd/vuinput/dev-input
        ${lib.getExe pkgs.mount} -t tmpfs -o rw,dev,nosuid tpmfs /run/vuinputd/vuinput/dev-input
      '';
      ExecStart = "${lib.getExe pkgs.vuinputd} --major 120 --minor 414795 --placement on-host";
      ExecStopPost = pkgs.writeShellScript "umount-dev-input" ''
        ${lib.getExe pkgs.umount} /run/vuinputd/vuinput/dev-input
      '';
      Restart = "on-failure";
      DeviceAllow = "char-* rwm";

      Environment = [
        "RUST_LOG=debug"
      ];
    };
  };
  systemd.services.vuinputd-chmod = {
    unitConfig.Description = "Chmod 666 the /dev/vuinput";
    wantedBy = ["vuinputd.service"];
    after = ["vuinputd.service"];

    serviceConfig = {
      ExecStart = pkgs.writeShellScript "chmod-vuinput" ''
        sleep 2 && chmod 666 /dev/vuinput
      '';
    };
  };

Sometimes the /dev/vuinput was not being truly functional inside the container due to permission issues, so using this approach helped me. About that ExecStartPre it probably is not needed anymore because its just a workaround to the virtual devices ownership inside the container.

I think creating a simple NixOS module for this is enough, just some options to automatically setup the placement on host if the user wants and maybe fixing this chmod as well. Another extraFlags to add custom flags as the major-minor

An example should be:

services.vuinputd = {
    enable = true;
    package = pkgs.vuinputd;
    placement = "host"; # may be others
    extraFlags = ["--major" "120"];
}
<!-- gh-comment-id:4393922470 --> @Markus328 commented on GitHub (May 7, 2026): ```nix systemd.services.vuinputd = { enable = true; wantedBy = ["multi-user.target"]; unitConfig = { Description = "Virtual input (/dev/vuinput) daemon"; }; serviceConfig = { Type = "exec"; ExecStartPre = pkgs.writeShellScript "mount-tmpfs-dev-input" '' mkdir -p /run/vuinputd/vuinput/dev-input ${lib.getExe pkgs.mount} -t tmpfs -o rw,dev,nosuid tpmfs /run/vuinputd/vuinput/dev-input ''; ExecStart = "${lib.getExe pkgs.vuinputd} --major 120 --minor 414795 --placement on-host"; ExecStopPost = pkgs.writeShellScript "umount-dev-input" '' ${lib.getExe pkgs.umount} /run/vuinputd/vuinput/dev-input ''; Restart = "on-failure"; DeviceAllow = "char-* rwm"; Environment = [ "RUST_LOG=debug" ]; }; }; systemd.services.vuinputd-chmod = { unitConfig.Description = "Chmod 666 the /dev/vuinput"; wantedBy = ["vuinputd.service"]; after = ["vuinputd.service"]; serviceConfig = { ExecStart = pkgs.writeShellScript "chmod-vuinput" '' sleep 2 && chmod 666 /dev/vuinput ''; }; }; ``` Sometimes the /dev/vuinput was not being truly functional inside the container due to permission issues, so using this approach helped me. About that ExecStartPre it probably is not needed anymore because its just a workaround to the virtual devices ownership inside the container. I think creating a simple NixOS module for this is enough, just some options to automatically setup the placement on host if the user wants and maybe fixing this chmod as well. Another `extraFlags` to add custom flags as the major-minor An example should be: ```nix services.vuinputd = { enable = true; package = pkgs.vuinputd; placement = "host"; # may be others extraFlags = ["--major" "120"]; } ```
Author
Owner

@Markus328 commented on GitHub (May 7, 2026):

{
  rustPlatform,
  fetchFromGitHub,
  pkg-config,
  udev,
  fuse3,
  isDebug ? false,
  ...
}:
rustPlatform.buildRustPackage (finalAttrs: {
  pname = "vuinputd";
  version = "0.3.2-git";

  buildType =
    if isDebug
    then "debug"
    else "release";

  nativeBuildInputs = [
    pkg-config
    rustPlatform.bindgenHook
    # breakpointHook
  ];
  buildInputs = [udev fuse3];

  src = fetchFromGitHub {
    owner = "joleuger";
    repo = "vuinputd";
    rev = "8c40fdc12005319ea16dceb752a8822abfc6039a";
    hash = "sha256-8Q34B04BngZqRLyixeFq8F1t5wFnk6JpaG3EEbgKRcU=";
  };

  cargoHash = "sha256-nJw9bRh6Yn9g1H5SeoT6zxgZLCqV3AtAs9gMfE+P+CU=";

  # Recent versions of fuse3 can also have libfuse_* types
  postPatch = ''
    substituteInPlace cuse-lowlevel/build.rs \
      --replace-fail '.allowlist_type("(?i)^fuse.*")' '.allowlist_type("(?i)^(fuse|libfuse).*")'
  '';

  postInstall = ''
    mkdir -p $out/lib/udev/rules.d
    mkdir $out/lib/udev/hwdb.d
    cp vuinputd/udev/*.rules $out/lib/udev/rules.d/
    cp vuinputd/udev/*.hwdb $out/lib/udev/hwdb.d/
  '';
})

Also, my outdated package of vuinputd. Think just changing the hash and removing that patch from allow_list (guess its already fixed?) would easily pack the latest version

<!-- gh-comment-id:4393958083 --> @Markus328 commented on GitHub (May 7, 2026): ```nix { rustPlatform, fetchFromGitHub, pkg-config, udev, fuse3, isDebug ? false, ... }: rustPlatform.buildRustPackage (finalAttrs: { pname = "vuinputd"; version = "0.3.2-git"; buildType = if isDebug then "debug" else "release"; nativeBuildInputs = [ pkg-config rustPlatform.bindgenHook # breakpointHook ]; buildInputs = [udev fuse3]; src = fetchFromGitHub { owner = "joleuger"; repo = "vuinputd"; rev = "8c40fdc12005319ea16dceb752a8822abfc6039a"; hash = "sha256-8Q34B04BngZqRLyixeFq8F1t5wFnk6JpaG3EEbgKRcU="; }; cargoHash = "sha256-nJw9bRh6Yn9g1H5SeoT6zxgZLCqV3AtAs9gMfE+P+CU="; # Recent versions of fuse3 can also have libfuse_* types postPatch = '' substituteInPlace cuse-lowlevel/build.rs \ --replace-fail '.allowlist_type("(?i)^fuse.*")' '.allowlist_type("(?i)^(fuse|libfuse).*")' ''; postInstall = '' mkdir -p $out/lib/udev/rules.d mkdir $out/lib/udev/hwdb.d cp vuinputd/udev/*.rules $out/lib/udev/rules.d/ cp vuinputd/udev/*.hwdb $out/lib/udev/hwdb.d/ ''; }) ``` Also, my outdated package of vuinputd. Think just changing the hash and removing that patch from allow_list (guess its already fixed?) would easily pack the latest version
Author
Owner

@joleuger commented on GitHub (May 7, 2026):

Cool, thanks for your input.

As first measure, I'll patch that fuse3 problem. I think it is a blocker for every nixos user at first. You were only patient enough to fix it.

The only reason I currently need root permissions on the host is that it is pretty easy to get root inside the affected container, too. That is needed to send a netlink message to make libinput realize that a mouse or keyboard was plugged in (this works via libudev). Other ideas to reach the same goal is to

  • patch libudev inside the container to make it recognize new devices via another mechanism
  • somehow get root inside the container via another mechanism (need to investigate)
  • put a binary inside the container that emits the netlink message and make vuinputd call it via nspawn / dbus

I think I will also make it default for the nixos configuration to make one vuinputd instance per container. The memory consumption is not too high (IIRC about 1,6 MB) and I think I can further reduce it.

<!-- gh-comment-id:4399914616 --> @joleuger commented on GitHub (May 7, 2026): Cool, thanks for your input. As first measure, I'll patch that fuse3 problem. I think it is a blocker for every nixos user at first. You were only patient enough to fix it. The only reason I currently need root permissions on the host is that it is pretty easy to get root inside the affected container, too. That is needed to send a netlink message to make libinput realize that a mouse or keyboard was plugged in (this works via libudev). Other ideas to reach the same goal is to - patch libudev inside the container to make it recognize new devices via another mechanism - somehow get root inside the container via another mechanism (need to investigate) - put a binary inside the container that emits the netlink message and make vuinputd call it via nspawn / dbus I think I will also make it default for the nixos configuration to make one vuinputd instance per container. The memory consumption is not too high (IIRC about 1,6 MB) and I think I can further reduce it.
Author
Owner

@Markus328 commented on GitHub (May 7, 2026):

Talking about the netlink message, I also found "a bug" when using rootless nspawn containers: Aparently the seatd refuses to accept the message because of the wrong owership, the devices can work well, but the hotplug behavior is not functional, the session doesn't know when the device is plugged in. If the session starts before the sunshine, then when sunshine create the devices the session doesnt catch it, so need restart. Anyway, I think this can be fixed both outside and inside, if you use the 3rd approach to call a binary as root (the container's root) inside the container

<!-- gh-comment-id:4400840630 --> @Markus328 commented on GitHub (May 7, 2026): Talking about the netlink message, I also found "a bug" when using rootless nspawn containers: Aparently the seatd refuses to accept the message because of the wrong owership, the devices can work well, but the hotplug behavior is not functional, the session doesn't know when the device is plugged in. If the session starts before the sunshine, then when sunshine create the devices the session doesnt catch it, so need restart. Anyway, I think this can be fixed both outside and inside, if you use the 3rd approach to call a binary as root (the container's root) inside the container
Author
Owner

@Markus328 commented on GitHub (May 7, 2026):

I will grab more details about my experience and ideas of how to do the per-container vuinputd instance, once I get home

<!-- gh-comment-id:4400848921 --> @Markus328 commented on GitHub (May 7, 2026): I will grab more details about my experience and ideas of how to do the per-container vuinputd instance, once I get home
Author
Owner

@Markus328 commented on GitHub (May 8, 2026):

Well, thinking more about it I can suggest you to create a per-instance vuinputd configuration at services.vuinputd.instances. Each instance will have its placement mode, device policy and dev name, as well as extraFlags and so on. It will also expose an config.services.vuinputd.instances.<name>.path of the virtual uinput instance. Doing this way, the vuinputd module would be agnostic of the container engine or solution, making it simple.

About the implementation itself, several systemd services would run for each instance, execing it + oneshotting the chmod stuff if needed. About some isolation, its need to search a way of doing this in a more unprivileged way.

The toplevel of module services.vuinputd would basically source the udev rules even with no instance configured

<!-- gh-comment-id:4403048121 --> @Markus328 commented on GitHub (May 8, 2026): Well, thinking more about it I can suggest you to create a per-instance vuinputd configuration at `services.vuinputd.instances`. Each instance will have its placement mode, device policy and dev name, as well as `extraFlags` and so on. It will also expose an `config.services.vuinputd.instances.<name>.path` of the virtual uinput instance. Doing this way, the vuinputd module would be agnostic of the container engine or solution, making it simple. About the implementation itself, several systemd services would run for each instance, execing it + oneshotting the chmod stuff if needed. About some isolation, its need to search a way of doing this in a more unprivileged way. The toplevel of module `services.vuinputd` would basically source the udev rules even with no instance configured
Author
Owner

@joleuger commented on GitHub (May 8, 2026):

Talking about the netlink message, I also found "a bug" when using rootless nspawn containers: Aparently the seatd refuses to accept the message because of the wrong owership, the devices can work well, but the hotplug behavior is not functional, the session doesn't know when the device is plugged in. If the session starts before the sunshine, then when sunshine create the devices the session doesnt catch it, so need restart. Anyway, I think this can be fixed both outside and inside, if you use the 3rd approach to call a binary as root (the container's root) inside the container

vuinpud makes no seat assignment and just removes the seats from the netlink message altogether. For libseat, I just used the noop plugin that just assigns everything internally to seat0 and it works fine in my case. Maybe you can give it a try:

Environment=WLR_BACKENDS=headless,libinput
Environment=WLR_LIBINPUT_NO_DEVICES=1
Environment=LIBSEAT_BACKEND=noop

Anyway, I created a issue https://github.com/joleuger/vuinputd/issues/15

<!-- gh-comment-id:4404176977 --> @joleuger commented on GitHub (May 8, 2026): > Talking about the netlink message, I also found "a bug" when using rootless nspawn containers: Aparently the seatd refuses to accept the message because of the wrong owership, the devices can work well, but the hotplug behavior is not functional, the session doesn't know when the device is plugged in. If the session starts before the sunshine, then when sunshine create the devices the session doesnt catch it, so need restart. Anyway, I think this can be fixed both outside and inside, if you use the 3rd approach to call a binary as root (the container's root) inside the container vuinpud makes no seat assignment and just removes the seats from the netlink message altogether. For libseat, I just used the noop plugin that just assigns everything internally to seat0 and it works fine in my case. Maybe you can give it a try: ``` Environment=WLR_BACKENDS=headless,libinput Environment=WLR_LIBINPUT_NO_DEVICES=1 Environment=LIBSEAT_BACKEND=noop ``` Anyway, I created a issue https://github.com/joleuger/vuinputd/issues/15
Author
Owner

@joleuger commented on GitHub (May 9, 2026):

Status update: As a first step to improve the experience on nix os, I have used the configuration of Markus328 to create an integration test and write some documentation for NixOS that should reduce the friction for first time users

Edit: Next step is to write a flake in the repo and update the documentation

<!-- gh-comment-id:4411992288 --> @joleuger commented on GitHub (May 9, 2026): Status update: As a first step to improve the experience on nix os, I have used the configuration of Markus328 to create an integration test and write some documentation for NixOS that should reduce the friction for first time users - https://github.com/joleuger/vuinputd/blob/main/docs/USAGE-NIXOS.md - https://github.com/joleuger/vuinputd/blob/main/distro-tests/nixos/README.md Edit: Next step is to write a flake in the repo and update the documentation
Sign in to join this conversation.
No labels
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
joleuger/vuinputd#12
No description provided.