1
0
Fork 0
forked from wry/wry

Merge pull request #166 from mahkoh/jorth/latch

Implement push-to-talk via shortcuts
This commit is contained in:
mahkoh 2024-04-17 12:21:30 +02:00 committed by GitHub
commit a3a7874506
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 868 additions and 161 deletions

View file

@ -208,6 +208,23 @@ The right-hand side should be an action.
See [spec.generated.md](../toml-spec/spec/spec.generated.md) for a full list of actions.
### Complex Shortcuts
If you need more control over shortcut execution, you can use the `complex-shortcuts` table.
```toml
[complex-shortcuts.alt-x]
action = { type = "exec", exec = ["pactl", "set-sink-mute", "0", "1"] }
latch = { type = "exec", exec = ["pactl", "set-sink-mute", "0", "0"] }
```
This mutes the audio output while the key is pressed and un-mutes once the `x` key is released.
The order in which `alt` and `x` are released does not matter for this.
This can also be used to implement push to talk.
See the specification for more details.
### Running Multiple Actions
In every place that accepts an action, you can also run multiple actions by wrapping them

View file

@ -108,6 +108,10 @@ By default, applications only have access to unprivileged protocols.
You can explicitly opt into giving applications access to privileged protocols via the Jay CLI or shortcuts.
## Push to Talk
Jay's shortcut system allows you to execute an action when a key is pressed and to execute a different action when the key is released.
## Protocol Support
Jay supports the following wayland protocols:

View file

@ -4,12 +4,18 @@ use {
crate::{
_private::{
bincode_ops,
ipc::{ClientMessage, InitMessage, Response, ServerMessage, WorkspaceSource},
ipc::{
ClientMessage, InitMessage, Response, ServerFeature, ServerMessage, WorkspaceSource,
},
logging, Config, ConfigEntry, ConfigEntryGen, PollableId, WireMode, VERSION,
},
exec::Command,
input::{acceleration::AccelProfile, capability::Capability, InputDevice, Seat},
keyboard::Keymap,
keyboard::{
mods::{Modifiers, RELEASE},
syms::KeySym,
Keymap,
},
logging::LogLevel,
tasks::{JoinHandle, JoinSlot},
theme::{colors::Colorable, sized::Resizable, Color},
@ -62,12 +68,19 @@ fn ignore_panic(name: &str, f: impl FnOnce()) {
}
}
struct KeyHandler {
registered_mask: Modifiers,
cb_mask: Modifiers,
cb: Option<Callback>,
latched: Vec<Box<dyn FnOnce()>>,
}
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>>>,
@ -86,6 +99,9 @@ pub(crate) struct Client {
tasks: Tasks,
status_task: Cell<Vec<JoinHandle<()>>>,
i3bar_separator: RefCell<Option<Rc<String>>>,
pressed_keysym: Cell<Option<KeySym>>,
feat_mod_mask: Cell<bool>,
}
struct Interest {
@ -210,6 +226,8 @@ pub unsafe extern "C" fn init(
tasks: Default::default(),
status_task: Default::default(),
i3bar_separator: Default::default(),
pressed_keysym: Cell::new(None),
feat_mod_mask: Cell::new(false),
});
let init = slice::from_raw_parts(init, size);
client.handle_init_msg(init);
@ -305,17 +323,16 @@ impl Client {
pub fn unbind<T: Into<ModifiedKeySym>>(&self, seat: Seat, mod_sym: T) {
let mod_sym = mod_sym.into();
let deregister = self
.key_handlers
.borrow_mut()
.remove(&(seat, mod_sym))
.is_some();
if deregister {
self.send(&ClientMessage::RemoveShortcut {
seat,
mods: mod_sym.mods,
sym: mod_sym.sym,
})
if let Entry::Occupied(mut oe) = self.key_handlers.borrow_mut().entry((seat, mod_sym)) {
oe.get_mut().cb = None;
if oe.get().latched.is_empty() {
oe.remove();
self.send(&ClientMessage::RemoveShortcut {
seat,
mods: mod_sym.mods,
sym: mod_sym.sym,
})
}
}
}
@ -913,36 +930,98 @@ impl Client {
keymap
}
pub fn bind<T: Into<ModifiedKeySym>, F: FnMut() + 'static>(
&self,
seat: Seat,
mod_sym: T,
mut f: F,
) {
let mod_sym = mod_sym.into();
pub fn latch<F: FnOnce() + 'static>(&self, seat: Seat, f: F) {
if !self.feat_mod_mask.get() {
log::error!("compositor does not support latching");
return;
}
let Some(keysym) = self.pressed_keysym.get() else {
log::error!("latch called while not executing shortcut");
return;
};
let mods = RELEASE;
let f = Box::new(f);
let register = {
let mut kh = self.key_handlers.borrow_mut();
let f = cb(move |_| f());
match kh.entry((seat, mod_sym)) {
match kh.entry((seat, mods | keysym)) {
Entry::Occupied(mut o) => {
*o.get_mut() = f;
false
let o = o.get_mut();
o.latched.push(f);
mem::replace(&mut o.registered_mask, mods) != mods
}
Entry::Vacant(v) => {
v.insert(f);
v.insert(KeyHandler {
cb_mask: mods,
registered_mask: mods,
cb: None,
latched: vec![f],
});
true
}
}
};
if register {
self.send(&ClientMessage::AddShortcut {
self.send(&ClientMessage::AddShortcut2 {
seat,
mods: mod_sym.mods,
sym: mod_sym.sym,
mods,
mod_mask: mods,
sym: keysym,
});
}
}
pub fn bind_masked<F: FnMut() + 'static>(
&self,
seat: Seat,
mut mod_mask: Modifiers,
mod_sym: ModifiedKeySym,
mut f: F,
) {
mod_mask |= mod_sym.mods | RELEASE;
let register = {
let mut kh = self.key_handlers.borrow_mut();
let cb = cb(move |_| f());
match kh.entry((seat, mod_sym)) {
Entry::Occupied(mut o) => {
let o = o.get_mut();
o.cb = Some(cb);
o.cb_mask = mod_mask;
let register = o.latched.is_empty() && o.registered_mask != o.cb_mask;
if register {
o.registered_mask = o.cb_mask;
}
register
}
Entry::Vacant(v) => {
v.insert(KeyHandler {
cb_mask: mod_mask,
registered_mask: mod_mask,
cb: Some(cb),
latched: vec![],
});
true
}
}
};
if register {
let msg = if self.feat_mod_mask.get() {
ClientMessage::AddShortcut2 {
seat,
mods: mod_sym.mods,
mod_mask,
sym: mod_sym.sym,
}
} else {
ClientMessage::AddShortcut {
seat,
mods: mod_sym.mods,
sym: mod_sym.sym,
}
};
self.send(&msg);
}
}
pub fn log(&self, level: LogLevel, msg: &str, file: Option<&str>, line: Option<u32>) {
self.send(&ClientMessage::Log {
level,
@ -1081,6 +1160,61 @@ impl Client {
self.tasks.tasks.borrow_mut().remove(&id);
}
fn handle_invoke_shortcut(
&self,
seat: Seat,
unmasked_mods: Modifiers,
mods: Modifiers,
sym: KeySym,
) {
let ms = ModifiedKeySym { mods, sym };
let handler = self
.key_handlers
.borrow_mut()
.get_mut(&(seat, ms))
.map(|kh| {
let cb = if kh.cb_mask & unmasked_mods == mods {
kh.cb.clone()
} else {
None
};
(mem::take(&mut kh.latched), cb)
});
let Some((latched, handler)) = handler else {
return;
};
let was_latched = !latched.is_empty();
if (mods & RELEASE).0 == 0 {
self.pressed_keysym.set(Some(sym));
}
for latched in latched {
ignore_panic("latch", latched);
}
if let Some(handler) = handler {
run_cb("shortcut", &handler, ());
}
self.pressed_keysym.set(None);
if was_latched {
if let Entry::Occupied(mut oe) = self.key_handlers.borrow_mut().entry((seat, ms)) {
let o = oe.get_mut();
if o.latched.is_empty() {
if o.cb.is_none() {
self.send(&ClientMessage::RemoveShortcut { seat, mods, sym });
oe.remove();
} else if o.cb_mask != o.registered_mask {
o.registered_mask = o.cb_mask;
self.send(&ClientMessage::AddShortcut2 {
seat,
mods: ms.mods,
mod_mask: o.cb_mask,
sym: ms.sym,
});
}
}
}
}
}
fn handle_msg2(&self, msg: &[u8]) {
let res = bincode_ops().deserialize::<ServerMessage>(msg);
let msg = match res {
@ -1101,11 +1235,15 @@ impl Client {
self.response.borrow_mut().push(response);
}
ServerMessage::InvokeShortcut { seat, mods, sym } => {
let ms = ModifiedKeySym { mods, sym };
let handler = self.key_handlers.borrow_mut().get(&(seat, ms)).cloned();
if let Some(handler) = handler {
run_cb("shortcut", &handler, ());
}
self.handle_invoke_shortcut(seat, mods, mods, sym);
}
ServerMessage::InvokeShortcut2 {
seat,
unmasked_mods,
effective_mods,
sym,
} => {
self.handle_invoke_shortcut(seat, unmasked_mods, effective_mods, sym);
}
ServerMessage::NewInputDevice { device } => {
let handler = self.on_new_input_device.borrow_mut().clone();
@ -1178,6 +1316,15 @@ impl Client {
}
}
}
ServerMessage::Features { features } => {
for feat in features {
match feat {
ServerFeature::NONE => {}
ServerFeature::MOD_MASK => self.feat_mod_mask.set(true),
_ => {}
}
}
}
}
}

View file

@ -13,6 +13,15 @@ use {
std::time::Duration,
};
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
#[serde(transparent)]
pub struct ServerFeature(u16);
impl ServerFeature {
pub const NONE: Self = Self(0);
pub const MOD_MASK: Self = Self(1);
}
#[derive(Serialize, Deserialize, Debug)]
pub enum ServerMessage {
Configure {
@ -62,6 +71,15 @@ pub enum ServerMessage {
writable: bool,
res: Result<(), String>,
},
Features {
features: Vec<ServerFeature>,
},
InvokeShortcut2 {
seat: Seat,
unmasked_mods: Modifiers,
effective_mods: Modifiers,
sym: KeySym,
},
}
#[derive(Serialize, Deserialize, Debug)]
@ -440,6 +458,12 @@ pub enum ClientMessage<'a> {
seat: Seat,
forward: bool,
},
AddShortcut2 {
seat: Seat,
mods: Modifiers,
mod_mask: Modifiers,
sym: KeySym,
},
}
#[derive(Serialize, Deserialize, Debug)]

View file

@ -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,47 @@ 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)
}
/// Registers a callback to be executed when the currently pressed key is released.
///
/// This should only be called in callbacks for key-press binds.
///
/// The callback will be executed once when the key is released regardless of any
/// modifiers.
pub fn latch<F: FnOnce() + 'static>(self, f: F) {
get!().latch(self, 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.

View file

@ -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.

View file

@ -38,7 +38,8 @@
clippy::uninlined_format_args,
clippy::len_zero,
clippy::single_char_pattern,
clippy::single_char_add_str
clippy::single_char_add_str,
clippy::single_match
)]
use {

View file

@ -17,11 +17,11 @@ use {
jay_config::{
_private::{
bincode_ops,
ipc::{InitMessage, ServerMessage, V1InitMessage},
ipc::{InitMessage, ServerFeature, ServerMessage, V1InitMessage},
ConfigEntry, VERSION,
},
input::{InputDevice, Seat},
keyboard::ModifiedKeySym,
keyboard::{mods::Modifiers, syms::KeySym},
video::{Connector, DrmDevice},
},
libloading::Library,
@ -63,12 +63,22 @@ impl ConfigProxy {
}
}
pub fn invoke_shortcut(&self, seat: SeatId, modsym: &ModifiedKeySym) {
self.send(&ServerMessage::InvokeShortcut {
seat: Seat(seat.raw() as _),
mods: modsym.mods,
sym: modsym.sym,
});
pub fn invoke_shortcut(&self, seat: SeatId, shortcut: &InvokedShortcut) {
let msg = if shortcut.unmasked_mods == shortcut.effective_mods {
ServerMessage::InvokeShortcut {
seat: Seat(seat.raw() as _),
mods: shortcut.effective_mods,
sym: shortcut.sym,
}
} else {
ServerMessage::InvokeShortcut2 {
seat: Seat(seat.raw() as _),
unmasked_mods: shortcut.unmasked_mods,
effective_mods: shortcut.effective_mods,
sym: shortcut.sym,
}
};
self.send(&msg);
}
pub fn new_drm_dev(&self, dev: DrmDeviceId) {
@ -203,6 +213,9 @@ impl ConfigProxy {
}
pub fn configure(&self, reload: bool) {
self.send(&ServerMessage::Features {
features: vec![ServerFeature::MOD_MASK],
});
self.send(&ServerMessage::Configure { reload });
}
@ -287,3 +300,9 @@ unsafe extern "C" fn handle_msg(data: *const u8, msg: *const u8, size: usize) {
rc.handle_request(msg);
mem::forget(rc);
}
pub struct InvokedShortcut {
pub unmasked_mods: Modifiers,
pub effective_mods: Modifiers,
pub sym: KeySym,
}

View file

@ -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(())
}

View file

@ -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>>,

View file

@ -2,6 +2,7 @@ use {
crate::{
backend::{ConnectorId, InputEvent, KeyState, AXIS_120},
client::ClientId,
config::InvokedShortcut,
fixed::Fixed,
ifs::{
ipc::{
@ -39,10 +40,9 @@ use {
jay_config::keyboard::{
mods::{Modifiers, CAPS, NUM, RELEASE},
syms::KeySym,
ModifiedKeySym,
},
smallvec::SmallVec,
std::{cell::RefCell, rc::Rc},
std::{cell::RefCell, collections::hash_map::Entry, rc::Rc},
};
#[derive(Default)]
@ -380,13 +380,19 @@ 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(InvokedShortcut {
unmasked_mods: Modifiers(mods),
effective_mods: Modifiers(key_mods),
sym: KeySym(sym),
});
}
}
}
}
}
@ -608,15 +614,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) {

View file

@ -95,6 +95,16 @@ unsafe extern "C" fn handle_msg(data: *const u8, msg: *const u8, size: usize) {
tc.invoked_shortcuts
.set((SeatId::from_raw(seat.0 as _), mods | sym), ());
}
ServerMessage::InvokeShortcut2 {
seat,
unmasked_mods,
effective_mods,
sym,
} => {
let _ = unmasked_mods;
tc.invoked_shortcuts
.set((SeatId::from_raw(seat.0 as _), effective_mods | sym), ());
}
ServerMessage::NewInputDevice { .. } => {}
ServerMessage::DelInputDevice { .. } => {}
ServerMessage::ConnectorConnect { .. } => {}
@ -109,6 +119,7 @@ unsafe extern "C" fn handle_msg(data: *const u8, msg: *const u8, size: usize) {
ServerMessage::Idle => {}
ServerMessage::DevicesEnumerated => {}
ServerMessage::InterestReady { .. } => {}
ServerMessage::Features { .. } => {}
}
}

View file

@ -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,19 @@ pub struct RepeatRate {
pub delay: i32,
}
#[derive(Debug, Clone)]
pub struct Shortcut {
pub mask: Modifiers,
pub keysym: ModifiedKeySym,
pub action: Action,
pub latch: Option<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>,

View file

@ -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() {

View file

@ -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)
}

View file

@ -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,24 @@ 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),
#[error("Could not parse the latch action")]
LatchError(#[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 +54,147 @@ 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,
latch: None,
});
}
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, latch_val) =
ext.extract((opt(str("mod-mask")), opt(val("action")), opt(val("latch"))))?;
let mod_mask = match mod_mask_val {
None => Modifiers(!0),
Some(v) => ModifiersParser
.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)?,
};
let mut latch = None;
if let Some(v) = latch_val {
latch = Some(
v.parse(&mut ActionParser(self.cx))
.map_spanned_err(ShortcutsParserError::LatchError)?,
);
}
Ok(Shortcut {
mask: mod_mask,
keysym: self.keysym,
action,
latch,
})
}
}
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 {
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},
@ -37,51 +37,75 @@ fn default_seat() -> Seat {
get_seat("default")
}
trait FnBuilder: Sized {
fn new<F: Fn() + 'static>(f: F) -> Self;
}
impl FnBuilder for Box<dyn Fn()> {
fn new<F: Fn() + 'static>(f: F) -> Self {
Box::new(f)
}
}
impl FnBuilder for Rc<dyn Fn()> {
fn new<F: Fn() + 'static>(f: F) -> Self {
Rc::new(f)
}
}
impl Action {
fn into_fn(self, state: &Rc<State>) -> Box<dyn FnMut()> {
fn into_fn(self, state: &Rc<State>) -> Box<dyn Fn()> {
self.into_fn_impl(state)
}
fn into_rc_fn(self, state: &Rc<State>) -> Rc<dyn Fn()> {
self.into_fn_impl(state)
}
fn into_fn_impl<B: FnBuilder>(self, state: &Rc<State>) -> B {
let s = state.persistent.seat;
match self {
Action::SimpleCommand { cmd } => match cmd {
SimpleCommand::Focus(dir) => Box::new(move || s.focus(dir)),
SimpleCommand::Move(dir) => Box::new(move || s.move_(dir)),
SimpleCommand::Split(axis) => Box::new(move || s.create_split(axis)),
SimpleCommand::ToggleSplit => Box::new(move || s.toggle_split()),
SimpleCommand::ToggleMono => Box::new(move || s.toggle_mono()),
SimpleCommand::ToggleFullscreen => Box::new(move || s.toggle_fullscreen()),
SimpleCommand::FocusParent => Box::new(move || s.focus_parent()),
SimpleCommand::Close => Box::new(move || s.close()),
SimpleCommand::Focus(dir) => B::new(move || s.focus(dir)),
SimpleCommand::Move(dir) => B::new(move || s.move_(dir)),
SimpleCommand::Split(axis) => B::new(move || s.create_split(axis)),
SimpleCommand::ToggleSplit => B::new(move || s.toggle_split()),
SimpleCommand::ToggleMono => B::new(move || s.toggle_mono()),
SimpleCommand::ToggleFullscreen => B::new(move || s.toggle_fullscreen()),
SimpleCommand::FocusParent => B::new(move || s.focus_parent()),
SimpleCommand::Close => B::new(move || s.close()),
SimpleCommand::DisablePointerConstraint => {
Box::new(move || s.disable_pointer_constraint())
B::new(move || s.disable_pointer_constraint())
}
SimpleCommand::ToggleFloating => Box::new(move || s.toggle_floating()),
SimpleCommand::Quit => Box::new(quit),
SimpleCommand::ToggleFloating => B::new(move || s.toggle_floating()),
SimpleCommand::Quit => B::new(quit),
SimpleCommand::ReloadConfigToml => {
let persistent = state.persistent.clone();
Box::new(move || load_config(false, &persistent))
B::new(move || load_config(false, &persistent))
}
SimpleCommand::ReloadConfigSo => Box::new(reload),
SimpleCommand::None => Box::new(|| ()),
SimpleCommand::Forward(bool) => Box::new(move || s.set_forward(bool)),
SimpleCommand::ReloadConfigSo => B::new(reload),
SimpleCommand::None => B::new(|| ()),
SimpleCommand::Forward(bool) => B::new(move || s.set_forward(bool)),
},
Action::Multi { actions } => {
let mut actions: Vec<_> = actions.into_iter().map(|a| a.into_fn(state)).collect();
Box::new(move || {
for action in &mut actions {
let actions: Vec<_> = actions.into_iter().map(|a| a.into_fn(state)).collect();
B::new(move || {
for action in &actions {
action();
}
})
}
Action::Exec { exec } => Box::new(move || create_command(&exec).spawn()),
Action::SwitchToVt { num } => Box::new(move || switch_to_vt(num)),
Action::Exec { exec } => B::new(move || create_command(&exec).spawn()),
Action::SwitchToVt { num } => B::new(move || switch_to_vt(num)),
Action::ShowWorkspace { name } => {
let workspace = get_workspace(&name);
Box::new(move || s.show_workspace(workspace))
B::new(move || s.show_workspace(workspace))
}
Action::MoveToWorkspace { name } => {
let workspace = get_workspace(&name);
Box::new(move || s.set_workspace(workspace))
B::new(move || s.set_workspace(workspace))
}
Action::ConfigureConnector { con } => Box::new(move || {
Action::ConfigureConnector { con } => B::new(move || {
for c in connectors() {
if con.match_.matches(c) {
con.apply(c);
@ -90,7 +114,7 @@ impl Action {
}),
Action::ConfigureInput { input } => {
let state = state.clone();
Box::new(move || {
B::new(move || {
for c in input_devices() {
if input.match_.matches(c, &state) {
input.apply(c, &state);
@ -100,7 +124,7 @@ impl Action {
}
Action::ConfigureOutput { out } => {
let state = state.clone();
Box::new(move || {
B::new(move || {
for c in connectors() {
if out.match_.matches(c, &state) {
out.apply(c);
@ -108,36 +132,36 @@ impl Action {
}
})
}
Action::SetEnv { env } => Box::new(move || {
Action::SetEnv { env } => B::new(move || {
for (k, v) in &env {
set_env(k, v);
}
}),
Action::UnsetEnv { env } => Box::new(move || {
Action::UnsetEnv { env } => B::new(move || {
for k in &env {
unset_env(k);
}
}),
Action::SetKeymap { map } => {
let state = state.clone();
Box::new(move || state.set_keymap(&map))
B::new(move || state.set_keymap(&map))
}
Action::SetStatus { status } => {
let state = state.clone();
Box::new(move || state.set_status(&status))
B::new(move || state.set_status(&status))
}
Action::SetTheme { theme } => {
let state = state.clone();
Box::new(move || state.apply_theme(&theme))
B::new(move || state.apply_theme(&theme))
}
Action::SetLogLevel { level } => Box::new(move || set_log_level(level)),
Action::SetGfxApi { api } => Box::new(move || set_gfx_api(api)),
Action::SetLogLevel { level } => B::new(move || set_log_level(level)),
Action::SetGfxApi { api } => B::new(move || set_gfx_api(api)),
Action::ConfigureDirectScanout { enabled } => {
Box::new(move || set_direct_scanout_enabled(enabled))
B::new(move || set_direct_scanout_enabled(enabled))
}
Action::ConfigureDrmDevice { dev } => {
let state = state.clone();
Box::new(move || {
B::new(move || {
for d in drm_devices() {
if dev.match_.matches(d, &state) {
dev.apply(d);
@ -147,7 +171,7 @@ impl Action {
}
Action::SetRenderDevice { dev } => {
let state = state.clone();
Box::new(move || {
B::new(move || {
for d in drm_devices() {
if dev.matches(d, &state) {
d.make_render_device();
@ -155,10 +179,10 @@ impl Action {
}
})
}
Action::ConfigureIdle { idle } => Box::new(move || set_idle(Some(idle))),
Action::ConfigureIdle { idle } => B::new(move || set_idle(Some(idle))),
Action::MoveToOutput { output, workspace } => {
let state = state.clone();
Box::new(move || {
B::new(move || {
let output = 'get_output: {
for connector in connectors() {
if connector.connected() && output.matches(connector, &state) {
@ -174,7 +198,7 @@ impl Action {
})
}
Action::SetRepeatRate { rate } => {
Box::new(move || s.set_repeat_rate(rate.rate, rate.delay))
B::new(move || s.set_repeat_rate(rate.rate, rate.delay))
}
}
}
@ -541,22 +565,33 @@ 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);
} else {
self.persistent.seat.bind(key, value.into_fn(self));
binds.insert(key);
if shortcut.latch.is_none() {
self.persistent.seat.unbind(shortcut.keysym);
binds.remove(&shortcut.keysym);
continue;
}
}
let mut f = shortcut.action.into_fn(self);
if let Some(l) = shortcut.latch {
let l = l.into_rc_fn(self);
let s = self.persistent.seat;
f = Box::new(move || {
f();
let l = l.clone();
s.latch(move || l());
});
}
self.persistent
.seat
.bind_masked(shortcut.mask, shortcut.keysym, move || f());
binds.insert(shortcut.keysym);
}
}

View file

@ -427,6 +427,25 @@
"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"
},
"latch": {
"description": "An action to execute when the key is released.\n\nThis registers an action to be executed when the key triggering the shortcut is\nreleased. The active modifiers are ignored for this purpose.\n\n- Example:\n\n To mute audio while the key is pressed:\n\n ```toml\n [complex-shortcuts.alt-x]\n action = { type = \"exec\", exec = [\"pactl\", \"set-sink-mute\", \"0\", \"1\"] }\n latch = { type = \"exec\", exec = [\"pactl\", \"set-sink-mute\", \"0\", \"0\"] }\n ```\n\n Audio will be un-muted once `x` key is released, regardless of any other keys\n that are pressed at the time.\n",
"$ref": "#/$defs/Action"
}
},
"required": []
},
"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 +466,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"

View file

@ -590,6 +590,78 @@ 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).
- `latch` (optional):
An action to execute when the key is released.
This registers an action to be executed when the key triggering the shortcut is
released. The active modifiers are ignored for this purpose.
- Example:
To mute audio while the key is pressed:
```toml
[complex-shortcuts.alt-x]
action = { type = "exec", exec = ["pactl", "set-sink-mute", "0", "1"] }
latch = { type = "exec", exec = ["pactl", "set-sink-mute", "0", "0"] }
```
Audio will be un-muted once `x` key is released, regardless of any other keys
that are pressed at the time.
The value of this field should be a [Action](#types-Action).
<a name="types-Config"></a>
### `Config`
@ -715,6 +787,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.

View file

@ -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,69 @@ 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"`.
latch:
ref: Action
required: false
description: |
An action to execute when the key is released.
This registers an action to be executed when the key triggering the shortcut is
released. The active modifiers are ignored for this purpose.
- Example:
To mute audio while the key is pressed:
```toml
[complex-shortcuts.alt-x]
action = { type = "exec", exec = ["pactl", "set-sink-mute", "0", "1"] }
latch = { type = "exec", exec = ["pactl", "set-sink-mute", "0", "0"] }
```
Audio will be un-muted once `x` key is released, regardless of any other keys
that are pressed at the time.