diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index 462b48b4..61c8af6c 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -903,6 +903,10 @@ impl Client { (rate, delay) } + pub fn set_forward(&self, seat: Seat, forward: bool) { + self.send(&ClientMessage::SetForward { seat, forward }) + } + pub fn parse_keymap(&self, keymap: &str) -> Keymap { let res = self.send_with_response(&ClientMessage::ParseKeymap { keymap }); get_response!(res, Keymap(0), ParseKeymap { keymap }); diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index 844f9a47..17210fa2 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -436,6 +436,10 @@ pub enum ClientMessage<'a> { device: InputDevice, keymap: Keymap, }, + SetForward { + seat: Seat, + forward: bool, + }, } #[derive(Serialize, Deserialize, Debug)] diff --git a/jay-config/src/input.rs b/jay-config/src/input.rs index 08322e6d..9c031a5c 100644 --- a/jay-config/src/input.rs +++ b/jay-config/src/input.rs @@ -335,6 +335,26 @@ impl Seat { pub fn move_to_output(self, connector: Connector) { get!().move_to_output(WorkspaceSource::Seat(self), connector); } + + /// Set whether the current key event is forwarded to the focused client. + /// + /// This only has an effect if called from a keyboard shortcut. + /// + /// By default, release events are forwarded and press events are consumed. Note that + /// consuming release events can cause clients to get stuck in the pressed state. + pub fn set_forward(self, forward: bool) { + get!().set_forward(self, forward); + } + + /// This is a shorthand for `set_forward(true)`. + pub fn forward(self) { + self.set_forward(true) + } + + /// This is a shorthand for `set_forward(false)`. + pub fn consume(self) { + self.set_forward(false) + } } /// Returns all seats. diff --git a/jay-config/src/keyboard/mods.rs b/jay-config/src/keyboard/mods.rs index 80b1dea8..d2351f0c 100644 --- a/jay-config/src/keyboard/mods.rs +++ b/jay-config/src/keyboard/mods.rs @@ -36,6 +36,11 @@ pub const NUM: Modifiers = MOD2; /// Alias for `MOD4`. pub const LOGO: Modifiers = MOD4; +/// Synthetic modifier matching key release events. +/// +/// This can be used to execute a callback on key release. +pub const RELEASE: Modifiers = Modifiers(1 << 31); + impl BitOr for Modifiers { type Output = Self; diff --git a/src/config/handler.rs b/src/config/handler.rs index 79a47237..fe78a71c 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -318,6 +318,12 @@ impl ConfigProxyHandler { Ok(()) } + fn handle_set_forward(&self, seat: Seat, forward: bool) -> Result<(), CphError> { + let seat = self.get_seat(seat)?; + seat.set_forward(forward); + Ok(()) + } + fn handle_set_status(&self, status: &str) { self.state.set_status(status); } @@ -1764,6 +1770,9 @@ impl ConfigProxyHandler { ClientMessage::DeviceSetKeymap { device, keymap } => self .handle_set_device_keymap(device, keymap) .wrn("set_device_keymap")?, + ClientMessage::SetForward { seat, forward } => { + self.handle_set_forward(seat, forward).wrn("set_forward")? + } } Ok(()) } diff --git a/src/ifs/wl_seat.rs b/src/ifs/wl_seat.rs index 11831159..4d37ffc4 100644 --- a/src/ifs/wl_seat.rs +++ b/src/ifs/wl_seat.rs @@ -175,6 +175,7 @@ pub struct WlSeatGlobal { text_input: CloneCell>>, input_method: CloneCell>>, input_method_grab: CloneCell>>, + forward: Cell, } const CHANGE_CURSOR_MOVED: u32 = 1 << 0; @@ -243,6 +244,7 @@ impl WlSeatGlobal { text_input: Default::default(), input_method: Default::default(), input_method_grab: Default::default(), + forward: Cell::new(false), }); state.add_cursor_size(*DEFAULT_CURSOR_SIZE); let seat = slf.clone(); @@ -1146,6 +1148,10 @@ impl WlSeatGlobal { } } } + + pub fn set_forward(&self, forward: bool) { + self.forward.set(forward); + } } global_base!(WlSeatGlobal, WlSeat, WlSeatError); diff --git a/src/ifs/wl_seat/event_handling.rs b/src/ifs/wl_seat/event_handling.rs index f39527ee..5d436797 100644 --- a/src/ifs/wl_seat/event_handling.rs +++ b/src/ifs/wl_seat/event_handling.rs @@ -35,9 +35,9 @@ use { wire::WlDataOfferId, xkbcommon::{KeyboardState, XkbState, XKB_KEY_DOWN, XKB_KEY_UP}, }, - isnt::std_1::primitive::IsntSlice2Ext, + isnt::std_1::primitive::{IsntSlice2Ext, IsntSliceExt}, jay_config::keyboard::{ - mods::{Modifiers, CAPS, NUM}, + mods::{Modifiers, CAPS, NUM, RELEASE}, syms::KeySym, ModifiedKeySym, }, @@ -375,11 +375,13 @@ impl WlSeatGlobal { let mut shortcuts = SmallVec::<[_; 1]>::new(); let new_mods; { - if !self.state.lock.locked.get() && state == wl_keyboard::PRESSED { - let old_mods = xkb_state.mods(); + if !self.state.lock.locked.get() { + let mut mods = xkb_state.mods().mods_effective & !(CAPS.0 | NUM.0); + if state == wl_keyboard::RELEASED { + mods |= RELEASE.0; + } let keysyms = xkb_state.unmodified_keysyms(key); for &sym in keysyms { - let mods = old_mods.mods_effective & !(CAPS.0 | NUM.0); if let Some(mods) = self.shortcuts.get(&(mods, sym)) { shortcuts.push(ModifiedKeySym { mods, @@ -395,22 +397,28 @@ impl WlSeatGlobal { }); let node = self.keyboard_node.get(); let input_method_grab = self.input_method_grab.get(); - if shortcuts.is_empty() { + let mut forward = true; + if shortcuts.is_not_empty() { + self.forward.set(state == wl_keyboard::RELEASED); + if let Some(config) = self.state.config.get() { + let id = xkb_state.kb_state.id; + drop(xkb_state); + for shortcut in shortcuts { + config.invoke_shortcut(self.id(), &shortcut); + } + xkb_state_rc = get_state(); + xkb_state = xkb_state_rc.borrow_mut(); + if id != xkb_state.kb_state.id { + return; + } + } + forward = self.forward.get(); + } + if forward { match &input_method_grab { Some(g) => g.on_key(time_usec, key, state, &xkb_state.kb_state), _ => node.node_on_key(self, time_usec, key, state, &xkb_state.kb_state), } - } else if let Some(config) = self.state.config.get() { - let id = xkb_state.kb_state.id; - drop(xkb_state); - for shortcut in shortcuts { - config.invoke_shortcut(self.id(), &shortcut); - } - xkb_state_rc = get_state(); - xkb_state = xkb_state_rc.borrow_mut(); - if id != xkb_state.kb_state.id { - return; - } } if new_mods { self.state.for_each_seat_tester(|t| { diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index d9b89760..61116c39 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -49,6 +49,7 @@ pub enum SimpleCommand { ToggleFullscreen, ToggleMono, ToggleSplit, + Forward(bool), } #[derive(Debug, Clone)] diff --git a/toml-config/src/config/parsers/action.rs b/toml-config/src/config/parsers/action.rs index 009e71ce..42b19df4 100644 --- a/toml-config/src/config/parsers/action.rs +++ b/toml-config/src/config/parsers/action.rs @@ -109,6 +109,8 @@ impl ActionParser<'_> { "reload-config-toml" => ReloadConfigToml, "reload-config-so" => ReloadConfigSo, "none" => None, + "forward" => Forward(true), + "consume" => Forward(false), _ => { return Err(ActionParserError::UnknownSimpleAction(string.to_string()).spanned(span)) } diff --git a/toml-config/src/config/parsers/modified_keysym.rs b/toml-config/src/config/parsers/modified_keysym.rs index 3320a257..b07855e9 100644 --- a/toml-config/src/config/parsers/modified_keysym.rs +++ b/toml-config/src/config/parsers/modified_keysym.rs @@ -7,7 +7,10 @@ use { toml::toml_span::{Span, SpannedExt}, }, jay_config::keyboard::{ - mods::{Modifiers, ALT, CAPS, CTRL, LOCK, LOGO, MOD1, MOD2, MOD3, MOD4, MOD5, NUM, SHIFT}, + mods::{ + Modifiers, ALT, CAPS, CTRL, LOCK, LOGO, MOD1, MOD2, MOD3, MOD4, MOD5, NUM, RELEASE, + SHIFT, + }, ModifiedKeySym, }, thiserror::Error, @@ -49,6 +52,7 @@ impl Parser for ModifiedKeysymParser { "alt" => ALT, "num" => NUM, "logo" => LOGO, + "release" => RELEASE, _ => match KEYSYMS.get(part) { Some(new) if sym.is_none() => { sym = Some(*new); diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs index fdabf3e1..3ef8988b 100644 --- a/toml-config/src/lib.rs +++ b/toml-config/src/lib.rs @@ -61,6 +61,7 @@ impl Action { } SimpleCommand::ReloadConfigSo => Box::new(reload), SimpleCommand::None => Box::new(|| ()), + SimpleCommand::Forward(bool) => Box::new(move || s.set_forward(bool)), }, Action::Multi { actions } => { let mut actions: Vec<_> = actions.into_iter().map(|a| a.into_fn(state)).collect(); diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index 81b1647e..4e7e30d9 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -440,7 +440,7 @@ "$ref": "#/$defs/RepeatRate" }, "shortcuts": { - "description": "The compositor shortcuts.\n\nThe keys should be in the following format:\n\n```\n(MOD-)*KEYSYM\n```\n\n`MOD` should be one of `shift`, `lock`, `ctrl`, `mod1`, `mod2`, `mod3`, `mod4`,\n`mod5`, `caps`, `alt`, `num`, or `logo`.\n\n`KEYSYM` should be the name of a keysym. The authorative location for these names\nis [1] with the `XKB_KEY_` prefix removed.\n\nThe keysym should be the unmodified keysym. E.g. `shift-q` not `shift-Q`.\n\n[1]: https://github.com/xkbcommon/libxkbcommon/blob/master/include/xkbcommon/xkbcommon-keysyms.h\n\n- Example:\n\n ```toml\n [shortcuts]\n alt-q = \"quit\"\n ```\n", + "description": "The compositor shortcuts.\n\nThe keys should be in the following format:\n\n```\n(MOD-)*KEYSYM\n```\n\n`MOD` should be one of `shift`, `lock`, `ctrl`, `mod1`, `mod2`, `mod3`, `mod4`,\n`mod5`, `caps`, `alt`, `num`, `logo`, or `release`.\n\nUsing the `release` modifier causes the shortcut to trigger when the key is\nreleased.\n\n`KEYSYM` should be the name of a keysym. The authorative location for these names\nis [1] with the `XKB_KEY_` prefix removed.\n\nThe keysym should be the unmodified keysym. E.g. `shift-q` not `shift-Q`.\n\n[1]: https://github.com/xkbcommon/libxkbcommon/blob/master/include/xkbcommon/xkbcommon-keysyms.h\n\n- Example:\n\n ```toml\n [shortcuts]\n alt-q = \"quit\"\n ```\n", "type": "object", "additionalProperties": { "description": "", @@ -1053,6 +1053,8 @@ "quit", "reload-config-toml", "reload-config-to", + "consume", + "forward", "none" ] }, diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index 5032b4fd..df621542 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -694,7 +694,10 @@ The table has the following fields: ``` `MOD` should be one of `shift`, `lock`, `ctrl`, `mod1`, `mod2`, `mod3`, `mod4`, - `mod5`, `caps`, `alt`, `num`, or `logo`. + `mod5`, `caps`, `alt`, `num`, `logo`, or `release`. + + Using the `release` modifier causes the shortcut to trigger when the key is + released. `KEYSYM` should be the name of a keysym. The authorative location for these names is [1] with the `XKB_KEY_` prefix removed. @@ -2221,6 +2224,26 @@ The string should have one of the following values: Reload the `config.so`. +- `consume`: + + Consume the current key event. Don't forward it to the focused application. + + This action only has an effect in shortcuts. + + Key-press events events that trigger shortcuts are consumed by default. + Key-release events events that trigger shortcuts are forwarded by default. + + Note that consuming key-release events can cause keys to get stuck in the focused + application. + + See the `forward` action to achieve the opposite effect. + +- `forward`: + + Forward the current key event to the focused application. + + See the `consume` action for more details. + - `none`: Perform no action. diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index 9d79a882..67a4a929 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -668,6 +668,24 @@ SimpleActionName: description: Reload the `config.toml`. - value: reload-config-to description: Reload the `config.so`. + - value: consume + description: | + Consume the current key event. Don't forward it to the focused application. + + This action only has an effect in shortcuts. + + Key-press events events that trigger shortcuts are consumed by default. + Key-release events events that trigger shortcuts are forwarded by default. + + Note that consuming key-release events can cause keys to get stuck in the focused + application. + + See the `forward` action to achieve the opposite effect. + - value: forward + description: | + Forward the current key event to the focused application. + + See the `consume` action for more details. - value: none description: | Perform no action. @@ -1705,7 +1723,10 @@ Config: ``` `MOD` should be one of `shift`, `lock`, `ctrl`, `mod1`, `mod2`, `mod3`, `mod4`, - `mod5`, `caps`, `alt`, `num`, or `logo`. + `mod5`, `caps`, `alt`, `num`, `logo`, or `release`. + + Using the `release` modifier causes the shortcut to trigger when the key is + released. `KEYSYM` should be the name of a keysym. The authorative location for these names is [1] with the `XKB_KEY_` prefix removed.