Merge pull request #166 from mahkoh/jorth/latch
Implement push-to-talk via shortcuts
This commit is contained in:
commit
a3a7874506
20 changed files with 868 additions and 161 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:
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
@ -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>>>,
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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);
|
||||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -913,36 +930,98 @@ impl Client {
|
|||
keymap
|
||||
}
|
||||
|
||||
pub fn bind<T: Into<ModifiedKeySym>, F: FnMut() + 'static>(
|
||||
&self,
|
||||
seat: Seat,
|
||||
mod_sym: T,
|
||||
mut f: F,
|
||||
) {
|
||||
let mod_sym = mod_sym.into();
|
||||
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();
|
||||
let f = cb(move |_| f());
|
||||
match kh.entry((seat, mod_sym)) {
|
||||
match kh.entry((seat, mods | keysym)) {
|
||||
Entry::Occupied(mut o) => {
|
||||
*o.get_mut() = f;
|
||||
false
|
||||
let o = o.get_mut();
|
||||
o.latched.push(f);
|
||||
mem::replace(&mut o.registered_mask, mods) != mods
|
||||
}
|
||||
Entry::Vacant(v) => {
|
||||
v.insert(f);
|
||||
v.insert(KeyHandler {
|
||||
cb_mask: mods,
|
||||
registered_mask: mods,
|
||||
cb: None,
|
||||
latched: vec![f],
|
||||
});
|
||||
true
|
||||
}
|
||||
}
|
||||
};
|
||||
if register {
|
||||
self.send(&ClientMessage::AddShortcut {
|
||||
self.send(&ClientMessage::AddShortcut2 {
|
||||
seat,
|
||||
mods: mod_sym.mods,
|
||||
sym: mod_sym.sym,
|
||||
mods,
|
||||
mod_mask: mods,
|
||||
sym: keysym,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bind_masked<F: FnMut() + 'static>(
|
||||
&self,
|
||||
seat: Seat,
|
||||
mut mod_mask: Modifiers,
|
||||
mod_sym: ModifiedKeySym,
|
||||
mut f: F,
|
||||
) {
|
||||
mod_mask |= mod_sym.mods | RELEASE;
|
||||
let register = {
|
||||
let mut kh = self.key_handlers.borrow_mut();
|
||||
let cb = cb(move |_| f());
|
||||
match kh.entry((seat, mod_sym)) {
|
||||
Entry::Occupied(mut o) => {
|
||||
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(KeyHandler {
|
||||
cb_mask: mod_mask,
|
||||
registered_mask: mod_mask,
|
||||
cb: Some(cb),
|
||||
latched: vec![],
|
||||
});
|
||||
true
|
||||
}
|
||||
}
|
||||
};
|
||||
if register {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn log(&self, level: LogLevel, msg: &str, file: Option<&str>, line: Option<u32>) {
|
||||
self.send(&ClientMessage::Log {
|
||||
level,
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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();
|
||||
|
|
@ -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),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
@ -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)]
|
||||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ pub mod capability;
|
|||
use {
|
||||
crate::{
|
||||
input::{acceleration::AccelProfile, capability::Capability},
|
||||
keyboard::Keymap,
|
||||
keyboard::{mods::Modifiers, Keymap},
|
||||
Axis, Direction, ModifiedKeySym, Workspace,
|
||||
_private::{ipc::WorkspaceSource, DEFAULT_SEAT_NAME},
|
||||
video::Connector,
|
||||
|
|
@ -188,12 +188,47 @@ impl Seat {
|
|||
/// CapsLock and NumLock are ignored during modifier evaluation. Therefore, bindings
|
||||
/// containing these modifiers will never be invoked.
|
||||
pub fn bind<T: Into<ModifiedKeySym>, F: FnMut() + 'static>(self, mod_sym: T, f: F) {
|
||||
get!().bind(self, mod_sym, f)
|
||||
self.bind_masked(Modifiers(!0), mod_sym, f)
|
||||
}
|
||||
|
||||
/// Creates a compositor-wide hotkey while ignoring some modifiers.
|
||||
///
|
||||
/// This is similar to `bind` except that only the masked modifiers are considered.
|
||||
///
|
||||
/// For example, if this function is invoked with `mod_mask = Modifiers::NONE` and
|
||||
/// `mod_sym = SYM_XF86AudioRaiseVolume`, then the callback will be invoked whenever
|
||||
/// `SYM_XF86AudioRaiseVolume` is pressed. Even if the user is simultaneously holding
|
||||
/// the shift key which would otherwise prevent the callback from taking effect.
|
||||
///
|
||||
/// For example, if this function is invoked with `mod_mask = CTRL | SHIFT` and
|
||||
/// `mod_sym = CTRL | SYM_x`, then the callback will be invoked whenever the user
|
||||
/// presses `ctrl+x` without pressing the shift key. Even if the user is
|
||||
/// simultaneously holding the alt key.
|
||||
///
|
||||
/// If `mod_sym` contains any modifiers, then these modifiers are automatically added
|
||||
/// to the mask. The synthetic `RELEASE` modifier is always added to the mask.
|
||||
pub fn bind_masked<T: Into<ModifiedKeySym>, F: FnMut() + 'static>(
|
||||
self,
|
||||
mod_mask: Modifiers,
|
||||
mod_sym: T,
|
||||
f: 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.
|
||||
pub fn unbind<T: Into<ModifiedKeySym>>(self, mod_sym: T) {
|
||||
get!().unbind(self, mod_sym)
|
||||
get!().unbind(self, mod_sym.into())
|
||||
}
|
||||
|
||||
/// Moves the keyboard focus of the seat in the specified direction.
|
||||
|
|
|
|||
|
|
@ -10,6 +10,11 @@ use {
|
|||
#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Default, Hash, Debug)]
|
||||
pub struct Modifiers(pub u32);
|
||||
|
||||
impl Modifiers {
|
||||
/// No modifiers.
|
||||
pub const NONE: Self = Modifiers(0);
|
||||
}
|
||||
|
||||
/// The Shift modifier
|
||||
pub const SHIFT: Modifiers = Modifiers(1 << 0);
|
||||
/// The CapsLock modifier.
|
||||
|
|
|
|||
|
|
@ -38,7 +38,8 @@
|
|||
clippy::uninlined_format_args,
|
||||
clippy::len_zero,
|
||||
clippy::single_char_pattern,
|
||||
clippy::single_char_add_str
|
||||
clippy::single_char_add_str,
|
||||
clippy::single_match
|
||||
)]
|
||||
|
||||
use {
|
||||
|
|
|
|||
|
|
@ -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,6 +213,9 @@ impl ConfigProxy {
|
|||
}
|
||||
|
||||
pub fn configure(&self, reload: bool) {
|
||||
self.send(&ServerMessage::Features {
|
||||
features: vec![ServerFeature::MOD_MASK],
|
||||
});
|
||||
self.send(&ServerMessage::Configure { reload });
|
||||
}
|
||||
|
||||
|
|
@ -287,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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1127,11 +1127,12 @@ impl ConfigProxyHandler {
|
|||
fn handle_add_shortcut(
|
||||
&self,
|
||||
seat: Seat,
|
||||
mod_mask: Modifiers,
|
||||
mods: Modifiers,
|
||||
sym: KeySym,
|
||||
) -> Result<(), CphError> {
|
||||
let seat = self.get_seat(seat)?;
|
||||
seat.add_shortcut(mods, sym);
|
||||
seat.add_shortcut(mod_mask, mods, sym);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -1499,7 +1500,7 @@ impl ConfigProxyHandler {
|
|||
self.handle_set_split(seat, axis).wrn("set_split")?
|
||||
}
|
||||
ClientMessage::AddShortcut { seat, mods, sym } => self
|
||||
.handle_add_shortcut(seat, mods, sym)
|
||||
.handle_add_shortcut(seat, Modifiers(!0), mods, sym)
|
||||
.wrn("add_shortcut")?,
|
||||
ClientMessage::RemoveShortcut { seat, mods, sym } => self
|
||||
.handle_remove_shortcut(seat, mods, sym)
|
||||
|
|
@ -1773,6 +1774,14 @@ impl ConfigProxyHandler {
|
|||
ClientMessage::SetForward { seat, forward } => {
|
||||
self.handle_set_forward(seat, forward).wrn("set_forward")?
|
||||
}
|
||||
ClientMessage::AddShortcut2 {
|
||||
seat,
|
||||
mod_mask,
|
||||
mods,
|
||||
sym,
|
||||
} => self
|
||||
.handle_add_shortcut(seat, mod_mask, mods, sym)
|
||||
.wrn("add_shortcut")?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,7 +73,6 @@ use {
|
|||
xkbcommon::{DynKeyboardState, KeyboardState, KeymapId, XkbKeymap, XkbState},
|
||||
},
|
||||
ahash::AHashMap,
|
||||
jay_config::keyboard::mods::Modifiers,
|
||||
smallvec::SmallVec,
|
||||
std::{
|
||||
cell::{Cell, RefCell},
|
||||
|
|
@ -160,7 +159,7 @@ pub struct WlSeatGlobal {
|
|||
pointer_owner: PointerOwnerHolder,
|
||||
kb_owner: KbOwnerHolder,
|
||||
dropped_dnd: RefCell<Option<DroppedDnd>>,
|
||||
shortcuts: CopyHashMap<(u32, u32), Modifiers>,
|
||||
shortcuts: RefCell<AHashMap<u32, SmallMap<u32, u32, 2>>>,
|
||||
queue_link: Cell<Option<LinkedNode<Rc<Self>>>>,
|
||||
tree_changed_handler: Cell<Option<SpawnedFuture<()>>>,
|
||||
output: CloneCell<Rc<OutputNode>>,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ use {
|
|||
crate::{
|
||||
backend::{ConnectorId, InputEvent, KeyState, AXIS_120},
|
||||
client::ClientId,
|
||||
config::InvokedShortcut,
|
||||
fixed::Fixed,
|
||||
ifs::{
|
||||
ipc::{
|
||||
|
|
@ -39,10 +40,9 @@ use {
|
|||
jay_config::keyboard::{
|
||||
mods::{Modifiers, CAPS, NUM, RELEASE},
|
||||
syms::KeySym,
|
||||
ModifiedKeySym,
|
||||
},
|
||||
smallvec::SmallVec,
|
||||
std::{cell::RefCell, rc::Rc},
|
||||
std::{cell::RefCell, collections::hash_map::Entry, rc::Rc},
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
|
|
@ -380,13 +380,19 @@ impl WlSeatGlobal {
|
|||
if state == wl_keyboard::RELEASED {
|
||||
mods |= RELEASE.0;
|
||||
}
|
||||
let scs = &*self.shortcuts.borrow();
|
||||
let keysyms = xkb_state.unmodified_keysyms(key);
|
||||
for &sym in keysyms {
|
||||
if let Some(mods) = self.shortcuts.get(&(mods, sym)) {
|
||||
shortcuts.push(ModifiedKeySym {
|
||||
mods,
|
||||
sym: KeySym(sym),
|
||||
});
|
||||
if let Some(key_mods) = scs.get(&sym) {
|
||||
for (key_mods, mask) in key_mods {
|
||||
if mods & mask == key_mods {
|
||||
shortcuts.push(InvokedShortcut {
|
||||
unmasked_mods: Modifiers(mods),
|
||||
effective_mods: Modifiers(key_mods),
|
||||
sym: KeySym(sym),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -608,15 +614,24 @@ impl WlSeatGlobal {
|
|||
}
|
||||
|
||||
pub fn clear_shortcuts(&self) {
|
||||
self.shortcuts.clear();
|
||||
self.shortcuts.borrow_mut().clear();
|
||||
}
|
||||
|
||||
pub fn add_shortcut(&self, mods: Modifiers, keysym: KeySym) {
|
||||
self.shortcuts.set((mods.0, keysym.0), mods);
|
||||
pub fn add_shortcut(&self, mod_mask: Modifiers, mods: Modifiers, keysym: KeySym) {
|
||||
self.shortcuts
|
||||
.borrow_mut()
|
||||
.entry(keysym.0)
|
||||
.or_default()
|
||||
.insert(mods.0, mod_mask.0);
|
||||
}
|
||||
|
||||
pub fn remove_shortcut(&self, mods: Modifiers, keysym: KeySym) {
|
||||
self.shortcuts.remove(&(mods.0, keysym.0));
|
||||
if let Entry::Occupied(mut oe) = self.shortcuts.borrow_mut().entry(keysym.0) {
|
||||
oe.get_mut().remove(&mods.0);
|
||||
if oe.get().is_empty() {
|
||||
oe.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn trigger_tree_changed(&self) {
|
||||
|
|
|
|||
|
|
@ -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 { .. } => {}
|
||||
|
|
@ -109,6 +119,7 @@ unsafe extern "C" fn handle_msg(data: *const u8, msg: *const u8, size: usize) {
|
|||
ServerMessage::Idle => {}
|
||||
ServerMessage::DevicesEnumerated => {}
|
||||
ServerMessage::InterestReady { .. } => {}
|
||||
ServerMessage::Features { .. } => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ use {
|
|||
},
|
||||
jay_config::{
|
||||
input::acceleration::AccelProfile,
|
||||
keyboard::{Keymap, ModifiedKeySym},
|
||||
keyboard::{mods::Modifiers, Keymap, ModifiedKeySym},
|
||||
logging::LogLevel,
|
||||
status::MessageFormat,
|
||||
theme::Color,
|
||||
|
|
@ -280,11 +280,19 @@ pub struct RepeatRate {
|
|||
pub delay: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Shortcut {
|
||||
pub mask: Modifiers,
|
||||
pub keysym: ModifiedKeySym,
|
||||
pub action: Action,
|
||||
pub latch: Option<Action>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Config {
|
||||
pub keymap: Option<ConfigKeymap>,
|
||||
pub repeat_rate: Option<RepeatRate>,
|
||||
pub shortcuts: Vec<(ModifiedKeySym, Action)>,
|
||||
pub shortcuts: Vec<Shortcut>,
|
||||
pub on_graphics_initialized: Option<Action>,
|
||||
pub on_idle: Option<Action>,
|
||||
pub status: Option<Status>,
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ use {
|
|||
log_level::LogLevelParser,
|
||||
output::OutputsParser,
|
||||
repeat_rate::RepeatRateParser,
|
||||
shortcuts::{ShortcutsParser, ShortcutsParserError},
|
||||
shortcuts::{ComplexShortcutsParser, ShortcutsParser, ShortcutsParserError},
|
||||
status::StatusParser,
|
||||
theme::ThemeParser,
|
||||
},
|
||||
|
|
@ -30,6 +30,7 @@ use {
|
|||
},
|
||||
},
|
||||
indexmap::IndexMap,
|
||||
std::collections::HashSet,
|
||||
thiserror::Error,
|
||||
};
|
||||
|
||||
|
|
@ -96,7 +97,7 @@ impl Parser for ConfigParser<'_> {
|
|||
_,
|
||||
idle_val,
|
||||
),
|
||||
(explicit_sync, repeat_rate_val),
|
||||
(explicit_sync, repeat_rate_val, complex_shortcuts_val),
|
||||
) = ext.extract((
|
||||
(
|
||||
opt(val("keymap")),
|
||||
|
|
@ -122,7 +123,11 @@ impl Parser for ConfigParser<'_> {
|
|||
opt(val("$schema")),
|
||||
opt(val("idle")),
|
||||
),
|
||||
(recover(opt(bol("explicit-sync"))), opt(val("repeat-rate"))),
|
||||
(
|
||||
recover(opt(bol("explicit-sync"))),
|
||||
opt(val("repeat-rate")),
|
||||
opt(val("complex-shortcuts")),
|
||||
),
|
||||
))?;
|
||||
let mut keymap = None;
|
||||
if let Some(value) = keymap_val {
|
||||
|
|
@ -136,10 +141,24 @@ impl Parser for ConfigParser<'_> {
|
|||
}
|
||||
}
|
||||
}
|
||||
let mut used_keys = HashSet::new();
|
||||
let mut shortcuts = vec![];
|
||||
if let Some(value) = shortcuts_val {
|
||||
shortcuts = value
|
||||
.parse(&mut ShortcutsParser(self.0))
|
||||
value
|
||||
.parse(&mut ShortcutsParser {
|
||||
cx: self.0,
|
||||
used_keys: &mut used_keys,
|
||||
shortcuts: &mut shortcuts,
|
||||
})
|
||||
.map_spanned_err(ConfigParserError::ParseShortcuts)?;
|
||||
}
|
||||
if let Some(value) = complex_shortcuts_val {
|
||||
value
|
||||
.parse(&mut ComplexShortcutsParser {
|
||||
cx: self.0,
|
||||
used_keys: &mut used_keys,
|
||||
shortcuts: &mut shortcuts,
|
||||
})
|
||||
.map_spanned_err(ConfigParserError::ParseShortcuts)?;
|
||||
}
|
||||
if shortcuts.is_empty() {
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ pub enum ModifiedKeysymParserError {
|
|||
MissingSym,
|
||||
#[error("Unknown keysym {0}")]
|
||||
UnknownKeysym(String),
|
||||
#[error("Unknown modifier {0}")]
|
||||
UnknownModifier(String),
|
||||
}
|
||||
|
||||
pub struct ModifiedKeysymParser;
|
||||
|
|
@ -39,20 +41,8 @@ impl Parser for ModifiedKeysymParser {
|
|||
let mut modifiers = Modifiers(0);
|
||||
let mut sym = None;
|
||||
for part in string.split("-") {
|
||||
let modifier = match part {
|
||||
"shift" => SHIFT,
|
||||
"lock" => LOCK,
|
||||
"ctrl" => CTRL,
|
||||
"mod1" => MOD1,
|
||||
"mod2" => MOD2,
|
||||
"mod3" => MOD3,
|
||||
"mod4" => MOD4,
|
||||
"mod5" => MOD5,
|
||||
"caps" => CAPS,
|
||||
"alt" => ALT,
|
||||
"num" => NUM,
|
||||
"logo" => LOGO,
|
||||
"release" => RELEASE,
|
||||
let modifier = match parse_mod(part) {
|
||||
Some(m) => m,
|
||||
_ => match KEYSYMS.get(part) {
|
||||
Some(new) if sym.is_none() => {
|
||||
sym = Some(*new);
|
||||
|
|
@ -73,3 +63,46 @@ impl Parser for ModifiedKeysymParser {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ModifiersParser;
|
||||
|
||||
impl Parser for ModifiersParser {
|
||||
type Value = Modifiers;
|
||||
type Error = ModifiedKeysymParserError;
|
||||
const EXPECTED: &'static [DataType] = &[DataType::String];
|
||||
|
||||
fn parse_string(&mut self, span: Span, string: &str) -> ParseResult<Self> {
|
||||
let mut modifiers = Modifiers(0);
|
||||
if !string.is_empty() {
|
||||
for part in string.split("-") {
|
||||
let Some(modifier) = parse_mod(part) else {
|
||||
return Err(
|
||||
ModifiedKeysymParserError::UnknownModifier(part.to_string()).spanned(span)
|
||||
);
|
||||
};
|
||||
modifiers |= modifier;
|
||||
}
|
||||
}
|
||||
Ok(modifiers)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_mod(part: &str) -> Option<Modifiers> {
|
||||
let modifier = match part {
|
||||
"shift" => SHIFT,
|
||||
"lock" => LOCK,
|
||||
"ctrl" => CTRL,
|
||||
"mod1" => MOD1,
|
||||
"mod2" => MOD2,
|
||||
"mod3" => MOD3,
|
||||
"mod4" => MOD4,
|
||||
"mod5" => MOD5,
|
||||
"caps" => CAPS,
|
||||
"alt" => ALT,
|
||||
"num" => NUM,
|
||||
"logo" => LOGO,
|
||||
"release" => RELEASE,
|
||||
_ => return None,
|
||||
};
|
||||
Some(modifier)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,16 @@ use {
|
|||
crate::{
|
||||
config::{
|
||||
context::Context,
|
||||
extractor::{opt, str, val, Extractor, ExtractorError},
|
||||
parser::{DataType, ParseResult, Parser, UnexpectedDataType},
|
||||
parsers::{action::ActionParser, modified_keysym::ModifiedKeysymParser},
|
||||
Action,
|
||||
parsers::{
|
||||
action::{ActionParser, ActionParserError},
|
||||
modified_keysym::{
|
||||
ModifiedKeysymParser, ModifiedKeysymParserError, ModifiersParser,
|
||||
},
|
||||
},
|
||||
spanned::SpannedErrorExt,
|
||||
Action, Shortcut, SimpleCommand,
|
||||
},
|
||||
toml::{
|
||||
toml_span::{Span, Spanned, SpannedExt},
|
||||
|
|
@ -12,7 +19,7 @@ use {
|
|||
},
|
||||
},
|
||||
indexmap::IndexMap,
|
||||
jay_config::keyboard::ModifiedKeySym,
|
||||
jay_config::keyboard::{mods::Modifiers, ModifiedKeySym},
|
||||
std::collections::HashSet,
|
||||
thiserror::Error,
|
||||
};
|
||||
|
|
@ -21,12 +28,24 @@ use {
|
|||
pub enum ShortcutsParserError {
|
||||
#[error(transparent)]
|
||||
Expected(#[from] UnexpectedDataType),
|
||||
#[error(transparent)]
|
||||
ExtractorError(#[from] ExtractorError),
|
||||
#[error("Could not parse the mod mask")]
|
||||
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>(pub &'a Context<'a>);
|
||||
pub struct ShortcutsParser<'a, 'b> {
|
||||
pub cx: &'a Context<'a>,
|
||||
pub used_keys: &'b mut HashSet<Spanned<ModifiedKeySym>>,
|
||||
pub shortcuts: &'b mut Vec<Shortcut>,
|
||||
}
|
||||
|
||||
impl Parser for ShortcutsParser<'_> {
|
||||
type Value = Vec<(ModifiedKeySym, Action)>;
|
||||
impl Parser for ShortcutsParser<'_, '_> {
|
||||
type Value = ();
|
||||
type Error = ShortcutsParserError;
|
||||
const EXPECTED: &'static [DataType] = &[DataType::Table];
|
||||
|
||||
|
|
@ -35,38 +54,147 @@ impl Parser for ShortcutsParser<'_> {
|
|||
_span: Span,
|
||||
table: &IndexMap<Spanned<String>, Spanned<Value>>,
|
||||
) -> ParseResult<Self> {
|
||||
let mut used_keys = HashSet::<Spanned<ModifiedKeySym>>::new();
|
||||
let mut res = vec![];
|
||||
for (key, value) in table.iter() {
|
||||
let keysym = match ModifiedKeysymParser.parse_string(key.span, &key.value) {
|
||||
Ok(k) => k,
|
||||
Err(e) => {
|
||||
log::warn!("Could not parse keysym: {}", self.0.error(e));
|
||||
continue;
|
||||
}
|
||||
let Some(keysym) = parse_modified_keysym(self.cx, key) else {
|
||||
continue;
|
||||
};
|
||||
let action = match value.parse(&mut ActionParser(self.0)) {
|
||||
Ok(a) => a,
|
||||
let Some(action) = parse_action(self.cx, &key.value, value) else {
|
||||
continue;
|
||||
};
|
||||
let spanned = keysym.spanned(key.span);
|
||||
log_used(self.cx, self.used_keys, spanned);
|
||||
self.shortcuts.push(Shortcut {
|
||||
mask: Modifiers(!0),
|
||||
keysym,
|
||||
action,
|
||||
latch: None,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ComplexShortcutsParser<'a, 'b> {
|
||||
pub cx: &'a Context<'a>,
|
||||
pub used_keys: &'b mut HashSet<Spanned<ModifiedKeySym>>,
|
||||
pub shortcuts: &'b mut Vec<Shortcut>,
|
||||
}
|
||||
|
||||
impl Parser for ComplexShortcutsParser<'_, '_> {
|
||||
type Value = ();
|
||||
type Error = ShortcutsParserError;
|
||||
const EXPECTED: &'static [DataType] = &[DataType::Table];
|
||||
|
||||
fn parse_table(
|
||||
&mut self,
|
||||
_span: Span,
|
||||
table: &IndexMap<Spanned<String>, Spanned<Value>>,
|
||||
) -> ParseResult<Self> {
|
||||
for (key, value) in table.iter() {
|
||||
let Some(keysym) = parse_modified_keysym(self.cx, key) else {
|
||||
continue;
|
||||
};
|
||||
let shortcut = match value.parse(&mut ComplexShortcutParser {
|
||||
keysym,
|
||||
cx: self.cx,
|
||||
}) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
log::warn!(
|
||||
"Could not parse action for keysym {}: {}",
|
||||
"Could not parse shortcut for keysym {}: {}",
|
||||
key.value,
|
||||
self.0.error(e)
|
||||
self.cx.error(e)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let spanned = keysym.spanned(key.span);
|
||||
if let Some(prev) = used_keys.get(&spanned) {
|
||||
log::warn!(
|
||||
"Duplicate key overrides previous definition: {}",
|
||||
self.0.error3(spanned.span)
|
||||
);
|
||||
log::info!("Previous definition here: {}", self.0.error3(prev.span));
|
||||
}
|
||||
used_keys.insert(spanned);
|
||||
res.push((keysym, action));
|
||||
log_used(self.cx, self.used_keys, spanned);
|
||||
self.shortcuts.push(shortcut);
|
||||
}
|
||||
Ok(res)
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct ComplexShortcutParser<'a> {
|
||||
pub keysym: ModifiedKeySym,
|
||||
pub cx: &'a Context<'a>,
|
||||
}
|
||||
|
||||
impl Parser for ComplexShortcutParser<'_> {
|
||||
type Value = Shortcut;
|
||||
type Error = ShortcutsParserError;
|
||||
const EXPECTED: &'static [DataType] = &[DataType::Table];
|
||||
|
||||
fn parse_table(
|
||||
&mut self,
|
||||
span: Span,
|
||||
table: &IndexMap<Spanned<String>, Spanned<Value>>,
|
||||
) -> ParseResult<Self> {
|
||||
let mut ext = Extractor::new(self.cx, span, table);
|
||||
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
|
||||
.parse_string(v.span, v.value)
|
||||
.map_spanned_err(ShortcutsParserError::ModMask)?,
|
||||
};
|
||||
let action = match action_val {
|
||||
None => Action::SimpleCommand {
|
||||
cmd: SimpleCommand::None,
|
||||
},
|
||||
Some(v) => v
|
||||
.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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_action(cx: &Context<'_>, key: &str, value: &Spanned<Value>) -> Option<Action> {
|
||||
match value.parse(&mut ActionParser(cx)) {
|
||||
Ok(a) => Some(a),
|
||||
Err(e) => {
|
||||
log::warn!("Could not parse action for keysym {key}: {}", cx.error(e));
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_modified_keysym(cx: &Context<'_>, key: &Spanned<String>) -> Option<ModifiedKeySym> {
|
||||
match ModifiedKeysymParser.parse_string(key.span, &key.value) {
|
||||
Ok(k) => Some(k),
|
||||
Err(e) => {
|
||||
log::warn!("Could not parse keysym {}: {}", key.value, cx.error(e));
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn log_used(
|
||||
cx: &Context<'_>,
|
||||
used: &mut HashSet<Spanned<ModifiedKeySym>>,
|
||||
key: Spanned<ModifiedKeySym>,
|
||||
) {
|
||||
if let Some(prev) = used.get(&key) {
|
||||
log::warn!(
|
||||
"Duplicate key overrides previous definition: {}",
|
||||
cx.error3(key.span)
|
||||
);
|
||||
log::info!("Previous definition here: {}", cx.error3(prev.span));
|
||||
}
|
||||
used.insert(key);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ mod toml;
|
|||
use {
|
||||
crate::config::{
|
||||
parse_config, Action, Config, ConfigConnector, ConfigDrmDevice, ConfigKeymap,
|
||||
ConnectorMatch, DrmDeviceMatch, Exec, Input, InputMatch, Output, OutputMatch,
|
||||
ConnectorMatch, DrmDeviceMatch, Exec, Input, InputMatch, Output, OutputMatch, Shortcut,
|
||||
SimpleCommand, Status, Theme,
|
||||
},
|
||||
ahash::{AHashMap, AHashSet},
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -541,22 +565,33 @@ impl State {
|
|||
}
|
||||
}
|
||||
|
||||
fn apply_shortcuts(
|
||||
self: &Rc<Self>,
|
||||
shortcuts: impl IntoIterator<Item = (ModifiedKeySym, Action)>,
|
||||
) {
|
||||
fn apply_shortcuts(self: &Rc<Self>, shortcuts: impl IntoIterator<Item = Shortcut>) {
|
||||
let mut binds = self.persistent.binds.borrow_mut();
|
||||
for (key, value) in shortcuts {
|
||||
for shortcut in shortcuts {
|
||||
if let Action::SimpleCommand {
|
||||
cmd: SimpleCommand::None,
|
||||
} = value
|
||||
} = shortcut.action
|
||||
{
|
||||
self.persistent.seat.unbind(key);
|
||||
binds.remove(&key);
|
||||
} else {
|
||||
self.persistent.seat.bind(key, value.into_fn(self));
|
||||
binds.insert(key);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -427,6 +427,25 @@
|
|||
"type": "string",
|
||||
"description": "A color.\n\nThe format should be one of the following:\n\n- `#rgb`\n- `#rrggbb`\n- `#rgba`\n- `#rrggbba`\n"
|
||||
},
|
||||
"ComplexShortcut": {
|
||||
"description": "Describes a complex shortcut.\n\n- Example:\n\n ```toml\n [complex-shortcuts.XF86AudioRaiseVolume]\n mod-mask = \"alt\"\n action = { type = \"exec\", exec = [\"pactl\", \"set-sink-volume\", \"0\", \"+10%\"] }\n ```\n",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mod-mask": {
|
||||
"type": "string",
|
||||
"description": "The mod mask to apply to this shortcut.\n\nShould be a string containing modifiers concatenated by `-`. See the description\nof `Config.shortcuts` for more details.\n\nIf this field is omitted, all modifiers are included in the mask.\n\n- Example:\n \n To raise the volume whenever the XF86AudioRaiseVolume key is pressed regardless\n of any modifiers except `alt`:\n\n ```toml\n [complex-shortcuts.XF86AudioRaiseVolume]\n mod-mask = \"alt\"\n action = { type = \"exec\", exec = [\"pactl\", \"set-sink-volume\", \"0\", \"+10%\"] }\n ```\n\n Set `mod-mask = \"\"` to ignore all modifiers.\n"
|
||||
},
|
||||
"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": []
|
||||
},
|
||||
"Config": {
|
||||
"description": "This is the top-level table.\n\n- Example:\n\n ```toml\n keymap = \"\"\"\n xkb_keymap {\n xkb_keycodes { include \"evdev+aliases(qwerty)\" };\n xkb_types { include \"complete\" };\n xkb_compat { include \"complete\" };\n xkb_symbols { include \"pc+us+inet(evdev)\" };\n };\n \"\"\"\n\n on-graphics-initialized = { type = \"exec\", exec = \"mako\" }\n\n [shortcuts]\n alt-h = \"focus-left\"\n alt-j = \"focus-down\"\n alt-k = \"focus-up\"\n alt-l = \"focus-right\"\n\n alt-shift-h = \"move-left\"\n alt-shift-j = \"move-down\"\n alt-shift-k = \"move-up\"\n alt-shift-l = \"move-right\"\n\n alt-d = \"split-horizontal\"\n alt-v = \"split-vertical\"\n\n alt-t = \"toggle-split\"\n alt-m = \"toggle-mono\"\n alt-u = \"toggle-fullscreen\"\n\n alt-f = \"focus-parent\"\n alt-shift-c = \"close\"\n alt-shift-f = \"toggle-floating\"\n Super_L = { type = \"exec\", exec = \"alacritty\" }\n alt-p = { type = \"exec\", exec = \"bemenu-run\" }\n alt-q = \"quit\"\n alt-shift-r = \"reload-config-toml\"\n\n ctrl-alt-F1 = { type = \"switch-to-vt\", num = 1 }\n ctrl-alt-F2 = { type = \"switch-to-vt\", num = 2 }\n # ...\n\n alt-F1 = { type = \"show-workspace\", name = \"1\" }\n alt-F2 = { type = \"show-workspace\", name = \"2\" }\n # ...\n\n alt-shift-F1 = { type = \"move-to-workspace\", name = \"1\" }\n alt-shift-F2 = { type = \"move-to-workspace\", name = \"2\" }\n # ...\n ```\n",
|
||||
"type": "object",
|
||||
|
|
@ -447,6 +466,14 @@
|
|||
"$ref": "#/$defs/Action"
|
||||
}
|
||||
},
|
||||
"complex-shortcuts": {
|
||||
"description": "Complex compositor shortcuts.\n\nThe keys should have the same format as in the `shortcuts` table.\n\n- Example:\n\n ```toml\n [complex-shortcuts.XF86AudioRaiseVolume]\n mod-mask = \"alt\"\n action = { type = \"exec\", exec = [\"pactl\", \"set-sink-volume\", \"0\", \"+10%\"] }\n ```\n",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"description": "",
|
||||
"$ref": "#/$defs/ComplexShortcut"
|
||||
}
|
||||
},
|
||||
"on-graphics-initialized": {
|
||||
"description": "An action to execute when the graphics have been initialized for the first time.\n\nThis is a good place to start graphical applications.\n\n- Example:\n\n ```toml\n on-graphics-initialized = { type = \"exec\", exec = \"mako\" }\n ```\n",
|
||||
"$ref": "#/$defs/Action"
|
||||
|
|
|
|||
|
|
@ -590,6 +590,78 @@ The format should be one of the following:
|
|||
Values of this type should be strings.
|
||||
|
||||
|
||||
<a name="types-ComplexShortcut"></a>
|
||||
### `ComplexShortcut`
|
||||
|
||||
Describes a complex shortcut.
|
||||
|
||||
- Example:
|
||||
|
||||
```toml
|
||||
[complex-shortcuts.XF86AudioRaiseVolume]
|
||||
mod-mask = "alt"
|
||||
action = { type = "exec", exec = ["pactl", "set-sink-volume", "0", "+10%"] }
|
||||
```
|
||||
|
||||
Values of this type should be tables.
|
||||
|
||||
The table has the following fields:
|
||||
|
||||
- `mod-mask` (optional):
|
||||
|
||||
The mod mask to apply to this shortcut.
|
||||
|
||||
Should be a string containing modifiers concatenated by `-`. See the description
|
||||
of `Config.shortcuts` for more details.
|
||||
|
||||
If this field is omitted, all modifiers are included in the mask.
|
||||
|
||||
- Example:
|
||||
|
||||
To raise the volume whenever the XF86AudioRaiseVolume key is pressed regardless
|
||||
of any modifiers except `alt`:
|
||||
|
||||
```toml
|
||||
[complex-shortcuts.XF86AudioRaiseVolume]
|
||||
mod-mask = "alt"
|
||||
action = { type = "exec", exec = ["pactl", "set-sink-volume", "0", "+10%"] }
|
||||
```
|
||||
|
||||
Set `mod-mask = ""` to ignore all modifiers.
|
||||
|
||||
The value of this field should be a string.
|
||||
|
||||
- `action` (optional):
|
||||
|
||||
The action to execute.
|
||||
|
||||
Omitting this is the same as setting it to `"none"`.
|
||||
|
||||
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`
|
||||
|
||||
|
|
@ -715,6 +787,22 @@ The table has the following fields:
|
|||
|
||||
The value of this field should be a table whose values are [Actions](#types-Action).
|
||||
|
||||
- `complex-shortcuts` (optional):
|
||||
|
||||
Complex compositor shortcuts.
|
||||
|
||||
The keys should have the same format as in the `shortcuts` table.
|
||||
|
||||
- Example:
|
||||
|
||||
```toml
|
||||
[complex-shortcuts.XF86AudioRaiseVolume]
|
||||
mod-mask = "alt"
|
||||
action = { type = "exec", exec = ["pactl", "set-sink-volume", "0", "+10%"] }
|
||||
```
|
||||
|
||||
The value of this field should be a table whose values are [ComplexShortcuts](#types-ComplexShortcut).
|
||||
|
||||
- `on-graphics-initialized` (optional):
|
||||
|
||||
An action to execute when the graphics have been initialized for the first time.
|
||||
|
|
|
|||
|
|
@ -1741,6 +1741,23 @@ Config:
|
|||
[shortcuts]
|
||||
alt-q = "quit"
|
||||
```
|
||||
complex-shortcuts:
|
||||
kind: map
|
||||
values:
|
||||
ref: ComplexShortcut
|
||||
required: false
|
||||
description: |
|
||||
Complex compositor shortcuts.
|
||||
|
||||
The keys should have the same format as in the `shortcuts` table.
|
||||
|
||||
- Example:
|
||||
|
||||
```toml
|
||||
[complex-shortcuts.XF86AudioRaiseVolume]
|
||||
mod-mask = "alt"
|
||||
action = { type = "exec", exec = ["pactl", "set-sink-volume", "0", "+10%"] }
|
||||
```
|
||||
on-graphics-initialized:
|
||||
ref: Action
|
||||
required: false
|
||||
|
|
@ -2069,3 +2086,69 @@ RepeatRate:
|
|||
required: true
|
||||
description: |
|
||||
The number of milliseconds after a key is pressed before repeating begins.
|
||||
|
||||
|
||||
ComplexShortcut:
|
||||
kind: table
|
||||
description: |
|
||||
Describes a complex shortcut.
|
||||
|
||||
- Example:
|
||||
|
||||
```toml
|
||||
[complex-shortcuts.XF86AudioRaiseVolume]
|
||||
mod-mask = "alt"
|
||||
action = { type = "exec", exec = ["pactl", "set-sink-volume", "0", "+10%"] }
|
||||
```
|
||||
fields:
|
||||
mod-mask:
|
||||
kind: string
|
||||
required: false
|
||||
description: |
|
||||
The mod mask to apply to this shortcut.
|
||||
|
||||
Should be a string containing modifiers concatenated by `-`. See the description
|
||||
of `Config.shortcuts` for more details.
|
||||
|
||||
If this field is omitted, all modifiers are included in the mask.
|
||||
|
||||
- Example:
|
||||
|
||||
To raise the volume whenever the XF86AudioRaiseVolume key is pressed regardless
|
||||
of any modifiers except `alt`:
|
||||
|
||||
```toml
|
||||
[complex-shortcuts.XF86AudioRaiseVolume]
|
||||
mod-mask = "alt"
|
||||
action = { type = "exec", exec = ["pactl", "set-sink-volume", "0", "+10%"] }
|
||||
```
|
||||
|
||||
Set `mod-mask = ""` to ignore all modifiers.
|
||||
action:
|
||||
ref: Action
|
||||
required: false
|
||||
description: |
|
||||
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