diff --git a/nixos/doc/manual/release-notes/rl-2305.section.md b/nixos/doc/manual/release-notes/rl-2305.section.md index b5e157cdb76ea..704ee0535e2c1 100644 --- a/nixos/doc/manual/release-notes/rl-2305.section.md +++ b/nixos/doc/manual/release-notes/rl-2305.section.md @@ -57,6 +57,8 @@ In addition to numerous new and upgraded packages, this release has the followin - [QDMR](https://dm3mat.darc.de/qdmr/), a GUI application and command line tool for programming DMR radios [programs.qdmr](#opt-programs.qdmr.enable) +- [keyd](https://github.com/rvaiya/keyd), a key remapping daemon for linux. Available as [services.keyd](#opt-services.keyd.enable). + - [v2rayA](https://v2raya.org), a Linux web GUI client of Project V which supports V2Ray, Xray, SS, SSR, Trojan and Pingtunnel. Available as [services.v2raya](options.html#opt-services.v2raya.enable). - [ulogd](https://www.netfilter.org/projects/ulogd/index.html), a userspace logging daemon for netfilter/iptables related logging. Available as [services.ulogd](options.html#opt-services.ulogd.enable). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 314d67419b7ff..7095241f67715 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -511,6 +511,7 @@ ./services/hardware/usbmuxd.nix ./services/hardware/usbrelayd.nix ./services/hardware/vdr.nix + ./services/hardware/keyd.nix ./services/home-automation/evcc.nix ./services/home-automation/home-assistant.nix ./services/home-automation/zigbee2mqtt.nix diff --git a/nixos/modules/services/hardware/keyd.nix b/nixos/modules/services/hardware/keyd.nix new file mode 100644 index 0000000000000..64c769405fabc --- /dev/null +++ b/nixos/modules/services/hardware/keyd.nix @@ -0,0 +1,112 @@ +{ config, lib, pkgs, ... }: +with lib; +let + cfg = config.services.keyd; + settingsFormat = pkgs.formats.ini { }; +in +{ + options = { + services.keyd = { + enable = mkEnableOption (lib.mdDoc "keyd, a key remapping daemon"); + + ids = mkOption { + type = types.listOf types.string; + default = [ "*" ]; + example = [ "*" "-0123:0456" ]; + description = lib.mdDoc '' + Device identifiers, as shown by {manpage}`keyd(1)`. + ''; + }; + + settings = mkOption { + type = settingsFormat.type; + default = { }; + example = { + main = { + capslock = "overload(control, esc)"; + rightalt = "layer(rightalt)"; + }; + + rightalt = { + j = "down"; + k = "up"; + h = "left"; + l = "right"; + }; + }; + description = lib.mdDoc '' + Configuration, except `ids` section, that is written to {file}`/etc/keyd/default.conf`. + See how to configure. + ''; + }; + }; + }; + + config = mkIf cfg.enable { + environment.etc."keyd/default.conf".source = pkgs.runCommand "default.conf" + { + ids = '' + [ids] + ${concatStringsSep "\n" cfg.ids} + ''; + passAsFile = [ "ids" ]; + } '' + cat $idsPath <(echo) ${settingsFormat.generate "keyd-main.conf" cfg.settings} >$out + ''; + + hardware.uinput.enable = lib.mkDefault true; + + systemd.services.keyd = { + description = "Keyd remapping daemon"; + documentation = [ "man:keyd(1)" ]; + + wantedBy = [ "multi-user.target" ]; + + restartTriggers = [ + config.environment.etc."keyd/default.conf".source + ]; + + # this is configurable in 2.4.2, later versions seem to remove this option. + # post-2.4.2 may need to set makeFlags in the derivation: + # + # makeFlags = [ "SOCKET_PATH/run/keyd/keyd.socket" ]; + environment.KEYD_SOCKET = "/run/keyd/keyd.sock"; + + serviceConfig = { + ExecStart = "${pkgs.keyd}/bin/keyd"; + Restart = "always"; + + DynamicUser = true; + SupplementaryGroups = [ + config.users.groups.input.name + config.users.groups.uinput.name + ]; + + RuntimeDirectory = "keyd"; + + # Hardening + CapabilityBoundingSet = ""; + DeviceAllow = [ + "char-input rw" + "/dev/uinput rw" + ]; + ProtectClock = true; + PrivateNetwork = true; + ProtectHome = true; + ProtectHostname = true; + PrivateUsers = true; + PrivateMounts = true; + RestrictNamespaces = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectControlGroups = true; + MemoryDenyWriteExecute = true; + RestrictRealtime = true; + LockPersonality = true; + ProtectProc = "noaccess"; + UMask = "0077"; + }; + }; + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index ee6b654244312..6f91a2f354f82 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -345,6 +345,7 @@ in { keter = handleTest ./keter.nix {}; kexec = handleTest ./kexec.nix {}; keycloak = discoverTests (import ./keycloak.nix); + keyd = handleTest ./keyd.nix {}; keymap = handleTest ./keymap.nix {}; knot = handleTest ./knot.nix {}; komga = handleTest ./komga.nix {}; diff --git a/nixos/tests/keyd.nix b/nixos/tests/keyd.nix new file mode 100644 index 0000000000000..d492cc194895c --- /dev/null +++ b/nixos/tests/keyd.nix @@ -0,0 +1,82 @@ +# The test template is taken from the `./keymap.nix` +{ system ? builtins.currentSystem +, config ? { } +, pkgs ? import ../.. { inherit system config; } +}: + +with import ../lib/testing-python.nix { inherit system pkgs; }; + +let + readyFile = "/tmp/readerReady"; + resultFile = "/tmp/readerResult"; + + testReader = pkgs.writeScript "test-input-reader" '' + rm -f ${resultFile} ${resultFile}.tmp + logger "testReader: START: Waiting for $1 characters, expecting '$2'." + touch ${readyFile} + read -r -N $1 chars + rm -f ${readyFile} + if [ "$chars" == "$2" ]; then + logger -s "testReader: PASS: Got '$2' as expected." 2>${resultFile}.tmp + else + logger -s "testReader: FAIL: Expected '$2' but got '$chars'." 2>${resultFile}.tmp + fi + # rename after the file is written to prevent a race condition + mv ${resultFile}.tmp ${resultFile} + ''; + + + mkKeyboardTest = name: { settings, test }: with pkgs.lib; makeTest { + inherit name; + + nodes.machine = { + services.keyd = { + enable = true; + inherit settings; + }; + }; + + testScript = '' + import shlex + + machine.wait_for_unit("keyd.service") + + def run_test_case(cmd, test_case_name, inputs, expected): + with subtest(test_case_name): + assert len(inputs) == len(expected) + machine.execute("rm -f ${readyFile} ${resultFile}") + # set up process that expects all the keys to be entered + machine.succeed( + "{} {} {} {} >&2 &".format( + cmd, + "${testReader}", + len(inputs), + shlex.quote("".join(expected)), + ) + ) + # wait for reader to be ready + machine.wait_for_file("${readyFile}") + # send all keys + for key in inputs: + machine.send_key(key) + # wait for result and check + machine.wait_for_file("${resultFile}") + machine.succeed("grep -q 'PASS:' ${resultFile}") + test = ${builtins.toJSON test} + run_test_case("openvt -sw --", "${name}", test["press"], test["expect"]) + ''; + }; + +in +pkgs.lib.mapAttrs mkKeyboardTest { + swap-ab_and_ctrl-as-shift = { + test.press = [ "a" "ctrl-b" "c" ]; + test.expect = [ "b" "A" "c" ]; + + settings.main = { + "a" = "b"; + "b" = "a"; + "control" = "oneshot(shift)"; + }; + }; +} diff --git a/pkgs/tools/inputmethods/keyd/default.nix b/pkgs/tools/inputmethods/keyd/default.nix index 4da42e52eaa6b..ba0c051644358 100644 --- a/pkgs/tools/inputmethods/keyd/default.nix +++ b/pkgs/tools/inputmethods/keyd/default.nix @@ -6,6 +6,7 @@ , systemd , runtimeShell , python3 +, nixosTests }: let @@ -59,11 +60,16 @@ stdenv.mkDerivation rec { enableParallelBuilding = true; + # post-2.4.2 may need this to unbreak the test + # makeFlags = [ "SOCKET_PATH/run/keyd/keyd.socket" ]; + postInstall = '' ln -sf ${lib.getExe appMap} $out/bin/${appMap.pname} rm -rf $out/etc ''; + passthru.tests.keyd = nixosTests.keyd; + meta = with lib; { description = "A key remapping daemon for linux."; license = licenses.mit;