Skip to content

Commit

Permalink
config: implement shortcut latching
Browse files Browse the repository at this point in the history
  • Loading branch information
mahkoh committed Apr 16, 2024
1 parent 6ca044a commit 254fbca
Show file tree
Hide file tree
Showing 13 changed files with 355 additions and 91 deletions.
17 changes: 17 additions & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
171 changes: 142 additions & 29 deletions jay-config/src/_private/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use {
input::{acceleration::AccelProfile, capability::Capability, InputDevice, Seat},
keyboard::{
mods::{Modifiers, RELEASE},
syms::KeySym,
Keymap,
},
logging::LogLevel,
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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,
})
}
}
}

Expand Down Expand Up @@ -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,
Expand All @@ -937,27 +984,37 @@ 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 {
let msg = if self.feat_mod_mask.get() {
ClientMessage::AddShortcut2 {
seat,
mods: mod_sym.mods,
mod_mask,
sym: mod_sym.sym,
}
} else {
ClientMessage::AddShortcut2 {
ClientMessage::AddShortcut {
seat,
mods: mod_sym.mods,
mod_mask,
sym: mod_sym.sym,
}
};
Expand Down Expand Up @@ -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 {
Expand All @@ -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();
Expand Down Expand Up @@ -1208,6 +1320,7 @@ impl Client {
for feat in features {
match feat {
ServerFeature::NONE => {}
ServerFeature::MOD_MASK => self.feat_mod_mask.set(true),
_ => {}
}
}
Expand Down
7 changes: 7 additions & 0 deletions jay-config/src/_private/ipc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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)]
Expand Down
10 changes: 10 additions & 0 deletions jay-config/src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
36 changes: 27 additions & 9 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 });
}

Expand Down Expand Up @@ -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,
}
Loading

0 comments on commit 254fbca

Please sign in to comment.