How to setup xcape in configuration.nix without home manager?

My setup is currently this: NixOS.

I use following for xcape, however it only works when manually running systemctl --user start xcape instead of starting automatically:

  # xcape service
  systemd.user.services.xcape = {
    restartIfChanged = true;
    description = "Combine Ctrl+Escape";
    wantedBy = ["graphical-session.target"];
    partOf = ["graphical-session.target"];
    serviceConfig = {
      Type = "forking";
      Restart = "always";
      ExecStart = ''${pkgs.xcape}/bin/xcape -e "Control_L=Escape"'';
    };
  };

What’s the output of $ systemctl --user status graphical-session.target?

If it’s inactive/hasn’t been reached, you need to configure your window manager/desktop environment to start and stop the target at the appropriate time. Here’s how I do it in my .xinitrc:

systemctl --user -q start graphical-session.target
herbstluftwm --locked
systemctl --user -q stop graphical-session.target
~ ❯ systemctl --user status graphical-session.target
● graphical-session.target - Current graphical user session
     Loaded: loaded (/etc/systemd/user/graphical-session.target; static)
    Drop-In: /nix/store/h9f84vyplfs1hdhdwphz7jwq6b8rnvy6-user-units/graphical-session.target.d
             └─overrides.conf
     Active: active since Fri 2022-07-22 22:39:14 CEST; 54min ago
      Until: Fri 2022-07-22 22:39:14 CEST; 54min ago
       Docs: man:systemd.special(7)

Jul 22 22:39:14 odd systemd[1176]: Reached target Current graphical user session.

Here’s xcape as well:

~ ❯ systemctl --user status xcape
● xcape.service - Combine Ctrl+Escape
     Loaded: loaded (/etc/systemd/user/xcape.service; enabled; vendor preset: enabled)
     Active: active (running) since Fri 2022-07-22 22:39:14 CEST; 54min ago
   Main PID: 1208 (xcape)
      Tasks: 2 (limit: 19089)
     Memory: 564.0K
        CPU: 259ms
     CGroup: /user.slice/user-1000.slice/user@1000.service/app.slice/xcape.service
             └─1208 /nix/store/2dxqy8lh3r03dxgqkg0zkr2ffja3wig9-xcape-unstable-2018-03-01/bin/xcape -e Control_L Escape

Jul 22 22:39:14 odd systemd[1176]: Starting Combine Ctrl+Escape...
Jul 22 22:39:14 odd systemd[1176]: Started Combine Ctrl+Escape.

Is the status output for xcape after you’ve manually started it? Because that certainly looks like it’s working.

It’s from when the system is booting. However the change doesn’t persist, for instance after sudo nixos-rebuild switch. I don’t think it works after booting either, will try.

Hmm, for some reason xcape starts before graphical-session.target

~ ❯ systemctl --user status graphical-session.target
● graphical-session.target - Current graphical user session
     Loaded: loaded (/etc/systemd/user/graphical-session.target; static)
    Drop-In: /nix/store/h9f84vyplfs1hdhdwphz7jwq6b8rnvy6-user-units/graphical-session.target.d
             └─overrides.conf
     Active: active since Fri 2022-07-22 23:53:37 CEST; 14s ago
      Until: Fri 2022-07-22 23:53:37 CEST; 14s ago
       Docs: man:systemd.special(7)

Jul 22 23:53:37 odd systemd[1171]: Reached target Current graphical user session.
~ ❯ systemctl --user status xcape
● xcape.service - Combine Ctrl+Escape
     Loaded: loaded (/etc/systemd/user/xcape.service; enabled; vendor preset: enabled)
     Active: active (running) since Fri 2022-07-22 23:53:37 CEST; 18s ago
    Process: 1202 ExecStart=/nix/store/2dxqy8lh3r03dxgqkg0zkr2ffja3wig9-xcape-unstable-2018-03-01/bin/xcape -e Control_L=Escape (code=exited,>
   Main PID: 1203 (xcape)
      Tasks: 2 (limit: 19089)
     Memory: 564.0K
        CPU: 6ms
     CGroup: /user.slice/user-1000.slice/user@1000.service/app.slice/xcape.service
             └─1203 /nix/store/2dxqy8lh3r03dxgqkg0zkr2ffja3wig9-xcape-unstable-2018-03-01/bin/xcape -e Control_L Escape

Jul 22 23:53:37 odd systemd[1171]: Starting Combine Ctrl+Escape...
Jul 22 23:53:37 odd systemd[1171]: Started Combine Ctrl+Escape.

Process seems to start but it has no effect. Restarting makes it work again. Then after some time it stops working. Don’t know why.

Yeah, when I was using xcape I found it was fairly finicky in general, the workaround I used at the time was having my window manager restart it when reloading it’s configuration.

If you don’t mind side-stepping the issue, I would recommend switching to interception-tools and the dual-function-keys plugin. It wraps evdev, so will work anywhere, be it X11, wayland, or the linux TTY.

Do you have an example of using either of those in NixOS? As long as I have control + escape it doesn’t matter :smiley:

I was just writing that up actually:

  services.interception-tools =
    let
      dfkConfig = pkgs.writeText "dual-function-keys.yaml" ''
        MAPPINGS:
          - KEY: KEY_CAPSLOCK
            TAP: KEY_ESC
            HOLD: KEY_LEFTCTRL
      '';
    in
    {
      enable = true;
      plugins = lib.mkForce [
        pkgs.interception-tools-plugins.dual-function-keys
      ];
      udevmonConfig = ''
        - JOB: "${pkgs.interception-tools}/bin/intercept -g $DEVNODE | ${pkgs.interception-tools-plugins.dual-function-keys}/bin/dual-function-keys -c ${dfkConfig} | ${pkgs.interception-tools}/bin/uinput -d $DEVNODE"
          DEVICE:
            NAME: "USB Keyboard"
            EVENTS:
              EV_KEY: [[KEY_CAPSLOCK, KEY_ESC, KEY_LEFTCTRL]]
      '';
    };

This puts left control and escape on capslock.

To get the device name you can use sudo uinput -p -d /dev/input/by-id/foo. Or just drop it, as it’s technically not required, it just limits interception tools from wrapping any compatible device (which can include things you wouldn’t really expect).

1 Like

Added this configuration here: configuration.nix, and control seems to work but not escape.

Hmm wait, services.xserver.xkbOptions might be interferring.

Now my caps lock is acting as backspace. Don’t know why this is the case.

EDIT:
Finally got it working here, thanks for the help!

I used xinput to find the proper NAME:

1 Like

That’s the default colemak behaviour.

Is $ systemctl status interception-tools showing you something like:

CGroup: /system.slice/interception-tools.service
        ├─3021901 /nix/store/ch2272gvqhzwrjxzcf6ldg39b61nrlfw-interception-tools-0.6.8/bin/udevmon -c /nix/store/v16yzikjkz6w3a8qgjfirgks39x63hdl-udevmon.yaml
        ├─3021967 sh -c "/nix/store/ch2272gvqhzwrjxzcf6ldg39b61nrlfw-interception-tools-0.6.8/bin/intercept -g \$DEVNODE | /nix/store/5vyziafj0x0jk8y89mlkaxnwi3wlf6c0-dual-function
        ├─3021970 /nix/store/ch2272gvqhzwrjxzcf6ldg39b61nrlfw-interception-tools-0.6.8/bin/intercept -g /dev/input/event1
        ├─3021971 /nix/store/5vyziafj0x0jk8y89mlkaxnwi3wlf6c0-dual-function-keys-1.4.0/bin/dual-function-keys -c /nix/store/bnlgww1gn5gqfk448f5964dx6m8bfv4q-dual-function-keys.yaml
        └─3021972 /nix/store/ch2272gvqhzwrjxzcf6ldg39b61nrlfw-interception-tools-0.6.8/bin/uinput -d /dev/input/event1
     CGroup: /system.slice/interception-tools.service
             └─5889 /nix/store/ch2272gvqhzwrjxzcf6ldg39b61nrlfw-interception-tools-0.6.8/bin/udevmon -c /nix/store/fx4g2aqvn9fb4bl7khvjhrfpfa>

Then it hasen’t found a device with a matching device name and EV_KEYs.

The system control in the device name is a bit suspicious, try dropping the name and see what happens.