From 6ca044ad30c30bdf5656f67fbc59bb82131667f3 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Tue, 16 Apr 2024 16:27:26 +0200 Subject: [PATCH] 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 | 64 +++++++ toml-spec/spec/spec.yaml | 60 ++++++ 15 files changed, 497 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..65f42372 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 repeat-rate = { rate = 25, delay = 250 }\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..4360c099 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -590,6 +590,54 @@ The format should be one of the following: Values of this type should be strings. + +### `ComplexShortcut` + +Describes a complex shortcut. + +- Example: + + ```toml + repeat-rate = { rate = 25, delay = 250 } + ``` + +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 +763,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..d225800e 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,46 @@ 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 + repeat-rate = { rate = 25, delay = 250 } + ``` + 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"`.