mirror of
https://github.com/k0kubun/xremap
synced 2024-11-02 03:40:25 +00:00
Implement mark
This commit is contained in:
parent
646a1dd53d
commit
c49a9b5cde
@ -19,6 +19,7 @@
|
|||||||
* Remap a key to two different keys depending on whether it's pressed alone or held.
|
* 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.
|
* Application-specific remapping. Even if it's not supported by your application, xremap can.
|
||||||
* Automatically remap newly connected devices by starting xremap with `--watch`.
|
* Automatically remap newly connected devices by starting xremap with `--watch`.
|
||||||
|
* Support [Emacs-like key remapping](example/emacs.yml), including the mark mode.
|
||||||
|
|
||||||
## Prerequisite
|
## Prerequisite
|
||||||
|
|
||||||
@ -146,6 +147,10 @@ keymap:
|
|||||||
# execute a command
|
# execute a command
|
||||||
MOD1-KEY_XXX:
|
MOD1-KEY_XXX:
|
||||||
launch: ["bash", "-c", "echo hello > /tmp/test"]
|
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
|
application: # Optional
|
||||||
not: [Application, ...]
|
not: [Application, ...]
|
||||||
# or
|
# or
|
||||||
|
60
example/emacs.yml
Normal file
60
example/emacs.yml
Normal file
@ -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 }]
|
@ -14,6 +14,10 @@ pub enum Action {
|
|||||||
Remap(HashMap<KeyPress, Vec<Action>>),
|
Remap(HashMap<KeyPress, Vec<Action>>),
|
||||||
#[serde(deserialize_with = "deserialize_launch")]
|
#[serde(deserialize_with = "deserialize_launch")]
|
||||||
Launch(Vec<String>),
|
Launch(Vec<String>),
|
||||||
|
#[serde(deserialize_with = "deserialize_with_mark")]
|
||||||
|
WithMark(KeyPress),
|
||||||
|
#[serde(deserialize_with = "deserialize_set_mark")]
|
||||||
|
SetMark(bool),
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deserialize_remap<'de, D>(deserializer: D) -> Result<HashMap<KeyPress, Vec<Action>>, D::Error>
|
fn deserialize_remap<'de, D>(deserializer: D) -> Result<HashMap<KeyPress, Vec<Action>>, D::Error>
|
||||||
@ -42,6 +46,32 @@ where
|
|||||||
Err(de::Error::custom("not a map with a single \"launch\" key"))
|
Err(de::Error::custom("not a map with a single \"launch\" key"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn deserialize_with_mark<'de, D>(deserializer: D) -> Result<KeyPress, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let mut action = HashMap::<String, KeyPress>::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<bool, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let mut action = HashMap::<String, bool>::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<Action>
|
// Used only for deserializing Vec<Action>
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
|
@ -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) {
|
fn assert_parse(yaml: &str) {
|
||||||
let result: Result<Config, Error> = serde_yaml::from_str(&yaml);
|
let result: Result<Config, Error> = serde_yaml::from_str(&yaml);
|
||||||
if let Err(e) = result {
|
if let Err(e) = result {
|
||||||
|
@ -26,7 +26,7 @@ static MOUSE_BTNS: [&str; 13] = [
|
|||||||
"BTN_RIGHT",
|
"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<VirtualDevice, Box<dyn Error>> {
|
pub fn output_device() -> Result<VirtualDevice, Box<dyn Error>> {
|
||||||
let mut keys: AttributeSet<Key> = AttributeSet::new();
|
let mut keys: AttributeSet<Key> = AttributeSet::new();
|
||||||
for code in Key::KEY_RESERVED.code()..Key::BTN_TRIGGER_HAPPY40.code() {
|
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<String>) -> bool
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn is_keyboard(device: &Device) -> 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() {
|
match device.supported_keys() {
|
||||||
Some(keys) => {
|
Some(keys) => {
|
||||||
keys.contains(Key::KEY_SPACE)
|
keys.contains(Key::KEY_SPACE)
|
||||||
|
@ -21,11 +21,12 @@ pub struct EventHandler {
|
|||||||
control: PressState,
|
control: PressState,
|
||||||
alt: PressState,
|
alt: PressState,
|
||||||
windows: PressState,
|
windows: PressState,
|
||||||
wm_client: WMClient,
|
application_client: WMClient,
|
||||||
application_cache: Option<String>,
|
application_cache: Option<String>,
|
||||||
multi_purpose_keys: HashMap<Key, MultiPurposeKeyState>,
|
multi_purpose_keys: HashMap<Key, MultiPurposeKeyState>,
|
||||||
override_remap: Option<HashMap<KeyPress, Vec<Action>>>,
|
override_remap: Option<HashMap<KeyPress, Vec<Action>>>,
|
||||||
sigaction_set: bool,
|
sigaction_set: bool,
|
||||||
|
mark_set: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventHandler {
|
impl EventHandler {
|
||||||
@ -36,11 +37,12 @@ impl EventHandler {
|
|||||||
control: PressState::new(false),
|
control: PressState::new(false),
|
||||||
alt: PressState::new(false),
|
alt: PressState::new(false),
|
||||||
windows: PressState::new(false),
|
windows: PressState::new(false),
|
||||||
wm_client: build_client(),
|
application_client: build_client(),
|
||||||
application_cache: None,
|
application_cache: None,
|
||||||
multi_purpose_keys: HashMap::new(),
|
multi_purpose_keys: HashMap::new(),
|
||||||
override_remap: None,
|
override_remap: None,
|
||||||
sigaction_set: false,
|
sigaction_set: false,
|
||||||
|
mark_set: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -187,7 +189,22 @@ impl EventHandler {
|
|||||||
|
|
||||||
fn dispatch_action(&mut self, action: &Action) -> Result<(), Box<dyn Error>> {
|
fn dispatch_action(&mut self, action: &Action) -> Result<(), Box<dyn Error>> {
|
||||||
match action {
|
match action {
|
||||||
Action::KeyPress(key_press) => {
|
Action::KeyPress(key_press) => self.send_key_press(key_press)?,
|
||||||
|
Action::Remap(remap) => {
|
||||||
|
let mut override_remap: HashMap<KeyPress, Vec<Action>> = HashMap::new();
|
||||||
|
for (key_press, actions) in remap.iter() {
|
||||||
|
override_remap.insert(key_press.clone(), actions.iter().map(|a| a.clone()).collect());
|
||||||
|
}
|
||||||
|
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 send_key_press(&mut self, key_press: &KeyPress) -> Result<(), Box<dyn Error>> {
|
||||||
let next_shift = self.build_state(Modifier::Shift, key_press.shift);
|
let next_shift = self.build_state(Modifier::Shift, key_press.shift);
|
||||||
let next_control = self.build_state(Modifier::Control, key_press.control);
|
let next_control = self.build_state(Modifier::Control, key_press.control);
|
||||||
let next_alt = self.build_state(Modifier::Alt, key_press.alt);
|
let next_alt = self.build_state(Modifier::Alt, key_press.alt);
|
||||||
@ -205,42 +222,9 @@ impl EventHandler {
|
|||||||
self.send_modifier(Modifier::Alt, &prev_alt)?;
|
self.send_modifier(Modifier::Alt, &prev_alt)?;
|
||||||
self.send_modifier(Modifier::Control, &prev_control)?;
|
self.send_modifier(Modifier::Control, &prev_control)?;
|
||||||
self.send_modifier(Modifier::Shift, &prev_shift)?;
|
self.send_modifier(Modifier::Shift, &prev_shift)?;
|
||||||
}
|
|
||||||
Action::Remap(remap) => {
|
|
||||||
let mut override_remap: HashMap<KeyPress, Vec<Action>> = HashMap::new();
|
|
||||||
for (key_press, actions) in remap.iter() {
|
|
||||||
override_remap.insert(key_press.clone(), actions.iter().map(|a| a.clone()).collect());
|
|
||||||
}
|
|
||||||
self.override_remap = Some(override_remap)
|
|
||||||
}
|
|
||||||
Action::Launch(command) => self.run_command(command.clone()),
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_command(&mut self, command: Vec<String>) {
|
|
||||||
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 send_modifier(&mut self, modifier: Modifier, desired: &PressState) -> Result<PressState, Box<dyn Error>> {
|
fn send_modifier(&mut self, modifier: Modifier, desired: &PressState) -> Result<PressState, Box<dyn Error>> {
|
||||||
let mut current = match modifier {
|
let mut current = match modifier {
|
||||||
Modifier::Shift => &self.shift,
|
Modifier::Shift => &self.shift,
|
||||||
@ -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<String>) {
|
||||||
|
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 {
|
fn match_application(&mut self, application_matcher: &Application) -> bool {
|
||||||
// Lazily fill the wm_class cache
|
// Lazily fill the wm_class cache
|
||||||
if let None = self.application_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),
|
Some(application) => self.application_cache = Some(application),
|
||||||
None => self.application_cache = Some(String::new()),
|
None => self.application_cache = Some(String::new()),
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user