├── .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 |
--------------------------------------------------------------------------------