├── .gitignore ├── Cargo.toml ├── GNUmakefile ├── LICENSE ├── README.md ├── inputplug.1 ├── inputplug.pod └── src ├── main.rs └── mask_iter.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.toml 3 | *.iml 4 | .idea 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "inputplug" 3 | version = "0.4.0" 4 | authors = ["Andrej Shadura "] 5 | edition = "2021" 6 | description = "XInput monitor daemon" 7 | license = "MIT" 8 | repository = "https://github.com/andrewshadura/inputplug" 9 | 10 | [dependencies] 11 | clap = { version = "3.0", features = ["derive", "std"] } 12 | nix = { version = ">= 0.19, <1.0", features = ["process"] } 13 | anyhow = "1.0" 14 | 15 | [dependencies.pidfile-rs] 16 | optional = true 17 | version = "0.2" 18 | 19 | [features] 20 | default = ["pidfile"] 21 | pidfile = ["pidfile-rs"] 22 | 23 | [dependencies.x11rb] 24 | version = "0.13" 25 | features = ["xinput"] 26 | -------------------------------------------------------------------------------- /GNUmakefile: -------------------------------------------------------------------------------- 1 | build: inputplug.1 inputplug.md 2 | cargo build --release 3 | 4 | install: build 5 | ifeq ($(DESTDIR),) 6 | cargo install --path . 7 | else 8 | cargo install --root "$(DESTDIR)" --path . 9 | endif 10 | 11 | inputplug.1: inputplug.pod 12 | pod2man -r "" -c "" -n $(shell echo $(@:%.1=%) | tr a-z A-Z) $< > $@ 13 | 14 | inputplug.md: inputplug.pod 15 | pod2markdown < $< | sed -e 's, - , — ,g' \ 16 | -e 's,^- ,* ,g' \ 17 | -e 's,man.he.net/man./,manpages.debian.org/,g' \ 18 | -e 's,\[\(<.*@.*>\)\](.*),\1,' > $@ 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a copy 2 | of this software and associated documentation files (the "Software"), to deal 3 | in the Software without restriction, including without limitation the rights 4 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 5 | copies of the Software, and to permit persons to whom the Software is 6 | furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in 9 | all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 15 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 17 | THE SOFTWARE. 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | inputplug 2 | ========= 3 | 4 | inputplug is a very simple daemon which monitors XInput events and runs 5 | arbitrary scripts on hierarchy change events (such as a device being 6 | attached, removed, enabled or disabled). 7 | 8 | To build the project, run `cargo build`. 9 | 10 | * * * 11 | 12 | # NAME 13 | 14 | inputplug — XInput event monitor 15 | 16 | # SYNOPSIS 17 | 18 | **inputplug** \[**-v**\] \[**-n**\] \[**-d**\] \[**-0**\] **-c** _command-prefix_ 19 | 20 | **inputplug** \[**-h**|**--help**\] 21 | 22 | # DESCRIPTION 23 | 24 | **inputplug** is a daemon which connects to a running X server 25 | and monitors its XInput hierarchy change events. Such events arrive 26 | when a device is being attached or removed, enabled or disabled etc. 27 | 28 | When a hierarchy change happens, **inputplug** parses the event notification 29 | structure, and calls the command specified by _command-prefix_. The command 30 | receives four arguments: 31 | 32 | * _command-prefix_ _event-type_ _device-id_ _device-type_ _device-name_ 33 | 34 | Event type may be one of the following: 35 | 36 | * _XIMasterAdded_ 37 | * _XIMasterRemoved_ 38 | * _XISlaveAdded_ 39 | * _XISlaveRemoved_ 40 | * _XISlaveAttached_ 41 | * _XISlaveDetached_ 42 | * _XIDeviceEnabled_ 43 | * _XIDeviceDisabled_ 44 | 45 | Device type may be any of those: 46 | 47 | * _XIMasterPointer_ 48 | * _XIMasterKeyboard_ 49 | * _XISlavePointer_ 50 | * _XISlaveKeyboard_ 51 | * _XIFloatingSlave_ 52 | 53 | Device identifier is an integer. The device name may have embedded spaces. 54 | 55 | # OPTIONS 56 | 57 | A summary of options is included below. 58 | 59 | * **-h**, **--help** 60 | 61 | Show help (**--help** shows more details). 62 | 63 | * **-v** 64 | 65 | Be a bit more verbose. 66 | 67 | * **-n** 68 | 69 | Start up, monitor events, but don't actually run anything. 70 | With verbose more enabled, would print the actual command it'd 71 | run. This implies **-d**. 72 | 73 | * **-d** 74 | 75 | Don't daemonise. Run in the foreground. 76 | 77 | * **-0** 78 | 79 | On start, trigger added and enabled events for each plugged devices. A 80 | master device will trigger the "added" event while a slave device will 81 | trigger both the "added" and the "enabled" device. 82 | 83 | * **-c** _command-prefix_ 84 | 85 | Command prefix to run. Unfortunately, currently this is passed to 86 | [execvp(3)](http://manpages.debian.org/execvp) directly, so spaces aren't allowed. This is subject to 87 | change in future. 88 | 89 | * **-p** _pidfile_ 90 | 91 | Write the process ID of the running daemon to the file _pidfile_ 92 | 93 | # ENVIRONMENT 94 | 95 | * _DISPLAY_ 96 | 97 | X11 display to connect to. 98 | 99 | # SEE ALSO 100 | 101 | [xinput(1)](http://manpages.debian.org/xinput) 102 | 103 | # COPYRIGHT 104 | 105 | Copyright (C) 2013, 2014, 2018, 2020, 2021 Andrej Shadura. 106 | 107 | Copyright (C) 2014, 2020 Vincent Bernat. 108 | 109 | Licensed as MIT/X11. 110 | 111 | # AUTHOR 112 | 113 | Andrej Shadura 114 | -------------------------------------------------------------------------------- /inputplug.1: -------------------------------------------------------------------------------- 1 | .\" Automatically generated by Pod::Man 4.11 (Pod::Simple 3.35) 2 | .\" 3 | .\" Standard preamble: 4 | .\" ======================================================================== 5 | .de Sp \" Vertical space (when we can't use .PP) 6 | .if t .sp .5v 7 | .if n .sp 8 | .. 9 | .de Vb \" Begin verbatim text 10 | .ft CW 11 | .nf 12 | .ne \\$1 13 | .. 14 | .de Ve \" End verbatim text 15 | .ft R 16 | .fi 17 | .. 18 | .\" Set up some character translations and predefined strings. \*(-- will 19 | .\" give an unbreakable dash, \*(PI will give pi, \*(L" will give a left 20 | .\" double quote, and \*(R" will give a right double quote. \*(C+ will 21 | .\" give a nicer C++. Capital omega is used to do unbreakable dashes and 22 | .\" therefore won't be available. \*(C` and \*(C' expand to `' in nroff, 23 | .\" nothing in troff, for use with C<>. 24 | .tr \(*W- 25 | .ds C+ C\v'-.1v'\h'-1p'\s-2+\h'-1p'+\s0\v'.1v'\h'-1p' 26 | .ie n \{\ 27 | . ds -- \(*W- 28 | . ds PI pi 29 | . if (\n(.H=4u)&(1m=24u) .ds -- \(*W\h'-12u'\(*W\h'-12u'-\" diablo 10 pitch 30 | . if (\n(.H=4u)&(1m=20u) .ds -- \(*W\h'-12u'\(*W\h'-8u'-\" diablo 12 pitch 31 | . ds L" "" 32 | . ds R" "" 33 | . ds C` "" 34 | . ds C' "" 35 | 'br\} 36 | .el\{\ 37 | . ds -- \|\(em\| 38 | . ds PI \(*p 39 | . ds L" `` 40 | . ds R" '' 41 | . ds C` 42 | . ds C' 43 | 'br\} 44 | .\" 45 | .\" Escape single quotes in literal strings from groff's Unicode transform. 46 | .ie \n(.g .ds Aq \(aq 47 | .el .ds Aq ' 48 | .\" 49 | .\" If the F register is >0, we'll generate index entries on stderr for 50 | .\" titles (.TH), headers (.SH), subsections (.SS), items (.Ip), and index 51 | .\" entries marked with X<> in POD. Of course, you'll have to process the 52 | .\" output yourself in some meaningful fashion. 53 | .\" 54 | .\" Avoid warning from groff about undefined register 'F'. 55 | .de IX 56 | .. 57 | .nr rF 0 58 | .if \n(.g .if rF .nr rF 1 59 | .if (\n(rF:(\n(.g==0)) \{\ 60 | . if \nF \{\ 61 | . de IX 62 | . tm Index:\\$1\t\\n%\t"\\$2" 63 | .. 64 | . if !\nF==2 \{\ 65 | . nr % 0 66 | . nr F 2 67 | . \} 68 | . \} 69 | .\} 70 | .rr rF 71 | .\" 72 | .\" Accent mark definitions (@(#)ms.acc 1.5 88/02/08 SMI; from UCB 4.2). 73 | .\" Fear. Run. Save yourself. No user-serviceable parts. 74 | . \" fudge factors for nroff and troff 75 | .if n \{\ 76 | . ds #H 0 77 | . ds #V .8m 78 | . ds #F .3m 79 | . ds #[ \f1 80 | . ds #] \fP 81 | .\} 82 | .if t \{\ 83 | . ds #H ((1u-(\\\\n(.fu%2u))*.13m) 84 | . ds #V .6m 85 | . ds #F 0 86 | . ds #[ \& 87 | . ds #] \& 88 | .\} 89 | . \" simple accents for nroff and troff 90 | .if n \{\ 91 | . ds ' \& 92 | . ds ` \& 93 | . ds ^ \& 94 | . ds , \& 95 | . ds ~ ~ 96 | . ds / 97 | .\} 98 | .if t \{\ 99 | . ds ' \\k:\h'-(\\n(.wu*8/10-\*(#H)'\'\h"|\\n:u" 100 | . ds ` \\k:\h'-(\\n(.wu*8/10-\*(#H)'\`\h'|\\n:u' 101 | . ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'^\h'|\\n:u' 102 | . ds , \\k:\h'-(\\n(.wu*8/10)',\h'|\\n:u' 103 | . ds ~ \\k:\h'-(\\n(.wu-\*(#H-.1m)'~\h'|\\n:u' 104 | . ds / \\k:\h'-(\\n(.wu*8/10-\*(#H)'\z\(sl\h'|\\n:u' 105 | .\} 106 | . \" troff and (daisy-wheel) nroff accents 107 | .ds : \\k:\h'-(\\n(.wu*8/10-\*(#H+.1m+\*(#F)'\v'-\*(#V'\z.\h'.2m+\*(#F'.\h'|\\n:u'\v'\*(#V' 108 | .ds 8 \h'\*(#H'\(*b\h'-\*(#H' 109 | .ds o \\k:\h'-(\\n(.wu+\w'\(de'u-\*(#H)/2u'\v'-.3n'\*(#[\z\(de\v'.3n'\h'|\\n:u'\*(#] 110 | .ds d- \h'\*(#H'\(pd\h'-\w'~'u'\v'-.25m'\f2\(hy\fP\v'.25m'\h'-\*(#H' 111 | .ds D- D\\k:\h'-\w'D'u'\v'-.11m'\z\(hy\v'.11m'\h'|\\n:u' 112 | .ds th \*(#[\v'.3m'\s+1I\s-1\v'-.3m'\h'-(\w'I'u*2/3)'\s-1o\s+1\*(#] 113 | .ds Th \*(#[\s+2I\s-2\h'-\w'I'u*3/5'\v'-.3m'o\v'.3m'\*(#] 114 | .ds ae a\h'-(\w'a'u*4/10)'e 115 | .ds Ae A\h'-(\w'A'u*4/10)'E 116 | . \" corrections for vroff 117 | .if v .ds ~ \\k:\h'-(\\n(.wu*9/10-\*(#H)'\s-2\u~\d\s+2\h'|\\n:u' 118 | .if v .ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'\v'-.4m'^\v'.4m'\h'|\\n:u' 119 | . \" for low resolution devices (crt and lpr) 120 | .if \n(.H>23 .if \n(.V>19 \ 121 | \{\ 122 | . ds : e 123 | . ds 8 ss 124 | . ds o a 125 | . ds d- d\h'-1'\(ga 126 | . ds D- D\h'-1'\(hy 127 | . ds th \o'bp' 128 | . ds Th \o'LP' 129 | . ds ae ae 130 | . ds Ae AE 131 | .\} 132 | .rm #[ #] #H #V #F C 133 | .\" ======================================================================== 134 | .\" 135 | .IX Title "INPUTPLUG 1" 136 | .TH INPUTPLUG 1 "2020-11-01" "" "" 137 | .\" For nroff, turn off justification. Always turn off hyphenation; it makes 138 | .\" way too many mistakes in technical documents. 139 | .if n .ad l 140 | .nh 141 | .SH "NAME" 142 | inputplug \- XInput event monitor 143 | .SH "SYNOPSIS" 144 | .IX Header "SYNOPSIS" 145 | \&\fBinputplug\fR [\fB\-v\fR] [\fB\-n\fR] [\fB\-d\fR] [\fB\-0\fR] \fB\-c\fR \fIcommand-prefix\fR 146 | .PP 147 | \&\fBinputplug\fR [\fB\-h\fR|\fB\-\-help\fR] 148 | .SH "DESCRIPTION" 149 | .IX Header "DESCRIPTION" 150 | \&\fBinputplug\fR is a daemon which connects to a running X server 151 | and monitors its XInput hierarchy change events. Such events arrive 152 | when a device is being attached or removed, enabled or disabled etc. 153 | .PP 154 | When a hierarchy change happens, \fBinputplug\fR parses the event notification 155 | structure, and calls the command specified by \fIcommand-prefix\fR. The command 156 | receives four arguments: 157 | .IP "\fIcommand-prefix\fR \fIevent-type\fR \fIdevice-id\fR \fIdevice-type\fR \fIdevice-name\fR" 4 158 | .IX Item "command-prefix event-type device-id device-type device-name" 159 | .PP 160 | Event type may be one of the following: 161 | .IP "\(bu" 4 162 | \&\fIXIMasterAdded\fR 163 | .IP "\(bu" 4 164 | \&\fIXIMasterRemoved\fR 165 | .IP "\(bu" 4 166 | \&\fIXISlaveAdded\fR 167 | .IP "\(bu" 4 168 | \&\fIXISlaveRemoved\fR 169 | .IP "\(bu" 4 170 | \&\fIXISlaveAttached\fR 171 | .IP "\(bu" 4 172 | \&\fIXISlaveDetached\fR 173 | .IP "\(bu" 4 174 | \&\fIXIDeviceEnabled\fR 175 | .IP "\(bu" 4 176 | \&\fIXIDeviceDisabled\fR 177 | .PP 178 | Device type may be any of those: 179 | .IP "\(bu" 4 180 | \&\fIXIMasterPointer\fR 181 | .IP "\(bu" 4 182 | \&\fIXIMasterKeyboard\fR 183 | .IP "\(bu" 4 184 | \&\fIXISlavePointer\fR 185 | .IP "\(bu" 4 186 | \&\fIXISlaveKeyboard\fR 187 | .IP "\(bu" 4 188 | \&\fIXIFloatingSlave\fR 189 | .PP 190 | Device identifier is an integer. The device name may have embedded spaces. 191 | .SH "OPTIONS" 192 | .IX Header "OPTIONS" 193 | A summary of options is included below. 194 | .IP "\fB\-h\fR, \fB\-\-help\fR" 4 195 | .IX Item "-h, --help" 196 | Show help (\fB\-\-help\fR shows more details). 197 | .IP "\fB\-v\fR" 4 198 | .IX Item "-v" 199 | Be a bit more verbose. 200 | .IP "\fB\-n\fR" 4 201 | .IX Item "-n" 202 | Start up, monitor events, but don't actually run anything. 203 | With verbose more enabled, would print the actual command it'd 204 | run. This implies \fB\-d\fR. 205 | .IP "\fB\-d\fR" 4 206 | .IX Item "-d" 207 | Don't daemonise. Run in the foreground. 208 | .IP "\fB\-0\fR" 4 209 | .IX Item "-0" 210 | On start, trigger added and enabled events for each plugged devices. A 211 | master device will trigger the \*(L"added\*(R" event while a slave device will 212 | trigger both the \*(L"added\*(R" and the \*(L"enabled\*(R" device. 213 | .IP "\fB\-c\fR \fIcommand-prefix\fR" 4 214 | .IX Item "-c command-prefix" 215 | Command prefix to run. Unfortunately, currently this is passed to 216 | \&\fBexecvp\fR\|(3) directly, so spaces aren't allowed. This is subject to 217 | change in future. 218 | .IP "\fB\-p\fR \fIpidfile\fR" 4 219 | .IX Item "-p pidfile" 220 | Write the process \s-1ID\s0 of the running daemon to the file \fIpidfile\fR 221 | .SH "ENVIRONMENT" 222 | .IX Header "ENVIRONMENT" 223 | .IP "\fI\s-1DISPLAY\s0\fR" 4 224 | .IX Item "DISPLAY" 225 | X11 display to connect to. 226 | .SH "SEE ALSO" 227 | .IX Header "SEE ALSO" 228 | \&\fBxinput\fR\|(1) 229 | .SH "COPYRIGHT" 230 | .IX Header "COPYRIGHT" 231 | Copyright (C) 2013, 2014, 2018, 2020, 2021 Andrej Shadura. 232 | .PP 233 | Copyright (C) 2014, 2020 Vincent Bernat. 234 | .PP 235 | Licensed as \s-1MIT/X11.\s0 236 | .SH "AUTHOR" 237 | .IX Header "AUTHOR" 238 | Andrej Shadura 239 | -------------------------------------------------------------------------------- /inputplug.pod: -------------------------------------------------------------------------------- 1 | =head1 NAME 2 | 3 | inputplug - XInput event monitor 4 | 5 | =head1 SYNOPSIS 6 | 7 | B [B<-v>] [B<-n>] [B<-d>] [B<-0>] B<-c> I 8 | 9 | B [B<-h>|B<--help>] 10 | 11 | =head1 DESCRIPTION 12 | 13 | B is a daemon which connects to a running X server 14 | and monitors its XInput hierarchy change events. Such events arrive 15 | when a device is being attached or removed, enabled or disabled etc. 16 | 17 | When a hierarchy change happens, B parses the event notification 18 | structure, and calls the command specified by I. The command 19 | receives four arguments: 20 | 21 | =over 22 | 23 | =item I I I I I 24 | 25 | =back 26 | 27 | Event type may be one of the following: 28 | 29 | =over 30 | 31 | =item * I 32 | 33 | =item * I 34 | 35 | =item * I 36 | 37 | =item * I 38 | 39 | =item * I 40 | 41 | =item * I 42 | 43 | =item * I 44 | 45 | =item * I 46 | 47 | =back 48 | 49 | Device type may be any of those: 50 | 51 | =over 52 | 53 | =item * I 54 | 55 | =item * I 56 | 57 | =item * I 58 | 59 | =item * I 60 | 61 | =item * I 62 | 63 | =back 64 | 65 | Device identifier is an integer. The device name may have embedded spaces. 66 | 67 | =head1 OPTIONS 68 | 69 | A summary of options is included below. 70 | 71 | =over 72 | 73 | =item B<-h>, B<--help> 74 | 75 | Show help (B<--help> shows more details). 76 | 77 | =item B<-v> 78 | 79 | Be a bit more verbose. 80 | 81 | =item B<-n> 82 | 83 | Start up, monitor events, but don't actually run anything. 84 | With verbose more enabled, would print the actual command it'd 85 | run. This implies B<-d>. 86 | 87 | =item B<-d> 88 | 89 | Don't daemonise. Run in the foreground. 90 | 91 | =item B<-0> 92 | 93 | On start, trigger added and enabled events for each plugged devices. A 94 | master device will trigger the "added" event while a slave device will 95 | trigger both the "added" and the "enabled" device. 96 | 97 | =item B<-c> I 98 | 99 | Command prefix to run. Unfortunately, currently this is passed to 100 | L directly, so spaces aren't allowed. This is subject to 101 | change in future. 102 | 103 | =item B<-p> I 104 | 105 | Write the process ID of the running daemon to the file I 106 | 107 | =back 108 | 109 | =head1 ENVIRONMENT 110 | 111 | =over 112 | 113 | =item I 114 | 115 | X11 display to connect to. 116 | 117 | =back 118 | 119 | =head1 SEE ALSO 120 | 121 | L 122 | 123 | =head1 COPYRIGHT 124 | 125 | Copyright (C) 2013, 2014, 2018, 2020, 2021 Andrej Shadura. 126 | 127 | Copyright (C) 2014, 2020 Vincent Bernat. 128 | 129 | Licensed as MIT/X11. 130 | 131 | =head1 AUTHOR 132 | 133 | Andrej Shadura L<< >> 134 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2020—2021 Andrej Shadura 2 | // SPDX-License-Identifier: MIT 3 | mod mask_iter; 4 | use mask_iter::IterableMask; 5 | use nix::unistd::daemon; 6 | #[cfg(feature = "pidfile")] 7 | use pidfile_rs::Pidfile; 8 | use std::convert::From; 9 | use clap::StructOpt; 10 | 11 | #[cfg(feature = "pidfile")] 12 | use std::{fs::Permissions, os::unix::fs::PermissionsExt, path::PathBuf}; 13 | 14 | use std::process::Command; 15 | 16 | use anyhow::{anyhow, Context, Result}; 17 | 18 | use x11rb::connection::{ 19 | Connection as _, RequestConnection 20 | }; 21 | use x11rb::protocol::Event; 22 | use x11rb::protocol::xinput::{ 23 | self, ConnectionExt as _, 24 | Device, DeviceId, DeviceType, EventMask, 25 | HierarchyInfo, HierarchyMask, 26 | XIDeviceInfo, XIEventMask 27 | }; 28 | use x11rb::protocol::xproto::GE_GENERIC_EVENT; 29 | 30 | #[derive(Debug, StructOpt)] 31 | #[structopt(name = "inputplug", about = "XInput event monitor")] 32 | struct Opt { 33 | /// Enable debug mode. 34 | #[structopt(long)] 35 | debug: bool, 36 | 37 | /// Be a bit more verbose. 38 | #[structopt(short, long)] 39 | verbose: bool, 40 | 41 | /// Don't daemonize, run in the foreground. 42 | #[structopt(short = 'd', long)] 43 | foreground: bool, 44 | 45 | /// Start up, monitor events, but don't actually run anything. 46 | /// 47 | /// With verbose more enabled, would print the actual command it'd run. 48 | #[structopt(short, long)] 49 | no_act: bool, 50 | 51 | /// On start, trigger added and enabled events for each plugged devices. 52 | /// 53 | /// A master device will trigger the "added" event while a slave 54 | /// device will trigger both the "added" and the "enabled" device. 55 | #[structopt(short = '0', long)] 56 | bootstrap: bool, 57 | 58 | /// Command prefix to run. 59 | #[structopt(short = 'c', long)] 60 | command: String, 61 | 62 | /// PID file 63 | #[cfg(feature = "pidfile")] 64 | #[structopt(short = 'p', long, parse(from_os_str))] 65 | pidfile: Option, 66 | } 67 | 68 | trait HierarchyChangeEvent { 69 | fn to_cmdline(&self, conn: &impl RequestConnection) -> Vec; 70 | } 71 | 72 | fn device_name(conn: &impl RequestConnection, deviceid: DeviceId) -> Option { 73 | if let Ok(r) = conn.xinput_xi_query_device(deviceid) { 74 | if let Ok(reply) = r.reply() { 75 | reply.infos.iter() 76 | .find(|info| info.deviceid == deviceid) 77 | .map(|info| String::from_utf8_lossy(&info.name).to_string()) 78 | } else { 79 | None 80 | } 81 | } else { 82 | None 83 | } 84 | } 85 | 86 | fn format_device_type(device_type: DeviceType) -> String { 87 | if device_type == DeviceType::from(0u8) { 88 | "".into() 89 | } else { 90 | format!("XI{:#?}", device_type) 91 | } 92 | } 93 | 94 | impl HierarchyChangeEvent for XIDeviceInfo { 95 | fn to_cmdline(&self, conn: &impl RequestConnection) -> Vec { 96 | vec![ 97 | self.deviceid.to_string(), 98 | format_device_type(self.type_), 99 | String::from_utf8_lossy(&self.name).to_string(), 100 | ] 101 | } 102 | } 103 | 104 | impl HierarchyChangeEvent for HierarchyInfo { 105 | fn to_cmdline(&self, conn: &impl RequestConnection) -> Vec { 106 | vec![ 107 | self.deviceid.to_string(), 108 | format_device_type(self.type_), 109 | device_name(conn, self.deviceid).unwrap_or("".to_string()), 110 | ] 111 | } 112 | } 113 | 114 | fn handle_device>( 115 | opt: &Opt, 116 | conn: &impl RequestConnection, 117 | device_info: &T, 118 | change: HierarchyMask 119 | ) { 120 | let mut command = Command::new(&opt.command); 121 | 122 | command.arg(format!("XI{:#?}", change)) 123 | .args(device_info.to_cmdline(conn)); 124 | if opt.verbose { 125 | println!("{:?}", &command); 126 | } 127 | if !opt.no_act { 128 | if let Err(e) = command.status() { 129 | eprintln!("Command failed: {}", e); 130 | } 131 | } 132 | } 133 | 134 | fn main() -> Result<()> { 135 | let opt = Opt::from_args(); 136 | 137 | let (conn, _) = x11rb::connect(None).context("Can't open X display")?; 138 | 139 | let xinput_info = conn 140 | .extension_information(xinput::X11_EXTENSION_NAME) 141 | .context("X Input extension cannot be detected.")? 142 | .ok_or(anyhow!("X Input extension not available."))?; 143 | 144 | if opt.debug { 145 | println!("X Input extension opcode: {}", xinput_info.major_opcode); 146 | } 147 | 148 | // We don’t want to inherit an open connection into the daemon 149 | drop(conn); 150 | 151 | #[cfg(feature = "pidfile")] 152 | let pidfile = if opt.pidfile.is_some() { 153 | Some(Pidfile::new( 154 | opt.pidfile.as_ref().unwrap(), 155 | Permissions::from_mode(0o600) 156 | )?) 157 | } else { 158 | None 159 | }; 160 | 161 | if !opt.foreground { 162 | daemon(false, opt.verbose).context("Cannot daemonize")?; 163 | 164 | println!("Daemonized."); 165 | 166 | #[cfg(feature = "pidfile")] 167 | if pidfile.is_some() { 168 | if let Err(error) = pidfile.unwrap().write() { 169 | eprintln!("Failed to write to the PID file: {:?}", error); 170 | } 171 | } 172 | } 173 | 174 | // Now that we’re in the daemon, reconnect to the X server 175 | let (conn, screen_num) = x11rb::connect(None) 176 | .context("Can't reconnect to the X display")?; 177 | 178 | let screen = &conn.setup().roots[screen_num]; 179 | 180 | if opt.bootstrap { 181 | if opt.debug { 182 | println!("Bootstrapping events"); 183 | } 184 | 185 | if let Ok(reply) = conn.xinput_xi_query_device(bool::from(Device::ALL)) { 186 | let reply = reply.reply()?; 187 | for info in reply.infos { 188 | match DeviceType::from(info.type_) { 189 | DeviceType::MASTER_POINTER | 190 | DeviceType::MASTER_KEYBOARD => { 191 | handle_device(&opt, &conn, &info, HierarchyMask::MASTER_ADDED) 192 | } 193 | DeviceType::SLAVE_POINTER | 194 | DeviceType::SLAVE_KEYBOARD | 195 | DeviceType::FLOATING_SLAVE => { 196 | handle_device(&opt, &conn, &info, HierarchyMask::SLAVE_ADDED); 197 | handle_device(&opt, &conn, &info, HierarchyMask::DEVICE_ENABLED) 198 | } 199 | _ => {} 200 | } 201 | } 202 | } 203 | } 204 | 205 | conn.xinput_xi_select_events( 206 | screen.root, 207 | &[EventMask { 208 | deviceid: bool::from(Device::ALL).into(), 209 | mask: vec![XIEventMask::HIERARCHY], 210 | }] 211 | )?; 212 | 213 | conn.flush()?; 214 | loop { 215 | let event = conn.wait_for_event() 216 | .context("Failed to get an event")?; 217 | if event.response_type() != GE_GENERIC_EVENT { 218 | continue; 219 | } 220 | if let Event::XinputHierarchy(hier_event) = event { 221 | if hier_event.extension != xinput_info.major_opcode { 222 | continue; 223 | } 224 | for info in hier_event.infos { 225 | let flags = IterableMask::from(u32::from(info.flags)) 226 | .map(|x| HierarchyMask::from(x as u8)) 227 | .collect::>(); 228 | 229 | for flag in flags { 230 | handle_device(&opt, &conn, &info, flag); 231 | } 232 | } 233 | } 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/mask_iter.rs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2020 Andrej Shadura 2 | // SPDX-License-Identifier: MIT 3 | use std::iter::Iterator; 4 | 5 | /// Iterate over the bits of a binary value 6 | /// 7 | /// `IterableMask` is an iterator producing only the set bits 8 | /// of a binary value it is created from: 9 | /// ```rust 10 | /// IterableMask::from(0x81_u8).collect::>() 11 | /// # [0x80, 1] 12 | /// ``` 13 | pub struct IterableMask { 14 | value: T, 15 | curr_mask: T 16 | } 17 | 18 | macro_rules! implement_iterable_mask { 19 | ($t: ty) => { 20 | impl Iterator for IterableMask<$t> { 21 | type Item = $t; 22 | 23 | fn next(&mut self) -> Option<$t> { 24 | loop { 25 | let bit = self.curr_mask & self.value; 26 | self.curr_mask >>= 1; 27 | if bit != 0 { 28 | return Some(bit); 29 | } 30 | if self.curr_mask == 0 { 31 | break; 32 | } 33 | } 34 | None 35 | } 36 | } 37 | 38 | impl From<$t> for IterableMask<$t> { 39 | fn from(value: $t) -> Self { 40 | IterableMask { 41 | value, 42 | curr_mask: <$t>::from(1u8).rotate_right(1) 43 | } 44 | } 45 | } 46 | }; 47 | } 48 | 49 | implement_iterable_mask!(u8); 50 | implement_iterable_mask!(u16); 51 | implement_iterable_mask!(u32); 52 | 53 | #[cfg(test)] 54 | mod tests { 55 | use super::*; 56 | 57 | #[test] 58 | fn basic() { 59 | assert_eq!( 60 | IterableMask::from(0x123ce_u32).collect::>(), 61 | [0x10000, 0x2000, 0x200, 0x100, 0x80, 0x40, 8, 4, 2] 62 | ); 63 | assert_eq!( 64 | IterableMask::from(0x23c5_u16).collect::>(), 65 | [0x2000, 0x200, 0x100, 0x80, 0x40, 4, 1] 66 | ); 67 | assert_eq!( 68 | IterableMask::from(0xce_u8).collect::>(), 69 | [0x80, 0x40, 8, 4, 2] 70 | ); 71 | assert_eq!(IterableMask::from(0_u8).collect::>(), []); 72 | } 73 | } 74 | --------------------------------------------------------------------------------