config: implement shortcut latching
This commit is contained in:
parent
90dbde99ab
commit
6f55675bdb
14 changed files with 367 additions and 93 deletions
|
|
@ -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.
|
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
|
### Running Multiple Actions
|
||||||
|
|
||||||
In every place that accepts an action, you can also run multiple actions by wrapping them
|
In every place that accepts an action, you can also run multiple actions by wrapping them
|
||||||
|
|
|
||||||
|
|
@ -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.
|
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
|
## Protocol Support
|
||||||
|
|
||||||
Jay supports the following wayland protocols:
|
Jay supports the following wayland protocols:
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ use {
|
||||||
input::{acceleration::AccelProfile, capability::Capability, InputDevice, Seat},
|
input::{acceleration::AccelProfile, capability::Capability, InputDevice, Seat},
|
||||||
keyboard::{
|
keyboard::{
|
||||||
mods::{Modifiers, RELEASE},
|
mods::{Modifiers, RELEASE},
|
||||||
|
syms::KeySym,
|
||||||
Keymap,
|
Keymap,
|
||||||
},
|
},
|
||||||
logging::LogLevel,
|
logging::LogLevel,
|
||||||
|
|
@ -68,8 +69,10 @@ fn ignore_panic(name: &str, f: impl FnOnce()) {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct KeyHandler {
|
struct KeyHandler {
|
||||||
mask: Modifiers,
|
registered_mask: Modifiers,
|
||||||
cb: Callback,
|
cb_mask: Modifiers,
|
||||||
|
cb: Option<Callback>,
|
||||||
|
latched: Vec<Box<dyn FnOnce()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct Client {
|
pub(crate) struct Client {
|
||||||
|
|
@ -96,6 +99,9 @@ pub(crate) struct Client {
|
||||||
tasks: Tasks,
|
tasks: Tasks,
|
||||||
status_task: Cell<Vec<JoinHandle<()>>>,
|
status_task: Cell<Vec<JoinHandle<()>>>,
|
||||||
i3bar_separator: RefCell<Option<Rc<String>>>,
|
i3bar_separator: RefCell<Option<Rc<String>>>,
|
||||||
|
pressed_keysym: Cell<Option<KeySym>>,
|
||||||
|
|
||||||
|
feat_mod_mask: Cell<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Interest {
|
struct Interest {
|
||||||
|
|
@ -220,6 +226,8 @@ pub unsafe extern "C" fn init(
|
||||||
tasks: Default::default(),
|
tasks: Default::default(),
|
||||||
status_task: Default::default(),
|
status_task: Default::default(),
|
||||||
i3bar_separator: 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);
|
let init = slice::from_raw_parts(init, size);
|
||||||
client.handle_init_msg(init);
|
client.handle_init_msg(init);
|
||||||
|
|
@ -315,17 +323,16 @@ impl Client {
|
||||||
|
|
||||||
pub fn unbind<T: Into<ModifiedKeySym>>(&self, seat: Seat, mod_sym: T) {
|
pub fn unbind<T: Into<ModifiedKeySym>>(&self, seat: Seat, mod_sym: T) {
|
||||||
let mod_sym = mod_sym.into();
|
let mod_sym = mod_sym.into();
|
||||||
let deregister = self
|
if let Entry::Occupied(mut oe) = self.key_handlers.borrow_mut().entry((seat, mod_sym)) {
|
||||||
.key_handlers
|
oe.get_mut().cb = None;
|
||||||
.borrow_mut()
|
if oe.get().latched.is_empty() {
|
||||||
.remove(&(seat, mod_sym))
|
oe.remove();
|
||||||
.is_some();
|
self.send(&ClientMessage::RemoveShortcut {
|
||||||
if deregister {
|
seat,
|
||||||
self.send(&ClientMessage::RemoveShortcut {
|
mods: mod_sym.mods,
|
||||||
seat,
|
sym: mod_sym.sym,
|
||||||
mods: mod_sym.mods,
|
})
|
||||||
sym: mod_sym.sym,
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -923,6 +930,46 @@ impl Client {
|
||||||
keymap
|
keymap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
match kh.entry((seat, mods | keysym)) {
|
||||||
|
Entry::Occupied(mut o) => {
|
||||||
|
let o = o.get_mut();
|
||||||
|
o.latched.push(f);
|
||||||
|
mem::replace(&mut o.registered_mask, mods) != mods
|
||||||
|
}
|
||||||
|
Entry::Vacant(v) => {
|
||||||
|
v.insert(KeyHandler {
|
||||||
|
cb_mask: mods,
|
||||||
|
registered_mask: mods,
|
||||||
|
cb: None,
|
||||||
|
latched: vec![f],
|
||||||
|
});
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if register {
|
||||||
|
self.send(&ClientMessage::AddShortcut2 {
|
||||||
|
seat,
|
||||||
|
mods,
|
||||||
|
mod_mask: mods,
|
||||||
|
sym: keysym,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn bind_masked<F: FnMut() + 'static>(
|
pub fn bind_masked<F: FnMut() + 'static>(
|
||||||
&self,
|
&self,
|
||||||
seat: Seat,
|
seat: Seat,
|
||||||
|
|
@ -937,29 +984,39 @@ impl Client {
|
||||||
match kh.entry((seat, mod_sym)) {
|
match kh.entry((seat, mod_sym)) {
|
||||||
Entry::Occupied(mut o) => {
|
Entry::Occupied(mut o) => {
|
||||||
let o = o.get_mut();
|
let o = o.get_mut();
|
||||||
o.cb = cb;
|
o.cb = Some(cb);
|
||||||
mem::replace(&mut o.mask, mod_mask) != mod_mask
|
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) => {
|
Entry::Vacant(v) => {
|
||||||
v.insert(KeyHandler { mask: mod_mask, cb });
|
v.insert(KeyHandler {
|
||||||
|
cb_mask: mod_mask,
|
||||||
|
registered_mask: mod_mask,
|
||||||
|
cb: Some(cb),
|
||||||
|
latched: vec![],
|
||||||
|
});
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if register {
|
if register {
|
||||||
let msg = if !mod_mask.0 == 0 {
|
let msg = if self.feat_mod_mask.get() {
|
||||||
ClientMessage::AddShortcut {
|
|
||||||
seat,
|
|
||||||
mods: mod_sym.mods,
|
|
||||||
sym: mod_sym.sym,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ClientMessage::AddShortcut2 {
|
ClientMessage::AddShortcut2 {
|
||||||
seat,
|
seat,
|
||||||
mods: mod_sym.mods,
|
mods: mod_sym.mods,
|
||||||
mod_mask,
|
mod_mask,
|
||||||
sym: mod_sym.sym,
|
sym: mod_sym.sym,
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
ClientMessage::AddShortcut {
|
||||||
|
seat,
|
||||||
|
mods: mod_sym.mods,
|
||||||
|
sym: mod_sym.sym,
|
||||||
|
}
|
||||||
};
|
};
|
||||||
self.send(&msg);
|
self.send(&msg);
|
||||||
}
|
}
|
||||||
|
|
@ -1103,6 +1160,61 @@ impl Client {
|
||||||
self.tasks.tasks.borrow_mut().remove(&id);
|
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]) {
|
fn handle_msg2(&self, msg: &[u8]) {
|
||||||
let res = bincode_ops().deserialize::<ServerMessage>(msg);
|
let res = bincode_ops().deserialize::<ServerMessage>(msg);
|
||||||
let msg = match res {
|
let msg = match res {
|
||||||
|
|
@ -1123,15 +1235,15 @@ impl Client {
|
||||||
self.response.borrow_mut().push(response);
|
self.response.borrow_mut().push(response);
|
||||||
}
|
}
|
||||||
ServerMessage::InvokeShortcut { seat, mods, sym } => {
|
ServerMessage::InvokeShortcut { seat, mods, sym } => {
|
||||||
let ms = ModifiedKeySym { mods, sym };
|
self.handle_invoke_shortcut(seat, mods, mods, sym);
|
||||||
let handler = self
|
}
|
||||||
.key_handlers
|
ServerMessage::InvokeShortcut2 {
|
||||||
.borrow_mut()
|
seat,
|
||||||
.get(&(seat, ms))
|
unmasked_mods,
|
||||||
.map(|k| k.cb.clone());
|
effective_mods,
|
||||||
if let Some(handler) = handler {
|
sym,
|
||||||
run_cb("shortcut", &handler, ());
|
} => {
|
||||||
}
|
self.handle_invoke_shortcut(seat, unmasked_mods, effective_mods, sym);
|
||||||
}
|
}
|
||||||
ServerMessage::NewInputDevice { device } => {
|
ServerMessage::NewInputDevice { device } => {
|
||||||
let handler = self.on_new_input_device.borrow_mut().clone();
|
let handler = self.on_new_input_device.borrow_mut().clone();
|
||||||
|
|
@ -1208,6 +1320,7 @@ impl Client {
|
||||||
for feat in features {
|
for feat in features {
|
||||||
match feat {
|
match feat {
|
||||||
ServerFeature::NONE => {}
|
ServerFeature::NONE => {}
|
||||||
|
ServerFeature::MOD_MASK => self.feat_mod_mask.set(true),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ pub struct ServerFeature(u16);
|
||||||
|
|
||||||
impl ServerFeature {
|
impl ServerFeature {
|
||||||
pub const NONE: Self = Self(0);
|
pub const NONE: Self = Self(0);
|
||||||
|
pub const MOD_MASK: Self = Self(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
|
@ -73,6 +74,12 @@ pub enum ServerMessage {
|
||||||
Features {
|
Features {
|
||||||
features: Vec<ServerFeature>,
|
features: Vec<ServerFeature>,
|
||||||
},
|
},
|
||||||
|
InvokeShortcut2 {
|
||||||
|
seat: Seat,
|
||||||
|
unmasked_mods: Modifiers,
|
||||||
|
effective_mods: Modifiers,
|
||||||
|
sym: KeySym,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
|
|
||||||
|
|
@ -216,6 +216,16 @@ impl Seat {
|
||||||
get!().bind_masked(self, mod_mask, mod_sym.into(), 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.
|
/// 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.into())
|
get!().unbind(self, mod_sym.into())
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,11 @@ use {
|
||||||
jay_config::{
|
jay_config::{
|
||||||
_private::{
|
_private::{
|
||||||
bincode_ops,
|
bincode_ops,
|
||||||
ipc::{InitMessage, ServerMessage, V1InitMessage},
|
ipc::{InitMessage, ServerFeature, ServerMessage, V1InitMessage},
|
||||||
ConfigEntry, VERSION,
|
ConfigEntry, VERSION,
|
||||||
},
|
},
|
||||||
input::{InputDevice, Seat},
|
input::{InputDevice, Seat},
|
||||||
keyboard::ModifiedKeySym,
|
keyboard::{mods::Modifiers, syms::KeySym},
|
||||||
video::{Connector, DrmDevice},
|
video::{Connector, DrmDevice},
|
||||||
},
|
},
|
||||||
libloading::Library,
|
libloading::Library,
|
||||||
|
|
@ -63,12 +63,22 @@ impl ConfigProxy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn invoke_shortcut(&self, seat: SeatId, modsym: &ModifiedKeySym) {
|
pub fn invoke_shortcut(&self, seat: SeatId, shortcut: &InvokedShortcut) {
|
||||||
self.send(&ServerMessage::InvokeShortcut {
|
let msg = if shortcut.unmasked_mods == shortcut.effective_mods {
|
||||||
seat: Seat(seat.raw() as _),
|
ServerMessage::InvokeShortcut {
|
||||||
mods: modsym.mods,
|
seat: Seat(seat.raw() as _),
|
||||||
sym: modsym.sym,
|
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) {
|
pub fn new_drm_dev(&self, dev: DrmDeviceId) {
|
||||||
|
|
@ -203,7 +213,9 @@ impl ConfigProxy {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn configure(&self, reload: bool) {
|
pub fn configure(&self, reload: bool) {
|
||||||
self.send(&ServerMessage::Features { features: vec![] });
|
self.send(&ServerMessage::Features {
|
||||||
|
features: vec![ServerFeature::MOD_MASK],
|
||||||
|
});
|
||||||
self.send(&ServerMessage::Configure { reload });
|
self.send(&ServerMessage::Configure { reload });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -288,3 +300,9 @@ unsafe extern "C" fn handle_msg(data: *const u8, msg: *const u8, size: usize) {
|
||||||
rc.handle_request(msg);
|
rc.handle_request(msg);
|
||||||
mem::forget(rc);
|
mem::forget(rc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct InvokedShortcut {
|
||||||
|
pub unmasked_mods: Modifiers,
|
||||||
|
pub effective_mods: Modifiers,
|
||||||
|
pub sym: KeySym,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ use {
|
||||||
crate::{
|
crate::{
|
||||||
backend::{ConnectorId, InputEvent, KeyState, AXIS_120},
|
backend::{ConnectorId, InputEvent, KeyState, AXIS_120},
|
||||||
client::ClientId,
|
client::ClientId,
|
||||||
|
config::InvokedShortcut,
|
||||||
fixed::Fixed,
|
fixed::Fixed,
|
||||||
ifs::{
|
ifs::{
|
||||||
ipc::{
|
ipc::{
|
||||||
|
|
@ -39,7 +40,6 @@ use {
|
||||||
jay_config::keyboard::{
|
jay_config::keyboard::{
|
||||||
mods::{Modifiers, CAPS, NUM, RELEASE},
|
mods::{Modifiers, CAPS, NUM, RELEASE},
|
||||||
syms::KeySym,
|
syms::KeySym,
|
||||||
ModifiedKeySym,
|
|
||||||
},
|
},
|
||||||
smallvec::SmallVec,
|
smallvec::SmallVec,
|
||||||
std::{cell::RefCell, collections::hash_map::Entry, rc::Rc},
|
std::{cell::RefCell, collections::hash_map::Entry, rc::Rc},
|
||||||
|
|
@ -386,8 +386,9 @@ impl WlSeatGlobal {
|
||||||
if let Some(key_mods) = scs.get(&sym) {
|
if let Some(key_mods) = scs.get(&sym) {
|
||||||
for (key_mods, mask) in key_mods {
|
for (key_mods, mask) in key_mods {
|
||||||
if mods & mask == key_mods {
|
if mods & mask == key_mods {
|
||||||
shortcuts.push(ModifiedKeySym {
|
shortcuts.push(InvokedShortcut {
|
||||||
mods: Modifiers(key_mods),
|
unmasked_mods: Modifiers(mods),
|
||||||
|
effective_mods: Modifiers(key_mods),
|
||||||
sym: KeySym(sym),
|
sym: KeySym(sym),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,16 @@ unsafe extern "C" fn handle_msg(data: *const u8, msg: *const u8, size: usize) {
|
||||||
tc.invoked_shortcuts
|
tc.invoked_shortcuts
|
||||||
.set((SeatId::from_raw(seat.0 as _), mods | sym), ());
|
.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::NewInputDevice { .. } => {}
|
||||||
ServerMessage::DelInputDevice { .. } => {}
|
ServerMessage::DelInputDevice { .. } => {}
|
||||||
ServerMessage::ConnectorConnect { .. } => {}
|
ServerMessage::ConnectorConnect { .. } => {}
|
||||||
|
|
|
||||||
|
|
@ -285,6 +285,7 @@ pub struct Shortcut {
|
||||||
pub mask: Modifiers,
|
pub mask: Modifiers,
|
||||||
pub keysym: ModifiedKeySym,
|
pub keysym: ModifiedKeySym,
|
||||||
pub action: Action,
|
pub action: Action,
|
||||||
|
pub latch: Option<Action>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,8 @@ pub enum ShortcutsParserError {
|
||||||
ModMask(#[source] ModifiedKeysymParserError),
|
ModMask(#[source] ModifiedKeysymParserError),
|
||||||
#[error("Could not parse the action")]
|
#[error("Could not parse the action")]
|
||||||
ActionParserError(#[source] ActionParserError),
|
ActionParserError(#[source] ActionParserError),
|
||||||
|
#[error("Could not parse the latch action")]
|
||||||
|
LatchError(#[source] ActionParserError),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ShortcutsParser<'a, 'b> {
|
pub struct ShortcutsParser<'a, 'b> {
|
||||||
|
|
@ -65,6 +67,7 @@ impl Parser for ShortcutsParser<'_, '_> {
|
||||||
mask: Modifiers(!0),
|
mask: Modifiers(!0),
|
||||||
keysym,
|
keysym,
|
||||||
action,
|
action,
|
||||||
|
latch: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -129,7 +132,8 @@ impl Parser for ComplexShortcutParser<'_> {
|
||||||
table: &IndexMap<Spanned<String>, Spanned<Value>>,
|
table: &IndexMap<Spanned<String>, Spanned<Value>>,
|
||||||
) -> ParseResult<Self> {
|
) -> ParseResult<Self> {
|
||||||
let mut ext = Extractor::new(self.cx, span, table);
|
let mut ext = Extractor::new(self.cx, span, table);
|
||||||
let (mod_mask_val, action_val) = ext.extract((opt(str("mod-mask")), opt(val("action"))))?;
|
let (mod_mask_val, action_val, latch_val) =
|
||||||
|
ext.extract((opt(str("mod-mask")), opt(val("action")), opt(val("latch"))))?;
|
||||||
let mod_mask = match mod_mask_val {
|
let mod_mask = match mod_mask_val {
|
||||||
None => Modifiers(!0),
|
None => Modifiers(!0),
|
||||||
Some(v) => ModifiersParser
|
Some(v) => ModifiersParser
|
||||||
|
|
@ -144,10 +148,18 @@ impl Parser for ComplexShortcutParser<'_> {
|
||||||
.parse(&mut ActionParser(self.cx))
|
.parse(&mut ActionParser(self.cx))
|
||||||
.map_spanned_err(ShortcutsParserError::ActionParserError)?,
|
.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 {
|
Ok(Shortcut {
|
||||||
mask: mod_mask,
|
mask: mod_mask,
|
||||||
keysym: self.keysym,
|
keysym: self.keysym,
|
||||||
action,
|
action,
|
||||||
|
latch,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,51 +37,75 @@ fn default_seat() -> Seat {
|
||||||
get_seat("default")
|
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 {
|
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;
|
let s = state.persistent.seat;
|
||||||
match self {
|
match self {
|
||||||
Action::SimpleCommand { cmd } => match cmd {
|
Action::SimpleCommand { cmd } => match cmd {
|
||||||
SimpleCommand::Focus(dir) => Box::new(move || s.focus(dir)),
|
SimpleCommand::Focus(dir) => B::new(move || s.focus(dir)),
|
||||||
SimpleCommand::Move(dir) => Box::new(move || s.move_(dir)),
|
SimpleCommand::Move(dir) => B::new(move || s.move_(dir)),
|
||||||
SimpleCommand::Split(axis) => Box::new(move || s.create_split(axis)),
|
SimpleCommand::Split(axis) => B::new(move || s.create_split(axis)),
|
||||||
SimpleCommand::ToggleSplit => Box::new(move || s.toggle_split()),
|
SimpleCommand::ToggleSplit => B::new(move || s.toggle_split()),
|
||||||
SimpleCommand::ToggleMono => Box::new(move || s.toggle_mono()),
|
SimpleCommand::ToggleMono => B::new(move || s.toggle_mono()),
|
||||||
SimpleCommand::ToggleFullscreen => Box::new(move || s.toggle_fullscreen()),
|
SimpleCommand::ToggleFullscreen => B::new(move || s.toggle_fullscreen()),
|
||||||
SimpleCommand::FocusParent => Box::new(move || s.focus_parent()),
|
SimpleCommand::FocusParent => B::new(move || s.focus_parent()),
|
||||||
SimpleCommand::Close => Box::new(move || s.close()),
|
SimpleCommand::Close => B::new(move || s.close()),
|
||||||
SimpleCommand::DisablePointerConstraint => {
|
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::ToggleFloating => B::new(move || s.toggle_floating()),
|
||||||
SimpleCommand::Quit => Box::new(quit),
|
SimpleCommand::Quit => B::new(quit),
|
||||||
SimpleCommand::ReloadConfigToml => {
|
SimpleCommand::ReloadConfigToml => {
|
||||||
let persistent = state.persistent.clone();
|
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::ReloadConfigSo => B::new(reload),
|
||||||
SimpleCommand::None => Box::new(|| ()),
|
SimpleCommand::None => B::new(|| ()),
|
||||||
SimpleCommand::Forward(bool) => Box::new(move || s.set_forward(bool)),
|
SimpleCommand::Forward(bool) => B::new(move || s.set_forward(bool)),
|
||||||
},
|
},
|
||||||
Action::Multi { actions } => {
|
Action::Multi { actions } => {
|
||||||
let mut actions: Vec<_> = actions.into_iter().map(|a| a.into_fn(state)).collect();
|
let actions: Vec<_> = actions.into_iter().map(|a| a.into_fn(state)).collect();
|
||||||
Box::new(move || {
|
B::new(move || {
|
||||||
for action in &mut actions {
|
for action in &actions {
|
||||||
action();
|
action();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Action::Exec { exec } => Box::new(move || create_command(&exec).spawn()),
|
Action::Exec { exec } => B::new(move || create_command(&exec).spawn()),
|
||||||
Action::SwitchToVt { num } => Box::new(move || switch_to_vt(num)),
|
Action::SwitchToVt { num } => B::new(move || switch_to_vt(num)),
|
||||||
Action::ShowWorkspace { name } => {
|
Action::ShowWorkspace { name } => {
|
||||||
let workspace = get_workspace(&name);
|
let workspace = get_workspace(&name);
|
||||||
Box::new(move || s.show_workspace(workspace))
|
B::new(move || s.show_workspace(workspace))
|
||||||
}
|
}
|
||||||
Action::MoveToWorkspace { name } => {
|
Action::MoveToWorkspace { name } => {
|
||||||
let workspace = get_workspace(&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() {
|
for c in connectors() {
|
||||||
if con.match_.matches(c) {
|
if con.match_.matches(c) {
|
||||||
con.apply(c);
|
con.apply(c);
|
||||||
|
|
@ -90,7 +114,7 @@ impl Action {
|
||||||
}),
|
}),
|
||||||
Action::ConfigureInput { input } => {
|
Action::ConfigureInput { input } => {
|
||||||
let state = state.clone();
|
let state = state.clone();
|
||||||
Box::new(move || {
|
B::new(move || {
|
||||||
for c in input_devices() {
|
for c in input_devices() {
|
||||||
if input.match_.matches(c, &state) {
|
if input.match_.matches(c, &state) {
|
||||||
input.apply(c, &state);
|
input.apply(c, &state);
|
||||||
|
|
@ -100,7 +124,7 @@ impl Action {
|
||||||
}
|
}
|
||||||
Action::ConfigureOutput { out } => {
|
Action::ConfigureOutput { out } => {
|
||||||
let state = state.clone();
|
let state = state.clone();
|
||||||
Box::new(move || {
|
B::new(move || {
|
||||||
for c in connectors() {
|
for c in connectors() {
|
||||||
if out.match_.matches(c, &state) {
|
if out.match_.matches(c, &state) {
|
||||||
out.apply(c);
|
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 {
|
for (k, v) in &env {
|
||||||
set_env(k, v);
|
set_env(k, v);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
Action::UnsetEnv { env } => Box::new(move || {
|
Action::UnsetEnv { env } => B::new(move || {
|
||||||
for k in &env {
|
for k in &env {
|
||||||
unset_env(k);
|
unset_env(k);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
Action::SetKeymap { map } => {
|
Action::SetKeymap { map } => {
|
||||||
let state = state.clone();
|
let state = state.clone();
|
||||||
Box::new(move || state.set_keymap(&map))
|
B::new(move || state.set_keymap(&map))
|
||||||
}
|
}
|
||||||
Action::SetStatus { status } => {
|
Action::SetStatus { status } => {
|
||||||
let state = state.clone();
|
let state = state.clone();
|
||||||
Box::new(move || state.set_status(&status))
|
B::new(move || state.set_status(&status))
|
||||||
}
|
}
|
||||||
Action::SetTheme { theme } => {
|
Action::SetTheme { theme } => {
|
||||||
let state = state.clone();
|
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::SetLogLevel { level } => B::new(move || set_log_level(level)),
|
||||||
Action::SetGfxApi { api } => Box::new(move || set_gfx_api(api)),
|
Action::SetGfxApi { api } => B::new(move || set_gfx_api(api)),
|
||||||
Action::ConfigureDirectScanout { enabled } => {
|
Action::ConfigureDirectScanout { enabled } => {
|
||||||
Box::new(move || set_direct_scanout_enabled(enabled))
|
B::new(move || set_direct_scanout_enabled(enabled))
|
||||||
}
|
}
|
||||||
Action::ConfigureDrmDevice { dev } => {
|
Action::ConfigureDrmDevice { dev } => {
|
||||||
let state = state.clone();
|
let state = state.clone();
|
||||||
Box::new(move || {
|
B::new(move || {
|
||||||
for d in drm_devices() {
|
for d in drm_devices() {
|
||||||
if dev.match_.matches(d, &state) {
|
if dev.match_.matches(d, &state) {
|
||||||
dev.apply(d);
|
dev.apply(d);
|
||||||
|
|
@ -147,7 +171,7 @@ impl Action {
|
||||||
}
|
}
|
||||||
Action::SetRenderDevice { dev } => {
|
Action::SetRenderDevice { dev } => {
|
||||||
let state = state.clone();
|
let state = state.clone();
|
||||||
Box::new(move || {
|
B::new(move || {
|
||||||
for d in drm_devices() {
|
for d in drm_devices() {
|
||||||
if dev.matches(d, &state) {
|
if dev.matches(d, &state) {
|
||||||
d.make_render_device();
|
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 } => {
|
Action::MoveToOutput { output, workspace } => {
|
||||||
let state = state.clone();
|
let state = state.clone();
|
||||||
Box::new(move || {
|
B::new(move || {
|
||||||
let output = 'get_output: {
|
let output = 'get_output: {
|
||||||
for connector in connectors() {
|
for connector in connectors() {
|
||||||
if connector.connected() && output.matches(connector, &state) {
|
if connector.connected() && output.matches(connector, &state) {
|
||||||
|
|
@ -174,7 +198,7 @@ impl Action {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Action::SetRepeatRate { rate } => {
|
Action::SetRepeatRate { rate } => {
|
||||||
Box::new(move || s.set_repeat_rate(rate.rate, rate.delay))
|
B::new(move || s.set_repeat_rate(rate.rate, rate.delay))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -548,16 +572,26 @@ impl State {
|
||||||
cmd: SimpleCommand::None,
|
cmd: SimpleCommand::None,
|
||||||
} = shortcut.action
|
} = shortcut.action
|
||||||
{
|
{
|
||||||
self.persistent.seat.unbind(shortcut.keysym);
|
if shortcut.latch.is_none() {
|
||||||
binds.remove(&shortcut.keysym);
|
self.persistent.seat.unbind(shortcut.keysym);
|
||||||
} else {
|
binds.remove(&shortcut.keysym);
|
||||||
self.persistent.seat.bind_masked(
|
continue;
|
||||||
shortcut.mask,
|
}
|
||||||
shortcut.keysym,
|
|
||||||
shortcut.action.into_fn(self),
|
|
||||||
);
|
|
||||||
binds.insert(shortcut.keysym);
|
|
||||||
}
|
}
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -438,6 +438,10 @@
|
||||||
"action": {
|
"action": {
|
||||||
"description": "The action to execute.\n\nOmitting this is the same as setting it to `\"none\"`.\n",
|
"description": "The action to execute.\n\nOmitting this is the same as setting it to `\"none\"`.\n",
|
||||||
"$ref": "#/$defs/Action"
|
"$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": []
|
"required": []
|
||||||
|
|
|
||||||
|
|
@ -639,6 +639,28 @@ The table has the following fields:
|
||||||
|
|
||||||
The value of this field should be a [Action](#types-Action).
|
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>
|
<a name="types-Config"></a>
|
||||||
### `Config`
|
### `Config`
|
||||||
|
|
|
||||||
|
|
@ -2131,3 +2131,24 @@ ComplexShortcut:
|
||||||
The action to execute.
|
The action to execute.
|
||||||
|
|
||||||
Omitting this is the same as setting it to `"none"`.
|
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.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue