├── .gitignore ├── .github ├── FUNDING.yml ├── workflows │ ├── rust.yml │ └── fmt.yml └── dependabot.yml ├── doc └── examples │ └── evoluent-vertical-mouse-4 │ ├── evoluent.png │ └── evremap.toml ├── Makefile ├── .rustfmt.toml ├── Cargo.toml ├── evremap.service ├── LICENSE.md ├── pixelbookgo.toml ├── src ├── mapping.rs ├── deviceinfo.rs ├── main.rs └── remapper.rs ├── README.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | .*.sw* 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: wez 2 | patreon: WezFurlong 3 | ko_fi: wezfurlong 4 | liberapay: wez 5 | -------------------------------------------------------------------------------- /doc/examples/evoluent-vertical-mouse-4/evoluent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wez/evremap/HEAD/doc/examples/evoluent-vertical-mouse-4/evoluent.png -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all fmt check test 2 | 3 | all: check 4 | 5 | test: 6 | cargo nextest run 7 | 8 | check: 9 | cargo check 10 | 11 | fmt: 12 | cargo +nightly fmt 13 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | # Please keep these in alphabetical order. 2 | # https://github.com/rust-lang/rustfmt/issues/3149 3 | edition = "2018" 4 | imports_granularity = "Module" 5 | tab_spaces = 4 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "evremap" 3 | version = "0.1.0" 4 | authors = ["Wez Furlong"] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | anyhow = "1.0" 9 | clap = {version="4.5", features=["derive"]} 10 | evdev-rs = "0.6.1" 11 | libc = "0.2" 12 | log = "0.4" 13 | env_logger = "0.11" 14 | serde = { version="1.0", features=["derive"]} 15 | thiserror = "1.0" 16 | toml = "0.8" 17 | -------------------------------------------------------------------------------- /evremap.service: -------------------------------------------------------------------------------- 1 | [Service] 2 | WorkingDirectory=/ 3 | # For reasons I don't care to troubleshoot, Fedora 31 won't let me start this 4 | # unless I use `bash -c` around it. Putting the command line in directly 5 | # yields a 203 permission denied error with no logs about what it didn't like. 6 | ExecStart=bash -c "/usr/bin/evremap remap /etc/evremap.toml -d 0" 7 | Restart=always 8 | 9 | [Install] 10 | WantedBy=multi-user.target 11 | 12 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Deps 20 | run: "sudo -n apt-get install -y libevdev-dev" 21 | - name: Build 22 | run: cargo build --verbose 23 | - name: Run tests 24 | run: cargo test --verbose 25 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Please see the documentation for all configuration options: 2 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: "cargo" 7 | directory: "/" 8 | schedule: 9 | interval: "weekly" 10 | groups: 11 | all: 12 | patterns: 13 | - "*" 14 | 15 | - package-ecosystem: "github-actions" 16 | directory: "/" 17 | schedule: 18 | interval: "weekly" 19 | -------------------------------------------------------------------------------- /.github/workflows/fmt.yml: -------------------------------------------------------------------------------- 1 | name: fmt 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: "Install Rust" 18 | uses: dtolnay/rust-toolchain@nightly 19 | with: 20 | components: rustfmt 21 | - name: Check formatting 22 | run: | 23 | source $HOME/.cargo/env 24 | cargo +nightly fmt --all -- --check 25 | 26 | 27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Wez Furlong 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 | -------------------------------------------------------------------------------- /doc/examples/evoluent-vertical-mouse-4/evremap.toml: -------------------------------------------------------------------------------- 1 | device_name = "Kingsis Peripherals Evoluent VerticalMouse 4" 2 | 3 | # Order of mappings is important. 4 | 5 | ################################################################################ 6 | # Secondary functions, enabled by holding Button 6. 7 | ################################################################################ 8 | 9 | # Button 6 + Button 1 => Copy. 10 | [[remap]] 11 | input = ["BTN_FORWARD", "BTN_LEFT"] 12 | output = ["KEY_COPY"] 13 | 14 | # Button 6 + Button 2 => Paste. 15 | [[remap]] 16 | input = ["BTN_FORWARD", "BTN_MIDDLE"] 17 | output = ["KEY_PASTE"] 18 | 19 | # Button 6 + Button 3 => Ctrl + W. 20 | # KEY_Z is used instead of KEY_W, because my keyboard layout is AZERTY. 21 | [[remap]] 22 | input = ["BTN_FORWARD", "BTN_RIGHT"] 23 | output = ["KEY_LEFTCTRL", "KEY_Z"] 24 | 25 | # Button 6 => Disabled. 26 | # This is done to avoid confusion, because it is dedicated to enable 27 | # secondary functions. 28 | [[remap]] 29 | input = ["BTN_FORWARD"] 30 | output = [] 31 | 32 | ################################################################################ 33 | # Primary functions. 34 | ################################################################################ 35 | 36 | # The middle finger => Right click. 37 | # Just like on a horizontal mouse. 38 | [[remap]] 39 | input = ["BTN_MIDDLE"] 40 | output = ["BTN_RIGHT"] 41 | 42 | # Button 3 => Escape. 43 | [[remap]] 44 | input = ["BTN_RIGHT"] 45 | output = ["KEY_ESC"] 46 | 47 | # The wheel button => Middle click. 48 | # Just like on a horizontal mouse. 49 | [[remap]] 50 | input = ["BTN_EXTRA"] 51 | output = ["BTN_MIDDLE"] 52 | 53 | # Button 5 => Enter. 54 | [[remap]] 55 | input = ["BTN_SIDE"] 56 | output = ["KEY_ENTER"] 57 | -------------------------------------------------------------------------------- /pixelbookgo.toml: -------------------------------------------------------------------------------- 1 | # This config file is the one that @wez uses on a PixelBook Go 2 | 3 | device_name = "AT Translated Set 2 keyboard" 4 | 5 | # Hold capslock for ctrl, tap for esc 6 | [[dual_role]] 7 | input = "KEY_CAPSLOCK" 8 | hold = ["KEY_LEFTCTRL"] 9 | tap = ["KEY_ESC"] 10 | 11 | # Function keys are remapped to alternate functions by 12 | # default, so arrange for ALT-FX to produce FX instead. 13 | # First, let's preserve the ability to switch virtual 14 | # terminal consoles for the first three slots 15 | [[remap]] 16 | input = ["KEY_LEFTALT", "KEY_LEFTCTRL", "KEY_F1"] 17 | output = ["KEY_LEFTALT", "KEY_LEFTCTRL", "KEY_F1"] 18 | 19 | [[remap]] 20 | input = ["KEY_LEFTALT", "KEY_LEFTCTRL", "KEY_F2"] 21 | output = ["KEY_LEFTALT", "KEY_LEFTCTRL", "KEY_F2"] 22 | 23 | [[remap]] 24 | input = ["KEY_LEFTALT", "KEY_LEFTCTRL", "KEY_F3"] 25 | output = ["KEY_LEFTALT", "KEY_LEFTCTRL", "KEY_F3"] 26 | 27 | [[remap]] 28 | input = ["KEY_LEFTALT", "KEY_F1"] 29 | output = ["KEY_F1"] 30 | 31 | [[remap]] 32 | input = ["KEY_LEFTALT", "KEY_F2"] 33 | output = ["KEY_F2"] 34 | 35 | [[remap]] 36 | input = ["KEY_LEFTALT", "KEY_F3"] 37 | output = ["KEY_F3"] 38 | 39 | [[remap]] 40 | input = ["KEY_LEFTALT", "KEY_F4"] 41 | output = ["KEY_F4"] 42 | 43 | [[remap]] 44 | input = ["KEY_LEFTALT", "KEY_F5"] 45 | output = ["KEY_F5"] 46 | 47 | [[remap]] 48 | input = ["KEY_LEFTALT", "KEY_F6"] 49 | output = ["KEY_F6"] 50 | 51 | [[remap]] 52 | input = ["KEY_LEFTALT", "KEY_F7"] 53 | output = ["KEY_F7"] 54 | 55 | [[remap]] 56 | input = ["KEY_LEFTALT", "KEY_F8"] 57 | output = ["KEY_F8"] 58 | 59 | [[remap]] 60 | input = ["KEY_LEFTALT", "KEY_F9"] 61 | output = ["KEY_F9"] 62 | 63 | [[remap]] 64 | input = ["KEY_F1"] 65 | output = ["KEY_BACK"] 66 | 67 | [[remap]] 68 | input = ["KEY_F2"] 69 | output = ["KEY_REFRESH"] 70 | 71 | [[remap]] 72 | input = ["KEY_F3"] 73 | # Copy! 74 | output = ["KEY_LEFTCTRL", "KEY_C"] 75 | 76 | [[remap]] 77 | input = ["KEY_F4"] 78 | # Paste! 79 | output = ["KEY_LEFTSHIFT", "KEY_INSERT"] 80 | 81 | [[remap]] 82 | input = ["KEY_LEFTALT", "KEY_F5"] 83 | output = ["KEY_KBDILLUMDOWN"] 84 | 85 | [[remap]] 86 | input = ["KEY_RIGHTALT", "KEY_F5"] 87 | output = ["KEY_KBDILLUMDOWN"] 88 | 89 | [[remap]] 90 | input = ["KEY_F5"] 91 | output = ["KEY_BRIGHTNESSDOWN"] 92 | 93 | [[remap]] 94 | input = ["KEY_LEFTALT", "KEY_F6"] 95 | output = ["KEY_KBDILLUMUP"] 96 | 97 | [[remap]] 98 | input = ["KEY_RIGHTALT", "KEY_F6"] 99 | output = ["KEY_KBDILLUMUP"] 100 | 101 | [[remap]] 102 | input = ["KEY_F6"] 103 | output = ["KEY_BRIGHTNESSUP"] 104 | 105 | [[remap]] 106 | input = ["KEY_F7"] 107 | output = ["KEY_PLAYPAUSE"] 108 | 109 | [[remap]] 110 | input = ["KEY_F8"] 111 | output = ["KEY_MUTE"] 112 | 113 | [[remap]] 114 | input = ["KEY_F9"] 115 | output = ["KEY_VOLUMEDOWN"] 116 | 117 | [[remap]] 118 | input = ["KEY_F10"] 119 | output = ["KEY_VOLUMEUP"] 120 | 121 | [[remap]] 122 | input = ["KEY_LEFTALT", "KEY_UP"] 123 | output = ["KEY_PAGEUP"] 124 | 125 | [[remap]] 126 | input = ["KEY_LEFTALT", "KEY_DOWN"] 127 | output = ["KEY_PAGEDOWN"] 128 | 129 | [[remap]] 130 | input = ["KEY_LEFTALT", "KEY_LEFT"] 131 | output = ["KEY_HOME"] 132 | 133 | [[remap]] 134 | input = ["KEY_LEFTALT", "KEY_RIGHT"] 135 | output = ["KEY_END"] 136 | -------------------------------------------------------------------------------- /src/mapping.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context; 2 | pub use evdev_rs::enums::{EventCode, EventType, EV_KEY as KeyCode}; 3 | use serde::Deserialize; 4 | use std::collections::HashSet; 5 | use std::path::Path; 6 | use thiserror::Error; 7 | 8 | #[derive(Debug, Clone)] 9 | pub struct MappingConfig { 10 | pub device_name: Option, 11 | pub phys: Option, 12 | pub mappings: Vec, 13 | } 14 | 15 | impl MappingConfig { 16 | pub fn from_file>(path: P) -> anyhow::Result { 17 | let path = path.as_ref(); 18 | let toml_data = std::fs::read_to_string(path) 19 | .context(format!("reading toml from {}", path.display()))?; 20 | let config_file: ConfigFile = 21 | toml::from_str(&toml_data).context(format!("parsing toml from {}", path.display()))?; 22 | let mut mappings = vec![]; 23 | for dual in config_file.dual_role { 24 | mappings.push(dual.into()); 25 | } 26 | for remap in config_file.remap { 27 | mappings.push(remap.into()); 28 | } 29 | Ok(Self { 30 | device_name: config_file.device_name, 31 | phys: config_file.phys, 32 | mappings, 33 | }) 34 | } 35 | } 36 | 37 | #[derive(Debug, Clone, Eq, PartialEq)] 38 | pub enum Mapping { 39 | DualRole { 40 | input: KeyCode, 41 | hold: Vec, 42 | tap: Vec, 43 | }, 44 | Remap { 45 | input: HashSet, 46 | output: HashSet, 47 | }, 48 | } 49 | 50 | #[derive(Debug, Deserialize)] 51 | #[serde(try_from = "String")] 52 | struct KeyCodeWrapper { 53 | pub code: KeyCode, 54 | } 55 | 56 | impl Into for KeyCodeWrapper { 57 | fn into(self) -> KeyCode { 58 | self.code 59 | } 60 | } 61 | 62 | #[derive(Error, Debug)] 63 | pub enum ConfigError { 64 | #[error("Invalid key `{0}`. Use `evremap list-keys` to see possible keys.")] 65 | InvalidKey(String), 66 | #[error("Impossible: parsed KEY_XXX but not into an EV_KEY")] 67 | ImpossibleParseKey, 68 | } 69 | 70 | impl std::convert::TryFrom for KeyCodeWrapper { 71 | type Error = ConfigError; 72 | fn try_from(s: String) -> Result { 73 | match EventCode::from_str(&EventType::EV_KEY, &s) { 74 | Some(code) => match code { 75 | EventCode::EV_KEY(code) => Ok(KeyCodeWrapper { code }), 76 | _ => Err(ConfigError::ImpossibleParseKey), 77 | }, 78 | None => Err(ConfigError::InvalidKey(s)), 79 | } 80 | } 81 | } 82 | 83 | #[derive(Debug, Deserialize)] 84 | struct DualRoleConfig { 85 | input: KeyCodeWrapper, 86 | hold: Vec, 87 | tap: Vec, 88 | } 89 | 90 | impl Into for DualRoleConfig { 91 | fn into(self) -> Mapping { 92 | Mapping::DualRole { 93 | input: self.input.into(), 94 | hold: self.hold.into_iter().map(Into::into).collect(), 95 | tap: self.tap.into_iter().map(Into::into).collect(), 96 | } 97 | } 98 | } 99 | 100 | #[derive(Debug, Deserialize)] 101 | struct RemapConfig { 102 | input: Vec, 103 | output: Vec, 104 | } 105 | 106 | impl Into for RemapConfig { 107 | fn into(self) -> Mapping { 108 | Mapping::Remap { 109 | input: self.input.into_iter().map(Into::into).collect(), 110 | output: self.output.into_iter().map(Into::into).collect(), 111 | } 112 | } 113 | } 114 | 115 | #[derive(Debug, Deserialize)] 116 | struct ConfigFile { 117 | #[serde(default)] 118 | device_name: Option, 119 | 120 | #[serde(default)] 121 | phys: Option, 122 | 123 | #[serde(default)] 124 | dual_role: Vec, 125 | 126 | #[serde(default)] 127 | remap: Vec, 128 | } 129 | -------------------------------------------------------------------------------- /src/deviceinfo.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{bail, Context, Result}; 2 | use evdev_rs::{Device, DeviceWrapper}; 3 | use std::cmp::Ordering; 4 | use std::path::PathBuf; 5 | 6 | #[derive(Debug, Clone)] 7 | pub struct DeviceInfo { 8 | pub name: String, 9 | pub path: PathBuf, 10 | pub phys: String, 11 | } 12 | 13 | impl DeviceInfo { 14 | pub fn with_path(path: PathBuf) -> Result { 15 | let f = std::fs::File::open(&path).context(format!("opening {}", path.display()))?; 16 | let input = Device::new_from_file(f) 17 | .with_context(|| format!("failed to create new Device from file {}", path.display()))?; 18 | 19 | Ok(Self { 20 | name: input.name().unwrap_or("").to_string(), 21 | phys: input.phys().unwrap_or("").to_string(), 22 | path, 23 | }) 24 | } 25 | 26 | pub fn with_name(name: &str, phys: Option<&str>) -> Result { 27 | let mut devices = Self::obtain_device_list()?; 28 | 29 | if let Some(phys) = phys { 30 | match devices.iter().position(|item| item.phys == phys) { 31 | Some(idx) => return Ok(devices.remove(idx)), 32 | None => { 33 | bail!( 34 | "Requested device `{}` with phys=`{}` was not found", 35 | name, 36 | phys 37 | ); 38 | } 39 | } 40 | } 41 | 42 | let mut devices_with_name: Vec<_> = devices 43 | .into_iter() 44 | .filter(|item| item.name == name) 45 | .collect(); 46 | 47 | if devices_with_name.is_empty() { 48 | bail!("No device found with name `{}`", name); 49 | } 50 | 51 | if devices_with_name.len() > 1 { 52 | log::warn!("The following devices match name `{}`:", name); 53 | for dev in &devices_with_name { 54 | log::warn!("{:?}", dev); 55 | } 56 | log::warn!( 57 | "evremap will use the first entry. If you want to \ 58 | use one of the others, add the corresponding phys \ 59 | value to your configuration, for example, \ 60 | `phys = \"{}\"` for the second entry in the list.", 61 | devices_with_name[1].phys 62 | ); 63 | } 64 | 65 | Ok(devices_with_name.remove(0)) 66 | } 67 | 68 | fn obtain_device_list() -> Result> { 69 | let mut devices = vec![]; 70 | for entry in std::fs::read_dir("/dev/input")? { 71 | let entry = entry?; 72 | 73 | if !entry 74 | .file_name() 75 | .to_str() 76 | .unwrap_or("") 77 | .starts_with("event") 78 | { 79 | continue; 80 | } 81 | let path = entry.path(); 82 | if path.is_dir() { 83 | continue; 84 | } 85 | 86 | match DeviceInfo::with_path(path) { 87 | Ok(item) => devices.push(item), 88 | Err(err) => log::error!("{:#}", err), 89 | } 90 | } 91 | 92 | // Order by name, but when multiple devices have the same name, 93 | // order by the event device unit number 94 | devices.sort_by(|a, b| match a.name.cmp(&b.name) { 95 | Ordering::Equal => { 96 | event_number_from_path(&a.path).cmp(&event_number_from_path(&b.path)) 97 | } 98 | different => different, 99 | }); 100 | Ok(devices) 101 | } 102 | } 103 | 104 | fn event_number_from_path(path: &PathBuf) -> u32 { 105 | match path.to_str() { 106 | Some(s) => match s.rfind("event") { 107 | Some(idx) => s[idx + 5..].parse().unwrap_or(0), 108 | None => 0, 109 | }, 110 | None => 0, 111 | } 112 | } 113 | 114 | pub fn list_devices() -> Result<()> { 115 | let devices = DeviceInfo::obtain_device_list()?; 116 | for item in &devices { 117 | println!("Name: {}", item.name); 118 | println!("Path: {}", item.path.display()); 119 | println!("Phys: {}", item.phys); 120 | println!(); 121 | } 122 | Ok(()) 123 | } 124 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # evremap 2 | 3 | *A keyboard input remapper for Linux/Wayland systems, written by @wez* 4 | 5 | ## Why? 6 | 7 | I couldn't find a good solution for the following: 8 | 9 | * Remap the `CAPSLOCK` key so that it produces `CTRL` when held, but `ESC` if tapped 10 | * Remap N keys to M keys. Eg: `F3` -> `CTRL+c`, and `ALT+LEFT` to `HOME` 11 | 12 | ## How? 13 | 14 | `evremap` works by grabbing exclusive access to an input device and maintaining 15 | a model of the keys that are pressed. It then applies your remapping configuration 16 | to produce the effective set of pressed keys and emits appropriate changes to a virtual 17 | output device. 18 | 19 | Because `evremap` targets the evdev layer of libinput, its remapping 20 | is effective system-wide: in Wayland, X11 and the linux console. 21 | 22 | ## Configuration 23 | 24 | Here's an example configuration that makes capslock useful: 25 | 26 | ```toml 27 | # The name of the device to remap. 28 | # Run `sudo evremap list-devices` to see the devices available 29 | # on your system. 30 | device_name = "AT Translated Set 2 keyboard" 31 | 32 | # If you have multiple devices with the same name, you can optionally 33 | # specify the `phys` value that is printed by the `list-devices` subcommand 34 | # phys = "usb-0000:07:00.3-2.1.1/input0" 35 | 36 | # Configure CAPSLOCK as a Dual Role key. 37 | # Holding it produces LEFTCTRL, but tapping it 38 | # will produce ESC. 39 | # Both `tap` and `hold` can expand to multiple output keys. 40 | [[dual_role]] 41 | input = "KEY_CAPSLOCK" 42 | hold = ["KEY_LEFTCTRL"] 43 | tap = ["KEY_ESC"] 44 | ``` 45 | 46 | You can also express simple remapping entries: 47 | 48 | ```toml 49 | # This config snippet is useful if your keyboard has an arrow 50 | # cluster, but doesn't have page up, page down, home or end 51 | # keys. Here we're configuring ALT+arrow to map to those functions. 52 | [[remap]] 53 | input = ["KEY_LEFTALT", "KEY_UP"] 54 | output = ["KEY_PAGEUP"] 55 | 56 | [[remap]] 57 | input = ["KEY_LEFTALT", "KEY_DOWN"] 58 | output = ["KEY_PAGEDOWN"] 59 | 60 | [[remap]] 61 | input = ["KEY_LEFTALT", "KEY_LEFT"] 62 | output = ["KEY_HOME"] 63 | 64 | [[remap]] 65 | input = ["KEY_LEFTALT", "KEY_RIGHT"] 66 | output = ["KEY_END"] 67 | ``` 68 | 69 | When applying remapping configuration, ordering is important: 70 | 71 | * Dual Role entries are always processed first 72 | * Remap entries are applied in the order that they appear in 73 | your configuration file 74 | 75 | Here's an example where ordering is important: on the PixelBook Go keyboard, 76 | the function key row has alternate functions on the keycaps. It is natural 77 | to want the mute button to mute by default, but to emit the F8 key when 78 | holding alt. We can express that with the following configuration: 79 | 80 | ```toml 81 | [[remap]] 82 | input = ["KEY_LEFTALT", "KEY_F8"] 83 | # When our `input` is matched, our list of `output` is prevented from 84 | # matching as the `input` of subsequent rules. 85 | output = ["KEY_F8"] 86 | 87 | [[remap]] 88 | input = ["KEY_F8"] 89 | output = ["KEY_MUTE"] 90 | ``` 91 | 92 | * How do I list available input devices? 93 | `sudo evremap list-devices` 94 | 95 | * How do I list available key codes? 96 | `evremap list-keys` 97 | 98 | * Is there a GUI for editing the config file? 99 | Yes, take a look at [Evremap-GUI](https://github.com/M8850/Evremap-GUI) 100 | 101 | ## Building it 102 | 103 | ```console 104 | $ sudo dnf install libevdev-devel # redhat/centos 105 | ## or 106 | $ sudo apt install libevdev-dev pkg-config # debian/ubuntu 107 | 108 | $ cargo build --release 109 | ``` 110 | 111 | ## Running it 112 | 113 | To run the remapper, invoke it *as root* (so that it can grab exclusive access to the input device): 114 | 115 | ```console 116 | $ sudo target/release/evremap remap my-config-file.toml 117 | ``` 118 | 119 | Or, grant an unprivileged user access to `evdev` and `uinput`. 120 | On Ubuntu, this can be configured by running the following commands and rebooting: 121 | 122 | ``` 123 | sudo gpasswd -a YOUR_USER input 124 | echo 'KERNEL=="uinput", GROUP="input"' | sudo tee /etc/udev/rules.d/input.rules 125 | ``` 126 | 127 | For some platforms, you might need to create an `input` group first and run: 128 | ``` 129 | echo 'KERNEL=="event*", NAME="input/%k", MODE="660", GROUP="input"' | sudo tee /etc/udev/rules.d/input.rules 130 | ``` 131 | as well. 132 | 133 | ## Systemd 134 | 135 | A sample system service unit is included in the repo. You'll want to adjust the paths to match 136 | your system and then install and enable it: 137 | 138 | ```console 139 | $ sudo cp evremap.service /usr/lib/systemd/system/ 140 | $ sudo systemctl daemon-reload 141 | $ sudo systemctl enable evremap.service 142 | $ sudo systemctl start evremap.service 143 | ``` 144 | 145 | ## Runit 146 | 147 | If you're using Runit instead of Systemd, follow these steps to create a service. 148 | 149 | * Create a directory called `evremap` and create a file called `run` under it 150 | ```console 151 | sudo mkdir /etc/sv/evremap 152 | sudo touch /etc/sv/evremap/run 153 | ``` 154 | 155 | * Copy these lines into the run file 156 | ```console 157 | #!/bin/sh 158 | set -e 159 | exec remap 160 | ``` 161 | 162 | Replace `` with the path to your evremap executable and `` with the path to your configuration file. 163 | 164 | * Finally, symlink the evremap directory to `/var/service` 165 | ```console 166 | sudo ln -s /etc/sv/evremap /var/service 167 | ``` 168 | 169 | ## OpenRC 170 | 171 | To make an OpenRC service, create the file `/etc/init.d/evremap` with the following contents... 172 | 173 | ```console 174 | #!/usr/bin/openrc-run 175 | 176 | supervisor=supervise-daemon 177 | command="" 178 | command_args="remap " 179 | ``` 180 | 181 | Replace `` with the path to your evremap executable and `` with the path to your configuration file. 182 | 183 | Make the file executable... 184 | 185 | ```console 186 | chmod +x /etc/init.d/evremap 187 | ``` 188 | 189 | Enable the service with... 190 | 191 | ```console 192 | rc-update add evremap 193 | ``` 194 | 195 | Start the service with... 196 | 197 | ```console 198 | rc-service evremap start 199 | ``` 200 | 201 | ## How do I make this execute a command when a key is pressed? 202 | 203 | That feature is not implemented. 204 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use crate::deviceinfo::DeviceInfo; 2 | use crate::mapping::*; 3 | use crate::remapper::*; 4 | use anyhow::{Context, Result}; 5 | use clap::Parser; 6 | use std::path::PathBuf; 7 | use std::time::Duration; 8 | 9 | mod deviceinfo; 10 | mod mapping; 11 | mod remapper; 12 | 13 | /// Remap libinput evdev keyboard inputs 14 | #[derive(Debug, Parser)] 15 | #[command(name = "evremap", about, author = "Wez Furlong")] 16 | enum Opt { 17 | /// Rather than running the remapper, list currently available devices. 18 | /// This is helpful to check their names when setting up the initial 19 | /// configuration 20 | ListDevices, 21 | 22 | /// Show a list of possible KEY_XXX values 23 | ListKeys, 24 | 25 | /// Listen to events and print them out to facilitate learning 26 | /// which keys/buttons have which labels for your device(s) 27 | DebugEvents { 28 | /// Specify the device name of interest 29 | #[arg(long)] 30 | device_name: String, 31 | 32 | /// Specify the phys device in case multiple devices have 33 | /// the same name 34 | #[arg(long)] 35 | phys: Option, 36 | }, 37 | 38 | /// Load a remapper config and run the remapper. 39 | /// This usually requires running as root to obtain exclusive access 40 | /// to the input devices. 41 | Remap { 42 | /// Specify the configuration file to be loaded 43 | #[arg(name = "CONFIG-FILE")] 44 | config_file: PathBuf, 45 | 46 | /// Number of seconds for user to release keys on startup 47 | #[arg(short, long, default_value = "2")] 48 | delay: f64, 49 | 50 | /// Override the device name specified by the config file 51 | #[arg(long)] 52 | device_name: Option, 53 | 54 | /// Override the phys device specified by the config file 55 | #[arg(long)] 56 | phys: Option, 57 | 58 | /// If the device isn't found on startup, wait forever 59 | /// until the device is plugged in. This works by polling 60 | /// the set of devices every few seconds. It is not as 61 | /// efficient as setting up a udev rule to spawn evremap, 62 | /// but is simpler to setup ad-hoc. 63 | #[arg(long)] 64 | wait_for_device: bool, 65 | }, 66 | } 67 | 68 | pub fn list_keys() -> Result<()> { 69 | let mut keys: Vec = EventCode::EV_KEY(KeyCode::KEY_RESERVED) 70 | .iter() 71 | .filter_map(|code| match code { 72 | EventCode::EV_KEY(_) => Some(format!("{}", code)), 73 | _ => None, 74 | }) 75 | .collect(); 76 | keys.sort(); 77 | for key in keys { 78 | println!("{}", key); 79 | } 80 | Ok(()) 81 | } 82 | 83 | fn setup_logger() { 84 | let mut builder = env_logger::Builder::new(); 85 | builder.filter_level(log::LevelFilter::Info); 86 | let env = env_logger::Env::new() 87 | .filter("EVREMAP_LOG") 88 | .write_style("EVREMAP_LOG_STYLE"); 89 | builder.parse_env(env); 90 | builder.init(); 91 | } 92 | 93 | fn get_device( 94 | device_name: &str, 95 | phys: Option<&str>, 96 | wait_for_device: bool, 97 | ) -> anyhow::Result { 98 | match deviceinfo::DeviceInfo::with_name(device_name, phys) { 99 | Ok(dev) => return Ok(dev), 100 | Err(err) if !wait_for_device => return Err(err), 101 | Err(err) => { 102 | log::warn!("{err:#}. Will wait until it is attached."); 103 | } 104 | } 105 | 106 | const MAX_SLEEP: Duration = Duration::from_secs(10); 107 | const ONE_SECOND: Duration = Duration::from_secs(1); 108 | let mut sleep = ONE_SECOND; 109 | 110 | loop { 111 | std::thread::sleep(sleep); 112 | sleep = (sleep + ONE_SECOND).min(MAX_SLEEP); 113 | 114 | match deviceinfo::DeviceInfo::with_name(device_name, phys) { 115 | Ok(dev) => return Ok(dev), 116 | Err(err) => { 117 | log::debug!("{err:#}"); 118 | } 119 | } 120 | } 121 | } 122 | 123 | fn debug_events(device: DeviceInfo) -> Result<()> { 124 | let f = 125 | std::fs::File::open(&device.path).context(format!("opening {}", device.path.display()))?; 126 | let input = evdev_rs::Device::new_from_file(f).with_context(|| { 127 | format!( 128 | "failed to create new Device from file {}", 129 | device.path.display() 130 | ) 131 | })?; 132 | 133 | loop { 134 | let (status, event) = 135 | input.next_event(evdev_rs::ReadFlag::NORMAL | evdev_rs::ReadFlag::BLOCKING)?; 136 | match status { 137 | evdev_rs::ReadStatus::Success => { 138 | if let EventCode::EV_KEY(key) = event.event_code { 139 | log::info!("{key:?} {}", event.value); 140 | } 141 | } 142 | evdev_rs::ReadStatus::Sync => anyhow::bail!("ReadStatus::Sync!"), 143 | } 144 | } 145 | } 146 | 147 | fn main() -> Result<()> { 148 | setup_logger(); 149 | let opt = Opt::parse(); 150 | 151 | match opt { 152 | Opt::ListDevices => deviceinfo::list_devices(), 153 | Opt::ListKeys => list_keys(), 154 | Opt::DebugEvents { device_name, phys } => { 155 | let device_info = get_device(&device_name, phys.as_deref(), false)?; 156 | debug_events(device_info) 157 | } 158 | Opt::Remap { 159 | config_file, 160 | delay, 161 | device_name, 162 | phys, 163 | wait_for_device, 164 | } => { 165 | let mut mapping_config = MappingConfig::from_file(&config_file).context(format!( 166 | "loading MappingConfig from {}", 167 | config_file.display() 168 | ))?; 169 | 170 | if let Some(device) = device_name { 171 | mapping_config.device_name = Some(device); 172 | } 173 | if let Some(phys) = phys { 174 | mapping_config.phys = Some(phys); 175 | } 176 | 177 | let device_name = mapping_config.device_name.as_deref().ok_or_else(|| { 178 | anyhow::anyhow!( 179 | "device_name is missing; \ 180 | specify it either in the config file or via the --device-name \ 181 | command line option" 182 | ) 183 | })?; 184 | 185 | log::warn!("Short delay: release any keys now!"); 186 | std::thread::sleep(Duration::from_secs_f64(delay)); 187 | 188 | let device_info = 189 | get_device(device_name, mapping_config.phys.as_deref(), wait_for_device)?; 190 | 191 | let mut mapper = InputMapper::create_mapper(device_info.path, mapping_config.mappings)?; 192 | mapper.run_mapper() 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /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 = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anstream" 16 | version = "0.6.14" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" 19 | dependencies = [ 20 | "anstyle", 21 | "anstyle-parse", 22 | "anstyle-query", 23 | "anstyle-wincon", 24 | "colorchoice", 25 | "is_terminal_polyfill", 26 | "utf8parse", 27 | ] 28 | 29 | [[package]] 30 | name = "anstyle" 31 | version = "1.0.7" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" 34 | 35 | [[package]] 36 | name = "anstyle-parse" 37 | version = "0.2.4" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" 40 | dependencies = [ 41 | "utf8parse", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle-query" 46 | version = "1.1.0" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" 49 | dependencies = [ 50 | "windows-sys", 51 | ] 52 | 53 | [[package]] 54 | name = "anstyle-wincon" 55 | version = "3.0.3" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" 58 | dependencies = [ 59 | "anstyle", 60 | "windows-sys", 61 | ] 62 | 63 | [[package]] 64 | name = "anyhow" 65 | version = "1.0.86" 66 | source = "registry+https://github.com/rust-lang/crates.io-index" 67 | checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" 68 | 69 | [[package]] 70 | name = "bitflags" 71 | version = "1.3.2" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 74 | 75 | [[package]] 76 | name = "cc" 77 | version = "1.0.99" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" 80 | 81 | [[package]] 82 | name = "clap" 83 | version = "4.5.7" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f" 86 | dependencies = [ 87 | "clap_builder", 88 | "clap_derive", 89 | ] 90 | 91 | [[package]] 92 | name = "clap_builder" 93 | version = "4.5.7" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f" 96 | dependencies = [ 97 | "anstream", 98 | "anstyle", 99 | "clap_lex", 100 | "strsim", 101 | ] 102 | 103 | [[package]] 104 | name = "clap_derive" 105 | version = "4.5.5" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6" 108 | dependencies = [ 109 | "heck", 110 | "proc-macro2", 111 | "quote", 112 | "syn", 113 | ] 114 | 115 | [[package]] 116 | name = "clap_lex" 117 | version = "0.7.1" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" 120 | 121 | [[package]] 122 | name = "colorchoice" 123 | version = "1.0.1" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" 126 | 127 | [[package]] 128 | name = "env_filter" 129 | version = "0.1.0" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" 132 | dependencies = [ 133 | "log", 134 | "regex", 135 | ] 136 | 137 | [[package]] 138 | name = "env_logger" 139 | version = "0.11.3" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9" 142 | dependencies = [ 143 | "anstream", 144 | "anstyle", 145 | "env_filter", 146 | "humantime", 147 | "log", 148 | ] 149 | 150 | [[package]] 151 | name = "equivalent" 152 | version = "1.0.1" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 155 | 156 | [[package]] 157 | name = "evdev-rs" 158 | version = "0.6.1" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "9812d5790fb6fcce449333eb6713dad335e8c979225ed98755c84a3987e06dba" 161 | dependencies = [ 162 | "bitflags", 163 | "evdev-sys", 164 | "libc", 165 | "log", 166 | ] 167 | 168 | [[package]] 169 | name = "evdev-sys" 170 | version = "0.2.5" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "14ead42b547b15d47089c1243d907bcf0eb94e457046d3b315a26ac9c9e9ea6d" 173 | dependencies = [ 174 | "cc", 175 | "libc", 176 | "pkg-config", 177 | ] 178 | 179 | [[package]] 180 | name = "evremap" 181 | version = "0.1.0" 182 | dependencies = [ 183 | "anyhow", 184 | "clap", 185 | "env_logger", 186 | "evdev-rs", 187 | "libc", 188 | "log", 189 | "serde", 190 | "thiserror", 191 | "toml", 192 | ] 193 | 194 | [[package]] 195 | name = "hashbrown" 196 | version = "0.14.5" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 199 | 200 | [[package]] 201 | name = "heck" 202 | version = "0.5.0" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 205 | 206 | [[package]] 207 | name = "humantime" 208 | version = "2.1.0" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 211 | 212 | [[package]] 213 | name = "indexmap" 214 | version = "2.2.6" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" 217 | dependencies = [ 218 | "equivalent", 219 | "hashbrown", 220 | ] 221 | 222 | [[package]] 223 | name = "is_terminal_polyfill" 224 | version = "1.70.0" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" 227 | 228 | [[package]] 229 | name = "libc" 230 | version = "0.2.155" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" 233 | 234 | [[package]] 235 | name = "log" 236 | version = "0.4.21" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" 239 | 240 | [[package]] 241 | name = "memchr" 242 | version = "2.7.4" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 245 | 246 | [[package]] 247 | name = "pkg-config" 248 | version = "0.3.30" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" 251 | 252 | [[package]] 253 | name = "proc-macro2" 254 | version = "1.0.85" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" 257 | dependencies = [ 258 | "unicode-ident", 259 | ] 260 | 261 | [[package]] 262 | name = "quote" 263 | version = "1.0.36" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 266 | dependencies = [ 267 | "proc-macro2", 268 | ] 269 | 270 | [[package]] 271 | name = "regex" 272 | version = "1.10.5" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" 275 | dependencies = [ 276 | "aho-corasick", 277 | "memchr", 278 | "regex-automata", 279 | "regex-syntax", 280 | ] 281 | 282 | [[package]] 283 | name = "regex-automata" 284 | version = "0.4.7" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" 287 | dependencies = [ 288 | "aho-corasick", 289 | "memchr", 290 | "regex-syntax", 291 | ] 292 | 293 | [[package]] 294 | name = "regex-syntax" 295 | version = "0.8.4" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" 298 | 299 | [[package]] 300 | name = "serde" 301 | version = "1.0.203" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" 304 | dependencies = [ 305 | "serde_derive", 306 | ] 307 | 308 | [[package]] 309 | name = "serde_derive" 310 | version = "1.0.203" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" 313 | dependencies = [ 314 | "proc-macro2", 315 | "quote", 316 | "syn", 317 | ] 318 | 319 | [[package]] 320 | name = "serde_spanned" 321 | version = "0.6.6" 322 | source = "registry+https://github.com/rust-lang/crates.io-index" 323 | checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" 324 | dependencies = [ 325 | "serde", 326 | ] 327 | 328 | [[package]] 329 | name = "strsim" 330 | version = "0.11.1" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 333 | 334 | [[package]] 335 | name = "syn" 336 | version = "2.0.66" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" 339 | dependencies = [ 340 | "proc-macro2", 341 | "quote", 342 | "unicode-ident", 343 | ] 344 | 345 | [[package]] 346 | name = "thiserror" 347 | version = "1.0.61" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" 350 | dependencies = [ 351 | "thiserror-impl", 352 | ] 353 | 354 | [[package]] 355 | name = "thiserror-impl" 356 | version = "1.0.61" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" 359 | dependencies = [ 360 | "proc-macro2", 361 | "quote", 362 | "syn", 363 | ] 364 | 365 | [[package]] 366 | name = "toml" 367 | version = "0.8.14" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" 370 | dependencies = [ 371 | "serde", 372 | "serde_spanned", 373 | "toml_datetime", 374 | "toml_edit", 375 | ] 376 | 377 | [[package]] 378 | name = "toml_datetime" 379 | version = "0.6.6" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" 382 | dependencies = [ 383 | "serde", 384 | ] 385 | 386 | [[package]] 387 | name = "toml_edit" 388 | version = "0.22.14" 389 | source = "registry+https://github.com/rust-lang/crates.io-index" 390 | checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" 391 | dependencies = [ 392 | "indexmap", 393 | "serde", 394 | "serde_spanned", 395 | "toml_datetime", 396 | "winnow", 397 | ] 398 | 399 | [[package]] 400 | name = "unicode-ident" 401 | version = "1.0.12" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 404 | 405 | [[package]] 406 | name = "utf8parse" 407 | version = "0.2.2" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 410 | 411 | [[package]] 412 | name = "windows-sys" 413 | version = "0.52.0" 414 | source = "registry+https://github.com/rust-lang/crates.io-index" 415 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 416 | dependencies = [ 417 | "windows-targets", 418 | ] 419 | 420 | [[package]] 421 | name = "windows-targets" 422 | version = "0.52.5" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" 425 | dependencies = [ 426 | "windows_aarch64_gnullvm", 427 | "windows_aarch64_msvc", 428 | "windows_i686_gnu", 429 | "windows_i686_gnullvm", 430 | "windows_i686_msvc", 431 | "windows_x86_64_gnu", 432 | "windows_x86_64_gnullvm", 433 | "windows_x86_64_msvc", 434 | ] 435 | 436 | [[package]] 437 | name = "windows_aarch64_gnullvm" 438 | version = "0.52.5" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" 441 | 442 | [[package]] 443 | name = "windows_aarch64_msvc" 444 | version = "0.52.5" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" 447 | 448 | [[package]] 449 | name = "windows_i686_gnu" 450 | version = "0.52.5" 451 | source = "registry+https://github.com/rust-lang/crates.io-index" 452 | checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" 453 | 454 | [[package]] 455 | name = "windows_i686_gnullvm" 456 | version = "0.52.5" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" 459 | 460 | [[package]] 461 | name = "windows_i686_msvc" 462 | version = "0.52.5" 463 | source = "registry+https://github.com/rust-lang/crates.io-index" 464 | checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" 465 | 466 | [[package]] 467 | name = "windows_x86_64_gnu" 468 | version = "0.52.5" 469 | source = "registry+https://github.com/rust-lang/crates.io-index" 470 | checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" 471 | 472 | [[package]] 473 | name = "windows_x86_64_gnullvm" 474 | version = "0.52.5" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" 477 | 478 | [[package]] 479 | name = "windows_x86_64_msvc" 480 | version = "0.52.5" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" 483 | 484 | [[package]] 485 | name = "winnow" 486 | version = "0.6.13" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" 489 | dependencies = [ 490 | "memchr", 491 | ] 492 | -------------------------------------------------------------------------------- /src/remapper.rs: -------------------------------------------------------------------------------- 1 | use crate::mapping::*; 2 | use anyhow::*; 3 | use evdev_rs::{Device, DeviceWrapper, GrabMode, InputEvent, ReadFlag, TimeVal, UInputDevice}; 4 | use std::cmp::Ordering; 5 | use std::collections::{HashMap, HashSet}; 6 | use std::path::Path; 7 | use std::time::Duration; 8 | 9 | #[derive(Clone, Copy, Debug)] 10 | enum KeyEventType { 11 | Release, 12 | Press, 13 | Repeat, 14 | Unknown(i32), 15 | } 16 | 17 | impl KeyEventType { 18 | fn from_value(value: i32) -> Self { 19 | match value { 20 | 0 => KeyEventType::Release, 21 | 1 => KeyEventType::Press, 22 | 2 => KeyEventType::Repeat, 23 | _ => KeyEventType::Unknown(value), 24 | } 25 | } 26 | 27 | fn value(&self) -> i32 { 28 | match self { 29 | Self::Release => 0, 30 | Self::Press => 1, 31 | Self::Repeat => 2, 32 | Self::Unknown(n) => *n, 33 | } 34 | } 35 | } 36 | 37 | fn timeval_diff(newer: &TimeVal, older: &TimeVal) -> Duration { 38 | const MICROS_PER_SECOND: libc::time_t = 1000000; 39 | let secs = newer.tv_sec - older.tv_sec; 40 | let usecs = newer.tv_usec - older.tv_usec; 41 | 42 | let (secs, usecs) = if usecs < 0 { 43 | (secs - 1, usecs + MICROS_PER_SECOND) 44 | } else { 45 | (secs, usecs) 46 | }; 47 | 48 | Duration::from_micros(((secs * MICROS_PER_SECOND) + usecs) as u64) 49 | } 50 | 51 | pub struct InputMapper { 52 | input: Device, 53 | output: UInputDevice, 54 | /// If present in this map, the key is down since the instant 55 | /// of its associated value 56 | input_state: HashMap, 57 | 58 | mappings: Vec, 59 | 60 | /// The most recent candidate for a tap function is held here 61 | tapping: Option, 62 | 63 | output_keys: HashSet, 64 | } 65 | 66 | fn enable_key_code(input: &mut Device, key: KeyCode) -> Result<()> { 67 | input 68 | .enable(EventCode::EV_KEY(key.clone())) 69 | .context(format!("enable key {:?}", key))?; 70 | Ok(()) 71 | } 72 | 73 | impl InputMapper { 74 | pub fn create_mapper>(path: P, mappings: Vec) -> Result { 75 | let path = path.as_ref(); 76 | let f = std::fs::File::open(path).context(format!("opening {}", path.display()))?; 77 | let mut input = Device::new_from_file(f) 78 | .with_context(|| format!("failed to create new Device from file {}", path.display()))?; 79 | 80 | input.set_name(&format!("evremap Virtual input for {}", path.display())); 81 | 82 | // Ensure that any remapped keys are supported by the generated output device 83 | for map in &mappings { 84 | match map { 85 | Mapping::DualRole { tap, hold, .. } => { 86 | for t in tap { 87 | enable_key_code(&mut input, t.clone())?; 88 | } 89 | for h in hold { 90 | enable_key_code(&mut input, h.clone())?; 91 | } 92 | } 93 | Mapping::Remap { output, .. } => { 94 | for o in output { 95 | enable_key_code(&mut input, o.clone())?; 96 | } 97 | } 98 | } 99 | } 100 | 101 | let output = UInputDevice::create_from_device(&input) 102 | .context(format!("creating UInputDevice from {}", path.display()))?; 103 | 104 | input 105 | .grab(GrabMode::Grab) 106 | .context(format!("grabbing exclusive access on {}", path.display()))?; 107 | 108 | Ok(Self { 109 | input, 110 | output, 111 | input_state: HashMap::new(), 112 | output_keys: HashSet::new(), 113 | tapping: None, 114 | mappings, 115 | }) 116 | } 117 | 118 | pub fn run_mapper(&mut self) -> Result<()> { 119 | log::info!("Going into read loop"); 120 | loop { 121 | let (status, event) = self 122 | .input 123 | .next_event(ReadFlag::NORMAL | ReadFlag::BLOCKING)?; 124 | match status { 125 | evdev_rs::ReadStatus::Success => { 126 | if let EventCode::EV_KEY(ref key) = event.event_code { 127 | log::trace!("IN {:?}", event); 128 | self.update_with_event(&event, key.clone())?; 129 | } else { 130 | log::trace!("PASSTHRU {:?}", event); 131 | self.output.write_event(&event)?; 132 | } 133 | } 134 | evdev_rs::ReadStatus::Sync => bail!("ReadStatus::Sync!"), 135 | } 136 | } 137 | } 138 | 139 | /// Compute the effective set of keys that are pressed 140 | fn compute_keys(&self) -> HashSet { 141 | // Start with the input keys 142 | let mut keys: HashSet = self.input_state.keys().cloned().collect(); 143 | 144 | // First phase is to apply any DualRole mappings as they are likely to 145 | // be used to produce modifiers when held. 146 | for map in &self.mappings { 147 | if let Mapping::DualRole { input, hold, .. } = map { 148 | if keys.contains(input) { 149 | keys.remove(input); 150 | for h in hold { 151 | keys.insert(h.clone()); 152 | } 153 | } 154 | } 155 | } 156 | 157 | let mut keys_minus_remapped = keys.clone(); 158 | 159 | // Second pass to apply Remap items 160 | for map in &self.mappings { 161 | if let Mapping::Remap { input, output } = map { 162 | if input.is_subset(&keys_minus_remapped) { 163 | for i in input { 164 | keys.remove(i); 165 | if !is_modifier(i) { 166 | keys_minus_remapped.remove(i); 167 | } 168 | } 169 | for o in output { 170 | keys.insert(o.clone()); 171 | // Outputs that apply are not visible as 172 | // inputs for later remap rules 173 | if !is_modifier(o) { 174 | keys_minus_remapped.remove(o); 175 | } 176 | } 177 | } 178 | } 179 | } 180 | 181 | keys 182 | } 183 | 184 | /// Compute the difference between our desired set of keys 185 | /// and the set of keys that are currently pressed in the 186 | /// output device. 187 | /// Release any keys that should not be pressed, and then 188 | /// press any keys that should be pressed. 189 | /// 190 | /// When releasing, release modifiers last so that mappings 191 | /// that produce eg: CTRL-C don't emit a random C character 192 | /// when released. 193 | /// 194 | /// Similarly, when pressing, emit modifiers first so that 195 | /// we don't emit C and then CTRL for such a mapping. 196 | fn compute_and_apply_keys(&mut self, time: &TimeVal) -> Result<()> { 197 | let desired_keys = self.compute_keys(); 198 | let mut to_release: Vec = self 199 | .output_keys 200 | .difference(&desired_keys) 201 | .cloned() 202 | .collect(); 203 | 204 | let mut to_press: Vec = desired_keys 205 | .difference(&self.output_keys) 206 | .cloned() 207 | .collect(); 208 | 209 | if !to_release.is_empty() { 210 | to_release.sort_by(modifiers_last); 211 | self.emit_keys(&to_release, time, KeyEventType::Release)?; 212 | } 213 | if !to_press.is_empty() { 214 | to_press.sort_by(modifiers_first); 215 | self.emit_keys(&to_press, time, KeyEventType::Press)?; 216 | } 217 | Ok(()) 218 | } 219 | 220 | fn lookup_dual_role_mapping(&self, code: KeyCode) -> Option { 221 | for map in &self.mappings { 222 | if let Mapping::DualRole { input, .. } = map { 223 | if *input == code { 224 | // A DualRole mapping has the highest precedence 225 | // so we've found our match 226 | return Some(map.clone()); 227 | } 228 | } 229 | } 230 | None 231 | } 232 | 233 | fn lookup_mapping(&self, code: KeyCode) -> Option { 234 | let mut candidates = vec![]; 235 | 236 | for map in &self.mappings { 237 | match map { 238 | Mapping::DualRole { input, .. } => { 239 | if *input == code { 240 | // A DualRole mapping has the highest precedence 241 | // so we've found our match 242 | return Some(map.clone()); 243 | } 244 | } 245 | Mapping::Remap { input, .. } => { 246 | // Look for a mapping that includes the current key. 247 | // If part of a chord, all of its component keys must 248 | // also be pressed. 249 | let mut code_matched = false; 250 | let mut all_matched = true; 251 | for i in input { 252 | if *i == code { 253 | code_matched = true; 254 | } else if !self.input_state.contains_key(i) { 255 | all_matched = false; 256 | break; 257 | } 258 | } 259 | if code_matched && all_matched { 260 | candidates.push(map); 261 | } 262 | } 263 | } 264 | } 265 | 266 | // Any matches must be Remap entries. We want the one 267 | // with the most active keys 268 | candidates.sort_by(|a, b| match (a, b) { 269 | (Mapping::Remap { input: input_a, .. }, Mapping::Remap { input: input_b, .. }) => { 270 | input_a.len().cmp(&input_b.len()).reverse() 271 | } 272 | _ => unreachable!(), 273 | }); 274 | 275 | candidates.get(0).map(|&m| m.clone()) 276 | } 277 | 278 | pub fn update_with_event(&mut self, event: &InputEvent, code: KeyCode) -> Result<()> { 279 | let event_type = KeyEventType::from_value(event.value); 280 | match event_type { 281 | KeyEventType::Release => { 282 | let pressed_at = match self.input_state.remove(&code) { 283 | None => { 284 | self.write_event_and_sync(event)?; 285 | return Ok(()); 286 | } 287 | Some(p) => p, 288 | }; 289 | 290 | self.compute_and_apply_keys(&event.time)?; 291 | 292 | if let Some(Mapping::DualRole { tap, .. }) = 293 | self.lookup_dual_role_mapping(code.clone()) 294 | { 295 | // If released quickly enough, becomes a tap press. 296 | if let Some(tapping) = self.tapping.take() { 297 | if tapping == code 298 | && timeval_diff(&event.time, &pressed_at) <= Duration::from_millis(200) 299 | { 300 | self.emit_keys(&tap, &event.time, KeyEventType::Press)?; 301 | self.emit_keys(&tap, &event.time, KeyEventType::Release)?; 302 | } 303 | } 304 | } 305 | } 306 | KeyEventType::Press => { 307 | self.input_state.insert(code.clone(), event.time.clone()); 308 | 309 | match self.lookup_mapping(code.clone()) { 310 | Some(_) => { 311 | self.compute_and_apply_keys(&event.time)?; 312 | self.tapping.replace(code); 313 | } 314 | None => { 315 | // Just pass it through 316 | self.cancel_pending_tap(); 317 | self.compute_and_apply_keys(&event.time)?; 318 | } 319 | } 320 | } 321 | KeyEventType::Repeat => { 322 | match self.lookup_mapping(code.clone()) { 323 | Some(Mapping::DualRole { hold, .. }) => { 324 | self.emit_keys(&hold, &event.time, KeyEventType::Repeat)?; 325 | } 326 | Some(Mapping::Remap { output, .. }) => { 327 | let output: Vec = output.iter().cloned().collect(); 328 | self.emit_keys(&output, &event.time, KeyEventType::Repeat)?; 329 | } 330 | None => { 331 | // Just pass it through 332 | self.cancel_pending_tap(); 333 | self.write_event_and_sync(event)?; 334 | } 335 | } 336 | } 337 | KeyEventType::Unknown(_) => { 338 | self.write_event_and_sync(event)?; 339 | } 340 | } 341 | 342 | Ok(()) 343 | } 344 | 345 | fn cancel_pending_tap(&mut self) { 346 | self.tapping.take(); 347 | } 348 | 349 | fn emit_keys( 350 | &mut self, 351 | key: &[KeyCode], 352 | time: &TimeVal, 353 | event_type: KeyEventType, 354 | ) -> Result<()> { 355 | for k in key { 356 | let event = make_event(k.clone(), time, event_type); 357 | self.write_event(&event)?; 358 | } 359 | self.generate_sync_event(time)?; 360 | Ok(()) 361 | } 362 | 363 | fn write_event_and_sync(&mut self, event: &InputEvent) -> Result<()> { 364 | self.write_event(event)?; 365 | self.generate_sync_event(&event.time)?; 366 | Ok(()) 367 | } 368 | 369 | fn write_event(&mut self, event: &InputEvent) -> Result<()> { 370 | log::trace!("OUT: {:?}", event); 371 | self.output.write_event(&event)?; 372 | if let EventCode::EV_KEY(ref key) = event.event_code { 373 | let event_type = KeyEventType::from_value(event.value); 374 | match event_type { 375 | KeyEventType::Press | KeyEventType::Repeat => { 376 | self.output_keys.insert(key.clone()); 377 | } 378 | KeyEventType::Release => { 379 | self.output_keys.remove(key); 380 | } 381 | _ => {} 382 | } 383 | } 384 | Ok(()) 385 | } 386 | 387 | fn generate_sync_event(&self, time: &TimeVal) -> Result<()> { 388 | self.output.write_event(&InputEvent::new( 389 | time, 390 | &EventCode::EV_SYN(evdev_rs::enums::EV_SYN::SYN_REPORT), 391 | 0, 392 | ))?; 393 | Ok(()) 394 | } 395 | } 396 | 397 | fn make_event(key: KeyCode, time: &TimeVal, event_type: KeyEventType) -> InputEvent { 398 | InputEvent::new(time, &EventCode::EV_KEY(key), event_type.value()) 399 | } 400 | 401 | fn is_modifier(key: &KeyCode) -> bool { 402 | match key { 403 | KeyCode::KEY_FN 404 | | KeyCode::KEY_LEFTALT 405 | | KeyCode::KEY_RIGHTALT 406 | | KeyCode::KEY_LEFTMETA 407 | | KeyCode::KEY_RIGHTMETA 408 | | KeyCode::KEY_LEFTCTRL 409 | | KeyCode::KEY_RIGHTCTRL 410 | | KeyCode::KEY_LEFTSHIFT 411 | | KeyCode::KEY_RIGHTSHIFT => true, 412 | _ => false, 413 | } 414 | } 415 | 416 | /// Orders modifier keys ahead of non-modifier keys. 417 | /// Unfortunately the underlying type doesn't allow direct 418 | /// comparison, but that's ok for our purposes. 419 | fn modifiers_first(a: &KeyCode, b: &KeyCode) -> Ordering { 420 | if is_modifier(a) { 421 | if is_modifier(b) { 422 | Ordering::Equal 423 | } else { 424 | Ordering::Less 425 | } 426 | } else if is_modifier(b) { 427 | Ordering::Greater 428 | } else { 429 | // Neither are modifiers 430 | Ordering::Equal 431 | } 432 | } 433 | 434 | fn modifiers_last(a: &KeyCode, b: &KeyCode) -> Ordering { 435 | modifiers_first(a, b).reverse() 436 | } 437 | --------------------------------------------------------------------------------