1
0
Fork 0
forked from wry/wry

config: add support for mod masks in shortcuts

This commit is contained in:
Julian Orth 2024-04-16 16:27:26 +02:00
parent 27f30f8d28
commit 90dbde99ab
15 changed files with 501 additions and 92 deletions

View file

@ -11,7 +11,10 @@ use {
}, },
exec::Command, exec::Command,
input::{acceleration::AccelProfile, capability::Capability, InputDevice, Seat}, input::{acceleration::AccelProfile, capability::Capability, InputDevice, Seat},
keyboard::Keymap, keyboard::{
mods::{Modifiers, RELEASE},
Keymap,
},
logging::LogLevel, logging::LogLevel,
tasks::{JoinHandle, JoinSlot}, tasks::{JoinHandle, JoinSlot},
theme::{colors::Colorable, sized::Resizable, Color}, 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 { pub(crate) struct Client {
configure: extern "C" fn(), configure: extern "C" fn(),
srv_data: *const u8, srv_data: *const u8,
srv_unref: unsafe extern "C" fn(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), srv_handler: unsafe extern "C" fn(data: *const u8, msg: *const u8, size: usize),
key_handlers: RefCell<HashMap<(Seat, ModifiedKeySym), Callback>>, key_handlers: RefCell<HashMap<(Seat, ModifiedKeySym), KeyHandler>>,
timer_handlers: RefCell<HashMap<Timer, Callback>>, timer_handlers: RefCell<HashMap<Timer, Callback>>,
response: RefCell<Vec<Response>>, response: RefCell<Vec<Response>>,
on_new_seat: RefCell<Option<Callback<Seat>>>, on_new_seat: RefCell<Option<Callback<Seat>>>,
@ -915,33 +923,45 @@ impl Client {
keymap keymap
} }
pub fn bind<T: Into<ModifiedKeySym>, F: FnMut() + 'static>( pub fn bind_masked<F: FnMut() + 'static>(
&self, &self,
seat: Seat, seat: Seat,
mod_sym: T, mut mod_mask: Modifiers,
mod_sym: ModifiedKeySym,
mut f: F, mut f: F,
) { ) {
let mod_sym = mod_sym.into(); mod_mask |= mod_sym.mods | RELEASE;
let register = { let register = {
let mut kh = self.key_handlers.borrow_mut(); let mut kh = self.key_handlers.borrow_mut();
let f = cb(move |_| f()); let cb = cb(move |_| f());
match kh.entry((seat, mod_sym)) { match kh.entry((seat, mod_sym)) {
Entry::Occupied(mut o) => { Entry::Occupied(mut o) => {
*o.get_mut() = f; let o = o.get_mut();
false o.cb = cb;
mem::replace(&mut o.mask, mod_mask) != mod_mask
} }
Entry::Vacant(v) => { Entry::Vacant(v) => {
v.insert(f); v.insert(KeyHandler { mask: mod_mask, cb });
true true
} }
} }
}; };
if register { if register {
self.send(&ClientMessage::AddShortcut { let msg = if !mod_mask.0 == 0 {
seat, ClientMessage::AddShortcut {
mods: mod_sym.mods, seat,
sym: mod_sym.sym, 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 } => { ServerMessage::InvokeShortcut { seat, mods, sym } => {
let ms = ModifiedKeySym { 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 { if let Some(handler) = handler {
run_cb("shortcut", &handler, ()); run_cb("shortcut", &handler, ());
} }

View file

@ -451,6 +451,12 @@ pub enum ClientMessage<'a> {
seat: Seat, seat: Seat,
forward: bool, forward: bool,
}, },
AddShortcut2 {
seat: Seat,
mods: Modifiers,
mod_mask: Modifiers,
sym: KeySym,
},
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]

View file

@ -6,7 +6,7 @@ pub mod capability;
use { use {
crate::{ crate::{
input::{acceleration::AccelProfile, capability::Capability}, input::{acceleration::AccelProfile, capability::Capability},
keyboard::Keymap, keyboard::{mods::Modifiers, Keymap},
Axis, Direction, ModifiedKeySym, Workspace, Axis, Direction, ModifiedKeySym, Workspace,
_private::{ipc::WorkspaceSource, DEFAULT_SEAT_NAME}, _private::{ipc::WorkspaceSource, DEFAULT_SEAT_NAME},
video::Connector, video::Connector,
@ -188,12 +188,37 @@ impl Seat {
/// CapsLock and NumLock are ignored during modifier evaluation. Therefore, bindings /// CapsLock and NumLock are ignored during modifier evaluation. Therefore, bindings
/// containing these modifiers will never be invoked. /// containing these modifiers will never be invoked.
pub fn bind<T: Into<ModifiedKeySym>, F: FnMut() + 'static>(self, mod_sym: T, f: F) { pub fn bind<T: Into<ModifiedKeySym>, 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<T: Into<ModifiedKeySym>, 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. /// Unbinds a hotkey.
pub fn unbind<T: Into<ModifiedKeySym>>(self, mod_sym: T) { pub fn unbind<T: Into<ModifiedKeySym>>(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. /// Moves the keyboard focus of the seat in the specified direction.

View file

@ -10,6 +10,11 @@ use {
#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Default, Hash, Debug)] #[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Default, Hash, Debug)]
pub struct Modifiers(pub u32); pub struct Modifiers(pub u32);
impl Modifiers {
/// No modifiers.
pub const NONE: Self = Modifiers(0);
}
/// The Shift modifier /// The Shift modifier
pub const SHIFT: Modifiers = Modifiers(1 << 0); pub const SHIFT: Modifiers = Modifiers(1 << 0);
/// The CapsLock modifier. /// The CapsLock modifier.

View file

@ -1127,11 +1127,12 @@ impl ConfigProxyHandler {
fn handle_add_shortcut( fn handle_add_shortcut(
&self, &self,
seat: Seat, seat: Seat,
mod_mask: Modifiers,
mods: Modifiers, mods: Modifiers,
sym: KeySym, sym: KeySym,
) -> Result<(), CphError> { ) -> Result<(), CphError> {
let seat = self.get_seat(seat)?; let seat = self.get_seat(seat)?;
seat.add_shortcut(mods, sym); seat.add_shortcut(mod_mask, mods, sym);
Ok(()) Ok(())
} }
@ -1499,7 +1500,7 @@ impl ConfigProxyHandler {
self.handle_set_split(seat, axis).wrn("set_split")? self.handle_set_split(seat, axis).wrn("set_split")?
} }
ClientMessage::AddShortcut { seat, mods, sym } => self ClientMessage::AddShortcut { seat, mods, sym } => self
.handle_add_shortcut(seat, mods, sym) .handle_add_shortcut(seat, Modifiers(!0), mods, sym)
.wrn("add_shortcut")?, .wrn("add_shortcut")?,
ClientMessage::RemoveShortcut { seat, mods, sym } => self ClientMessage::RemoveShortcut { seat, mods, sym } => self
.handle_remove_shortcut(seat, mods, sym) .handle_remove_shortcut(seat, mods, sym)
@ -1773,6 +1774,14 @@ impl ConfigProxyHandler {
ClientMessage::SetForward { seat, forward } => { ClientMessage::SetForward { seat, forward } => {
self.handle_set_forward(seat, forward).wrn("set_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(()) Ok(())
} }

View file

@ -73,7 +73,6 @@ use {
xkbcommon::{DynKeyboardState, KeyboardState, KeymapId, XkbKeymap, XkbState}, xkbcommon::{DynKeyboardState, KeyboardState, KeymapId, XkbKeymap, XkbState},
}, },
ahash::AHashMap, ahash::AHashMap,
jay_config::keyboard::mods::Modifiers,
smallvec::SmallVec, smallvec::SmallVec,
std::{ std::{
cell::{Cell, RefCell}, cell::{Cell, RefCell},
@ -160,7 +159,7 @@ pub struct WlSeatGlobal {
pointer_owner: PointerOwnerHolder, pointer_owner: PointerOwnerHolder,
kb_owner: KbOwnerHolder, kb_owner: KbOwnerHolder,
dropped_dnd: RefCell<Option<DroppedDnd>>, dropped_dnd: RefCell<Option<DroppedDnd>>,
shortcuts: CopyHashMap<(u32, u32), Modifiers>, shortcuts: RefCell<AHashMap<u32, SmallMap<u32, u32, 2>>>,
queue_link: Cell<Option<LinkedNode<Rc<Self>>>>, queue_link: Cell<Option<LinkedNode<Rc<Self>>>>,
tree_changed_handler: Cell<Option<SpawnedFuture<()>>>, tree_changed_handler: Cell<Option<SpawnedFuture<()>>>,
output: CloneCell<Rc<OutputNode>>, output: CloneCell<Rc<OutputNode>>,

View file

@ -42,7 +42,7 @@ use {
ModifiedKeySym, ModifiedKeySym,
}, },
smallvec::SmallVec, smallvec::SmallVec,
std::{cell::RefCell, rc::Rc}, std::{cell::RefCell, collections::hash_map::Entry, rc::Rc},
}; };
#[derive(Default)] #[derive(Default)]
@ -380,13 +380,18 @@ impl WlSeatGlobal {
if state == wl_keyboard::RELEASED { if state == wl_keyboard::RELEASED {
mods |= RELEASE.0; mods |= RELEASE.0;
} }
let scs = &*self.shortcuts.borrow();
let keysyms = xkb_state.unmodified_keysyms(key); let keysyms = xkb_state.unmodified_keysyms(key);
for &sym in keysyms { for &sym in keysyms {
if let Some(mods) = self.shortcuts.get(&(mods, sym)) { if let Some(key_mods) = scs.get(&sym) {
shortcuts.push(ModifiedKeySym { for (key_mods, mask) in key_mods {
mods, if mods & mask == key_mods {
sym: KeySym(sym), shortcuts.push(ModifiedKeySym {
}); mods: Modifiers(key_mods),
sym: KeySym(sym),
});
}
}
} }
} }
} }
@ -608,15 +613,24 @@ impl WlSeatGlobal {
} }
pub fn clear_shortcuts(&self) { pub fn clear_shortcuts(&self) {
self.shortcuts.clear(); self.shortcuts.borrow_mut().clear();
} }
pub fn add_shortcut(&self, mods: Modifiers, keysym: KeySym) { pub fn add_shortcut(&self, mod_mask: Modifiers, mods: Modifiers, keysym: KeySym) {
self.shortcuts.set((mods.0, keysym.0), mods); self.shortcuts
.borrow_mut()
.entry(keysym.0)
.or_default()
.insert(mods.0, mod_mask.0);
} }
pub fn remove_shortcut(&self, mods: Modifiers, keysym: KeySym) { 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) { pub fn trigger_tree_changed(&self) {

View file

@ -17,7 +17,7 @@ use {
}, },
jay_config::{ jay_config::{
input::acceleration::AccelProfile, input::acceleration::AccelProfile,
keyboard::{Keymap, ModifiedKeySym}, keyboard::{mods::Modifiers, Keymap, ModifiedKeySym},
logging::LogLevel, logging::LogLevel,
status::MessageFormat, status::MessageFormat,
theme::Color, theme::Color,
@ -280,11 +280,18 @@ pub struct RepeatRate {
pub delay: i32, pub delay: i32,
} }
#[derive(Debug, Clone)]
pub struct Shortcut {
pub mask: Modifiers,
pub keysym: ModifiedKeySym,
pub action: Action,
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Config { pub struct Config {
pub keymap: Option<ConfigKeymap>, pub keymap: Option<ConfigKeymap>,
pub repeat_rate: Option<RepeatRate>, pub repeat_rate: Option<RepeatRate>,
pub shortcuts: Vec<(ModifiedKeySym, Action)>, pub shortcuts: Vec<Shortcut>,
pub on_graphics_initialized: Option<Action>, pub on_graphics_initialized: Option<Action>,
pub on_idle: Option<Action>, pub on_idle: Option<Action>,
pub status: Option<Status>, pub status: Option<Status>,

View file

@ -17,7 +17,7 @@ use {
log_level::LogLevelParser, log_level::LogLevelParser,
output::OutputsParser, output::OutputsParser,
repeat_rate::RepeatRateParser, repeat_rate::RepeatRateParser,
shortcuts::{ShortcutsParser, ShortcutsParserError}, shortcuts::{ComplexShortcutsParser, ShortcutsParser, ShortcutsParserError},
status::StatusParser, status::StatusParser,
theme::ThemeParser, theme::ThemeParser,
}, },
@ -30,6 +30,7 @@ use {
}, },
}, },
indexmap::IndexMap, indexmap::IndexMap,
std::collections::HashSet,
thiserror::Error, thiserror::Error,
}; };
@ -96,7 +97,7 @@ impl Parser for ConfigParser<'_> {
_, _,
idle_val, idle_val,
), ),
(explicit_sync, repeat_rate_val), (explicit_sync, repeat_rate_val, complex_shortcuts_val),
) = ext.extract(( ) = ext.extract((
( (
opt(val("keymap")), opt(val("keymap")),
@ -122,7 +123,11 @@ impl Parser for ConfigParser<'_> {
opt(val("$schema")), opt(val("$schema")),
opt(val("idle")), 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; let mut keymap = None;
if let Some(value) = keymap_val { if let Some(value) = keymap_val {
@ -136,10 +141,24 @@ impl Parser for ConfigParser<'_> {
} }
} }
} }
let mut used_keys = HashSet::new();
let mut shortcuts = vec![]; let mut shortcuts = vec![];
if let Some(value) = shortcuts_val { if let Some(value) = shortcuts_val {
shortcuts = value value
.parse(&mut ShortcutsParser(self.0)) .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)?; .map_spanned_err(ConfigParserError::ParseShortcuts)?;
} }
if shortcuts.is_empty() { if shortcuts.is_empty() {

View file

@ -26,6 +26,8 @@ pub enum ModifiedKeysymParserError {
MissingSym, MissingSym,
#[error("Unknown keysym {0}")] #[error("Unknown keysym {0}")]
UnknownKeysym(String), UnknownKeysym(String),
#[error("Unknown modifier {0}")]
UnknownModifier(String),
} }
pub struct ModifiedKeysymParser; pub struct ModifiedKeysymParser;
@ -39,20 +41,8 @@ impl Parser for ModifiedKeysymParser {
let mut modifiers = Modifiers(0); let mut modifiers = Modifiers(0);
let mut sym = None; let mut sym = None;
for part in string.split("-") { for part in string.split("-") {
let modifier = match part { let modifier = match parse_mod(part) {
"shift" => SHIFT, Some(m) => m,
"lock" => LOCK,
"ctrl" => CTRL,
"mod1" => MOD1,
"mod2" => MOD2,
"mod3" => MOD3,
"mod4" => MOD4,
"mod5" => MOD5,
"caps" => CAPS,
"alt" => ALT,
"num" => NUM,
"logo" => LOGO,
"release" => RELEASE,
_ => match KEYSYMS.get(part) { _ => match KEYSYMS.get(part) {
Some(new) if sym.is_none() => { Some(new) if sym.is_none() => {
sym = Some(*new); 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<Self> {
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<Modifiers> {
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)
}

View file

@ -2,9 +2,16 @@ use {
crate::{ crate::{
config::{ config::{
context::Context, context::Context,
extractor::{opt, str, val, Extractor, ExtractorError},
parser::{DataType, ParseResult, Parser, UnexpectedDataType}, parser::{DataType, ParseResult, Parser, UnexpectedDataType},
parsers::{action::ActionParser, modified_keysym::ModifiedKeysymParser}, parsers::{
Action, action::{ActionParser, ActionParserError},
modified_keysym::{
ModifiedKeysymParser, ModifiedKeysymParserError, ModifiersParser,
},
},
spanned::SpannedErrorExt,
Action, Shortcut, SimpleCommand,
}, },
toml::{ toml::{
toml_span::{Span, Spanned, SpannedExt}, toml_span::{Span, Spanned, SpannedExt},
@ -12,7 +19,7 @@ use {
}, },
}, },
indexmap::IndexMap, indexmap::IndexMap,
jay_config::keyboard::ModifiedKeySym, jay_config::keyboard::{mods::Modifiers, ModifiedKeySym},
std::collections::HashSet, std::collections::HashSet,
thiserror::Error, thiserror::Error,
}; };
@ -21,12 +28,22 @@ use {
pub enum ShortcutsParserError { pub enum ShortcutsParserError {
#[error(transparent)] #[error(transparent)]
Expected(#[from] UnexpectedDataType), 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<Spanned<ModifiedKeySym>>,
pub shortcuts: &'b mut Vec<Shortcut>,
}
impl Parser for ShortcutsParser<'_> { impl Parser for ShortcutsParser<'_, '_> {
type Value = Vec<(ModifiedKeySym, Action)>; type Value = ();
type Error = ShortcutsParserError; type Error = ShortcutsParserError;
const EXPECTED: &'static [DataType] = &[DataType::Table]; const EXPECTED: &'static [DataType] = &[DataType::Table];
@ -35,38 +52,137 @@ impl Parser for ShortcutsParser<'_> {
_span: Span, _span: Span,
table: &IndexMap<Spanned<String>, Spanned<Value>>, table: &IndexMap<Spanned<String>, Spanned<Value>>,
) -> ParseResult<Self> { ) -> ParseResult<Self> {
let mut used_keys = HashSet::<Spanned<ModifiedKeySym>>::new();
let mut res = vec![];
for (key, value) in table.iter() { for (key, value) in table.iter() {
let keysym = match ModifiedKeysymParser.parse_string(key.span, &key.value) { let Some(keysym) = parse_modified_keysym(self.cx, key) else {
Ok(k) => k, continue;
Err(e) => {
log::warn!("Could not parse keysym: {}", self.0.error(e));
continue;
}
}; };
let action = match value.parse(&mut ActionParser(self.0)) { let Some(action) = parse_action(self.cx, &key.value, value) else {
Ok(a) => a, 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<Spanned<ModifiedKeySym>>,
pub shortcuts: &'b mut Vec<Shortcut>,
}
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<String>, Spanned<Value>>,
) -> ParseResult<Self> {
for (key, value) in table.iter() {
let Some(keysym) = parse_modified_keysym(self.cx, key) else {
continue;
};
let shortcut = match value.parse(&mut ComplexShortcutParser {
keysym,
cx: self.cx,
}) {
Ok(v) => v,
Err(e) => { Err(e) => {
log::warn!( log::warn!(
"Could not parse action for keysym {}: {}", "Could not parse shortcut for keysym {}: {}",
key.value, key.value,
self.0.error(e) self.cx.error(e)
); );
continue; continue;
} }
}; };
let spanned = keysym.spanned(key.span); let spanned = keysym.spanned(key.span);
if let Some(prev) = used_keys.get(&spanned) { log_used(self.cx, self.used_keys, spanned);
log::warn!( self.shortcuts.push(shortcut);
"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));
} }
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<String>, Spanned<Value>>,
) -> ParseResult<Self> {
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<Value>) -> Option<Action> {
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<String>) -> Option<ModifiedKeySym> {
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<Spanned<ModifiedKeySym>>,
key: Spanned<ModifiedKeySym>,
) {
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);
}

View file

@ -6,7 +6,7 @@ mod toml;
use { use {
crate::config::{ crate::config::{
parse_config, Action, Config, ConfigConnector, ConfigDrmDevice, ConfigKeymap, 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, SimpleCommand, Status, Theme,
}, },
ahash::{AHashMap, AHashSet}, ahash::{AHashMap, AHashSet},
@ -541,21 +541,22 @@ impl State {
} }
} }
fn apply_shortcuts( fn apply_shortcuts(self: &Rc<Self>, shortcuts: impl IntoIterator<Item = Shortcut>) {
self: &Rc<Self>,
shortcuts: impl IntoIterator<Item = (ModifiedKeySym, Action)>,
) {
let mut binds = self.persistent.binds.borrow_mut(); let mut binds = self.persistent.binds.borrow_mut();
for (key, value) in shortcuts { for shortcut in shortcuts {
if let Action::SimpleCommand { if let Action::SimpleCommand {
cmd: SimpleCommand::None, cmd: SimpleCommand::None,
} = value } = shortcut.action
{ {
self.persistent.seat.unbind(key); self.persistent.seat.unbind(shortcut.keysym);
binds.remove(&key); binds.remove(&shortcut.keysym);
} else { } else {
self.persistent.seat.bind(key, value.into_fn(self)); self.persistent.seat.bind_masked(
binds.insert(key); shortcut.mask,
shortcut.keysym,
shortcut.action.into_fn(self),
);
binds.insert(shortcut.keysym);
} }
} }
} }

View file

@ -427,6 +427,21 @@
"type": "string", "type": "string",
"description": "A color.\n\nThe format should be one of the following:\n\n- `#rgb`\n- `#rrggbb`\n- `#rgba`\n- `#rrggbba`\n" "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": { "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", "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", "type": "object",
@ -447,6 +462,14 @@
"$ref": "#/$defs/Action" "$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": { "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", "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" "$ref": "#/$defs/Action"

View file

@ -590,6 +590,56 @@ The format should be one of the following:
Values of this type should be strings. Values of this type should be strings.
<a name="types-ComplexShortcut"></a>
### `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).
<a name="types-Config"></a> <a name="types-Config"></a>
### `Config` ### `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). 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): - `on-graphics-initialized` (optional):
An action to execute when the graphics have been initialized for the first time. An action to execute when the graphics have been initialized for the first time.

View file

@ -1741,6 +1741,23 @@ Config:
[shortcuts] [shortcuts]
alt-q = "quit" 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: on-graphics-initialized:
ref: Action ref: Action
required: false required: false
@ -2069,3 +2086,48 @@ RepeatRate:
required: true required: true
description: | description: |
The number of milliseconds after a key is pressed before repeating begins. 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"`.