Skip to content

Instantly share code, notes, and snippets.

@hamidzr
Last active October 3, 2022 16:59
Show Gist options
  • Save hamidzr/452a2423b6384ee74531017fc62f7110 to your computer and use it in GitHub Desktop.
Save hamidzr/452a2423b6384ee74531017fc62f7110 to your computer and use it in GitHub Desktop.
Python APIs to configure Wacom tablets, stylus, and pen on Linux. Set ratios and rotations based on a selected area of screen and more.
#!/usr/bin/env python3
from typing import Literal, Union
from py_utils.v1.bash import get_output, run
import dataclasses
import functools
"""
from Arch Linux wiki:
Reducing the drawing area height
Run:
$ xsetwacom set stylus Area 0 0 tablet_width height
where height is tablet_width * screen_height / screen_width.
The tablet resolution can be reset back to the default using:
$ xsetwacom set stylus ResetArea
Reducing the screen area width
Run:
$ xsetwacom set stylus MapToOutput WIDTHxSCREEN_HEIGHT+0+0
where WIDTH is screen_height * tablet_width / tablet_height.
"""
@dataclasses.dataclass
class Geometry:
w: int
h: int
x: int # x offset
y: int # y offset
def __str__(self):
return f"{self.w}x{self.h}+{self.x}+{self.y}"
def __repr__(self):
return f"Geometry({self.w}, {self.h}, {self.x}, {self.y})"
@classmethod
def from_str(cls, s):
w, h = s.split("+")[0].split("x")
x, y = s.split("+")[1:]
return cls(int(w), int(h), int(x), int(y))
def aspect_ratio(self):
return self.w / self.h
def isHorizontal(self):
return self.w > self.h
def copy(self):
return Geometry(self.w, self.h, self.x, self.y)
def scale(self, factor):
self.w = int(self.w * factor)
self.h = int(self.h * factor)
return self
def crop_to_ratio(self, other: "Geometry"):
if self.aspect_ratio() > other.aspect_ratio():
self.w = int(self.h * other.aspect_ratio())
else:
self.h = int(self.w / other.aspect_ratio())
return self
def rotate_90(self):
self.w, self.h = self.h, self.w
return self
def center_on(self, other: "Geometry"):
self.x = int((other.w - self.w) / 2)
self.y = int((other.h - self.h) / 2)
return self
def top_left(self):
return (self.x, self.y)
def bottom_right(self):
return (self.x + self.w, self.y + self.h)
@functools.cache
def get_stylus_name():
"""the pen"""
return get_output("xsetwacom --list devices | grep stylus | cut -f 1")
@functools.cache
def get_pad_name():
"""the board"""
return get_output("xsetwacom --list devices | grep pad | cut -f 1")
def set_stylus_attr(attr: str, value: str):
run(f"xsetwacom set '{get_stylus_name()}' {attr} {value}")
def set_stylus_area(geo: Geometry):
"""
man xsetwacom
"""
x1, y1 = geo.top_left()
x2, y2 = geo.bottom_right()
set_stylus_attr("Area", f"{x1} {y1} {x2} {y2}")
def get_stylus_geometry():
out = get_output(f"xsetwacom get '{get_stylus_name()}' Area")
# parse the output from X Y width height into dimensions and offset
x1, y1, x2, y2 = [int(i) for i in out.split()]
return Geometry(x2 - x1, y2 - y1, x1, y1)
def get_screen_geometry(name: str):
out = get_output(f"xrandr --query | grep {name} | grep -P '[\\dx+]{{8,20}}' -o")
return Geometry.from_str(out)
def get_slop_geometry():
"""interactive selection of a screen area"""
out = get_output("slop -f '%w %h %x %y'")
return Geometry(*[int(i) for i in out.split()])
def sync_geo_to_stylus(geo: Geometry):
"""
keep the aspect ratio of the stylus and the screen
the same by manipulating what the stylus maps to.
cropping the screen area to match the stylus area
aspect ratio
"""
# get the stylus geometry
stylus = get_stylus_geometry()
# base on the geometry aspect ratio figure out
# which dimension to crop and compute the output geometry
out = geo.copy().crop_to_ratio(stylus)
set_stylus_attr("MapToOutput", str(out))
def sync_stylus_to_geo(geo: Geometry):
"""
syncs the stylus area to the given geometry's aspect ratio
compromise tablet area to get exact mapping to geo
"""
stylus = get_stylus_geometry()
if geo.isHorizontal():
stylus.crop_to_ratio(geo)
else:
geo.rotate_90()
base_stylus = stylus.copy()
stylus.crop_to_ratio(geo).center_on(base_stylus)
print(base_stylus, stylus)
geo.rotate_90()
set_rotation("vertical")
set_stylus_area(stylus)
set_stylus_attr("MapToOutput", str(geo))
def list_screen_names():
out = get_output("xrandr --query | grep ' connected' | cut -d' ' -f1")
return sorted(out.split())
def set_mode(mode: Union[Literal["absolute"], Literal["relative"]]):
assert mode in ("relative", "absolute")
set_stylus_attr("Mode", mode)
def set_rotation(rotation: Union[Literal["horizontal"], Literal["vertical"]]):
assert rotation in ("horizontal", "vertical")
if rotation == "horizontal":
set_stylus_attr("Rotate", "none")
else:
print("use tablet in vertical mode")
set_stylus_attr("Rotate", "cw")
def reset_stylus():
stylus_config = {
"Mode": "absolute",
"Rotate": "none",
"MapToOutput": "desktop",
"ResetArea": "",
# has two button on the pen. pen tip is button 1
# "Button 2": "key +ctrl z -ctrl", # macros work as well
# "Button 3": "key +ctrl z -ctrl", # macros work as well
}
pad_config = {
# eg 4 buttons on the pad/tablet
"Button 1": "key +ctrl z -ctrl",
"Button 2": "key space",
}
for attr, value in stylus_config.items():
set_stylus_attr(attr, value)
for attr, value in pad_config.items():
run(f"xsetwacom set '{get_pad_name()}' {attr} {value}")
if __name__ == "__main__":
# set up argparser with help to get screen name as an argument
# import argparse
# parser = argparse.ArgumentParser()
# args = parser.parse_args()
reset_stylus()
print(get_stylus_name())
print(get_stylus_geometry())
print(list_screen_names())
sync_stylus_to_geo(get_slop_geometry())
@hamidzr
Copy link
Author

hamidzr commented Oct 3, 2022

dependency: https://github.com/hamidzr/py-utils
this could be swapped out without too much effort

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