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.
|
||||
|
||||
### 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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ use {
|
|||
input::{acceleration::AccelProfile, capability::Capability, InputDevice, Seat},
|
||||
keyboard::{
|
||||
mods::{Modifiers, RELEASE},
|
||||
syms::KeySym,
|
||||
Keymap,
|
||||
},
|
||||
logging::LogLevel,
|
||||
|
|
@ -68,8 +69,10 @@ fn ignore_panic(name: &str, f: impl FnOnce()) {
|
|||
}
|
||||
|
||||
struct KeyHandler {
|
||||
mask: Modifiers,
|
||||
cb: Callback,
|
||||
registered_mask: Modifiers,
|
||||
cb_mask: Modifiers,
|
||||
cb: Option<Callback>,
|
||||
latched: Vec<Box<dyn FnOnce()>>,
|
||||
}
|
||||
|
||||
pub(crate) struct Client {
|
||||
|
|
@ -96,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 {
|
||||
|
|
@ -220,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);
|
||||
|
|
@ -315,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,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -923,6 +930,46 @@ impl Client {
|
|||
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>(
|
||||
&self,
|
||||
seat: Seat,
|
||||
|
|
@ -937,29 +984,39 @@ impl Client {
|
|||
match kh.entry((seat, mod_sym)) {
|
||||
Entry::Occupied(mut o) => {
|
||||
let o = o.get_mut();
|
||||
o.cb = cb;
|
||||
mem::replace(&mut o.mask, mod_mask) != mod_mask
|
||||
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 { mask: mod_mask, cb });
|
||||
v.insert(KeyHandler {
|
||||
cb_mask: mod_mask,
|
||||
registered_mask: mod_mask,
|
||||
cb: Some(cb),
|
||||
latched: vec![],
|
||||
});
|
||||
true
|
||||
}
|
||||
}
|
||||
};
|
||||
if register {
|
||||
let msg = if !mod_mask.0 == 0 {
|
||||
ClientMessage::AddShortcut {
|
||||
seat,
|
||||
mods: mod_sym.mods,
|
||||
sym: mod_sym.sym,
|
||||
}
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
|
|
@ -1103,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 {
|
||||
|
|
@ -1123,15 +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))
|
||||
.map(|k| k.cb.clone());
|
||||
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();
|
||||
|
|
@ -1208,6 +1320,7 @@ impl Client {
|
|||
for feat in features {
|
||||
match feat {
|
||||
ServerFeature::NONE => {}
|
||||
ServerFeature::MOD_MASK => self.feat_mod_mask.set(true),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ pub struct ServerFeature(u16);
|
|||
|
||||
impl ServerFeature {
|
||||
pub const NONE: Self = Self(0);
|
||||
pub const MOD_MASK: Self = Self(1);
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
|
|
@ -73,6 +74,12 @@ pub enum ServerMessage {
|
|||
Features {
|
||||
features: Vec<ServerFeature>,
|
||||
},
|
||||
InvokeShortcut2 {
|
||||
seat: Seat,
|
||||
unmasked_mods: Modifiers,
|
||||
effective_mods: Modifiers,
|
||||
sym: KeySym,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
|
|
|
|||
|
|
@ -216,6 +216,16 @@ impl Seat {
|
|||
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.into())
|
||||
|
|
|
|||
|
|
@ -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,7 +213,9 @@ impl ConfigProxy {
|
|||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
|
|
@ -288,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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ use {
|
|||
crate::{
|
||||
backend::{ConnectorId, InputEvent, KeyState, AXIS_120},
|
||||
client::ClientId,
|
||||
config::InvokedShortcut,
|
||||
fixed::Fixed,
|
||||
ifs::{
|
||||
ipc::{
|
||||
|
|
@ -39,7 +40,6 @@ use {
|
|||
jay_config::keyboard::{
|
||||
mods::{Modifiers, CAPS, NUM, RELEASE},
|
||||
syms::KeySym,
|
||||
ModifiedKeySym,
|
||||
},
|
||||
smallvec::SmallVec,
|
||||
std::{cell::RefCell, collections::hash_map::Entry, rc::Rc},
|
||||
|
|
@ -386,8 +386,9 @@ impl WlSeatGlobal {
|
|||
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),
|
||||
shortcuts.push(InvokedShortcut {
|
||||
unmasked_mods: Modifiers(mods),
|
||||
effective_mods: Modifiers(key_mods),
|
||||
sym: KeySym(sym),
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 { .. } => {}
|
||||
|
|
|
|||
|
|
@ -285,6 +285,7 @@ pub struct Shortcut {
|
|||
pub mask: Modifiers,
|
||||
pub keysym: ModifiedKeySym,
|
||||
pub action: Action,
|
||||
pub latch: Option<Action>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ pub enum ShortcutsParserError {
|
|||
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, 'b> {
|
||||
|
|
@ -65,6 +67,7 @@ impl Parser for ShortcutsParser<'_, '_> {
|
|||
mask: Modifiers(!0),
|
||||
keysym,
|
||||
action,
|
||||
latch: None,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
|
|
@ -129,7 +132,8 @@ impl Parser for ComplexShortcutParser<'_> {
|
|||
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_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
|
||||
|
|
@ -144,10 +148,18 @@ impl Parser for ComplexShortcutParser<'_> {
|
|||
.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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -548,16 +572,26 @@ impl State {
|
|||
cmd: SimpleCommand::None,
|
||||
} = shortcut.action
|
||||
{
|
||||
self.persistent.seat.unbind(shortcut.keysym);
|
||||
binds.remove(&shortcut.keysym);
|
||||
} else {
|
||||
self.persistent.seat.bind_masked(
|
||||
shortcut.mask,
|
||||
shortcut.keysym,
|
||||
shortcut.action.into_fn(self),
|
||||
);
|
||||
binds.insert(shortcut.keysym);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -438,6 +438,10 @@
|
|||
"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": []
|
||||
|
|
|
|||
|
|
@ -639,6 +639,28 @@ The table has the following fields:
|
|||
|
||||
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`
|
||||
|
|
|
|||
|
|
@ -2131,3 +2131,24 @@ ComplexShortcut:
|
|||
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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue