├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── etc └── remap.conf ├── install.sh ├── src ├── config.rs └── main.rs ├── surface-pen-button.service └── uninstall.sh /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "anyhow" 7 | version = "1.0.70" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" 10 | 11 | [[package]] 12 | name = "autocfg" 13 | version = "1.1.0" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 16 | 17 | [[package]] 18 | name = "bitflags" 19 | version = "1.3.2" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 22 | 23 | [[package]] 24 | name = "cc" 25 | version = "1.0.79" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" 28 | 29 | [[package]] 30 | name = "cfg-if" 31 | version = "1.0.0" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 34 | 35 | [[package]] 36 | name = "evdev-rs" 37 | version = "0.6.1" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "9812d5790fb6fcce449333eb6713dad335e8c979225ed98755c84a3987e06dba" 40 | dependencies = [ 41 | "bitflags", 42 | "evdev-sys", 43 | "libc", 44 | "log", 45 | "serde", 46 | ] 47 | 48 | [[package]] 49 | name = "evdev-sys" 50 | version = "0.2.5" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | checksum = "14ead42b547b15d47089c1243d907bcf0eb94e457046d3b315a26ac9c9e9ea6d" 53 | dependencies = [ 54 | "cc", 55 | "libc", 56 | "pkg-config", 57 | ] 58 | 59 | [[package]] 60 | name = "glob" 61 | version = "0.3.1" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" 64 | 65 | [[package]] 66 | name = "hashbrown" 67 | version = "0.12.3" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 70 | 71 | [[package]] 72 | name = "indexmap" 73 | version = "1.9.3" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" 76 | dependencies = [ 77 | "autocfg", 78 | "hashbrown", 79 | ] 80 | 81 | [[package]] 82 | name = "libc" 83 | version = "0.2.140" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" 86 | 87 | [[package]] 88 | name = "log" 89 | version = "0.4.17" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 92 | dependencies = [ 93 | "cfg-if", 94 | ] 95 | 96 | [[package]] 97 | name = "memchr" 98 | version = "2.5.0" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 101 | 102 | [[package]] 103 | name = "memoffset" 104 | version = "0.7.1" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" 107 | dependencies = [ 108 | "autocfg", 109 | ] 110 | 111 | [[package]] 112 | name = "nix" 113 | version = "0.26.2" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" 116 | dependencies = [ 117 | "bitflags", 118 | "cfg-if", 119 | "libc", 120 | "memoffset", 121 | "pin-utils", 122 | "static_assertions", 123 | ] 124 | 125 | [[package]] 126 | name = "pin-utils" 127 | version = "0.1.0" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 130 | 131 | [[package]] 132 | name = "pkg-config" 133 | version = "0.3.26" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" 136 | 137 | [[package]] 138 | name = "proc-macro2" 139 | version = "1.0.55" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "1d0dd4be24fcdcfeaa12a432d588dc59bbad6cad3510c67e74a2b6b2fc950564" 142 | dependencies = [ 143 | "unicode-ident", 144 | ] 145 | 146 | [[package]] 147 | name = "quote" 148 | version = "1.0.26" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" 151 | dependencies = [ 152 | "proc-macro2", 153 | ] 154 | 155 | [[package]] 156 | name = "serde" 157 | version = "1.0.159" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "3c04e8343c3daeec41f58990b9d77068df31209f2af111e059e9fe9646693065" 160 | dependencies = [ 161 | "serde_derive", 162 | ] 163 | 164 | [[package]] 165 | name = "serde_derive" 166 | version = "1.0.159" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "4c614d17805b093df4b147b51339e7e44bf05ef59fba1e45d83500bcfb4d8585" 169 | dependencies = [ 170 | "proc-macro2", 171 | "quote", 172 | "syn", 173 | ] 174 | 175 | [[package]] 176 | name = "serde_spanned" 177 | version = "0.6.1" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4" 180 | dependencies = [ 181 | "serde", 182 | ] 183 | 184 | [[package]] 185 | name = "static_assertions" 186 | version = "1.1.0" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 189 | 190 | [[package]] 191 | name = "surface-pen-button" 192 | version = "0.1.0" 193 | dependencies = [ 194 | "anyhow", 195 | "evdev-rs", 196 | "glob", 197 | "nix", 198 | "serde", 199 | "toml", 200 | ] 201 | 202 | [[package]] 203 | name = "syn" 204 | version = "2.0.13" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "4c9da457c5285ac1f936ebd076af6dac17a61cfe7826f2076b4d015cf47bc8ec" 207 | dependencies = [ 208 | "proc-macro2", 209 | "quote", 210 | "unicode-ident", 211 | ] 212 | 213 | [[package]] 214 | name = "toml" 215 | version = "0.7.3" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "b403acf6f2bb0859c93c7f0d967cb4a75a7ac552100f9322faf64dc047669b21" 218 | dependencies = [ 219 | "serde", 220 | "serde_spanned", 221 | "toml_datetime", 222 | "toml_edit", 223 | ] 224 | 225 | [[package]] 226 | name = "toml_datetime" 227 | version = "0.6.1" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" 230 | dependencies = [ 231 | "serde", 232 | ] 233 | 234 | [[package]] 235 | name = "toml_edit" 236 | version = "0.19.8" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13" 239 | dependencies = [ 240 | "indexmap", 241 | "serde", 242 | "serde_spanned", 243 | "toml_datetime", 244 | "winnow", 245 | ] 246 | 247 | [[package]] 248 | name = "unicode-ident" 249 | version = "1.0.8" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" 252 | 253 | [[package]] 254 | name = "winnow" 255 | version = "0.4.1" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "ae8970b36c66498d8ff1d66685dc86b91b29db0c7739899012f63a63814b4b28" 258 | dependencies = [ 259 | "memchr", 260 | ] 261 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "surface-pen-button" 3 | version = "0.1.0" 4 | authors = ["Maximilian Luz "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | evdev-rs = { version = "0.6.1", features = ["serde"] } 9 | nix = "0.26.2" 10 | glob = "0.3.1" 11 | serde = { version = "1.0.159", features = ["derive"] } 12 | toml = "0.7.3" 13 | anyhow = "1.0.70" 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Maximilian Luz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Remapper for the Bluetooth (Eraser) Button of the Surface Pen 2 | 3 | Work in progress. 4 | 5 | The Surface Pen bluetooth-button can distinguish between three actions, which are reported as key events: 6 | 7 | - Single-click (⌘ Meta+F20). 8 | - Double-click (⌘ Meta+F19). 9 | - Press and hold (⌘ Meta+F18). 10 | 11 | This utility allows you to re-map these actions via evedev/uinput so that they can be used e.g. in presentation tools that do not allow custom keyboard shortcuts or have trouble handling the meta key. 12 | For more hardware-details, have a look at the [official Windows specification page][windows-spec]. 13 | 14 | [windows-spec]: https://docs.microsoft.com/en-us/windows-hardware/design/component-guidelines/windows-pen-designs#bluetooth-button-implimentation 15 | 16 | ### Configure 17 | 18 | 19 | 20 | If you are configuring before installation: Open the file `remap.conf` in the folder `etc` and edit it: 21 | 22 | If you are configuring after installation: Open the file `remap.conf` in the folder `/etc/surface-pen-button/` and edit it: 23 | > The content of this file is toml syntax so keys are simple strings and actions are arrays of strings. 24 | 1) Change `vendor` if neccessary. You can check the vendor id of your pen by using `sudo evtest` and then selecting `Surface Pen Keyboard`. You'll find you vendor id at `Input device ID` after `vendor`. 25 | 2) Change `product` if neccessary. You can check the product id of your pen by using `sudo evtest` and then selecting `Surface Pen Keyboard`. You'll find you product id at `Input device ID` after `product`. 26 | 3) Change the key combinations for the actions. You have to put the keys in quotation marks (e. g. "KEY_ESC"). If you want multiple keys, you have to put a comma (, ) between them. 27 |
Available keys: 28 | 29 | ``` 30 | KEY_ESC 31 | KEY_1 32 | KEY_2 33 | KEY_3 34 | KEY_4 35 | KEY_5 36 | KEY_6 37 | KEY_7 38 | KEY_8 39 | KEY_9 40 | KEY_0 41 | KEY_MINUS 42 | KEY_EQUAL 43 | KEY_BACKSPACE 44 | KEY_TAB 45 | KEY_Q 46 | KEY_W 47 | KEY_E 48 | KEY_R 49 | KEY_T 50 | KEY_Y 51 | KEY_U 52 | KEY_I 53 | KEY_O 54 | KEY_P 55 | KEY_LEFTBRACE 56 | KEY_RIGHTBRACE 57 | KEY_ENTER 58 | KEY_LEFTCTRL 59 | KEY_A 60 | KEY_S 61 | KEY_D 62 | KEY_F 63 | KEY_G 64 | KEY_H 65 | KEY_J 66 | KEY_K 67 | KEY_L 68 | KEY_SEMICOLON 69 | KEY_APOSTROPHE 70 | KEY_GRAVE 71 | KEY_LEFTSHIFT 72 | KEY_BACKSLASH 73 | KEY_Z 74 | KEY_X 75 | KEY_C 76 | KEY_V 77 | KEY_B 78 | KEY_N 79 | KEY_M 80 | KEY_COMMA 81 | KEY_DOT 82 | KEY_SLASH 83 | KEY_RIGHTSHIFT 84 | KEY_KPASTERISK 85 | KEY_LEFTALT 86 | KEY_SPACE 87 | KEY_CAPSLOCK 88 | KEY_F1 89 | KEY_F2 90 | KEY_F3 91 | KEY_F4 92 | KEY_F5 93 | KEY_F6 94 | KEY_F7 95 | KEY_F8 96 | KEY_F9 97 | KEY_F10 98 | KEY_NUMLOCK 99 | KEY_SCROLLLOCK 100 | KEY_KP7 101 | KEY_KP8 102 | KEY_KP9 103 | KEY_KPMINUS 104 | KEY_KP4 105 | KEY_KP5 106 | KEY_KP6 107 | KEY_KPPLUS 108 | KEY_KP1 109 | KEY_KP2 110 | KEY_KP3 111 | KEY_KP0 112 | KEY_KPDOT 113 | KEY_102ND 114 | KEY_F11 115 | KEY_F12 116 | KEY_KPENTER 117 | KEY_RIGHTCTRL 118 | KEY_KPSLASH 119 | KEY_SYSRQ 120 | KEY_RIGHTALT 121 | KEY_HOME 122 | KEY_UP 123 | KEY_PAGEUP 124 | KEY_LEFT 125 | KEY_RIGHT 126 | KEY_END 127 | KEY_DOWN 128 | KEY_PAGEDOWN 129 | KEY_INSERT 130 | KEY_DELETE 131 | KEY_POWER 132 | KEY_KPEQUAL 133 | KEY_PAUSE 134 | KEY_LEFTMETA 135 | KEY_RIGHTMETA 136 | KEY_COMPOSE 137 | KEY_F13 138 | KEY_F14 139 | KEY_F15 140 | KEY_F16 141 | KEY_F17 142 | KEY_F18 143 | KEY_F19 144 | KEY_F20 145 | ``` 146 | 147 |
148 | 149 | 4) If you are configuring after installation: Stop this program via `sudo killall surface-pen-button`. If you have installed the systemd service, the program will start again after 2 seconds. 150 | 151 | 152 | ### Install 153 | 154 | Run `./install.sh` in this repository, then follow the instructions. 155 | 156 | ### Uninstall 157 | 158 | Run `./uninstall.sh` in this repository. 159 | -------------------------------------------------------------------------------- /etc/remap.conf: -------------------------------------------------------------------------------- 1 | [device] 2 | vendor = 0x045e 3 | product = 0x0921 4 | 5 | [actions] 6 | single = ["KEY_RIGHT"] 7 | double = ["KEY_LEFT"] 8 | hold = ["KEY_LEFTCTRL", "KEY_Q"] 9 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . /etc/os-release 4 | if [ "$ID" == "fedora" ] || [ "$ID_LIKE" == "fedora" ] || [ "$ID_LIKE" == "rhel fedora" ]; then 5 | echo "Installing dependencies..." 6 | sudo dnf install cargo libevdev-devel -y 7 | elif [ "$ID" == "ubuntu" ] || [ "$ID" == "debian" ] || [ "$ID_LIKE" == "debian" ] || [ "$ID_LIKE" == "ubuntu" ]; then 8 | echo "Installing dependendies..." 9 | sudo apt install cargo libevdev-dev -y 10 | else 11 | echo "You have to install the packages cargo and libevdev-dev/libevdev-devel to use this program. Please do that before continuing" 12 | read -p "Press Enter to continue" , 24 | pub double: Vec, 25 | pub hold: Vec, 26 | } 27 | 28 | pub fn load() -> Result { 29 | let path = std::env::var_os("SURFACE_PEN_BUTTON_CONFIG") 30 | .map(PathBuf::from) 31 | .unwrap_or(PathBuf::from(DEFAULT_CONFIG_PATH)); 32 | 33 | let config = std::fs::read_to_string(path)?; 34 | let config = toml::from_str(&config)?; 35 | 36 | Ok(config) 37 | } 38 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use std::collections::HashSet; 3 | use std::fs::File; 4 | 5 | use evdev_rs::enums::{EventCode, EV_KEY, EV_SYN}; 6 | use evdev_rs::{ 7 | Device, DeviceWrapper, GrabMode, InputEvent, ReadFlag, ReadStatus, TimeVal, UInputDevice, 8 | }; 9 | 10 | use nix::errno::Errno; 11 | 12 | mod config; 13 | use config::Config; 14 | 15 | #[derive(Debug)] 16 | enum EventType { 17 | Single, 18 | Double, 19 | Hold, 20 | } 21 | 22 | #[derive(Debug)] 23 | enum EventState { 24 | Pressed, 25 | Released, 26 | } 27 | 28 | #[derive(Debug)] 29 | struct Event { 30 | time: TimeVal, 31 | ty: EventType, 32 | state: EventState, 33 | } 34 | 35 | fn match_device(config: &Config, device: &Device) -> bool { 36 | device.bustype() == 5 /* BUS_BLUETOOTH */ && 37 | device.vendor_id() == config.device.vendor as _ && 38 | device.product_id() == config.device.product as _ && 39 | device.has(EventCode::EV_KEY(EV_KEY::KEY_LEFTMETA)) && 40 | device.has(EventCode::EV_KEY(EV_KEY::KEY_F18)) && 41 | device.has(EventCode::EV_KEY(EV_KEY::KEY_F19)) && 42 | device.has(EventCode::EV_KEY(EV_KEY::KEY_F20)) 43 | } 44 | 45 | fn find_device(config: &Config) -> Result> { 46 | for entry in glob::glob("/dev/input/event*").unwrap() { 47 | let entry = entry.map_err(|e| e.into_error())?; 48 | 49 | let device = File::open(entry)?; 50 | let device = Device::new_from_file(device)?; 51 | 52 | if match_device(config, &device) { 53 | return Ok(Some(device)); 54 | } 55 | } 56 | 57 | Ok(None) 58 | } 59 | 60 | fn setup_uinput_device(config: &Config) -> Result { 61 | let device = evdev_rs::UninitDevice::new().unwrap(); 62 | device.set_name("Surface Pen Keyboard (mapped)"); 63 | device.set_vendor_id(config.device.vendor as _); 64 | device.set_product_id(config.device.product as _); 65 | 66 | let mut keys = HashSet::new(); 67 | keys.extend(config.actions.single.iter().cloned()); 68 | keys.extend(config.actions.double.iter().cloned()); 69 | keys.extend(config.actions.hold.iter().cloned()); 70 | 71 | for key in keys { 72 | device.enable_event_code(&EventCode::EV_KEY(key), None)?; 73 | } 74 | 75 | let device = UInputDevice::create_from_device(&device)?; 76 | Ok(device) 77 | } 78 | 79 | fn output_event(config: &Config, event: Event, output: &UInputDevice) -> Result<()> { 80 | println!("{:?}", event); // TODO 81 | 82 | let value = match event.state { 83 | EventState::Pressed => 1, 84 | EventState::Released => 0, 85 | }; 86 | 87 | let keys = match event.ty { 88 | EventType::Single => &config.actions.single, 89 | EventType::Double => &config.actions.double, 90 | EventType::Hold => &config.actions.hold, 91 | }; 92 | 93 | for key in keys { 94 | let evt = InputEvent::new(&event.time, &EventCode::EV_KEY(*key), value); 95 | output.write_event(&evt)?; 96 | } 97 | 98 | if !keys.is_empty() { 99 | let syn = InputEvent::new(&event.time, &EventCode::EV_SYN(EV_SYN::SYN_REPORT), 0); 100 | output.write_event(&syn)?; 101 | } 102 | 103 | Ok(()) 104 | } 105 | 106 | fn handle_event_batch(config: &Config, events: &[InputEvent], output: &UInputDevice) -> Result<()> { 107 | if events.is_empty() { 108 | return Ok(()); 109 | } 110 | 111 | let time = events[0].time; 112 | let meta = events 113 | .iter() 114 | .find(|e| e.event_code == EventCode::EV_KEY(EV_KEY::KEY_LEFTMETA)); 115 | 116 | if meta.is_none() { 117 | return Ok(()); 118 | } 119 | let meta = meta.unwrap(); 120 | 121 | for e in events { 122 | let ty = match e.event_code { 123 | EventCode::EV_KEY(EV_KEY::KEY_F20) => EventType::Single, 124 | EventCode::EV_KEY(EV_KEY::KEY_F19) => EventType::Double, 125 | EventCode::EV_KEY(EV_KEY::KEY_F18) => EventType::Hold, 126 | _ => continue, 127 | }; 128 | 129 | let state = if meta.value == 1 && e.value == 1 { 130 | EventState::Pressed 131 | } else if meta.value == 0 && e.value == 0 { 132 | EventState::Released 133 | } else { 134 | continue; 135 | }; 136 | 137 | let event = Event { time, ty, state }; 138 | output_event(config, event, output)?; 139 | } 140 | 141 | Ok(()) 142 | } 143 | 144 | fn handle_events(config: &Config, mut input: Device, output: UInputDevice) -> Result<()> { 145 | input.grab(GrabMode::Grab)?; 146 | 147 | let mut events = Vec::with_capacity(4); 148 | let mut flags = ReadFlag::NORMAL | ReadFlag::BLOCKING; 149 | loop { 150 | match input.next_event(flags) { 151 | Ok((status, evt)) => { 152 | flags = match status { 153 | ReadStatus::Success => ReadFlag::NORMAL | ReadFlag::BLOCKING, 154 | ReadStatus::Sync => ReadFlag::SYNC, 155 | }; 156 | 157 | match evt.event_code { 158 | EventCode::EV_SYN(EV_SYN::SYN_REPORT) => { 159 | handle_event_batch(config, &events, &output)?; 160 | events.clear(); 161 | } 162 | EventCode::EV_KEY(_) => { 163 | events.push(evt); 164 | } 165 | _ => {} 166 | }; 167 | } 168 | Err(err) => { 169 | if Errno::from_i32(err.raw_os_error().unwrap_or(0)) == Errno::EAGAIN { 170 | flags = ReadFlag::NORMAL | ReadFlag::BLOCKING; 171 | } else { 172 | Err(err)? 173 | } 174 | } 175 | } 176 | } 177 | } 178 | 179 | fn main() -> Result<()> { 180 | let config = config::load().context("Failed to load configuration file")?; 181 | 182 | if let Some(device) = find_device(&config)? { 183 | println!("Found device: '{}'", device.name().unwrap_or("")); 184 | handle_events(&config, device, setup_uinput_device(&config)?) 185 | } else { 186 | anyhow::bail!("Device not found"); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /surface-pen-button.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Surface Pen Bluetooth Button Mapping 3 | 4 | [Service] 5 | ExecStart=/usr/bin/surface-pen-button 6 | Restart=always 7 | RestartSec=2 8 | StartLimitIntervalSec=1 9 | 10 | [Install] 11 | WantedBy=multi-user.target 12 | -------------------------------------------------------------------------------- /uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | sudo killall surface-pen-button 4 | sudo rm -r /etc/surface-pen-button/ 5 | sudo rm -r /usr/bin/surface-pen-button 6 | 7 | SYSTEMD_FILE=/etc/systemd/system/surface-pen-button.service 8 | 9 | if test -f "$SYSTEMD_FILE"; then 10 | sudo systemctl stop surface-pen-button.service 11 | sudo rm -r /etc/systemd/system/surface-pen-button.service 12 | sudo systemctl daemon-reload 13 | echo "Finished!" 14 | else 15 | echo "Finished!" 16 | fi 17 | --------------------------------------------------------------------------------