DEV Community

Kir Axanov
Kir Axanov

Posted on

Configs. Fix any vertical mouse

Hi!

It's really dumb, but every vertical mouse I saw has one crucial mistake in its design - the sensor is misaligned and when you try to hold it the proper (ergonomic!) way and move horizontally, it also moves up / down...

But we can fix it with xinput!

Steps

  1. Identify your fancy mouse. Run the following command with the mouse unplugged and then plugged again to see which device you need (get name after and before id=):

    xinput list
    

    Mine is called USB OPTICAL MOUSE.

  2. Prepare the Python3 environment for script in next step. You'll need the scipy package.

    For NixOS just replace the shebang in this script with this one:

    #!/usr/bin/env nix-shell
    #!nix-shell -i python3 -p python3 python313Packages.scipy
    
  3. Use python script below to experiment with rotation angle:

    #!/usr/bin/env python3
    # Set rotation matrix for mouse.
    # Original: https://luator.de/linux/2022/03/05/xinput-rotate-mouse-axes.html
    #
    # Usage example:
    # python3 rotate-mouse.py DEVICE_NAME ANGLE
    #
    # See all devices with:
    # xinput list
    #
    # The script also prints the command for selected angle, so you can add it to autostart.
    #
    # Positive values result in clock-wise rotation.
    # The rotation is always relative to the original orientation,
    # so you can easily reset by setting the angle to 0.
    
    """Use xinput to rotate the mouse axes by a given angle."""
    import argparse
    import os
    
    from scipy.spatial.transform import Rotation
    
    def main():
        parser = argparse.ArgumentParser(description=__doc__)
        parser.add_argument("device", type=str, help="Mouse name from `xinput list`.")
        parser.add_argument("angle", type=int, help="Rotation angle in degrees.")
        args = parser.parse_args()
    
        # Construct rotation matrix from given angle (xinput expects an affine 2d
        # transformation but since we are only rotating, this is equivalent to a
        # simple 3d rotation matrix around the z-axis).
        rot = Rotation.from_euler("z", args.angle, degrees=True).as_matrix()
        matrix = [str(x) for x in rot.flatten()]
    
        xinput_cmd = [
            "xinput",
            "set-prop",
            args.device,
            "Coordinate Transformation Matrix",
        ] + matrix
    
        xinput_str = f"xinput set-prop '{args.device}' 'Coordinate Transformation Matrix' {' '.join(matrix)}    # Rotates mouse by {args.angle}°"
        print(xinput_str)
    
        os.execvp("xinput", xinput_cmd)
    
    if __name__ == "__main__":
        main()
    

    Original script was taken from Luator's Blog.

  4. The script above also prints applied xinput command, which you can add to your autostart scripts to make it persistent after reboot.

  5. After finding the matrix, you can ditch created above python environment.

Bonus: InputLeap struggles

I use InputLeap to share my keyboard and mouse between my machines time to time, which apparently does not go well with such xinput changes (leave a comment if I miss something). On clients the mouse comes stuck at the top right corner.

So we need to reset our transformation matrix when we go to InputLeap client (and struggle with not-so-ergonomic-mouse-with-misaligned-sensor, alas!).

As it's mentioned in the script, you can reset the matrix by running it with zero angle, or straight with xinput:

xinput set-prop 'YOUR_MOUSE_NAME' 'Coordinate Transformation Matrix' 1.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 1.0
Enter fullscreen mode Exit fullscreen mode

So we just need to run this reset command when we go to client and run the rotation command when we are back on server.

InputLeap has the --screen-change-script argument, which you can use... but somehow applying any arguments to input-leap on NixOS does nothing, it only respects the settings set up in its GUI.

Thus, here is a uniform way to catch when InputLeap changes screens via monitoring its log file:

  1. Open InputLeap GUI, go to InputLeap top menu -> Change Settings -> Logging

    • set Logging level to info
    • check Log to file and choose the log file you want to use (i.e. /tmp/inputleap.log)
    • hit OK and restart it
  2. Save and edit script below:

    • change LOG_FILE variable to the log file path which you set in InputLeap GUI
    • change YOUR_MOUSE_NAME with, you guessed it, your mouse name from xinput list
    • change ROTATE_CMD and RESET_CMD with appropriate xinput commands found above
    • change SERVER_SCREEN to the name of InputLeap server's screen (it's in General -> Screen name of InputLeap GUI settings from previous step)
    • also, you can uncomment the line with notify-send to see the screen name when you move between them
    #!/usr/bin/env bash
    # Rotates mouse based on current InputLeap screen.
    
    LOG_FILE="/tmp/inputleap.log"
    
    rm -f "$LOG_FILE"
    touch "$LOG_FILE"
    
    tail -f "$LOG_FILE" | grep "INFO: switch from" --line-buffered | while read -r line ; do
      SCREEN=$(echo "$line" | sed 's/.*to "\(.*\)".*/\1/g')
      # notify-send -a InputLeap "Switched to $SCREEN"
      case "$SCREEN" in
        "SERVER_SCREEN") ROTATE_CMD;;
        *) RESET_CMD;;
      esac
    done
    
  3. Run the script above (add to autostart if needed).

Bye!

Top comments (0)