├── .flake8 ├── .github └── ISSUE_TEMPLATE │ └── Bug_report.md ├── .gitignore ├── Makefile ├── README.md ├── internal ├── internal-test ├── libinput-gestures ├── libinput-gestures-setup ├── libinput-gestures.conf ├── libinput-gestures.desktop ├── libinput-gestures.png └── list-version-hashes /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E126,E127,E128,E302,E305,E401 3 | max-line-length = 80 4 | max-complexity = 30 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: New Issue 3 | about: Create a new issue report 4 | 5 | --- 6 | 7 | Before creating a new issue, please follow each step in the TROUBLESHOOTING section 8 | of the main README. 9 | 10 | ``` 11 | Run libinput-gestures -l on the command line and paste the output here. 12 | ``` 13 | 14 | **Describe the issue** 15 | A clear and concise description of what the issue is. 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | README.html 4 | *.pyc 5 | __pycache__ 6 | *~ 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Mark Blakeney. This program is distributed under 2 | # the terms of the GNU General Public License. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or any 7 | # later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, but 10 | # WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # General Public License at for more 13 | # details. 14 | 15 | DOC = README.md 16 | SHELLCHECK_OPTS = -eSC2053,SC2064,SC2086,SC1117,SC2162,SC2181,SC2034,SC1090,SC2115 17 | 18 | DOCOUT = $(DOC:.md=.html) 19 | 20 | all: 21 | @echo "Type sudo make install|uninstall" 22 | 23 | install: 24 | @./libinput-gestures-setup -d "$(DESTDIR)" install 25 | 26 | uninstall: 27 | @./libinput-gestures-setup -d "$(DESTDIR)" uninstall 28 | 29 | check: 30 | flake8 libinput-gestures internal internal-test 31 | shellcheck $(SHELLCHECK_OPTS) libinput-gestures-setup list-version-hashes 32 | vermin -i -q -t 3.4 --no-tips libinput-gestures 33 | vermin -i -q --no-tips internal internal-test 34 | 35 | doc: $(DOCOUT) 36 | 37 | $(DOCOUT): $(DOC) 38 | markdown $< >$@ 39 | 40 | test: 41 | @./internal-test 42 | 43 | clean: 44 | rm -rf $(DOCOUT) 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### LIBINPUT-GESTURES 2 | 3 | [Libinput-gestures][REPO] is a utility which reads [libinput 4 | gestures](https://wayland.freedesktop.org/libinput/doc/latest/gestures.html) 5 | from your touchpad and maps them to gestures you configure in a 6 | configuration file. Each gesture can be configured to activate a shell 7 | command which is typically an [_xdotool_][XDOTOOL] command to action 8 | desktop/window/application keyboard combinations and commands. See the 9 | examples in the provided `libinput-gestures.conf` file. My motivation 10 | for creating this is to use triple swipe up/down to switch workspaces, 11 | and triple swipe right/left to go backwards/forwards in my browser, as 12 | per the default configuration. 13 | 14 | This small and simple utility is only intended to be used temporarily 15 | until GNOME and other DE's action libinput gestures natively. It parses 16 | the output of the _libinput list-devices_ and _libinput debug-events_ 17 | utilities so is a little fragile to any version changes in their output 18 | format. 19 | 20 | This utility is developed and tested on Arch linux using the GNOME 3 DE 21 | on Xorg and Wayland. It works somewhat incompletely on Wayland (via 22 | XWayland). See the WAYLAND section below and the comments in the default 23 | `libinput-gestures.conf` file. It has been [reported to work with 24 | KDE](http://www.lorenzobettini.it/2017/02/touchpad-gestures-in-linux-kde-with-libinput-gestures/). 25 | I am not sure how well this will work on all distros and DE's etc. 26 | 27 | The latest version and documentation is available at 28 | https://github.com/bulletmark/libinput-gestures. 29 | 30 | ### INSTALLATION 31 | 32 | IMPORTANT: You must be a member of the _input_ group to have permission 33 | to read the touchpad device: 34 | 35 | sudo gpasswd -a $USER input 36 | 37 | After executing the above command, **log out of your session 38 | completely**, and then log back in to assign this group (or just 39 | reboot). 40 | 41 | NOTE: Arch users can just install [_libinput-gestures from the 42 | AUR_][AUR]. Then skip to the next CONFIGURATION section. 43 | 44 | You need python 3.4 or later, python2 is not supported. You also need 45 | libinput release 1.0 or later. Install prerequisites: 46 | 47 | # E.g. On Arch: 48 | sudo pacman -S xdotool wmctrl 49 | 50 | # E.g. On Debian based systems, e.g. Ubuntu: 51 | sudo apt-get install xdotool wmctrl 52 | 53 | # E.g. On Fedora: 54 | sudo dnf install xdotool wmctrl 55 | 56 | Debian and Ubuntu users may also need to install `libinput-tools` if 57 | that package exists in your release: 58 | 59 | sudo apt-get install libinput-tools 60 | 61 | Install this software: 62 | 63 | git clone https://github.com/bulletmark/libinput-gestures.git 64 | cd libinput-gestures 65 | sudo make install (or sudo ./libinput-gestures-setup install) 66 | 67 | ### CONFIGURATION 68 | 69 | It is helpful to start by reading the documentation about [what libinput 70 | calls gestures](https://wayland.freedesktop.org/libinput/doc/latest/gestures.html). 71 | Many users will be happy with the default configuration in which case 72 | you can just type the following and you are ready to go: 73 | 74 | libinput-gestures-setup autostart 75 | libinput-gestures-setup start 76 | 77 | Otherwise, if you want to create your own custom gestures etc, keep 78 | reading .. 79 | 80 | The default gestures are in `/etc/libinput-gestures.conf`. If you want 81 | to create your own custom gestures then copy that file to 82 | `~/.config/libinput-gestures.conf` and edit it. There are many examples 83 | and options described in that file. The available gestures are: 84 | 85 | |Gesture |Example Mapping | 86 | |------- |--------------- | 87 | |`swipe up` |GNOME/KDE/etc move to next workspace | 88 | |`swipe down` |GNOME/KDE/etc move to prev workspace | 89 | |`swipe left` |Web browser go forward | 90 | |`swipe right` |Web browser go back | 91 | |`swipe left_up` |Jump to next open web browser tab | 92 | |`swipe left_down` |Jump to previous open web browser tab | 93 | |`swipe right_up` |Close current web browser tab | 94 | |`swipe right_down` |Reopen and jump to last closed web browser tab | 95 | |`pinch in` |GNOME open/close overview | 96 | |`pinch out` |GNOME open/close overview | 97 | |`pinch clockwise` || 98 | |`pinch anticlockwise` || 99 | 100 | NOTE: If you don't use "natural" scrolling direction for your touchpad 101 | then you may want to swap the default left/right and up/down 102 | configurations. 103 | 104 | You can choose to specify a specific finger count, typically [3 or more 105 | fingers for swipe](https://wayland.freedesktop.org/libinput/doc/latest/gestures.html#swipe-gestures), 106 | and [2 or more for pinch](https://wayland.freedesktop.org/libinput/doc/latest/gestures.html#pinch-gestures). 107 | If a finger count is specified then the command is executed when exactly that 108 | number of fingers is used in the gesture. If not specified then the 109 | command is executed when that gesture is invoked with any number of 110 | fingers. Gestures specified with finger count have priority over the 111 | same gesture specified without any finger count. 112 | 113 | Of course, 2 finger swipes and taps are already interpreted by your DE 114 | and apps [for scrolling](https://wayland.freedesktop.org/libinput/doc/latest/scrolling.html#two-finger-scrolling) etc. 115 | 116 | IMPORTANT: Test the program. Check for reported errors in your custom 117 | gestures, missing packages, etc: 118 | 119 | # Ensure the program is stopped 120 | libinput-gestures-setup stop 121 | 122 | # Test to print out commands that would be executed: 123 | libinput-gestures -d 124 | ( to stop) 125 | 126 | Confirm that the correct commands are reported for your 3 finger 127 | swipe up/down/left/right gestures, and your 2 or 3 finger pinch 128 | in/out gestures. Some touchpads can also support 4 finger gestures. 129 | If you have problems then follow the TROUBLESHOOTING steps below. 130 | 131 | Apart from simple environment variable and `~` substitutions within the 132 | configured command name, `libinput-gestures` does not run the configured 133 | command under a shell so shell argument substitutions and expansions etc 134 | will not be parsed. This is for efficiency and because most don't need 135 | it. This also means your `PATH` is not respected of course so you must 136 | specify the full path to any command. If you need something more 137 | complicated, you can add your commands in an executable personal script, 138 | e.g. `~/bin/libinput-gestures.sh` e.g. with a `#!/bin/sh` shebang . Run 139 | that script by hand until you get it working then configure the script 140 | path as your command in your `libinput-gestures.conf`. 141 | 142 | In most cases, `libinput-gestures` automatically determines your 143 | touchpad device. However, you can specify it in your configuration file 144 | if needed. If you have multiple touchpads you can also specify 145 | `libinput-gestures` to use all devices. See the notes in the default 146 | `libinput-gestures.conf` file about the `device` configuration command. 147 | 148 | ### STARTING AND STOPPING 149 | 150 | Search for, and then start, the `libinput-gestures` app in your DE or 151 | you can start it immediately in the background using the command line 152 | utility: 153 | 154 | libinput-gestures-setup start 155 | 156 | You can stop the background app with: 157 | 158 | libinput-gestures-setup stop 159 | 160 | You can enable the app to start automatically in the background when you 161 | log in (on an XDG compliant DE such as GNOME and KDE) with: 162 | 163 | libinput-gestures-setup autostart 164 | 165 | You can disable the app from starting automatically with: 166 | 167 | libinput-gestures-setup autostop 168 | 169 | You can restart the app or reload the configuration file with: 170 | 171 | libinput-gestures-setup restart 172 | 173 | You can check the status of the app with: 174 | 175 | libinput-gestures-setup status 176 | 177 | ### UPGRADE 178 | 179 | # cd to source dir, as above 180 | git pull 181 | sudo make install (or sudo ./libinput-gestures-setup install) 182 | libinput-gestures-setup restart 183 | 184 | ### REMOVAL 185 | 186 | libinput-gestures-setup stop 187 | libinput-gestures-setup autostop 188 | sudo libinput-gestures-setup uninstall 189 | 190 | ### WAYLAND AND OTHER NOTES 191 | 192 | This utility exploits `xdotool` which unfortunately only works with 193 | X11/Xorg based applications. So `xdotool` shortcuts for the desktop do 194 | not work under GNOME on Wayland which is the default since GNOME 195 | 3.22. However, it is found that `wmctrl` desktop selection commands do work 196 | under GNOME on Wayland (via XWayland) so this utility adds a built-in 197 | `_internal` command which can be used to switch workspaces using the 198 | swipe commands. 199 | The `_internal` `ws_up` and `ws_down` commands use `wmctrl` to work out 200 | the current workspace and select the next one. Since this works on both 201 | Wayland and Xorg, and with GNOME, KDE, and other EWMH compliant 202 | desktops, it is the default configuration command for swipe up and 203 | down commands in `libinput-gestures.conf`. See the comments in that file 204 | about other options you can do with the `_internal` command. 205 | Unfortunately `_internal` does not work with Compiz for Ubuntu 206 | Unity desktop so also see the explicit example there for Unity. 207 | 208 | Of course, `xdotool` commands do work via XWayland for Xorg based apps 209 | so, for example, page forward/back swipe gestures do work for Firefox 210 | and Chrome browsers when running on Wayland as per the default 211 | configuration. 212 | 213 | Note that GNOME on Wayland natively implements the following gestures: 214 | 215 | - 3 finger pinch opens/close the GNOME overview. 216 | - 4 finger swipe up/down changes workspaces. 217 | 218 | So if you choose to run `libinput-gestures` on Wayland, be sure to 219 | change or disable the your `libinput-gestures.conf` pinch and swipe 220 | up/down gestures to not clash with these. E.g, configure your 221 | `libinput-gestures.conf` pinch gestures for only 2 fingers, and the 222 | swipe up/down for only 3 fingers so they work independently of the 223 | native gestures. 224 | 225 | GNOME on Xorg does not natively implement any gestures. 226 | 227 | ### EXTENDED GESTURES 228 | 229 | They are not enabled in the default `libinput-gestures.conf` 230 | configuration file but you can enable extended gestures which augment 231 | the gestures listed above in CONFIGURATION. See the commented out 232 | examples in `libinput-gestures.conf`. 233 | 234 | - `swipe right_up` (e.g. jump to next open browser tab) 235 | - `swipe left_up` (e.g. jump to previous open browser tab) 236 | - `swipe left_down` (e.g. close current browser tab) 237 | - `swipe right_down` (e.g. reopen and jump to last closed browser tab) 238 | - `pinch clockwise` 239 | - `pinch anticlockwise` 240 | 241 | So instead of just configuring the usual swipe up/down and left/right 242 | each at 90 degrees separation, you can add the above extra 4 swipes to 243 | give a total of 8 swipe gestures each at 45 degrees separation. It works 244 | better than you may expect, at least after some practice. It means you 245 | can completely manage browser tabs from your touchpad. 246 | 247 | ### TROUBLESHOOTING 248 | 249 | Please don't raise a github issue but provide little information about 250 | your problem, and please don't raise an issue until you have considered 251 | all the following steps. **If you raise an issue ALWAYS include the 252 | output of `libinput-gestures -l` to show the environment and 253 | configuration you are using, regardless of what the issue is about**. 254 | 255 | 1. Ensure you are running the latest version from the 256 | [libinput-gestures github repository][REPO] or from the [Arch AUR][AUR]. 257 | 258 | 2. Ensure you have followed the installation instructions here 259 | carefully. The most common mistake is that you have not added your 260 | user to the _input_ group and re-logged in as described above. 261 | 262 | 3. Perhaps temporarily remove your custom configuration to try with the 263 | default configuration. 264 | 265 | 4. Run `libinput-gestures` on the command line in debug mode while 266 | performing some 3 and 4 finger left/right/up/down swipes, and some 267 | pinch in/outs. In debug mode, configured commands are not executed, 268 | they are merely output to the screen: 269 | ```` 270 | libinput-gestures-setup stop 271 | libinput-gestures -d 272 | ( to stop) 273 | ```` 274 | 275 | 5. Run `libinput-gestures` in raw mode by repeating the same commands as 276 | above step but use the `-r` (`--raw`) switch instead of `-d` 277 | (`--debug`). Raw mode does nothing more than echo the raw gesture 278 | events received from `libinput debug-events`. If you see `POINTER_*` 279 | events but no `GESTURE_*` events then unfortunately your touchpad 280 | and/or libinput combination can report simple finger movements but 281 | does not report multi-finger gestures so `libinput-gestures` will not 282 | work. Also note that discrimination of `SWIPE` and `PINCH` gestures 283 | is done completely within libinput, before they get to 284 | `libinput-gestures`. 285 | 286 | 6. Search the web for Linux kernel and/or libinput issues relating to 287 | your specific touchpad device and/or laptop/pc. Update your BIOS if 288 | possible. 289 | 290 | 7. Be sure that a configured external command works exactly how you want 291 | when you run it directly on the command line, **before** you configure 292 | it for `libinput-gestures`. E.g. run `xdotool` manually and 293 | experiment with various arguments to work out exactly what arguments 294 | it requires to do what you want, and only then add that command + 295 | arguments to your custom configuration in 296 | `~/.config/libinput-gestures.conf`. Clearly, if the your manual 297 | `xdotool` command does not work correctly then there is no point 298 | raising an `libinput-gestures` issue about it! 299 | 300 | 8. **If you raise an issue, always include the output of 301 | `libinput-gestures -l` to show the environment and configuration you 302 | are using**. If appropriate, also paste the output from steps 4 and 5 303 | above. If your device is not being recognised by `libinput-gestures` 304 | at all, paste the complete output of `libinput list-devices` 305 | (`libinput-list-devices` on libinput < v1.8). 306 | 307 | ### LICENSE 308 | 309 | Copyright (C) 2015 Mark Blakeney. This program is distributed under the 310 | terms of the GNU General Public License. 311 | This program is free software: you can redistribute it and/or modify it 312 | under the terms of the GNU General Public License as published by the 313 | Free Software Foundation, either version 3 of the License, or any later 314 | version. 315 | This program is distributed in the hope that it will be useful, but 316 | WITHOUT ANY WARRANTY; without even the implied warranty of 317 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General 318 | Public License at for more details. 319 | 320 | [REPO]: https://github.com/bulletmark/libinput-gestures/ 321 | [AUR]: https://aur.archlinux.org/packages/libinput-gestures/ 322 | [XDOTOOL]: https://www.semicomplete.com/projects/xdotool/ 323 | 324 | 325 | -------------------------------------------------------------------------------- /internal: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 'Command line program to exercise/test/debug the _internal command.' 3 | # Mark Blakeney, Oct 2019 4 | import sys, importlib, argparse 5 | from pathlib import Path 6 | 7 | CMD = '_internal' 8 | GESTURE = 'swipe' 9 | PROG = Path(sys.argv[0]).resolve() 10 | NAME = Path.cwd().name 11 | CACHE = Path.home() / '.cache' / PROG.name 12 | 13 | def import_path(path, add_to_path=False): 14 | 'Import given module path' 15 | modname = Path(path).stem.replace('-', '_') 16 | spec = importlib.util.spec_from_loader(modname, 17 | importlib.machinery.SourceFileLoader(modname, path)) 18 | module = importlib.util.module_from_spec(spec) 19 | spec.loader.exec_module(module) 20 | if add_to_path: 21 | sys.modules[modname] = module 22 | return module 23 | 24 | opt = argparse.ArgumentParser(description=__doc__) 25 | opt.add_argument('-c', '--conffile', 26 | help='alternative configuration file') 27 | opt.add_argument('-i', '--initial', type=int, 28 | help='initial desktop') 29 | opt.add_argument('-C', '--cols', type=int, default=1, 30 | help='number of columns') 31 | opt.add_argument('-t', '--text', action='store_true', 32 | help='output desktop change in text') 33 | opt.add_argument('-n', '--nocache', action='store_true', 34 | help='do not use cache') 35 | opt.add_argument('num', type=int, 36 | help='number of desktops') 37 | opt.add_argument('action', nargs='?', 38 | help='action to perform') 39 | opt.add_argument('-d', '--display', type=int, 40 | help=argparse.SUPPRESS) 41 | opt.add_argument('-s', '--set', type=int, help=argparse.SUPPRESS) 42 | args = opt.parse_args() 43 | 44 | def showgrid(pos, num, cols): 45 | print() 46 | for i in range(num): 47 | end = '\n' if (i % cols) == (cols - 1) else '' 48 | if i == pos: 49 | print(' {:02} '.format(i), end=end) 50 | else: 51 | print(' ** ', end=end) 52 | if end != '\n': 53 | print() 54 | 55 | if args.set is not None: 56 | print(args.set) 57 | sys.exit(0) 58 | 59 | if args.display is not None: 60 | for i in range(args.num): 61 | print('{} {} -'.format(i, '*' if i == args.display else '-')) 62 | sys.exit(0) 63 | 64 | if args.initial is None: 65 | if args.nocache: 66 | opt.error('Initial value must be specified') 67 | if not CACHE.exists(): 68 | opt.error('Need initial desktop specified') 69 | start = int(CACHE.read_text().strip()) 70 | else: 71 | start = args.initial 72 | 73 | lg = import_path(NAME) 74 | icmd = lg.internal_commands[CMD] 75 | 76 | icmd.CMDLIST = '{} -d {} {}'.format(PROG, start, args.num).split() 77 | icmd.CMDSET = '{} {} -s'.format(PROG, args.num).split() 78 | 79 | lg.read_conf(args.conffile, NAME + '.conf') 80 | motions = lg.handlers[GESTURE.upper()].motions 81 | actions = {k: v for k, v in motions.items() if isinstance(v, icmd)} 82 | 83 | if not args.action or args.action not in actions: 84 | opt.error('action must be one of {}'.format(list(actions.keys()))) 85 | 86 | cmd = motions[args.action] 87 | print('Command "{} {} is "{}"'.format(GESTURE, args.action, cmd)) 88 | res = cmd.run(block=True) 89 | 90 | if res: 91 | end = int(res.strip()) 92 | if not args.nocache: 93 | CACHE.write_text(str(end)) 94 | else: 95 | end = start 96 | 97 | if end < 0 or end >= args.num: 98 | sys.exit('Desktop change from {} to {}, out of range 0 to <{}!'.format( 99 | start, end, args.num)) 100 | 101 | if args.text: 102 | if start != end: 103 | print('Desktop change from {} to {}'.format(start, end)) 104 | else: 105 | print('No change') 106 | else: 107 | showgrid(start, args.num, args.cols) 108 | showgrid(end, args.num, args.cols) 109 | -------------------------------------------------------------------------------- /internal-test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 'Command line utility to run test suite for internal command.' 3 | # Mark Blakeney, Oct 2019 4 | import sys, tempfile, subprocess, argparse 5 | 6 | # flake8: noqa: E122 7 | TESTS = ( 8 | ##### 9 | ( 10 | '1 column, up/down, no wrap', 11 | ''' 12 | gesture swipe up _internal ws_up 13 | gesture swipe down _internal ws_down 14 | ''', ( 15 | '3 desktops', 16 | ('up', 1, 3), 17 | ('up', 2), 18 | ('up', 2), 19 | ('down', 1), 20 | ('down', 0), 21 | ('down', 0), 22 | ), 23 | ), 24 | ##### 25 | ( 26 | '1 column, up/down, with wrap', 27 | ''' 28 | gesture swipe up _internal -w ws_up 29 | gesture swipe down _internal -w ws_down 30 | ''', ( 31 | '3 desktops', 32 | ('up', 1, 3), 33 | ('up', 2), 34 | ('up', 0), 35 | ('up', 1), 36 | ('up', 2), 37 | ('up', 0), 38 | ('down', 2), 39 | ('down', 1), 40 | ('down', 0), 41 | ('down', 2), 42 | ), 43 | ), 44 | ##### 45 | ( 46 | '1 row, left/right, no wrap', 47 | ''' 48 | gesture swipe left _internal ws_left 49 | gesture swipe right _internal ws_right 50 | ''', ( 51 | '3 desktops', 52 | ('left', 1, 3), 53 | ('left', 2), 54 | ('left', 2), 55 | ('right', 1), 56 | ('right', 0), 57 | ('right', 0), 58 | ), 59 | ), 60 | ##### 61 | ( 62 | '1 row, left/right, no wrap, configured as columns', 63 | ''' 64 | gesture swipe left _internal -c 3 ws_left 65 | gesture swipe right _internal -c 3 ws_right 66 | ''', ( 67 | '3 desktops', 68 | ('left', 1, 3), 69 | ('left', 2), 70 | ('left', 2), 71 | ('right', 1), 72 | ('right', 0), 73 | ('right', 0), 74 | ), 75 | ), 76 | ##### 77 | ( 78 | '1 row, left/right, with wrap', 79 | ''' 80 | gesture swipe left _internal -w ws_left 81 | gesture swipe right _internal -w ws_right 82 | ''', ( 83 | '3 desktops', 84 | ('left', 1, 3), 85 | ('left', 2), 86 | ('left', 0), 87 | ('left', 1), 88 | ('left', 2), 89 | ('left', 0), 90 | ('right', 2), 91 | ('right', 1), 92 | ('right', 0), 93 | ('right', 2), 94 | ), 95 | ), 96 | ##### 97 | ( 98 | '1 row, left/right, with wrap, configured as columns', 99 | ''' 100 | gesture swipe left _internal -w -c 3 ws_left 101 | gesture swipe right _internal -w -c 3 ws_right 102 | ''', ( 103 | '3 desktops', 104 | ('left', 1, 3), 105 | ('left', 2), 106 | ('left', 0), 107 | ('left', 1), 108 | ('left', 2), 109 | ('left', 0), 110 | ('right', 2), 111 | ('right', 1), 112 | ('right', 0), 113 | ('right', 2), 114 | ), 115 | ), 116 | ##### 117 | ( 118 | '4 column, up/down/left/right, no wrap', 119 | ''' 120 | gesture swipe up _internal -c 4 ws_up 121 | gesture swipe down _internal -c 4 ws_down 122 | gesture swipe left _internal -c 4 ws_left 123 | gesture swipe right _internal -c 4 ws_right 124 | ''', ( 125 | '12 desktops', 126 | ('up', 4, 12), 127 | ('up', 8), 128 | ('up', 8), 129 | ('down', 4), 130 | ('down', 0), 131 | ('down', 0), 132 | ('left', 1), 133 | ('left', 2), 134 | ('left', 3), 135 | ('left', 3), 136 | ('right', 2), 137 | ('right', 1), 138 | ('right', 0), 139 | ('right', 0), 140 | ('left', 1), 141 | ('up', 5), 142 | ('up', 9), 143 | ('up', 9), 144 | ('left', 10), 145 | ('up', 10), 146 | ('down', 6), 147 | ('down', 2), 148 | ('down', 2), 149 | ('left', 3), 150 | ('up', 7), 151 | ('up', 11), 152 | ('up', 11), 153 | '10 desktops', 154 | ('up', 4, 10), 155 | ('up', 8), 156 | ('up', 8), 157 | ('left', 9), 158 | ('left', 9), 159 | ('down', 5), 160 | ('left', 6), 161 | ('up', 6), 162 | ('left', 7), 163 | ('left', 7), 164 | ('up', 7), 165 | ('down', 3), 166 | ('down', 3), 167 | ), 168 | ), 169 | ##### 170 | ( 171 | '4 column, up/down/left/right, no wrap, legacy config', 172 | ''' 173 | gesture swipe up _internal --col=3 ws_up 174 | gesture swipe down _internal --col=3 ws_down 175 | gesture swipe left _internal --row=4 ws_up 176 | gesture swipe right _internal --row=4 ws_down 177 | ''', ( 178 | '12 desktops', 179 | ('up', 4, 12), 180 | ('up', 8), 181 | ('up', 8), 182 | ('down', 4), 183 | ('down', 0), 184 | ('down', 0), 185 | ('left', 1), 186 | ('left', 2), 187 | ('left', 3), 188 | ('left', 3), 189 | ('right', 2), 190 | ('right', 1), 191 | ('right', 0), 192 | ('right', 0), 193 | ('left', 1), 194 | ('up', 5), 195 | ('up', 9), 196 | ('up', 9), 197 | ('left', 10), 198 | ('up', 10), 199 | ('down', 6), 200 | ('down', 2), 201 | ('down', 2), 202 | ('left', 3), 203 | ('up', 7), 204 | ('up', 11), 205 | ('up', 11), 206 | ), 207 | ), 208 | ##### 209 | ( 210 | '4 column, up/down/left/right, with wrap', 211 | ''' 212 | gesture swipe up _internal -w -c 4 ws_up 213 | gesture swipe down _internal -w -c 4 ws_down 214 | gesture swipe left _internal -w -c 4 ws_left 215 | gesture swipe right _internal -w -c 4 ws_right 216 | ''', ( 217 | '12 desktops', 218 | ('up', 4, 12), 219 | ('up', 8), 220 | ('up', 0), 221 | ('down', 8), 222 | ('down', 4), 223 | ('down', 0), 224 | ('down', 8), 225 | ('up' , 0), 226 | ('right', 3), 227 | ('left', 0), 228 | ('left', 1), 229 | ('left', 2), 230 | ('left', 3), 231 | ('left', 0), 232 | ('right', 3), 233 | ('up', 7), 234 | ('up', 11), 235 | ('up', 3), 236 | '10 desktops', 237 | ('up', 4, 10), 238 | ('up', 8), 239 | ('up', 0), 240 | ('down', 8), 241 | ('left', 9), 242 | ('left', 8), 243 | ('left', 9), 244 | ('up', 1), 245 | ('up', 5), 246 | ('up', 9), 247 | ('down', 5), 248 | ('left', 6), 249 | ('left', 7), 250 | ('left', 4), 251 | ('right', 7), 252 | ('right', 6), 253 | ('up', 2), 254 | ('down', 6), 255 | ), 256 | ), 257 | ##### 258 | ( 259 | '4 column, up/down/left/right, with wrap, legacy config', 260 | ''' 261 | gesture swipe up _internal -w --col=3 ws_up 262 | gesture swipe down _internal -w --col=3 ws_down 263 | gesture swipe left _internal -w --row=4 ws_up 264 | gesture swipe right _internal -w --row=4 ws_down 265 | ''', ( 266 | '12 desktops', 267 | ('up', 4, 12), 268 | ('up', 8), 269 | ('up', 0), 270 | ('down', 8), 271 | ('down', 4), 272 | ('down', 0), 273 | ('down', 8), 274 | ('up' , 0), 275 | ('right', 3), 276 | ('left', 0), 277 | ('left', 1), 278 | ('left', 2), 279 | ('left', 3), 280 | ('left', 0), 281 | ('right', 3), 282 | ('up', 7), 283 | ('up', 11), 284 | ('up', 3), 285 | ('down', 11), 286 | ), 287 | ), 288 | ##### 289 | ( 290 | '4 column, up/down/left/right & diagonal, no wrap', 291 | ''' 292 | gesture swipe up _internal -c 4 ws_up 293 | gesture swipe down _internal -c 4 ws_down 294 | gesture swipe left _internal -c 4 ws_left 295 | gesture swipe right _internal -c 4 ws_right 296 | gesture swipe left_up _internal -c 4 ws_left_up 297 | gesture swipe left_down _internal -c 4 ws_left_down 298 | gesture swipe right_up _internal -c 4 ws_right_up 299 | gesture swipe right_down _internal -c 4 ws_right_down 300 | ''', ( 301 | '12 desktops', 302 | ('left_up', 5, 12), 303 | ('left_up', 10), 304 | ('right_down', 5), 305 | ('right_down', 0), 306 | ('left', 1), 307 | ('left_up', 6), 308 | ('left_up', 11), 309 | ('down', 7), 310 | ('right_down', 2), 311 | ('left', 3), 312 | ('left', 3), 313 | ('right_up', 6), 314 | ('right_up', 9), 315 | ('right_up', 9), 316 | '10 desktops', 317 | ('left_up', 5, 10), 318 | ('left_up', 5), 319 | ('right_down', 0), 320 | ('left', 1), 321 | ('left_up', 6), 322 | ('left_up', 6), 323 | ('left_down', 3), 324 | ('up', 7), 325 | ('right_up', 7), 326 | ('right_down', 2), 327 | ('right_up', 5), 328 | ('right_up', 8), 329 | ('right_up', 8), 330 | '16 desktops', 331 | ('left_up', 5, 16), 332 | ('left_up', 10), 333 | ('left_up', 15), 334 | ('left_up', 15), 335 | ('right_down', 10), 336 | ('right_down', 5), 337 | ('right_down', 0), 338 | ('right_down', 0), 339 | ('up', 4), 340 | ('up', 8), 341 | ('up', 12), 342 | ('right_down', 12), 343 | ('left_down', 9), 344 | ('left_down', 6), 345 | ('left_down', 3), 346 | ('left_down', 3), 347 | ('right_up', 6), 348 | ('right_up', 9), 349 | ('right_up', 12), 350 | ), 351 | ), 352 | ##### 353 | ( 354 | '4 column, up/down/left/right & diagonal, with wrap', 355 | ''' 356 | gesture swipe up _internal -w -c 4 ws_up 357 | gesture swipe down _internal -w -c 4 ws_down 358 | gesture swipe left _internal -w -c 4 ws_left 359 | gesture swipe right _internal -w -c 4 ws_right 360 | gesture swipe left_up _internal -w -c 4 ws_left_up 361 | gesture swipe left_down _internal -w -c 4 ws_left_down 362 | gesture swipe right_up _internal -w -c 4 ws_right_up 363 | gesture swipe right_down _internal -w -c 4 ws_right_down 364 | ''', ( 365 | '12 desktops', 366 | ('left_up', 5, 12), 367 | ('left_up', 10), 368 | ('left_up', 3), 369 | ('left_up', 4), 370 | ('left_up', 9), 371 | ('left_up', 2), 372 | ('left_up', 7), 373 | ('down', 3), 374 | ('right_up', 6), 375 | ('right_up', 9), 376 | ('right_up', 0), 377 | '10 desktops', 378 | ('left_up', 5, 10), 379 | ('left_up', 2), 380 | ('left', 3), 381 | ('right_up', 6), 382 | ('right_up', 9), 383 | ('right_up', 0), 384 | '16 desktops', 385 | ('left_up', 5, 16), 386 | ('left_up', 10), 387 | ('left_up', 15), 388 | ('left_up', 0), 389 | ('right_down', 15), 390 | ('right_down', 10), 391 | ('right_down', 5), 392 | ('right_down', 0), 393 | ('right_down', 15), 394 | ('left_up', 0), 395 | ('up', 4), 396 | ('up', 8), 397 | ('up', 12), 398 | ('right_up', 3), 399 | ('right_up', 6), 400 | ('right_up', 9), 401 | ('right_up', 12), 402 | ), 403 | ), 404 | ) 405 | 406 | PROG = 'internal' 407 | 408 | opt = argparse.ArgumentParser(description=__doc__) 409 | opt.add_argument('-q', '--quiet', action='store_true', 410 | help='quiet output') 411 | opt.add_argument('-l', '--list', action='store_true', 412 | help='list test sets') 413 | opt.add_argument('testset', nargs='*', type=int, 414 | help='test sets to execute, default all {}'.format(len(TESTS))) 415 | args = opt.parse_args() 416 | 417 | lastnum = 0 418 | lastpos = 0 419 | 420 | def test(fname, action, end, num=-1, start=-1): 421 | global lastnum, lastpos 422 | if num < 0: 423 | num = lastnum 424 | else: 425 | lastpos = 0 426 | 427 | if start < 0: 428 | start = lastpos 429 | 430 | lastnum = num 431 | lastpos = end 432 | 433 | cmd = './{} -n -t -c {} -i {} {} {}'.format(PROG, fname, start, num, action) 434 | res = subprocess.run(cmd.split(), universal_newlines=True, 435 | stdout=subprocess.PIPE) 436 | if res.returncode != 0: 437 | return 'Command failed' 438 | out = res.stdout.strip().splitlines()[-1].split()[-1] 439 | res_end = int(out) if out.isdigit() else start 440 | return 'should be {}'.format(res_end) if res_end != end else None 441 | 442 | tmpfile = tempfile.NamedTemporaryFile('r+t') 443 | sets = 0 444 | total = 0 445 | bad = 0 446 | 447 | for testset, (title, confstr, tests) in enumerate(TESTS, 1): 448 | if args.testset and testset not in args.testset: 449 | continue 450 | sets += 1 451 | confstr = confstr.strip() 452 | tmpfile.seek(0) 453 | tmpfile.write(confstr + '\n') 454 | tmpfile.truncate() 455 | tmpfile.flush() 456 | lastpos = 0 457 | lastnum = 0 458 | print('======= Set {}: {}:\n{}'.format(testset, title, confstr)) 459 | for fargs in tests: 460 | if isinstance(fargs, str): 461 | print(' ###### Subset {}:'.format(fargs)) 462 | continue 463 | total += 1 464 | 465 | if args.list: 466 | continue 467 | 468 | res = test(tmpfile.name, *fargs) 469 | if res: 470 | print(' FAILED: {}: {}'.format(fargs, res)) 471 | bad += 1 472 | elif not args.quiet: 473 | print(' ok : {}'.format(fargs)) 474 | 475 | print('\nTotal sets = {}, tests = {}, failed = {}'.format(sets, total, bad)) 476 | sys.exit(0 if bad == 0 else 1) 477 | -------------------------------------------------------------------------------- /libinput-gestures: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 'Read gestures from libinput touchpad and action shell commands.' 3 | # Mark Blakeney, Sep 2015 4 | import os 5 | import sys 6 | import argparse 7 | import subprocess 8 | import shlex 9 | import re 10 | import getpass 11 | import fcntl 12 | import platform 13 | import math 14 | import signal 15 | from time import monotonic 16 | from collections import OrderedDict 17 | from pathlib import Path 18 | from distutils.version import LooseVersion as Version 19 | 20 | PROGPATH = Path(sys.argv[0]) 21 | PROGNAME = PROGPATH.stem 22 | 23 | # Conf file containing gesture commands. 24 | # Search first for user file then system file. 25 | CONFNAME = '{}.conf'.format(PROGNAME) 26 | USERDIR = os.getenv('XDG_CONFIG_HOME', os.path.expanduser('~/.config')) 27 | CONFDIRS = (USERDIR, '/etc') 28 | 29 | # Ratio of X/Y (or Y/X) at which we consider an oblique swipe gesture. 30 | # The number is the trigger angle in degrees and set to 360/8/2. 31 | OBLIQUE_RATIO = math.tan(math.radians(22.5)) 32 | 33 | # Default minimum significant distance to move for swipes, in dots. 34 | # Can be changed using configuration command. 35 | swipe_min_threshold = 0 36 | 37 | args = None 38 | abzsquare = None 39 | 40 | # Timeout on gesture action from start to end. 0 = no timeout. In secs. 41 | # Can be changed using configuration command. 42 | DEFAULT_TIMEOUT = 1.5 43 | timeoutv = DEFAULT_TIMEOUT 44 | 45 | # Rotation threshold in degrees to discriminate pinch rotate from in/out 46 | ROTATE_ANGLE = 15.0 47 | 48 | def open_lock(*args): 49 | 'Create a lock based on given list of arguments' 50 | # We use exclusive assess to a file for this 51 | fp = Path('/tmp', '-'.join(args) + '.lock').open('w') 52 | try: 53 | fcntl.lockf(fp, fcntl.LOCK_EX | fcntl.LOCK_NB) 54 | except IOError: 55 | return None 56 | 57 | return fp 58 | 59 | def run(cmd, *, check=True, block=True): 60 | 'Run function and return standard output, Popen() handle, or None' 61 | # Maintain subprocess compatibility with python 3.4 so use 62 | # check_output() rather than run(). 63 | try: 64 | if block: 65 | result = subprocess.check_output(cmd, universal_newlines=True, 66 | stderr=(None if check else subprocess.DEVNULL)) 67 | else: 68 | result = subprocess.Popen(cmd) 69 | except Exception as e: 70 | result = None 71 | if check: 72 | print(str(e), file=sys.stderr) 73 | 74 | return result 75 | 76 | def get_libinput_vers(): 77 | 'Return the libinput installed version number string' 78 | # Try to use newer libinput interface then fall back to old 79 | # (depreciated) interface. 80 | res = run(('libinput', '--version'), check=False) 81 | return res.strip() if res else \ 82 | run(('libinput-list-devices', '--version'), check=False) 83 | 84 | def get_devices_list(cmd_list_devices, device_list): 85 | 'Get list of devices and their attributes (as a dict) from libinput' 86 | if device_list: 87 | with open(device_list) as fd: 88 | stdout = fd.read() 89 | else: 90 | stdout = run(cmd_list_devices.split()) 91 | 92 | if stdout: 93 | dev = {} 94 | for line in stdout.splitlines(): 95 | line = line.strip() 96 | if line and ':' in line: 97 | key, value = line.split(':', maxsplit=1) 98 | dev[key.strip().lower()] = value.strip() 99 | elif dev: 100 | yield dev 101 | dev = {} 102 | 103 | # Ensure we include last device 104 | if dev: 105 | yield dev 106 | 107 | def get_device_info(name, cmd_list_devices, device_list): 108 | 'Determine libinput touchpad device and return device info' 109 | devices = list(get_devices_list(cmd_list_devices, device_list)) 110 | 111 | if not devices: 112 | print('Can not see any devices, did you add yourself to the ' 113 | 'input group and log out/in?', file=sys.stderr) 114 | return None 115 | 116 | # If a specific device name was asked for then return that device 117 | # This is the "Device" name from libinput list-devices command. 118 | if name: 119 | kdev = str(Path(name).resolve()) if name[0] == '/' else None 120 | for d in devices: 121 | # If the device name starts with a '/' then it is instead 122 | # considered as the explicit device path although since 123 | # device paths can change through reboots this is best to be 124 | # a symlink. E.g. users should use the corresponding full 125 | # path link under /dev/input/by-path/ or /dev/input/by-id/. 126 | if kdev: 127 | if d.get('kernel') == kdev: 128 | return d 129 | elif d.get('device') == name: 130 | return d 131 | return None 132 | 133 | # Otherwise look for 1st device with touchpad capabilities 134 | for d in devices: 135 | if 'size' in d and 'pointer' in d.get('capabilities'): 136 | return d 137 | # Otherwise look for 1st device with touchpad in it's name 138 | # or, failing that, 1st device with trackpad in it's name 139 | for txt in ('touch ?pad', 'track ?pad'): 140 | for d in devices: 141 | if re.search(txt, d.get('device', ''), re.I): 142 | return d 143 | 144 | # Give up 145 | return None 146 | 147 | def get_device(name, cmd_list_devices, device_list): 148 | 'Determine libinput touchpad device and add fixed path info' 149 | dev = get_device_info(name, cmd_list_devices, device_list) 150 | if dev: 151 | devname = dev.get('kernel') 152 | evname = '' 153 | if devname: 154 | devpath = Path(devname) 155 | 156 | # Also determine and prefer a non-volatile path merely 157 | # because it is more identifying for users. 158 | for dirstr in ('/dev/input/by-path', '/dev/input/by-id'): 159 | dirpath = Path(dirstr) 160 | if dirpath.exists(): 161 | for path in dirpath.iterdir(): 162 | if path.resolve() == devpath: 163 | devname = str(path) 164 | evname = '({})'.format(devpath.name) 165 | break 166 | if evname: 167 | break 168 | 169 | dev['_path'] = devname 170 | dev['_diag'] = '{}{}: {}'.format(devname, evname, 171 | dev.get('device', '?')) 172 | return dev 173 | 174 | class COMMAND: 175 | 'Generic command handler' 176 | def __init__(self, args): 177 | self.reprstr = ' '.join(args) 178 | 179 | # Expand '~' and env vars in executable command name 180 | args[0] = os.path.expandvars(os.path.expanduser(args[0])) 181 | self.argslist = args 182 | 183 | def run(self): 184 | 'Run this command + arguments' 185 | run(self.argslist, block=False) 186 | 187 | def __str__(self): 188 | 'Return string representation' 189 | return self.reprstr 190 | 191 | # Table of internal commands 192 | internal_commands = OrderedDict() 193 | 194 | def add_internal_command(cls): 195 | 'Add configuration command to command lookup table based on name' 196 | internal_commands[re.sub('^COMMAND', '', cls.__name__)] = cls 197 | 198 | class ArgumentParser(argparse.ArgumentParser): 199 | 'Custom ArgumentParser to return error text' 200 | def error(self, msg): 201 | raise(Exception(msg)) 202 | 203 | @add_internal_command 204 | class COMMAND_internal(COMMAND): 205 | 'Internal command handler.' 206 | # Commands currently supported follow. Each is configured with the 207 | # (X,Y) translation to be applied to the desktop grid. 208 | commands = ( 209 | ('ws_up', ( 0, 1)), # noqa: E241,E201 210 | ('ws_down', ( 0, -1)), # noqa: E241,E201 211 | ('ws_left', ( 1, 0)), # noqa: E241,E201 212 | ('ws_right', (-1, 0)), # noqa: E241,E201 213 | ('ws_left_up', ( 1, 1)), # noqa: E241,E201 214 | ('ws_left_down', ( 1, -1)), # noqa: E241,E201 215 | ('ws_right_up', (-1, 1)), # noqa: E241,E201 216 | ('ws_right_down', (-1, -1)), # noqa: E241,E201 217 | ) 218 | 219 | commands_list = [c[0] for c in commands] 220 | 221 | CMDLIST = 'wmctrl -d'.split() 222 | CMDSET = 'wmctrl -s'.split() 223 | 224 | def __init__(self, args): 225 | 'Action internal swipe commands' 226 | super().__init__(args) 227 | 228 | # Set up command line arguments 229 | opt = ArgumentParser(prog=self.argslist[0], description=self.__doc__) 230 | opt.add_argument('-w', '--wrap', action='store_true', 231 | help='wrap workspaces when switching to/from start/end') 232 | opt.add_argument('-c', '--cols', type=int, 233 | help='number of columns in virtual desktop grid, default=1') 234 | opt.add_argument('--row', type=int, default=0, help=argparse.SUPPRESS) 235 | opt.add_argument('--col', type=int, default=0, help=argparse.SUPPRESS) 236 | opt.add_argument('action', choices=self.commands_list, 237 | help='Internal command to action') 238 | args = opt.parse_args(self.argslist[1:]) 239 | self.nowrap = not args.wrap 240 | self.rows = 0 241 | self.cols = 0 242 | cmdi = self.commands_list.index(args.action) 243 | 244 | if cmdi >= 2: 245 | if args.row or args.col: 246 | opt.error('Legacy "--row" and "--col" not supported') 247 | if args.cols is None: 248 | if cmdi < 4: 249 | self.cols = 1 250 | cmdi -= 2 251 | else: 252 | opt.error('"--cols" must be specified') 253 | elif args.cols < 1: 254 | opt.error('"--cols" must be >= 1') 255 | else: 256 | self.cols = args.cols 257 | else: 258 | # Convert old legacy/depreciated arguments to new arguments 259 | if args.cols is not None: 260 | if args.cols < 1: 261 | opt.error('"--cols" must be >= 1') 262 | self.cols = args.cols 263 | elif args.row: 264 | cmdi += 2 265 | self.cols = args.row 266 | elif args.col: 267 | self.rows = args.col 268 | else: 269 | self.cols = 1 270 | 271 | # Save the translations appropriate to this command 272 | self.xmove, self.ymove = self.commands[cmdi][1] 273 | 274 | def run(self, block=False): 275 | 'Get list of current workspaces and select next one' 276 | stdout = run(self.CMDLIST, check=False) 277 | if not stdout: 278 | # This command can fail on GNOME when you have only a single 279 | # dynamic workspace using Xorg (probably a GNOME bug) so let's 280 | # just ignore it given there is no other workspace to switch to 281 | # anyhow. 282 | return 283 | 284 | # Parse the output of above command 285 | lines = [l.split(maxsplit=2)[1] for l in stdout.strip().splitlines()] 286 | start = index = lines.index('*') 287 | num = len(lines) 288 | cols = self.cols or num // self.rows 289 | numv = ((num - 1) // cols + 1) * cols 290 | 291 | # Calculate new workspace X direction index 292 | count = self.xmove 293 | if count < 0: 294 | if index % cols == 0: 295 | if self.nowrap: 296 | return 297 | index += cols - 1 298 | if index >= num: 299 | if self.ymove == 0: 300 | if self.nowrap: 301 | return 302 | index = num - 1 303 | else: 304 | index += count 305 | elif count > 0: 306 | index += count 307 | if index % cols == 0: 308 | if self.nowrap: 309 | return 310 | index -= cols 311 | elif index >= num: 312 | if self.ymove == 0: 313 | if self.nowrap: 314 | return 315 | index -= numv - index 316 | 317 | # Calculate new workspace Y direction index 318 | count = self.ymove * cols 319 | if count < 0: 320 | if index < cols and self.nowrap: 321 | return 322 | index = (index + count) % numv 323 | if index >= num: 324 | index += count 325 | elif count > 0: 326 | index += count 327 | if index >= numv: 328 | if self.nowrap: 329 | return 330 | index = index % numv 331 | elif index >= num: 332 | if self.nowrap: 333 | return 334 | index = (index + count) % numv 335 | 336 | # Switch to desired workspace 337 | return run(self.CMDSET + [str(index)], block=block) \ 338 | if index != start else None 339 | 340 | # Table of gesture handlers 341 | handlers = OrderedDict() 342 | 343 | def add_gesture_handler(cls): 344 | 'Create gesture handler instance and add to lookup table based on name' 345 | handlers[cls.__name__] = cls() 346 | 347 | class GESTURE: 348 | 'Abstract base class for handling for gestures' 349 | def __init__(self): 350 | 'Initialise this gesture at program start' 351 | self.name = type(self).__name__ 352 | self.motions = OrderedDict() 353 | self.has_extended = False 354 | 355 | def add(self, motion, fingers, command): 356 | 'Add a configured motion command for this gesture' 357 | if motion not in self.SUPPORTED_MOTIONS: 358 | return 'Gesture {} does not support motion "{}".\n' \ 359 | 'Must be "{}"'.format(self.name.lower(), motion, 360 | '" or "'.join(self.SUPPORTED_MOTIONS)) 361 | if not command: 362 | return 'No command configured' 363 | 364 | # If any extended gestures configured then set flag to enable 365 | # their discrimination 366 | if self.extended_text in motion: 367 | self.has_extended = True 368 | 369 | key = (motion, fingers) if fingers else motion 370 | 371 | try: 372 | cmds = shlex.split(command) 373 | except Exception as e: 374 | return str(e) 375 | 376 | cls = internal_commands.get(cmds[0], COMMAND) 377 | 378 | try: 379 | self.motions[key] = cls(cmds) 380 | except Exception as e: 381 | return str(e) 382 | 383 | return None 384 | 385 | def begin(self, fingers): 386 | 'Initialise this gesture at the start of motion' 387 | self.fingers = fingers 388 | self.data = [0.0, 0.0] 389 | self.starttime = monotonic() 390 | 391 | def action(self, motion): 392 | 'Action a motion command for this gesture' 393 | command = self.motions.get((motion, self.fingers)) or \ 394 | self.motions.get(motion) 395 | 396 | if args.verbose: 397 | print('{}: {} {} {} {}'.format(PROGNAME, self.name, motion, 398 | self.fingers, self.data)) 399 | if command: 400 | print(' ', command) 401 | 402 | if timeoutv > 0 and (self.starttime + timeoutv) < monotonic(): 403 | if args.verbose: 404 | print(' ', 'timeout - no action') 405 | return 406 | 407 | if command and not args.debug: 408 | command.run() 409 | 410 | @add_gesture_handler 411 | class SWIPE(GESTURE): 412 | 'Class to handle this type of gesture' 413 | SUPPORTED_MOTIONS = ('left', 'right', 'up', 'down', 414 | 'left_up', 'right_up', 'left_down', 'right_down') 415 | extended_text = '_' 416 | 417 | def update(self, coords): 418 | 'Update this gesture for a motion' 419 | # Ignore this update if we can not parse the numbers we expect 420 | try: 421 | x = float(coords[2]) 422 | y = float(coords[3]) 423 | except (ValueError, IndexError): 424 | return False 425 | 426 | self.data[0] += x 427 | self.data[1] += y 428 | return True 429 | 430 | def end(self): 431 | 'Action this gesture at the end of a motion sequence' 432 | x, y = self.data 433 | abx = abs(x) 434 | aby = abs(y) 435 | 436 | # Require absolute distance movement beyond a small thresh-hold. 437 | if abx**2 + aby**2 < abzsquare: 438 | return 439 | 440 | # Discriminate left/right or up/down. 441 | # If significant movement in both planes the consider it a 442 | # oblique swipe (but only if any are configured) 443 | if abx > aby: 444 | motion = 'left' if x < 0 else 'right' 445 | if self.has_extended and abx > 0 and aby / abx > OBLIQUE_RATIO: 446 | motion += '_up' if y < 0 else '_down' 447 | else: 448 | motion = 'up' if y < 0 else 'down' 449 | if self.has_extended and aby > 0 and abx / aby > OBLIQUE_RATIO: 450 | motion = ('left_' if x < 0 else 'right_') + motion 451 | 452 | self.action(motion) 453 | 454 | @add_gesture_handler 455 | class PINCH(GESTURE): 456 | 'Class to handle this type of gesture' 457 | SUPPORTED_MOTIONS = ('in', 'out', 'clockwise', 'anticlockwise') 458 | extended_text = 'clock' 459 | 460 | def update(self, coords): 461 | 'Update this gesture for a motion' 462 | # Ignore this update if we can not parse the numbers we expect 463 | try: 464 | x = float(coords[4]) 465 | y = float(coords[5]) 466 | except (ValueError, IndexError): 467 | return False 468 | 469 | self.data[0] += x - 1.0 470 | self.data[1] += y 471 | return True 472 | 473 | def end(self): 474 | 'Action this gesture at the end of a motion sequence' 475 | ratio, angle = self.data 476 | 477 | if self.has_extended and abs(angle) > ROTATE_ANGLE: 478 | self.action('clockwise' if angle >= 0.0 else 'anticlockwise') 479 | elif ratio != 0.0: 480 | self.action('in' if ratio <= 0.0 else 'out') 481 | 482 | # Table of configuration commands 483 | conf_commands = OrderedDict() 484 | 485 | def add_conf_command(func): 486 | 'Add configuration command to command lookup table based on name' 487 | conf_commands[re.sub('^conf_', '', func.__name__)] = func 488 | 489 | @add_conf_command 490 | def conf_gesture(lineargs): 491 | 'Process a single gesture line in conf file' 492 | fields = lineargs.split(maxsplit=2) 493 | 494 | # Look for configured gesture. Sanity check the line. 495 | if len(fields) < 3: 496 | return 'Invalid gesture line - not enough fields' 497 | 498 | gesture, motion, command = fields 499 | handler = handlers.get(gesture.upper()) 500 | 501 | if not handler: 502 | return 'Gesture "{}" is not supported.\nMust be "{}"'.format( 503 | gesture, '" or "'.join([h.lower() for h in handlers])) 504 | 505 | # Gesture command can be configured with optional specific finger 506 | # count so look for that 507 | fingers, *fcommand = command.split(maxsplit=1) 508 | if fingers.isdigit() and len(fingers) == 1: 509 | command = fcommand[0] if fcommand else '' 510 | else: 511 | fingers = None 512 | 513 | # Add the configured gesture 514 | return handler.add(motion.lower(), fingers, command) 515 | 516 | @add_conf_command 517 | def conf_device(lineargs): 518 | 'Process a single device line in conf file' 519 | # Command line overrides configuration file 520 | if not args.device: 521 | args.device = lineargs 522 | 523 | return None if args.device else 'No device specified' 524 | 525 | @add_conf_command 526 | def swipe_threshold(lineargs): 527 | 'Change swipe threshold' 528 | global swipe_min_threshold 529 | try: 530 | swipe_min_threshold = int(lineargs) 531 | except Exception: 532 | return 'Must be integer value' 533 | 534 | return None if swipe_min_threshold >= 0 else 'Must be >= 0' 535 | 536 | @add_conf_command 537 | def timeout(lineargs): 538 | 'Change gesture timeout' 539 | global timeoutv 540 | try: 541 | timeoutv = float(lineargs) 542 | except Exception: 543 | return 'Must be float value' 544 | 545 | return None if timeoutv >= 0 else 'Must be >= 0' 546 | 547 | def get_conf_line(line): 548 | 'Process a single line in conf file' 549 | key, *argslist = line.split(maxsplit=1) 550 | 551 | # Old format conf files may have a ":" appended to the key 552 | key = key.rstrip(':') 553 | conf_func = conf_commands.get(key) 554 | 555 | if not conf_func: 556 | return 'Configuration command "{}" is not supported.\n' \ 557 | 'Must be "{}"'.format(key, '" or "'.join(conf_commands)) 558 | 559 | return conf_func(argslist[0] if argslist else '') 560 | 561 | def get_conf(conffile, confname): 562 | 'Read given configuration file and store internal actions etc' 563 | with conffile.open() as fp: 564 | for num, line in enumerate(fp, 1): 565 | line = line.strip() 566 | if not line or line[0] == '#': 567 | continue 568 | 569 | errmsg = get_conf_line(line) 570 | if errmsg: 571 | sys.exit('Error at line {} in file {}:\n>> {} <<\n{}.'.format( 572 | num, confname, line, errmsg)) 573 | 574 | def unexpanduser(cfile): 575 | 'Return absolute path name, with $HOME replaced by ~' 576 | relslash = Path(os.path.abspath(str(cfile))) 577 | try: 578 | relhome = relslash.relative_to(os.getenv('HOME')) 579 | except (ValueError, TypeError): 580 | relhome = None 581 | 582 | return ('~/' + str(relhome)) if relhome else str(relslash) 583 | 584 | # Search for configuration file. Use file given as command line 585 | # argument, else look for file in search dir order. 586 | def read_conf(conffile, defname): 587 | if conffile: 588 | confpath = Path(conffile) 589 | if not confpath.exists(): 590 | sys.exit('Conf file "{}" does not exist.'.format(conffile)) 591 | else: 592 | for confdir in CONFDIRS: 593 | confpath = Path(confdir, defname) 594 | if confpath.exists(): 595 | break 596 | else: 597 | sys.exit('No file {} in {}.'.format(defname, ' or '.join( 598 | [unexpanduser(Path(c)) for c in CONFDIRS]))) 599 | 600 | # Hide any personal user dir/names from diag output 601 | confname = unexpanduser(confpath) 602 | 603 | # Read and process the conf file 604 | get_conf(confpath, confname) 605 | return confname 606 | 607 | def main(): 608 | global args, abzsquare 609 | 610 | # Set up command line arguments 611 | opt = argparse.ArgumentParser(description=__doc__) 612 | opt.add_argument('-c', '--conffile', 613 | help='alternative configuration file') 614 | opt.add_argument('-v', '--verbose', action='store_true', 615 | help='output diagnostic messages') 616 | opt.add_argument('-d', '--debug', action='store_true', 617 | help='output diagnostic messages only, do not action gestures') 618 | opt.add_argument('-r', '--raw', action='store_true', 619 | help='output raw libinput debug-event messages only, ' 620 | 'do not action gestures') 621 | opt.add_argument('-l', '--list', action='store_true', 622 | help='just list out environment and configuration') 623 | opt.add_argument('--device', 624 | help='explicit device name to use (or path if starts with /)') 625 | # Test/diag hidden option to specify a file containing libinput list 626 | # device output to parse 627 | opt.add_argument('--device-list', help=argparse.SUPPRESS) 628 | args = opt.parse_args() 629 | 630 | if args.debug or args.raw or args.list: 631 | args.verbose = True 632 | 633 | # Libinput changed the way in which it's utilities are called 634 | libvers = get_libinput_vers() 635 | if not libvers: 636 | sys.exit('libinput helper tools do not seem to be installed?') 637 | 638 | if Version(libvers) >= Version('1.8'): 639 | cmd_debug_events = 'libinput debug-events' 640 | cmd_list_devices = 'libinput list-devices' 641 | else: 642 | cmd_debug_events = 'libinput-debug-events' 643 | cmd_list_devices = 'libinput-list-devices' 644 | 645 | if args.verbose: 646 | # Output various info/version info 647 | xsession = os.getenv('XDG_SESSION_DESKTOP') or \ 648 | os.getenv('DESKTOP_SESSION') or 'unknown' 649 | xtype = os.getenv('XDG_SESSION_TYPE') or 'unknown' 650 | print('{}: session {}+{} on {}, python {}, libinput {}'.format( 651 | PROGNAME, xsession, xtype, platform.platform(), 652 | platform.python_version(), libvers)) 653 | 654 | # Output hash version/checksum of this program 655 | vers = run(('md5sum', str(PROGPATH)), check=False) 656 | vers = str(vers.split()[0]) if vers else '?' 657 | print('{}: hash {}'.format(PROGPATH, vers)) 658 | 659 | # Read and process the conf file 660 | confname = read_conf(args.conffile, CONFNAME) 661 | 662 | # List out available gestures if that is asked for 663 | if args.verbose: 664 | if not args.raw: 665 | print('Gestures configured in {}:'.format(confname)) 666 | for h in handlers.values(): 667 | for mpair, cmd in h.motions.items(): 668 | motion, fingers = (mpair, '') \ 669 | if isinstance(mpair, str) else mpair 670 | print('{} {:10}{:>2} {}'.format(h.name.lower(), motion, 671 | fingers, cmd)) 672 | 673 | if swipe_min_threshold: 674 | print('swipe_threshold {}'.format(swipe_min_threshold)) 675 | if timeoutv != DEFAULT_TIMEOUT: 676 | print('timeout {}'.format(timeoutv)) 677 | 678 | if args.device: 679 | print('device {}'.format(args.device)) 680 | 681 | # Get touchpad device 682 | if not args.device or args.device.lower() != "all": 683 | device = get_device(args.device, cmd_list_devices, args.device_list) 684 | if not device: 685 | sys.exit('Could not determine touchpad device.') 686 | else: 687 | device = None 688 | 689 | if args.verbose: 690 | if device: 691 | print('{}: device {}'.format(PROGNAME, device.get('_diag'))) 692 | else: 693 | print('{}: monitoring all devices'.format(PROGNAME)) 694 | 695 | # If just called to list out above environment info then exit now 696 | if args.list: 697 | sys.exit() 698 | 699 | # Make sure only one instance running for current user 700 | user = getpass.getuser() 701 | proglock = open_lock(PROGNAME, user) 702 | if not proglock: 703 | sys.exit('{} is already running for {}, terminating ..'.format( 704 | PROGNAME, user)) 705 | 706 | # Set up square of swipe threshold 707 | abzsquare = swipe_min_threshold**2 708 | 709 | # Note your must "sudo gpasswd -a $USER input" then log out/in for 710 | # permission to access the device. 711 | devstr = ' --device {}'.format(device.get('_path')) if device else '' 712 | command = 'stdbuf -oL -- {}{}'.format(cmd_debug_events, devstr) 713 | 714 | cmd = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE, 715 | bufsize=1, universal_newlines=True) 716 | 717 | # Avoid producing zombie processes 718 | signal.signal(signal.SIGCHLD, signal.SIG_IGN) 719 | 720 | # Sit in a loop forever reading the libinput messages .. 721 | handler = None 722 | for line in cmd.stdout: 723 | 724 | # Just output raw messages if in that mode 725 | if args.raw: 726 | print(line.strip()) 727 | continue 728 | 729 | # Only interested in gestures 730 | if 'GESTURE_' not in line: 731 | continue 732 | 733 | # Split received message line into relevant fields 734 | dev, gevent, time, other = line.strip().split(maxsplit=3) 735 | gesture, event = gevent[8:].split('_') 736 | fingers, *argslist = other.split(maxsplit=1) 737 | params = argslist[0] if argslist else '' 738 | 739 | # Action each type of event 740 | if event == 'UPDATE': 741 | if handler: 742 | # Split parameters into list of clean numbers 743 | if not handler.update(re.split(r'[^-.\d]+', params)): 744 | print('Could not parse {} {}: {}'.format(gesture, event, 745 | params), file=sys.stderr) 746 | 747 | elif event == 'BEGIN': 748 | handler = handlers.get(gesture) 749 | if handler: 750 | handler.begin(fingers) 751 | else: 752 | print('Unknown gesture received: {}.'.format(gesture), 753 | file=sys.stderr) 754 | elif event == 'END': 755 | # Ignore gesture if final action is cancelled 756 | if handler: 757 | if params != 'cancelled': 758 | handler.end() 759 | handler = None 760 | else: 761 | print('Unknown gesture + event received: {} + {}.'.format(gesture, 762 | event), file=sys.stderr) 763 | 764 | if __name__ == '__main__': 765 | main() 766 | -------------------------------------------------------------------------------- /libinput-gestures-setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # User setup script. 3 | # (C) Mark Blakeney, Aug 2016. 4 | 5 | PROG="$(basename $0)" 6 | NAME=${PROG%-*} 7 | 8 | BINDIR="/usr/bin" 9 | APPDIR="/usr/share/applications" 10 | ICOBAS="/usr/share/icons/hicolor" 11 | ICODIR="$ICOBAS/128x128/apps" 12 | OCODIR="/usr/share/pixmaps" 13 | DOCDIR="/usr/share/doc/$NAME" 14 | CNFDIR="/etc" 15 | HCFDIR="${XDG_CONFIG_HOME:-$HOME/.config}" 16 | AUTDIR="$HCFDIR/autostart" 17 | 18 | usage() { 19 | echo "Usage:" 20 | echo "As root: sudo $PROG install|uninstall" 21 | echo "As user: $PROG start|stop|restart|autostart|autostop|status" 22 | echo 23 | echo "-d (option sets DESTDIR for install/uninstall)" 24 | echo "-r (force allow root to perform user commands. PLEASE AVOID USING THIS!)" 25 | exit 1 26 | } 27 | 28 | # Process command line options 29 | DESTDIR="" 30 | FORCEROOT=0 31 | while getopts d:r c; do 32 | case $c in 33 | d) DESTDIR="$OPTARG";; 34 | r) FORCEROOT=1;; 35 | \?) usage;; 36 | esac 37 | done 38 | 39 | shift $((OPTIND - 1)) 40 | 41 | if [[ $# -ne 1 ]]; then 42 | usage 43 | fi 44 | 45 | cmd="$1" 46 | 47 | # Launch given desktop app. First work out most suitable launcher. 48 | # Pretty crude at present but should work for at least GNOME and KDE. 49 | launch() { 50 | local app="$1" 51 | 52 | if echo $XDG_CURRENT_DESKTOP | grep -q KDE; then 53 | if hash kioclient5 &>/dev/null; then 54 | kioclient5 exec "$APPDIR/$app.desktop" >/dev/null 55 | return $? 56 | elif hash kioclient &>/dev/null; then 57 | kioclient exec "$APPDIR/$app.desktop" >/dev/null 58 | return $? 59 | fi 60 | fi 61 | 62 | if hash gtk-launch &>/dev/null; then 63 | gtk-launch "$app" 64 | return $? 65 | fi 66 | 67 | if hash i3-msg &>/dev/null; then 68 | i3-msg exec $BINDIR/$app >/dev/null 69 | # i3-msg will return successfully even if "$app" fails to start 70 | return $? 71 | fi 72 | 73 | echo "Don't know how to invoke $app.desktop" >&2 74 | return 1 75 | } 76 | 77 | # Set up desktop entry link for auto start of app, if it doesn't already 78 | # exist 79 | auto_start() { 80 | if [[ ! -f $APPDIR/$NAME.desktop ]]; then 81 | if [[ -e $AUTDIR/$NAME.desktop ]]; then 82 | echo "Removed old $AUTDIR/$NAME.desktop" 83 | rm -f $AUTDIR/$NAME.desktop 84 | fi 85 | return 1 86 | fi 87 | 88 | if ! cmp -s $APPDIR/$NAME.desktop $AUTDIR/$NAME.desktop; then 89 | mkdir -p $AUTDIR 90 | cp $APPDIR/$NAME.desktop $AUTDIR 91 | echo "installed or updated $AUTDIR/$NAME.desktop" 92 | fi 93 | return 0 94 | } 95 | 96 | # Action given user command 97 | user_action() { 98 | local cmd=$1 99 | 100 | if [[ $cmd == start ]]; then 101 | if [[ ! -f $APPDIR/$NAME.desktop ]]; then 102 | echo "$NAME is not installed." 103 | exit 1 104 | fi 105 | if launch "$NAME"; then 106 | echo "$NAME started." 107 | fi 108 | elif [[ $cmd == stop ]]; then 109 | for prog in libinput-debug-events $NAME; do 110 | if pkill -u $USER -f "$prog\$|$prog " &>/dev/null; then 111 | echo "$prog stopped." 112 | fi 113 | done 114 | elif [[ $cmd == autostart ]]; then 115 | if ! auto_start; then 116 | echo "$NAME is not installed." 117 | exit 1 118 | fi 119 | elif [[ $cmd == autostop ]]; then 120 | rm -fv $AUTDIR/$NAME.desktop 121 | elif [[ $cmd == status ]]; then 122 | if [[ -f $APPDIR/$NAME.desktop ]]; then 123 | echo "$NAME is installed." 124 | else 125 | echo "$NAME is not installed." 126 | fi 127 | if [[ -f $AUTDIR/$NAME.desktop ]]; then 128 | echo "$NAME is set to autostart." 129 | else 130 | echo "$NAME is not set to autostart." 131 | fi 132 | if pgrep -u $USER -f "$NAME\$|$NAME " &>/dev/null; then 133 | echo "$NAME is running." 134 | else 135 | echo "$NAME is not running." 136 | fi 137 | if [[ -f $HCFDIR/$NAME.conf ]]; then 138 | echo "$NAME is using custom configuration." 139 | else 140 | echo "$NAME is using default configuration." 141 | fi 142 | else 143 | usage 144 | fi 145 | } 146 | 147 | if [[ $cmd == install || $cmd == uninstall ]]; then 148 | DESTDIR="${DESTDIR%%+(/)}" 149 | if [[ -z $DESTDIR && $(id -un) != root ]]; then 150 | echo "Install or uninstall must be run as sudo/root." 151 | exit 1 152 | fi 153 | 154 | # Remove any old files from earlier versions of program 155 | rm -f $DESTDIR$OCODIR/$NAME.png 156 | 157 | if [[ $cmd == install ]]; then 158 | install -CDv -m 755 -t $DESTDIR$BINDIR $NAME-setup 159 | install -CDv -m 755 -t $DESTDIR$BINDIR $NAME 160 | install -CDv -m 644 -t $DESTDIR$APPDIR $NAME.desktop 161 | install -CDv -m 644 -t $DESTDIR$ICODIR $NAME.png 162 | install -CDv -m 644 -t $DESTDIR$CNFDIR $NAME.conf 163 | install -CDv -m 644 -t $DESTDIR$DOCDIR README.md 164 | 165 | # Also install HTML file if markdown is available 166 | if hash markdown &>/dev/null; then 167 | markdown -o $DESTDIR$DOCDIR/README.html README.md 168 | fi 169 | else 170 | rm -rfv $DESTDIR$BINDIR/$NAME 171 | rm -rfv $DESTDIR$APPDIR/$NAME.desktop 172 | rm -rfv $DESTDIR$ICODIR/$NAME.png 173 | rm -rfv $DESTDIR$CNFDIR/$NAME.conf 174 | rm -rfv $DESTDIR$DOCDIR 175 | rm -rfv $DESTDIR$BINDIR/$NAME-setup 176 | fi 177 | 178 | if [[ -z $DESTDIR ]]; then 179 | if [[ -x /usr/bin/update-desktop-database ]]; then 180 | /usr/bin/update-desktop-database -q 181 | fi 182 | if [[ -x /usr/bin/gtk-update-icon-cache ]]; then 183 | /usr/bin/gtk-update-icon-cache $ICOBAS 184 | fi 185 | fi 186 | else 187 | if [[ $(id -un) == root && $FORCEROOT == 0 ]]; then 188 | echo "Non-installation commands must be run as your own user." 189 | exit 1 190 | fi 191 | 192 | # Remove any old configuration from earlier versions of program 193 | rm -fv ~/bin/$NAME 2>/dev/null 194 | rm -fv ~/.local/bin/$NAME 2>/dev/null 195 | rm -fv ~/.local/share/applications/$NAME.desktop 2>/dev/null 196 | rm -fv ~/.local/share/icons/$NAME.png 2>/dev/null 197 | 198 | # Look for and update any autostart file if it is a link or not 199 | # pointing to the latest desktop entry. Apparently user autostart 200 | # files should not be symlinks to system dir files. 201 | if [[ -e $AUTDIR/$NAME.desktop ]]; then 202 | if [[ -L $AUTDIR/$NAME.desktop ]]; then 203 | echo "Removed old $AUTDIR/$NAME.desktop link" 204 | rm -f $AUTDIR/$NAME.desktop 205 | fi 206 | auto_start 207 | fi 208 | 209 | if [[ $cmd == restart ]]; then 210 | user_action "stop" 211 | user_action "start" 212 | else 213 | user_action $cmd 214 | fi 215 | fi 216 | 217 | exit 0 218 | -------------------------------------------------------------------------------- /libinput-gestures.conf: -------------------------------------------------------------------------------- 1 | # Configuration file for libinput-gestures. 2 | # Mark Blakeney, Sep 2015 3 | # 4 | # The default configuration file exists at /etc/libinput-gestures.conf 5 | # but a user can create a personal custom configuration file at 6 | # ~/.config/libinput-gestures.conf. 7 | # 8 | # Lines starting with '#' and blank lines are ignored. Currently 9 | # "gesture" and "device" configuration keywords are supported as 10 | # described below. The keyword can optionally be appended with a ":" (to 11 | # maintain compatibility with original format configuration files). 12 | # 13 | # Each gesture line has 3 [or 4] arguments separated by whitespace: 14 | # 15 | # action motion [finger_count] command 16 | # 17 | # where action and motion is either: 18 | # swipe up 19 | # swipe down 20 | # swipe left 21 | # swipe right 22 | # swipe left_up 23 | # swipe left_down 24 | # swipe right_up 25 | # swipe right_down 26 | # pinch in 27 | # pinch out 28 | # pinch clockwise 29 | # pinch anticlockwise 30 | # 31 | # command is the remainder of the line and is any valid shell command + 32 | # arguments. 33 | # 34 | # finger_count is a single numeric digit and is optional (and is 35 | # typically 3 or 4). If specified then the command is executed when 36 | # exactly that number of fingers is used in the gesture. If not 37 | # specified then the command is executed when that gesture is executed 38 | # with any number of fingers. Gesture lines specified with finger_count 39 | # have priority over the same gesture specified without any 40 | # finger_count. 41 | # 42 | # Typically command will be xdotool, or wmctrl. See "man xdotool" for 43 | # the many things you can action with that tool. Note that unfortunately 44 | # xdotool does not work with native Wayland clients. 45 | 46 | ############################################################################### 47 | # SWIPE GESTURES: 48 | ############################################################################### 49 | 50 | # Note the default is an "internal" command that uses wmctrl to switch 51 | # workspaces and, unlike xdotool, works on both Xorg and Wayland (via 52 | # XWayland). It also can be configured for vertical and horizontal 53 | # switching over tabular workspaces, as per the example below. You can 54 | # also add "-w" to the internal command to allow wrapping workspaces. 55 | 56 | # Move to next workspace (works for GNOME/KDE/etc on Wayland and Xorg) 57 | gesture swipe up _internal ws_up 58 | 59 | # NOTE ABOUT FINGER COUNT: 60 | # The above command will configure this command for all fingers (i.e. 3 61 | # for 4) but to configure it for 3 fingers only, change it to: 62 | # gesture swipe up 3 _internal ws_up 63 | # Then you can configure something else for 4 fingers or leave 4 fingers 64 | # unconfigured. You can configure an explicit finger count like this for 65 | # all example commands in this configuration file. 66 | # 67 | # gesture swipe up xdotool key super+Page_Down 68 | 69 | # Move to prev workspace (works for GNOME/KDE/etc on Wayland and Xorg) 70 | gesture swipe down _internal ws_down 71 | # gesture swipe down xdotool key super+Page_Up 72 | 73 | # Browser go forward (works only for Xorg, and Xwayland clients) 74 | gesture swipe left xdotool key alt+Right 75 | 76 | # Browser go back (works only for Xorg, and Xwayland clients) 77 | gesture swipe right xdotool key alt+Left 78 | 79 | # NOTE: If you don't use "natural" scrolling direction for your touchpad 80 | # then you may want to swap the above default left/right and up/down 81 | # configurations. 82 | 83 | # Optional extended swipe gestures, e.g. for browser tab navigation: 84 | # 85 | # Jump to next open browser tab 86 | # gesture swipe right_up xdotool key control+Tab 87 | # 88 | # Jump to previous open browser tab 89 | # gesture swipe left_up xdotool key control+shift+Tab 90 | # 91 | # Close current browser tab 92 | # gesture swipe left_down xdotool key control+w 93 | # 94 | # Reopen and jump to last closed browser tab 95 | # gesture swipe right_down xdotool key control+shift+t 96 | 97 | # Example of 8 static workspaces, e.g. using KDE virtual-desktops, 98 | # arranged in 2 rows of 4 columns across using swipe up/down/left/right 99 | # to navigate in fixed planes. You can also add the "-w/--wrap" option 100 | # to allow wrapping in any direction. You must configure your virtual 101 | # desktops with the same column dimension. 102 | # gesture swipe up _internal --cols 4 ws_up 103 | # gesture swipe down _internal --cols 4 ws_down 104 | # gesture swipe left _internal --cols 4 ws_left 105 | # gesture swipe right _internal --cols 4 ws_right 106 | # 107 | # Example of 16 static workspaces, e.g. using KDE virtual-desktops, 108 | # arranged in 4 rows of 4 columns across using swipe up/down/left/right 109 | # to navigate in fixed planes, and also using swipe 110 | # left_up/left_down/right_up/right_down to navigate diagonally. You can 111 | # also add the "-w/--wrap" option to allow wrapping in any direction 112 | # and/or diagonally. You must configure your virtual desktops with the 113 | # same column dimension. 114 | # gesture swipe up _internal --cols 4 ws_up 115 | # gesture swipe down _internal --cols 4 ws_down 116 | # gesture swipe left _internal --cols 4 ws_left 117 | # gesture swipe right _internal --cols 4 ws_right 118 | # gesture swipe left_up _internal --cols 4 ws_left_up 119 | # gesture swipe left_down _internal --cols 4 ws_left_down 120 | # gesture swipe right_up _internal --cols 4 ws_right_up 121 | # gesture swipe right_down _internal --cols 4 ws_right_down 122 | 123 | # Example virtual desktop switching for Ubuntu Unity/Compiz. The 124 | # _internal command does not work for Compiz but you can explicitly 125 | # configure the swipe commands to work for a Compiz virtual 2 126 | # dimensional desktop as follows: 127 | # gesture swipe up xdotool key ctrl+alt+Up 128 | # gesture swipe down xdotool key ctrl+alt+Down 129 | # gesture swipe left xdotool key ctrl+alt+Left 130 | # gesture swipe right xdotool key ctrl+alt+Right 131 | 132 | # Example to change audio volume: 133 | # Note this only works on an Xorg desktop (not Wayland). 134 | # gesture swipe up xdotool key XF86AudioRaiseVolume 135 | # gesture swipe down xdotool key XF86AudioLowerVolume 136 | 137 | ############################################################################### 138 | # PINCH GESTURES: 139 | ############################################################################### 140 | 141 | # GNOME SHELL open/close overview (works for GNOME on Xorg only) 142 | gesture pinch in xdotool key super+s 143 | gesture pinch out xdotool key super+s 144 | 145 | # KDE Plasma open/close overview 146 | # gesture pinch in xdotool key ctrl+F9 147 | # gesture pinch out xdotool key ctrl+F9 148 | 149 | # GNOME SHELL open/close overview (works for GNOME on Wayland and Xorg) 150 | # Note since GNOME 3.24 on Wayland this is implemented natively so no 151 | # real point configuring for Wayland. 152 | # gesture pinch in dbus-send --session --type=method_call --dest=org.gnome.Shell /org/gnome/Shell org.gnome.Shell.Eval string:'Main.overview.toggle();' 153 | # gesture pinch out dbus-send --session --type=method_call --dest=org.gnome.Shell /org/gnome/Shell org.gnome.Shell.Eval string:'Main.overview.toggle();' 154 | 155 | # Optional extended pinch gestures: 156 | # gesture pinch clockwise 157 | # gesture pinch anticlockwise 158 | 159 | ############################################################################### 160 | # This application normally determines your touchpad device 161 | # automatically. Some users may have multiple touchpads but by default 162 | # we use only the first one found. However, you can choose to specify 163 | # the explicit device name to use. Run "libinput list-devices" to work 164 | # out the name of your device (from the "Device:" field). Then add a 165 | # device line specifying that name, e.g: 166 | # 167 | # device DLL0665:01 06CB:76AD Touchpad 168 | # 169 | # If the device name starts with a '/' then it is instead considered as 170 | # the explicit device path although since device paths can change 171 | # through reboots this is best to be a symlink. E.g. instead of specifying 172 | # /dev/input/event12, you should use the corresponding full path link 173 | # under /dev/input/by-path/ or /dev/input/by-id/. 174 | # 175 | # You can choose to use ALL touchpad devices by setting the device name 176 | # to "all". E.g. Do this if you have multiple touchpads which you want 177 | # to use in parallel. This reduces performance slightly so only set this 178 | # if you have to. 179 | # 180 | # device all 181 | 182 | ############################################################################### 183 | # You can set a minimum travel distance threshold before swipe gestures 184 | # are actioned using the swipe_threshold configuration command. 185 | # Specify this value in dots. The default is 0. 186 | # E.g. set it to 100 dots with "swipe_threshold 100". 187 | # swipe_threshold 0 188 | 189 | ############################################################################### 190 | # You can set a timeout on gestures from start to end. The default is 191 | # the value commented below. It can be any value in float secs >= 0. 192 | # 0 = no timeout. E.g. set it to 2 secs with "timeout 2". 193 | # timeout 1.5 194 | -------------------------------------------------------------------------------- /libinput-gestures.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Terminal=false 4 | Name=Libinput Gestures 5 | Exec=/usr/bin/libinput-gestures 6 | Icon=libinput-gestures 7 | Comment=Background application to intercept and action libinput gestures from touchpad. 8 | Categories=GNOME;GTK;System; 9 | -------------------------------------------------------------------------------- /libinput-gestures.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sudnyeshtalekar/libinput-gestures-for-ubuntu/07f70250d21b00992524a0c75bfd3e8fa41f0313/libinput-gestures.png -------------------------------------------------------------------------------- /list-version-hashes: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # M.Blakeney, Oct 2018. 3 | 4 | PROG="$(basename $0)" 5 | PROGDIR="$(dirname $0)" 6 | SUMPROG="md5sum" 7 | 8 | usage() { 9 | echo "Usage: $PROG [-options] [hashsum]" 10 | echo "Development utility to list version and hash sums, and flag those that" 11 | echo "match given hash sum. Must be run from the libinput-gestures git repo dir." 12 | echo "Options: (none)" 13 | exit 1 14 | } 15 | 16 | # Process command line options 17 | while getopts \? c; do 18 | case $c in 19 | \?) usage;; 20 | esac 21 | done 22 | 23 | shift $((OPTIND - 1)) 24 | 25 | if [[ $# -eq 1 ]]; then 26 | HASHSUM="$1" 27 | elif [[ $# -ne 0 ]]; then 28 | usage 29 | else 30 | HASHSUM="" 31 | fi 32 | 33 | output() { 34 | local tag=$1 35 | local hashsum=${2/ */} 36 | 37 | if [[ -n $HASHSUM && $HASHSUM == $hashsum ]]; then 38 | found=" *" 39 | else 40 | found="" 41 | fi 42 | 43 | printf "%-24s %s%s\n" $tag $hashsum "$found" 44 | } 45 | 46 | cd ${PROGDIR:-.} || exit 1 47 | 48 | # Iterate through all tags and output the md5 hash for each version 49 | tag="" 50 | while read hashc; do 51 | tag=$(git describe --tags --always $hashc) 52 | hashsum=$(git show $hashc:libinput-gestures | $SUMPROG) 53 | output $tag $hashsum 54 | done <<< "$(git rev-list --all --reverse)" 55 | 56 | # Output a version for the working tree as well 57 | tagw=$(git describe --tags --always --dirty) 58 | if [[ $tagw != $tag ]]; then 59 | hashsum=$($SUMPROG libinput-gestures) 60 | output $tagw $hashsum 61 | fi 62 | --------------------------------------------------------------------------------