Add module xinput-toggle to toggle devices (touchpad, mouse, etc.)

This commit is contained in:
Kayomarz Gazder 2025-01-07 15:24:07 +05:30
parent c4f077b1fe
commit 621f56206d
5 changed files with 328 additions and 0 deletions

View file

@ -145,3 +145,4 @@ Advertise your module here, open a PR and include a org-mode link!
- [[./util/wacom/README.org][wacom]] :: Map StumpWM frames to Wacom tablets using `xsetwacom`.
- [[./util/windowtags/README.org][windowtags]] :: Add metadata to windows to manipulate them en mass.
- [[./util/winner-mode/README.org][winner-mode]] :: Emacs' winner-mode for StumpWM
- [[./util/xinput-toggle/README.org][xinput-toggle]] :: Toggle (on/off) devices such as the touchpad, mouse, etc.

View file

@ -0,0 +1,117 @@
** xinput-toggle
Enable, disable or toggle devices such as the mouse, touchpad, etc...
Specify devices using ~cl-ppcre~ regular expressions:
https://edicl.github.io/cl-ppcre/.
This module depends on ~xinput~ which is part of the ~X.Org~ suite of tools.
** Usage
Add something like the following to your =~/.stumpwmrc=:
#+BEGIN_SRC lisp
(load "xinput-toggle")
;; disable all devices whose names match the regex "touchpad".
(xinput-toggle:xinput-disable-devices "touchpad")
;; enable all devices whose names match the regex "wireless mouse".
(xinput-toggle:xinput-enable-devices "wireless mouse")
;; bind a key to toggle all devices whose names match the regex "touchpad"
(define-key *top-map* (kbd "XF86TouchpadToggle")
"xinput-toggle-devices touchpad")
#+END_SRC
~touchpad~ and ~wireless mouse~ are regular expressions matching device names.
On my system the touchpad device name is ~FTCS1000:00 2808:0101 Touchpad~. The
regex ~touchpad~ matches it.
*** ~*case-insensitive-regex*~
~xinput-toggle:*case-insensitive-regex*~ allows case-insensitive matching. By
default its value is ~T~ and can be set to ~NIL~ for case-sensitive matching.
** Discover device names
To specify a device you need its name. Hence the first step is to discover the
device name. You can then decide suitable regular expressions.
*** Using StumpWM command xinput-list-devices
One way to discover device names is to run the command (StumpWM command (colon))
~xinput-list-devices~ passing NAME-REGEX as an argument. It lists devices whose
names match NAME-REGEX.
List all devices:
#+begin_src
xinput-list-devices ""
#+end_src
List all devices whose names matche ~mouse~ or ~touchpad~:
#+begin_src
xinput-list-devices "mouse|touchpad"
#+end_src
*** Using the xinput utility program
Alternatively list devices using the ~xinput~ program via the command line:
#+BEGIN_SRC bash :results output
xinput list --name-only
#+END_SRC
#+RESULTS:
#+begin_example
Virtual core pointer
Virtual core XTEST pointer
Logitech Wireless Mouse
FTCS1000:00 2808:0101 Mouse
ImPS/2 Logitech Wheel Mouse
Virtual core keyboard
Virtual core XTEST keyboard
Power Button
Video Bus
Power Button
Sleep Button
BisonCam,NB Pro: BisonCam,NB Pr
AT Translated Set 2 keyboard
FTCS1000:00 2808:0101 Touchpad
#+end_example
See https://linux.die.net/man/1/xinput.
** Excluding keyboards
The following example would have disabled all devices including keyboards.
However if keyboards get disabled by mistake, there might be no way to interact
with StumpWM.
Hence there is a guard in place to exclude device names containing "keyboard".
#+BEGIN_SRC lisp
(xinput-toggle:xinput-disable-devices "")
#+END_SRC
~xinput:*exclude-keyboards*~ acts as a guard against disabling keyboards by
mistake. By default its value is ~T~ during which ~xinput-toggle~ excludes
devices whose name contains the string ~keyboard~ (always case insensitive). It
can be set to ~NIL~ to disable the guard.
The below will not list keyboard devices as the guard is enabled.
#+BEGIN_SRC lisp
(xinput-toggle:xinput-list-devices "keyboard")
#+END_SRC
This below lists device names matching "keyboard" as we disable the guard.
#+BEGIN_SRC lisp
(let ((xinput-toggle:*exclude-keyboards* NIL))
(xinput-toggle:xinput-list-devices "keyboard"))
#+END_SRC

View file

@ -0,0 +1,14 @@
;;;; package.lisp
(defpackage #:xinput-toggle
(:use #:cl)
(:import-from #:stumpwm
#:defcommand
#:message
#:run-shell-command)
(:export #:*case-insensitive-regex*
#:*exclude-keyboards*
#:xinput-disable-devices
#:xinput-enable-devices
#:xinput-list-devices
#:xinput-toggle-devices))

View file

@ -0,0 +1,10 @@
;;;; xinput-toggle.asd
(asdf:defsystem #:xinput-toggle
:serial t
:description "Toggle (on/off) devices such as the touchpad, mouse, etc."
:author "Kayomarz Gazder <kayomarz@gmail.com>"
:license "GPLv3"
:depends-on (#:stumpwm #:cl-ppcre)
:components ((:file "package")
(:file "xinput-toggle")))

View file

@ -0,0 +1,186 @@
;;;; xinput-toggle.lisp
(in-package #:xinput-toggle)
;;; Module to toggle (on/off) xinput devices (touchpad, mouse, etc.)
;;; Copyright 2025 Kayomarz Gazder.
(defvar *case-insensitive-regex* t
"Device names are specified via a regex. This variable decides if the
regex match should be case insensitive or not.")
(defvar *exclude-keyboards* t
"When using a regex to match devices to turn on or off, this variable
guards against accidental disabling of keyboards (xinput devices with
\"keyboard\" in their name). For instance, the regex \"\" matches all devices
including the keyboards. Disabling keyboards may render StumpWM unusable.
By default, this variable is set to T and devices containing \"keyboard\" will
be excluded from the set of matched devices.
When set to NIL, the guard is removed and device names containing \"keyboard\"
will be part of the set of devices. Hence keyboards are at the risk of being
disabled.
Disabling keyboards may render StumpWM unusable.
Use with caution.
PS: To test what device names matche a regex, use the function
XINPUT-LIST-DEVICES, passing regex arg.")
(defvar *whitespaces*
(list #\Space #\Tab #\Newline #\Return #\Linefeed #\Page)
"Characters considered as whitespace when parsing an xinput line.")
(defclass device ()
((id :reader device-id :initarg :id)
(name :reader device-name :initarg :name)
(info :reader device-info :initarg :info)))
(defmethod name-matches-p ((d device) regex)
"Returns T if device name matches REGEX, else returns NIL."
(let ((scanner (ppcre:create-scanner
regex :case-insensitive-mode *case-insensitive-regex*)))
(when (cl-ppcre:scan-to-strings scanner (device-name d))
t)))
(defmethod is-keyboard-p ((d device))
"Returns T if device name contains string \"keyboard\", else returns NIL."
(name-matches-p d "keyboard"))
(defun make-device-from-line (line)
"Make a device object by parsing a text line representing a device .
An example of a line representing a device is:
\"FTCS1000:00 2808:0101 Touchpad\" \"13\" \"[floating slave]\""
(let ((parts (multiple-value-bind (_ parts)
(ppcre:scan-to-strings
"^[^\\w]*(.*)id\\s*=\\s*(\\d*)(.*)"
line)
(declare (ignore _))
(map 'vector
(lambda (s) (string-trim *whitespaces* s))
parts))))
(make-instance 'device :name (elt parts 0)
:id (parse-integer (elt parts 1))
:info (elt parts 2))))
(defmethod enabledp ((d device))
(xinput-enabled-p (device-id d)))
(defmethod disable ((d device))
"Enables device and returns a string indicating the action."
(xinput-cmd "disable" (device-id d))
(format nil "disabled ~A" d))
(defmethod enable ((d device))
"Enables device and returns a string indicating the action."
(xinput-cmd "enable" (device-id d))
(format nil "enabled ~A" d))
(defmethod toggle ((d device))
"Toggles the device and returns a string indicating if it was enabled or
disabled along with the device name and id."
(if (enabledp d) (disable d) (enable d)))
(defmethod print-object ((d device) stream)
(format stream "~A (device ~A)"
(device-name d)
(device-id d)))
(defun xinput-cmd (&rest args)
"Run the `xinput` utility program with args. For example:
(xinput-cmd \"enable\" 10) ; enable device id 10
The equivalent shell command would be:
xinput enable 10"
(run-shell-command (format nil "xinput ~{~A~^ ~}" args) t))
(defun xinput-error ()
(error (format nil
"xinput error.~%Make sure 'xinput' is installed.")))
(defun get-all-devices ()
(let ((stdout (xinput-cmd "list" "--short")))
(if (eq (length stdout) 0) ;; something went wrong if no stdout
(xinput-error)
(mapcar
(lambda (line)
(make-device-from-line line))
(ppcre:split #\Newline stdout)))))
(defun sort-devices (device-list)
(sort device-list
(lambda (d1 d2)
(< (device-id d1) (device-id d2)))))
(defun get-devices (&optional (regex nil))
"Get a list of devices whose name match regex. Each device is represented by
the DEVICE class."
(sort-devices
(let* ((all-devices (get-all-devices))
(devices (if (not regex)
all-devices
(remove-if-not (lambda (d) (name-matches-p d regex))
all-devices))))
(if *exclude-keyboards*
(remove-if (lambda (d) (is-keyboard-p d))
devices)
devices))))
(defun xinput-enabled-p (device-id)
"Returns T if the device is enabled, else NIL."
(let* ((scanner (ppcre:create-scanner "device\\s*enabled[^:]*:\\s*(\\d*)"
:case-insensitive-mode t))
(xinput-stdout (xinput-cmd "list-props" device-id))
(groups (nth-value 1 (ppcre:scan-to-strings scanner xinput-stdout)))
(status (elt groups 0)))
(equal status "1")))
(defun for-devices (name-regex fn)
"For each device matching NAME-REGEX, call fn, passing the device as arg."
(let ((devices (get-devices name-regex)))
(if (null devices)
(message "xinput-toggle: No devices match regex \"~A\"." name-regex)
(message (format nil "~{~A~^~%~}"
(mapcar (lambda (d) (funcall fn d)) devices))))))
(defcommand xinput-list-devices (name-regex) ((:string))
"List xinput devices whose names match NAME-REGEX.
If NAME-REGEX is empty, all devices are listed. By default, devices containing
'keyboard' in their name are excluded unless `*exclude-keyboards*` is set to
NIL."
(message (format nil "xinput: Devices matching regex \"~A\":~% ~%~{~A~^~%~}"
name-regex
(mapcar #'princ-to-string (get-devices name-regex)))))
(defcommand xinput-enable-devices (name-regex) ((:string))
"Enable xinput devices whose names match NAME-REGEX.
NAME-REGEX is a regular expression used to match device names. By default,
devices containing 'keyboard' (case-insensitive) in their name are excluded to
avoid accidental disabling of keyboards. Set `*exclude-keyboards*` to NIL to
include keyboards.
To view xinput device names on your system, use the shell command:
xinput --list --name-only
For example to toggle devices whoose name contains \"touchpad\":
(enable-devices \"touchpad\")"
(for-devices name-regex (lambda (d) (enable d))))
(defcommand xinput-disable-devices (name-regex) ((:string))
"Disable xinput devices whose names match NAME-REGEX.
Also see documentation of `enable-devices`"
(for-devices name-regex (lambda (d) (disable d))))
(defcommand xinput-toggle-devices (name-regex) ((:string))
"Toggle the state of xinput devices whose names match NAME-REGEX.
Also see documentation of `enable-devices`"
(for-devices name-regex (lambda (d) (toggle d))))