From 27f30f8d285fd3bac8d1bd08e610654ae6048ddf Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Tue, 16 Apr 2024 17:24:47 +0200 Subject: [PATCH 1/3] config: send server-side capabilities to config --- jay-config/src/_private/client.rs | 12 +++++++++++- jay-config/src/_private/ipc.rs | 11 +++++++++++ jay-config/src/lib.rs | 3 ++- src/config.rs | 1 + src/it/test_config.rs | 1 + 5 files changed, 26 insertions(+), 2 deletions(-) diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index 61c8af6c..3cf9c686 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -4,7 +4,9 @@ use { crate::{ _private::{ bincode_ops, - ipc::{ClientMessage, InitMessage, Response, ServerMessage, WorkspaceSource}, + ipc::{ + ClientMessage, InitMessage, Response, ServerFeature, ServerMessage, WorkspaceSource, + }, logging, Config, ConfigEntry, ConfigEntryGen, PollableId, WireMode, VERSION, }, exec::Command, @@ -1178,6 +1180,14 @@ impl Client { } } } + ServerMessage::Features { features } => { + for feat in features { + match feat { + ServerFeature::NONE => {} + _ => {} + } + } + } } } diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index 17210fa2..783949f4 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -13,6 +13,14 @@ use { std::time::Duration, }; +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +#[serde(transparent)] +pub struct ServerFeature(u16); + +impl ServerFeature { + pub const NONE: Self = Self(0); +} + #[derive(Serialize, Deserialize, Debug)] pub enum ServerMessage { Configure { @@ -62,6 +70,9 @@ pub enum ServerMessage { writable: bool, res: Result<(), String>, }, + Features { + features: Vec, + }, } #[derive(Serialize, Deserialize, Debug)] diff --git a/jay-config/src/lib.rs b/jay-config/src/lib.rs index 0b94684e..118d7fa0 100644 --- a/jay-config/src/lib.rs +++ b/jay-config/src/lib.rs @@ -38,7 +38,8 @@ clippy::uninlined_format_args, clippy::len_zero, clippy::single_char_pattern, - clippy::single_char_add_str + clippy::single_char_add_str, + clippy::single_match )] use { diff --git a/src/config.rs b/src/config.rs index 3c3f2a43..8334d924 100644 --- a/src/config.rs +++ b/src/config.rs @@ -203,6 +203,7 @@ impl ConfigProxy { } pub fn configure(&self, reload: bool) { + self.send(&ServerMessage::Features { features: vec![] }); self.send(&ServerMessage::Configure { reload }); } diff --git a/src/it/test_config.rs b/src/it/test_config.rs index 377387d3..22273420 100644 --- a/src/it/test_config.rs +++ b/src/it/test_config.rs @@ -109,6 +109,7 @@ unsafe extern "C" fn handle_msg(data: *const u8, msg: *const u8, size: usize) { ServerMessage::Idle => {} ServerMessage::DevicesEnumerated => {} ServerMessage::InterestReady { .. } => {} + ServerMessage::Features { .. } => {} } } From 90dbde99ab862fc81b7b3e120647392c370eefe5 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Tue, 16 Apr 2024 16:27:26 +0200 Subject: [PATCH 2/3] config: add support for mod masks in shortcuts --- jay-config/src/_private/client.rs | 54 ++++-- jay-config/src/_private/ipc.rs | 6 + jay-config/src/input.rs | 31 +++- jay-config/src/keyboard/mods.rs | 5 + src/config/handler.rs | 13 +- src/ifs/wl_seat.rs | 3 +- src/ifs/wl_seat/event_handling.rs | 34 +++- toml-config/src/config.rs | 11 +- toml-config/src/config/parsers/config.rs | 29 ++- .../src/config/parsers/modified_keysym.rs | 61 +++++-- toml-config/src/config/parsers/shortcuts.rs | 172 +++++++++++++++--- toml-config/src/lib.rs | 23 +-- toml-spec/spec/spec.generated.json | 23 +++ toml-spec/spec/spec.generated.md | 66 +++++++ toml-spec/spec/spec.yaml | 62 +++++++ 15 files changed, 501 insertions(+), 92 deletions(-) diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index 3cf9c686..3fc480e5 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -11,7 +11,10 @@ use { }, exec::Command, input::{acceleration::AccelProfile, capability::Capability, InputDevice, Seat}, - keyboard::Keymap, + keyboard::{ + mods::{Modifiers, RELEASE}, + Keymap, + }, logging::LogLevel, tasks::{JoinHandle, JoinSlot}, theme::{colors::Colorable, sized::Resizable, Color}, @@ -64,12 +67,17 @@ fn ignore_panic(name: &str, f: impl FnOnce()) { } } +struct KeyHandler { + mask: Modifiers, + cb: Callback, +} + pub(crate) struct Client { configure: extern "C" fn(), srv_data: *const u8, srv_unref: unsafe extern "C" fn(data: *const u8), srv_handler: unsafe extern "C" fn(data: *const u8, msg: *const u8, size: usize), - key_handlers: RefCell>, + key_handlers: RefCell>, timer_handlers: RefCell>, response: RefCell>, on_new_seat: RefCell>>, @@ -915,33 +923,45 @@ impl Client { keymap } - pub fn bind, F: FnMut() + 'static>( + pub fn bind_masked( &self, seat: Seat, - mod_sym: T, + mut mod_mask: Modifiers, + mod_sym: ModifiedKeySym, mut f: F, ) { - let mod_sym = mod_sym.into(); + mod_mask |= mod_sym.mods | RELEASE; let register = { let mut kh = self.key_handlers.borrow_mut(); - let f = cb(move |_| f()); + let cb = cb(move |_| f()); match kh.entry((seat, mod_sym)) { Entry::Occupied(mut o) => { - *o.get_mut() = f; - false + let o = o.get_mut(); + o.cb = cb; + mem::replace(&mut o.mask, mod_mask) != mod_mask } Entry::Vacant(v) => { - v.insert(f); + v.insert(KeyHandler { mask: mod_mask, cb }); true } } }; if register { - self.send(&ClientMessage::AddShortcut { - seat, - mods: mod_sym.mods, - sym: mod_sym.sym, - }); + let msg = if !mod_mask.0 == 0 { + ClientMessage::AddShortcut { + seat, + mods: mod_sym.mods, + sym: mod_sym.sym, + } + } else { + ClientMessage::AddShortcut2 { + seat, + mods: mod_sym.mods, + mod_mask, + sym: mod_sym.sym, + } + }; + self.send(&msg); } } @@ -1104,7 +1124,11 @@ impl Client { } ServerMessage::InvokeShortcut { seat, mods, sym } => { let ms = ModifiedKeySym { mods, sym }; - let handler = self.key_handlers.borrow_mut().get(&(seat, ms)).cloned(); + let handler = self + .key_handlers + .borrow_mut() + .get(&(seat, ms)) + .map(|k| k.cb.clone()); if let Some(handler) = handler { run_cb("shortcut", &handler, ()); } diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index 783949f4..67821e87 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -451,6 +451,12 @@ pub enum ClientMessage<'a> { seat: Seat, forward: bool, }, + AddShortcut2 { + seat: Seat, + mods: Modifiers, + mod_mask: Modifiers, + sym: KeySym, + }, } #[derive(Serialize, Deserialize, Debug)] diff --git a/jay-config/src/input.rs b/jay-config/src/input.rs index 9c031a5c..2d18d7e5 100644 --- a/jay-config/src/input.rs +++ b/jay-config/src/input.rs @@ -6,7 +6,7 @@ pub mod capability; use { crate::{ input::{acceleration::AccelProfile, capability::Capability}, - keyboard::Keymap, + keyboard::{mods::Modifiers, Keymap}, Axis, Direction, ModifiedKeySym, Workspace, _private::{ipc::WorkspaceSource, DEFAULT_SEAT_NAME}, video::Connector, @@ -188,12 +188,37 @@ impl Seat { /// CapsLock and NumLock are ignored during modifier evaluation. Therefore, bindings /// containing these modifiers will never be invoked. pub fn bind, F: FnMut() + 'static>(self, mod_sym: T, f: F) { - get!().bind(self, mod_sym, f) + self.bind_masked(Modifiers(!0), mod_sym, f) + } + + /// Creates a compositor-wide hotkey while ignoring some modifiers. + /// + /// This is similar to `bind` except that only the masked modifiers are considered. + /// + /// For example, if this function is invoked with `mod_mask = Modifiers::NONE` and + /// `mod_sym = SYM_XF86AudioRaiseVolume`, then the callback will be invoked whenever + /// `SYM_XF86AudioRaiseVolume` is pressed. Even if the user is simultaneously holding + /// the shift key which would otherwise prevent the callback from taking effect. + /// + /// For example, if this function is invoked with `mod_mask = CTRL | SHIFT` and + /// `mod_sym = CTRL | SYM_x`, then the callback will be invoked whenever the user + /// presses `ctrl+x` without pressing the shift key. Even if the user is + /// simultaneously holding the alt key. + /// + /// If `mod_sym` contains any modifiers, then these modifiers are automatically added + /// to the mask. The synthetic `RELEASE` modifier is always added to the mask. + pub fn bind_masked, F: FnMut() + 'static>( + self, + mod_mask: Modifiers, + mod_sym: T, + f: F, + ) { + get!().bind_masked(self, mod_mask, mod_sym.into(), f) } /// Unbinds a hotkey. pub fn unbind>(self, mod_sym: T) { - get!().unbind(self, mod_sym) + get!().unbind(self, mod_sym.into()) } /// Moves the keyboard focus of the seat in the specified direction. diff --git a/jay-config/src/keyboard/mods.rs b/jay-config/src/keyboard/mods.rs index d2351f0c..bd4a2329 100644 --- a/jay-config/src/keyboard/mods.rs +++ b/jay-config/src/keyboard/mods.rs @@ -10,6 +10,11 @@ use { #[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Default, Hash, Debug)] pub struct Modifiers(pub u32); +impl Modifiers { + /// No modifiers. + pub const NONE: Self = Modifiers(0); +} + /// The Shift modifier pub const SHIFT: Modifiers = Modifiers(1 << 0); /// The CapsLock modifier. diff --git a/src/config/handler.rs b/src/config/handler.rs index fe78a71c..c778d0a0 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -1127,11 +1127,12 @@ impl ConfigProxyHandler { fn handle_add_shortcut( &self, seat: Seat, + mod_mask: Modifiers, mods: Modifiers, sym: KeySym, ) -> Result<(), CphError> { let seat = self.get_seat(seat)?; - seat.add_shortcut(mods, sym); + seat.add_shortcut(mod_mask, mods, sym); Ok(()) } @@ -1499,7 +1500,7 @@ impl ConfigProxyHandler { self.handle_set_split(seat, axis).wrn("set_split")? } ClientMessage::AddShortcut { seat, mods, sym } => self - .handle_add_shortcut(seat, mods, sym) + .handle_add_shortcut(seat, Modifiers(!0), mods, sym) .wrn("add_shortcut")?, ClientMessage::RemoveShortcut { seat, mods, sym } => self .handle_remove_shortcut(seat, mods, sym) @@ -1773,6 +1774,14 @@ impl ConfigProxyHandler { ClientMessage::SetForward { seat, forward } => { self.handle_set_forward(seat, forward).wrn("set_forward")? } + ClientMessage::AddShortcut2 { + seat, + mod_mask, + mods, + sym, + } => self + .handle_add_shortcut(seat, mod_mask, mods, sym) + .wrn("add_shortcut")?, } Ok(()) } diff --git a/src/ifs/wl_seat.rs b/src/ifs/wl_seat.rs index 4d37ffc4..029f082a 100644 --- a/src/ifs/wl_seat.rs +++ b/src/ifs/wl_seat.rs @@ -73,7 +73,6 @@ use { xkbcommon::{DynKeyboardState, KeyboardState, KeymapId, XkbKeymap, XkbState}, }, ahash::AHashMap, - jay_config::keyboard::mods::Modifiers, smallvec::SmallVec, std::{ cell::{Cell, RefCell}, @@ -160,7 +159,7 @@ pub struct WlSeatGlobal { pointer_owner: PointerOwnerHolder, kb_owner: KbOwnerHolder, dropped_dnd: RefCell>, - shortcuts: CopyHashMap<(u32, u32), Modifiers>, + shortcuts: RefCell>>, queue_link: Cell>>>, tree_changed_handler: Cell>>, output: CloneCell>, diff --git a/src/ifs/wl_seat/event_handling.rs b/src/ifs/wl_seat/event_handling.rs index 5d436797..614bc96e 100644 --- a/src/ifs/wl_seat/event_handling.rs +++ b/src/ifs/wl_seat/event_handling.rs @@ -42,7 +42,7 @@ use { ModifiedKeySym, }, smallvec::SmallVec, - std::{cell::RefCell, rc::Rc}, + std::{cell::RefCell, collections::hash_map::Entry, rc::Rc}, }; #[derive(Default)] @@ -380,13 +380,18 @@ impl WlSeatGlobal { if state == wl_keyboard::RELEASED { mods |= RELEASE.0; } + let scs = &*self.shortcuts.borrow(); let keysyms = xkb_state.unmodified_keysyms(key); for &sym in keysyms { - if let Some(mods) = self.shortcuts.get(&(mods, sym)) { - shortcuts.push(ModifiedKeySym { - mods, - sym: KeySym(sym), - }); + if let Some(key_mods) = scs.get(&sym) { + for (key_mods, mask) in key_mods { + if mods & mask == key_mods { + shortcuts.push(ModifiedKeySym { + mods: Modifiers(key_mods), + sym: KeySym(sym), + }); + } + } } } } @@ -608,15 +613,24 @@ impl WlSeatGlobal { } pub fn clear_shortcuts(&self) { - self.shortcuts.clear(); + self.shortcuts.borrow_mut().clear(); } - pub fn add_shortcut(&self, mods: Modifiers, keysym: KeySym) { - self.shortcuts.set((mods.0, keysym.0), mods); + pub fn add_shortcut(&self, mod_mask: Modifiers, mods: Modifiers, keysym: KeySym) { + self.shortcuts + .borrow_mut() + .entry(keysym.0) + .or_default() + .insert(mods.0, mod_mask.0); } pub fn remove_shortcut(&self, mods: Modifiers, keysym: KeySym) { - self.shortcuts.remove(&(mods.0, keysym.0)); + if let Entry::Occupied(mut oe) = self.shortcuts.borrow_mut().entry(keysym.0) { + oe.get_mut().remove(&mods.0); + if oe.get().is_empty() { + oe.remove(); + } + } } pub fn trigger_tree_changed(&self) { diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index 61116c39..00e18649 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -17,7 +17,7 @@ use { }, jay_config::{ input::acceleration::AccelProfile, - keyboard::{Keymap, ModifiedKeySym}, + keyboard::{mods::Modifiers, Keymap, ModifiedKeySym}, logging::LogLevel, status::MessageFormat, theme::Color, @@ -280,11 +280,18 @@ pub struct RepeatRate { pub delay: i32, } +#[derive(Debug, Clone)] +pub struct Shortcut { + pub mask: Modifiers, + pub keysym: ModifiedKeySym, + pub action: Action, +} + #[derive(Debug, Clone)] pub struct Config { pub keymap: Option, pub repeat_rate: Option, - pub shortcuts: Vec<(ModifiedKeySym, Action)>, + pub shortcuts: Vec, pub on_graphics_initialized: Option, pub on_idle: Option, pub status: Option, diff --git a/toml-config/src/config/parsers/config.rs b/toml-config/src/config/parsers/config.rs index 89e213b6..837bfdba 100644 --- a/toml-config/src/config/parsers/config.rs +++ b/toml-config/src/config/parsers/config.rs @@ -17,7 +17,7 @@ use { log_level::LogLevelParser, output::OutputsParser, repeat_rate::RepeatRateParser, - shortcuts::{ShortcutsParser, ShortcutsParserError}, + shortcuts::{ComplexShortcutsParser, ShortcutsParser, ShortcutsParserError}, status::StatusParser, theme::ThemeParser, }, @@ -30,6 +30,7 @@ use { }, }, indexmap::IndexMap, + std::collections::HashSet, thiserror::Error, }; @@ -96,7 +97,7 @@ impl Parser for ConfigParser<'_> { _, idle_val, ), - (explicit_sync, repeat_rate_val), + (explicit_sync, repeat_rate_val, complex_shortcuts_val), ) = ext.extract(( ( opt(val("keymap")), @@ -122,7 +123,11 @@ impl Parser for ConfigParser<'_> { opt(val("$schema")), opt(val("idle")), ), - (recover(opt(bol("explicit-sync"))), opt(val("repeat-rate"))), + ( + recover(opt(bol("explicit-sync"))), + opt(val("repeat-rate")), + opt(val("complex-shortcuts")), + ), ))?; let mut keymap = None; if let Some(value) = keymap_val { @@ -136,10 +141,24 @@ impl Parser for ConfigParser<'_> { } } } + let mut used_keys = HashSet::new(); let mut shortcuts = vec![]; if let Some(value) = shortcuts_val { - shortcuts = value - .parse(&mut ShortcutsParser(self.0)) + value + .parse(&mut ShortcutsParser { + cx: self.0, + used_keys: &mut used_keys, + shortcuts: &mut shortcuts, + }) + .map_spanned_err(ConfigParserError::ParseShortcuts)?; + } + if let Some(value) = complex_shortcuts_val { + value + .parse(&mut ComplexShortcutsParser { + cx: self.0, + used_keys: &mut used_keys, + shortcuts: &mut shortcuts, + }) .map_spanned_err(ConfigParserError::ParseShortcuts)?; } if shortcuts.is_empty() { diff --git a/toml-config/src/config/parsers/modified_keysym.rs b/toml-config/src/config/parsers/modified_keysym.rs index b07855e9..4d191914 100644 --- a/toml-config/src/config/parsers/modified_keysym.rs +++ b/toml-config/src/config/parsers/modified_keysym.rs @@ -26,6 +26,8 @@ pub enum ModifiedKeysymParserError { MissingSym, #[error("Unknown keysym {0}")] UnknownKeysym(String), + #[error("Unknown modifier {0}")] + UnknownModifier(String), } pub struct ModifiedKeysymParser; @@ -39,20 +41,8 @@ impl Parser for ModifiedKeysymParser { let mut modifiers = Modifiers(0); let mut sym = None; for part in string.split("-") { - let modifier = match part { - "shift" => SHIFT, - "lock" => LOCK, - "ctrl" => CTRL, - "mod1" => MOD1, - "mod2" => MOD2, - "mod3" => MOD3, - "mod4" => MOD4, - "mod5" => MOD5, - "caps" => CAPS, - "alt" => ALT, - "num" => NUM, - "logo" => LOGO, - "release" => RELEASE, + let modifier = match parse_mod(part) { + Some(m) => m, _ => match KEYSYMS.get(part) { Some(new) if sym.is_none() => { sym = Some(*new); @@ -73,3 +63,46 @@ impl Parser for ModifiedKeysymParser { } } } + +pub struct ModifiersParser; + +impl Parser for ModifiersParser { + type Value = Modifiers; + type Error = ModifiedKeysymParserError; + const EXPECTED: &'static [DataType] = &[DataType::String]; + + fn parse_string(&mut self, span: Span, string: &str) -> ParseResult { + let mut modifiers = Modifiers(0); + if !string.is_empty() { + for part in string.split("-") { + let Some(modifier) = parse_mod(part) else { + return Err( + ModifiedKeysymParserError::UnknownModifier(part.to_string()).spanned(span) + ); + }; + modifiers |= modifier; + } + } + Ok(modifiers) + } +} + +fn parse_mod(part: &str) -> Option { + let modifier = match part { + "shift" => SHIFT, + "lock" => LOCK, + "ctrl" => CTRL, + "mod1" => MOD1, + "mod2" => MOD2, + "mod3" => MOD3, + "mod4" => MOD4, + "mod5" => MOD5, + "caps" => CAPS, + "alt" => ALT, + "num" => NUM, + "logo" => LOGO, + "release" => RELEASE, + _ => return None, + }; + Some(modifier) +} diff --git a/toml-config/src/config/parsers/shortcuts.rs b/toml-config/src/config/parsers/shortcuts.rs index c11d576f..dc44967c 100644 --- a/toml-config/src/config/parsers/shortcuts.rs +++ b/toml-config/src/config/parsers/shortcuts.rs @@ -2,9 +2,16 @@ use { crate::{ config::{ context::Context, + extractor::{opt, str, val, Extractor, ExtractorError}, parser::{DataType, ParseResult, Parser, UnexpectedDataType}, - parsers::{action::ActionParser, modified_keysym::ModifiedKeysymParser}, - Action, + parsers::{ + action::{ActionParser, ActionParserError}, + modified_keysym::{ + ModifiedKeysymParser, ModifiedKeysymParserError, ModifiersParser, + }, + }, + spanned::SpannedErrorExt, + Action, Shortcut, SimpleCommand, }, toml::{ toml_span::{Span, Spanned, SpannedExt}, @@ -12,7 +19,7 @@ use { }, }, indexmap::IndexMap, - jay_config::keyboard::ModifiedKeySym, + jay_config::keyboard::{mods::Modifiers, ModifiedKeySym}, std::collections::HashSet, thiserror::Error, }; @@ -21,12 +28,22 @@ use { pub enum ShortcutsParserError { #[error(transparent)] Expected(#[from] UnexpectedDataType), + #[error(transparent)] + ExtractorError(#[from] ExtractorError), + #[error("Could not parse the mod mask")] + ModMask(#[source] ModifiedKeysymParserError), + #[error("Could not parse the action")] + ActionParserError(#[source] ActionParserError), } -pub struct ShortcutsParser<'a>(pub &'a Context<'a>); +pub struct ShortcutsParser<'a, 'b> { + pub cx: &'a Context<'a>, + pub used_keys: &'b mut HashSet>, + pub shortcuts: &'b mut Vec, +} -impl Parser for ShortcutsParser<'_> { - type Value = Vec<(ModifiedKeySym, Action)>; +impl Parser for ShortcutsParser<'_, '_> { + type Value = (); type Error = ShortcutsParserError; const EXPECTED: &'static [DataType] = &[DataType::Table]; @@ -35,38 +52,137 @@ impl Parser for ShortcutsParser<'_> { _span: Span, table: &IndexMap, Spanned>, ) -> ParseResult { - let mut used_keys = HashSet::>::new(); - let mut res = vec![]; for (key, value) in table.iter() { - let keysym = match ModifiedKeysymParser.parse_string(key.span, &key.value) { - Ok(k) => k, - Err(e) => { - log::warn!("Could not parse keysym: {}", self.0.error(e)); - continue; - } + let Some(keysym) = parse_modified_keysym(self.cx, key) else { + continue; + }; + let Some(action) = parse_action(self.cx, &key.value, value) else { + continue; + }; + let spanned = keysym.spanned(key.span); + log_used(self.cx, self.used_keys, spanned); + self.shortcuts.push(Shortcut { + mask: Modifiers(!0), + keysym, + action, + }); + } + Ok(()) + } +} + +pub struct ComplexShortcutsParser<'a, 'b> { + pub cx: &'a Context<'a>, + pub used_keys: &'b mut HashSet>, + pub shortcuts: &'b mut Vec, +} + +impl Parser for ComplexShortcutsParser<'_, '_> { + type Value = (); + type Error = ShortcutsParserError; + const EXPECTED: &'static [DataType] = &[DataType::Table]; + + fn parse_table( + &mut self, + _span: Span, + table: &IndexMap, Spanned>, + ) -> ParseResult { + for (key, value) in table.iter() { + let Some(keysym) = parse_modified_keysym(self.cx, key) else { + continue; }; - let action = match value.parse(&mut ActionParser(self.0)) { - Ok(a) => a, + let shortcut = match value.parse(&mut ComplexShortcutParser { + keysym, + cx: self.cx, + }) { + Ok(v) => v, Err(e) => { log::warn!( - "Could not parse action for keysym {}: {}", + "Could not parse shortcut for keysym {}: {}", key.value, - self.0.error(e) + self.cx.error(e) ); continue; } }; let spanned = keysym.spanned(key.span); - if let Some(prev) = used_keys.get(&spanned) { - log::warn!( - "Duplicate key overrides previous definition: {}", - self.0.error3(spanned.span) - ); - log::info!("Previous definition here: {}", self.0.error3(prev.span)); - } - used_keys.insert(spanned); - res.push((keysym, action)); + log_used(self.cx, self.used_keys, spanned); + self.shortcuts.push(shortcut); } - Ok(res) + Ok(()) + } +} + +struct ComplexShortcutParser<'a> { + pub keysym: ModifiedKeySym, + pub cx: &'a Context<'a>, +} + +impl Parser for ComplexShortcutParser<'_> { + type Value = Shortcut; + type Error = ShortcutsParserError; + const EXPECTED: &'static [DataType] = &[DataType::Table]; + + fn parse_table( + &mut self, + span: Span, + table: &IndexMap, Spanned>, + ) -> ParseResult { + let mut ext = Extractor::new(self.cx, span, table); + let (mod_mask_val, action_val) = ext.extract((opt(str("mod-mask")), opt(val("action"))))?; + let mod_mask = match mod_mask_val { + None => Modifiers(!0), + Some(v) => ModifiersParser + .parse_string(v.span, v.value) + .map_spanned_err(ShortcutsParserError::ModMask)?, + }; + let action = match action_val { + None => Action::SimpleCommand { + cmd: SimpleCommand::None, + }, + Some(v) => v + .parse(&mut ActionParser(self.cx)) + .map_spanned_err(ShortcutsParserError::ActionParserError)?, + }; + Ok(Shortcut { + mask: mod_mask, + keysym: self.keysym, + action, + }) + } +} + +fn parse_action(cx: &Context<'_>, key: &str, value: &Spanned) -> Option { + match value.parse(&mut ActionParser(cx)) { + Ok(a) => Some(a), + Err(e) => { + log::warn!("Could not parse action for keysym {key}: {}", cx.error(e)); + None + } + } +} + +fn parse_modified_keysym(cx: &Context<'_>, key: &Spanned) -> Option { + match ModifiedKeysymParser.parse_string(key.span, &key.value) { + Ok(k) => Some(k), + Err(e) => { + log::warn!("Could not parse keysym {}: {}", key.value, cx.error(e)); + None + } + } +} + +fn log_used( + cx: &Context<'_>, + used: &mut HashSet>, + key: Spanned, +) { + if let Some(prev) = used.get(&key) { + log::warn!( + "Duplicate key overrides previous definition: {}", + cx.error3(key.span) + ); + log::info!("Previous definition here: {}", cx.error3(prev.span)); } + used.insert(key); } diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs index 3ef8988b..e4de826e 100644 --- a/toml-config/src/lib.rs +++ b/toml-config/src/lib.rs @@ -6,7 +6,7 @@ mod toml; use { crate::config::{ parse_config, Action, Config, ConfigConnector, ConfigDrmDevice, ConfigKeymap, - ConnectorMatch, DrmDeviceMatch, Exec, Input, InputMatch, Output, OutputMatch, + ConnectorMatch, DrmDeviceMatch, Exec, Input, InputMatch, Output, OutputMatch, Shortcut, SimpleCommand, Status, Theme, }, ahash::{AHashMap, AHashSet}, @@ -541,21 +541,22 @@ impl State { } } - fn apply_shortcuts( - self: &Rc, - shortcuts: impl IntoIterator, - ) { + fn apply_shortcuts(self: &Rc, shortcuts: impl IntoIterator) { let mut binds = self.persistent.binds.borrow_mut(); - for (key, value) in shortcuts { + for shortcut in shortcuts { if let Action::SimpleCommand { cmd: SimpleCommand::None, - } = value + } = shortcut.action { - self.persistent.seat.unbind(key); - binds.remove(&key); + self.persistent.seat.unbind(shortcut.keysym); + binds.remove(&shortcut.keysym); } else { - self.persistent.seat.bind(key, value.into_fn(self)); - binds.insert(key); + self.persistent.seat.bind_masked( + shortcut.mask, + shortcut.keysym, + shortcut.action.into_fn(self), + ); + binds.insert(shortcut.keysym); } } } diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index 4e7e30d9..a2d88992 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -427,6 +427,21 @@ "type": "string", "description": "A color.\n\nThe format should be one of the following:\n\n- `#rgb`\n- `#rrggbb`\n- `#rgba`\n- `#rrggbba`\n" }, + "ComplexShortcut": { + "description": "Describes a complex shortcut.\n\n- Example:\n\n ```toml\n [complex-shortcuts.XF86AudioRaiseVolume]\n mod-mask = \"alt\"\n action = { type = \"exec\", exec = [\"pactl\", \"set-sink-volume\", \"0\", \"+10%\"] }\n ```\n", + "type": "object", + "properties": { + "mod-mask": { + "type": "string", + "description": "The mod mask to apply to this shortcut.\n\nShould be a string containing modifiers concatenated by `-`. See the description\nof `Config.shortcuts` for more details.\n\nIf this field is omitted, all modifiers are included in the mask.\n\n- Example:\n \n To raise the volume whenever the XF86AudioRaiseVolume key is pressed regardless\n of any modifiers except `alt`:\n\n ```toml\n [complex-shortcuts.XF86AudioRaiseVolume]\n mod-mask = \"alt\"\n action = { type = \"exec\", exec = [\"pactl\", \"set-sink-volume\", \"0\", \"+10%\"] }\n ```\n\n Set `mod-mask = \"\"` to ignore all modifiers.\n" + }, + "action": { + "description": "The action to execute.\n\nOmitting this is the same as setting it to `\"none\"`.\n", + "$ref": "#/$defs/Action" + } + }, + "required": [] + }, "Config": { "description": "This is the top-level table.\n\n- Example:\n\n ```toml\n keymap = \"\"\"\n xkb_keymap {\n xkb_keycodes { include \"evdev+aliases(qwerty)\" };\n xkb_types { include \"complete\" };\n xkb_compat { include \"complete\" };\n xkb_symbols { include \"pc+us+inet(evdev)\" };\n };\n \"\"\"\n\n on-graphics-initialized = { type = \"exec\", exec = \"mako\" }\n\n [shortcuts]\n alt-h = \"focus-left\"\n alt-j = \"focus-down\"\n alt-k = \"focus-up\"\n alt-l = \"focus-right\"\n\n alt-shift-h = \"move-left\"\n alt-shift-j = \"move-down\"\n alt-shift-k = \"move-up\"\n alt-shift-l = \"move-right\"\n\n alt-d = \"split-horizontal\"\n alt-v = \"split-vertical\"\n\n alt-t = \"toggle-split\"\n alt-m = \"toggle-mono\"\n alt-u = \"toggle-fullscreen\"\n\n alt-f = \"focus-parent\"\n alt-shift-c = \"close\"\n alt-shift-f = \"toggle-floating\"\n Super_L = { type = \"exec\", exec = \"alacritty\" }\n alt-p = { type = \"exec\", exec = \"bemenu-run\" }\n alt-q = \"quit\"\n alt-shift-r = \"reload-config-toml\"\n\n ctrl-alt-F1 = { type = \"switch-to-vt\", num = 1 }\n ctrl-alt-F2 = { type = \"switch-to-vt\", num = 2 }\n # ...\n\n alt-F1 = { type = \"show-workspace\", name = \"1\" }\n alt-F2 = { type = \"show-workspace\", name = \"2\" }\n # ...\n\n alt-shift-F1 = { type = \"move-to-workspace\", name = \"1\" }\n alt-shift-F2 = { type = \"move-to-workspace\", name = \"2\" }\n # ...\n ```\n", "type": "object", @@ -447,6 +462,14 @@ "$ref": "#/$defs/Action" } }, + "complex-shortcuts": { + "description": "Complex compositor shortcuts.\n\nThe keys should have the same format as in the `shortcuts` table.\n\n- Example:\n\n ```toml\n [complex-shortcuts.XF86AudioRaiseVolume]\n mod-mask = \"alt\"\n action = { type = \"exec\", exec = [\"pactl\", \"set-sink-volume\", \"0\", \"+10%\"] }\n ```\n", + "type": "object", + "additionalProperties": { + "description": "", + "$ref": "#/$defs/ComplexShortcut" + } + }, "on-graphics-initialized": { "description": "An action to execute when the graphics have been initialized for the first time.\n\nThis is a good place to start graphical applications.\n\n- Example:\n\n ```toml\n on-graphics-initialized = { type = \"exec\", exec = \"mako\" }\n ```\n", "$ref": "#/$defs/Action" diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index df621542..b36ca4d4 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -590,6 +590,56 @@ The format should be one of the following: Values of this type should be strings. + +### `ComplexShortcut` + +Describes a complex shortcut. + +- Example: + + ```toml + [complex-shortcuts.XF86AudioRaiseVolume] + mod-mask = "alt" + action = { type = "exec", exec = ["pactl", "set-sink-volume", "0", "+10%"] } + ``` + +Values of this type should be tables. + +The table has the following fields: + +- `mod-mask` (optional): + + The mod mask to apply to this shortcut. + + Should be a string containing modifiers concatenated by `-`. See the description + of `Config.shortcuts` for more details. + + If this field is omitted, all modifiers are included in the mask. + + - Example: + + To raise the volume whenever the XF86AudioRaiseVolume key is pressed regardless + of any modifiers except `alt`: + + ```toml + [complex-shortcuts.XF86AudioRaiseVolume] + mod-mask = "alt" + action = { type = "exec", exec = ["pactl", "set-sink-volume", "0", "+10%"] } + ``` + + Set `mod-mask = ""` to ignore all modifiers. + + The value of this field should be a string. + +- `action` (optional): + + The action to execute. + + Omitting this is the same as setting it to `"none"`. + + The value of this field should be a [Action](#types-Action). + + ### `Config` @@ -715,6 +765,22 @@ The table has the following fields: The value of this field should be a table whose values are [Actions](#types-Action). +- `complex-shortcuts` (optional): + + Complex compositor shortcuts. + + The keys should have the same format as in the `shortcuts` table. + + - Example: + + ```toml + [complex-shortcuts.XF86AudioRaiseVolume] + mod-mask = "alt" + action = { type = "exec", exec = ["pactl", "set-sink-volume", "0", "+10%"] } + ``` + + The value of this field should be a table whose values are [ComplexShortcuts](#types-ComplexShortcut). + - `on-graphics-initialized` (optional): An action to execute when the graphics have been initialized for the first time. diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index 67a4a929..2af75f33 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -1741,6 +1741,23 @@ Config: [shortcuts] alt-q = "quit" ``` + complex-shortcuts: + kind: map + values: + ref: ComplexShortcut + required: false + description: | + Complex compositor shortcuts. + + The keys should have the same format as in the `shortcuts` table. + + - Example: + + ```toml + [complex-shortcuts.XF86AudioRaiseVolume] + mod-mask = "alt" + action = { type = "exec", exec = ["pactl", "set-sink-volume", "0", "+10%"] } + ``` on-graphics-initialized: ref: Action required: false @@ -2069,3 +2086,48 @@ RepeatRate: required: true description: | The number of milliseconds after a key is pressed before repeating begins. + + +ComplexShortcut: + kind: table + description: | + Describes a complex shortcut. + + - Example: + + ```toml + [complex-shortcuts.XF86AudioRaiseVolume] + mod-mask = "alt" + action = { type = "exec", exec = ["pactl", "set-sink-volume", "0", "+10%"] } + ``` + fields: + mod-mask: + kind: string + required: false + description: | + The mod mask to apply to this shortcut. + + Should be a string containing modifiers concatenated by `-`. See the description + of `Config.shortcuts` for more details. + + If this field is omitted, all modifiers are included in the mask. + + - Example: + + To raise the volume whenever the XF86AudioRaiseVolume key is pressed regardless + of any modifiers except `alt`: + + ```toml + [complex-shortcuts.XF86AudioRaiseVolume] + mod-mask = "alt" + action = { type = "exec", exec = ["pactl", "set-sink-volume", "0", "+10%"] } + ``` + + Set `mod-mask = ""` to ignore all modifiers. + action: + ref: Action + required: false + description: | + The action to execute. + + Omitting this is the same as setting it to `"none"`. From 6f55675bdb2ea37869cd155b8047891866fba0b2 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Tue, 16 Apr 2024 18:47:40 +0200 Subject: [PATCH 3/3] config: implement shortcut latching --- docs/config.md | 17 ++ docs/features.md | 4 + jay-config/src/_private/client.rs | 171 ++++++++++++++++---- jay-config/src/_private/ipc.rs | 7 + jay-config/src/input.rs | 10 ++ src/config.rs | 36 +++-- src/ifs/wl_seat/event_handling.rs | 7 +- src/it/test_config.rs | 10 ++ toml-config/src/config.rs | 1 + toml-config/src/config/parsers/shortcuts.rs | 14 +- toml-config/src/lib.rs | 132 +++++++++------ toml-spec/spec/spec.generated.json | 4 + toml-spec/spec/spec.generated.md | 22 +++ toml-spec/spec/spec.yaml | 21 +++ 14 files changed, 365 insertions(+), 91 deletions(-) diff --git a/docs/config.md b/docs/config.md index 345b35b0..266bc349 100644 --- a/docs/config.md +++ b/docs/config.md @@ -208,6 +208,23 @@ The right-hand side should be an action. See [spec.generated.md](../toml-spec/spec/spec.generated.md) for a full list of actions. +### Complex Shortcuts + +If you need more control over shortcut execution, you can use the `complex-shortcuts` table. + +```toml +[complex-shortcuts.alt-x] +action = { type = "exec", exec = ["pactl", "set-sink-mute", "0", "1"] } +latch = { type = "exec", exec = ["pactl", "set-sink-mute", "0", "0"] } +``` + +This mutes the audio output while the key is pressed and un-mutes once the `x` key is released. +The order in which `alt` and `x` are released does not matter for this. + +This can also be used to implement push to talk. + +See the specification for more details. + ### Running Multiple Actions In every place that accepts an action, you can also run multiple actions by wrapping them diff --git a/docs/features.md b/docs/features.md index d9348a9e..5c01f338 100644 --- a/docs/features.md +++ b/docs/features.md @@ -108,6 +108,10 @@ By default, applications only have access to unprivileged protocols. You can explicitly opt into giving applications access to privileged protocols via the Jay CLI or shortcuts. +## Push to Talk + +Jay's shortcut system allows you to execute an action when a key is pressed and to execute a different action when the key is released. + ## Protocol Support Jay supports the following wayland protocols: diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index 3fc480e5..fcc42e91 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -13,6 +13,7 @@ use { input::{acceleration::AccelProfile, capability::Capability, InputDevice, Seat}, keyboard::{ mods::{Modifiers, RELEASE}, + syms::KeySym, Keymap, }, logging::LogLevel, @@ -68,8 +69,10 @@ fn ignore_panic(name: &str, f: impl FnOnce()) { } struct KeyHandler { - mask: Modifiers, - cb: Callback, + registered_mask: Modifiers, + cb_mask: Modifiers, + cb: Option, + latched: Vec>, } pub(crate) struct Client { @@ -96,6 +99,9 @@ pub(crate) struct Client { tasks: Tasks, status_task: Cell>>, i3bar_separator: RefCell>>, + pressed_keysym: Cell>, + + feat_mod_mask: Cell, } struct Interest { @@ -220,6 +226,8 @@ pub unsafe extern "C" fn init( tasks: Default::default(), status_task: Default::default(), i3bar_separator: Default::default(), + pressed_keysym: Cell::new(None), + feat_mod_mask: Cell::new(false), }); let init = slice::from_raw_parts(init, size); client.handle_init_msg(init); @@ -315,17 +323,16 @@ impl Client { pub fn unbind>(&self, seat: Seat, mod_sym: T) { let mod_sym = mod_sym.into(); - let deregister = self - .key_handlers - .borrow_mut() - .remove(&(seat, mod_sym)) - .is_some(); - if deregister { - self.send(&ClientMessage::RemoveShortcut { - seat, - mods: mod_sym.mods, - sym: mod_sym.sym, - }) + if let Entry::Occupied(mut oe) = self.key_handlers.borrow_mut().entry((seat, mod_sym)) { + oe.get_mut().cb = None; + if oe.get().latched.is_empty() { + oe.remove(); + self.send(&ClientMessage::RemoveShortcut { + seat, + mods: mod_sym.mods, + sym: mod_sym.sym, + }) + } } } @@ -923,6 +930,46 @@ impl Client { keymap } + pub fn latch(&self, seat: Seat, f: F) { + if !self.feat_mod_mask.get() { + log::error!("compositor does not support latching"); + return; + } + let Some(keysym) = self.pressed_keysym.get() else { + log::error!("latch called while not executing shortcut"); + return; + }; + let mods = RELEASE; + let f = Box::new(f); + let register = { + let mut kh = self.key_handlers.borrow_mut(); + match kh.entry((seat, mods | keysym)) { + Entry::Occupied(mut o) => { + let o = o.get_mut(); + o.latched.push(f); + mem::replace(&mut o.registered_mask, mods) != mods + } + Entry::Vacant(v) => { + v.insert(KeyHandler { + cb_mask: mods, + registered_mask: mods, + cb: None, + latched: vec![f], + }); + true + } + } + }; + if register { + self.send(&ClientMessage::AddShortcut2 { + seat, + mods, + mod_mask: mods, + sym: keysym, + }); + } + } + pub fn bind_masked( &self, seat: Seat, @@ -937,27 +984,37 @@ impl Client { match kh.entry((seat, mod_sym)) { Entry::Occupied(mut o) => { let o = o.get_mut(); - o.cb = cb; - mem::replace(&mut o.mask, mod_mask) != mod_mask + o.cb = Some(cb); + o.cb_mask = mod_mask; + let register = o.latched.is_empty() && o.registered_mask != o.cb_mask; + if register { + o.registered_mask = o.cb_mask; + } + register } Entry::Vacant(v) => { - v.insert(KeyHandler { mask: mod_mask, cb }); + v.insert(KeyHandler { + cb_mask: mod_mask, + registered_mask: mod_mask, + cb: Some(cb), + latched: vec![], + }); true } } }; if register { - let msg = if !mod_mask.0 == 0 { - ClientMessage::AddShortcut { + let msg = if self.feat_mod_mask.get() { + ClientMessage::AddShortcut2 { seat, mods: mod_sym.mods, + mod_mask, sym: mod_sym.sym, } } else { - ClientMessage::AddShortcut2 { + ClientMessage::AddShortcut { seat, mods: mod_sym.mods, - mod_mask, sym: mod_sym.sym, } }; @@ -1103,6 +1160,61 @@ impl Client { self.tasks.tasks.borrow_mut().remove(&id); } + fn handle_invoke_shortcut( + &self, + seat: Seat, + unmasked_mods: Modifiers, + mods: Modifiers, + sym: KeySym, + ) { + let ms = ModifiedKeySym { mods, sym }; + let handler = self + .key_handlers + .borrow_mut() + .get_mut(&(seat, ms)) + .map(|kh| { + let cb = if kh.cb_mask & unmasked_mods == mods { + kh.cb.clone() + } else { + None + }; + (mem::take(&mut kh.latched), cb) + }); + let Some((latched, handler)) = handler else { + return; + }; + let was_latched = !latched.is_empty(); + if (mods & RELEASE).0 == 0 { + self.pressed_keysym.set(Some(sym)); + } + for latched in latched { + ignore_panic("latch", latched); + } + if let Some(handler) = handler { + run_cb("shortcut", &handler, ()); + } + self.pressed_keysym.set(None); + if was_latched { + if let Entry::Occupied(mut oe) = self.key_handlers.borrow_mut().entry((seat, ms)) { + let o = oe.get_mut(); + if o.latched.is_empty() { + if o.cb.is_none() { + self.send(&ClientMessage::RemoveShortcut { seat, mods, sym }); + oe.remove(); + } else if o.cb_mask != o.registered_mask { + o.registered_mask = o.cb_mask; + self.send(&ClientMessage::AddShortcut2 { + seat, + mods: ms.mods, + mod_mask: o.cb_mask, + sym: ms.sym, + }); + } + } + } + } + } + fn handle_msg2(&self, msg: &[u8]) { let res = bincode_ops().deserialize::(msg); let msg = match res { @@ -1123,15 +1235,15 @@ impl Client { self.response.borrow_mut().push(response); } ServerMessage::InvokeShortcut { seat, mods, sym } => { - let ms = ModifiedKeySym { mods, sym }; - let handler = self - .key_handlers - .borrow_mut() - .get(&(seat, ms)) - .map(|k| k.cb.clone()); - if let Some(handler) = handler { - run_cb("shortcut", &handler, ()); - } + self.handle_invoke_shortcut(seat, mods, mods, sym); + } + ServerMessage::InvokeShortcut2 { + seat, + unmasked_mods, + effective_mods, + sym, + } => { + self.handle_invoke_shortcut(seat, unmasked_mods, effective_mods, sym); } ServerMessage::NewInputDevice { device } => { let handler = self.on_new_input_device.borrow_mut().clone(); @@ -1208,6 +1320,7 @@ impl Client { for feat in features { match feat { ServerFeature::NONE => {} + ServerFeature::MOD_MASK => self.feat_mod_mask.set(true), _ => {} } } diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index 67821e87..4d170c9a 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -19,6 +19,7 @@ pub struct ServerFeature(u16); impl ServerFeature { pub const NONE: Self = Self(0); + pub const MOD_MASK: Self = Self(1); } #[derive(Serialize, Deserialize, Debug)] @@ -73,6 +74,12 @@ pub enum ServerMessage { Features { features: Vec, }, + InvokeShortcut2 { + seat: Seat, + unmasked_mods: Modifiers, + effective_mods: Modifiers, + sym: KeySym, + }, } #[derive(Serialize, Deserialize, Debug)] diff --git a/jay-config/src/input.rs b/jay-config/src/input.rs index 2d18d7e5..e42b5787 100644 --- a/jay-config/src/input.rs +++ b/jay-config/src/input.rs @@ -216,6 +216,16 @@ impl Seat { get!().bind_masked(self, mod_mask, mod_sym.into(), f) } + /// Registers a callback to be executed when the currently pressed key is released. + /// + /// This should only be called in callbacks for key-press binds. + /// + /// The callback will be executed once when the key is released regardless of any + /// modifiers. + pub fn latch(self, f: F) { + get!().latch(self, f) + } + /// Unbinds a hotkey. pub fn unbind>(self, mod_sym: T) { get!().unbind(self, mod_sym.into()) diff --git a/src/config.rs b/src/config.rs index 8334d924..0305a4ad 100644 --- a/src/config.rs +++ b/src/config.rs @@ -17,11 +17,11 @@ use { jay_config::{ _private::{ bincode_ops, - ipc::{InitMessage, ServerMessage, V1InitMessage}, + ipc::{InitMessage, ServerFeature, ServerMessage, V1InitMessage}, ConfigEntry, VERSION, }, input::{InputDevice, Seat}, - keyboard::ModifiedKeySym, + keyboard::{mods::Modifiers, syms::KeySym}, video::{Connector, DrmDevice}, }, libloading::Library, @@ -63,12 +63,22 @@ impl ConfigProxy { } } - pub fn invoke_shortcut(&self, seat: SeatId, modsym: &ModifiedKeySym) { - self.send(&ServerMessage::InvokeShortcut { - seat: Seat(seat.raw() as _), - mods: modsym.mods, - sym: modsym.sym, - }); + pub fn invoke_shortcut(&self, seat: SeatId, shortcut: &InvokedShortcut) { + let msg = if shortcut.unmasked_mods == shortcut.effective_mods { + ServerMessage::InvokeShortcut { + seat: Seat(seat.raw() as _), + mods: shortcut.effective_mods, + sym: shortcut.sym, + } + } else { + ServerMessage::InvokeShortcut2 { + seat: Seat(seat.raw() as _), + unmasked_mods: shortcut.unmasked_mods, + effective_mods: shortcut.effective_mods, + sym: shortcut.sym, + } + }; + self.send(&msg); } pub fn new_drm_dev(&self, dev: DrmDeviceId) { @@ -203,7 +213,9 @@ impl ConfigProxy { } pub fn configure(&self, reload: bool) { - self.send(&ServerMessage::Features { features: vec![] }); + self.send(&ServerMessage::Features { + features: vec![ServerFeature::MOD_MASK], + }); self.send(&ServerMessage::Configure { reload }); } @@ -288,3 +300,9 @@ unsafe extern "C" fn handle_msg(data: *const u8, msg: *const u8, size: usize) { rc.handle_request(msg); mem::forget(rc); } + +pub struct InvokedShortcut { + pub unmasked_mods: Modifiers, + pub effective_mods: Modifiers, + pub sym: KeySym, +} diff --git a/src/ifs/wl_seat/event_handling.rs b/src/ifs/wl_seat/event_handling.rs index 614bc96e..50377725 100644 --- a/src/ifs/wl_seat/event_handling.rs +++ b/src/ifs/wl_seat/event_handling.rs @@ -2,6 +2,7 @@ use { crate::{ backend::{ConnectorId, InputEvent, KeyState, AXIS_120}, client::ClientId, + config::InvokedShortcut, fixed::Fixed, ifs::{ ipc::{ @@ -39,7 +40,6 @@ use { jay_config::keyboard::{ mods::{Modifiers, CAPS, NUM, RELEASE}, syms::KeySym, - ModifiedKeySym, }, smallvec::SmallVec, std::{cell::RefCell, collections::hash_map::Entry, rc::Rc}, @@ -386,8 +386,9 @@ impl WlSeatGlobal { if let Some(key_mods) = scs.get(&sym) { for (key_mods, mask) in key_mods { if mods & mask == key_mods { - shortcuts.push(ModifiedKeySym { - mods: Modifiers(key_mods), + shortcuts.push(InvokedShortcut { + unmasked_mods: Modifiers(mods), + effective_mods: Modifiers(key_mods), sym: KeySym(sym), }); } diff --git a/src/it/test_config.rs b/src/it/test_config.rs index 22273420..c33bfac6 100644 --- a/src/it/test_config.rs +++ b/src/it/test_config.rs @@ -95,6 +95,16 @@ unsafe extern "C" fn handle_msg(data: *const u8, msg: *const u8, size: usize) { tc.invoked_shortcuts .set((SeatId::from_raw(seat.0 as _), mods | sym), ()); } + ServerMessage::InvokeShortcut2 { + seat, + unmasked_mods, + effective_mods, + sym, + } => { + let _ = unmasked_mods; + tc.invoked_shortcuts + .set((SeatId::from_raw(seat.0 as _), effective_mods | sym), ()); + } ServerMessage::NewInputDevice { .. } => {} ServerMessage::DelInputDevice { .. } => {} ServerMessage::ConnectorConnect { .. } => {} diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index 00e18649..b4b047f7 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -285,6 +285,7 @@ pub struct Shortcut { pub mask: Modifiers, pub keysym: ModifiedKeySym, pub action: Action, + pub latch: Option, } #[derive(Debug, Clone)] diff --git a/toml-config/src/config/parsers/shortcuts.rs b/toml-config/src/config/parsers/shortcuts.rs index dc44967c..9bc63a83 100644 --- a/toml-config/src/config/parsers/shortcuts.rs +++ b/toml-config/src/config/parsers/shortcuts.rs @@ -34,6 +34,8 @@ pub enum ShortcutsParserError { ModMask(#[source] ModifiedKeysymParserError), #[error("Could not parse the action")] ActionParserError(#[source] ActionParserError), + #[error("Could not parse the latch action")] + LatchError(#[source] ActionParserError), } pub struct ShortcutsParser<'a, 'b> { @@ -65,6 +67,7 @@ impl Parser for ShortcutsParser<'_, '_> { mask: Modifiers(!0), keysym, action, + latch: None, }); } Ok(()) @@ -129,7 +132,8 @@ impl Parser for ComplexShortcutParser<'_> { table: &IndexMap, Spanned>, ) -> ParseResult { let mut ext = Extractor::new(self.cx, span, table); - let (mod_mask_val, action_val) = ext.extract((opt(str("mod-mask")), opt(val("action"))))?; + let (mod_mask_val, action_val, latch_val) = + ext.extract((opt(str("mod-mask")), opt(val("action")), opt(val("latch"))))?; let mod_mask = match mod_mask_val { None => Modifiers(!0), Some(v) => ModifiersParser @@ -144,10 +148,18 @@ impl Parser for ComplexShortcutParser<'_> { .parse(&mut ActionParser(self.cx)) .map_spanned_err(ShortcutsParserError::ActionParserError)?, }; + let mut latch = None; + if let Some(v) = latch_val { + latch = Some( + v.parse(&mut ActionParser(self.cx)) + .map_spanned_err(ShortcutsParserError::LatchError)?, + ); + } Ok(Shortcut { mask: mod_mask, keysym: self.keysym, action, + latch, }) } } diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs index e4de826e..16738249 100644 --- a/toml-config/src/lib.rs +++ b/toml-config/src/lib.rs @@ -37,51 +37,75 @@ fn default_seat() -> Seat { get_seat("default") } +trait FnBuilder: Sized { + fn new(f: F) -> Self; +} + +impl FnBuilder for Box { + fn new(f: F) -> Self { + Box::new(f) + } +} + +impl FnBuilder for Rc { + fn new(f: F) -> Self { + Rc::new(f) + } +} + impl Action { - fn into_fn(self, state: &Rc) -> Box { + fn into_fn(self, state: &Rc) -> Box { + self.into_fn_impl(state) + } + + fn into_rc_fn(self, state: &Rc) -> Rc { + self.into_fn_impl(state) + } + + fn into_fn_impl(self, state: &Rc) -> B { let s = state.persistent.seat; match self { Action::SimpleCommand { cmd } => match cmd { - SimpleCommand::Focus(dir) => Box::new(move || s.focus(dir)), - SimpleCommand::Move(dir) => Box::new(move || s.move_(dir)), - SimpleCommand::Split(axis) => Box::new(move || s.create_split(axis)), - SimpleCommand::ToggleSplit => Box::new(move || s.toggle_split()), - SimpleCommand::ToggleMono => Box::new(move || s.toggle_mono()), - SimpleCommand::ToggleFullscreen => Box::new(move || s.toggle_fullscreen()), - SimpleCommand::FocusParent => Box::new(move || s.focus_parent()), - SimpleCommand::Close => Box::new(move || s.close()), + SimpleCommand::Focus(dir) => B::new(move || s.focus(dir)), + SimpleCommand::Move(dir) => B::new(move || s.move_(dir)), + SimpleCommand::Split(axis) => B::new(move || s.create_split(axis)), + SimpleCommand::ToggleSplit => B::new(move || s.toggle_split()), + SimpleCommand::ToggleMono => B::new(move || s.toggle_mono()), + SimpleCommand::ToggleFullscreen => B::new(move || s.toggle_fullscreen()), + SimpleCommand::FocusParent => B::new(move || s.focus_parent()), + SimpleCommand::Close => B::new(move || s.close()), SimpleCommand::DisablePointerConstraint => { - Box::new(move || s.disable_pointer_constraint()) + B::new(move || s.disable_pointer_constraint()) } - SimpleCommand::ToggleFloating => Box::new(move || s.toggle_floating()), - SimpleCommand::Quit => Box::new(quit), + SimpleCommand::ToggleFloating => B::new(move || s.toggle_floating()), + SimpleCommand::Quit => B::new(quit), SimpleCommand::ReloadConfigToml => { let persistent = state.persistent.clone(); - Box::new(move || load_config(false, &persistent)) + B::new(move || load_config(false, &persistent)) } - SimpleCommand::ReloadConfigSo => Box::new(reload), - SimpleCommand::None => Box::new(|| ()), - SimpleCommand::Forward(bool) => Box::new(move || s.set_forward(bool)), + SimpleCommand::ReloadConfigSo => B::new(reload), + SimpleCommand::None => B::new(|| ()), + SimpleCommand::Forward(bool) => B::new(move || s.set_forward(bool)), }, Action::Multi { actions } => { - let mut actions: Vec<_> = actions.into_iter().map(|a| a.into_fn(state)).collect(); - Box::new(move || { - for action in &mut actions { + let actions: Vec<_> = actions.into_iter().map(|a| a.into_fn(state)).collect(); + B::new(move || { + for action in &actions { action(); } }) } - Action::Exec { exec } => Box::new(move || create_command(&exec).spawn()), - Action::SwitchToVt { num } => Box::new(move || switch_to_vt(num)), + Action::Exec { exec } => B::new(move || create_command(&exec).spawn()), + Action::SwitchToVt { num } => B::new(move || switch_to_vt(num)), Action::ShowWorkspace { name } => { let workspace = get_workspace(&name); - Box::new(move || s.show_workspace(workspace)) + B::new(move || s.show_workspace(workspace)) } Action::MoveToWorkspace { name } => { let workspace = get_workspace(&name); - Box::new(move || s.set_workspace(workspace)) + B::new(move || s.set_workspace(workspace)) } - Action::ConfigureConnector { con } => Box::new(move || { + Action::ConfigureConnector { con } => B::new(move || { for c in connectors() { if con.match_.matches(c) { con.apply(c); @@ -90,7 +114,7 @@ impl Action { }), Action::ConfigureInput { input } => { let state = state.clone(); - Box::new(move || { + B::new(move || { for c in input_devices() { if input.match_.matches(c, &state) { input.apply(c, &state); @@ -100,7 +124,7 @@ impl Action { } Action::ConfigureOutput { out } => { let state = state.clone(); - Box::new(move || { + B::new(move || { for c in connectors() { if out.match_.matches(c, &state) { out.apply(c); @@ -108,36 +132,36 @@ impl Action { } }) } - Action::SetEnv { env } => Box::new(move || { + Action::SetEnv { env } => B::new(move || { for (k, v) in &env { set_env(k, v); } }), - Action::UnsetEnv { env } => Box::new(move || { + Action::UnsetEnv { env } => B::new(move || { for k in &env { unset_env(k); } }), Action::SetKeymap { map } => { let state = state.clone(); - Box::new(move || state.set_keymap(&map)) + B::new(move || state.set_keymap(&map)) } Action::SetStatus { status } => { let state = state.clone(); - Box::new(move || state.set_status(&status)) + B::new(move || state.set_status(&status)) } Action::SetTheme { theme } => { let state = state.clone(); - Box::new(move || state.apply_theme(&theme)) + B::new(move || state.apply_theme(&theme)) } - Action::SetLogLevel { level } => Box::new(move || set_log_level(level)), - Action::SetGfxApi { api } => Box::new(move || set_gfx_api(api)), + Action::SetLogLevel { level } => B::new(move || set_log_level(level)), + Action::SetGfxApi { api } => B::new(move || set_gfx_api(api)), Action::ConfigureDirectScanout { enabled } => { - Box::new(move || set_direct_scanout_enabled(enabled)) + B::new(move || set_direct_scanout_enabled(enabled)) } Action::ConfigureDrmDevice { dev } => { let state = state.clone(); - Box::new(move || { + B::new(move || { for d in drm_devices() { if dev.match_.matches(d, &state) { dev.apply(d); @@ -147,7 +171,7 @@ impl Action { } Action::SetRenderDevice { dev } => { let state = state.clone(); - Box::new(move || { + B::new(move || { for d in drm_devices() { if dev.matches(d, &state) { d.make_render_device(); @@ -155,10 +179,10 @@ impl Action { } }) } - Action::ConfigureIdle { idle } => Box::new(move || set_idle(Some(idle))), + Action::ConfigureIdle { idle } => B::new(move || set_idle(Some(idle))), Action::MoveToOutput { output, workspace } => { let state = state.clone(); - Box::new(move || { + B::new(move || { let output = 'get_output: { for connector in connectors() { if connector.connected() && output.matches(connector, &state) { @@ -174,7 +198,7 @@ impl Action { }) } Action::SetRepeatRate { rate } => { - Box::new(move || s.set_repeat_rate(rate.rate, rate.delay)) + B::new(move || s.set_repeat_rate(rate.rate, rate.delay)) } } } @@ -548,16 +572,26 @@ impl State { cmd: SimpleCommand::None, } = shortcut.action { - self.persistent.seat.unbind(shortcut.keysym); - binds.remove(&shortcut.keysym); - } else { - self.persistent.seat.bind_masked( - shortcut.mask, - shortcut.keysym, - shortcut.action.into_fn(self), - ); - binds.insert(shortcut.keysym); - } + if shortcut.latch.is_none() { + self.persistent.seat.unbind(shortcut.keysym); + binds.remove(&shortcut.keysym); + continue; + } + } + let mut f = shortcut.action.into_fn(self); + if let Some(l) = shortcut.latch { + let l = l.into_rc_fn(self); + let s = self.persistent.seat; + f = Box::new(move || { + f(); + let l = l.clone(); + s.latch(move || l()); + }); + } + self.persistent + .seat + .bind_masked(shortcut.mask, shortcut.keysym, move || f()); + binds.insert(shortcut.keysym); } } diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index a2d88992..fd9672f8 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -438,6 +438,10 @@ "action": { "description": "The action to execute.\n\nOmitting this is the same as setting it to `\"none\"`.\n", "$ref": "#/$defs/Action" + }, + "latch": { + "description": "An action to execute when the key is released.\n\nThis registers an action to be executed when the key triggering the shortcut is\nreleased. The active modifiers are ignored for this purpose.\n\n- Example:\n\n To mute audio while the key is pressed:\n\n ```toml\n [complex-shortcuts.alt-x]\n action = { type = \"exec\", exec = [\"pactl\", \"set-sink-mute\", \"0\", \"1\"] }\n latch = { type = \"exec\", exec = [\"pactl\", \"set-sink-mute\", \"0\", \"0\"] }\n ```\n\n Audio will be un-muted once `x` key is released, regardless of any other keys\n that are pressed at the time.\n", + "$ref": "#/$defs/Action" } }, "required": [] diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index b36ca4d4..f0400838 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -639,6 +639,28 @@ The table has the following fields: The value of this field should be a [Action](#types-Action). +- `latch` (optional): + + An action to execute when the key is released. + + This registers an action to be executed when the key triggering the shortcut is + released. The active modifiers are ignored for this purpose. + + - Example: + + To mute audio while the key is pressed: + + ```toml + [complex-shortcuts.alt-x] + action = { type = "exec", exec = ["pactl", "set-sink-mute", "0", "1"] } + latch = { type = "exec", exec = ["pactl", "set-sink-mute", "0", "0"] } + ``` + + Audio will be un-muted once `x` key is released, regardless of any other keys + that are pressed at the time. + + The value of this field should be a [Action](#types-Action). + ### `Config` diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index 2af75f33..2b1ce4f2 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -2131,3 +2131,24 @@ ComplexShortcut: The action to execute. Omitting this is the same as setting it to `"none"`. + latch: + ref: Action + required: false + description: | + An action to execute when the key is released. + + This registers an action to be executed when the key triggering the shortcut is + released. The active modifiers are ignored for this purpose. + + - Example: + + To mute audio while the key is pressed: + + ```toml + [complex-shortcuts.alt-x] + action = { type = "exec", exec = ["pactl", "set-sink-mute", "0", "1"] } + latch = { type = "exec", exec = ["pactl", "set-sink-mute", "0", "0"] } + ``` + + Audio will be un-muted once `x` key is released, regardless of any other keys + that are pressed at the time.