summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--meson.build10
-rw-r--r--tools/Makefile.am9
-rwxr-xr-xtools/show-stylus.py192
3 files changed, 209 insertions, 2 deletions
diff --git a/meson.build b/meson.build
index 6d9552f..8036efb 100644
--- a/meson.build
+++ b/meson.build
@@ -4,6 +4,7 @@ project('libwacom', 'c',
default_options: [ 'c_std=gnu99', 'warning_level=2' ],
meson_version: '>= 0.50.0')
+dir_bin = join_paths(get_option('prefix'), get_option('bindir'))
dir_data = join_paths(get_option('prefix'), get_option('datadir'), 'libwacom')
dir_etc = join_paths(get_option('prefix'), get_option('sysconfdir'), 'libwacom')
dir_man1 = join_paths(get_option('prefix'), get_option('mandir'), 'man1')
@@ -172,6 +173,15 @@ install_man(configure_file(input: 'tools/libwacom-list-local-devices.man',
output: '@BASENAME@.1',
copy: true))
+showstylus_config = configuration_data()
+showstylus_config.set('DATADIR', dir_data)
+showstylus_config.set('ETCDIR', dir_etc)
+configure_file(output: 'libwacom-show-stylus',
+ input: 'tools/show-stylus.py',
+ configuration: showstylus_config,
+ install_dir: dir_bin,
+ install: true)
+
############### docs ###########################
docs_feature = get_option('documentation')
doxygen = find_program('doxygen', required: docs_feature)
diff --git a/tools/Makefile.am b/tools/Makefile.am
index a88db1a..e81e60c 100644
--- a/tools/Makefile.am
+++ b/tools/Makefile.am
@@ -30,5 +30,10 @@ dist_udev_hwdb_DATA = $(hwdb)
$(hwdb): generate-hwdb
$(builddir)/$< > $@
-CLEANFILES = $(hwdb)
-EXTRA_DIST = $(rules) $(hwdb)
+bin_SCRIPTS = libwacom-show-stylus
+libwacom-show-stylus: show-stylus.py
+ sed -e 's|@DATADIR@|$(datadir)/libwacom|' -e 's|@ETCDIR@|$(sysconfdir)/libwacom|' $< > $@
+
+
+CLEANFILES = $(hwdb) $(bin_SCRIPTS)
+EXTRA_DIST = $(rules) $(hwdb) show-stylus.py
diff --git a/tools/show-stylus.py b/tools/show-stylus.py
new file mode 100755
index 0000000..be734a4
--- /dev/null
+++ b/tools/show-stylus.py
@@ -0,0 +1,192 @@
+#!/usr/bin/env python3
+#
+# Permission to use, copy, modify, distribute, and sell this software
+# and its documentation for any purpose is hereby granted without
+# fee, provided that the above copyright notice appear in all copies
+# and that both that copyright notice and this permission notice
+# appear in supporting documentation, and that the name of Red Hat
+# not be used in advertising or publicity pertaining to distribution
+# of the software without specific, written prior permission. Red
+# Hat makes no representations about the suitability of this software
+# for any purpose. It is provided "as is" without express or implied
+# warranty.
+#
+# THE AUTHORS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
+# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN
+# NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY SPECIAL, INDIRECT OR
+# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
+# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+import argparse
+import configparser
+import sys
+from pathlib import Path
+
+try:
+ import libevdev
+ import pyudev
+except ModuleNotFoundError as e:
+ print("Error: {}".format(str(e)), file=sys.stderr)
+ print(
+ "One or more python modules are missing. Please install those "
+ "modules and re-run this tool."
+ )
+ sys.exit(1)
+
+
+class Ansi:
+ clearline = "\x1B[K"
+
+ @classmethod
+ def up(cls, count):
+ return f"\x1B[{count}A"
+
+ @classmethod
+ def down(cls, count):
+ return f"\x1B[{count}B"
+
+ @classmethod
+ def right(cls, count):
+ return f"\x1B[{count}C"
+
+ @classmethod
+ def left(cls, count):
+ return f"\x1B[{count}D"
+
+
+def die(msg):
+ print(msg, file=sys.stderr)
+ sys.exit(1)
+
+
+def select_device():
+ context = pyudev.Context()
+ for device in context.list_devices(subsystem="input"):
+ if device.get("ID_INPUT_TABLET", 0) and (device.device_node or "").startswith(
+ "/dev/input/event"
+ ):
+ name = device.get("NAME", None)
+ if not name:
+ name = next(
+ (p.get("NAME") for p in device.ancestors if p.get("NAME")),
+ "unknown",
+ )
+
+ print("Using {}: {}".format(name or "unknown", device.device_node))
+ return device.device_node
+
+ die("Unable to find a tablet device.")
+
+
+def record_events(ns):
+ with open(ns.device_path, "rb") as fd:
+ d = libevdev.Device(fd)
+ if not d.absinfo[libevdev.EV_ABS.ABS_MISC]:
+ die("Device only supports generic styli")
+
+ tool_bits = set(
+ c for c in libevdev.EV_KEY.codes if c.name.startswith("BTN_TOOL_")
+ )
+ styli = {} # dict of (type, serial) = proximity_state
+ current_type, current_serial = 0, 0
+ in_prox = False
+ dirty = False
+
+ print("Please put tool in proximity")
+
+ try:
+ while True:
+ for event in d.events():
+ if event.matches(libevdev.EV_ABS.ABS_MISC):
+ if event.value != 0:
+ current_type = event.value
+ dirty = True
+ elif event.matches(libevdev.EV_MSC.MSC_SERIAL):
+ if event.value != 0:
+ current_serial = event.value & 0xFFFFFFFF
+ dirty = True
+ elif event.code in tool_bits:
+ # print(f'Current prox: {event.value}')
+ in_prox = event.value != 0
+ dirty = True
+ elif event.matches(libevdev.EV_SYN.SYN_REPORT) and dirty:
+ dirty = False
+ print(
+ f"{Ansi.up(len(styli))}{Ansi.left(10000)}{Ansi.clearline}",
+ end="",
+ )
+ styli[(current_type, current_serial)] = in_prox
+ for s, prox in styli.items():
+ tid, serial = s
+ print(
+ f"Tool id {tid:#x} serial {serial:#x} in-proximity: {prox} "
+ )
+ except KeyboardInterrupt:
+ print("Terminating")
+
+ return [s[0] for s in styli.keys()]
+
+
+def load_data_files():
+ lookup_paths = (
+ ("./data/",),
+ ("@DATADIR@", "@ETCDIR@"),
+ ("/usr/share/libwacom/", "/etc/libwacom/"),
+ )
+ stylusfiles = []
+ for paths in lookup_paths:
+ stylusfiles = []
+ for p in paths:
+ files = list(Path(p).glob("*.stylus"))
+ if files:
+ stylusfiles += files
+
+ if any(stylusfiles):
+ break
+ else:
+ die("Unable to find a libwacom.stylus data file")
+
+ print(f'Using stylus file(s): {", ".join([str(s) for s in stylusfiles])}')
+
+ styli = {}
+
+ for path in stylusfiles:
+ config = configparser.ConfigParser()
+ config.read(path)
+ for stylus_id in config.sections():
+ sid = int(stylus_id, 16)
+ styli[sid] = config[stylus_id].get("Group", sid)
+
+ return styli
+
+
+def main():
+ parser = argparse.ArgumentParser(description="Tool to show tablet stylus ids")
+ parser.add_argument(
+ "device_path", nargs="?", default=None, help="Path to the /dev/input/event node"
+ )
+
+ ns = parser.parse_args()
+ if not ns.device_path:
+ ns.device_path = select_device()
+
+ all_styli = load_data_files()
+ styli = record_events(ns)
+ groups = []
+ for sid in styli:
+ if sid in all_styli:
+ groups.append(all_styli[sid])
+ else:
+ print(f"Unknown stylus id {sid:#x}. New entry needed")
+ print("Suggested line for .tablet file:")
+ print(f"Styli={';'.join(set(groups))}")
+
+
+if __name__ == "__main__":
+ try:
+ main()
+ except PermissionError:
+ die("Insufficient permissions, please run me as root")