diff --git a/README.org b/README.org index 9673283..56e7573 100644 --- a/README.org +++ b/README.org @@ -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. diff --git a/util/xinput-toggle/README.org b/util/xinput-toggle/README.org new file mode 100644 index 0000000..1a9aa00 --- /dev/null +++ b/util/xinput-toggle/README.org @@ -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 diff --git a/util/xinput-toggle/package.lisp b/util/xinput-toggle/package.lisp new file mode 100644 index 0000000..0e54d45 --- /dev/null +++ b/util/xinput-toggle/package.lisp @@ -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)) diff --git a/util/xinput-toggle/xinput-toggle.asd b/util/xinput-toggle/xinput-toggle.asd new file mode 100644 index 0000000..09f8a92 --- /dev/null +++ b/util/xinput-toggle/xinput-toggle.asd @@ -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 " + :license "GPLv3" + :depends-on (#:stumpwm #:cl-ppcre) + :components ((:file "package") + (:file "xinput-toggle"))) diff --git a/util/xinput-toggle/xinput-toggle.lisp b/util/xinput-toggle/xinput-toggle.lisp new file mode 100644 index 0000000..65ec35d --- /dev/null +++ b/util/xinput-toggle/xinput-toggle.lisp @@ -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))))