config: add support for mod masks in shortcuts
This commit is contained in:
parent
27f30f8d28
commit
90dbde99ab
15 changed files with 501 additions and 92 deletions
|
|
@ -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<HashMap<(Seat, ModifiedKeySym), Callback>>,
|
||||
key_handlers: RefCell<HashMap<(Seat, ModifiedKeySym), KeyHandler>>,
|
||||
timer_handlers: RefCell<HashMap<Timer, Callback>>,
|
||||
response: RefCell<Vec<Response>>,
|
||||
on_new_seat: RefCell<Option<Callback<Seat>>>,
|
||||
|
|
@ -915,33 +923,45 @@ impl Client {
|
|||
keymap
|
||||
}
|
||||
|
||||
pub fn bind<T: Into<ModifiedKeySym>, F: FnMut() + 'static>(
|
||||
pub fn bind_masked<F: FnMut() + 'static>(
|
||||
&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, ());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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<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.
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Option<DroppedDnd>>,
|
||||
shortcuts: CopyHashMap<(u32, u32), Modifiers>,
|
||||
shortcuts: RefCell<AHashMap<u32, SmallMap<u32, u32, 2>>>,
|
||||
queue_link: Cell<Option<LinkedNode<Rc<Self>>>>,
|
||||
tree_changed_handler: Cell<Option<SpawnedFuture<()>>>,
|
||||
output: CloneCell<Rc<OutputNode>>,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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<ConfigKeymap>,
|
||||
pub repeat_rate: Option<RepeatRate>,
|
||||
pub shortcuts: Vec<(ModifiedKeySym, Action)>,
|
||||
pub shortcuts: Vec<Shortcut>,
|
||||
pub on_graphics_initialized: Option<Action>,
|
||||
pub on_idle: Option<Action>,
|
||||
pub status: Option<Status>,
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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<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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Spanned<ModifiedKeySym>>,
|
||||
pub shortcuts: &'b mut Vec<Shortcut>,
|
||||
}
|
||||
|
||||
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<String>, Spanned<Value>>,
|
||||
) -> ParseResult<Self> {
|
||||
let mut used_keys = HashSet::<Spanned<ModifiedKeySym>>::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 action = match value.parse(&mut ActionParser(self.0)) {
|
||||
Ok(a) => a,
|
||||
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<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) => {
|
||||
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<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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Self>,
|
||||
shortcuts: impl IntoIterator<Item = (ModifiedKeySym, Action)>,
|
||||
) {
|
||||
fn apply_shortcuts(self: &Rc<Self>, shortcuts: impl IntoIterator<Item = Shortcut>) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -590,6 +590,56 @@ The format should be one of the following:
|
|||
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>
|
||||
### `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.
|
||||
|
|
|
|||
|
|
@ -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"`.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue