Last active
October 3, 2022 16:59
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
dependency: https://github.com/hamidzr/py-utils
this could be swapped out without too much effort