├── .gitignore ├── src ├── core │ ├── shared.rs │ ├── mod.rs │ ├── mapping.rs │ ├── proxy.rs │ ├── buffer.rs │ ├── event.rs │ ├── layer.rs │ ├── input.rs │ ├── tap_dance.rs │ ├── combo.rs │ └── adapter.rs ├── cli │ ├── utils │ │ ├── mod.rs │ │ ├── device.rs │ │ └── systemctl.rs │ ├── commands │ │ ├── mod.rs │ │ ├── device.rs │ │ ├── start.rs │ │ └── service.rs │ └── mod.rs ├── config │ ├── mod.rs │ ├── defaults.rs │ └── schema.rs ├── fs │ ├── mod.rs │ ├── device.rs │ ├── service.rs │ └── config.rs ├── lib.rs ├── tests │ ├── mod.rs │ ├── config │ │ ├── mappings.yaml │ │ ├── combos.yaml │ │ ├── tap_dances.yaml │ │ ├── macros.yaml │ │ ├── shift.yaml │ │ └── layers.yaml │ ├── mapping.rs │ ├── shift.rs │ ├── schema.rs │ ├── tap_dance.rs │ ├── combo.rs │ ├── macros.rs │ ├── utils.rs │ └── layers.rs └── main.rs ├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── CHANGELOG.md ├── examples ├── default_settings.yaml ├── combo_hrm.yaml ├── tap_dance_hrm.yaml ├── key_mapping.yaml ├── macro_types.yaml └── personal_config.yaml ├── Cargo.toml ├── LICENSE ├── assets └── logo.svg ├── README.md ├── schema └── okey.json └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /src/core/shared.rs: -------------------------------------------------------------------------------- 1 | pub type RawKeyCode = u16; 2 | -------------------------------------------------------------------------------- /src/cli/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod device; 2 | pub mod systemctl; 3 | -------------------------------------------------------------------------------- /src/config/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod defaults; 2 | pub mod schema; 3 | -------------------------------------------------------------------------------- /src/fs/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod config; 2 | pub mod device; 3 | pub mod service; 4 | -------------------------------------------------------------------------------- /src/cli/commands/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod device; 2 | pub mod service; 3 | pub mod start; 4 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod cli; 2 | 3 | mod config; 4 | mod core; 5 | mod fs; 6 | 7 | #[cfg(test)] 8 | mod tests; 9 | -------------------------------------------------------------------------------- /src/tests/mod.rs: -------------------------------------------------------------------------------- 1 | mod combo; 2 | mod layers; 3 | mod macros; 4 | mod mapping; 5 | mod schema; 6 | mod shift; 7 | mod tap_dance; 8 | mod utils; 9 | -------------------------------------------------------------------------------- /src/cli/utils/device.rs: -------------------------------------------------------------------------------- 1 | use evdev::{Device, KeyCode}; 2 | 3 | pub fn is_keyboard(device: &Device) -> bool { 4 | device 5 | .supported_keys() 6 | .is_some_and(|keys| keys.contains(KeyCode::KEY_A)) 7 | } 8 | -------------------------------------------------------------------------------- /src/core/mod.rs: -------------------------------------------------------------------------------- 1 | mod adapter; 2 | mod buffer; 3 | mod combo; 4 | mod event; 5 | mod input; 6 | mod layer; 7 | mod mapping; 8 | mod proxy; 9 | mod shared; 10 | mod tap_dance; 11 | 12 | pub use adapter::KeyAdapter; 13 | pub use proxy::InputProxy; 14 | 15 | #[cfg(test)] 16 | pub use proxy::EventProxy; 17 | -------------------------------------------------------------------------------- /src/tests/config/mappings.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/luckasRanarison/okey/refs/heads/master/schema/okey.json 2 | 3 | keyboards: 4 | - name: "Mapping test keyboard" 5 | 6 | keys: 7 | KEY_Q: KEY_W 8 | KEY_Z: KEY_CUSTOM 9 | KEY_CUSTOM: KEY_A 10 | -------------------------------------------------------------------------------- /src/tests/config/combos.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/luckasRanarison/okey/refs/heads/master/schema/okey.json 2 | 3 | keyboards: 4 | - name: "Combo test keyboard" 5 | 6 | combos: 7 | - keys: [KEY_D, KEY_F] 8 | action: KEY_LEFTCTRL 9 | 10 | - keys: [KEY_U, KEY_I] 11 | action: [KEY_H, KEY_E, KEY_Y] 12 | -------------------------------------------------------------------------------- /src/config/defaults.rs: -------------------------------------------------------------------------------- 1 | #[rustfmt::skip] 2 | mod constants { 3 | pub fn event_poll_timeout() -> u16 { 1 } 4 | pub fn combo_threshold() -> u16 { 10 } 5 | pub fn tap_dance_timeout() -> u16 { 200 } 6 | pub fn deferred_key_delay() -> u16 { 0 } 7 | pub fn unicode_input_delay() -> u16 { 50 } 8 | pub fn maximum_lookup_depth() -> u8 { 10 } 9 | } 10 | 11 | pub use constants::*; 12 | -------------------------------------------------------------------------------- /src/tests/config/tap_dances.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/luckasRanarison/okey/refs/heads/master/schema/okey.json 2 | 3 | keyboards: 4 | - name: "Tap dance test keyboard" 5 | 6 | tap_dances: 7 | KEY_S: 8 | tap: KEY_S 9 | hold: KEY_LEFTSHIFT 10 | 11 | KEY_H: 12 | tap: [KEY_H, KEY_I] 13 | hold: [KEY_H, KEY_E, KEY_Y] 14 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 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 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Build 20 | run: cargo build --verbose 21 | 22 | - name: Test 23 | run: cargo test 24 | 25 | - name: Lint 26 | run: cargo clippy -- -D warnings 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v0.1.2 - 01-05-2025 2 | 3 | - Added special keycodes for shifted characters. 4 | - Fixed string macros input. 5 | - Optimize build size. (https://github.com/luckasRanarison/okey/issues/2) 6 | 7 | ## v0.1.1 - 30-04-2025 8 | 9 | Small fixes, removed unused dependencies and added release actions for building binaries. 10 | 11 | ## v0.1.0 - 29-04-2025 12 | 13 | The first version of `okey`, featuring: 14 | 15 | - Key remapping 16 | - Macros 17 | - Combos 18 | - Tap dance (multi-function keys on tap/hold) 19 | - Virtual layers 20 | -------------------------------------------------------------------------------- /examples/default_settings.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/luckasRanarison/okey/refs/heads/master/schema/okey.json 2 | 3 | defaults: 4 | general: 5 | deferred_key_delay: 20 # Delay for inserting keys following a non-acknowledged special key 6 | unicode_input_delay: 50 # Delay for inserting unicode macros to allow correct flushing 7 | event_poll_timeout: 1 # Main event loop interval 8 | 9 | tap_dance: 10 | default_timeout: 200 # Fallback tap dance timeout 11 | 12 | combo: 13 | default_threshold: 50 # Combo detection window 14 | 15 | keyboards: 16 | - name: "My keyboard" 17 | keys: {} 18 | -------------------------------------------------------------------------------- /src/tests/config/macros.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/luckasRanarison/okey/refs/heads/master/schema/okey.json 2 | 3 | keyboards: 4 | - name: "Macro test keyboard" 5 | 6 | keys: 7 | KEY_Q: [KEY_H, KEY_E, KEY_L, KEY_L, KEY_O] 8 | KEY_R: { string: "Hi, you!" } 9 | KEY_W: { env: FOO } 10 | KEY_T: { unicode: 🙂👍 } 11 | KEY_Z: { shell: "echo 'foo'", trim: true } 12 | 13 | KEY_X: 14 | [ 15 | { press: KEY_LEFTSHIFT }, 16 | { hold: KEY_LEFTSHIFT }, 17 | { press: KEY_O }, 18 | { release: KEY_O }, 19 | { release: KEY_LEFTSHIFT }, 20 | { delay: 500 }, 21 | KEY_K, 22 | ] 23 | -------------------------------------------------------------------------------- /src/core/mapping.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::config::schema::{KeyAction, KeyCode}; 4 | 5 | use super::shared::RawKeyCode; 6 | 7 | #[derive(Debug)] 8 | pub struct MappingManager { 9 | mappings: HashMap, 10 | } 11 | 12 | impl MappingManager { 13 | pub fn new(mappings: HashMap) -> Self { 14 | Self { 15 | mappings: mappings 16 | .into_iter() 17 | .map(|(key, value)| (key.value(), value)) 18 | .collect(), 19 | } 20 | } 21 | 22 | pub fn map(&self, code: &RawKeyCode) -> KeyAction { 23 | self.mappings 24 | .get(code) 25 | .cloned() 26 | .unwrap_or(KeyAction::KeyCode(KeyCode::new(*code))) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/tests/config/shift.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/luckasRanarison/okey/refs/heads/master/schema/okey.json 2 | 3 | keyboards: 4 | - name: "Shift test keyboard" 5 | 6 | keys: 7 | KEY_Q: KEY_EXCLAMATION 8 | KEY_W: KEY_AT 9 | KEY_E: KEY_HASH 10 | KEY_R: KEY_DOLLARSIGN 11 | KEY_T: KEY_PERCENT 12 | KEY_Y: KEY_CARET 13 | KEY_U: KEY_AMPERSAND 14 | KEY_I: KEY_STAR 15 | KEY_O: KEY_LEFTPAREN 16 | KEY_P: KEY_RIGHTPAREN 17 | KEY_A: KEY_UNDERSCORE 18 | KEY_S: KEY_PLUS 19 | KEY_D: KEY_LEFTCURLY 20 | KEY_F: KEY_RIGHTCURLY 21 | KEY_G: KEY_COLON 22 | KEY_H: KEY_DOUBLEQUOTE 23 | KEY_J: KEY_LESS 24 | KEY_K: KEY_GREATER 25 | KEY_L: KEY_QUESTION 26 | KEY_Z: KEY_TILDE 27 | KEY_X: KEY_PIPE 28 | -------------------------------------------------------------------------------- /examples/combo_hrm.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/luckasRanarison/okey/refs/heads/master/schema/okey.json 2 | 3 | # Home row mods implementation using combos 4 | # See https://www.youtube.com/watch?v=sLWQ4Gx88h4 5 | 6 | keyboards: 7 | - name: "My keyboard" 8 | 9 | combos: 10 | - keys: [KEY_A, KEY_S] 11 | action: KEY_LEFTMETA 12 | 13 | - keys: [KEY_S, KEY_D] 14 | action: KEY_LEFTSHIFT 15 | 16 | - keys: [KEY_D, KEY_F] 17 | action: KEY_LEFTCTRL 18 | 19 | - keys: [KEY_S, KEY_F] 20 | action: KEY_LEFTALT 21 | 22 | - keys: [KEY_J, KEY_K] 23 | action: KEY_RIGHTCTRL 24 | 25 | - keys: [KEY_K, KEY_L] 26 | action: KEY_RIGHTSHIFT 27 | 28 | - keys: [KEY_J, KEY_L] 29 | action: KEY_RIGHTALT 30 | 31 | - keys: [KEY_L, KEY_SEMICOLON] 32 | action: KEY_RIGHTMETA 33 | -------------------------------------------------------------------------------- /src/tests/mapping.rs: -------------------------------------------------------------------------------- 1 | use super::utils::*; 2 | 3 | const CONFIG: &str = include_str!("./config/mappings.yaml"); 4 | 5 | #[test] 6 | fn test_basic_key() -> Result<()> { 7 | let mut proxy = EventProxyMock::default(); 8 | let mut adapter = KeyAdapter::with_config(CONFIG, &mut proxy); 9 | 10 | adapter.process(InputBuffer::tap_hold(KeyCode::KEY_Q))?; 11 | 12 | let expected = InputBuffer::tap_hold(KeyCode::KEY_W); 13 | 14 | assert_eq!(proxy.queue(), expected.value()); 15 | 16 | Ok(()) 17 | } 18 | 19 | #[test] 20 | fn test_custom_code() -> Result<()> { 21 | let mut proxy = EventProxyMock::default(); 22 | let mut adapter = KeyAdapter::with_config(CONFIG, &mut proxy); 23 | 24 | adapter.process(InputBuffer::tap(KeyCode::KEY_Z))?; 25 | 26 | let expected = InputBuffer::tap(KeyCode::KEY_A); 27 | 28 | assert_eq!(proxy.queue(), expected.value()); 29 | 30 | Ok(()) 31 | } 32 | -------------------------------------------------------------------------------- /src/fs/device.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | 3 | use anyhow::Result; 4 | use evdev::Device; 5 | 6 | pub fn find_device_by_name(name: &str) -> Result> { 7 | let devices = find_input_devices()?; 8 | 9 | let device = devices 10 | .into_iter() 11 | .find(|dev| dev.name().is_some_and(|value| value == name)); 12 | 13 | Ok(device) 14 | } 15 | 16 | pub fn find_input_devices() -> Result> { 17 | let files = fs::read_dir("/dev/input")?; 18 | let mut results = Vec::new(); 19 | 20 | for file in files { 21 | let entry = file?; 22 | 23 | if !entry.file_name().to_string_lossy().contains("event") { 24 | continue; 25 | } 26 | 27 | if let Ok(device) = Device::open(entry.path()) { 28 | if device.supported_keys().is_some() { 29 | results.push(device); 30 | } 31 | } 32 | } 33 | 34 | Ok(results) 35 | } 36 | -------------------------------------------------------------------------------- /src/cli/commands/device.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | use crate::{cli::utils::device::is_keyboard, fs::device as fs}; 4 | 5 | pub fn list(keyboard: bool) -> Result<()> { 6 | let devices = fs::find_input_devices()?; 7 | 8 | for device in devices { 9 | if keyboard && !is_keyboard(&device) { 10 | continue; 11 | } 12 | 13 | let name = device.name().unwrap_or("Unknown"); 14 | let phys = device.physical_path().unwrap_or_default(); 15 | let uniq = device.unique_name().unwrap_or_default(); 16 | let input_id = device.input_id(); 17 | 18 | println!("• {name}"); 19 | println!(" ├─ Path : {phys}"); 20 | println!(" ├─ Unique ID : {uniq}"); 21 | println!(" ├─ Vendor : {:#06x}", input_id.vendor()); 22 | println!(" ├─ Product : {:#06x}", input_id.product()); 23 | println!(" └─ Version : {:#06x}", input_id.version()); 24 | println!(); 25 | } 26 | 27 | Ok(()) 28 | } 29 | -------------------------------------------------------------------------------- /examples/tap_dance_hrm.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/luckasRanarison/okey/refs/heads/master/schema/okey.json 2 | 3 | # Home row mods implementation using tap dance 4 | # See https://www.youtube.com/watch?v=sLWQ4Gx88h4 5 | 6 | keyboards: 7 | - name: "My keyboard" 8 | 9 | tap_dances: 10 | KEY_A: 11 | tap: KEY_A 12 | hold: KEY_LEFTMETA 13 | # timeout: 250 (default: 200ms) 14 | 15 | KEY_S: 16 | tap: KEY_S 17 | hold: KEY_LEFTSHIFT 18 | 19 | KEY_D: 20 | tap: KEY_D 21 | hold: KEY_LEFTALT 22 | 23 | KEY_F: 24 | tap: KEY_F 25 | hold: KEY_LEFTCTRL 26 | 27 | KEY_J: 28 | tap: KEY_J 29 | hold: KEY_RIGHTCTRL 30 | 31 | KEY_K: 32 | tap: KEY_K 33 | hold: KEY_RIGHTALT 34 | 35 | KEY_L: 36 | tap: KEY_L 37 | hold: KEY_RIGHTSHIFT 38 | 39 | KEY_SEMICOLON: 40 | tap: KEY_F 41 | hold: KEY_RIGHTMETA 42 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "okey-cli" 3 | version = "0.1.2" 4 | edition = "2021" 5 | authors = ["LIOKA Ranarison Fiderana "] 6 | description = "An advanced, easy-to-use key remapper for Linux" 7 | license = "MIT" 8 | repository = "https://github.com/luckasRanarison/okey" 9 | readme = "README.md" 10 | keywords = ["keyboard", "cli"] 11 | categories = ["command-line-interface", "hardware-support"] 12 | 13 | [lib] 14 | name = "okey" 15 | 16 | [[bin]] 17 | name = "okey" 18 | path = "src/main.rs" 19 | 20 | [dependencies] 21 | anyhow = "1.0.98" 22 | clap = { version = "4.5.37", features = ["derive"] } 23 | evdev = "0.13.1" 24 | log = "0.4.27" 25 | nix = { version = "0.29.0", features = ["user", "process"]} 26 | ringbuffer = "0.15.0" 27 | serde = { version = "1.0.219", features = ["derive"] } 28 | serde_yaml = "0.9.34" 29 | simple_logger = "5.0.0" 30 | smallvec = "1.15.0" 31 | 32 | [dev-dependencies] 33 | jsonschema = "0.30.0" 34 | serde_json = "1.0.140" 35 | 36 | [profile.release] 37 | codegen-units = 1 38 | lto = true 39 | -------------------------------------------------------------------------------- /src/fs/service.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | env, fs, 3 | path::{Path, PathBuf}, 4 | }; 5 | 6 | use anyhow::Result; 7 | use nix::unistd; 8 | 9 | pub fn write_systemd_service(config: &str) -> Result<()> { 10 | let dir_path = get_systemd_dir_path()?; 11 | let file_path = resolve_service_file_path(&dir_path); 12 | 13 | fs::create_dir_all(dir_path)?; 14 | fs::write(file_path, config)?; 15 | 16 | Ok(()) 17 | } 18 | 19 | pub fn remove_systemd_service() -> Result<()> { 20 | let dir_path = get_systemd_dir_path()?; 21 | let file_path = resolve_service_file_path(&dir_path); 22 | 23 | fs::remove_file(file_path)?; 24 | 25 | Ok(()) 26 | } 27 | 28 | pub fn get_systemd_dir_path() -> Result { 29 | let dir_path = match unistd::geteuid().is_root() { 30 | true => Path::new("/etc/systemd/system/").to_path_buf(), 31 | false => Path::new(&env::var("HOME")?).join(".config/systemd/user"), 32 | }; 33 | 34 | Ok(dir_path) 35 | } 36 | 37 | pub fn resolve_service_file_path(dir_path: &Path) -> PathBuf { 38 | dir_path.join("okey.service") 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 LIOKA Ranarison Fiderana 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 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use clap::Parser; 3 | use okey::cli::{commands, Cli, Command, DeviceSubcommand, SystemdSubcommand}; 4 | 5 | fn main() -> Result<()> { 6 | let cli = Cli::parse(); 7 | 8 | match cli.command { 9 | Command::Start { 10 | config, 11 | daemon, 12 | systemd: _, 13 | } => match daemon { 14 | true => commands::start::start_daemon(config), 15 | false => commands::start::start(config), 16 | }, 17 | 18 | Command::Service { command } => match command { 19 | SystemdSubcommand::Start => commands::service::start(), 20 | SystemdSubcommand::Restart => commands::service::restart(), 21 | SystemdSubcommand::Stop => commands::service::stop(), 22 | SystemdSubcommand::Status => commands::service::status(), 23 | SystemdSubcommand::Install => commands::service::install(), 24 | SystemdSubcommand::Uninstall => commands::service::uninstall(), 25 | }, 26 | 27 | Command::Device { command } => match command { 28 | DeviceSubcommand::List { keyboard } => commands::device::list(keyboard), 29 | }, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/tests/shift.rs: -------------------------------------------------------------------------------- 1 | use crate::config::schema::{Config, KeyAction}; 2 | 3 | use super::utils::*; 4 | 5 | const CONFIG: &str = include_str!("./config/shift.yaml"); 6 | 7 | #[test] 8 | fn test_shifted_keycodes() { 9 | let mut config: Config = serde_yaml::from_str(CONFIG).unwrap(); 10 | let keyboard = config.keyboards.remove(0); 11 | 12 | for (key, action) in keyboard.keys { 13 | if let KeyAction::KeyCode(code) = action { 14 | assert!(code.is_shifted()) 15 | } else { 16 | panic!("Non shifted keycode mapped to {key:?}") 17 | } 18 | } 19 | } 20 | 21 | #[test] 22 | fn test_shifted_key_event() -> Result<()> { 23 | let mut proxy = EventProxyMock::default(); 24 | let mut adapter = KeyAdapter::with_config(CONFIG, &mut proxy); 25 | 26 | adapter.process(InputBuffer::press(KeyCode::KEY_Q).hold().release())?; 27 | 28 | let expected = InputBuffer::new(KeyCode::KEY_LEFTSHIFT) 29 | .press() 30 | .hold_then(KeyCode::KEY_1) 31 | .press() 32 | .hold_then(KeyCode::KEY_LEFTSHIFT) 33 | .release_then(KeyCode::KEY_1) 34 | .release(); 35 | 36 | assert_eq!(proxy.queue(), expected.value()); 37 | 38 | Ok(()) 39 | } 40 | -------------------------------------------------------------------------------- /src/cli/utils/systemctl.rs: -------------------------------------------------------------------------------- 1 | use std::process::{Command, Stdio}; 2 | 3 | use anyhow::Result; 4 | use nix::unistd; 5 | 6 | fn systemctl(args: &[&str]) -> Result<()> { 7 | let args = match unistd::geteuid().is_root() { 8 | true => args.to_vec(), 9 | false => [&["--user"], args].concat(), 10 | }; 11 | 12 | Command::new("systemctl") 13 | .args(args) 14 | .stdout(Stdio::null()) 15 | .stderr(Stdio::null()) 16 | .status()?; 17 | 18 | Ok(()) 19 | } 20 | 21 | pub fn reload_daemon() -> Result<()> { 22 | systemctl(&["daemon-reload"]) 23 | } 24 | 25 | pub fn start() -> Result<()> { 26 | systemctl(&["enable", "okey"])?; 27 | systemctl(&["start", "okey"]) 28 | } 29 | 30 | pub fn restart() -> Result<()> { 31 | systemctl(&["restart", "okey"]) 32 | } 33 | 34 | pub fn stop() -> Result<()> { 35 | systemctl(&["stop", "okey"])?; 36 | systemctl(&["disable", "okey"]) 37 | } 38 | 39 | pub fn status() -> Result<()> { 40 | let args = match unistd::geteuid().is_root() { 41 | true => vec!["status", "okey"], 42 | false => vec!["--user", "status", "okey"], 43 | }; 44 | 45 | Command::new("systemctl").args(args).status()?; 46 | 47 | Ok(()) 48 | } 49 | -------------------------------------------------------------------------------- /src/tests/config/layers.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/luckasRanarison/okey/refs/heads/master/schema/okey.json 2 | 3 | keyboards: 4 | - name: "Layer test keyboard" 5 | 6 | tap_dances: 7 | KEY_A: 8 | tap: KEY_TOGLAYER 9 | hold: KEY_MOMLAYER 10 | 11 | KEY_O: 12 | tap: KEY_ONELAYER 13 | hold: KEY_TAB 14 | 15 | combos: 16 | - keys: [KEY_S, KEY_D] 17 | action: KEY_MOMLAYER 18 | 19 | - keys: [KEY_K, KEY_L] 20 | action: KEY_TOGLAYER 21 | 22 | - keys: [KEY_D, KEY_F] 23 | action: KEY_ONELAYER 24 | 25 | layers: 26 | first_layer: 27 | modifier: KEY_SPACE 28 | keys: 29 | KEY_P: KEY_Q 30 | KEY_V: KEY_B 31 | 32 | second_layer: 33 | modifier: KEY_B 34 | keys: 35 | KEY_P: KEY_X 36 | 37 | mom_layer: 38 | modifier: KEY_MOMLAYER 39 | keys: 40 | KEY_P: KEY_X 41 | 42 | tog_layer: 43 | modifier: 44 | key: KEY_TOGLAYER 45 | type: toggle 46 | keys: 47 | KEY_P: KEY_X 48 | 49 | one_layer: 50 | modifier: 51 | key: KEY_ONELAYER 52 | type: oneshoot 53 | keys: 54 | KEY_P: KEY_X 55 | -------------------------------------------------------------------------------- /examples/key_mapping.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/luckasRanarison/okey/refs/heads/master/schema/okey.json 2 | 3 | keyboards: 4 | - name: "My keyboard" 5 | 6 | keys: 7 | KEY_X: KEY_Y 8 | KEY_TAB: KEY_ONELAYER # a custom keycode to activate the layer below 9 | 10 | tap_dances: 11 | KEY_CAPSLOCK: 12 | tap: KEY_TAB 13 | hold: KEY_MOMLAYER 14 | # timeout: 200 15 | 16 | layers: 17 | momentary: 18 | modifier: KEY_MOMLAYER # type is momentary by default, active on hold 19 | 20 | # modifier: 21 | # key: KEY_MOMLAYER 22 | # type: momentary 23 | 24 | keys: 25 | KEY_Q: KEY_A 26 | KEY_H: { unicode: 😂 } 27 | KEY_D: { shell: 'date "+%d %B %Y"', trim: true } 28 | KEY_M: [{ env: USERNAME }, { string: "@gmail.com" }] # macros are composable 29 | KEY_O: [{ press: KEY_O }, { release: KEY_O }, { delay: 500 }, KEY_K] 30 | 31 | one: 32 | modifier: 33 | key: KEY_ONELAYER 34 | type: oneshoot # active for one keypress 35 | 36 | keys: 37 | KEY_O: KEY_K 38 | 39 | toggle: 40 | modifier: 41 | key: KEY_F12 42 | type: toggle # active until switched off 43 | 44 | keys: 45 | KEY_O: KEY_K 46 | -------------------------------------------------------------------------------- /examples/macro_types.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/luckasRanarison/okey/refs/heads/master/schema/okey.json 2 | 3 | keyboards: 4 | - name: "My keyboard" 5 | 6 | keys: 7 | # string macros emit raw character keycode and not remap 8 | KEY_Y: KEY_X 9 | KEY_U: KEY_I 10 | 11 | KEY_F1: [KEY_H, KEY_E, KEY_L, KEY_L, KEY_O] # executes simple key sequences (press + release) 12 | KEY_F2: { string: "Hi, you!" } # inserts an ASCII string 13 | KEY_F3: { env: FOO } # inserts the value of the environment variable 14 | KEY_F4: { unicode: 🙂👍 } # inserts unicode characters using CTRL + SHIFT + U + + ENTER 15 | KEY_F5: { shell: "echo 'foo'", trim: true } # inserts shell script output 16 | 17 | KEY_F6: [ 18 | { press: KEY_O }, 19 | { hold: KEY_O }, 20 | { delay: 1000 }, 21 | { release: KEY_O }, 22 | KEY_K, # press + release 23 | ] # executes detailed key sequences 24 | 25 | KEY_F7: [{ env: USERNAME }, { string: "@gmail.com" }] # all types of macro are composable 26 | 27 | KEY_F8: 28 | [ 29 | { string: "Hello, " }, 30 | { env: USERNAME }, 31 | { string: "!" }, 32 | { delay: 500 }, 33 | { string: " Today is: " }, 34 | { shell: "date '+%A, %B %d, %Y'", trim: true }, 35 | KEY_ENTER, 36 | ] 37 | -------------------------------------------------------------------------------- /src/fs/config.rs: -------------------------------------------------------------------------------- 1 | use std::{env, fs, io, path::Path}; 2 | 3 | use anyhow::{Result, anyhow}; 4 | use nix::unistd; 5 | 6 | use crate::config::schema::Config; 7 | 8 | pub fn get_config_dir_path() -> Result { 9 | if unistd::geteuid().is_root() { 10 | Ok("/etc/okey".to_string()) 11 | } else { 12 | let home_path = env::var("HOME")?; 13 | let default_path = Path::new(&home_path).join(".config/okey"); 14 | let default_path_str = default_path.to_string_lossy().to_string(); 15 | 16 | Ok(default_path_str) 17 | } 18 | } 19 | 20 | pub fn get_default_config_path() -> Result { 21 | let config_dir_path = get_config_dir_path()?; 22 | let default_path = Path::new(&config_dir_path).join("config.yaml"); 23 | let default_path_str = default_path.to_string_lossy().to_string(); 24 | 25 | Ok(default_path_str) 26 | } 27 | 28 | pub fn read_config(path: Option) -> Result { 29 | let config_path = path 30 | .unwrap_or_else(|| get_default_config_path().expect("Failed to get default config path")); 31 | 32 | let parsed = fs::read_to_string(&config_path) 33 | .map_err(|err| match err.kind() { 34 | io::ErrorKind::NotFound => anyhow!("Configuration file not found at {config_path}"), 35 | _ => err.into(), 36 | }) 37 | .and_then(|config| Ok(serde_yaml::from_str(&config)?))?; 38 | 39 | Ok(parsed) 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | release: 13 | name: Release - ${{ matrix.platform.os-name }} 14 | strategy: 15 | matrix: 16 | platform: 17 | - os-name: linux-x86_64 18 | target: x86_64-unknown-linux-gnu 19 | 20 | - os-name: linux-aarch64 21 | target: aarch64-unknown-linux-gnu 22 | 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v4 27 | 28 | - name: Build binary 29 | uses: houseabsolute/actions-rust-cross@v1 30 | with: 31 | command: build 32 | target: ${{ matrix.platform.target }} 33 | args: "--locked --release" 34 | strip: true 35 | 36 | - name: Publish artifacts and release 37 | uses: houseabsolute/actions-rust-release@v0 38 | with: 39 | executable-name: okey 40 | changes-file: "CHANGELOG.md" 41 | target: ${{ matrix.platform.target }} 42 | 43 | publish: 44 | runs-on: ubuntu-latest 45 | steps: 46 | - uses: actions/checkout@v3 47 | - uses: actions-rs/toolchain@v1 48 | with: 49 | toolchain: stable 50 | override: true 51 | - uses: katyo/publish-crates@v2 52 | with: 53 | registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }} 54 | ignore-unpublished-changes: true 55 | -------------------------------------------------------------------------------- /src/core/proxy.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use evdev::{Device, InputEvent, uinput::VirtualDevice}; 3 | use nix::sys::epoll::{Epoll, EpollCreateFlags, EpollEvent, EpollFlags}; 4 | 5 | pub trait EventProxy { 6 | fn emit(&mut self, events: &[InputEvent]) -> Result<()>; 7 | fn wait(&mut self, timeout: u16) -> Result<()>; 8 | } 9 | 10 | #[derive(Debug)] 11 | pub struct InputProxy { 12 | epoll: Epoll, 13 | event_buffer: [EpollEvent; 1], 14 | virtual_device: VirtualDevice, 15 | } 16 | 17 | impl InputProxy { 18 | pub fn try_from_device(device: &Device) -> Result { 19 | let name = format!("{} (virtual)", device.name().unwrap_or("Unknown device")); 20 | let virtual_device = VirtualDevice::builder()? 21 | .name(&name) 22 | .with_keys(device.supported_keys().unwrap_or_default())? 23 | .build()?; 24 | 25 | let epoll = Epoll::new(EpollCreateFlags::EPOLL_CLOEXEC)?; 26 | let event = EpollEvent::new(EpollFlags::EPOLLIN, 0); 27 | let event_buffer = [EpollEvent::empty(); 1]; 28 | 29 | epoll.add(device, event)?; 30 | 31 | Ok(Self { 32 | epoll, 33 | event_buffer, 34 | virtual_device, 35 | }) 36 | } 37 | } 38 | 39 | impl EventProxy for InputProxy { 40 | fn emit(&mut self, events: &[InputEvent]) -> Result<()> { 41 | self.virtual_device.emit(events)?; 42 | Ok(()) 43 | } 44 | 45 | fn wait(&mut self, timeout: u16) -> Result<()> { 46 | self.epoll.wait(&mut self.event_buffer, timeout)?; 47 | Ok(()) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/core/buffer.rs: -------------------------------------------------------------------------------- 1 | use ringbuffer::{ConstGenericRingBuffer as RingBuffer, RingBuffer as _}; 2 | use smallvec::SmallVec; 3 | 4 | use crate::config::schema::KeyCode; 5 | 6 | use super::{adapter::InputResult, shared::RawKeyCode}; 7 | 8 | #[derive(Debug, Default)] 9 | pub struct InputBuffer { 10 | results: RingBuffer, 11 | processed: RingBuffer, 12 | deferred_keys: RingBuffer, 13 | pending_keys: SmallVec<[KeyCode; 4]>, 14 | } 15 | 16 | impl InputBuffer { 17 | pub fn push_result(&mut self, result: InputResult) { 18 | self.results.enqueue(result); 19 | } 20 | 21 | pub fn pop_result(&mut self) -> Option { 22 | self.results.dequeue() 23 | } 24 | 25 | pub fn push_key(&mut self, key: RawKeyCode) { 26 | self.processed.enqueue(key); 27 | } 28 | 29 | pub fn pop_key(&mut self) -> Option { 30 | self.processed.dequeue() 31 | } 32 | 33 | pub fn is_pending_key(&mut self, code: &KeyCode) -> bool { 34 | self.pending_keys.contains(code) 35 | } 36 | 37 | pub fn has_pending_keys(&mut self) -> bool { 38 | !self.pending_keys.is_empty() 39 | } 40 | 41 | pub fn set_pending_key(&mut self, code: KeyCode) { 42 | self.pending_keys.push(code); 43 | } 44 | 45 | pub fn clear_pending_key(&mut self, code: &KeyCode) { 46 | self.pending_keys.retain(|value| value != code); 47 | } 48 | 49 | pub fn defer_key(&mut self, code: KeyCode) { 50 | self.deferred_keys.enqueue(code); 51 | } 52 | 53 | pub fn pop_deferred_key(&mut self) -> Option { 54 | self.deferred_keys.dequeue() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/core/event.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use evdev::{EventType, InputEvent}; 3 | 4 | use crate::config::schema::{EventMacro, KeyCode}; 5 | 6 | use super::{ 7 | adapter::InputResult, 8 | input::{command_to_input, string_to_input, unicode_to_input}, 9 | }; 10 | 11 | pub const PRESS_EVENT: i32 = 1; 12 | pub const HOLD_EVENT: i32 = 2; 13 | pub const RELEASE_EVENT: i32 = 0; 14 | 15 | pub trait IntoInputEvent { 16 | fn to_event(&self, value: i32) -> InputEvent; 17 | } 18 | 19 | pub trait ToInputResult { 20 | fn to_results(&self, delay: u16) -> Result>; 21 | } 22 | 23 | impl IntoInputEvent for KeyCode { 24 | fn to_event(&self, value: i32) -> InputEvent { 25 | InputEvent::new(EventType::KEY.0, self.value(), value) 26 | } 27 | } 28 | 29 | impl ToInputResult for EventMacro { 30 | fn to_results(&self, delay: u16) -> Result> { 31 | match self { 32 | EventMacro::Press { press } => Ok(vec![InputResult::Press(*press)]), 33 | EventMacro::Hold { hold } => Ok(vec![InputResult::Hold(*hold)]), 34 | EventMacro::Release { release } => Ok(vec![InputResult::Release(*release)]), 35 | EventMacro::Delay { delay: sleep } => Ok(vec![InputResult::Delay(*sleep)]), 36 | EventMacro::String { string } => string_to_input(string), 37 | EventMacro::Env { env } => string_to_input(&std::env::var(env)?), 38 | EventMacro::Unicode { unicode } => unicode_to_input(unicode, delay), 39 | EventMacro::Shell { shell, trim } => command_to_input(shell, *trim), 40 | EventMacro::Tap(code) => Ok(vec![InputResult::DoubleSequence(Box::new([ 41 | InputResult::Press(*code), 42 | InputResult::Release(*code), 43 | ]))]), 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/tests/schema.rs: -------------------------------------------------------------------------------- 1 | use jsonschema::draft7 as jsonschema; 2 | 3 | const SCHEMA: &str = include_str!("../../schema/okey.json"); 4 | 5 | const TD_TEST: &str = include_str!("./config/tap_dances.yaml"); 6 | const COMBO_TEST: &str = include_str!("./config/combos.yaml"); 7 | const LAYER_TEST: &str = include_str!("./config/layers.yaml"); 8 | const MACRO_TEST: &str = include_str!("./config/macros.yaml"); 9 | const MAPPING_TEST: &str = include_str!("./config/mappings.yaml"); 10 | const SHIFT_TEST: &str = include_str!("./config/shift.yaml"); 11 | 12 | const TD_EX: &str = include_str!("../../examples/tap_dance_hrm.yaml"); 13 | const COMBO_EX: &str = include_str!("../../examples/combo_hrm.yaml"); 14 | const MACRO_EX: &str = include_str!("../../examples/macro_types.yaml"); 15 | const MAPPING_EX: &str = include_str!("../../examples/key_mapping.yaml"); 16 | const SETTING_EX: &str = include_str!("../../examples/default_settings.yaml"); 17 | 18 | fn yaml_to_json(source: &str) -> serde_json::Value { 19 | serde_yaml::from_str(source).unwrap() 20 | } 21 | 22 | #[test] 23 | fn test_config_validation() { 24 | let schema = serde_json::from_str(SCHEMA).unwrap(); 25 | 26 | assert!(jsonschema::is_valid(&schema, &yaml_to_json(TD_TEST))); 27 | assert!(jsonschema::is_valid(&schema, &yaml_to_json(COMBO_TEST))); 28 | assert!(jsonschema::is_valid(&schema, &yaml_to_json(LAYER_TEST))); 29 | assert!(jsonschema::is_valid(&schema, &yaml_to_json(MACRO_TEST))); 30 | assert!(jsonschema::is_valid(&schema, &yaml_to_json(MAPPING_TEST))); 31 | assert!(jsonschema::is_valid(&schema, &yaml_to_json(SHIFT_TEST))); 32 | 33 | assert!(jsonschema::is_valid(&schema, &yaml_to_json(TD_EX))); 34 | assert!(jsonschema::is_valid(&schema, &yaml_to_json(COMBO_EX))); 35 | assert!(jsonschema::is_valid(&schema, &yaml_to_json(SETTING_EX))); 36 | assert!(jsonschema::is_valid(&schema, &yaml_to_json(MACRO_EX))); 37 | assert!(jsonschema::is_valid(&schema, &yaml_to_json(MAPPING_EX))); 38 | } 39 | -------------------------------------------------------------------------------- /src/cli/commands/start.rs: -------------------------------------------------------------------------------- 1 | use std::{fs::File, io, os::fd::AsRawFd, process, thread}; 2 | 3 | use anyhow::{anyhow, Result}; 4 | use nix::unistd::{self, ForkResult}; 5 | 6 | use crate::{ 7 | core::{InputProxy, KeyAdapter}, 8 | fs::{config::read_config, device::find_device_by_name}, 9 | }; 10 | 11 | pub fn start(config_path: Option) -> Result<()> { 12 | let parsed = read_config(config_path)?; 13 | 14 | let handles = parsed.keyboards.into_iter().map(|keyboard| { 15 | let defaults = parsed.defaults.clone(); 16 | 17 | thread::spawn(move || -> Result<()> { 18 | let mut device = find_device_by_name(&keyboard.name)? 19 | .ok_or(anyhow!("Device not found: {}", keyboard.name))?; 20 | 21 | let mut proxy = InputProxy::try_from_device(&device)?; 22 | let mut adapter = KeyAdapter::new(keyboard, defaults, &mut proxy); 23 | 24 | adapter.hook(&mut device) 25 | }) 26 | }); 27 | 28 | simple_logger::init()?; 29 | 30 | for handle in handles { 31 | handle.join().unwrap()? 32 | } 33 | 34 | Ok(()) 35 | } 36 | 37 | pub fn start_daemon(config_path: Option) -> Result<()> { 38 | match unsafe { unistd::fork() } { 39 | Ok(ForkResult::Parent { child, .. }) => { 40 | println!("okey daemon started in the background... (PID: {})", child); 41 | process::exit(0); 42 | } 43 | Ok(ForkResult::Child) => { 44 | unistd::setsid()?; 45 | unistd::chdir("/")?; 46 | 47 | let dev_null = File::open("/dev/null")?; 48 | let dev_null_fd = dev_null.as_raw_fd(); 49 | 50 | unistd::dup2(dev_null_fd, io::stdin().as_raw_fd())?; 51 | unistd::dup2(dev_null_fd, io::stdout().as_raw_fd())?; 52 | unistd::dup2(dev_null_fd, io::stderr().as_raw_fd())?; 53 | 54 | start(config_path)?; 55 | } 56 | Err(err) => Err(err)?, 57 | } 58 | 59 | Ok(()) 60 | } 61 | -------------------------------------------------------------------------------- /src/cli/commands/service.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use anyhow::{Result, anyhow}; 4 | use nix::unistd; 5 | 6 | use crate::{cli::utils::systemctl, fs::service as fs}; 7 | 8 | pub use systemctl::{restart, status, stop}; 9 | 10 | pub fn start() -> Result<()> { 11 | let dir_path = fs::get_systemd_dir_path()?; 12 | let file_path = fs::resolve_service_file_path(&dir_path); 13 | 14 | if !file_path.exists() { 15 | return Err(anyhow!( 16 | "The systemd service is not installed, run 'okey service install'", 17 | )); 18 | } 19 | 20 | systemctl::reload_daemon()?; 21 | systemctl::start()?; 22 | 23 | Ok(()) 24 | } 25 | 26 | pub fn install() -> Result<()> { 27 | let exe_path = env::current_exe()?; 28 | let exe_path_str = exe_path.to_string_lossy(); 29 | 30 | let config = format!( 31 | r#"[Unit] 32 | Description=Okey Service 33 | 34 | [Service] 35 | ExecStart={exe_path_str} start --systemd 36 | Restart=on-failure 37 | StandardOutput=journal 38 | StandardError=journal 39 | Nice=-20{} 40 | 41 | [Install] 42 | WantedBy=multi-user.target"#, 43 | if unistd::geteuid().is_root() { 44 | r#" 45 | CPUSchedulingPolicy=rr 46 | CPUSchedulingPriority=99 47 | IOSchedulingClass=realtime 48 | IOSchedulingPriority=0"# 49 | } else { 50 | "" 51 | } 52 | ); 53 | 54 | fs::write_systemd_service(&config)?; 55 | 56 | println!("The systemd service has been installed, run 'okey service start' to start it"); 57 | 58 | Ok(()) 59 | } 60 | 61 | pub fn uninstall() -> Result<()> { 62 | let dir_path = fs::get_systemd_dir_path()?; 63 | let file_path = fs::resolve_service_file_path(&dir_path); 64 | 65 | if file_path.exists() { 66 | systemctl::stop()?; 67 | fs::remove_systemd_service()?; 68 | systemctl::reload_daemon()?; 69 | 70 | println!("The systemd service has been removed"); 71 | } else { 72 | println!("The systemd service does not exist"); 73 | } 74 | 75 | Ok(()) 76 | } 77 | -------------------------------------------------------------------------------- /src/cli/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod commands; 2 | 3 | mod utils; 4 | 5 | use clap::Parser; 6 | 7 | #[derive(Parser, Debug)] 8 | #[command(version, about)] 9 | pub struct Cli { 10 | #[command(subcommand)] 11 | pub command: Command, 12 | } 13 | 14 | #[derive(Parser, Debug)] 15 | pub enum Command { 16 | /// Start the keyboard remapping hook 17 | Start { 18 | /// Configuration file path (default: ~/.config/okey/config.yaml) 19 | #[arg(short, long)] 20 | config: Option, 21 | /// Whether to start the process as a daemon 22 | #[arg(short, long, default_value_t = false)] 23 | daemon: bool, 24 | 25 | #[arg( 26 | long, 27 | hide_short_help = true, 28 | hide_long_help = true, 29 | default_value_t = false 30 | )] 31 | systemd: bool, 32 | }, 33 | 34 | /// Utility commands for the systemd service 35 | Service { 36 | #[command(subcommand)] 37 | command: SystemdSubcommand, 38 | }, 39 | 40 | /// Utility commands for debugging input devices 41 | Device { 42 | #[command(subcommand)] 43 | command: DeviceSubcommand, 44 | }, 45 | } 46 | 47 | #[derive(Parser, Debug)] 48 | pub enum SystemdSubcommand { 49 | /// Shorthand for 'systemctl --user enable okey && systemctl --user start okey' 50 | Start, 51 | /// Shorthand for 'systemctl --user stop okey && systemctl --user disable okey' 52 | Stop, 53 | /// Shorthand for 'systemctl --user restart okey' 54 | Restart, 55 | /// Shorthand for 'systemctl --user status okey' 56 | Status, 57 | /// Create the systemd service file 58 | Install, 59 | /// Disable and remove the service file 60 | Uninstall, 61 | } 62 | 63 | #[derive(Parser, Debug)] 64 | pub enum DeviceSubcommand { 65 | /// List all input devices that support keys 66 | List { 67 | /// Whether to only show keyboards 68 | #[arg(short, long, default_value_t = false)] 69 | keyboard: bool, 70 | }, 71 | } 72 | -------------------------------------------------------------------------------- /src/tests/tap_dance.rs: -------------------------------------------------------------------------------- 1 | use std::{thread, time::Duration}; 2 | 3 | use super::utils::*; 4 | 5 | const CONFIG: &str = include_str!("./config/tap_dances.yaml"); 6 | 7 | #[test] 8 | fn test_key_tap() -> Result<()> { 9 | let mut proxy = EventProxyMock::default(); 10 | let mut adapter = KeyAdapter::with_config(CONFIG, &mut proxy); 11 | 12 | let expected = InputBuffer::tap(KeyCode::KEY_S); 13 | 14 | adapter.process(expected.clone())?; 15 | 16 | assert_eq!(proxy.queue(), expected.value()); 17 | 18 | Ok(()) 19 | } 20 | 21 | #[test] 22 | fn test_key_hold() -> Result<()> { 23 | let mut proxy = EventProxyMock::default(); 24 | let mut adapter = KeyAdapter::with_config(CONFIG, &mut proxy); 25 | 26 | adapter.process(InputBuffer::press(KeyCode::KEY_S).hold())?; 27 | 28 | thread::sleep(Duration::from_millis(250)); 29 | 30 | adapter.post_process()?; 31 | adapter.process(InputBuffer::release(KeyCode::KEY_S))?; 32 | 33 | let expected = InputBuffer::tap_hold(KeyCode::KEY_LEFTSHIFT); 34 | 35 | assert_eq!(proxy.queue(), expected.value()); 36 | 37 | Ok(()) 38 | } 39 | 40 | #[test] 41 | fn test_macro_tap() -> Result<()> { 42 | let mut proxy = EventProxyMock::default(); 43 | let mut adapter = KeyAdapter::with_config(CONFIG, &mut proxy); 44 | 45 | adapter.process(InputBuffer::tap(KeyCode::KEY_H))?; 46 | 47 | let expected = InputBuffer::new(KeyCode::KEY_H) 48 | .tap_then(KeyCode::KEY_I) 49 | .tap(); 50 | 51 | assert_eq!(proxy.queue(), expected.value()); 52 | 53 | Ok(()) 54 | } 55 | 56 | #[test] 57 | fn test_macro_hold() -> Result<()> { 58 | let mut proxy = EventProxyMock::default(); 59 | let mut adapter = KeyAdapter::with_config(CONFIG, &mut proxy); 60 | 61 | adapter.process(InputBuffer::press(KeyCode::KEY_H).hold())?; 62 | 63 | thread::sleep(Duration::from_millis(250)); 64 | 65 | adapter.post_process()?; 66 | 67 | let expected = InputBuffer::new(KeyCode::KEY_H) 68 | .tap_then(KeyCode::KEY_E) 69 | .tap_then(KeyCode::KEY_Y) 70 | .tap(); 71 | 72 | // macros should not repeat on hold 73 | assert_eq!(proxy.queue(), expected.value()); 74 | 75 | Ok(()) 76 | } 77 | 78 | #[test] 79 | fn test_tap_dance_key() -> Result<()> { 80 | let mut proxy = EventProxyMock::default(); 81 | let mut adapter = KeyAdapter::with_config(CONFIG, &mut proxy); 82 | 83 | let expected = InputBuffer::new(KeyCode::KEY_S) 84 | .tap_then(KeyCode::KEY_A) 85 | .tap(); 86 | 87 | adapter.process(expected.clone())?; 88 | 89 | assert_eq!(proxy.queue(), expected.value()); 90 | 91 | Ok(()) 92 | } 93 | -------------------------------------------------------------------------------- /src/tests/combo.rs: -------------------------------------------------------------------------------- 1 | use std::{thread, time::Duration}; 2 | 3 | use super::utils::*; 4 | 5 | const CONFIG: &str = include_str!("./config/combos.yaml"); 6 | 7 | #[test] 8 | fn test_tap_combo() -> Result<()> { 9 | let mut proxy = EventProxyMock::default(); 10 | let mut adapter = KeyAdapter::with_config(CONFIG, &mut proxy); 11 | 12 | adapter.process(InputBuffer::combo_press(KeyCode::KEY_D, KeyCode::KEY_F))?; 13 | 14 | thread::sleep(Duration::from_millis(20)); 15 | 16 | adapter.process(InputBuffer::combo_release(KeyCode::KEY_D, KeyCode::KEY_F))?; 17 | 18 | let expected = InputBuffer::tap(KeyCode::KEY_LEFTCTRL); 19 | 20 | assert_eq!(proxy.queue(), expected.value()); 21 | 22 | Ok(()) 23 | } 24 | 25 | #[test] 26 | fn test_macro_combo() -> Result<()> { 27 | let mut proxy = EventProxyMock::default(); 28 | let mut adapter = KeyAdapter::with_config(CONFIG, &mut proxy); 29 | 30 | let expected = InputBuffer::new(KeyCode::KEY_H) 31 | .tap_then(KeyCode::KEY_E) 32 | .tap_then(KeyCode::KEY_Y) 33 | .tap(); 34 | 35 | adapter.process(InputBuffer::combo_press(KeyCode::KEY_U, KeyCode::KEY_I))?; 36 | 37 | thread::sleep(Duration::from_millis(20)); 38 | 39 | adapter.process(InputBuffer::combo_release(KeyCode::KEY_U, KeyCode::KEY_I))?; 40 | 41 | assert_eq!(proxy.queue(), expected.value()); 42 | 43 | Ok(()) 44 | } 45 | 46 | #[test] 47 | fn test_macro_combo_hold() -> Result<()> { 48 | let mut proxy = EventProxyMock::default(); 49 | let mut adapter = KeyAdapter::with_config(CONFIG, &mut proxy); 50 | 51 | let expected = InputBuffer::new(KeyCode::KEY_H) 52 | .tap_then(KeyCode::KEY_E) 53 | .tap_then(KeyCode::KEY_Y) 54 | .tap(); 55 | 56 | adapter.process(InputBuffer::combo_press(KeyCode::KEY_U, KeyCode::KEY_I))?; 57 | 58 | thread::sleep(Duration::from_millis(90)); 59 | 60 | adapter.process(InputBuffer::combo_hold(KeyCode::KEY_U, KeyCode::KEY_I))?; 61 | 62 | thread::sleep(Duration::from_millis(90)); 63 | 64 | adapter.process(InputBuffer::combo_release(KeyCode::KEY_U, KeyCode::KEY_I))?; 65 | 66 | // macros should not repeat on hold 67 | assert_eq!(proxy.queue(), expected.value()); 68 | 69 | Ok(()) 70 | } 71 | 72 | #[test] 73 | fn test_expired_combo_key() -> Result<()> { 74 | let mut proxy = EventProxyMock::default(); 75 | let mut adapter = KeyAdapter::with_config(CONFIG, &mut proxy); 76 | 77 | adapter.process(InputBuffer::press(KeyCode::KEY_D))?; 78 | 79 | thread::sleep(Duration::from_millis(50)); 80 | 81 | adapter.process(InputBuffer::release(KeyCode::KEY_D))?; 82 | 83 | let expected = InputBuffer::tap(KeyCode::KEY_D); 84 | 85 | assert_eq!(proxy.queue(), expected.value()); 86 | 87 | proxy.clear(); 88 | 89 | Ok(()) 90 | } 91 | 92 | #[test] 93 | fn test_derred_combo_key() -> Result<()> { 94 | let mut proxy = EventProxyMock::default(); 95 | let mut adapter = KeyAdapter::with_config(CONFIG, &mut proxy); 96 | 97 | let expected = InputBuffer::new(KeyCode::KEY_D) 98 | .tap_then(KeyCode::KEY_A) 99 | .tap(); 100 | 101 | adapter.process(InputBuffer::press(KeyCode::KEY_D))?; 102 | 103 | thread::sleep(Duration::from_millis(60)); 104 | 105 | adapter.process( 106 | InputBuffer::new(KeyCode::KEY_D) 107 | .release_then(KeyCode::KEY_A) 108 | .tap(), 109 | )?; 110 | 111 | assert_eq!(proxy.queue(), expected.value()); 112 | 113 | Ok(()) 114 | } 115 | -------------------------------------------------------------------------------- /assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 37 | 39 | 42 | 46 | 50 | 51 | 60 | 61 | 65 | 73 | 81 | 89 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /examples/personal_config.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/luckasRanarison/okey/refs/heads/master/schema/okey.json 2 | 3 | defaults: 4 | general: 5 | deferred_key_delay: 0 6 | 7 | keyboards: 8 | - name: "AT Translated Set 2 keyboard" 9 | 10 | keys: 11 | KEY_CAPSLOCK: KEY_TAB 12 | 13 | tap_dances: 14 | TD_ZERO: 15 | tap: KEY_0 16 | hold: LAYER_COMP 17 | 18 | KEY_SPACE: 19 | tap: KEY_SPACE 20 | hold: LAYER_MACRO 21 | 22 | layers: 23 | symbols: 24 | modifier: KEY_LEFTALT 25 | 26 | keys: 27 | KEY_Q: KEY_LEFTBRACE 28 | KEY_W: KEY_RIGHTBRACE 29 | KEY_E: KEY_LEFTCURLY 30 | KEY_R: KEY_RIGHTCURLY 31 | KEY_T: KEY_CARET 32 | KEY_A: KEY_STAR 33 | KEY_S: KEY_BACKSLASH 34 | KEY_D: KEY_LEFTPAREN 35 | KEY_F: KEY_RIGHTPAREN 36 | KEY_G: KEY_PIPE 37 | KEY_Z: KEY_EXCLAMATION 38 | KEY_X: KEY_TILDE 39 | KEY_C: KEY_HASH 40 | KEY_V: KEY_PERCENT 41 | KEY_B: KEY_AT 42 | 43 | KEY_Y: KEY_PLUS 44 | KEY_U: KEY_1 45 | KEY_I: KEY_2 46 | KEY_O: KEY_3 47 | KEY_P: KEY_MINUS 48 | KEY_H: KEY_EQUAL 49 | KEY_J: KEY_4 50 | KEY_K: KEY_5 51 | KEY_L: KEY_6 52 | KEY_SEMICOLON: KEY_UNDERSCORE 53 | KEY_N: KEY_AMPERSAND 54 | KEY_M: KEY_7 55 | KEY_COMMA: KEY_8 56 | KEY_DOT: KEY_9 57 | KEY_SLASH: KEY_DOLLARSIGN 58 | KEY_APOSTROPHE: KEY_GRAVE 59 | KEY_RIGHTALT: TD_ZERO 60 | 61 | composite: 62 | modifier: LAYER_COMP 63 | keys: 64 | KEY_Q: { string: "?." } 65 | KEY_W: { string: "//" } 66 | KEY_E: { string: "::" } 67 | KEY_R: { string: "-=" } 68 | KEY_T: { string: "!==" } 69 | KEY_A: { string: "&&" } 70 | KEY_S: { string: "||" } 71 | KEY_D: { string: "??" } 72 | KEY_F: { string: "+=" } 73 | KEY_G: { string: "!=" } 74 | 75 | KEY_Y: { string: "===" } 76 | KEY_U: { string: "<=" } 77 | KEY_I: { string: ">=" } 78 | KEY_O: { string: "->" } 79 | KEY_P: { string: "::<_>()" } 80 | KEY_H: { string: "==" } 81 | KEY_J: KEY_GREATER 82 | KEY_K: KEY_LESS 83 | KEY_L: { string: "=>" } 84 | KEY_SEMICOLON: { string: "..." } 85 | KEY_APOSTROPHE: { string: "```" } 86 | 87 | edit: 88 | modifier: KEY_RIGHTALT 89 | keys: 90 | KEY_H: KEY_LEFT 91 | KEY_J: KEY_DOWN 92 | KEY_K: KEY_UP 93 | KEY_L: KEY_RIGHT 94 | 95 | macro: 96 | modifier: LAYER_MACRO 97 | keys: 98 | KEY_A: [{ string: async }] 99 | KEY_E: [{ env: EMAIL }] 100 | KEY_N: [{ env: FULL_NAME }] 101 | KEY_P: [{ env: PASSWORD }] 102 | KEY_H: [{ unicode: 😂 }] 103 | KEY_F: [{ string: "() => {\n" }] 104 | KEY_C: [{ string: "|| {\n" }] 105 | 106 | combos: 107 | - keys: [KEY_A, KEY_S] 108 | action: KEY_LEFTMETA 109 | 110 | - keys: [KEY_S, KEY_D] 111 | action: KEY_LEFTSHIFT 112 | 113 | - keys: [KEY_D, KEY_F] 114 | action: KEY_LEFTCTRL 115 | 116 | - keys: [KEY_S, KEY_F] 117 | action: KEY_LEFTALT 118 | 119 | - keys: [KEY_J, KEY_K] 120 | action: KEY_RIGHTCTRL 121 | 122 | - keys: [KEY_K, KEY_L] 123 | action: KEY_RIGHTSHIFT 124 | 125 | - keys: [KEY_J, KEY_L] 126 | action: KEY_RIGHTALT 127 | 128 | - keys: [KEY_L, KEY_SEMICOLON] 129 | action: KEY_RIGHTMETA 130 | -------------------------------------------------------------------------------- /src/tests/macros.rs: -------------------------------------------------------------------------------- 1 | use super::utils::*; 2 | 3 | const CONFIG: &str = include_str!("./config/macros.yaml"); 4 | 5 | #[test] 6 | fn test_macro_key() -> Result<()> { 7 | let mut proxy = EventProxyMock::default(); 8 | let mut adapter = KeyAdapter::with_config(CONFIG, &mut proxy); 9 | 10 | let expected = InputBuffer::new(KeyCode::KEY_H) 11 | .tap_then(KeyCode::KEY_E) 12 | .tap_then(KeyCode::KEY_L) 13 | .tap() 14 | .tap_then(KeyCode::KEY_O) 15 | .tap(); 16 | 17 | adapter.process(InputBuffer::tap(KeyCode::KEY_Q))?; 18 | 19 | assert_eq!(proxy.queue(), expected.value()); 20 | 21 | Ok(()) 22 | } 23 | 24 | #[test] 25 | fn test_macro_hold() -> Result<()> { 26 | let mut proxy = EventProxyMock::default(); 27 | let mut adapter = KeyAdapter::with_config(CONFIG, &mut proxy); 28 | 29 | let expected = InputBuffer::new(KeyCode::KEY_H) 30 | .tap_then(KeyCode::KEY_E) 31 | .tap_then(KeyCode::KEY_L) 32 | .tap() 33 | .tap_then(KeyCode::KEY_O) 34 | .tap(); 35 | 36 | adapter.process(InputBuffer::tap_hold(KeyCode::KEY_Q))?; 37 | 38 | // macros should not repeat on hold 39 | assert_eq!(proxy.queue(), expected.value()); 40 | 41 | Ok(()) 42 | } 43 | 44 | #[test] 45 | fn test_event_macro() -> Result<()> { 46 | let mut proxy = EventProxyMock::default(); 47 | let mut adapter = KeyAdapter::with_config(CONFIG, &mut proxy); 48 | 49 | adapter.process(InputBuffer::tap(KeyCode::KEY_X))?; 50 | 51 | let expected = InputBuffer::new(KeyCode::KEY_O) 52 | .shifted() 53 | .then(KeyCode::KEY_K) 54 | .tap(); 55 | 56 | assert_eq!(proxy.queue(), expected.value()); 57 | 58 | Ok(()) 59 | } 60 | 61 | #[test] 62 | fn test_string_macro() -> Result<()> { 63 | let mut proxy = EventProxyMock::default(); 64 | let mut adapter = KeyAdapter::with_config(CONFIG, &mut proxy); 65 | 66 | adapter.process(InputBuffer::tap(KeyCode::KEY_R))?; 67 | 68 | let expected = InputBuffer::new(KeyCode::KEY_H) 69 | .shifted() 70 | .then(KeyCode::KEY_I) 71 | .tap_then(KeyCode::KEY_COMMA) 72 | .tap_then(KeyCode::KEY_SPACE) 73 | .tap_then(KeyCode::KEY_Y) 74 | .tap_then(KeyCode::KEY_O) 75 | .tap_then(KeyCode::KEY_U) 76 | .tap_then(KeyCode::KEY_1) 77 | .shifted(); 78 | 79 | assert_eq!(proxy.queue(), expected.value()); 80 | 81 | Ok(()) 82 | } 83 | 84 | #[test] 85 | fn test_env_macro() -> Result<()> { 86 | let mut proxy = EventProxyMock::default(); 87 | let mut adapter = KeyAdapter::with_config(CONFIG, &mut proxy); 88 | 89 | unsafe { std::env::set_var("FOO", "foo") }; 90 | 91 | adapter.process(InputBuffer::tap(KeyCode::KEY_W))?; 92 | 93 | let expected = InputBuffer::new(KeyCode::KEY_F) 94 | .tap_then(KeyCode::KEY_O) 95 | .tap() 96 | .tap(); 97 | 98 | assert_eq!(proxy.queue(), expected.value()); 99 | 100 | Ok(()) 101 | } 102 | 103 | #[test] 104 | fn test_unicode_macro() -> Result<()> { 105 | let mut proxy = EventProxyMock::default(); 106 | let mut adapter = KeyAdapter::with_config(CONFIG, &mut proxy); 107 | 108 | adapter.process(InputBuffer::tap(KeyCode::KEY_T))?; 109 | 110 | let expected = unicode() 111 | .then(KeyCode::KEY_1) 112 | .tap_then(KeyCode::KEY_F) 113 | .tap_then(KeyCode::KEY_6) 114 | .tap_then(KeyCode::KEY_4) 115 | .tap_then(KeyCode::KEY_2) 116 | .tap_then(KeyCode::KEY_ENTER) 117 | .tap() 118 | .chain(unicode()) 119 | .then(KeyCode::KEY_1) 120 | .tap_then(KeyCode::KEY_F) 121 | .tap_then(KeyCode::KEY_4) 122 | .tap_then(KeyCode::KEY_4) 123 | .tap_then(KeyCode::KEY_D) 124 | .tap_then(KeyCode::KEY_ENTER) 125 | .tap(); 126 | 127 | assert_eq!(proxy.queue(), expected.value()); 128 | 129 | Ok(()) 130 | } 131 | 132 | #[test] 133 | fn test_shell_macro() -> Result<()> { 134 | let mut proxy = EventProxyMock::default(); 135 | let mut adapter = KeyAdapter::with_config(CONFIG, &mut proxy); 136 | 137 | adapter.process(InputBuffer::tap(KeyCode::KEY_Z))?; 138 | 139 | let expected = InputBuffer::new(KeyCode::KEY_F) 140 | .tap_then(KeyCode::KEY_O) 141 | .tap() 142 | .tap(); 143 | 144 | assert_eq!(proxy.queue(), expected.value()); 145 | 146 | Ok(()) 147 | } 148 | -------------------------------------------------------------------------------- /src/core/layer.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use smallvec::SmallVec; 4 | 5 | use crate::config::schema::{KeyAction, LayerDefinition, LayerModifierKind}; 6 | 7 | use super::{adapter::InputResult, shared::RawKeyCode}; 8 | 9 | #[derive(Debug, Clone)] 10 | pub struct LayerItem { 11 | modifier: RawKeyCode, 12 | base_layer: Option, 13 | } 14 | 15 | #[derive(Debug)] 16 | pub struct LayerManager { 17 | layer_map: HashMap, 18 | layer_stack: SmallVec<[LayerItem; 5]>, 19 | pending: SmallVec<[LayerItem; 5]>, 20 | } 21 | 22 | impl LayerManager { 23 | pub fn new(definitions: HashMap) -> Self { 24 | Self { 25 | layer_map: definitions 26 | .into_values() 27 | .map(|value| (value.modifier.get_modifer().value(), value)) 28 | .collect(), 29 | layer_stack: SmallVec::default(), 30 | pending: SmallVec::default(), 31 | } 32 | } 33 | 34 | pub fn map(&mut self, action: KeyAction) -> KeyAction { 35 | let KeyAction::KeyCode(code) = &action else { 36 | return action; 37 | }; 38 | 39 | for layer in self.layer_stack.iter().rev() { 40 | let mapped_action = self 41 | .layer_map 42 | .get(&layer.modifier) 43 | .and_then(|layer| layer.keys.get(code)); 44 | 45 | if let Some(action) = mapped_action { 46 | return action.clone(); 47 | }; 48 | } 49 | 50 | action 51 | } 52 | 53 | pub fn handle_press(&mut self, code: RawKeyCode) -> Option { 54 | if let Some(config) = self.layer_map.get(&code) { 55 | match config.modifier.get_modifer_kind() { 56 | LayerModifierKind::Toggle if self.is_layer_active(code) => self.pop_layer(code), 57 | _ if !self.is_layer_active(code) => self.push_layer(code), 58 | _ => {} 59 | }; 60 | 61 | Some(InputResult::None) 62 | } else { 63 | None 64 | } 65 | } 66 | 67 | pub fn handle_hold(&mut self, code: RawKeyCode) -> Option { 68 | if self.is_layer_active(code) { 69 | Some(InputResult::None) 70 | } else { 71 | None 72 | } 73 | } 74 | 75 | pub fn handle_release(&mut self, code: RawKeyCode) -> Option { 76 | if let Some(definition) = self.layer_map.get(&code) { 77 | if let LayerModifierKind::Momentary = definition.modifier.get_modifer_kind() { 78 | if let Some(dependent) = self.find_dependent_layer(code) { 79 | self.pending.push(dependent); 80 | } else { 81 | self.pop_layer(code); 82 | } 83 | } 84 | 85 | Some(InputResult::None) 86 | } else { 87 | if let Some(layer) = self.get_oneshoot_layer() { 88 | self.pop_layer(layer.modifier); 89 | } 90 | 91 | None 92 | } 93 | } 94 | 95 | fn is_layer_active(&self, modifier: RawKeyCode) -> bool { 96 | self.layer_stack 97 | .iter() 98 | .any(|layer| layer.modifier == modifier) 99 | } 100 | 101 | fn push_layer(&mut self, modifier: RawKeyCode) { 102 | let base_layer = self.layer_stack.last().map(|value| value.modifier); 103 | 104 | self.layer_stack.push(LayerItem { 105 | modifier, 106 | base_layer, 107 | }); 108 | } 109 | 110 | fn pop_layer(&mut self, modifier: RawKeyCode) { 111 | if let Some(pending_layer) = self.find_pending_layer(modifier) { 112 | self.pending 113 | .retain(|layer| layer.modifier != pending_layer.modifier); 114 | 115 | if let Some(base_layer) = pending_layer.base_layer { 116 | self.pop_layer(base_layer); 117 | } 118 | } 119 | 120 | self.layer_stack.retain(|layer| layer.modifier != modifier); 121 | } 122 | 123 | fn find_pending_layer(&self, modifier: RawKeyCode) -> Option { 124 | self.pending 125 | .iter() 126 | .find(|layer| layer.modifier == modifier) 127 | .cloned() 128 | } 129 | 130 | fn find_dependent_layer(&self, modifier: RawKeyCode) -> Option { 131 | self.layer_stack 132 | .iter() 133 | .rev() 134 | .find(|layer| layer.base_layer.is_some_and(|base| base == modifier)) 135 | .cloned() 136 | } 137 | 138 | fn get_oneshoot_layer(&self) -> Option { 139 | self.layer_stack 140 | .iter() 141 | .rev() 142 | .find(|layer| self.is_oneshoot_layer(layer)) 143 | .cloned() 144 | } 145 | 146 | fn is_oneshoot_layer(&self, layer: &LayerItem) -> bool { 147 | self.layer_map 148 | .get(&layer.modifier) 149 | .map(|layer| layer.modifier.get_modifer_kind()) 150 | .is_some_and(|kind| matches!(kind, LayerModifierKind::Oneshoot)) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/core/input.rs: -------------------------------------------------------------------------------- 1 | use std::{process::Command, str::FromStr}; 2 | 3 | use anyhow::{anyhow, Result}; 4 | 5 | use crate::config::schema::KeyCode; 6 | 7 | use super::{ 8 | adapter::InputResult, 9 | event::{IntoInputEvent, HOLD_EVENT, PRESS_EVENT, RELEASE_EVENT}, 10 | }; 11 | 12 | fn char_to_input(char: char) -> Result { 13 | let result = match char { 14 | char if char.is_alphabetic() | char.is_numeric() => { 15 | let raw_keycode = format!("KEY_{}", char.to_uppercase()); 16 | let keycode = evdev::KeyCode::from_str(&raw_keycode).unwrap(); 17 | Some((keycode, char.is_uppercase())) 18 | } 19 | 20 | '!' => Some((evdev::KeyCode::KEY_1, true)), 21 | '@' => Some((evdev::KeyCode::KEY_2, true)), 22 | '#' => Some((evdev::KeyCode::KEY_3, true)), 23 | '$' => Some((evdev::KeyCode::KEY_4, true)), 24 | '%' => Some((evdev::KeyCode::KEY_5, true)), 25 | '^' => Some((evdev::KeyCode::KEY_6, true)), 26 | '&' => Some((evdev::KeyCode::KEY_7, true)), 27 | '*' => Some((evdev::KeyCode::KEY_8, true)), 28 | '(' => Some((evdev::KeyCode::KEY_9, true)), 29 | ')' => Some((evdev::KeyCode::KEY_0, true)), 30 | 31 | ' ' => Some((evdev::KeyCode::KEY_SPACE, false)), 32 | '\t' => Some((evdev::KeyCode::KEY_TAB, false)), 33 | '\n' => Some((evdev::KeyCode::KEY_ENTER, false)), 34 | 35 | '`' => Some((evdev::KeyCode::KEY_GRAVE, false)), 36 | '-' => Some((evdev::KeyCode::KEY_MINUS, false)), 37 | '=' => Some((evdev::KeyCode::KEY_EQUAL, false)), 38 | '[' => Some((evdev::KeyCode::KEY_LEFTBRACE, false)), 39 | ']' => Some((evdev::KeyCode::KEY_RIGHTBRACE, false)), 40 | '\\' => Some((evdev::KeyCode::KEY_BACKSLASH, false)), 41 | ';' => Some((evdev::KeyCode::KEY_SEMICOLON, false)), 42 | '\'' => Some((evdev::KeyCode::KEY_APOSTROPHE, false)), 43 | ',' => Some((evdev::KeyCode::KEY_COMMA, false)), 44 | '.' => Some((evdev::KeyCode::KEY_DOT, false)), 45 | '/' => Some((evdev::KeyCode::KEY_SLASH, false)), 46 | 47 | '~' => Some((evdev::KeyCode::KEY_GRAVE, true)), 48 | '_' => Some((evdev::KeyCode::KEY_MINUS, true)), 49 | '+' => Some((evdev::KeyCode::KEY_EQUAL, true)), 50 | '{' => Some((evdev::KeyCode::KEY_LEFTBRACE, true)), 51 | '}' => Some((evdev::KeyCode::KEY_RIGHTBRACE, true)), 52 | '|' => Some((evdev::KeyCode::KEY_BACKSLASH, true)), 53 | ':' => Some((evdev::KeyCode::KEY_SEMICOLON, true)), 54 | '"' => Some((evdev::KeyCode::KEY_APOSTROPHE, true)), 55 | '<' => Some((evdev::KeyCode::KEY_COMMA, true)), 56 | '>' => Some((evdev::KeyCode::KEY_DOT, true)), 57 | '?' => Some((evdev::KeyCode::KEY_SLASH, true)), 58 | 59 | _ => None, 60 | }; 61 | 62 | match result { 63 | Some((code, uppercase)) if uppercase => { 64 | let shift = KeyCode::from(evdev::KeyCode::KEY_LEFTSHIFT); 65 | let code = KeyCode::from(code); 66 | 67 | let results = vec![ 68 | shift.to_event(PRESS_EVENT), 69 | shift.to_event(HOLD_EVENT), 70 | code.to_event(PRESS_EVENT), 71 | code.to_event(RELEASE_EVENT), 72 | shift.to_event(RELEASE_EVENT), 73 | ]; 74 | 75 | Ok(InputResult::Raw(results)) 76 | } 77 | 78 | Some((code, _)) => Ok(InputResult::Raw(vec![ 79 | KeyCode::from(code).to_event(PRESS_EVENT), 80 | KeyCode::from(code).to_event(RELEASE_EVENT), 81 | ])), 82 | 83 | None => Err(anyhow!("Invalid character literal")), 84 | } 85 | } 86 | 87 | pub fn string_to_input(source: &str) -> Result> { 88 | source 89 | .chars() 90 | .map(char_to_input) 91 | .collect::>>() 92 | } 93 | 94 | pub fn unicode_to_input(source: &str, delay: u16) -> Result> { 95 | let ctrl = KeyCode::from(evdev::KeyCode::KEY_LEFTCTRL); 96 | let shift = KeyCode::from(evdev::KeyCode::KEY_LEFTSHIFT); 97 | let u = KeyCode::from(evdev::KeyCode::KEY_U); 98 | let enter = KeyCode::from(evdev::KeyCode::KEY_ENTER); 99 | 100 | let value = source 101 | .chars() 102 | .map(|c| format!("{:x}", c as u32)) 103 | .map(|c| string_to_input(&c)) 104 | .collect::>>()? 105 | .into_iter() 106 | .flat_map(|value| { 107 | let ctrl_shift_u = [InputResult::Raw(vec![ 108 | ctrl.to_event(PRESS_EVENT), 109 | shift.to_event(PRESS_EVENT), 110 | u.to_event(PRESS_EVENT), 111 | ctrl.to_event(HOLD_EVENT), 112 | shift.to_event(HOLD_EVENT), 113 | u.to_event(HOLD_EVENT), 114 | ctrl.to_event(RELEASE_EVENT), 115 | shift.to_event(RELEASE_EVENT), 116 | u.to_event(RELEASE_EVENT), 117 | ])]; 118 | 119 | ctrl_shift_u 120 | .into_iter() 121 | .chain(value) 122 | .chain([ 123 | InputResult::Raw(vec![ 124 | enter.to_event(PRESS_EVENT), 125 | enter.to_event(RELEASE_EVENT), 126 | ]), 127 | InputResult::Delay(delay.into()), 128 | ]) 129 | .collect::>() 130 | }); 131 | 132 | Ok(value.collect()) 133 | } 134 | 135 | pub fn command_to_input(command: &str, trim: Option) -> Result> { 136 | let output = Command::new("bash").arg("-c").arg(command).output()?; 137 | let parsed = String::from_utf8(output.stdout)?; 138 | let trim = trim.unwrap_or_default(); 139 | let slice = if trim { parsed.trim() } else { &parsed }; 140 | 141 | string_to_input(slice) 142 | } 143 | -------------------------------------------------------------------------------- /src/core/tap_dance.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, time::Instant}; 2 | 3 | use smallvec::SmallVec; 4 | 5 | use crate::{ 6 | config::schema::{DefaultTapDanceConfig, KeyAction, KeyCode, TapDanceConfig}, 7 | core::buffer::InputBuffer, 8 | }; 9 | 10 | use super::{adapter::InputResult, shared::RawKeyCode}; 11 | 12 | #[derive(Debug)] 13 | pub struct TapDanceManager { 14 | tap_dances: HashMap, 15 | pressed_keys: SmallVec<[PressedKey; 4]>, 16 | supressed_keys: SmallVec<[RawKeyCode; 2]>, 17 | config: DefaultTapDanceConfig, 18 | } 19 | 20 | impl TapDanceManager { 21 | pub fn new( 22 | tap_dances: HashMap, 23 | config: DefaultTapDanceConfig, 24 | ) -> Self { 25 | let tap_dances = tap_dances 26 | .into_iter() 27 | .map(|(key, value)| (key.value(), value)) 28 | .collect(); 29 | 30 | Self { 31 | config, 32 | tap_dances, 33 | pressed_keys: SmallVec::default(), 34 | supressed_keys: SmallVec::default(), 35 | } 36 | } 37 | 38 | pub fn handle_press(&mut self, code: RawKeyCode) -> Option { 39 | if let Some(config) = self.tap_dances.get(&code) { 40 | self.pressed_keys 41 | .push(PressedKey::new(code, config, self.config.default_timeout)); 42 | 43 | Some(InputResult::Pending(KeyCode::new(code))) 44 | } else { 45 | None 46 | } 47 | } 48 | 49 | pub fn handle_hold(&mut self, code: RawKeyCode) -> Option { 50 | self.tap_dances 51 | .contains_key(&code) 52 | .then_some(InputResult::None) 53 | } 54 | 55 | pub fn handle_release(&mut self, code: RawKeyCode) -> Option { 56 | let key = self.pressed_keys.iter_mut().find(|s| s.code == code); 57 | 58 | if !self.supressed_keys.is_empty() { 59 | self.supressed_keys.retain(|key| *key != code); 60 | } 61 | 62 | if let Some(key) = key { 63 | key.released = true; 64 | Some(InputResult::None) 65 | } else { 66 | None 67 | } 68 | } 69 | 70 | pub fn process(&mut self, buffer: &mut InputBuffer) { 71 | if self.pressed_keys.is_empty() { 72 | return; 73 | } 74 | 75 | let now = Instant::now(); 76 | 77 | for (idx, state) in self.pressed_keys.iter().enumerate() { 78 | if self.supressed_keys.contains(&state.code) { 79 | continue; 80 | } 81 | 82 | let timeout = state.reached_timeout(now); 83 | let result = state.get_dance_result(timeout); 84 | let code = KeyCode::new(state.code); 85 | 86 | match &result { 87 | InputResult::Macro(_) => { 88 | self.supressed_keys.push(state.code); 89 | } 90 | InputResult::DoubleSequence(inner) if !timeout => { 91 | if let [InputResult::Press(out_code), _] = inner.as_ref() { 92 | if *out_code != code { 93 | buffer.clear_pending_key(&code); 94 | } 95 | } 96 | } 97 | _ => {} 98 | } 99 | 100 | if timeout { 101 | buffer.clear_pending_key(&code); 102 | } 103 | 104 | if state.released { 105 | buffer.push_key(idx as u16); 106 | } 107 | 108 | buffer.push_result(result); 109 | } 110 | 111 | while let Some(idx) = buffer.pop_key() { 112 | self.pressed_keys.remove(idx as usize); 113 | } 114 | } 115 | } 116 | 117 | #[derive(Debug)] 118 | struct PressedKey { 119 | code: RawKeyCode, 120 | timeout: u16, 121 | timestamp: Instant, 122 | released: bool, 123 | tap: KeyAction, 124 | hold: KeyAction, 125 | } 126 | 127 | impl PressedKey { 128 | fn new(code: RawKeyCode, config: &TapDanceConfig, default_timeout: u16) -> Self { 129 | PressedKey { 130 | code, 131 | timeout: config.timeout.unwrap_or(default_timeout), 132 | timestamp: Instant::now(), 133 | released: false, 134 | tap: config.tap.clone(), 135 | hold: config.hold.clone(), 136 | } 137 | } 138 | 139 | fn reached_timeout(&self, now: Instant) -> bool { 140 | let elapsed = now.duration_since(self.timestamp).as_millis(); 141 | let timeout = self.timeout as u128; 142 | elapsed > timeout 143 | } 144 | 145 | fn get_dance_result(&self, timeout: bool) -> InputResult { 146 | if self.released && timeout { 147 | self.get_release_result() 148 | } else if self.released { 149 | self.get_tap_result() 150 | } else if timeout { 151 | self.get_hold_result() 152 | } else { 153 | InputResult::None 154 | } 155 | } 156 | 157 | fn get_release_result(&self) -> InputResult { 158 | match &self.hold { 159 | KeyAction::KeyCode(code) => InputResult::Release(*code), 160 | KeyAction::Macro(_) => InputResult::None, 161 | } 162 | } 163 | 164 | fn get_tap_result(&self) -> InputResult { 165 | match &self.tap { 166 | KeyAction::KeyCode(code) => InputResult::DoubleSequence(Box::new([ 167 | InputResult::Press(*code), 168 | InputResult::Release(*code), 169 | ])), 170 | KeyAction::Macro(codes) => InputResult::Macro(codes.clone()), 171 | } 172 | } 173 | 174 | fn get_hold_result(&self) -> InputResult { 175 | match &self.hold { 176 | KeyAction::KeyCode(code) => InputResult::DoubleSequence(Box::new([ 177 | InputResult::Press(*code), 178 | InputResult::Hold(*code), 179 | ])), 180 | KeyAction::Macro(codes) => InputResult::Macro(codes.clone()), 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/tests/utils.rs: -------------------------------------------------------------------------------- 1 | use std::{thread, time::Duration}; 2 | 3 | use evdev::{EventType, InputEvent}; 4 | 5 | use crate::{config::schema::Config, core::EventProxy}; 6 | 7 | pub trait EventProcessor { 8 | fn process_input(&mut self, event: InputEvent) -> Result<()>; 9 | 10 | fn process(&mut self, buffer: InputBuffer) -> Result<()> { 11 | for event in buffer.value() { 12 | self.process_input(*event)?; 13 | } 14 | 15 | Ok(()) 16 | } 17 | } 18 | 19 | impl EventProcessor for KeyAdapter<'_, P> { 20 | fn process_input(&mut self, event: InputEvent) -> Result<()> { 21 | self.process_event(event)?; 22 | self.post_process() 23 | } 24 | } 25 | 26 | #[derive(Debug, Default)] 27 | pub struct EventProxyMock { 28 | queue: Vec, 29 | } 30 | 31 | impl EventProxyMock { 32 | pub fn queue(&self) -> &[InputEvent] { 33 | &self.queue 34 | } 35 | 36 | pub fn clear(&mut self) { 37 | self.queue.clear(); 38 | } 39 | } 40 | 41 | impl EventProxy for EventProxyMock { 42 | fn emit(&mut self, events: &[evdev::InputEvent]) -> anyhow::Result<()> { 43 | self.queue.extend(events); 44 | Ok(()) 45 | } 46 | 47 | fn wait(&mut self, timeout: u16) -> Result<()> { 48 | thread::sleep(Duration::from_millis(timeout.into())); 49 | Ok(()) 50 | } 51 | } 52 | 53 | impl<'a, P: EventProxy> KeyAdapter<'a, P> { 54 | pub fn with_config(config: &str, proxy: &'a mut P) -> Self { 55 | let mut config: Config = serde_yaml::from_str(config).unwrap(); 56 | let keyboard = config.keyboards.remove(0); 57 | let defaults = config.defaults.clone(); 58 | 59 | KeyAdapter::new(keyboard, defaults, proxy) 60 | } 61 | } 62 | 63 | pub trait EventTarget: Sized { 64 | fn code(&self) -> KeyCode; 65 | fn release(self) -> InputBuffer; 66 | fn press(self) -> InputBuffer; 67 | fn hold(self) -> InputBuffer; 68 | fn into_value(self) -> Vec; 69 | 70 | fn tap(self) -> InputBuffer { 71 | self.press().release() 72 | } 73 | 74 | fn tap_hold(self) -> InputBuffer { 75 | self.press().hold().release() 76 | } 77 | 78 | fn tap_then(self, next: KeyCode) -> InputBuffer { 79 | self.tap().then(next) 80 | } 81 | 82 | fn press_then(self, next: KeyCode) -> InputBuffer { 83 | self.press().then(next) 84 | } 85 | 86 | fn hold_then(self, next: KeyCode) -> InputBuffer { 87 | self.hold().then(next) 88 | } 89 | 90 | fn release_then(self, next: KeyCode) -> InputBuffer { 91 | self.release().then(next) 92 | } 93 | 94 | fn shifted(self) -> InputBuffer { 95 | let code = self.code(); 96 | 97 | let buffer = InputBuffer::new(KeyCode::KEY_LEFTSHIFT) 98 | .press() 99 | .hold() 100 | .then(code) 101 | .tap_then(KeyCode::KEY_LEFTSHIFT) 102 | .release(); 103 | 104 | InputBuffer::new(code).chain(self).chain(buffer) 105 | } 106 | } 107 | 108 | #[derive(Debug, Clone)] 109 | pub struct InputBuffer { 110 | current_code: KeyCode, 111 | buffer: Vec, 112 | } 113 | 114 | impl InputBuffer { 115 | pub fn new(code: KeyCode) -> Self { 116 | Self { 117 | current_code: code, 118 | buffer: Vec::new(), 119 | } 120 | } 121 | 122 | pub fn then(mut self, code: KeyCode) -> Self { 123 | self.current_code = code; 124 | self 125 | } 126 | 127 | pub fn value(&self) -> &[InputEvent] { 128 | &self.buffer 129 | } 130 | 131 | pub fn chain(mut self, other: E) -> Self { 132 | self.buffer.extend(other.into_value()); 133 | self 134 | } 135 | 136 | pub fn press(code: KeyCode) -> Self { 137 | Self::new(code).press() 138 | } 139 | 140 | pub fn release(code: KeyCode) -> Self { 141 | Self::new(code).release() 142 | } 143 | 144 | pub fn tap(code: KeyCode) -> Self { 145 | Self::new(code).tap() 146 | } 147 | 148 | pub fn tap_hold(code: KeyCode) -> Self { 149 | Self::new(code).tap_hold() 150 | } 151 | 152 | pub fn combo_press(first: KeyCode, other: KeyCode) -> Self { 153 | Self::new(first).press_then(other).press() 154 | } 155 | 156 | pub fn combo_hold(first: KeyCode, other: KeyCode) -> Self { 157 | Self::new(first).hold_then(other).hold() 158 | } 159 | 160 | pub fn combo_release(first: KeyCode, other: KeyCode) -> Self { 161 | Self::new(first).release_then(other).release() 162 | } 163 | } 164 | 165 | impl EventTarget for InputBuffer { 166 | fn code(&self) -> KeyCode { 167 | self.current_code 168 | } 169 | 170 | fn into_value(self) -> Vec { 171 | self.buffer 172 | } 173 | 174 | fn release(mut self) -> InputBuffer { 175 | self.buffer.push(release(self.current_code)); 176 | self 177 | } 178 | 179 | fn press(mut self) -> InputBuffer { 180 | self.buffer.push(press(self.current_code)); 181 | self 182 | } 183 | 184 | fn hold(mut self) -> InputBuffer { 185 | self.buffer.push(hold(self.current_code)); 186 | self 187 | } 188 | } 189 | 190 | pub fn unicode() -> InputBuffer { 191 | InputBuffer::new(KeyCode::KEY_LEFTCTRL) 192 | .press_then(KeyCode::KEY_LEFTSHIFT) 193 | .press_then(KeyCode::KEY_U) 194 | .press_then(KeyCode::KEY_LEFTCTRL) 195 | .hold_then(KeyCode::KEY_LEFTSHIFT) 196 | .hold_then(KeyCode::KEY_U) 197 | .hold_then(KeyCode::KEY_LEFTCTRL) 198 | .release_then(KeyCode::KEY_LEFTSHIFT) 199 | .release_then(KeyCode::KEY_U) 200 | .release() 201 | } 202 | 203 | fn release(code: KeyCode) -> InputEvent { 204 | InputEvent::new(EventType::KEY.0, code.code(), 0) 205 | } 206 | 207 | fn press(code: KeyCode) -> InputEvent { 208 | InputEvent::new(EventType::KEY.0, code.code(), 1) 209 | } 210 | 211 | fn hold(code: KeyCode) -> InputEvent { 212 | InputEvent::new(EventType::KEY.0, code.code(), 2) 213 | } 214 | 215 | pub use anyhow::Result; 216 | pub use evdev::KeyCode; 217 | 218 | pub use crate::core::KeyAdapter; 219 | -------------------------------------------------------------------------------- /src/tests/layers.rs: -------------------------------------------------------------------------------- 1 | use std::{thread, time::Duration}; 2 | 3 | use super::utils::*; 4 | 5 | const CONFIG: &str = include_str!("./config/layers.yaml"); 6 | 7 | #[test] 8 | fn test_simple_momentary_layer() -> Result<()> { 9 | let mut proxy = EventProxyMock::default(); 10 | let mut adapter = KeyAdapter::with_config(CONFIG, &mut proxy); 11 | 12 | adapter.process( 13 | InputBuffer::new(KeyCode::KEY_SPACE) 14 | .press() 15 | .hold_then(KeyCode::KEY_P) 16 | .tap_then(KeyCode::KEY_V) // second layer 17 | .press() 18 | .hold_then(KeyCode::KEY_P) 19 | .tap_then(KeyCode::KEY_V) 20 | .release_then(KeyCode::KEY_SPACE) 21 | .release_then(KeyCode::KEY_P) 22 | .tap(), 23 | )?; 24 | 25 | let expected = InputBuffer::new(KeyCode::KEY_Q) 26 | .tap_then(KeyCode::KEY_X) 27 | .tap_then(KeyCode::KEY_P) 28 | .tap(); 29 | 30 | assert_eq!(proxy.queue(), expected.value()); 31 | 32 | Ok(()) 33 | } 34 | 35 | #[test] 36 | fn test_combo_momentary_layer() -> Result<()> { 37 | let mut proxy = EventProxyMock::default(); 38 | let mut adapter = KeyAdapter::with_config(CONFIG, &mut proxy); 39 | 40 | adapter.process(InputBuffer::combo_press(KeyCode::KEY_S, KeyCode::KEY_D))?; 41 | 42 | thread::sleep(Duration::from_millis(90)); 43 | 44 | adapter.process(InputBuffer::combo_hold(KeyCode::KEY_S, KeyCode::KEY_D))?; 45 | 46 | thread::sleep(Duration::from_millis(90)); 47 | 48 | adapter.process(InputBuffer::tap(KeyCode::KEY_P))?; 49 | adapter.process(InputBuffer::combo_release(KeyCode::KEY_S, KeyCode::KEY_D))?; 50 | adapter.process(InputBuffer::tap(KeyCode::KEY_P))?; 51 | 52 | let expected = InputBuffer::new(KeyCode::KEY_X) 53 | .tap_then(KeyCode::KEY_P) 54 | .tap(); 55 | 56 | assert_eq!(proxy.queue(), expected.value()); 57 | 58 | Ok(()) 59 | } 60 | 61 | #[test] 62 | fn test_tap_dance_momentary_layer() -> Result<()> { 63 | let mut proxy = EventProxyMock::default(); 64 | let mut adapter = KeyAdapter::with_config(CONFIG, &mut proxy); 65 | 66 | adapter.process(InputBuffer::press(KeyCode::KEY_A).hold())?; 67 | 68 | thread::sleep(Duration::from_millis(250)); 69 | 70 | adapter.post_process()?; 71 | adapter.process(InputBuffer::tap(KeyCode::KEY_P))?; 72 | adapter.process(InputBuffer::release(KeyCode::KEY_A))?; 73 | adapter.process(InputBuffer::tap(KeyCode::KEY_P))?; 74 | 75 | let expected = InputBuffer::new(KeyCode::KEY_X) 76 | .tap_then(KeyCode::KEY_P) 77 | .tap(); 78 | 79 | assert_eq!(proxy.queue(), expected.value()); 80 | 81 | Ok(()) 82 | } 83 | 84 | #[test] 85 | fn test_tap_dance_toggle_layer() -> Result<()> { 86 | let mut proxy = EventProxyMock::default(); 87 | let mut adapter = KeyAdapter::with_config(CONFIG, &mut proxy); 88 | 89 | adapter.process(InputBuffer::tap(KeyCode::KEY_A))?; 90 | 91 | adapter.process(InputBuffer::tap(KeyCode::KEY_P))?; 92 | adapter.process(InputBuffer::tap(KeyCode::KEY_P))?; 93 | adapter.process(InputBuffer::tap(KeyCode::KEY_E))?; 94 | 95 | adapter.process(InputBuffer::tap(KeyCode::KEY_A))?; 96 | 97 | adapter.process(InputBuffer::tap(KeyCode::KEY_P))?; 98 | 99 | let expected = InputBuffer::new(KeyCode::KEY_X) 100 | .tap_then(KeyCode::KEY_X) 101 | .tap_then(KeyCode::KEY_E) 102 | .tap_then(KeyCode::KEY_P) 103 | .tap(); 104 | 105 | assert_eq!(proxy.queue(), expected.value()); 106 | 107 | Ok(()) 108 | } 109 | 110 | #[test] 111 | fn test_combo_toggle_layer() -> Result<()> { 112 | let mut proxy = EventProxyMock::default(); 113 | let mut adapter = KeyAdapter::with_config(CONFIG, &mut proxy); 114 | 115 | adapter.process(InputBuffer::combo_press(KeyCode::KEY_K, KeyCode::KEY_L))?; 116 | adapter.process(InputBuffer::combo_release(KeyCode::KEY_K, KeyCode::KEY_L))?; 117 | 118 | adapter.process(InputBuffer::tap(KeyCode::KEY_P))?; 119 | adapter.process(InputBuffer::tap(KeyCode::KEY_P))?; 120 | adapter.process(InputBuffer::tap(KeyCode::KEY_E))?; 121 | 122 | adapter.process(InputBuffer::combo_press(KeyCode::KEY_K, KeyCode::KEY_L))?; 123 | adapter.process(InputBuffer::combo_release(KeyCode::KEY_K, KeyCode::KEY_L))?; 124 | 125 | adapter.process(InputBuffer::tap(KeyCode::KEY_P))?; 126 | 127 | let expected = InputBuffer::new(KeyCode::KEY_X) 128 | .tap_then(KeyCode::KEY_X) 129 | .tap_then(KeyCode::KEY_E) 130 | .tap_then(KeyCode::KEY_P) 131 | .tap(); 132 | 133 | assert_eq!(proxy.queue(), expected.value()); 134 | 135 | Ok(()) 136 | } 137 | 138 | #[test] 139 | fn test_tap_dance_oneshoot_layer() -> Result<()> { 140 | let mut proxy = EventProxyMock::default(); 141 | let mut adapter = KeyAdapter::with_config(CONFIG, &mut proxy); 142 | 143 | adapter.process(InputBuffer::tap(KeyCode::KEY_O))?; 144 | 145 | adapter.process(InputBuffer::tap(KeyCode::KEY_P))?; 146 | adapter.process(InputBuffer::tap(KeyCode::KEY_P))?; 147 | adapter.process(InputBuffer::tap(KeyCode::KEY_E))?; 148 | 149 | adapter.process(InputBuffer::tap(KeyCode::KEY_O))?; 150 | 151 | adapter.process(InputBuffer::tap(KeyCode::KEY_P))?; 152 | 153 | let expected = InputBuffer::new(KeyCode::KEY_X) 154 | .tap_then(KeyCode::KEY_P) 155 | .tap_then(KeyCode::KEY_E) 156 | .tap_then(KeyCode::KEY_X) 157 | .tap(); 158 | 159 | assert_eq!(proxy.queue(), expected.value()); 160 | 161 | Ok(()) 162 | } 163 | 164 | #[test] 165 | fn test_combo_oneshoot_layer() -> Result<()> { 166 | let mut proxy = EventProxyMock::default(); 167 | let mut adapter = KeyAdapter::with_config(CONFIG, &mut proxy); 168 | 169 | adapter.process(InputBuffer::combo_press(KeyCode::KEY_D, KeyCode::KEY_F))?; 170 | adapter.process(InputBuffer::combo_release(KeyCode::KEY_D, KeyCode::KEY_F))?; 171 | 172 | adapter.process(InputBuffer::tap(KeyCode::KEY_P))?; 173 | adapter.process(InputBuffer::tap(KeyCode::KEY_P))?; 174 | adapter.process(InputBuffer::tap(KeyCode::KEY_E))?; 175 | 176 | adapter.process(InputBuffer::combo_press(KeyCode::KEY_D, KeyCode::KEY_F))?; 177 | adapter.process(InputBuffer::combo_release(KeyCode::KEY_D, KeyCode::KEY_F))?; 178 | 179 | adapter.process(InputBuffer::tap(KeyCode::KEY_P))?; 180 | 181 | let expected = InputBuffer::new(KeyCode::KEY_X) 182 | .tap_then(KeyCode::KEY_P) 183 | .tap_then(KeyCode::KEY_E) 184 | .tap_then(KeyCode::KEY_X) 185 | .tap(); 186 | 187 | assert_eq!(proxy.queue(), expected.value()); 188 | 189 | Ok(()) 190 | } 191 | 192 | #[test] 193 | fn test_reverse_release_momentary_layers() -> Result<()> { 194 | let mut proxy = EventProxyMock::default(); 195 | let mut adapter = KeyAdapter::with_config(CONFIG, &mut proxy); 196 | 197 | adapter.process( 198 | InputBuffer::new(KeyCode::KEY_SPACE) 199 | .press() 200 | .hold_then(KeyCode::KEY_P) 201 | .tap_then(KeyCode::KEY_V) 202 | .release_then(KeyCode::KEY_SPACE) 203 | .release_then(KeyCode::KEY_P) 204 | .tap(), 205 | )?; 206 | 207 | let expected = InputBuffer::new(KeyCode::KEY_Q) 208 | .tap_then(KeyCode::KEY_P) 209 | .tap(); 210 | 211 | assert_eq!(proxy.queue(), expected.value()); 212 | 213 | Ok(()) 214 | } 215 | -------------------------------------------------------------------------------- /src/core/combo.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashSet, time::Instant}; 2 | 3 | use smallvec::SmallVec; 4 | 5 | use crate::{ 6 | config::schema::{ComboConfig, ComboDefinition, DefaultComboConfig, KeyAction, KeyCode}, 7 | core::buffer::InputBuffer, 8 | }; 9 | 10 | use super::{adapter::InputResult, shared::RawKeyCode}; 11 | 12 | #[derive(Debug)] 13 | pub struct ComboManager { 14 | key_set: HashSet, 15 | definitions: Vec, 16 | pressed_keys: SmallVec<[ComboKey; 6]>, 17 | supressed_keys: SmallVec<[RawKeyCode; 6]>, 18 | active_combos: SmallVec<[ActiveCombo; 3]>, 19 | config: DefaultComboConfig, 20 | } 21 | 22 | impl ComboManager { 23 | pub fn new(combos: ComboConfig, config: DefaultComboConfig) -> Self { 24 | let key_set = combos 25 | .0 26 | .iter() 27 | .flat_map(|def| def.keys.iter().map(|key| key.value())) 28 | .collect(); 29 | 30 | let mut definitions = combos.0; 31 | 32 | definitions.sort_by(|a, b| b.keys.len().cmp(&a.keys.len())); 33 | 34 | Self { 35 | config, 36 | key_set, 37 | definitions, 38 | pressed_keys: SmallVec::default(), 39 | supressed_keys: SmallVec::default(), 40 | active_combos: SmallVec::default(), 41 | } 42 | } 43 | 44 | pub fn handle_press(&mut self, code: RawKeyCode) -> Option { 45 | if self.key_set.contains(&code) { 46 | self.pressed_keys.push(ComboKey::new(code)); 47 | Some(InputResult::Pending(KeyCode::new(code))) 48 | } else { 49 | None 50 | } 51 | } 52 | 53 | pub fn handle_hold(&mut self, code: RawKeyCode) -> Option { 54 | let key = self.pressed_keys.iter_mut().find(|key| key.code == code); 55 | 56 | if let Some(key) = key { 57 | let now = Instant::now(); 58 | let elapsed = now.duration_since(key.timestamp).as_millis(); 59 | 60 | if elapsed > self.config.default_threshold as u128 { 61 | key.hold = true; 62 | return Some(InputResult::None); 63 | } 64 | } 65 | 66 | None 67 | } 68 | 69 | pub fn handle_release(&mut self, code: RawKeyCode) -> Option { 70 | let key = self.pressed_keys.iter_mut().find(|key| key.code == code); 71 | 72 | if let Some(key) = key { 73 | key.released = true; 74 | Some(InputResult::None) 75 | } else { 76 | None 77 | } 78 | } 79 | 80 | pub fn process(&mut self, buffer: &mut InputBuffer) { 81 | if self.definitions.is_empty() { 82 | return; 83 | } 84 | 85 | self.process_key_results(buffer); // keys that exceeded threshold 86 | self.process_active_combos(buffer); 87 | self.process_combo_trigger(buffer); 88 | } 89 | 90 | fn process_key_results(&mut self, buffer: &mut InputBuffer) { 91 | let now = Instant::now(); 92 | let threshold = self.config.default_threshold; 93 | 94 | for key in &self.pressed_keys { 95 | if key.released { 96 | buffer.push_key(key.code); 97 | } 98 | 99 | if self.supressed_keys.contains(&key.code) { 100 | continue; 101 | } 102 | 103 | if let Some(result) = key.get_key_result(key.code, now, threshold) { 104 | // Hold event, pass control back to the main handler 105 | if let InputResult::Press(_) = &result { 106 | buffer.push_key(key.code); 107 | } 108 | 109 | buffer.push_result(result); 110 | } 111 | } 112 | 113 | while let Some(code) = buffer.pop_key() { 114 | self.pressed_keys.retain(|key| key.code != code); 115 | } 116 | } 117 | 118 | fn process_active_combos(&mut self, buffer: &mut InputBuffer) { 119 | for combo in &self.active_combos { 120 | let pressed_key = self.get_pressed_combo_key(combo); 121 | 122 | if let Some(key) = pressed_key { 123 | if let Some(code) = combo.code { 124 | if key.hold { 125 | buffer.push_result(InputResult::Hold(KeyCode::new(code))); 126 | } 127 | } 128 | } else { 129 | if let Some(code) = combo.code { 130 | buffer.push_result(InputResult::Release(KeyCode::new(code))); 131 | } 132 | 133 | for key in &combo.keys { 134 | self.supressed_keys.retain(|code| *code == key.value()); 135 | } 136 | 137 | buffer.push_key(combo.id); 138 | } 139 | } 140 | 141 | while let Some(id) = buffer.pop_key() { 142 | self.active_combos.retain(|combo| combo.id != id); 143 | } 144 | } 145 | 146 | fn process_combo_trigger(&mut self, buffer: &mut InputBuffer) { 147 | for (id, combo) in self.definitions.iter().enumerate() { 148 | if !self.should_activate_combo(combo) || self.is_combo_supressed(combo) { 149 | continue; 150 | } 151 | 152 | self.supressed_keys 153 | .extend(combo.keys.iter().map(|key| key.value())); 154 | 155 | let (code, result) = match &combo.action { 156 | KeyAction::KeyCode(code) => (Some(code.value()), InputResult::Press(*code)), 157 | KeyAction::Macro(codes) => (None, InputResult::Macro(codes.clone())), 158 | }; 159 | 160 | for key in &combo.keys { 161 | buffer.clear_pending_key(key); 162 | } 163 | 164 | self.active_combos 165 | .push(ActiveCombo::new(id as u16, code, combo.keys.clone())); 166 | 167 | buffer.push_result(result); 168 | } 169 | } 170 | 171 | fn get_pressed_combo_key(&self, combo: &ActiveCombo) -> Option<&ComboKey> { 172 | combo 173 | .keys 174 | .iter() 175 | .find_map(|key| self.pressed_keys.iter().find(|k| k.code == key.value())) 176 | } 177 | 178 | fn is_combo_supressed(&self, combo: &ComboDefinition) -> bool { 179 | combo 180 | .keys 181 | .iter() 182 | .any(|k| self.supressed_keys.contains(&k.value())) 183 | } 184 | 185 | fn should_activate_combo(&self, combo: &ComboDefinition) -> bool { 186 | combo.keys.iter().all(|key| { 187 | self.pressed_keys 188 | .iter() 189 | .find(|k| k.code == key.value()) 190 | .is_some_and(|value| !value.hold) 191 | }) 192 | } 193 | } 194 | 195 | #[derive(Debug)] 196 | struct ComboKey { 197 | code: RawKeyCode, 198 | timestamp: Instant, 199 | released: bool, 200 | hold: bool, 201 | } 202 | 203 | impl ComboKey { 204 | fn new(code: RawKeyCode) -> Self { 205 | ComboKey { 206 | code, 207 | timestamp: Instant::now(), 208 | released: false, 209 | hold: false, 210 | } 211 | } 212 | 213 | fn get_key_result( 214 | &self, 215 | code: RawKeyCode, 216 | now: Instant, 217 | threshold: u16, 218 | ) -> Option { 219 | let elapsed = now.duration_since(self.timestamp).as_millis(); 220 | 221 | if elapsed < threshold as u128 { 222 | return None; 223 | } 224 | 225 | if self.released { 226 | let result = if self.hold { 227 | InputResult::Release(KeyCode::new(code)) 228 | } else { 229 | InputResult::DoubleSequence(Box::new([ 230 | InputResult::Press(KeyCode::new(code)), 231 | InputResult::Release(KeyCode::new(code)), 232 | ])) 233 | }; 234 | 235 | return Some(result); 236 | } else if self.hold { 237 | return Some(InputResult::Press(KeyCode::new(code))); 238 | }; 239 | 240 | None 241 | } 242 | } 243 | 244 | #[derive(Debug)] 245 | struct ActiveCombo { 246 | id: u16, 247 | code: Option, 248 | keys: Vec, 249 | } 250 | 251 | impl ActiveCombo { 252 | fn new(id: u16, code: Option, keys: Vec) -> Self { 253 | Self { id, code, keys } 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /src/config/schema.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, collections::HashMap, hash::Hash, str::FromStr}; 2 | 3 | use serde::{ 4 | de::{value::StringDeserializer, IntoDeserializer}, 5 | Deserialize, Deserializer, 6 | }; 7 | 8 | use super::defaults; 9 | 10 | #[derive(Debug, Deserialize)] 11 | pub struct Config { 12 | #[serde(default)] 13 | pub defaults: DefaultConfig, 14 | pub keyboards: Vec, 15 | } 16 | 17 | #[derive(Debug, Default, Clone, Deserialize)] 18 | pub struct DefaultConfig { 19 | #[serde(default)] 20 | pub tap_dance: DefaultTapDanceConfig, 21 | #[serde(default)] 22 | pub combo: DefaultComboConfig, 23 | #[serde(default)] 24 | pub general: GeneralConfig, 25 | } 26 | 27 | #[derive(Debug, Clone, Deserialize)] 28 | pub struct DefaultTapDanceConfig { 29 | #[serde(default = "defaults::tap_dance_timeout")] 30 | pub default_timeout: u16, 31 | } 32 | 33 | impl Default for DefaultTapDanceConfig { 34 | fn default() -> Self { 35 | Self { 36 | default_timeout: defaults::tap_dance_timeout(), 37 | } 38 | } 39 | } 40 | 41 | #[derive(Debug, Clone, Deserialize)] 42 | pub struct DefaultComboConfig { 43 | #[serde(default = "defaults::combo_threshold")] 44 | pub default_threshold: u16, 45 | } 46 | 47 | impl Default for DefaultComboConfig { 48 | fn default() -> Self { 49 | Self { 50 | default_threshold: defaults::combo_threshold(), 51 | } 52 | } 53 | } 54 | 55 | #[derive(Debug, Clone, Deserialize)] 56 | pub struct GeneralConfig { 57 | #[serde(default = "defaults::event_poll_timeout")] 58 | pub event_poll_timeout: u16, 59 | #[serde(default = "defaults::deferred_key_delay")] 60 | pub deferred_key_delay: u16, 61 | #[serde(default = "defaults::unicode_input_delay")] 62 | pub unicode_input_delay: u16, 63 | #[serde(default = "defaults::maximum_lookup_depth")] 64 | pub maximum_lookup_depth: u8, 65 | } 66 | 67 | impl Default for GeneralConfig { 68 | fn default() -> Self { 69 | Self { 70 | event_poll_timeout: defaults::event_poll_timeout(), 71 | deferred_key_delay: defaults::deferred_key_delay(), 72 | unicode_input_delay: defaults::unicode_input_delay(), 73 | maximum_lookup_depth: defaults::maximum_lookup_depth(), 74 | } 75 | } 76 | } 77 | 78 | #[derive(Debug, Deserialize)] 79 | pub struct KeyboardConfig { 80 | pub name: String, 81 | #[serde(default)] 82 | pub keys: HashMap, 83 | #[serde(default)] 84 | pub combos: ComboConfig, 85 | #[serde(default)] 86 | pub tap_dances: HashMap, 87 | #[serde(default)] 88 | pub layers: HashMap, 89 | } 90 | 91 | #[derive(Debug, Default, Deserialize)] 92 | pub struct ComboConfig(pub Vec); 93 | 94 | #[derive(Debug, Deserialize)] 95 | pub struct ComboDefinition { 96 | pub keys: Vec, 97 | pub action: KeyAction, 98 | } 99 | 100 | #[derive(Debug, Deserialize)] 101 | pub struct TapDanceConfig { 102 | pub timeout: Option, 103 | pub tap: KeyAction, 104 | pub hold: KeyAction, 105 | } 106 | 107 | #[derive(Debug, Deserialize)] 108 | pub struct LayerDefinition { 109 | pub modifier: LayerModiferConfig, 110 | pub keys: HashMap, 111 | } 112 | 113 | #[derive(Debug, Deserialize)] 114 | #[serde(untagged)] 115 | pub enum LayerModiferConfig { 116 | Simple(KeyCode), 117 | Custom { 118 | key: KeyCode, 119 | #[serde(default)] 120 | r#type: LayerModifierKind, 121 | }, 122 | } 123 | 124 | impl LayerModiferConfig { 125 | pub fn get_modifer(&self) -> &KeyCode { 126 | match self { 127 | LayerModiferConfig::Simple(keycode) => keycode, 128 | LayerModiferConfig::Custom { key, r#type: _ } => key, 129 | } 130 | } 131 | 132 | pub fn get_modifer_kind(&self) -> LayerModifierKind { 133 | match self { 134 | LayerModiferConfig::Simple(_) => LayerModifierKind::Momentary, 135 | LayerModiferConfig::Custom { key: _, r#type } => *r#type, 136 | } 137 | } 138 | } 139 | 140 | #[derive(Debug, Default, Clone, Copy, Deserialize)] 141 | #[serde(rename_all = "lowercase")] 142 | pub enum LayerModifierKind { 143 | #[default] 144 | Momentary, 145 | Toggle, 146 | Oneshoot, 147 | } 148 | 149 | #[derive(Debug, Clone, Deserialize)] 150 | #[serde(untagged)] 151 | pub enum KeyAction { 152 | KeyCode(KeyCode), 153 | Macro(Macro), 154 | } 155 | 156 | #[derive(Debug, Clone, Deserialize)] 157 | #[serde(untagged)] 158 | pub enum Macro { 159 | Single(EventMacro), 160 | Sequence(Vec), 161 | } 162 | 163 | #[derive(Debug, Clone, Deserialize)] 164 | #[serde(untagged)] 165 | pub enum EventMacro { 166 | Tap(KeyCode), 167 | Press { press: KeyCode }, 168 | Hold { hold: KeyCode }, 169 | Release { release: KeyCode }, 170 | Delay { delay: u32 }, 171 | String { string: String }, 172 | Env { env: String }, 173 | Unicode { unicode: String }, 174 | Shell { shell: String, trim: Option }, 175 | } 176 | 177 | const SAFE_KEYCODE_START: u16 = 999; 178 | const SHIFTED_KEYCODE_START: u16 = 800; 179 | 180 | #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] 181 | pub struct KeyCode(evdev::KeyCode); 182 | 183 | impl From for KeyCode { 184 | fn from(value: evdev::KeyCode) -> Self { 185 | Self(value) 186 | } 187 | } 188 | 189 | const fn to_shifted_code(code: evdev::KeyCode) -> isize { 190 | (SHIFTED_KEYCODE_START + code.code()) as isize 191 | } 192 | 193 | #[derive(Debug, Clone, Copy, Deserialize)] 194 | #[serde(rename_all = "SCREAMING_SNAKE_CASE")] 195 | #[allow(clippy::enum_variant_names)] 196 | pub enum ShiftedKeycodes { 197 | KeyExclamation = to_shifted_code(evdev::KeyCode::KEY_1), 198 | KeyAt = to_shifted_code(evdev::KeyCode::KEY_2), 199 | KeyHash = to_shifted_code(evdev::KeyCode::KEY_3), 200 | KeyDollarsign = to_shifted_code(evdev::KeyCode::KEY_4), 201 | KeyPercent = to_shifted_code(evdev::KeyCode::KEY_5), 202 | KeyCaret = to_shifted_code(evdev::KeyCode::KEY_6), 203 | KeyAmpersand = to_shifted_code(evdev::KeyCode::KEY_7), 204 | KeyStar = to_shifted_code(evdev::KeyCode::KEY_8), 205 | KeyLeftparen = to_shifted_code(evdev::KeyCode::KEY_9), 206 | KeyRightparen = to_shifted_code(evdev::KeyCode::KEY_0), 207 | KeyUnderscore = to_shifted_code(evdev::KeyCode::KEY_MINUS), 208 | KeyPlus = to_shifted_code(evdev::KeyCode::KEY_EQUAL), 209 | KeyLeftcurly = to_shifted_code(evdev::KeyCode::KEY_LEFTBRACE), 210 | KeyRightcurly = to_shifted_code(evdev::KeyCode::KEY_RIGHTBRACE), 211 | KeyColon = to_shifted_code(evdev::KeyCode::KEY_SEMICOLON), 212 | KeyDoublequote = to_shifted_code(evdev::KeyCode::KEY_APOSTROPHE), 213 | KeyLess = to_shifted_code(evdev::KeyCode::KEY_COMMA), 214 | KeyGreater = to_shifted_code(evdev::KeyCode::KEY_DOT), 215 | KeyQuestion = to_shifted_code(evdev::KeyCode::KEY_SLASH), 216 | KeyTilde = to_shifted_code(evdev::KeyCode::KEY_GRAVE), 217 | KeyPipe = to_shifted_code(evdev::KeyCode::KEY_BACKSLASH), 218 | } 219 | 220 | impl KeyCode { 221 | pub fn new(code: u16) -> Self { 222 | Self(evdev::KeyCode::new(code)) 223 | } 224 | 225 | pub fn value(self) -> u16 { 226 | self.0.code() 227 | } 228 | 229 | pub fn is_custom(self) -> bool { 230 | self.0.code() >= SAFE_KEYCODE_START 231 | } 232 | 233 | pub fn is_shifted(self) -> bool { 234 | self.0.code() > SHIFTED_KEYCODE_START && self.0.code() < SAFE_KEYCODE_START 235 | } 236 | 237 | pub fn unshift(self) -> Self { 238 | KeyCode::new(self.0.code() - SHIFTED_KEYCODE_START) 239 | } 240 | } 241 | 242 | thread_local! { 243 | static CUSTOM_KEYCODES: RefCell> = HashMap::default().into(); 244 | } 245 | 246 | impl<'de> Deserialize<'de> for KeyCode { 247 | fn deserialize(deserializer: D) -> Result 248 | where 249 | D: Deserializer<'de>, 250 | { 251 | let key = String::deserialize(deserializer)?; 252 | let key_deserializer: StringDeserializer = key.clone().into_deserializer(); 253 | 254 | if let Ok(shifted) = ShiftedKeycodes::deserialize(key_deserializer) { 255 | return Ok(KeyCode(evdev::KeyCode(shifted as u16))); 256 | } 257 | 258 | if let Ok(value) = evdev::KeyCode::from_str(&key) { 259 | return Ok(KeyCode(value)); 260 | } 261 | 262 | CUSTOM_KEYCODES.with_borrow_mut(|keycodes| { 263 | let count = keycodes.len() as u16; 264 | let entry = keycodes.entry(key).or_insert(count + SAFE_KEYCODE_START); 265 | Ok(KeyCode(evdev::KeyCode::new(*entry))) 266 | }) 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /src/core/adapter.rs: -------------------------------------------------------------------------------- 1 | use std::{thread, time::Duration}; 2 | 3 | use anyhow::Result; 4 | use evdev::{Device, EventType, InputEvent}; 5 | 6 | use crate::config::schema::{ 7 | DefaultConfig, GeneralConfig, KeyAction, KeyCode, KeyboardConfig, Macro, 8 | }; 9 | 10 | use super::{ 11 | buffer::InputBuffer, 12 | combo::ComboManager, 13 | event::{IntoInputEvent, ToInputResult, HOLD_EVENT, PRESS_EVENT, RELEASE_EVENT}, 14 | layer::LayerManager, 15 | mapping::MappingManager, 16 | proxy::EventProxy, 17 | tap_dance::TapDanceManager, 18 | }; 19 | 20 | #[derive(Debug)] 21 | pub enum InputResult { 22 | Press(KeyCode), 23 | Pending(KeyCode), 24 | Hold(KeyCode), 25 | Release(KeyCode), 26 | Macro(Macro), 27 | DoubleSequence(Box<[InputResult; 2]>), 28 | Raw(Vec), 29 | Delay(u32), 30 | None, 31 | } 32 | 33 | #[derive(Debug)] 34 | pub struct KeyAdapter<'a, P: EventProxy> { 35 | proxy: &'a mut P, 36 | buffer: InputBuffer, 37 | config: GeneralConfig, 38 | mapping_manager: MappingManager, 39 | combo_manager: ComboManager, 40 | tap_dance_manager: TapDanceManager, 41 | layer_manager: LayerManager, 42 | depth: u8, 43 | } 44 | 45 | impl<'a, P: EventProxy> KeyAdapter<'a, P> { 46 | pub fn new(config: KeyboardConfig, defaults: DefaultConfig, proxy: &'a mut P) -> Self { 47 | let mapping_manager = MappingManager::new(config.keys); 48 | let combo_manager = ComboManager::new(config.combos, defaults.combo); 49 | let tap_dance_manager = TapDanceManager::new(config.tap_dances, defaults.tap_dance); 50 | let layer_manager = LayerManager::new(config.layers); 51 | 52 | Self { 53 | proxy, 54 | mapping_manager, 55 | tap_dance_manager, 56 | combo_manager, 57 | layer_manager, 58 | config: defaults.general, 59 | buffer: InputBuffer::default(), 60 | depth: 0, 61 | } 62 | } 63 | 64 | pub fn hook(&mut self, device: &mut Device) -> Result<()> { 65 | device.grab()?; 66 | device.set_nonblocking(true)?; 67 | 68 | loop { 69 | self.proxy.wait(self.config.event_poll_timeout)?; 70 | 71 | if let Ok(events) = device.fetch_events() { 72 | for event in events { 73 | if event.event_type() == EventType::KEY { 74 | self.process_event(event)?; 75 | } 76 | } 77 | } 78 | 79 | self.post_process()?; 80 | } 81 | } 82 | 83 | pub fn process_event(&mut self, event: InputEvent) -> Result<()> { 84 | let action = self.mapping_manager.map(&event.code()); 85 | let action = self.layer_manager.map(action); 86 | 87 | let result = match event.value() { 88 | PRESS_EVENT => self.handle_press(action), 89 | HOLD_EVENT => self.handle_hold(action), 90 | RELEASE_EVENT => self.handle_release(action), 91 | value => unreachable!("Unknown event value: {value}"), 92 | }; 93 | 94 | self.dispatch_result(&result) 95 | } 96 | 97 | pub fn post_process(&mut self) -> Result<()> { 98 | self.tap_dance_manager.process(&mut self.buffer); 99 | self.combo_manager.process(&mut self.buffer); 100 | 101 | while let Some(result) = self.buffer.pop_result() { 102 | self.dispatch_result(&result)?; 103 | } 104 | 105 | Ok(()) 106 | } 107 | 108 | fn dispatch_result(&mut self, result: &InputResult) -> Result<()> { 109 | if self.depth > self.config.maximum_lookup_depth { 110 | log::warn!("Maximum keycode lookup depth exceeded"); 111 | return Ok(()); 112 | } 113 | 114 | self.depth += 1; 115 | 116 | match result { 117 | InputResult::Pending(code) => { 118 | self.buffer.set_pending_key(*code); 119 | } 120 | 121 | InputResult::Release(code) if self.buffer.is_pending_key(code) => { 122 | self.dispatch_pending_key(code, result)?; 123 | } 124 | 125 | InputResult::Press(code) 126 | if !self.buffer.is_pending_key(code) && self.buffer.has_pending_keys() => 127 | { 128 | self.buffer.defer_key(*code); 129 | } 130 | 131 | InputResult::Press(code) | InputResult::Hold(code) | InputResult::Release(code) => { 132 | self.dispatch_event_result(result, *code)?; 133 | } 134 | 135 | InputResult::DoubleSequence(results) => { 136 | let [first, second] = results.as_ref(); 137 | self.dispatch_result(first)?; 138 | self.dispatch_result(second)?; 139 | } 140 | 141 | InputResult::Macro(value) => { 142 | self.dispatch_event_macro(value)?; 143 | } 144 | 145 | InputResult::Raw(results) => { 146 | self.proxy.emit(results)?; 147 | } 148 | 149 | InputResult::Delay(timeout) => { 150 | thread::sleep(Duration::from_millis(*timeout as u64)); 151 | } 152 | 153 | InputResult::None => {} 154 | } 155 | 156 | self.depth -= 1; 157 | 158 | Ok(()) 159 | } 160 | 161 | fn dispatch_event_result(&mut self, result: &InputResult, code: KeyCode) -> Result<()> { 162 | let (event_kind, handler): (_, fn(&mut Self, KeyAction) -> InputResult) = match result { 163 | InputResult::Press(_) => (PRESS_EVENT, Self::handle_press), 164 | InputResult::Hold(_) => (HOLD_EVENT, Self::handle_hold), 165 | InputResult::Release(_) => (RELEASE_EVENT, Self::handle_release), 166 | value => unreachable!("Unexpected input result: {value:?}"), 167 | }; 168 | 169 | let action = self.mapping_manager.map(&code.value()); 170 | let action = self.layer_manager.map(action); 171 | 172 | match action { 173 | KeyAction::KeyCode(code) if code.is_shifted() => { 174 | self.dispatch_shifted_key(code, event_kind) 175 | } 176 | KeyAction::KeyCode(code) if !code.is_custom() => { 177 | self.proxy.emit(&[code.to_event(event_kind)]) 178 | } 179 | _ => { 180 | let result = handler(self, action); 181 | self.dispatch_result(&result) 182 | } 183 | } 184 | } 185 | 186 | fn dispatch_shifted_key(&mut self, code: KeyCode, event_kind: i32) -> Result<()> { 187 | match event_kind { 188 | PRESS_EVENT => self.proxy.emit(&[ 189 | KeyCode::from(evdev::KeyCode::KEY_LEFTSHIFT).to_event(PRESS_EVENT), 190 | KeyCode::from(evdev::KeyCode::KEY_LEFTSHIFT).to_event(HOLD_EVENT), 191 | code.unshift().to_event(PRESS_EVENT), 192 | ]), 193 | HOLD_EVENT => self.proxy.emit(&[code.unshift().to_event(HOLD_EVENT)]), 194 | RELEASE_EVENT => self.proxy.emit(&[ 195 | KeyCode::from(evdev::KeyCode::KEY_LEFTSHIFT).to_event(RELEASE_EVENT), 196 | code.unshift().to_event(RELEASE_EVENT), 197 | ]), 198 | value => unreachable!("Unknown event kind: {value}"), 199 | } 200 | } 201 | 202 | fn dispatch_pending_key(&mut self, code: &KeyCode, result: &InputResult) -> Result<()> { 203 | self.buffer.clear_pending_key(code); 204 | self.dispatch_event_result(result, *code)?; 205 | 206 | if !self.buffer.has_pending_keys() { 207 | while let Some(key) = self.buffer.pop_deferred_key() { 208 | if self.config.deferred_key_delay > 0 { 209 | self.proxy.wait(self.config.deferred_key_delay)?; // add a small delay to make input smoother 210 | } 211 | 212 | let result = InputResult::DoubleSequence(Box::new([ 213 | InputResult::Press(key), 214 | InputResult::Release(key), 215 | ])); 216 | 217 | self.dispatch_result(&result)?; 218 | } 219 | } 220 | 221 | Ok(()) 222 | } 223 | 224 | fn dispatch_event_macro(&mut self, value: &Macro) -> Result<()> { 225 | let delay = self.config.unicode_input_delay; 226 | 227 | let events = match value { 228 | Macro::Sequence(value) => value 229 | .iter() 230 | .map(|m| m.to_results(delay)) 231 | .collect::>>()? 232 | .into_iter() 233 | .flatten() 234 | .collect(), 235 | Macro::Single(event) => event.to_results(delay)?, 236 | }; 237 | 238 | for event in events { 239 | self.dispatch_result(&event)?; 240 | } 241 | 242 | Ok(()) 243 | } 244 | 245 | fn handle_press(&mut self, action: KeyAction) -> InputResult { 246 | match action { 247 | KeyAction::KeyCode(code) => { 248 | let value = code.value(); 249 | 250 | self.tap_dance_manager 251 | .handle_press(value) 252 | .or_else(|| self.combo_manager.handle_press(value)) 253 | .or_else(|| self.layer_manager.handle_press(value)) 254 | .unwrap_or(InputResult::Press(code)) 255 | } 256 | KeyAction::Macro(codes) => InputResult::Macro(codes), 257 | } 258 | } 259 | 260 | fn handle_hold(&mut self, action: KeyAction) -> InputResult { 261 | match action { 262 | KeyAction::KeyCode(code) => { 263 | let value = code.value(); 264 | 265 | self.tap_dance_manager 266 | .handle_hold(value) 267 | .or_else(|| self.combo_manager.handle_hold(value)) 268 | .or_else(|| self.layer_manager.handle_hold(value)) 269 | .unwrap_or(InputResult::Hold(code)) 270 | } 271 | KeyAction::Macro(_) => InputResult::None, 272 | } 273 | } 274 | 275 | fn handle_release(&mut self, action: KeyAction) -> InputResult { 276 | match action { 277 | KeyAction::KeyCode(code) => { 278 | let value = code.value(); 279 | 280 | self.tap_dance_manager 281 | .handle_release(value) 282 | .or_else(|| self.combo_manager.handle_release(value)) 283 | .or_else(|| self.layer_manager.handle_release(value)) 284 | .unwrap_or(InputResult::Release(code)) 285 | } 286 | _ => InputResult::None, 287 | } 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | okey
4 | 5 | [![crates.io](https://img.shields.io/crates/v/okey-cli?style=for-the-badge)](https://crates.io/crates/okey-cli) 6 | ![Build](https://img.shields.io/github/actions/workflow/status/luckasranarison/okey/ci.yml?style=for-the-badge&label=Build&labelColor=3b434b&color=30c352) 7 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=for-the-badge&labelColor=3b434b&color=blue)](https://github.com/luckasRanarison/luckasranarison.github.io/blob/master/LICENSE) 8 | ![Stars](https://img.shields.io/github/stars/luckasranarison/okey?style=for-the-badge&label=Stars&labelColor=3b434b&color=yellow) 9 | 10 |
11 | 12 | # okey 13 | 14 | An advanced, easy-to-use key remapper for Linux written in Rust, inspired by [QMK](https://qmk.fm/). 15 | 16 | > [!NOTE] 17 | > This project is currently in its early stages of development. More comprehensive documentation will be available soon. 18 | 19 | ## Contents 20 | 21 | 1. [Features](#features) 22 | 2. [Installation](#installation) 23 | 3. [Usage](#usage) 24 | 4. [Configuration Schema](#configuration-schema) 25 | - [Defaults](#defaults-optional) 26 | - [Keyboards](#keyboards-array) 27 | 5. [License](#license) 28 | 29 | ## Features 30 | 31 | > [!TIP] 32 | > Click on the feature to expand the example configuration. 33 | 34 |
35 | 36 | Easy-to-use: designed to be used as a systemd service, configured using simple YAML with IDE support (see schema).
37 | 38 | ```yaml 39 | # yaml-language-server: $schema=https://raw.githubusercontent.com/luckasRanarison/okey/refs/heads/master/schema/okey.json 40 | 41 | keyboards: 42 | - name: "My keyboard" 43 | 44 | keys: 45 | KEY_X: KEY_Y 46 | 47 | combos: 48 | - keys: [KEY_D, KEY_F] 49 | action: KEY_LEFTCTRL 50 | 51 | tap_dances: 52 | KEY_CAPSLOCK: 53 | tap: KEY_TAB 54 | hold: KEY_MOMLAYER 55 | 56 | layers: 57 | momentary: 58 | modifier: KEY_MOMLAYER 59 | 60 | keys: 61 | KEY_O: KEY_K 62 | ``` 63 | 64 |
65 | 66 |
67 | 68 | Key remapping: change the default global key actions.
69 | 70 | ```yaml 71 | keyboards: 72 | - name: My keyboard 73 | 74 | keys: 75 | KEY_CAPSLOCK: KEY_TAB 76 | KEY_TAB: CUSTOM_KEYCODE # can be used to activate a layer or to trigger other actions 77 | ``` 78 | 79 |
80 | 81 |
82 | 83 | Macros: execute arbitrary key sequences with a single key stroke.
84 | 85 | ```yaml 86 | keyboards: 87 | - name: My keyboard 88 | 89 | keys: 90 | KEY_F1: [KEY_H, KEY_E, KEY_L, KEY_L, KEY_O] # executes simple key sequences (press + release) 91 | KEY_F2: { string: "Hi, you!" } # inserts an ASCII string 92 | KEY_F3: { env: FOO } # inserts the value of the environment variable 93 | KEY_F4: { unicode: 🙂👍 } # inserts unicode characters using CTRL + SHIFT + U + + ENTER 94 | KEY_F5: { shell: "echo 'foo'", trim: true } # inserts shell script output 95 | 96 | KEY_F6: [ 97 | { press: KEY_O }, 98 | { hold: KEY_O }, 99 | { delay: 1000 }, 100 | { release: KEY_O }, 101 | KEY_K, # press + release 102 | ] # executes detailed key sequences 103 | 104 | KEY_F7: [{ env: USERNAME }, { string: "@gmail.com" }] # all types of macro are composable 105 | ``` 106 | 107 |
108 | 109 |
110 | 111 | Combos: trigger an action when two or more keys are pressed simultaneously.
112 | 113 | ```yaml 114 | keyboards: 115 | - name: My keyboard 116 | 117 | combos: 118 | - keys: [KEY_D, KEY_F] 119 | action: LEFT_CTRL 120 | ``` 121 | 122 |
123 | 124 |
125 | 126 | Tap dance: overload keys by binding different actions to a key whether it is tapped or held.
127 | 128 | ```yaml 129 | keyboards: 130 | - name: My keyboard 131 | 132 | tap_dances: 133 | tap: KEY_S 134 | hold: KEY_LEFTSHIFT 135 | timeout: 250 # (default: 200ms) 136 | ``` 137 | 138 |
139 | 140 |
141 | 142 | Virtual layers: create custom layers, similar to holding Shift. It supports momentary, toggle and oneshoot layers.
143 | 144 | ```yaml 145 | keyboards: 146 | - name: "My keyboard" 147 | 148 | keys: 149 | KEY_TAB: KEY_ONELAYER # a custom keycode to activate the layer below 150 | 151 | tap_dances: 152 | KEY_CAPSLOCK: 153 | tap: KEY_TAB 154 | hold: KEY_MOMLAYER 155 | 156 | layers: 157 | momentary: 158 | modifier: KEY_MOMLAYER # type is momentary by default, active on hold 159 | 160 | keys: 161 | KEY_X: KEY_Y 162 | 163 | one: 164 | modifier: 165 | key: KEY_ONELAYER 166 | type: oneshoot # active for one keypress 167 | 168 | keys: 169 | KEY_O: KEY_K 170 | 171 | toggle: 172 | modifier: 173 | key: KEY_F12 174 | type: toggle # active until switched off 175 | 176 | keys: 177 | KEY_K: KEY_O 178 | ``` 179 | 180 |
181 | 182 | > [!NOTE] 183 | > The features are composable. For example, you can use a combo to trigger a tap dance. 184 | 185 | ## Installation 186 | 187 | > [!IMPORTANT] 188 | > The [Rust toolchain](https://rustup.rs/) is required to build the project. 189 | 190 | There are currently no packaged binaries for specific distros, but you can download `okey` from the [releases](https://github.com/luckasRanarison/okey/releases) or install it with [cargo](https://doc.rust-lang.org/cargo/): 191 | 192 | ```bash 193 | cargo install okey-cli 194 | ``` 195 | 196 | You can also build the project from source using the following commands: 197 | 198 | ```bash 199 | git clone --depth 1 https://github.com/luckasRanarison/okey/ 200 | cd okey && cargo install --path . 201 | ``` 202 | 203 | ## Usage 204 | 205 | `okey` is designed to be used as a [systemd](https://github.com/systemd/systemd) service. It expects a configuration file at `~/.config/okey/config.yaml` when installed at the user level, or at `/etc/okey/config.yaml` when installed with root privileges (see the [schema](#configuration-schema)). 206 | 207 | For simple testing, you can use the `start` command to activate keymaps. 208 | 209 | ```bash 210 | okey start # using ~/.config/okey/config.yaml 211 | okey start --config ./path/to/config/okey.yaml 212 | okey start --config ./path/to/config/okey.yaml --daemon # to run as a daemon in the background 213 | ``` 214 | 215 | To use `okey` as systemd a service at the user level, you can use the following commands: 216 | 217 |
218 | 219 | okey.service (expand)
220 | 221 | ```ini 222 | [Unit] 223 | Description=Okey Service 224 | 225 | [Service] 226 | ExecStart=/usr/bin/okey start --systemd 227 | Restart=on-failure 228 | StandardOutput=journal 229 | StandardError=journal 230 | Nice=-20 231 | 232 | [Install] 233 | WantedBy=multi-user.target 234 | ``` 235 | 236 |
237 | 238 | ```bash 239 | okey service install # creates the okey.service file at ~/.config/systemd/ 240 | okey service start # shorthand for systemctl --user enable okey && systemctl --user start okey 241 | 242 | okey service stop # shorthand for systemctl --user stop okey && systemctl --user disable okey 243 | okey service restart # shorthand for systemctl --user restart okey 244 | okey service status # shorthand for systemctl --user status okey 245 | okey service uninstall # disables the service and remove the okey.service file 246 | ``` 247 | 248 | But to get access to higher priority settings and capabilities, it is recommended to install `okey` at the root level. 249 | 250 |
251 | 252 | okey.service (expand)
253 | 254 | ```ini 255 | [Unit] 256 | Description=Okey Service 257 | 258 | [Service] 259 | ExecStart=/usr/bin/okey start --systemd 260 | Restart=on-failure 261 | StandardOutput=journal 262 | StandardError=journal 263 | Nice=-20 264 | CPUSchedulingPolicy=rr 265 | CPUSchedulingPriority=99 266 | IOSchedulingClass=realtime 267 | IOSchedulingPriority=0 268 | 269 | [Install] 270 | WantedBy=multi-user.target 271 | ``` 272 | 273 |
274 | 275 | ```bash 276 | sudo okey service install # creates the okey.service file at /etc/systemd/system/ 277 | sudo okey service start # shorthand for systemctl enable okey && systemctl start okey 278 | ``` 279 | 280 | Use `okey --help` to see all the available commands. 281 | 282 | ## Configuration Schema 283 | 284 | The configuration for okey is written in [YAML](https://yaml.org/), it defines how `okey` remaps keys and sets up advanced behaviors. Check out the [examples](./examples/) folder for practical use cases. 285 | 286 | > [!TIP] 287 | > If you are using [yaml-language-server](https://github.com/redhat-developer/yaml-language-server), you can get autocompletion and IDE support by adding the following at the top of your file: 288 | > ```yaml 289 | > # yaml-language-server: $schema=https://raw.githubusercontent.com/luckasRanarison/okey/refs/heads/master/schema/okey.json 290 | > ``` 291 | 292 | Here's a breakdown of the schema: 293 | 294 | ### `defaults` (optional) 295 | 296 | Shared global settings, fields: 297 | 298 | #### `general` 299 | 300 | - `deferred_key_delay`: Delay for keys following non-acknowledged special keys. 301 | 302 | _Type_: `number` 303 | 304 | _Default_: `0` (ms) 305 | 306 | - `unicode_input_delay`: Delay for inserting unicode code points with macro. (flushing) 307 | 308 | _Type_: `number` 309 | 310 | _Default_: `50` (ms) 311 | 312 | - `event_poll_timeout`: Controls the main event loop interval. 313 | 314 | _Type_: `number` 315 | 316 | _Default_: `1` (ms) 317 | 318 | #### `tap_dance` 319 | 320 | - `default_timeout`: Fallback tap dance timeout. 321 | 322 | _Type_: `number` 323 | 324 | _Default_: `200` (ms) 325 | 326 | #### `combo` 327 | 328 | - `default_threshold`: Window for acknowledging combos. 329 | 330 | _Type_: `number` 331 | 332 | _Default_: `10` (ms) 333 | 334 | ### `keyboards` (array) 335 | 336 | Per keyboard configuration. 337 | 338 | Type aliases: 339 | 340 |
341 | 342 | KeyCode (expand) 343 | 344 | #### `KeyCode` 345 | 346 |
347 | 348 | Shifted keycodes (expand) 349 | 350 | - `KEY_EXCLAMATION` 351 | - `KEY_AT` 352 | - `KEY_HASH` 353 | - `KEY_DOLLARSIGN` 354 | - `KEY_PERCENT` 355 | - `KEY_CARET` 356 | - `KEY_AMPERSAND` 357 | - `KEY_STAR` 358 | - `KEY_LEFTPAREN` 359 | - `KEY_RIGHTPAREN` 360 | - `KEY_UNDERSCORE` 361 | - `KEY_PLUS` 362 | - `KEY_LEFTCURLY` 363 | - `KEY_RIGHTCURLY` 364 | - `KEY_COLON` 365 | - `KEY_DOUBLEQUOTE` 366 | - `KEY_LESS` 367 | - `KEY_GREATER` 368 | - `KEY_QUESTION` 369 | - `KEY_TILDE` 370 | - `KEY_PIPE` 371 | 372 |
373 | 374 | A custom string or one of: 375 | 376 | - `KEY_RESERVED` 377 | - `KEY_ESC` 378 | - `KEY_1` 379 | - `KEY_2` 380 | - `KEY_3` 381 | - `KEY_4` 382 | - `KEY_5` 383 | - `KEY_6` 384 | - `KEY_7` 385 | - `KEY_8` 386 | - `KEY_9` 387 | - `KEY_0` 388 | - `KEY_MINUS` 389 | - `KEY_EQUAL` 390 | - `KEY_BACKSPACE` 391 | - `KEY_TAB` 392 | - `KEY_Q` 393 | - `KEY_W` 394 | - `KEY_E` 395 | - `KEY_R` 396 | - `KEY_T` 397 | - `KEY_Y` 398 | - `KEY_U` 399 | - `KEY_I` 400 | - `KEY_O` 401 | - `KEY_P` 402 | - `KEY_LEFTBRACE` 403 | - `KEY_RIGHTBRACE` 404 | - `KEY_ENTER` 405 | - `KEY_LEFTCTRL` 406 | - `KEY_A` 407 | - `KEY_S` 408 | - `KEY_D` 409 | - `KEY_F` 410 | - `KEY_G` 411 | - `KEY_H` 412 | - `KEY_J` 413 | - `KEY_K` 414 | - `KEY_L` 415 | - `KEY_SEMICOLON` 416 | - `KEY_APOSTROPHE` 417 | - `KEY_GRAVE` 418 | - `KEY_LEFTSHIFT` 419 | - `KEY_BACKSLASH` 420 | - `KEY_Z` 421 | - `KEY_X` 422 | - `KEY_C` 423 | - `KEY_V` 424 | - `KEY_B` 425 | - `KEY_N` 426 | - `KEY_M` 427 | - `KEY_COMMA` 428 | - `KEY_DOT` 429 | - `KEY_SLASH` 430 | - `KEY_RIGHTSHIFT` 431 | - `KEY_KPASTERISK` 432 | - `KEY_LEFTALT` 433 | - `KEY_SPACE` 434 | - `KEY_CAPSLOCK` 435 | - `KEY_F1` 436 | - `KEY_F2` 437 | - `KEY_F3` 438 | - `KEY_F4` 439 | - `KEY_F5` 440 | - `KEY_F6` 441 | - `KEY_F7` 442 | - `KEY_F8` 443 | - `KEY_F9` 444 | - `KEY_F10` 445 | - `KEY_NUMLOCK` 446 | - `KEY_SCROLLLOCK` 447 | - `KEY_KP7` 448 | - `KEY_KP8` 449 | - `KEY_KP9` 450 | - `KEY_KPMINUS` 451 | - `KEY_KP4` 452 | - `KEY_KP5` 453 | - `KEY_KP6` 454 | - `KEY_KPPLUS` 455 | - `KEY_KP1` 456 | - `KEY_KP2` 457 | - `KEY_KP3` 458 | - `KEY_KP0` 459 | - `KEY_KPDOT` 460 | - `KEY_ZENKAKUHANKAKU` 461 | - `KEY_102ND` 462 | - `KEY_F11` 463 | - `KEY_F12` 464 | - `KEY_RO` 465 | - `KEY_KATAKANA` 466 | - `KEY_HIRAGANA` 467 | - `KEY_HENKAN` 468 | - `KEY_KATAKANAHIRAGANA` 469 | - `KEY_MUHENKAN` 470 | - `KEY_KPJPCOMMA` 471 | - `KEY_KPENTER` 472 | - `KEY_RIGHTCTRL` 473 | - `KEY_KPSLASH` 474 | - `KEY_SYSRQ` 475 | - `KEY_RIGHTALT` 476 | - `KEY_LINEFEED` 477 | - `KEY_HOME` 478 | - `KEY_UP` 479 | - `KEY_PAGEUP` 480 | - `KEY_LEFT` 481 | - `KEY_RIGHT` 482 | - `KEY_END` 483 | - `KEY_DOWN` 484 | - `KEY_PAGEDOWN` 485 | - `KEY_INSERT` 486 | - `KEY_DELETE` 487 | - `KEY_MACRO` 488 | - `KEY_MUTE` 489 | - `KEY_VOLUMEDOWN` 490 | - `KEY_VOLUMEUP` 491 | - `KEY_POWER` 492 | - `KEY_KPEQUAL` 493 | - `KEY_KPPLUSMINUS` 494 | - `KEY_PAUSE` 495 | - `KEY_SCALE` 496 | - `KEY_KPCOMMA` 497 | - `KEY_HANGEUL` 498 | - `KEY_HANJA` 499 | - `KEY_YEN` 500 | - `KEY_LEFTMETA` 501 | - `KEY_RIGHTMETA` 502 | - `KEY_COMPOSE` 503 | - `KEY_STOP` 504 | - `KEY_AGAIN` 505 | - `KEY_PROPS` 506 | - `KEY_UNDO` 507 | - `KEY_FRONT` 508 | - `KEY_COPY` 509 | - `KEY_OPEN` 510 | - `KEY_PASTE` 511 | - `KEY_FIND` 512 | - `KEY_CUT` 513 | - `KEY_HELP` 514 | - `KEY_MENU` 515 | - `KEY_CALC` 516 | - `KEY_SETUP` 517 | - `KEY_SLEEP` 518 | - `KEY_WAKEUP` 519 | - `KEY_FILE` 520 | - `KEY_SENDFILE` 521 | - `KEY_DELETEFILE` 522 | - `KEY_XFER` 523 | - `KEY_PROG1` 524 | - `KEY_PROG2` 525 | - `KEY_WWW` 526 | - `KEY_MSDOS` 527 | - `KEY_COFFEE` 528 | - `KEY_DIRECTION` 529 | - `KEY_ROTATE_DISPLAY` 530 | - `KEY_CYCLEWINDOWS` 531 | - `KEY_MAIL` 532 | - `KEY_BOOKMARKS` 533 | - `KEY_COMPUTER` 534 | - `KEY_BACK` 535 | - `KEY_FORWARD` 536 | - `KEY_CLOSECD` 537 | - `KEY_EJECTCD` 538 | - `KEY_EJECTCLOSECD` 539 | - `KEY_NEXTSONG` 540 | - `KEY_PLAYPAUSE` 541 | - `KEY_PREVIOUSSONG` 542 | - `KEY_STOPCD` 543 | - `KEY_RECORD` 544 | - `KEY_REWIND` 545 | - `KEY_PHONE` 546 | - `KEY_ISO` 547 | - `KEY_CONFIG` 548 | - `KEY_HOMEPAGE` 549 | - `KEY_REFRESH` 550 | - `KEY_EXIT` 551 | - `KEY_MOVE` 552 | - `KEY_EDIT` 553 | - `KEY_SCROLLUP` 554 | - `KEY_SCROLLDOWN` 555 | - `KEY_KPLEFTPAREN` 556 | - `KEY_KPRIGHTPAREN` 557 | - `KEY_NEW` 558 | - `KEY_REDO` 559 | - `KEY_F13` 560 | - `KEY_F14` 561 | - `KEY_F15` 562 | - `KEY_F16` 563 | - `KEY_F17` 564 | - `KEY_F18` 565 | - `KEY_F19` 566 | - `KEY_F20` 567 | - `KEY_F21` 568 | - `KEY_F22` 569 | - `KEY_F23` 570 | - `KEY_F24` 571 | - `KEY_PLAYCD` 572 | - `KEY_PAUSECD` 573 | - `KEY_PROG3` 574 | - `KEY_PROG4` 575 | - `KEY_DASHBOARD` 576 | - `KEY_SUSPEND` 577 | - `KEY_CLOSE` 578 | - `KEY_PLAY` 579 | - `KEY_FASTFORWARD` 580 | - `KEY_BASSBOOST` 581 | - `KEY_PRINT` 582 | - `KEY_HP` 583 | - `KEY_CAMERA` 584 | - `KEY_SOUND` 585 | - `KEY_QUESTION` 586 | - `KEY_EMAIL` 587 | - `KEY_CHAT` 588 | - `KEY_SEARCH` 589 | - `KEY_CONNECT` 590 | - `KEY_FINANCE` 591 | - `KEY_SPORT` 592 | - `KEY_SHOP` 593 | - `KEY_ALTERASE` 594 | - `KEY_CANCEL` 595 | - `KEY_BRIGHTNESSDOWN` 596 | - `KEY_BRIGHTNESSUP` 597 | - `KEY_MEDIA` 598 | - `KEY_SWITCHVIDEOMODE` 599 | - `KEY_KBDILLUMTOGGLE` 600 | - `KEY_KBDILLUMDOWN` 601 | - `KEY_KBDILLUMUP` 602 | - `KEY_SEND` 603 | - `KEY_REPLY` 604 | - `KEY_FORWARDMAIL` 605 | - `KEY_SAVE` 606 | - `KEY_DOCUMENTS` 607 | - `KEY_BATTERY` 608 | - `KEY_BLUETOOTH` 609 | - `KEY_WLAN` 610 | - `KEY_UWB` 611 | - `KEY_UNKNOWN` 612 | - `KEY_VIDEO_NEXT` 613 | - `KEY_VIDEO_PREV` 614 | - `KEY_BRIGHTNESS_CYCLE` 615 | - `KEY_BRIGHTNESS_AUTO` 616 | - `KEY_DISPLAY_OFF` 617 | - `KEY_WWAN` 618 | - `KEY_RFKILL` 619 | - `KEY_MICMUTE` 620 | - `BTN_0` 621 | - `BTN_1` 622 | - `BTN_2` 623 | - `BTN_3` 624 | - `BTN_4` 625 | - `BTN_5` 626 | - `BTN_6` 627 | - `BTN_7` 628 | - `BTN_8` 629 | - `BTN_9` 630 | - `BTN_LEFT` 631 | - `BTN_RIGHT` 632 | - `BTN_MIDDLE` 633 | - `BTN_SIDE` 634 | - `BTN_EXTRA` 635 | - `BTN_FORWARD` 636 | - `BTN_BACK` 637 | - `BTN_TASK` 638 | - `BTN_TRIGGER` 639 | - `BTN_THUMB` 640 | - `BTN_THUMB2` 641 | - `BTN_TOP` 642 | - `BTN_TOP2` 643 | - `BTN_PINKIE` 644 | - `BTN_BASE` 645 | - `BTN_BASE2` 646 | - `BTN_BASE3` 647 | - `BTN_BASE4` 648 | - `BTN_BASE5` 649 | - `BTN_BASE6` 650 | - `BTN_DEAD` 651 | - `BTN_SOUTH` 652 | - `BTN_EAST` 653 | - `BTN_C` 654 | - `BTN_NORTH` 655 | - `BTN_WEST` 656 | - `BTN_Z` 657 | - `BTN_TL` 658 | - `BTN_TR` 659 | - `BTN_TL2` 660 | - `BTN_TR2` 661 | - `BTN_SELECT` 662 | - `BTN_START` 663 | - `BTN_MODE` 664 | - `BTN_THUMBL` 665 | - `BTN_THUMBR` 666 | - `BTN_TOOL_PEN` 667 | - `BTN_TOOL_RUBBER` 668 | - `BTN_TOOL_BRUSH` 669 | - `BTN_TOOL_PENCIL` 670 | - `BTN_TOOL_AIRBRUSH` 671 | - `BTN_TOOL_FINGER` 672 | - `BTN_TOOL_MOUSE` 673 | - `BTN_TOOL_LENS` 674 | - `BTN_TOOL_QUINTTAP` 675 | - `BTN_TOUCH` 676 | - `BTN_STYLUS` 677 | - `BTN_STYLUS2` 678 | - `BTN_TOOL_DOUBLETAP` 679 | - `BTN_TOOL_TRIPLETAP` 680 | - `BTN_TOOL_QUADTAP` 681 | - `BTN_GEAR_DOWN` 682 | - `BTN_GEAR_UP` 683 | - `KEY_OK` 684 | - `KEY_SELECT` 685 | - `KEY_GOTO` 686 | - `KEY_CLEAR` 687 | - `KEY_POWER2` 688 | - `KEY_OPTION` 689 | - `KEY_INFO` 690 | - `KEY_TIME` 691 | - `KEY_VENDOR` 692 | - `KEY_ARCHIVE` 693 | - `KEY_PROGRAM` 694 | - `KEY_CHANNEL` 695 | - `KEY_FAVORITES` 696 | - `KEY_EPG` 697 | - `KEY_PVR` 698 | - `KEY_MHP` 699 | - `KEY_LANGUAGE` 700 | - `KEY_TITLE` 701 | - `KEY_SUBTITLE` 702 | - `KEY_ANGLE` 703 | - `KEY_ZOOM` 704 | - `KEY_FULL_SCREEN` 705 | - `KEY_MODE` 706 | - `KEY_KEYBOARD` 707 | - `KEY_SCREEN` 708 | - `KEY_PC` 709 | - `KEY_TV` 710 | - `KEY_TV2` 711 | - `KEY_VCR` 712 | - `KEY_VCR2` 713 | - `KEY_SAT` 714 | - `KEY_SAT2` 715 | - `KEY_CD` 716 | - `KEY_TAPE` 717 | - `KEY_RADIO` 718 | - `KEY_TUNER` 719 | - `KEY_PLAYER` 720 | - `KEY_TEXT` 721 | - `KEY_DVD` 722 | - `KEY_AUX` 723 | - `KEY_MP3` 724 | - `KEY_AUDIO` 725 | - `KEY_VIDEO` 726 | - `KEY_DIRECTORY` 727 | - `KEY_LIST` 728 | - `KEY_MEMO` 729 | - `KEY_CALENDAR` 730 | - `KEY_RED` 731 | - `KEY_GREEN` 732 | - `KEY_YELLOW` 733 | - `KEY_BLUE` 734 | - `KEY_CHANNELUP` 735 | - `KEY_CHANNELDOWN` 736 | - `KEY_FIRST` 737 | - `KEY_LAST` 738 | - `KEY_AB` 739 | - `KEY_NEXT` 740 | - `KEY_RESTART` 741 | - `KEY_SLOW` 742 | - `KEY_SHUFFLE` 743 | - `KEY_BREAK` 744 | - `KEY_PREVIOUS` 745 | - `KEY_DIGITS` 746 | - `KEY_TEEN` 747 | - `KEY_TWEN` 748 | - `KEY_VIDEOPHONE` 749 | - `KEY_GAMES` 750 | - `KEY_ZOOMIN` 751 | - `KEY_ZOOMOUT` 752 | - `KEY_ZOOMRESET` 753 | - `KEY_WORDPROCESSOR` 754 | - `KEY_EDITOR` 755 | - `KEY_SPREADSHEET` 756 | - `KEY_GRAPHICSEDITOR` 757 | - `KEY_PRESENTATION` 758 | - `KEY_DATABASE` 759 | - `KEY_NEWS` 760 | - `KEY_VOICEMAIL` 761 | - `KEY_ADDRESSBOOK` 762 | - `KEY_MESSENGER` 763 | - `KEY_DISPLAYTOGGLE` 764 | - `KEY_SPELLCHECK` 765 | - `KEY_LOGOFF` 766 | - `KEY_DOLLAR` 767 | - `KEY_EURO` 768 | - `KEY_FRAMEBACK` 769 | - `KEY_FRAMEFORWARD` 770 | - `KEY_CONTEXT_MENU` 771 | - `KEY_MEDIA_REPEAT` 772 | - `KEY_10CHANNELSUP` 773 | - `KEY_10CHANNELSDOWN` 774 | - `KEY_IMAGES` 775 | - `KEY_PICKUP_PHONE` 776 | - `KEY_HANGUP_PHONE` 777 | - `KEY_DEL_EOL` 778 | - `KEY_DEL_EOS` 779 | - `KEY_INS_LINE` 780 | - `KEY_DEL_LINE` 781 | - `KEY_FN` 782 | - `KEY_FN_ESC` 783 | - `KEY_FN_F1` 784 | - `KEY_FN_F2` 785 | - `KEY_FN_F3` 786 | - `KEY_FN_F4` 787 | - `KEY_FN_F5` 788 | - `KEY_FN_F6` 789 | - `KEY_FN_F7` 790 | - `KEY_FN_F8` 791 | - `KEY_FN_F9` 792 | - `KEY_FN_F10` 793 | - `KEY_FN_F11` 794 | - `KEY_FN_F12` 795 | - `KEY_FN_1` 796 | - `KEY_FN_2` 797 | - `KEY_FN_D` 798 | - `KEY_FN_E` 799 | - `KEY_FN_F` 800 | - `KEY_FN_S` 801 | - `KEY_FN_B` 802 | - `KEY_BRL_DOT1` 803 | - `KEY_BRL_DOT2` 804 | - `KEY_BRL_DOT3` 805 | - `KEY_BRL_DOT4` 806 | - `KEY_BRL_DOT5` 807 | - `KEY_BRL_DOT6` 808 | - `KEY_BRL_DOT7` 809 | - `KEY_BRL_DOT8` 810 | - `KEY_BRL_DOT9` 811 | - `KEY_BRL_DOT10` 812 | - `KEY_NUMERIC_0` 813 | - `KEY_NUMERIC_1` 814 | - `KEY_NUMERIC_2` 815 | - `KEY_NUMERIC_3` 816 | - `KEY_NUMERIC_4` 817 | - `KEY_NUMERIC_5` 818 | - `KEY_NUMERIC_6` 819 | - `KEY_NUMERIC_7` 820 | - `KEY_NUMERIC_8` 821 | - `KEY_NUMERIC_9` 822 | - `KEY_NUMERIC_STAR` 823 | - `KEY_NUMERIC_POUND` 824 | - `KEY_NUMERIC_A` 825 | - `KEY_NUMERIC_B` 826 | - `KEY_NUMERIC_C` 827 | - `KEY_NUMERIC_D` 828 | - `KEY_CAMERA_FOCUS` 829 | - `KEY_WPS_BUTTON` 830 | - `KEY_TOUCHPAD_TOGGLE` 831 | - `KEY_TOUCHPAD_ON` 832 | - `KEY_TOUCHPAD_OFF` 833 | - `KEY_CAMERA_ZOOMIN` 834 | - `KEY_CAMERA_ZOOMOUT` 835 | - `KEY_CAMERA_UP` 836 | - `KEY_CAMERA_DOWN` 837 | - `KEY_CAMERA_LEFT` 838 | - `KEY_CAMERA_RIGHT` 839 | - `KEY_ATTENDANT_ON` 840 | - `KEY_ATTENDANT_OFF` 841 | - `KEY_ATTENDANT_TOGGLE` 842 | - `KEY_LIGHTS_TOGGLE` 843 | - `BTN_DPAD_UP` 844 | - `BTN_DPAD_DOWN` 845 | - `BTN_DPAD_LEFT` 846 | - `BTN_DPAD_RIGHT` 847 | - `KEY_ALS_TOGGLE` 848 | - `KEY_BUTTONCONFIG` 849 | - `KEY_TASKMANAGER` 850 | - `KEY_JOURNAL` 851 | - `KEY_CONTROLPANEL` 852 | - `KEY_APPSELECT` 853 | - `KEY_SCREENSAVER` 854 | - `KEY_VOICECOMMAND` 855 | - `KEY_ASSISTANT` 856 | - `KEY_KBD_LAYOUT_NEXT` 857 | - `KEY_BRIGHTNESS_MIN` 858 | - `KEY_BRIGHTNESS_MAX` 859 | - `KEY_KBDINPUTASSIST_PREV` 860 | - `KEY_KBDINPUTASSIST_NEXT` 861 | - `KEY_KBDINPUTASSIST_PREVGROUP` 862 | - `KEY_KBDINPUTASSIST_NEXTGROUP` 863 | - `KEY_KBDINPUTASSIST_ACCEPT` 864 | - `KEY_KBDINPUTASSIST_CANCEL` 865 | - `KEY_RIGHT_UP` 866 | - `KEY_RIGHT_DOWN` 867 | - `KEY_LEFT_UP` 868 | - `KEY_LEFT_DOWN` 869 | - `KEY_ROOT_MENU` 870 | - `KEY_MEDIA_TOP_MENU` 871 | - `KEY_NUMERIC_11` 872 | - `KEY_NUMERIC_12` 873 | - `KEY_AUDIO_DESC` 874 | - `KEY_3D_MODE` 875 | - `KEY_NEXT_FAVORITE` 876 | - `KEY_STOP_RECORD` 877 | - `KEY_PAUSE_RECORD` 878 | - `KEY_VOD` 879 | - `KEY_UNMUTE` 880 | - `KEY_FASTREVERSE` 881 | - `KEY_SLOWREVERSE` 882 | - `KEY_DATA` 883 | - `KEY_ONSCREEN_KEYBOARD` 884 | - `KEY_PRIVACY_SCREEN_TOGGLE` 885 | - `KEY_SELECTIVE_SCREENSHOT` 886 | - `BTN_TRIGGER_HAPPY1` 887 | - `BTN_TRIGGER_HAPPY2` 888 | - `BTN_TRIGGER_HAPPY3` 889 | - `BTN_TRIGGER_HAPPY4` 890 | - `BTN_TRIGGER_HAPPY5` 891 | - `BTN_TRIGGER_HAPPY6` 892 | - `BTN_TRIGGER_HAPPY7` 893 | - `BTN_TRIGGER_HAPPY8` 894 | - `BTN_TRIGGER_HAPPY9` 895 | - `BTN_TRIGGER_HAPPY10` 896 | - `BTN_TRIGGER_HAPPY11` 897 | - `BTN_TRIGGER_HAPPY12` 898 | - `BTN_TRIGGER_HAPPY13` 899 | - `BTN_TRIGGER_HAPPY14` 900 | - `BTN_TRIGGER_HAPPY15` 901 | - `BTN_TRIGGER_HAPPY16` 902 | - `BTN_TRIGGER_HAPPY17` 903 | - `BTN_TRIGGER_HAPPY18` 904 | - `BTN_TRIGGER_HAPPY19` 905 | - `BTN_TRIGGER_HAPPY20` 906 | - `BTN_TRIGGER_HAPPY21` 907 | - `BTN_TRIGGER_HAPPY22` 908 | - `BTN_TRIGGER_HAPPY23` 909 | - `BTN_TRIGGER_HAPPY24` 910 | - `BTN_TRIGGER_HAPPY25` 911 | - `BTN_TRIGGER_HAPPY26` 912 | - `BTN_TRIGGER_HAPPY27` 913 | - `BTN_TRIGGER_HAPPY28` 914 | - `BTN_TRIGGER_HAPPY29` 915 | - `BTN_TRIGGER_HAPPY30` 916 | - `BTN_TRIGGER_HAPPY31` 917 | - `BTN_TRIGGER_HAPPY32` 918 | - `BTN_TRIGGER_HAPPY33` 919 | - `BTN_TRIGGER_HAPPY34` 920 | - `BTN_TRIGGER_HAPPY35` 921 | - `BTN_TRIGGER_HAPPY36` 922 | - `BTN_TRIGGER_HAPPY37` 923 | - `BTN_TRIGGER_HAPPY38` 924 | - `BTN_TRIGGER_HAPPY39` 925 | - `BTN_TRIGGER_HAPPY40` 926 | 927 |
928 | 929 |
930 | 931 | KeyAction (expand) 932 | 933 | #### `KeyAction` 934 | 935 | A single keycode or a sequence of key events (macro). 936 | 937 | _Type_: `KeyCode` | `KeyEvent[]` 938 | 939 | _Example_: `KEY_C`, `[KEY_H, { press: KEY_I }, { release: KEY_I }]` 940 | 941 | #### `KeyEvent` 942 | 943 | > To **hold** a key, a press event musy be preceding hold. 944 | 945 | _Type_: 946 | 947 | - `{ press: KeyCode }` 948 | - `{ hold: KeyCode }` 949 | - `{ release: KeyCode }` 950 | - `{ delay: number }`: Input delay in milliseconds. 951 | - `{ string: string }`: ASCII string. 952 | - `{ env: string }`: Environment variable key. 953 | - `{ unicode: string }`: Unicode string. 954 | - `{ shell: string }`: Bash shell command. 955 | - `KeyCode`: Press + Release. 956 | 957 |
958 | 959 |
960 | 961 | TapDance (expand) 962 | 963 | #### `TapDance` 964 | 965 | Tap dance entry configuration. 966 | 967 | - `tap`: Action on tap, on release below timeout. 968 | 969 | _Type_: `KeyAction` 970 | 971 | - `hold`: Action on hold, exceeded timeout. 972 | 973 | _Type_: `KeyAction` 974 | 975 | - `timeout`: When to consider as a hold. 976 | 977 | _Type_: `number` 978 | 979 | _Default_: `250` (ms) 980 | 981 |
982 | 983 |
984 | 985 | Combo (expand) 986 | 987 | #### `Combo` 988 | 989 | Combo entry cobfiguration. 990 | 991 | - `keys`: Set of keys to activate the combo. 992 | 993 | _Type_: `KeyCode[]` 994 | 995 | - `action`: Action when keys are pressed/held at the same time. 996 | 997 | _Type_: `KeyAction` 998 | 999 |
1000 | 1001 |
1002 | 1003 | Layer (expand) 1004 | 1005 | #### `Layer` 1006 | 1007 | Layer entry configuration. 1008 | 1009 | - `modifier`: Layer activation key and behavior. 1010 | 1011 | _Type_: `KeyCode` | `{ key: KeyCode; type?: "momentary" | "toggle" | "oneshoot" }` 1012 | 1013 | - `keys`: Key mappings for the layer. 1014 | 1015 | _Type_: `Record` 1016 | 1017 |
1018 | 1019 | Fields: 1020 | 1021 | #### `name` 1022 | 1023 | Name of the keyboard as an input device, you can use `okey device list --keyboard` to find the name of a keyboard. 1024 | 1025 | _Type_: `string` 1026 | 1027 | #### `keys` (optional) 1028 | 1029 | Key mappings for the main layer. 1030 | 1031 | _Type_: `Record` 1032 | 1033 | #### `tap_dances` (optional) 1034 | 1035 | Dual function keys on tap/hold. 1036 | 1037 | _Type_: `Record` 1038 | 1039 | #### `combos` (optional) 1040 | 1041 | List of combo mappings. 1042 | 1043 | _Type_: `Combo[]` 1044 | 1045 | #### `layers` (optional) 1046 | 1047 | Virtual layers (shift-like). 1048 | 1049 | _Type_: `Record` 1050 | 1051 | ## License 1052 | 1053 | MIT 1054 | -------------------------------------------------------------------------------- /schema/okey.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "Config", 4 | "type": "object", 5 | "required": ["keyboards"], 6 | "additionalProperties": false, 7 | "properties": { 8 | "defaults": { 9 | "type": "object", 10 | "description": "Shared global settings", 11 | "properties": { 12 | "tap_dance": { 13 | "type": "object", 14 | "properties": { 15 | "default_timeout": { 16 | "type": "integer", 17 | "description": "Fallback tap dance timeout" 18 | } 19 | }, 20 | "additionalProperties": false 21 | }, 22 | "combo": { 23 | "type": "object", 24 | "properties": { 25 | "default_threshold": { 26 | "type": "integer", 27 | "description": "Delay for inserting unicode codepoints with macro" 28 | } 29 | }, 30 | "additionalProperties": false 31 | }, 32 | "general": { 33 | "type": "object", 34 | "properties": { 35 | "event_poll_timeout": { 36 | "type": "integer", 37 | "description": "Main event loop interval" 38 | }, 39 | "deferred_key_delay": { 40 | "type": "integer", 41 | "description": "Delay for keys following non-acknowledged special keys" 42 | }, 43 | "unicode_input_delay": { 44 | "type": "integer", 45 | "description": "Delay for inserting unicode codepoints with macro" 46 | } 47 | }, 48 | "additionalProperties": false 49 | } 50 | }, 51 | "additionalProperties": false 52 | }, 53 | "keyboards": { 54 | "type": "array", 55 | "description": "Per keyboard configuration", 56 | "items": { 57 | "type": "object", 58 | "properties": { 59 | "name": { 60 | "type": "string", 61 | "description": "The name of the keyboard as an input device (use `okey device list --keyboard`)" 62 | }, 63 | "keys": { 64 | "description": "Key mappings for the main layer", 65 | "type": "object", 66 | "additionalProperties": { 67 | "$ref": "#/$defs/KeyAction" 68 | } 69 | }, 70 | "combos": { 71 | "type": "array", 72 | "description": "List of combo mappings", 73 | "items": { "$ref": "#/$defs/ComboDefinition" } 74 | }, 75 | "tap_dances": { 76 | "type": "object", 77 | "description": "Dual function keys on tap/hold", 78 | "additionalProperties": { 79 | "$ref": "#/$defs/TapDanceConfig" 80 | } 81 | }, 82 | "layers": { 83 | "type": "object", 84 | "description": "Virtual layers (shift-like)", 85 | "additionalProperties": { 86 | "$ref": "#/$defs/LayerDefinition" 87 | } 88 | } 89 | }, 90 | "required": ["name"], 91 | "additionalProperties": false 92 | } 93 | } 94 | }, 95 | "$defs": { 96 | "ComboDefinition": { 97 | "type": "object", 98 | "properties": { 99 | "keys": { 100 | "type": "array", 101 | "items": { "$ref": "#/$defs/KeyCode" } 102 | }, 103 | "action": { "$ref": "#/$defs/KeyAction" } 104 | }, 105 | "required": ["keys", "action"], 106 | "additionalProperties": false 107 | }, 108 | "TapDanceConfig": { 109 | "type": "object", 110 | "properties": { 111 | "timeout": { "type": "integer" }, 112 | "tap": { "$ref": "#/$defs/KeyAction" }, 113 | "hold": { "$ref": "#/$defs/KeyAction" } 114 | }, 115 | "required": ["tap", "hold"], 116 | "additionalProperties": false 117 | }, 118 | "LayerDefinition": { 119 | "type": "object", 120 | "properties": { 121 | "modifier": { 122 | "$ref": "#/$defs/LayerModifierConfig", 123 | "description": "Layer activation and behavior" 124 | }, 125 | "keys": { 126 | "type": "object", 127 | "description": "Key mappings for the current layer", 128 | "additionalProperties": { "$ref": "#/$defs/KeyAction" } 129 | } 130 | }, 131 | "required": ["modifier", "keys"], 132 | "additionalProperties": false 133 | }, 134 | "LayerModifierConfig": { 135 | "oneOf": [ 136 | { "$ref": "#/$defs/KeyCode" }, 137 | { 138 | "type": "object", 139 | "properties": { 140 | "key": { 141 | "$ref": "#/$defs/KeyCode", 142 | "description": "Layer activation key" 143 | }, 144 | "type": { 145 | "type": "string", 146 | "description": "Layer activation behavior", 147 | "enum": ["momentary", "toggle", "oneshoot"] 148 | } 149 | }, 150 | "required": ["key"], 151 | "additionalProperties": false 152 | } 153 | ] 154 | }, 155 | "KeyAction": { "$ref": "#/$defs/Macro" }, 156 | "Macro": { 157 | "oneOf": [ 158 | { "$ref": "#/$defs/EventMacro" }, 159 | { 160 | "type": "array", 161 | "items": { "$ref": "#/$defs/EventMacro" } 162 | } 163 | ] 164 | }, 165 | "EventMacro": { 166 | "oneOf": [ 167 | { "$ref": "#/$defs/KeyCode" }, 168 | { 169 | "type": "object", 170 | "oneOf": [ 171 | { 172 | "type": "object", 173 | "required": ["press"], 174 | "properties": { 175 | "press": { 176 | "$ref": "#/$defs/KeyCode", 177 | "description": "Keycode press event" 178 | } 179 | }, 180 | "additionalProperties": false 181 | }, 182 | { 183 | "type": "object", 184 | "required": ["hold"], 185 | "properties": { 186 | "hold": { 187 | "$ref": "#/$defs/KeyCode", 188 | "description": "Keycode hold event" 189 | } 190 | }, 191 | "additionalProperties": false 192 | }, 193 | { 194 | "type": "object", 195 | "required": ["release"], 196 | "properties": { 197 | "release": { 198 | "$ref": "#/$defs/KeyCode", 199 | "description": "Keycode release event" 200 | } 201 | }, 202 | "additionalProperties": false 203 | }, 204 | { 205 | "type": "object", 206 | "required": ["delay"], 207 | "properties": { 208 | "delay": { 209 | "type": "integer", 210 | "description": "Wait a given delay in milliseconds" 211 | } 212 | }, 213 | "additionalProperties": false 214 | }, 215 | { 216 | "type": "object", 217 | "required": ["string"], 218 | "properties": { 219 | "string": { "type": "string", "description": "ASCII string" } 220 | }, 221 | "additionalProperties": false 222 | }, 223 | { 224 | "required": ["env"], 225 | "properties": { 226 | "env": { 227 | "type": "string", 228 | "description": "Environment variable name" 229 | } 230 | }, 231 | "additionalProperties": false 232 | }, 233 | { 234 | "type": "object", 235 | "required": ["unicode"], 236 | "properties": { 237 | "unicode": { 238 | "type": "string", 239 | "description": "Unicode characters" 240 | } 241 | }, 242 | "additionalProperties": false 243 | }, 244 | { 245 | "type": "object", 246 | "required": ["shell"], 247 | "properties": { 248 | "shell": { 249 | "type": "string", 250 | "description": "Bash shell command" 251 | }, 252 | "trim": { 253 | "type": "boolean", 254 | "description": "Whether to trim trailing whitespaces or not" 255 | } 256 | }, 257 | "additionalProperties": false 258 | } 259 | ] 260 | } 261 | ] 262 | }, 263 | "KeyCode": { 264 | "anyOf": [ 265 | { 266 | "type": "string" 267 | }, 268 | { 269 | "enum": [ 270 | "KEY_RESERVED", 271 | "KEY_ESC", 272 | "KEY_1", 273 | "KEY_2", 274 | "KEY_3", 275 | "KEY_4", 276 | "KEY_5", 277 | "KEY_6", 278 | "KEY_7", 279 | "KEY_8", 280 | "KEY_9", 281 | "KEY_0", 282 | "KEY_MINUS", 283 | "KEY_EQUAL", 284 | "KEY_BACKSPACE", 285 | "KEY_TAB", 286 | "KEY_Q", 287 | "KEY_W", 288 | "KEY_E", 289 | "KEY_R", 290 | "KEY_T", 291 | "KEY_Y", 292 | "KEY_U", 293 | "KEY_I", 294 | "KEY_O", 295 | "KEY_P", 296 | "KEY_LEFTBRACE", 297 | "KEY_RIGHTBRACE", 298 | "KEY_ENTER", 299 | "KEY_LEFTCTRL", 300 | "KEY_A", 301 | "KEY_S", 302 | "KEY_D", 303 | "KEY_F", 304 | "KEY_G", 305 | "KEY_H", 306 | "KEY_J", 307 | "KEY_K", 308 | "KEY_L", 309 | "KEY_SEMICOLON", 310 | "KEY_APOSTROPHE", 311 | "KEY_GRAVE", 312 | "KEY_LEFTSHIFT", 313 | "KEY_BACKSLASH", 314 | "KEY_Z", 315 | "KEY_X", 316 | "KEY_C", 317 | "KEY_V", 318 | "KEY_B", 319 | "KEY_N", 320 | "KEY_M", 321 | "KEY_COMMA", 322 | "KEY_DOT", 323 | "KEY_SLASH", 324 | "KEY_RIGHTSHIFT", 325 | "KEY_KPASTERISK", 326 | "KEY_LEFTALT", 327 | "KEY_SPACE", 328 | "KEY_CAPSLOCK", 329 | "KEY_F1", 330 | "KEY_F2", 331 | "KEY_F3", 332 | "KEY_F4", 333 | "KEY_F5", 334 | "KEY_F6", 335 | "KEY_F7", 336 | "KEY_F8", 337 | "KEY_F9", 338 | "KEY_F10", 339 | "KEY_NUMLOCK", 340 | "KEY_SCROLLLOCK", 341 | "KEY_KP7", 342 | "KEY_KP8", 343 | "KEY_KP9", 344 | "KEY_KPMINUS", 345 | "KEY_KP4", 346 | "KEY_KP5", 347 | "KEY_KP6", 348 | "KEY_KPPLUS", 349 | "KEY_KP1", 350 | "KEY_KP2", 351 | "KEY_KP3", 352 | "KEY_KP0", 353 | "KEY_KPDOT", 354 | "KEY_ZENKAKUHANKAKU", 355 | "KEY_102ND", 356 | "KEY_F11", 357 | "KEY_F12", 358 | "KEY_RO", 359 | "KEY_KATAKANA", 360 | "KEY_HIRAGANA", 361 | "KEY_HENKAN", 362 | "KEY_KATAKANAHIRAGANA", 363 | "KEY_MUHENKAN", 364 | "KEY_KPJPCOMMA", 365 | "KEY_KPENTER", 366 | "KEY_RIGHTCTRL", 367 | "KEY_KPSLASH", 368 | "KEY_SYSRQ", 369 | "KEY_RIGHTALT", 370 | "KEY_LINEFEED", 371 | "KEY_HOME", 372 | "KEY_UP", 373 | "KEY_PAGEUP", 374 | "KEY_LEFT", 375 | "KEY_RIGHT", 376 | "KEY_END", 377 | "KEY_DOWN", 378 | "KEY_PAGEDOWN", 379 | "KEY_INSERT", 380 | "KEY_DELETE", 381 | "KEY_MACRO", 382 | "KEY_MUTE", 383 | "KEY_VOLUMEDOWN", 384 | "KEY_VOLUMEUP", 385 | "KEY_POWER", 386 | "KEY_KPEQUAL", 387 | "KEY_KPPLUSMINUS", 388 | "KEY_PAUSE", 389 | "KEY_SCALE", 390 | "KEY_KPCOMMA", 391 | "KEY_HANGEUL", 392 | "KEY_HANJA", 393 | "KEY_YEN", 394 | "KEY_LEFTMETA", 395 | "KEY_RIGHTMETA", 396 | "KEY_COMPOSE", 397 | "KEY_STOP", 398 | "KEY_AGAIN", 399 | "KEY_PROPS", 400 | "KEY_UNDO", 401 | "KEY_FRONT", 402 | "KEY_COPY", 403 | "KEY_OPEN", 404 | "KEY_PASTE", 405 | "KEY_FIND", 406 | "KEY_CUT", 407 | "KEY_HELP", 408 | "KEY_MENU", 409 | "KEY_CALC", 410 | "KEY_SETUP", 411 | "KEY_SLEEP", 412 | "KEY_WAKEUP", 413 | "KEY_FILE", 414 | "KEY_SENDFILE", 415 | "KEY_DELETEFILE", 416 | "KEY_XFER", 417 | "KEY_PROG1", 418 | "KEY_PROG2", 419 | "KEY_WWW", 420 | "KEY_MSDOS", 421 | "KEY_COFFEE", 422 | "KEY_DIRECTION", 423 | "KEY_ROTATE_DISPLAY", 424 | "KEY_CYCLEWINDOWS", 425 | "KEY_MAIL", 426 | "KEY_BOOKMARKS", 427 | "KEY_COMPUTER", 428 | "KEY_BACK", 429 | "KEY_FORWARD", 430 | "KEY_CLOSECD", 431 | "KEY_EJECTCD", 432 | "KEY_EJECTCLOSECD", 433 | "KEY_NEXTSONG", 434 | "KEY_PLAYPAUSE", 435 | "KEY_PREVIOUSSONG", 436 | "KEY_STOPCD", 437 | "KEY_RECORD", 438 | "KEY_REWIND", 439 | "KEY_PHONE", 440 | "KEY_ISO", 441 | "KEY_CONFIG", 442 | "KEY_HOMEPAGE", 443 | "KEY_REFRESH", 444 | "KEY_EXIT", 445 | "KEY_MOVE", 446 | "KEY_EDIT", 447 | "KEY_SCROLLUP", 448 | "KEY_SCROLLDOWN", 449 | "KEY_KPLEFTPAREN", 450 | "KEY_KPRIGHTPAREN", 451 | "KEY_NEW", 452 | "KEY_REDO", 453 | "KEY_F13", 454 | "KEY_F14", 455 | "KEY_F15", 456 | "KEY_F16", 457 | "KEY_F17", 458 | "KEY_F18", 459 | "KEY_F19", 460 | "KEY_F20", 461 | "KEY_F21", 462 | "KEY_F22", 463 | "KEY_F23", 464 | "KEY_F24", 465 | "KEY_PLAYCD", 466 | "KEY_PAUSECD", 467 | "KEY_PROG3", 468 | "KEY_PROG4", 469 | "KEY_DASHBOARD", 470 | "KEY_SUSPEND", 471 | "KEY_CLOSE", 472 | "KEY_PLAY", 473 | "KEY_FASTFORWARD", 474 | "KEY_BASSBOOST", 475 | "KEY_PRINT", 476 | "KEY_HP", 477 | "KEY_CAMERA", 478 | "KEY_SOUND", 479 | "KEY_QUESTION", 480 | "KEY_EMAIL", 481 | "KEY_CHAT", 482 | "KEY_SEARCH", 483 | "KEY_CONNECT", 484 | "KEY_FINANCE", 485 | "KEY_SPORT", 486 | "KEY_SHOP", 487 | "KEY_ALTERASE", 488 | "KEY_CANCEL", 489 | "KEY_BRIGHTNESSDOWN", 490 | "KEY_BRIGHTNESSUP", 491 | "KEY_MEDIA", 492 | "KEY_SWITCHVIDEOMODE", 493 | "KEY_KBDILLUMTOGGLE", 494 | "KEY_KBDILLUMDOWN", 495 | "KEY_KBDILLUMUP", 496 | "KEY_SEND", 497 | "KEY_REPLY", 498 | "KEY_FORWARDMAIL", 499 | "KEY_SAVE", 500 | "KEY_DOCUMENTS", 501 | "KEY_BATTERY", 502 | "KEY_BLUETOOTH", 503 | "KEY_WLAN", 504 | "KEY_UWB", 505 | "KEY_UNKNOWN", 506 | "KEY_VIDEO_NEXT", 507 | "KEY_VIDEO_PREV", 508 | "KEY_BRIGHTNESS_CYCLE", 509 | "KEY_BRIGHTNESS_AUTO", 510 | "KEY_DISPLAY_OFF", 511 | "KEY_WWAN", 512 | "KEY_RFKILL", 513 | "KEY_MICMUTE", 514 | "BTN_0", 515 | "BTN_1", 516 | "BTN_2", 517 | "BTN_3", 518 | "BTN_4", 519 | "BTN_5", 520 | "BTN_6", 521 | "BTN_7", 522 | "BTN_8", 523 | "BTN_9", 524 | "BTN_LEFT", 525 | "BTN_RIGHT", 526 | "BTN_MIDDLE", 527 | "BTN_SIDE", 528 | "BTN_EXTRA", 529 | "BTN_FORWARD", 530 | "BTN_BACK", 531 | "BTN_TASK", 532 | "BTN_TRIGGER", 533 | "BTN_THUMB", 534 | "BTN_THUMB2", 535 | "BTN_TOP", 536 | "BTN_TOP2", 537 | "BTN_PINKIE", 538 | "BTN_BASE", 539 | "BTN_BASE2", 540 | "BTN_BASE3", 541 | "BTN_BASE4", 542 | "BTN_BASE5", 543 | "BTN_BASE6", 544 | "BTN_DEAD", 545 | "BTN_SOUTH", 546 | "BTN_EAST", 547 | "BTN_C", 548 | "BTN_NORTH", 549 | "BTN_WEST", 550 | "BTN_Z", 551 | "BTN_TL", 552 | "BTN_TR", 553 | "BTN_TL2", 554 | "BTN_TR2", 555 | "BTN_SELECT", 556 | "BTN_START", 557 | "BTN_MODE", 558 | "BTN_THUMBL", 559 | "BTN_THUMBR", 560 | "BTN_TOOL_PEN", 561 | "BTN_TOOL_RUBBER", 562 | "BTN_TOOL_BRUSH", 563 | "BTN_TOOL_PENCIL", 564 | "BTN_TOOL_AIRBRUSH", 565 | "BTN_TOOL_FINGER", 566 | "BTN_TOOL_MOUSE", 567 | "BTN_TOOL_LENS", 568 | "BTN_TOOL_QUINTTAP", 569 | "BTN_TOUCH", 570 | "BTN_STYLUS", 571 | "BTN_STYLUS2", 572 | "BTN_TOOL_DOUBLETAP", 573 | "BTN_TOOL_TRIPLETAP", 574 | "BTN_TOOL_QUADTAP", 575 | "BTN_GEAR_DOWN", 576 | "BTN_GEAR_UP", 577 | "KEY_OK", 578 | "KEY_SELECT", 579 | "KEY_GOTO", 580 | "KEY_CLEAR", 581 | "KEY_POWER2", 582 | "KEY_OPTION", 583 | "KEY_INFO", 584 | "KEY_TIME", 585 | "KEY_VENDOR", 586 | "KEY_ARCHIVE", 587 | "KEY_PROGRAM", 588 | "KEY_CHANNEL", 589 | "KEY_FAVORITES", 590 | "KEY_EPG", 591 | "KEY_PVR", 592 | "KEY_MHP", 593 | "KEY_LANGUAGE", 594 | "KEY_TITLE", 595 | "KEY_SUBTITLE", 596 | "KEY_ANGLE", 597 | "KEY_ZOOM", 598 | "KEY_FULL_SCREEN", 599 | "KEY_MODE", 600 | "KEY_KEYBOARD", 601 | "KEY_SCREEN", 602 | "KEY_PC", 603 | "KEY_TV", 604 | "KEY_TV2", 605 | "KEY_VCR", 606 | "KEY_VCR2", 607 | "KEY_SAT", 608 | "KEY_SAT2", 609 | "KEY_CD", 610 | "KEY_TAPE", 611 | "KEY_RADIO", 612 | "KEY_TUNER", 613 | "KEY_PLAYER", 614 | "KEY_TEXT", 615 | "KEY_DVD", 616 | "KEY_AUX", 617 | "KEY_MP3", 618 | "KEY_AUDIO", 619 | "KEY_VIDEO", 620 | "KEY_DIRECTORY", 621 | "KEY_LIST", 622 | "KEY_MEMO", 623 | "KEY_CALENDAR", 624 | "KEY_RED", 625 | "KEY_GREEN", 626 | "KEY_YELLOW", 627 | "KEY_BLUE", 628 | "KEY_CHANNELUP", 629 | "KEY_CHANNELDOWN", 630 | "KEY_FIRST", 631 | "KEY_LAST", 632 | "KEY_AB", 633 | "KEY_NEXT", 634 | "KEY_RESTART", 635 | "KEY_SLOW", 636 | "KEY_SHUFFLE", 637 | "KEY_BREAK", 638 | "KEY_PREVIOUS", 639 | "KEY_DIGITS", 640 | "KEY_TEEN", 641 | "KEY_TWEN", 642 | "KEY_VIDEOPHONE", 643 | "KEY_GAMES", 644 | "KEY_ZOOMIN", 645 | "KEY_ZOOMOUT", 646 | "KEY_ZOOMRESET", 647 | "KEY_WORDPROCESSOR", 648 | "KEY_EDITOR", 649 | "KEY_SPREADSHEET", 650 | "KEY_GRAPHICSEDITOR", 651 | "KEY_PRESENTATION", 652 | "KEY_DATABASE", 653 | "KEY_NEWS", 654 | "KEY_VOICEMAIL", 655 | "KEY_ADDRESSBOOK", 656 | "KEY_MESSENGER", 657 | "KEY_DISPLAYTOGGLE", 658 | "KEY_SPELLCHECK", 659 | "KEY_LOGOFF", 660 | "KEY_DOLLAR", 661 | "KEY_EURO", 662 | "KEY_FRAMEBACK", 663 | "KEY_FRAMEFORWARD", 664 | "KEY_CONTEXT_MENU", 665 | "KEY_MEDIA_REPEAT", 666 | "KEY_10CHANNELSUP", 667 | "KEY_10CHANNELSDOWN", 668 | "KEY_IMAGES", 669 | "KEY_PICKUP_PHONE", 670 | "KEY_HANGUP_PHONE", 671 | "KEY_DEL_EOL", 672 | "KEY_DEL_EOS", 673 | "KEY_INS_LINE", 674 | "KEY_DEL_LINE", 675 | "KEY_FN", 676 | "KEY_FN_ESC", 677 | "KEY_FN_F1", 678 | "KEY_FN_F2", 679 | "KEY_FN_F3", 680 | "KEY_FN_F4", 681 | "KEY_FN_F5", 682 | "KEY_FN_F6", 683 | "KEY_FN_F7", 684 | "KEY_FN_F8", 685 | "KEY_FN_F9", 686 | "KEY_FN_F10", 687 | "KEY_FN_F11", 688 | "KEY_FN_F12", 689 | "KEY_FN_1", 690 | "KEY_FN_2", 691 | "KEY_FN_D", 692 | "KEY_FN_E", 693 | "KEY_FN_F", 694 | "KEY_FN_S", 695 | "KEY_FN_B", 696 | "KEY_BRL_DOT1", 697 | "KEY_BRL_DOT2", 698 | "KEY_BRL_DOT3", 699 | "KEY_BRL_DOT4", 700 | "KEY_BRL_DOT5", 701 | "KEY_BRL_DOT6", 702 | "KEY_BRL_DOT7", 703 | "KEY_BRL_DOT8", 704 | "KEY_BRL_DOT9", 705 | "KEY_BRL_DOT10", 706 | "KEY_NUMERIC_0", 707 | "KEY_NUMERIC_1", 708 | "KEY_NUMERIC_2", 709 | "KEY_NUMERIC_3", 710 | "KEY_NUMERIC_4", 711 | "KEY_NUMERIC_5", 712 | "KEY_NUMERIC_6", 713 | "KEY_NUMERIC_7", 714 | "KEY_NUMERIC_8", 715 | "KEY_NUMERIC_9", 716 | "KEY_NUMERIC_STAR", 717 | "KEY_NUMERIC_POUND", 718 | "KEY_NUMERIC_A", 719 | "KEY_NUMERIC_B", 720 | "KEY_NUMERIC_C", 721 | "KEY_NUMERIC_D", 722 | "KEY_CAMERA_FOCUS", 723 | "KEY_WPS_BUTTON", 724 | "KEY_TOUCHPAD_TOGGLE", 725 | "KEY_TOUCHPAD_ON", 726 | "KEY_TOUCHPAD_OFF", 727 | "KEY_CAMERA_ZOOMIN", 728 | "KEY_CAMERA_ZOOMOUT", 729 | "KEY_CAMERA_UP", 730 | "KEY_CAMERA_DOWN", 731 | "KEY_CAMERA_LEFT", 732 | "KEY_CAMERA_RIGHT", 733 | "KEY_ATTENDANT_ON", 734 | "KEY_ATTENDANT_OFF", 735 | "KEY_ATTENDANT_TOGGLE", 736 | "KEY_LIGHTS_TOGGLE", 737 | "BTN_DPAD_UP", 738 | "BTN_DPAD_DOWN", 739 | "BTN_DPAD_LEFT", 740 | "BTN_DPAD_RIGHT", 741 | "KEY_ALS_TOGGLE", 742 | "KEY_BUTTONCONFIG", 743 | "KEY_TASKMANAGER", 744 | "KEY_JOURNAL", 745 | "KEY_CONTROLPANEL", 746 | "KEY_APPSELECT", 747 | "KEY_SCREENSAVER", 748 | "KEY_VOICECOMMAND", 749 | "KEY_ASSISTANT", 750 | "KEY_KBD_LAYOUT_NEXT", 751 | "KEY_BRIGHTNESS_MIN", 752 | "KEY_BRIGHTNESS_MAX", 753 | "KEY_KBDINPUTASSIST_PREV", 754 | "KEY_KBDINPUTASSIST_NEXT", 755 | "KEY_KBDINPUTASSIST_PREVGROUP", 756 | "KEY_KBDINPUTASSIST_NEXTGROUP", 757 | "KEY_KBDINPUTASSIST_ACCEPT", 758 | "KEY_KBDINPUTASSIST_CANCEL", 759 | "KEY_RIGHT_UP", 760 | "KEY_RIGHT_DOWN", 761 | "KEY_LEFT_UP", 762 | "KEY_LEFT_DOWN", 763 | "KEY_ROOT_MENU", 764 | "KEY_MEDIA_TOP_MENU", 765 | "KEY_NUMERIC_11", 766 | "KEY_NUMERIC_12", 767 | "KEY_AUDIO_DESC", 768 | "KEY_3D_MODE", 769 | "KEY_NEXT_FAVORITE", 770 | "KEY_STOP_RECORD", 771 | "KEY_PAUSE_RECORD", 772 | "KEY_VOD", 773 | "KEY_UNMUTE", 774 | "KEY_FASTREVERSE", 775 | "KEY_SLOWREVERSE", 776 | "KEY_DATA", 777 | "KEY_ONSCREEN_KEYBOARD", 778 | "KEY_PRIVACY_SCREEN_TOGGLE", 779 | "KEY_SELECTIVE_SCREENSHOT", 780 | "BTN_TRIGGER_HAPPY1", 781 | "BTN_TRIGGER_HAPPY2", 782 | "BTN_TRIGGER_HAPPY3", 783 | "BTN_TRIGGER_HAPPY4", 784 | "BTN_TRIGGER_HAPPY5", 785 | "BTN_TRIGGER_HAPPY6", 786 | "BTN_TRIGGER_HAPPY7", 787 | "BTN_TRIGGER_HAPPY8", 788 | "BTN_TRIGGER_HAPPY9", 789 | "BTN_TRIGGER_HAPPY10", 790 | "BTN_TRIGGER_HAPPY11", 791 | "BTN_TRIGGER_HAPPY12", 792 | "BTN_TRIGGER_HAPPY13", 793 | "BTN_TRIGGER_HAPPY14", 794 | "BTN_TRIGGER_HAPPY15", 795 | "BTN_TRIGGER_HAPPY16", 796 | "BTN_TRIGGER_HAPPY17", 797 | "BTN_TRIGGER_HAPPY18", 798 | "BTN_TRIGGER_HAPPY19", 799 | "BTN_TRIGGER_HAPPY20", 800 | "BTN_TRIGGER_HAPPY21", 801 | "BTN_TRIGGER_HAPPY22", 802 | "BTN_TRIGGER_HAPPY23", 803 | "BTN_TRIGGER_HAPPY24", 804 | "BTN_TRIGGER_HAPPY25", 805 | "BTN_TRIGGER_HAPPY26", 806 | "BTN_TRIGGER_HAPPY27", 807 | "BTN_TRIGGER_HAPPY28", 808 | "BTN_TRIGGER_HAPPY29", 809 | "BTN_TRIGGER_HAPPY30", 810 | "BTN_TRIGGER_HAPPY31", 811 | "BTN_TRIGGER_HAPPY32", 812 | "BTN_TRIGGER_HAPPY33", 813 | "BTN_TRIGGER_HAPPY34", 814 | "BTN_TRIGGER_HAPPY35", 815 | "BTN_TRIGGER_HAPPY36", 816 | "BTN_TRIGGER_HAPPY37", 817 | "BTN_TRIGGER_HAPPY38", 818 | "BTN_TRIGGER_HAPPY39", 819 | "BTN_TRIGGER_HAPPY40", 820 | 821 | "KEY_EXCLAMATION", 822 | "KEY_AT", 823 | "KEY_HASH", 824 | "KEY_DOLLARSIGN", 825 | "KEY_PERCENT", 826 | "KEY_CARET", 827 | "KEY_AMPERSAND", 828 | "KEY_STAR", 829 | "KEY_LEFTPAREN", 830 | "KEY_RIGHTPAREN", 831 | "KEY_UNDERSCORE", 832 | "KEY_PLUS", 833 | "KEY_LEFTCURLY", 834 | "KEY_RIGHTCURLY", 835 | "KEY_COLON", 836 | "KEY_DOUBLEQUOTE", 837 | "KEY_LESS", 838 | "KEY_GREATER", 839 | "KEY_QUESTION", 840 | "KEY_TILDE", 841 | "KEY_PIPE" 842 | ] 843 | } 844 | ] 845 | } 846 | } 847 | } 848 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 | 20 | [[package]] 21 | name = "ahash" 22 | version = "0.8.11" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" 25 | dependencies = [ 26 | "cfg-if", 27 | "getrandom", 28 | "once_cell", 29 | "serde", 30 | "version_check", 31 | "zerocopy", 32 | ] 33 | 34 | [[package]] 35 | name = "aho-corasick" 36 | version = "1.1.3" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 39 | dependencies = [ 40 | "memchr", 41 | ] 42 | 43 | [[package]] 44 | name = "anstream" 45 | version = "0.6.18" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 48 | dependencies = [ 49 | "anstyle", 50 | "anstyle-parse", 51 | "anstyle-query", 52 | "anstyle-wincon", 53 | "colorchoice", 54 | "is_terminal_polyfill", 55 | "utf8parse", 56 | ] 57 | 58 | [[package]] 59 | name = "anstyle" 60 | version = "1.0.10" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 63 | 64 | [[package]] 65 | name = "anstyle-parse" 66 | version = "0.2.6" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 69 | dependencies = [ 70 | "utf8parse", 71 | ] 72 | 73 | [[package]] 74 | name = "anstyle-query" 75 | version = "1.1.2" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 78 | dependencies = [ 79 | "windows-sys 0.59.0", 80 | ] 81 | 82 | [[package]] 83 | name = "anstyle-wincon" 84 | version = "3.0.7" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 87 | dependencies = [ 88 | "anstyle", 89 | "once_cell", 90 | "windows-sys 0.59.0", 91 | ] 92 | 93 | [[package]] 94 | name = "anyhow" 95 | version = "1.0.98" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" 98 | 99 | [[package]] 100 | name = "autocfg" 101 | version = "1.4.0" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 104 | 105 | [[package]] 106 | name = "backtrace" 107 | version = "0.3.74" 108 | source = "registry+https://github.com/rust-lang/crates.io-index" 109 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 110 | dependencies = [ 111 | "addr2line", 112 | "cfg-if", 113 | "libc", 114 | "miniz_oxide", 115 | "object", 116 | "rustc-demangle", 117 | "windows-targets 0.52.6", 118 | ] 119 | 120 | [[package]] 121 | name = "base64" 122 | version = "0.22.1" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 125 | 126 | [[package]] 127 | name = "bit-set" 128 | version = "0.8.0" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" 131 | dependencies = [ 132 | "bit-vec", 133 | ] 134 | 135 | [[package]] 136 | name = "bit-vec" 137 | version = "0.8.0" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" 140 | 141 | [[package]] 142 | name = "bitflags" 143 | version = "2.9.0" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 146 | 147 | [[package]] 148 | name = "bitvec" 149 | version = "1.0.1" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" 152 | dependencies = [ 153 | "funty", 154 | "radium", 155 | "tap", 156 | "wyz", 157 | ] 158 | 159 | [[package]] 160 | name = "borrow-or-share" 161 | version = "0.2.2" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "3eeab4423108c5d7c744f4d234de88d18d636100093ae04caf4825134b9c3a32" 164 | 165 | [[package]] 166 | name = "bumpalo" 167 | version = "3.17.0" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" 170 | 171 | [[package]] 172 | name = "bytecount" 173 | version = "0.6.8" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce" 176 | 177 | [[package]] 178 | name = "bytes" 179 | version = "1.10.1" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 182 | 183 | [[package]] 184 | name = "cfg-if" 185 | version = "1.0.0" 186 | source = "registry+https://github.com/rust-lang/crates.io-index" 187 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 188 | 189 | [[package]] 190 | name = "cfg_aliases" 191 | version = "0.2.1" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 194 | 195 | [[package]] 196 | name = "clap" 197 | version = "4.5.37" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071" 200 | dependencies = [ 201 | "clap_builder", 202 | "clap_derive", 203 | ] 204 | 205 | [[package]] 206 | name = "clap_builder" 207 | version = "4.5.37" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2" 210 | dependencies = [ 211 | "anstream", 212 | "anstyle", 213 | "clap_lex", 214 | "strsim", 215 | ] 216 | 217 | [[package]] 218 | name = "clap_derive" 219 | version = "4.5.32" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" 222 | dependencies = [ 223 | "heck", 224 | "proc-macro2", 225 | "quote", 226 | "syn", 227 | ] 228 | 229 | [[package]] 230 | name = "clap_lex" 231 | version = "0.7.4" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 234 | 235 | [[package]] 236 | name = "colorchoice" 237 | version = "1.0.3" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 240 | 241 | [[package]] 242 | name = "colored" 243 | version = "2.2.0" 244 | source = "registry+https://github.com/rust-lang/crates.io-index" 245 | checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" 246 | dependencies = [ 247 | "lazy_static", 248 | "windows-sys 0.59.0", 249 | ] 250 | 251 | [[package]] 252 | name = "deranged" 253 | version = "0.4.0" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" 256 | dependencies = [ 257 | "powerfmt", 258 | ] 259 | 260 | [[package]] 261 | name = "displaydoc" 262 | version = "0.2.5" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 265 | dependencies = [ 266 | "proc-macro2", 267 | "quote", 268 | "syn", 269 | ] 270 | 271 | [[package]] 272 | name = "email_address" 273 | version = "0.2.9" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" 276 | dependencies = [ 277 | "serde", 278 | ] 279 | 280 | [[package]] 281 | name = "equivalent" 282 | version = "1.0.2" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 285 | 286 | [[package]] 287 | name = "evdev" 288 | version = "0.13.1" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | checksum = "a3c10865aeab1a7399b3c2d6046e8dcc7f5227b656f235ed63ef5ee45a47b8f8" 291 | dependencies = [ 292 | "bitvec", 293 | "cfg-if", 294 | "libc", 295 | "nix", 296 | ] 297 | 298 | [[package]] 299 | name = "fancy-regex" 300 | version = "0.14.0" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298" 303 | dependencies = [ 304 | "bit-set", 305 | "regex-automata", 306 | "regex-syntax", 307 | ] 308 | 309 | [[package]] 310 | name = "fluent-uri" 311 | version = "0.3.2" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "1918b65d96df47d3591bed19c5cca17e3fa5d0707318e4b5ef2eae01764df7e5" 314 | dependencies = [ 315 | "borrow-or-share", 316 | "ref-cast", 317 | "serde", 318 | ] 319 | 320 | [[package]] 321 | name = "fnv" 322 | version = "1.0.7" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 325 | 326 | [[package]] 327 | name = "form_urlencoded" 328 | version = "1.2.1" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 331 | dependencies = [ 332 | "percent-encoding", 333 | ] 334 | 335 | [[package]] 336 | name = "fraction" 337 | version = "0.15.3" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "0f158e3ff0a1b334408dc9fb811cd99b446986f4d8b741bb08f9df1604085ae7" 340 | dependencies = [ 341 | "lazy_static", 342 | "num", 343 | ] 344 | 345 | [[package]] 346 | name = "funty" 347 | version = "2.0.0" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" 350 | 351 | [[package]] 352 | name = "futures-channel" 353 | version = "0.3.31" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 356 | dependencies = [ 357 | "futures-core", 358 | "futures-sink", 359 | ] 360 | 361 | [[package]] 362 | name = "futures-core" 363 | version = "0.3.31" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 366 | 367 | [[package]] 368 | name = "futures-io" 369 | version = "0.3.31" 370 | source = "registry+https://github.com/rust-lang/crates.io-index" 371 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 372 | 373 | [[package]] 374 | name = "futures-sink" 375 | version = "0.3.31" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 378 | 379 | [[package]] 380 | name = "futures-task" 381 | version = "0.3.31" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 384 | 385 | [[package]] 386 | name = "futures-util" 387 | version = "0.3.31" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 390 | dependencies = [ 391 | "futures-core", 392 | "futures-io", 393 | "futures-sink", 394 | "futures-task", 395 | "memchr", 396 | "pin-project-lite", 397 | "pin-utils", 398 | "slab", 399 | ] 400 | 401 | [[package]] 402 | name = "getrandom" 403 | version = "0.2.16" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" 406 | dependencies = [ 407 | "cfg-if", 408 | "libc", 409 | "wasi", 410 | ] 411 | 412 | [[package]] 413 | name = "gimli" 414 | version = "0.31.1" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 417 | 418 | [[package]] 419 | name = "hashbrown" 420 | version = "0.15.2" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 423 | 424 | [[package]] 425 | name = "heck" 426 | version = "0.5.0" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 429 | 430 | [[package]] 431 | name = "http" 432 | version = "1.3.1" 433 | source = "registry+https://github.com/rust-lang/crates.io-index" 434 | checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" 435 | dependencies = [ 436 | "bytes", 437 | "fnv", 438 | "itoa", 439 | ] 440 | 441 | [[package]] 442 | name = "http-body" 443 | version = "1.0.1" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 446 | dependencies = [ 447 | "bytes", 448 | "http", 449 | ] 450 | 451 | [[package]] 452 | name = "http-body-util" 453 | version = "0.1.3" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" 456 | dependencies = [ 457 | "bytes", 458 | "futures-core", 459 | "http", 460 | "http-body", 461 | "pin-project-lite", 462 | ] 463 | 464 | [[package]] 465 | name = "httparse" 466 | version = "1.10.1" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" 469 | 470 | [[package]] 471 | name = "hyper" 472 | version = "1.6.0" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" 475 | dependencies = [ 476 | "bytes", 477 | "futures-channel", 478 | "futures-util", 479 | "http", 480 | "http-body", 481 | "httparse", 482 | "itoa", 483 | "pin-project-lite", 484 | "smallvec", 485 | "tokio", 486 | "want", 487 | ] 488 | 489 | [[package]] 490 | name = "hyper-util" 491 | version = "0.1.11" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" 494 | dependencies = [ 495 | "bytes", 496 | "futures-channel", 497 | "futures-util", 498 | "http", 499 | "http-body", 500 | "hyper", 501 | "libc", 502 | "pin-project-lite", 503 | "socket2", 504 | "tokio", 505 | "tower-service", 506 | "tracing", 507 | ] 508 | 509 | [[package]] 510 | name = "icu_collections" 511 | version = "1.5.0" 512 | source = "registry+https://github.com/rust-lang/crates.io-index" 513 | checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" 514 | dependencies = [ 515 | "displaydoc", 516 | "yoke", 517 | "zerofrom", 518 | "zerovec", 519 | ] 520 | 521 | [[package]] 522 | name = "icu_locid" 523 | version = "1.5.0" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" 526 | dependencies = [ 527 | "displaydoc", 528 | "litemap", 529 | "tinystr", 530 | "writeable", 531 | "zerovec", 532 | ] 533 | 534 | [[package]] 535 | name = "icu_locid_transform" 536 | version = "1.5.0" 537 | source = "registry+https://github.com/rust-lang/crates.io-index" 538 | checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" 539 | dependencies = [ 540 | "displaydoc", 541 | "icu_locid", 542 | "icu_locid_transform_data", 543 | "icu_provider", 544 | "tinystr", 545 | "zerovec", 546 | ] 547 | 548 | [[package]] 549 | name = "icu_locid_transform_data" 550 | version = "1.5.1" 551 | source = "registry+https://github.com/rust-lang/crates.io-index" 552 | checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" 553 | 554 | [[package]] 555 | name = "icu_normalizer" 556 | version = "1.5.0" 557 | source = "registry+https://github.com/rust-lang/crates.io-index" 558 | checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" 559 | dependencies = [ 560 | "displaydoc", 561 | "icu_collections", 562 | "icu_normalizer_data", 563 | "icu_properties", 564 | "icu_provider", 565 | "smallvec", 566 | "utf16_iter", 567 | "utf8_iter", 568 | "write16", 569 | "zerovec", 570 | ] 571 | 572 | [[package]] 573 | name = "icu_normalizer_data" 574 | version = "1.5.1" 575 | source = "registry+https://github.com/rust-lang/crates.io-index" 576 | checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" 577 | 578 | [[package]] 579 | name = "icu_properties" 580 | version = "1.5.1" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" 583 | dependencies = [ 584 | "displaydoc", 585 | "icu_collections", 586 | "icu_locid_transform", 587 | "icu_properties_data", 588 | "icu_provider", 589 | "tinystr", 590 | "zerovec", 591 | ] 592 | 593 | [[package]] 594 | name = "icu_properties_data" 595 | version = "1.5.1" 596 | source = "registry+https://github.com/rust-lang/crates.io-index" 597 | checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" 598 | 599 | [[package]] 600 | name = "icu_provider" 601 | version = "1.5.0" 602 | source = "registry+https://github.com/rust-lang/crates.io-index" 603 | checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" 604 | dependencies = [ 605 | "displaydoc", 606 | "icu_locid", 607 | "icu_provider_macros", 608 | "stable_deref_trait", 609 | "tinystr", 610 | "writeable", 611 | "yoke", 612 | "zerofrom", 613 | "zerovec", 614 | ] 615 | 616 | [[package]] 617 | name = "icu_provider_macros" 618 | version = "1.5.0" 619 | source = "registry+https://github.com/rust-lang/crates.io-index" 620 | checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" 621 | dependencies = [ 622 | "proc-macro2", 623 | "quote", 624 | "syn", 625 | ] 626 | 627 | [[package]] 628 | name = "idna" 629 | version = "1.0.3" 630 | source = "registry+https://github.com/rust-lang/crates.io-index" 631 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 632 | dependencies = [ 633 | "idna_adapter", 634 | "smallvec", 635 | "utf8_iter", 636 | ] 637 | 638 | [[package]] 639 | name = "idna_adapter" 640 | version = "1.2.0" 641 | source = "registry+https://github.com/rust-lang/crates.io-index" 642 | checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" 643 | dependencies = [ 644 | "icu_normalizer", 645 | "icu_properties", 646 | ] 647 | 648 | [[package]] 649 | name = "indexmap" 650 | version = "2.9.0" 651 | source = "registry+https://github.com/rust-lang/crates.io-index" 652 | checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" 653 | dependencies = [ 654 | "equivalent", 655 | "hashbrown", 656 | ] 657 | 658 | [[package]] 659 | name = "ipnet" 660 | version = "2.11.0" 661 | source = "registry+https://github.com/rust-lang/crates.io-index" 662 | checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" 663 | 664 | [[package]] 665 | name = "is_terminal_polyfill" 666 | version = "1.70.1" 667 | source = "registry+https://github.com/rust-lang/crates.io-index" 668 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 669 | 670 | [[package]] 671 | name = "itoa" 672 | version = "1.0.15" 673 | source = "registry+https://github.com/rust-lang/crates.io-index" 674 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 675 | 676 | [[package]] 677 | name = "js-sys" 678 | version = "0.3.77" 679 | source = "registry+https://github.com/rust-lang/crates.io-index" 680 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 681 | dependencies = [ 682 | "once_cell", 683 | "wasm-bindgen", 684 | ] 685 | 686 | [[package]] 687 | name = "jsonschema" 688 | version = "0.30.0" 689 | source = "registry+https://github.com/rust-lang/crates.io-index" 690 | checksum = "f1b46a0365a611fbf1d2143104dcf910aada96fafd295bab16c60b802bf6fa1d" 691 | dependencies = [ 692 | "ahash", 693 | "base64", 694 | "bytecount", 695 | "email_address", 696 | "fancy-regex", 697 | "fraction", 698 | "idna", 699 | "itoa", 700 | "num-cmp", 701 | "num-traits", 702 | "once_cell", 703 | "percent-encoding", 704 | "referencing", 705 | "regex", 706 | "regex-syntax", 707 | "reqwest", 708 | "serde", 709 | "serde_json", 710 | "uuid-simd", 711 | ] 712 | 713 | [[package]] 714 | name = "lazy_static" 715 | version = "1.5.0" 716 | source = "registry+https://github.com/rust-lang/crates.io-index" 717 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 718 | 719 | [[package]] 720 | name = "libc" 721 | version = "0.2.172" 722 | source = "registry+https://github.com/rust-lang/crates.io-index" 723 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 724 | 725 | [[package]] 726 | name = "litemap" 727 | version = "0.7.5" 728 | source = "registry+https://github.com/rust-lang/crates.io-index" 729 | checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" 730 | 731 | [[package]] 732 | name = "lock_api" 733 | version = "0.4.12" 734 | source = "registry+https://github.com/rust-lang/crates.io-index" 735 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 736 | dependencies = [ 737 | "autocfg", 738 | "scopeguard", 739 | ] 740 | 741 | [[package]] 742 | name = "log" 743 | version = "0.4.27" 744 | source = "registry+https://github.com/rust-lang/crates.io-index" 745 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 746 | 747 | [[package]] 748 | name = "memchr" 749 | version = "2.7.4" 750 | source = "registry+https://github.com/rust-lang/crates.io-index" 751 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 752 | 753 | [[package]] 754 | name = "mime" 755 | version = "0.3.17" 756 | source = "registry+https://github.com/rust-lang/crates.io-index" 757 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 758 | 759 | [[package]] 760 | name = "miniz_oxide" 761 | version = "0.8.8" 762 | source = "registry+https://github.com/rust-lang/crates.io-index" 763 | checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" 764 | dependencies = [ 765 | "adler2", 766 | ] 767 | 768 | [[package]] 769 | name = "mio" 770 | version = "1.0.3" 771 | source = "registry+https://github.com/rust-lang/crates.io-index" 772 | checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 773 | dependencies = [ 774 | "libc", 775 | "wasi", 776 | "windows-sys 0.52.0", 777 | ] 778 | 779 | [[package]] 780 | name = "nix" 781 | version = "0.29.0" 782 | source = "registry+https://github.com/rust-lang/crates.io-index" 783 | checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" 784 | dependencies = [ 785 | "bitflags", 786 | "cfg-if", 787 | "cfg_aliases", 788 | "libc", 789 | ] 790 | 791 | [[package]] 792 | name = "num" 793 | version = "0.4.3" 794 | source = "registry+https://github.com/rust-lang/crates.io-index" 795 | checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" 796 | dependencies = [ 797 | "num-bigint", 798 | "num-complex", 799 | "num-integer", 800 | "num-iter", 801 | "num-rational", 802 | "num-traits", 803 | ] 804 | 805 | [[package]] 806 | name = "num-bigint" 807 | version = "0.4.6" 808 | source = "registry+https://github.com/rust-lang/crates.io-index" 809 | checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" 810 | dependencies = [ 811 | "num-integer", 812 | "num-traits", 813 | ] 814 | 815 | [[package]] 816 | name = "num-cmp" 817 | version = "0.1.0" 818 | source = "registry+https://github.com/rust-lang/crates.io-index" 819 | checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" 820 | 821 | [[package]] 822 | name = "num-complex" 823 | version = "0.4.6" 824 | source = "registry+https://github.com/rust-lang/crates.io-index" 825 | checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" 826 | dependencies = [ 827 | "num-traits", 828 | ] 829 | 830 | [[package]] 831 | name = "num-conv" 832 | version = "0.1.0" 833 | source = "registry+https://github.com/rust-lang/crates.io-index" 834 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 835 | 836 | [[package]] 837 | name = "num-integer" 838 | version = "0.1.46" 839 | source = "registry+https://github.com/rust-lang/crates.io-index" 840 | checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" 841 | dependencies = [ 842 | "num-traits", 843 | ] 844 | 845 | [[package]] 846 | name = "num-iter" 847 | version = "0.1.45" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" 850 | dependencies = [ 851 | "autocfg", 852 | "num-integer", 853 | "num-traits", 854 | ] 855 | 856 | [[package]] 857 | name = "num-rational" 858 | version = "0.4.2" 859 | source = "registry+https://github.com/rust-lang/crates.io-index" 860 | checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" 861 | dependencies = [ 862 | "num-bigint", 863 | "num-integer", 864 | "num-traits", 865 | ] 866 | 867 | [[package]] 868 | name = "num-traits" 869 | version = "0.2.19" 870 | source = "registry+https://github.com/rust-lang/crates.io-index" 871 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 872 | dependencies = [ 873 | "autocfg", 874 | ] 875 | 876 | [[package]] 877 | name = "num_threads" 878 | version = "0.1.7" 879 | source = "registry+https://github.com/rust-lang/crates.io-index" 880 | checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" 881 | dependencies = [ 882 | "libc", 883 | ] 884 | 885 | [[package]] 886 | name = "object" 887 | version = "0.36.7" 888 | source = "registry+https://github.com/rust-lang/crates.io-index" 889 | checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 890 | dependencies = [ 891 | "memchr", 892 | ] 893 | 894 | [[package]] 895 | name = "okey-cli" 896 | version = "0.1.2" 897 | dependencies = [ 898 | "anyhow", 899 | "clap", 900 | "evdev", 901 | "jsonschema", 902 | "log", 903 | "nix", 904 | "ringbuffer", 905 | "serde", 906 | "serde_json", 907 | "serde_yaml", 908 | "simple_logger", 909 | "smallvec", 910 | ] 911 | 912 | [[package]] 913 | name = "once_cell" 914 | version = "1.21.3" 915 | source = "registry+https://github.com/rust-lang/crates.io-index" 916 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 917 | 918 | [[package]] 919 | name = "outref" 920 | version = "0.5.2" 921 | source = "registry+https://github.com/rust-lang/crates.io-index" 922 | checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" 923 | 924 | [[package]] 925 | name = "parking_lot" 926 | version = "0.12.3" 927 | source = "registry+https://github.com/rust-lang/crates.io-index" 928 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 929 | dependencies = [ 930 | "lock_api", 931 | "parking_lot_core", 932 | ] 933 | 934 | [[package]] 935 | name = "parking_lot_core" 936 | version = "0.9.10" 937 | source = "registry+https://github.com/rust-lang/crates.io-index" 938 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 939 | dependencies = [ 940 | "cfg-if", 941 | "libc", 942 | "redox_syscall", 943 | "smallvec", 944 | "windows-targets 0.52.6", 945 | ] 946 | 947 | [[package]] 948 | name = "percent-encoding" 949 | version = "2.3.1" 950 | source = "registry+https://github.com/rust-lang/crates.io-index" 951 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 952 | 953 | [[package]] 954 | name = "pin-project-lite" 955 | version = "0.2.16" 956 | source = "registry+https://github.com/rust-lang/crates.io-index" 957 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 958 | 959 | [[package]] 960 | name = "pin-utils" 961 | version = "0.1.0" 962 | source = "registry+https://github.com/rust-lang/crates.io-index" 963 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 964 | 965 | [[package]] 966 | name = "powerfmt" 967 | version = "0.2.0" 968 | source = "registry+https://github.com/rust-lang/crates.io-index" 969 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 970 | 971 | [[package]] 972 | name = "proc-macro2" 973 | version = "1.0.95" 974 | source = "registry+https://github.com/rust-lang/crates.io-index" 975 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 976 | dependencies = [ 977 | "unicode-ident", 978 | ] 979 | 980 | [[package]] 981 | name = "quote" 982 | version = "1.0.40" 983 | source = "registry+https://github.com/rust-lang/crates.io-index" 984 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 985 | dependencies = [ 986 | "proc-macro2", 987 | ] 988 | 989 | [[package]] 990 | name = "radium" 991 | version = "0.7.0" 992 | source = "registry+https://github.com/rust-lang/crates.io-index" 993 | checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" 994 | 995 | [[package]] 996 | name = "redox_syscall" 997 | version = "0.5.11" 998 | source = "registry+https://github.com/rust-lang/crates.io-index" 999 | checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" 1000 | dependencies = [ 1001 | "bitflags", 1002 | ] 1003 | 1004 | [[package]] 1005 | name = "ref-cast" 1006 | version = "1.0.24" 1007 | source = "registry+https://github.com/rust-lang/crates.io-index" 1008 | checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" 1009 | dependencies = [ 1010 | "ref-cast-impl", 1011 | ] 1012 | 1013 | [[package]] 1014 | name = "ref-cast-impl" 1015 | version = "1.0.24" 1016 | source = "registry+https://github.com/rust-lang/crates.io-index" 1017 | checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" 1018 | dependencies = [ 1019 | "proc-macro2", 1020 | "quote", 1021 | "syn", 1022 | ] 1023 | 1024 | [[package]] 1025 | name = "referencing" 1026 | version = "0.30.0" 1027 | source = "registry+https://github.com/rust-lang/crates.io-index" 1028 | checksum = "c8eff4fa778b5c2a57e85c5f2fe3a709c52f0e60d23146e2151cbef5893f420e" 1029 | dependencies = [ 1030 | "ahash", 1031 | "fluent-uri", 1032 | "once_cell", 1033 | "parking_lot", 1034 | "percent-encoding", 1035 | "serde_json", 1036 | ] 1037 | 1038 | [[package]] 1039 | name = "regex" 1040 | version = "1.11.1" 1041 | source = "registry+https://github.com/rust-lang/crates.io-index" 1042 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 1043 | dependencies = [ 1044 | "aho-corasick", 1045 | "memchr", 1046 | "regex-automata", 1047 | "regex-syntax", 1048 | ] 1049 | 1050 | [[package]] 1051 | name = "regex-automata" 1052 | version = "0.4.9" 1053 | source = "registry+https://github.com/rust-lang/crates.io-index" 1054 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 1055 | dependencies = [ 1056 | "aho-corasick", 1057 | "memchr", 1058 | "regex-syntax", 1059 | ] 1060 | 1061 | [[package]] 1062 | name = "regex-syntax" 1063 | version = "0.8.5" 1064 | source = "registry+https://github.com/rust-lang/crates.io-index" 1065 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 1066 | 1067 | [[package]] 1068 | name = "reqwest" 1069 | version = "0.12.15" 1070 | source = "registry+https://github.com/rust-lang/crates.io-index" 1071 | checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" 1072 | dependencies = [ 1073 | "base64", 1074 | "bytes", 1075 | "futures-channel", 1076 | "futures-core", 1077 | "futures-util", 1078 | "http", 1079 | "http-body", 1080 | "http-body-util", 1081 | "hyper", 1082 | "hyper-util", 1083 | "ipnet", 1084 | "js-sys", 1085 | "log", 1086 | "mime", 1087 | "once_cell", 1088 | "percent-encoding", 1089 | "pin-project-lite", 1090 | "serde", 1091 | "serde_json", 1092 | "serde_urlencoded", 1093 | "sync_wrapper", 1094 | "tokio", 1095 | "tower", 1096 | "tower-service", 1097 | "url", 1098 | "wasm-bindgen", 1099 | "wasm-bindgen-futures", 1100 | "web-sys", 1101 | "windows-registry", 1102 | ] 1103 | 1104 | [[package]] 1105 | name = "ringbuffer" 1106 | version = "0.15.0" 1107 | source = "registry+https://github.com/rust-lang/crates.io-index" 1108 | checksum = "3df6368f71f205ff9c33c076d170dd56ebf68e8161c733c0caa07a7a5509ed53" 1109 | 1110 | [[package]] 1111 | name = "rustc-demangle" 1112 | version = "0.1.24" 1113 | source = "registry+https://github.com/rust-lang/crates.io-index" 1114 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 1115 | 1116 | [[package]] 1117 | name = "rustversion" 1118 | version = "1.0.20" 1119 | source = "registry+https://github.com/rust-lang/crates.io-index" 1120 | checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" 1121 | 1122 | [[package]] 1123 | name = "ryu" 1124 | version = "1.0.20" 1125 | source = "registry+https://github.com/rust-lang/crates.io-index" 1126 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 1127 | 1128 | [[package]] 1129 | name = "scopeguard" 1130 | version = "1.2.0" 1131 | source = "registry+https://github.com/rust-lang/crates.io-index" 1132 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1133 | 1134 | [[package]] 1135 | name = "serde" 1136 | version = "1.0.219" 1137 | source = "registry+https://github.com/rust-lang/crates.io-index" 1138 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 1139 | dependencies = [ 1140 | "serde_derive", 1141 | ] 1142 | 1143 | [[package]] 1144 | name = "serde_derive" 1145 | version = "1.0.219" 1146 | source = "registry+https://github.com/rust-lang/crates.io-index" 1147 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 1148 | dependencies = [ 1149 | "proc-macro2", 1150 | "quote", 1151 | "syn", 1152 | ] 1153 | 1154 | [[package]] 1155 | name = "serde_json" 1156 | version = "1.0.140" 1157 | source = "registry+https://github.com/rust-lang/crates.io-index" 1158 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 1159 | dependencies = [ 1160 | "itoa", 1161 | "memchr", 1162 | "ryu", 1163 | "serde", 1164 | ] 1165 | 1166 | [[package]] 1167 | name = "serde_urlencoded" 1168 | version = "0.7.1" 1169 | source = "registry+https://github.com/rust-lang/crates.io-index" 1170 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1171 | dependencies = [ 1172 | "form_urlencoded", 1173 | "itoa", 1174 | "ryu", 1175 | "serde", 1176 | ] 1177 | 1178 | [[package]] 1179 | name = "serde_yaml" 1180 | version = "0.9.34+deprecated" 1181 | source = "registry+https://github.com/rust-lang/crates.io-index" 1182 | checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" 1183 | dependencies = [ 1184 | "indexmap", 1185 | "itoa", 1186 | "ryu", 1187 | "serde", 1188 | "unsafe-libyaml", 1189 | ] 1190 | 1191 | [[package]] 1192 | name = "simple_logger" 1193 | version = "5.0.0" 1194 | source = "registry+https://github.com/rust-lang/crates.io-index" 1195 | checksum = "e8c5dfa5e08767553704aa0ffd9d9794d527103c736aba9854773851fd7497eb" 1196 | dependencies = [ 1197 | "colored", 1198 | "log", 1199 | "time", 1200 | "windows-sys 0.48.0", 1201 | ] 1202 | 1203 | [[package]] 1204 | name = "slab" 1205 | version = "0.4.9" 1206 | source = "registry+https://github.com/rust-lang/crates.io-index" 1207 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 1208 | dependencies = [ 1209 | "autocfg", 1210 | ] 1211 | 1212 | [[package]] 1213 | name = "smallvec" 1214 | version = "1.15.0" 1215 | source = "registry+https://github.com/rust-lang/crates.io-index" 1216 | checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" 1217 | 1218 | [[package]] 1219 | name = "socket2" 1220 | version = "0.5.9" 1221 | source = "registry+https://github.com/rust-lang/crates.io-index" 1222 | checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" 1223 | dependencies = [ 1224 | "libc", 1225 | "windows-sys 0.52.0", 1226 | ] 1227 | 1228 | [[package]] 1229 | name = "stable_deref_trait" 1230 | version = "1.2.0" 1231 | source = "registry+https://github.com/rust-lang/crates.io-index" 1232 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 1233 | 1234 | [[package]] 1235 | name = "strsim" 1236 | version = "0.11.1" 1237 | source = "registry+https://github.com/rust-lang/crates.io-index" 1238 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1239 | 1240 | [[package]] 1241 | name = "syn" 1242 | version = "2.0.100" 1243 | source = "registry+https://github.com/rust-lang/crates.io-index" 1244 | checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" 1245 | dependencies = [ 1246 | "proc-macro2", 1247 | "quote", 1248 | "unicode-ident", 1249 | ] 1250 | 1251 | [[package]] 1252 | name = "sync_wrapper" 1253 | version = "1.0.2" 1254 | source = "registry+https://github.com/rust-lang/crates.io-index" 1255 | checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 1256 | dependencies = [ 1257 | "futures-core", 1258 | ] 1259 | 1260 | [[package]] 1261 | name = "synstructure" 1262 | version = "0.13.1" 1263 | source = "registry+https://github.com/rust-lang/crates.io-index" 1264 | checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" 1265 | dependencies = [ 1266 | "proc-macro2", 1267 | "quote", 1268 | "syn", 1269 | ] 1270 | 1271 | [[package]] 1272 | name = "tap" 1273 | version = "1.0.1" 1274 | source = "registry+https://github.com/rust-lang/crates.io-index" 1275 | checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" 1276 | 1277 | [[package]] 1278 | name = "time" 1279 | version = "0.3.41" 1280 | source = "registry+https://github.com/rust-lang/crates.io-index" 1281 | checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" 1282 | dependencies = [ 1283 | "deranged", 1284 | "itoa", 1285 | "libc", 1286 | "num-conv", 1287 | "num_threads", 1288 | "powerfmt", 1289 | "serde", 1290 | "time-core", 1291 | "time-macros", 1292 | ] 1293 | 1294 | [[package]] 1295 | name = "time-core" 1296 | version = "0.1.4" 1297 | source = "registry+https://github.com/rust-lang/crates.io-index" 1298 | checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" 1299 | 1300 | [[package]] 1301 | name = "time-macros" 1302 | version = "0.2.22" 1303 | source = "registry+https://github.com/rust-lang/crates.io-index" 1304 | checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" 1305 | dependencies = [ 1306 | "num-conv", 1307 | "time-core", 1308 | ] 1309 | 1310 | [[package]] 1311 | name = "tinystr" 1312 | version = "0.7.6" 1313 | source = "registry+https://github.com/rust-lang/crates.io-index" 1314 | checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" 1315 | dependencies = [ 1316 | "displaydoc", 1317 | "zerovec", 1318 | ] 1319 | 1320 | [[package]] 1321 | name = "tokio" 1322 | version = "1.44.2" 1323 | source = "registry+https://github.com/rust-lang/crates.io-index" 1324 | checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" 1325 | dependencies = [ 1326 | "backtrace", 1327 | "libc", 1328 | "mio", 1329 | "pin-project-lite", 1330 | "socket2", 1331 | "windows-sys 0.52.0", 1332 | ] 1333 | 1334 | [[package]] 1335 | name = "tower" 1336 | version = "0.5.2" 1337 | source = "registry+https://github.com/rust-lang/crates.io-index" 1338 | checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" 1339 | dependencies = [ 1340 | "futures-core", 1341 | "futures-util", 1342 | "pin-project-lite", 1343 | "sync_wrapper", 1344 | "tokio", 1345 | "tower-layer", 1346 | "tower-service", 1347 | ] 1348 | 1349 | [[package]] 1350 | name = "tower-layer" 1351 | version = "0.3.3" 1352 | source = "registry+https://github.com/rust-lang/crates.io-index" 1353 | checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 1354 | 1355 | [[package]] 1356 | name = "tower-service" 1357 | version = "0.3.3" 1358 | source = "registry+https://github.com/rust-lang/crates.io-index" 1359 | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 1360 | 1361 | [[package]] 1362 | name = "tracing" 1363 | version = "0.1.41" 1364 | source = "registry+https://github.com/rust-lang/crates.io-index" 1365 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 1366 | dependencies = [ 1367 | "pin-project-lite", 1368 | "tracing-core", 1369 | ] 1370 | 1371 | [[package]] 1372 | name = "tracing-core" 1373 | version = "0.1.33" 1374 | source = "registry+https://github.com/rust-lang/crates.io-index" 1375 | checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" 1376 | dependencies = [ 1377 | "once_cell", 1378 | ] 1379 | 1380 | [[package]] 1381 | name = "try-lock" 1382 | version = "0.2.5" 1383 | source = "registry+https://github.com/rust-lang/crates.io-index" 1384 | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 1385 | 1386 | [[package]] 1387 | name = "unicode-ident" 1388 | version = "1.0.18" 1389 | source = "registry+https://github.com/rust-lang/crates.io-index" 1390 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 1391 | 1392 | [[package]] 1393 | name = "unsafe-libyaml" 1394 | version = "0.2.11" 1395 | source = "registry+https://github.com/rust-lang/crates.io-index" 1396 | checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" 1397 | 1398 | [[package]] 1399 | name = "url" 1400 | version = "2.5.4" 1401 | source = "registry+https://github.com/rust-lang/crates.io-index" 1402 | checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 1403 | dependencies = [ 1404 | "form_urlencoded", 1405 | "idna", 1406 | "percent-encoding", 1407 | ] 1408 | 1409 | [[package]] 1410 | name = "utf16_iter" 1411 | version = "1.0.5" 1412 | source = "registry+https://github.com/rust-lang/crates.io-index" 1413 | checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" 1414 | 1415 | [[package]] 1416 | name = "utf8_iter" 1417 | version = "1.0.4" 1418 | source = "registry+https://github.com/rust-lang/crates.io-index" 1419 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 1420 | 1421 | [[package]] 1422 | name = "utf8parse" 1423 | version = "0.2.2" 1424 | source = "registry+https://github.com/rust-lang/crates.io-index" 1425 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1426 | 1427 | [[package]] 1428 | name = "uuid" 1429 | version = "1.16.0" 1430 | source = "registry+https://github.com/rust-lang/crates.io-index" 1431 | checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" 1432 | 1433 | [[package]] 1434 | name = "uuid-simd" 1435 | version = "0.8.0" 1436 | source = "registry+https://github.com/rust-lang/crates.io-index" 1437 | checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8" 1438 | dependencies = [ 1439 | "outref", 1440 | "uuid", 1441 | "vsimd", 1442 | ] 1443 | 1444 | [[package]] 1445 | name = "version_check" 1446 | version = "0.9.5" 1447 | source = "registry+https://github.com/rust-lang/crates.io-index" 1448 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 1449 | 1450 | [[package]] 1451 | name = "vsimd" 1452 | version = "0.8.0" 1453 | source = "registry+https://github.com/rust-lang/crates.io-index" 1454 | checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" 1455 | 1456 | [[package]] 1457 | name = "want" 1458 | version = "0.3.1" 1459 | source = "registry+https://github.com/rust-lang/crates.io-index" 1460 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 1461 | dependencies = [ 1462 | "try-lock", 1463 | ] 1464 | 1465 | [[package]] 1466 | name = "wasi" 1467 | version = "0.11.0+wasi-snapshot-preview1" 1468 | source = "registry+https://github.com/rust-lang/crates.io-index" 1469 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1470 | 1471 | [[package]] 1472 | name = "wasm-bindgen" 1473 | version = "0.2.100" 1474 | source = "registry+https://github.com/rust-lang/crates.io-index" 1475 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 1476 | dependencies = [ 1477 | "cfg-if", 1478 | "once_cell", 1479 | "rustversion", 1480 | "wasm-bindgen-macro", 1481 | ] 1482 | 1483 | [[package]] 1484 | name = "wasm-bindgen-backend" 1485 | version = "0.2.100" 1486 | source = "registry+https://github.com/rust-lang/crates.io-index" 1487 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 1488 | dependencies = [ 1489 | "bumpalo", 1490 | "log", 1491 | "proc-macro2", 1492 | "quote", 1493 | "syn", 1494 | "wasm-bindgen-shared", 1495 | ] 1496 | 1497 | [[package]] 1498 | name = "wasm-bindgen-futures" 1499 | version = "0.4.50" 1500 | source = "registry+https://github.com/rust-lang/crates.io-index" 1501 | checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" 1502 | dependencies = [ 1503 | "cfg-if", 1504 | "js-sys", 1505 | "once_cell", 1506 | "wasm-bindgen", 1507 | "web-sys", 1508 | ] 1509 | 1510 | [[package]] 1511 | name = "wasm-bindgen-macro" 1512 | version = "0.2.100" 1513 | source = "registry+https://github.com/rust-lang/crates.io-index" 1514 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 1515 | dependencies = [ 1516 | "quote", 1517 | "wasm-bindgen-macro-support", 1518 | ] 1519 | 1520 | [[package]] 1521 | name = "wasm-bindgen-macro-support" 1522 | version = "0.2.100" 1523 | source = "registry+https://github.com/rust-lang/crates.io-index" 1524 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 1525 | dependencies = [ 1526 | "proc-macro2", 1527 | "quote", 1528 | "syn", 1529 | "wasm-bindgen-backend", 1530 | "wasm-bindgen-shared", 1531 | ] 1532 | 1533 | [[package]] 1534 | name = "wasm-bindgen-shared" 1535 | version = "0.2.100" 1536 | source = "registry+https://github.com/rust-lang/crates.io-index" 1537 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 1538 | dependencies = [ 1539 | "unicode-ident", 1540 | ] 1541 | 1542 | [[package]] 1543 | name = "web-sys" 1544 | version = "0.3.77" 1545 | source = "registry+https://github.com/rust-lang/crates.io-index" 1546 | checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" 1547 | dependencies = [ 1548 | "js-sys", 1549 | "wasm-bindgen", 1550 | ] 1551 | 1552 | [[package]] 1553 | name = "windows-link" 1554 | version = "0.1.1" 1555 | source = "registry+https://github.com/rust-lang/crates.io-index" 1556 | checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" 1557 | 1558 | [[package]] 1559 | name = "windows-registry" 1560 | version = "0.4.0" 1561 | source = "registry+https://github.com/rust-lang/crates.io-index" 1562 | checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" 1563 | dependencies = [ 1564 | "windows-result", 1565 | "windows-strings", 1566 | "windows-targets 0.53.0", 1567 | ] 1568 | 1569 | [[package]] 1570 | name = "windows-result" 1571 | version = "0.3.2" 1572 | source = "registry+https://github.com/rust-lang/crates.io-index" 1573 | checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" 1574 | dependencies = [ 1575 | "windows-link", 1576 | ] 1577 | 1578 | [[package]] 1579 | name = "windows-strings" 1580 | version = "0.3.1" 1581 | source = "registry+https://github.com/rust-lang/crates.io-index" 1582 | checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" 1583 | dependencies = [ 1584 | "windows-link", 1585 | ] 1586 | 1587 | [[package]] 1588 | name = "windows-sys" 1589 | version = "0.48.0" 1590 | source = "registry+https://github.com/rust-lang/crates.io-index" 1591 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1592 | dependencies = [ 1593 | "windows-targets 0.48.5", 1594 | ] 1595 | 1596 | [[package]] 1597 | name = "windows-sys" 1598 | version = "0.52.0" 1599 | source = "registry+https://github.com/rust-lang/crates.io-index" 1600 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1601 | dependencies = [ 1602 | "windows-targets 0.52.6", 1603 | ] 1604 | 1605 | [[package]] 1606 | name = "windows-sys" 1607 | version = "0.59.0" 1608 | source = "registry+https://github.com/rust-lang/crates.io-index" 1609 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1610 | dependencies = [ 1611 | "windows-targets 0.52.6", 1612 | ] 1613 | 1614 | [[package]] 1615 | name = "windows-targets" 1616 | version = "0.48.5" 1617 | source = "registry+https://github.com/rust-lang/crates.io-index" 1618 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1619 | dependencies = [ 1620 | "windows_aarch64_gnullvm 0.48.5", 1621 | "windows_aarch64_msvc 0.48.5", 1622 | "windows_i686_gnu 0.48.5", 1623 | "windows_i686_msvc 0.48.5", 1624 | "windows_x86_64_gnu 0.48.5", 1625 | "windows_x86_64_gnullvm 0.48.5", 1626 | "windows_x86_64_msvc 0.48.5", 1627 | ] 1628 | 1629 | [[package]] 1630 | name = "windows-targets" 1631 | version = "0.52.6" 1632 | source = "registry+https://github.com/rust-lang/crates.io-index" 1633 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1634 | dependencies = [ 1635 | "windows_aarch64_gnullvm 0.52.6", 1636 | "windows_aarch64_msvc 0.52.6", 1637 | "windows_i686_gnu 0.52.6", 1638 | "windows_i686_gnullvm 0.52.6", 1639 | "windows_i686_msvc 0.52.6", 1640 | "windows_x86_64_gnu 0.52.6", 1641 | "windows_x86_64_gnullvm 0.52.6", 1642 | "windows_x86_64_msvc 0.52.6", 1643 | ] 1644 | 1645 | [[package]] 1646 | name = "windows-targets" 1647 | version = "0.53.0" 1648 | source = "registry+https://github.com/rust-lang/crates.io-index" 1649 | checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" 1650 | dependencies = [ 1651 | "windows_aarch64_gnullvm 0.53.0", 1652 | "windows_aarch64_msvc 0.53.0", 1653 | "windows_i686_gnu 0.53.0", 1654 | "windows_i686_gnullvm 0.53.0", 1655 | "windows_i686_msvc 0.53.0", 1656 | "windows_x86_64_gnu 0.53.0", 1657 | "windows_x86_64_gnullvm 0.53.0", 1658 | "windows_x86_64_msvc 0.53.0", 1659 | ] 1660 | 1661 | [[package]] 1662 | name = "windows_aarch64_gnullvm" 1663 | version = "0.48.5" 1664 | source = "registry+https://github.com/rust-lang/crates.io-index" 1665 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1666 | 1667 | [[package]] 1668 | name = "windows_aarch64_gnullvm" 1669 | version = "0.52.6" 1670 | source = "registry+https://github.com/rust-lang/crates.io-index" 1671 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1672 | 1673 | [[package]] 1674 | name = "windows_aarch64_gnullvm" 1675 | version = "0.53.0" 1676 | source = "registry+https://github.com/rust-lang/crates.io-index" 1677 | checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" 1678 | 1679 | [[package]] 1680 | name = "windows_aarch64_msvc" 1681 | version = "0.48.5" 1682 | source = "registry+https://github.com/rust-lang/crates.io-index" 1683 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1684 | 1685 | [[package]] 1686 | name = "windows_aarch64_msvc" 1687 | version = "0.52.6" 1688 | source = "registry+https://github.com/rust-lang/crates.io-index" 1689 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1690 | 1691 | [[package]] 1692 | name = "windows_aarch64_msvc" 1693 | version = "0.53.0" 1694 | source = "registry+https://github.com/rust-lang/crates.io-index" 1695 | checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" 1696 | 1697 | [[package]] 1698 | name = "windows_i686_gnu" 1699 | version = "0.48.5" 1700 | source = "registry+https://github.com/rust-lang/crates.io-index" 1701 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1702 | 1703 | [[package]] 1704 | name = "windows_i686_gnu" 1705 | version = "0.52.6" 1706 | source = "registry+https://github.com/rust-lang/crates.io-index" 1707 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1708 | 1709 | [[package]] 1710 | name = "windows_i686_gnu" 1711 | version = "0.53.0" 1712 | source = "registry+https://github.com/rust-lang/crates.io-index" 1713 | checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" 1714 | 1715 | [[package]] 1716 | name = "windows_i686_gnullvm" 1717 | version = "0.52.6" 1718 | source = "registry+https://github.com/rust-lang/crates.io-index" 1719 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1720 | 1721 | [[package]] 1722 | name = "windows_i686_gnullvm" 1723 | version = "0.53.0" 1724 | source = "registry+https://github.com/rust-lang/crates.io-index" 1725 | checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" 1726 | 1727 | [[package]] 1728 | name = "windows_i686_msvc" 1729 | version = "0.48.5" 1730 | source = "registry+https://github.com/rust-lang/crates.io-index" 1731 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1732 | 1733 | [[package]] 1734 | name = "windows_i686_msvc" 1735 | version = "0.52.6" 1736 | source = "registry+https://github.com/rust-lang/crates.io-index" 1737 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1738 | 1739 | [[package]] 1740 | name = "windows_i686_msvc" 1741 | version = "0.53.0" 1742 | source = "registry+https://github.com/rust-lang/crates.io-index" 1743 | checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" 1744 | 1745 | [[package]] 1746 | name = "windows_x86_64_gnu" 1747 | version = "0.48.5" 1748 | source = "registry+https://github.com/rust-lang/crates.io-index" 1749 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1750 | 1751 | [[package]] 1752 | name = "windows_x86_64_gnu" 1753 | version = "0.52.6" 1754 | source = "registry+https://github.com/rust-lang/crates.io-index" 1755 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1756 | 1757 | [[package]] 1758 | name = "windows_x86_64_gnu" 1759 | version = "0.53.0" 1760 | source = "registry+https://github.com/rust-lang/crates.io-index" 1761 | checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" 1762 | 1763 | [[package]] 1764 | name = "windows_x86_64_gnullvm" 1765 | version = "0.48.5" 1766 | source = "registry+https://github.com/rust-lang/crates.io-index" 1767 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1768 | 1769 | [[package]] 1770 | name = "windows_x86_64_gnullvm" 1771 | version = "0.52.6" 1772 | source = "registry+https://github.com/rust-lang/crates.io-index" 1773 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1774 | 1775 | [[package]] 1776 | name = "windows_x86_64_gnullvm" 1777 | version = "0.53.0" 1778 | source = "registry+https://github.com/rust-lang/crates.io-index" 1779 | checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" 1780 | 1781 | [[package]] 1782 | name = "windows_x86_64_msvc" 1783 | version = "0.48.5" 1784 | source = "registry+https://github.com/rust-lang/crates.io-index" 1785 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1786 | 1787 | [[package]] 1788 | name = "windows_x86_64_msvc" 1789 | version = "0.52.6" 1790 | source = "registry+https://github.com/rust-lang/crates.io-index" 1791 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1792 | 1793 | [[package]] 1794 | name = "windows_x86_64_msvc" 1795 | version = "0.53.0" 1796 | source = "registry+https://github.com/rust-lang/crates.io-index" 1797 | checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" 1798 | 1799 | [[package]] 1800 | name = "write16" 1801 | version = "1.0.0" 1802 | source = "registry+https://github.com/rust-lang/crates.io-index" 1803 | checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" 1804 | 1805 | [[package]] 1806 | name = "writeable" 1807 | version = "0.5.5" 1808 | source = "registry+https://github.com/rust-lang/crates.io-index" 1809 | checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" 1810 | 1811 | [[package]] 1812 | name = "wyz" 1813 | version = "0.5.1" 1814 | source = "registry+https://github.com/rust-lang/crates.io-index" 1815 | checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" 1816 | dependencies = [ 1817 | "tap", 1818 | ] 1819 | 1820 | [[package]] 1821 | name = "yoke" 1822 | version = "0.7.5" 1823 | source = "registry+https://github.com/rust-lang/crates.io-index" 1824 | checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" 1825 | dependencies = [ 1826 | "serde", 1827 | "stable_deref_trait", 1828 | "yoke-derive", 1829 | "zerofrom", 1830 | ] 1831 | 1832 | [[package]] 1833 | name = "yoke-derive" 1834 | version = "0.7.5" 1835 | source = "registry+https://github.com/rust-lang/crates.io-index" 1836 | checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" 1837 | dependencies = [ 1838 | "proc-macro2", 1839 | "quote", 1840 | "syn", 1841 | "synstructure", 1842 | ] 1843 | 1844 | [[package]] 1845 | name = "zerocopy" 1846 | version = "0.7.35" 1847 | source = "registry+https://github.com/rust-lang/crates.io-index" 1848 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 1849 | dependencies = [ 1850 | "zerocopy-derive", 1851 | ] 1852 | 1853 | [[package]] 1854 | name = "zerocopy-derive" 1855 | version = "0.7.35" 1856 | source = "registry+https://github.com/rust-lang/crates.io-index" 1857 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 1858 | dependencies = [ 1859 | "proc-macro2", 1860 | "quote", 1861 | "syn", 1862 | ] 1863 | 1864 | [[package]] 1865 | name = "zerofrom" 1866 | version = "0.1.6" 1867 | source = "registry+https://github.com/rust-lang/crates.io-index" 1868 | checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 1869 | dependencies = [ 1870 | "zerofrom-derive", 1871 | ] 1872 | 1873 | [[package]] 1874 | name = "zerofrom-derive" 1875 | version = "0.1.6" 1876 | source = "registry+https://github.com/rust-lang/crates.io-index" 1877 | checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 1878 | dependencies = [ 1879 | "proc-macro2", 1880 | "quote", 1881 | "syn", 1882 | "synstructure", 1883 | ] 1884 | 1885 | [[package]] 1886 | name = "zerovec" 1887 | version = "0.10.4" 1888 | source = "registry+https://github.com/rust-lang/crates.io-index" 1889 | checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" 1890 | dependencies = [ 1891 | "yoke", 1892 | "zerofrom", 1893 | "zerovec-derive", 1894 | ] 1895 | 1896 | [[package]] 1897 | name = "zerovec-derive" 1898 | version = "0.10.3" 1899 | source = "registry+https://github.com/rust-lang/crates.io-index" 1900 | checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" 1901 | dependencies = [ 1902 | "proc-macro2", 1903 | "quote", 1904 | "syn", 1905 | ] 1906 | --------------------------------------------------------------------------------