Skip to content

Commit

Permalink
Merge pull request #166 from mahkoh/jorth/latch
Browse files Browse the repository at this point in the history
Implement push-to-talk via shortcuts
  • Loading branch information
mahkoh authored Apr 17, 2024
2 parents c235f02 + 6f55675 commit a3a7874
Show file tree
Hide file tree
Showing 20 changed files with 866 additions and 159 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
209 changes: 178 additions & 31 deletions jay-config/src/_private/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,18 @@ use {
crate::{
_private::{
bincode_ops,
ipc::{ClientMessage, InitMessage, Response, ServerMessage, WorkspaceSource},
ipc::{
ClientMessage, InitMessage, Response, ServerFeature, ServerMessage, WorkspaceSource,
},
logging, Config, ConfigEntry, ConfigEntryGen, PollableId, WireMode, VERSION,
},
exec::Command,
input::{acceleration::AccelProfile, capability::Capability, InputDevice, Seat},
keyboard::Keymap,
keyboard::{
mods::{Modifiers, RELEASE},
syms::KeySym,
Keymap,
},
logging::LogLevel,
tasks::{JoinHandle, JoinSlot},
theme::{colors::Colorable, sized::Resizable, Color},
Expand Down Expand Up @@ -62,12 +68,19 @@ fn ignore_panic(name: &str, f: impl FnOnce()) {
}
}

struct KeyHandler {
registered_mask: Modifiers,
cb_mask: Modifiers,
cb: Option<Callback>,
latched: Vec<Box<dyn FnOnce()>>,
}

pub(crate) struct Client {
configure: extern "C" fn(),
srv_data: *const u8,
srv_unref: unsafe extern "C" fn(data: *const u8),
srv_handler: unsafe extern "C" fn(data: *const u8, msg: *const u8, size: usize),
key_handlers: RefCell<HashMap<(Seat, ModifiedKeySym), Callback>>,
key_handlers: RefCell<HashMap<(Seat, ModifiedKeySym), KeyHandler>>,
timer_handlers: RefCell<HashMap<Timer, Callback>>,
response: RefCell<Vec<Response>>,
on_new_seat: RefCell<Option<Callback<Seat>>>,
Expand All @@ -86,6 +99,9 @@ pub(crate) struct Client {
tasks: Tasks,
status_task: Cell<Vec<JoinHandle<()>>>,
i3bar_separator: RefCell<Option<Rc<String>>>,
pressed_keysym: Cell<Option<KeySym>>,

feat_mod_mask: Cell<bool>,
}

struct Interest {
Expand Down Expand Up @@ -210,6 +226,8 @@ pub unsafe extern "C" fn init(
tasks: Default::default(),
status_task: Default::default(),
i3bar_separator: Default::default(),
pressed_keysym: Cell::new(None),
feat_mod_mask: Cell::new(false),
});
let init = slice::from_raw_parts(init, size);
client.handle_init_msg(init);
Expand Down Expand Up @@ -305,17 +323,16 @@ impl Client {

pub fn unbind<T: Into<ModifiedKeySym>>(&self, seat: Seat, mod_sym: T) {
let mod_sym = mod_sym.into();
let deregister = self
.key_handlers
.borrow_mut()
.remove(&(seat, mod_sym))
.is_some();
if deregister {
self.send(&ClientMessage::RemoveShortcut {
seat,
mods: mod_sym.mods,
sym: mod_sym.sym,
})
if let Entry::Occupied(mut oe) = self.key_handlers.borrow_mut().entry((seat, mod_sym)) {
oe.get_mut().cb = None;
if oe.get().latched.is_empty() {
oe.remove();
self.send(&ClientMessage::RemoveShortcut {
seat,
mods: mod_sym.mods,
sym: mod_sym.sym,
})
}
}
}

Expand Down Expand Up @@ -913,33 +930,95 @@ impl Client {
keymap
}

pub fn bind<T: Into<ModifiedKeySym>, F: FnMut() + 'static>(
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,
mod_sym: T,
mut mod_mask: Modifiers,
mod_sym: ModifiedKeySym,
mut f: F,
) {
let mod_sym = mod_sym.into();
mod_mask |= mod_sym.mods | RELEASE;
let register = {
let mut kh = self.key_handlers.borrow_mut();
let f = cb(move |_| f());
let cb = cb(move |_| f());
match kh.entry((seat, mod_sym)) {
Entry::Occupied(mut o) => {
*o.get_mut() = f;
false
let o = o.get_mut();
o.cb = Some(cb);
o.cb_mask = mod_mask;
let register = o.latched.is_empty() && o.registered_mask != o.cb_mask;
if register {
o.registered_mask = o.cb_mask;
}
register
}
Entry::Vacant(v) => {
v.insert(f);
v.insert(KeyHandler {
cb_mask: mod_mask,
registered_mask: mod_mask,
cb: Some(cb),
latched: vec![],
});
true
}
}
};
if register {
self.send(&ClientMessage::AddShortcut {
seat,
mods: mod_sym.mods,
sym: mod_sym.sym,
});
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);
}
}

Expand Down Expand Up @@ -1081,6 +1160,61 @@ impl Client {
self.tasks.tasks.borrow_mut().remove(&id);
}

fn handle_invoke_shortcut(
&self,
seat: Seat,
unmasked_mods: Modifiers,
mods: Modifiers,
sym: KeySym,
) {
let ms = ModifiedKeySym { mods, sym };
let handler = self
.key_handlers
.borrow_mut()
.get_mut(&(seat, ms))
.map(|kh| {
let cb = if kh.cb_mask & unmasked_mods == mods {
kh.cb.clone()
} else {
None
};
(mem::take(&mut kh.latched), cb)
});
let Some((latched, handler)) = handler else {
return;
};
let was_latched = !latched.is_empty();
if (mods & RELEASE).0 == 0 {
self.pressed_keysym.set(Some(sym));
}
for latched in latched {
ignore_panic("latch", latched);
}
if let Some(handler) = handler {
run_cb("shortcut", &handler, ());
}
self.pressed_keysym.set(None);
if was_latched {
if let Entry::Occupied(mut oe) = self.key_handlers.borrow_mut().entry((seat, ms)) {
let o = oe.get_mut();
if o.latched.is_empty() {
if o.cb.is_none() {
self.send(&ClientMessage::RemoveShortcut { seat, mods, sym });
oe.remove();
} else if o.cb_mask != o.registered_mask {
o.registered_mask = o.cb_mask;
self.send(&ClientMessage::AddShortcut2 {
seat,
mods: ms.mods,
mod_mask: o.cb_mask,
sym: ms.sym,
});
}
}
}
}
}

fn handle_msg2(&self, msg: &[u8]) {
let res = bincode_ops().deserialize::<ServerMessage>(msg);
let msg = match res {
Expand All @@ -1101,11 +1235,15 @@ impl Client {
self.response.borrow_mut().push(response);
}
ServerMessage::InvokeShortcut { seat, mods, sym } => {
let ms = ModifiedKeySym { mods, sym };
let handler = self.key_handlers.borrow_mut().get(&(seat, ms)).cloned();
if let Some(handler) = handler {
run_cb("shortcut", &handler, ());
}
self.handle_invoke_shortcut(seat, mods, mods, sym);
}
ServerMessage::InvokeShortcut2 {
seat,
unmasked_mods,
effective_mods,
sym,
} => {
self.handle_invoke_shortcut(seat, unmasked_mods, effective_mods, sym);
}
ServerMessage::NewInputDevice { device } => {
let handler = self.on_new_input_device.borrow_mut().clone();
Expand Down Expand Up @@ -1178,6 +1316,15 @@ impl Client {
}
}
}
ServerMessage::Features { features } => {
for feat in features {
match feat {
ServerFeature::NONE => {}
ServerFeature::MOD_MASK => self.feat_mod_mask.set(true),
_ => {}
}
}
}
}
}

Expand Down
24 changes: 24 additions & 0 deletions jay-config/src/_private/ipc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ use {
std::time::Duration,
};

#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
#[serde(transparent)]
pub struct ServerFeature(u16);

impl ServerFeature {
pub const NONE: Self = Self(0);
pub const MOD_MASK: Self = Self(1);
}

#[derive(Serialize, Deserialize, Debug)]
pub enum ServerMessage {
Configure {
Expand Down Expand Up @@ -62,6 +71,15 @@ pub enum ServerMessage {
writable: bool,
res: Result<(), String>,
},
Features {
features: Vec<ServerFeature>,
},
InvokeShortcut2 {
seat: Seat,
unmasked_mods: Modifiers,
effective_mods: Modifiers,
sym: KeySym,
},
}

#[derive(Serialize, Deserialize, Debug)]
Expand Down Expand Up @@ -440,6 +458,12 @@ pub enum ClientMessage<'a> {
seat: Seat,
forward: bool,
},
AddShortcut2 {
seat: Seat,
mods: Modifiers,
mod_mask: Modifiers,
sym: KeySym,
},
}

#[derive(Serialize, Deserialize, Debug)]
Expand Down
Loading

0 comments on commit a3a7874

Please sign in to comment.