1
0
Fork 0
forked from wry/wry

config: implement shortcut latching

This commit is contained in:
Julian Orth 2024-04-16 18:47:40 +02:00
parent 90dbde99ab
commit 6f55675bdb
14 changed files with 367 additions and 93 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. 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

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

View file

@ -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),
_ => {} _ => {}
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -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 { .. } => {}

View file

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

View file

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

View file

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

View file

@ -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": []

View file

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

View file

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