Skip to content

Instantly share code, notes, and snippets.

@bcdonadio
Created June 18, 2025 05:17
Show Gist options
  • Select an option

  • Save bcdonadio/70b5aed200bc467aadce2e688f2ebf80 to your computer and use it in GitHub Desktop.

Select an option

Save bcdonadio/70b5aed200bc467aadce2e688f2ebf80 to your computer and use it in GitHub Desktop.
Properly map a Wacom tablet to a given screen, even on Fedora 42.
#!/usr/bin/env bash
# ---------- USER TWEAKABLE CONSTANTS ----------
PATTERN="Wacom Intuos Pro L Pen" # substring to search for (case-insensitive)
MON="DP-4" # xrandr output to map to
ANCHOR="BOTTOM" # FULL|LEFT|RIGHT|TOP|BOTTOM
# ---------------------------------------------
set -euo pipefail
# Function to get monitor information
get_monitor_info() {
local monitor="$1"
local monitor_line
monitor_line=$(xrandr | grep "^$monitor " | head -1)
if [[ -z "$monitor_line" ]]; then
echo "Error: Monitor '$monitor' not found in xrandr output" >&2
echo "Available monitors:" >&2
xrandr | grep " connected" >&2
exit 1
fi
# Extract resolution and position from xrandr output
# Format: "DP-4 connected 2560x1440+3840+0 (normal left inverted right x axis y axis) 597mm x 336mm"
local resolution_part
resolution_part=$(echo "$monitor_line" | grep -o '[0-9]*x[0-9]*+[0-9]*+[0-9]*')
local width height offset_x offset_y
width=$(echo "$resolution_part" | cut -d'x' -f1)
height=$(echo "$resolution_part" | cut -d'x' -f2 | cut -d'+' -f1)
offset_x=$(echo "$resolution_part" | cut -d'+' -f2)
offset_y=$(echo "$resolution_part" | cut -d'+' -f3)
echo "$width $height $offset_x $offset_y"
}
# Function to find matching xinput devices
find_xinput_devices() {
local pattern="$1"
local device_ids
device_ids=$(xinput list | grep -i "$pattern" | grep -o 'id=[0-9]*' | cut -d'=' -f2)
if [[ -z "$device_ids" ]]; then
echo "Error: No xinput devices found matching pattern '$pattern'" >&2
echo "Available devices:" >&2
xinput list >&2
exit 1
fi
echo "$device_ids"
}
# Function to get device tablet area
get_device_area() {
local device_id="$1"
# Try to get tablet area from device properties
local area_line
area_line=$(xinput list-props "$device_id" | grep -i "tablet area" || true)
if [[ -n "$area_line" ]]; then
# Extract tablet area coordinates
local coords
coords=$(echo "$area_line" | grep -o '[0-9]*[[:space:]]*,[[:space:]]*[0-9]*[[:space:]]*,[[:space:]]*[0-9]*[[:space:]]*,[[:space:]]*[0-9]*')
local x1 y1 x2 y2
x1=$(echo "$coords" | cut -d',' -f1 | tr -d ' ')
y1=$(echo "$coords" | cut -d',' -f2 | tr -d ' ')
x2=$(echo "$coords" | cut -d',' -f3 | tr -d ' ')
y2=$(echo "$coords" | cut -d',' -f4 | tr -d ' ')
local width=$((x2 - x1))
local height=$((y2 - y1))
echo "$width $height"
else
# Fallback: try to get from coordinate transformation matrix bounds
# or use reasonable defaults based on device name
echo "Error: Could not determine tablet area for device $device_id" >&2
echo "Device properties:" >&2
xinput list-props "$device_id" >&2
exit 1
fi
}
# Function to apply transformation to device
apply_transformation() {
local device_id="$1"
local screen_w="$2" screen_h="$3" screen_x="$4" screen_y="$5"
local tablet_w="$6" tablet_h="$7"
local anchor="$8"
# Calculate screen aspect ratio
local screen_ar
screen_ar=$(echo "scale=6; $screen_w / $screen_h" | bc -l)
# Calculate tablet aspect ratio
local tablet_ar
tablet_ar=$(echo "scale=6; $tablet_w / $tablet_h" | bc -l)
# Determine usable tablet area that maintains screen aspect ratio
local usable_w usable_h
if (( $(echo "$screen_ar > $tablet_ar" | bc -l) )); then
# Screen is wider - use full tablet width, partial height
usable_w="$tablet_w"
usable_h=$(echo "scale=6; $tablet_w / $screen_ar" | bc -l)
else
# Screen is taller - use full tablet height, partial width
usable_h="$tablet_h"
usable_w=$(echo "scale=6; $tablet_h * $screen_ar" | bc -l)
fi
# Calculate position of usable area within tablet based on ANCHOR
local tablet_start_x tablet_start_y
case "$anchor" in
FULL)
# Center the usable area within the tablet
tablet_start_x=$(echo "scale=6; ($tablet_w - $usable_w) / 2" | bc -l)
tablet_start_y=$(echo "scale=6; ($tablet_h - $usable_h) / 2" | bc -l)
;;
LEFT)
# Anchor to left edge
tablet_start_x="0"
tablet_start_y=$(echo "scale=6; ($tablet_h - $usable_h) / 2" | bc -l)
;;
RIGHT)
# Anchor to right edge
tablet_start_x=$(echo "scale=6; $tablet_w - $usable_w" | bc -l)
tablet_start_y=$(echo "scale=6; ($tablet_h - $usable_h) / 2" | bc -l)
;;
TOP)
# Anchor to top edge
tablet_start_x=$(echo "scale=6; ($tablet_w - $usable_w) / 2" | bc -l)
tablet_start_y="0"
;;
BOTTOM)
# Anchor to bottom edge
tablet_start_x=$(echo "scale=6; ($tablet_w - $usable_w) / 2" | bc -l)
tablet_start_y=$(echo "scale=6; $tablet_h - $usable_h" | bc -l)
;;
esac
# Apply coordinate transformation matrix that maps the usable tablet area to the target screen
# Get total desktop dimensions for normalization
local desktop_w desktop_h
desktop_w=$(xrandr | grep "current" | grep -o "current [0-9]* x [0-9]*" | cut -d' ' -f2)
desktop_h=$(xrandr | grep "current" | grep -o "current [0-9]* x [0-9]*" | cut -d' ' -f4)
# Calculate transformation matrix that maps the usable tablet area to the screen
# The matrix needs to:
# 1. Scale the tablet coordinates so that the usable area maps to screen size
# 2. Translate so that the usable area maps to the screen position
local c0 c1 c2 c3 c4 c5 c6 c7 c8
# Scale factors: screen size relative to the usable tablet area, normalized by desktop
c0=$(echo "scale=6; ($screen_w * $tablet_w) / ($usable_w * $desktop_w)" | bc -l)
c1="0"
# X translation: position screen on desktop, accounting for tablet anchor offset
c2=$(echo "scale=6; ($screen_x - $tablet_start_x * $screen_w / $usable_w) / $desktop_w" | bc -l)
c3="0"
# Y scale factor: screen height relative to usable tablet area, normalized by desktop
c4=$(echo "scale=6; ($screen_h * $tablet_h) / ($usable_h * $desktop_h)" | bc -l)
# Y translation: position screen on desktop, accounting for tablet anchor offset
c5=$(echo "scale=6; ($screen_y - $tablet_start_y * $screen_h / $usable_h) / $desktop_h" | bc -l)
c6="0"
c7="0"
c8="1"
echo "Applying coordinate transformation matrix:"
echo " Usable tablet area: ${usable_w}x${usable_h} at offset ${tablet_start_x},${tablet_start_y} (anchor: $anchor)"
echo " Target screen area: ${screen_w}x${screen_h} at offset ${screen_x},${screen_y}"
echo " Desktop size: ${desktop_w}x${desktop_h}"
echo " Matrix: $c0 $c1 $c2 $c3 $c4 $c5 $c6 $c7 $c8"
xinput set-prop "$device_id" "Coordinate Transformation Matrix" $c0 $c1 $c2 $c3 $c4 $c5 $c6 $c7 $c8
}
# Main execution
main() {
echo "Wacom Tablet Mapping Script"
echo "Pattern: $PATTERN"
echo "Monitor: $MON"
echo "Anchor: $ANCHOR"
echo ""
# Check for required commands
if ! command -v xrandr &> /dev/null; then
echo "Error: xrandr command not found" >&2
exit 1
fi
if ! command -v xinput &> /dev/null; then
echo "Error: xinput command not found" >&2
exit 1
fi
if ! command -v bc &> /dev/null; then
echo "Error: bc command not found (required for calculations)" >&2
exit 1
fi
# Get monitor information
echo "Getting monitor information..."
local monitor_info
monitor_info=$(get_monitor_info "$MON")
read -r screen_w screen_h screen_x screen_y <<< "$monitor_info"
echo "Monitor $MON: ${screen_w}x${screen_h} at offset +${screen_x}+${screen_y}"
# Find matching devices
echo "Finding xinput devices matching '$PATTERN'..."
local device_ids
device_ids=$(find_xinput_devices "$PATTERN")
echo "Found devices: $device_ids"
# Process each device
for device_id in $device_ids; do
echo ""
echo "Processing device $device_id..."
# Get device name for reference
local device_name
device_name=$(xinput list | grep "id=$device_id" | sed 's/.*↳//; s/id=.*//' | xargs)
echo "Device name: $device_name"
# Get tablet area
local tablet_info
tablet_info=$(get_device_area "$device_id")
read -r tablet_w tablet_h <<< "$tablet_info"
echo "Tablet area: ${tablet_w}x${tablet_h}"
# Apply transformation using the new method
apply_transformation "$device_id" "$screen_w" "$screen_h" "$screen_x" "$screen_y" "$tablet_w" "$tablet_h" "$ANCHOR"
echo "Transformation applied successfully"
done
echo ""
echo "Wacom mapping completed successfully!"
}
# Run main function
main "$@"
@bcdonadio
Copy link
Author

Yeah. Vibe-coding. Just wanted to get this shit working again. I noticed that it only works for two monitors, but fuck it, I don't have more than that. Blame Sonnet 4.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment