From c49a9b5cdecf853e222cdc2d4f0de0a0d7d78041 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Fri, 31 Dec 2021 21:43:16 -0800 Subject: [PATCH] Implement mark --- README.md | 5 +++ example/emacs.yml | 60 +++++++++++++++++++++++++ src/config/action.rs | 30 +++++++++++++ src/config/tests.rs | 12 +++++ src/device.rs | 4 +- src/event_handler.rs | 101 +++++++++++++++++++++++++------------------ 6 files changed, 168 insertions(+), 44 deletions(-) create mode 100644 example/emacs.yml diff --git a/README.md b/README.md index 6691ff5..8224ac9 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ * Remap a key to two different keys depending on whether it's pressed alone or held. * Application-specific remapping. Even if it's not supported by your application, xremap can. * Automatically remap newly connected devices by starting xremap with `--watch`. +* Support [Emacs-like key remapping](example/emacs.yml), including the mark mode. ## Prerequisite @@ -146,6 +147,10 @@ keymap: # execute a command MOD1-KEY_XXX: launch: ["bash", "-c", "echo hello > /tmp/test"] + # let `with_mark` also press a Shift key (useful for Emacs emulation) + MOD1-KEY_XXX: { set_mark: true } # use { set_mark: false } to disable it + # also press Shift only when { set_mark: true } is used before + MOD1-KEY_XXX: { with_mark: MOD2-KEY_YYY } application: # Optional not: [Application, ...] # or diff --git a/example/emacs.yml b/example/emacs.yml new file mode 100644 index 0000000..2a4201e --- /dev/null +++ b/example/emacs.yml @@ -0,0 +1,60 @@ +# Credit: https://github.com/mooz/xkeysnail/blob/bf3c93b4fe6efd42893db4e6588e5ef1c4909cfb/example/config.py#L62-L125 +keymap: + - name: Emacs + remap: + # Cursor + C-b: { with_mark: left } + C-f: { with_mark: right } + C-p: { with_mark: up } + C-n: { with_mark: down } + # Forward/Backward word + M-b: { with_mark: C-left } + M-f: { with_mark: C-right } + # Beginning/End of line + C-a: { with_mark: home } + C-e: { with_mark: end } + # Page up/down + M-v: { with_mark: pageup } + C-v: { with_mark: pagedown } + # Beginning/End of file + M-Shift-comma: { with_mark: C-home } + M-Shift-dot: { with_mark: C-end } + # Newline + C-m: enter + C-j: enter + C-o: [enter, left] + # Copy + C-w: [C-x, { set_mark: false }] + M-w: [C-c, { set_mark: false }] + C-y: [C-v, { set_mark: false }] + # Delete + C-d: [delete, { set_mark: false }] + M-d: [C-delete, { set_mark: false }] + # Kill line + C-k: [Shift-end, C-x, { set_mark: false }] + # Undo + C-slash: [C-z, { set_mark: false }] + C-Shift-ro: C-z + # Mark + C-space: { set_mark: true } + # Search + C-s: F3 + C-r: Shift-F3 + M-Shift-5: C-h + # Cancel + C-g: [esc, { set_mark: false }] + # C-x YYY + C-x: + remap: + # C-x h (select all) + h: [C-home, C-a, { set_mark: true }] + # C-x C-f (open) + C-f: C-o + # C-x C-s (save) + C-s: C-s + # C-x k (kill tab) + k: C-f4 + # C-x C-c (exit) + C-c: C-q + # C-x u (undo) + u: [C-z, { set_mark: false }] diff --git a/src/config/action.rs b/src/config/action.rs index e343ece..cbf6c7a 100644 --- a/src/config/action.rs +++ b/src/config/action.rs @@ -14,6 +14,10 @@ pub enum Action { Remap(HashMap>), #[serde(deserialize_with = "deserialize_launch")] Launch(Vec), + #[serde(deserialize_with = "deserialize_with_mark")] + WithMark(KeyPress), + #[serde(deserialize_with = "deserialize_set_mark")] + SetMark(bool), } fn deserialize_remap<'de, D>(deserializer: D) -> Result>, D::Error> @@ -42,6 +46,32 @@ where Err(de::Error::custom("not a map with a single \"launch\" key")) } +fn deserialize_with_mark<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let mut action = HashMap::::deserialize(deserializer)?; + if let Some(key_press) = action.remove("with_mark") { + if action.is_empty() { + return Ok(key_press); + } + } + Err(de::Error::custom("not a map with a single \"with_mark\" key")) +} + +fn deserialize_set_mark<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let mut action = HashMap::::deserialize(deserializer)?; + if let Some(set) = action.remove("set_mark") { + if action.is_empty() { + return Ok(set); + } + } + Err(de::Error::custom("not a map with a single \"set_mark\" key")) +} + // Used only for deserializing Vec #[derive(Deserialize)] #[serde(untagged)] diff --git a/src/config/tests.rs b/src/config/tests.rs index a0706b8..7b2cdf5 100644 --- a/src/config/tests.rs +++ b/src/config/tests.rs @@ -114,6 +114,18 @@ fn test_keymap_launch() { "#}) } +#[test] +fn test_keymap_mark() { + assert_parse(indoc! {" + keymap: + - remap: + C-space: { set_mark: true } + C-g: [esc, { set_mark: false }] + C-b: { with_mark: left } + M-b: { with_mark: C-left } + "}) +} + fn assert_parse(yaml: &str) { let result: Result = serde_yaml::from_str(&yaml); if let Err(e) = result { diff --git a/src/device.rs b/src/device.rs index 11e086b..e747393 100644 --- a/src/device.rs +++ b/src/device.rs @@ -26,7 +26,7 @@ static MOUSE_BTNS: [&str; 13] = [ "BTN_RIGHT", ]; -// Credit: https://github.com/mooz/xkeysnail/blob/master/xkeysnail/output.py#L10-L32 +// Credit: https://github.com/mooz/xkeysnail/blob/bf3c93b4fe6efd42893db4e6588e5ef1c4909cfb/xkeysnail/output.py#L10-L32 pub fn output_device() -> Result> { let mut keys: AttributeSet = AttributeSet::new(); for code in Key::KEY_RESERVED.code()..Key::BTN_TRIGGER_HAPPY40.code() { @@ -183,7 +183,7 @@ fn match_device(path: &str, device: &Device, device_opts: &Vec) -> bool } fn is_keyboard(device: &Device) -> bool { - // Credit: https://github.com/mooz/xkeysnail/blob/master/xkeysnail/input.py#L17-L32 + // Credit: https://github.com/mooz/xkeysnail/blob/bf3c93b4fe6efd42893db4e6588e5ef1c4909cfb/xkeysnail/input.py#L17-L32 match device.supported_keys() { Some(keys) => { keys.contains(Key::KEY_SPACE) diff --git a/src/event_handler.rs b/src/event_handler.rs index 60beb47..4485bb9 100644 --- a/src/event_handler.rs +++ b/src/event_handler.rs @@ -21,11 +21,12 @@ pub struct EventHandler { control: PressState, alt: PressState, windows: PressState, - wm_client: WMClient, + application_client: WMClient, application_cache: Option, multi_purpose_keys: HashMap, override_remap: Option>>, sigaction_set: bool, + mark_set: bool, } impl EventHandler { @@ -36,11 +37,12 @@ impl EventHandler { control: PressState::new(false), alt: PressState::new(false), windows: PressState::new(false), - wm_client: build_client(), + application_client: build_client(), application_cache: None, multi_purpose_keys: HashMap::new(), override_remap: None, sigaction_set: false, + mark_set: false, } } @@ -187,25 +189,7 @@ impl EventHandler { fn dispatch_action(&mut self, action: &Action) -> Result<(), Box> { match action { - Action::KeyPress(key_press) => { - let next_shift = self.build_state(Modifier::Shift, key_press.shift); - let next_control = self.build_state(Modifier::Control, key_press.control); - let next_alt = self.build_state(Modifier::Alt, key_press.alt); - let next_windows = self.build_state(Modifier::Windows, key_press.windows); - - let prev_shift = self.send_modifier(Modifier::Shift, &next_shift)?; - let prev_control = self.send_modifier(Modifier::Control, &next_control)?; - let prev_alt = self.send_modifier(Modifier::Alt, &next_alt)?; - let prev_windows = self.send_modifier(Modifier::Windows, &next_windows)?; - - self.send_key(&key_press.key, PRESS)?; - self.send_key(&key_press.key, RELEASE)?; - - self.send_modifier(Modifier::Windows, &prev_windows)?; - self.send_modifier(Modifier::Alt, &prev_alt)?; - self.send_modifier(Modifier::Control, &prev_control)?; - self.send_modifier(Modifier::Shift, &prev_shift)?; - } + Action::KeyPress(key_press) => self.send_key_press(key_press)?, Action::Remap(remap) => { let mut override_remap: HashMap> = HashMap::new(); for (key_press, actions) in remap.iter() { @@ -214,31 +198,31 @@ impl EventHandler { self.override_remap = Some(override_remap) } Action::Launch(command) => self.run_command(command.clone()), + Action::WithMark(key_press) => self.send_key_press(&self.with_mark(key_press))?, + Action::SetMark(set) => self.mark_set = *set, } Ok(()) } - fn run_command(&mut self, command: Vec) { - if !self.sigaction_set { - // Avoid defunct processes - let sig_action = SigAction::new(SigHandler::SigDfl, SaFlags::SA_NOCLDWAIT, SigSet::empty()); - unsafe { - sigaction(signal::SIGCHLD, &sig_action).expect("Failed to register SIGCHLD handler"); - } - self.sigaction_set = true; - } + fn send_key_press(&mut self, key_press: &KeyPress) -> Result<(), Box> { + let next_shift = self.build_state(Modifier::Shift, key_press.shift); + let next_control = self.build_state(Modifier::Control, key_press.control); + let next_alt = self.build_state(Modifier::Alt, key_press.alt); + let next_windows = self.build_state(Modifier::Windows, key_press.windows); - debug!("Running command: {:?}", command); - match Command::new(&command[0]) - .args(&command[1..]) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn() - { - Ok(child) => debug!("Process spawned: {:?}, pid {}", command, child.id()), - Err(e) => error!("Error running command: {:?}", e), - } + let prev_shift = self.send_modifier(Modifier::Shift, &next_shift)?; + let prev_control = self.send_modifier(Modifier::Control, &next_control)?; + let prev_alt = self.send_modifier(Modifier::Alt, &next_alt)?; + let prev_windows = self.send_modifier(Modifier::Windows, &next_windows)?; + + self.send_key(&key_press.key, PRESS)?; + self.send_key(&key_press.key, RELEASE)?; + + self.send_modifier(Modifier::Windows, &prev_windows)?; + self.send_modifier(Modifier::Alt, &prev_alt)?; + self.send_modifier(Modifier::Control, &prev_control)?; + self.send_modifier(Modifier::Shift, &prev_shift)?; + Ok(()) } fn send_modifier(&mut self, modifier: Modifier, desired: &PressState) -> Result> { @@ -310,10 +294,43 @@ impl EventHandler { } } + fn with_mark(&self, key_press: &KeyPress) -> KeyPress { + KeyPress { + key: key_press.key.clone(), + shift: key_press.shift || self.mark_set, + control: key_press.shift, + alt: key_press.alt, + windows: key_press.windows, + } + } + + fn run_command(&mut self, command: Vec) { + if !self.sigaction_set { + // Avoid defunct processes + let sig_action = SigAction::new(SigHandler::SigDfl, SaFlags::SA_NOCLDWAIT, SigSet::empty()); + unsafe { + sigaction(signal::SIGCHLD, &sig_action).expect("Failed to register SIGCHLD handler"); + } + self.sigaction_set = true; + } + + debug!("Running command: {:?}", command); + match Command::new(&command[0]) + .args(&command[1..]) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + { + Ok(child) => debug!("Process spawned: {:?}, pid {}", command, child.id()), + Err(e) => error!("Error running command: {:?}", e), + } + } + fn match_application(&mut self, application_matcher: &Application) -> bool { // Lazily fill the wm_class cache if let None = self.application_cache { - match self.wm_client.current_application() { + match self.application_client.current_application() { Some(application) => self.application_cache = Some(application), None => self.application_cache = Some(String::new()), }