Skip to content

joshman1019/middle-mouse-zoom

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 

Repository files navigation

Middle Mouse Zoom Script Documentation

A Linux accessibility tool that remaps the middle mouse button to screen zoom controls, with support for per-application allowlisting.

Features

  • Quick click: Zoom in (Super+=)
  • Hold + drag down-right (toward 5 o'clock): Reset zoom (Super+0) - triggers immediately when threshold reached
  • Allowlist support: Specified applications receive native middle-click passthrough instead of zoom behavior
  • Complete middle-click blocking: For non-allowlisted apps, middle-click is entirely blocked (no passthrough)

Dependencies

Install the following packages:

# Arch Linux (AUR)
yay -S evsieve

# Also required
pacman -S xdotool ydotool evtest
  • evsieve: Grabs the input device and blocks/filters events
  • xdotool: Detects focused window and process name for allowlist checking
  • ydotool: Sends synthetic key presses and mouse clicks
  • evtest: (optional) Useful for debugging input events

Files

All files are located in ~/.local/bin/:

1. middle-mouse-zoom.sh (Main Script)

#!/bin/bash
# Middle Mouse Button Zoom Script with Gesture Support (evsieve version)
# Quick click: Super+= (zoom in)
# Hold + drag down-right (toward 5 o'clock): Super+0 (reset zoom) - triggers immediately
#
# ALLOWLIST: Apps in middle-mouse-allowlist.json get native middle-click passthrough
#            (zoom features disabled). All other apps have middle-click blocked.
#
# Uses evsieve to grab the device and block middle-click from passing through.

DEVICE="/dev/input/by-id/usb-Kensington_Orbit_Fusion_Wireless_Trackball-event-mouse"
STATE_FILE="/tmp/middle-mouse-state"
ALLOWLIST_FILE="${HOME}/.local/bin/middle-mouse-allowlist.json"
HANDLER_SCRIPT="${HOME}/.local/bin/middle-mouse-handler.sh"
MOVE_THRESHOLD_X=50
MOVE_THRESHOLD_Y=50

cleanup() {
    rm -f "$STATE_FILE"
    # Kill any lingering mouse tracker
    pkill -f "middle-mouse-tracker" 2>/dev/null
    exit 0
}
trap cleanup SIGTERM SIGINT

rm -f "$STATE_FILE"
echo "idle 0 0" > "$STATE_FILE"

echo "Starting middle-mouse-zoom with evsieve..."
echo "Middle-click is blocked for all apps except those in allowlist."

# Export for handler
export STATE_FILE ALLOWLIST_FILE MOVE_THRESHOLD_X MOVE_THRESHOLD_Y

# Use evsieve to:
# 1. Grab the device exclusively
# 2. Use --hook to call handler on middle button press/release
# 3. Block BTN_MIDDLE from output (prevents passthrough)
# 4. Pass rel:x and rel:y through --print to track movement
# 5. Output all other events to a virtual device
evsieve \
    --input "$DEVICE" grab \
    --hook btn:middle:1 exec-shell="$HANDLER_SCRIPT press" \
    --hook btn:middle:0 exec-shell="$HANDLER_SCRIPT release" \
    --print rel:x rel:y \
    --block btn:middle \
    --output name="Kensington Orbit (filtered)" \
    2>/dev/null | "$HANDLER_SCRIPT" track

2. middle-mouse-handler.sh (Event Handler)

#!/bin/bash
# Handler script for middle-mouse-zoom evsieve hooks
# Called by evsieve with argument: "press", "release", or "track"

STATE_FILE="${STATE_FILE:-/tmp/middle-mouse-state}"
ALLOWLIST_FILE="${ALLOWLIST_FILE:-${HOME}/.local/bin/middle-mouse-allowlist.json}"
MOVE_THRESHOLD_X="${MOVE_THRESHOLD_X:-50}"
MOVE_THRESHOLD_Y="${MOVE_THRESHOLD_Y:-50}"

# Load allowlist
load_allowlist() {
    if [ -f "$ALLOWLIST_FILE" ]; then
        grep -oP '(?<=")[^"]+(?=")' "$ALLOWLIST_FILE" | grep -v "description\|allowlist" | tr '\n' '|' | sed 's/|$//'
    fi
}

ALLOWLIST_CACHE=$(load_allowlist)

# Check if focused window's process is in allowlist
is_allowlisted_app() {
    [ -z "$ALLOWLIST_CACHE" ] && return 1
    
    local window_id=$(xdotool getactivewindow 2>/dev/null)
    [ -z "$window_id" ] && return 1
    
    local pid=$(xdotool getwindowpid "$window_id" 2>/dev/null)
    [ -z "$pid" ] && return 1
    
    local proc_name=$(cat /proc/"$pid"/comm 2>/dev/null)
    [ -z "$proc_name" ] && return 1
    
    echo "$proc_name" | grep -qiE "$ALLOWLIST_CACHE"
}

send_middle_click() {
    ydotool click 0xC0002
}

check_and_trigger_gesture() {
    local cur_x=$1
    local cur_y=$2
    
    if [ "$cur_x" -gt "$MOVE_THRESHOLD_X" ] && [ "$cur_y" -gt "$MOVE_THRESHOLD_Y" ]; then
        # Reset zoom: Super+0
        ydotool key 125:1 11:1 11:0 125:0
        return 0
    fi
    return 1
}

case "$1" in
    press)
        if is_allowlisted_app; then
            echo "allowlisted 0 0" > "$STATE_FILE"
        else
            echo "pressed 0 0" > "$STATE_FILE"
        fi
        ;;
    release)
        read -r state cur_x cur_y < "$STATE_FILE" 2>/dev/null
        
        if [ "$state" = "allowlisted" ]; then
            send_middle_click
        elif [ "$state" = "pressed" ] || [ "$state" = "dragging" ]; then
            # Only zoom in if minimal movement (not a drag gesture)
            if [ "$cur_x" -gt "-$MOVE_THRESHOLD_X" ] && [ "$cur_x" -lt "$MOVE_THRESHOLD_X" ] && \
               [ "$cur_y" -gt "-$MOVE_THRESHOLD_Y" ] && [ "$cur_y" -lt "$MOVE_THRESHOLD_Y" ]; then
                # Zoom in: Super+=
                ydotool key 125:1 13:1 13:0 125:0
            fi
        fi
        # If state was "triggered", gesture already executed - do nothing
        
        echo "idle 0 0" > "$STATE_FILE"
        ;;
    track)
        # Track mouse movement from evsieve --print output
        # Format: Event:  type:code = rel:x          value = 5           domain = ...
        while read -r line; do
            read -r state cur_x cur_y < "$STATE_FILE" 2>/dev/null
            
            # Skip if idle, allowlisted, or already triggered
            [ "$state" = "idle" ] && continue
            [ "$state" = "allowlisted" ] && continue
            [ "$state" = "triggered" ] && continue
            
            if echo "$line" | grep -q "rel:x"; then
                rel_x=$(echo "$line" | grep -oP 'value\s*=\s*\K-?[0-9]+')
                [ -z "$rel_x" ] && continue
                new_x=$((cur_x + rel_x))
                
                if check_and_trigger_gesture "$new_x" "$cur_y"; then
                    echo "triggered $new_x $cur_y" > "$STATE_FILE"
                else
                    echo "dragging $new_x $cur_y" > "$STATE_FILE"
                fi
            elif echo "$line" | grep -q "rel:y"; then
                rel_y=$(echo "$line" | grep -oP 'value\s*=\s*\K-?[0-9]+')
                [ -z "$rel_y" ] && continue
                new_y=$((cur_y + rel_y))
                
                if check_and_trigger_gesture "$cur_x" "$new_y"; then
                    echo "triggered $cur_x $new_y" > "$STATE_FILE"
                else
                    echo "dragging $cur_x $new_y" > "$STATE_FILE"
                fi
            fi
        done
        ;;
esac

3. middle-mouse-allowlist.json (Configuration)

{
  "description": "Apps where middle-click passthrough is ENABLED (bypasses zoom remapping)",
  "allowlist": [
    "steam_app_",
    "csgo",
    "dota2",
    "hl2_linux"
  ]
}

Installation

  1. Create the files in ~/.local/bin/:

    mkdir -p ~/.local/bin
    # Copy the scripts above to:
    # ~/.local/bin/middle-mouse-zoom.sh
    # ~/.local/bin/middle-mouse-handler.sh
    # ~/.local/bin/middle-mouse-allowlist.json
  2. Make scripts executable:

    chmod +x ~/.local/bin/middle-mouse-zoom.sh
    chmod +x ~/.local/bin/middle-mouse-handler.sh
  3. Update the DEVICE variable in middle-mouse-zoom.sh to match your pointing device:

    # Prefer a stable symlink (survives reboots):
    ls -l /dev/input/by-id/   | grep -Ei 'mouse|trackball|touchpad'
    ls -l /dev/input/by-path/ | grep -Ei 'mouse|trackball|touchpad'
    
    # (Optional) verify you picked the right one by watching events while you move/click:
    sudo evtest /dev/input/by-id/<your-device>-event-mouse
  4. Ensure ydotool daemon is running:

    systemctl --user enable --now ydotool.service

Systemd Service (Optional)

Create ~/.config/systemd/user/middle-mouse-zoom.service:

[Unit]
Description=Middle Mouse Zoom Script
Wants=graphical-session.target
After=graphical-session.target ydotool.service
Requires=ydotool.service
PartOf=graphical-session.target

[Service]
Type=simple
ExecStart=%h/.local/bin/middle-mouse-zoom.sh
Restart=always
RestartSec=3

[Install]
WantedBy=graphical-session.target

Note: on many systems, access to /dev/input/event* is granted by logind after the session becomes active. The script now waits until it can read the device before starting evsieve.

Enable and start (run these without sudo):

systemctl --user daemon-reload
systemctl --user enable middle-mouse-zoom
systemctl --user start middle-mouse-zoom

Configuration

Adjusting Gesture Threshold

Edit MOVE_THRESHOLD_X and MOVE_THRESHOLD_Y in middle-mouse-zoom.sh to change how far you need to drag before the reset zoom gesture triggers. Default is 50 pixels in each direction.

Adding Apps to Allowlist

Edit middle-mouse-allowlist.json and add process names to the allowlist array. Use partial matches (regex patterns work). To find a process name:

# While the app is focused:
xdotool getactivewindow getwindowpid | xargs -I{} cat /proc/{}/comm

How It Works

  1. evsieve grabs the trackball device exclusively and creates a virtual output device
  2. Middle button events are blocked from the output (preventing passthrough)
  3. --hook triggers the handler script on press/release
  4. --print streams mouse movement to the handler's track mode
  5. The handler tracks cumulative movement while the button is held
  6. On release, it either sends zoom-in (minimal movement) or does nothing (gesture already triggered)

Troubleshooting

systemctl --user fails under sudo

If you run sudo systemctl --user ... you may get: Failed to connect to user scope bus ... $DBUS_SESSION_BUS_ADDRESS and $XDG_RUNTIME_DIR not defined

Fix: run the command as your regular user (no sudo):

systemctl --user restart middle-mouse-zoom.service

If you truly need to run it from a root shell, target the user instance explicitly:

# replace $USER if needed
uid=$(id -u "$USER")
sudo -u "$USER" XDG_RUNTIME_DIR="/run/user/$uid" \
  systemctl --user restart middle-mouse-zoom.service

Script not starting

  • Check the device path exists: ls -la /dev/input/by-id/ /dev/input/by-path/
  • If you set DEVICE=/dev/input/eventX, switch to a /dev/input/by-id/... (or by-path) symlink since eventX numbers can change
  • Ensure you have permission: user may need to be in input group

Zoom not working

  • Check ydotool is running: systemctl --user status ydotool.service
  • Verify key codes match your desktop environment's zoom shortcuts

Allowlist not working

  • Check xdotool can get window info: xdotool getactivewindow
  • Verify process name matches: cat /proc/$(xdotool getactivewindow getwindowpid)/comm

About

An accessability script that enables middle-click to zoom on KDE.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages