1
0
Fork 0
forked from wry/wry

config: add support for mod masks in shortcuts

This commit is contained in:
Julian Orth 2024-04-16 16:27:26 +02:00
parent 27f30f8d28
commit 90dbde99ab
15 changed files with 501 additions and 92 deletions

View file

@ -11,7 +11,10 @@ use {
},
exec::Command,
input::{acceleration::AccelProfile, capability::Capability, InputDevice, Seat},
keyboard::Keymap,
keyboard::{
mods::{Modifiers, RELEASE},
Keymap,
},
logging::LogLevel,
tasks::{JoinHandle, JoinSlot},
theme::{colors::Colorable, sized::Resizable, Color},
@ -64,12 +67,17 @@ fn ignore_panic(name: &str, f: impl FnOnce()) {
}
}
struct KeyHandler {
mask: Modifiers,
cb: Callback,
}
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>>>,
@ -915,33 +923,45 @@ impl Client {
keymap
}
pub fn bind<T: Into<ModifiedKeySym>, F: FnMut() + 'static>(
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 = cb;
mem::replace(&mut o.mask, mod_mask) != mod_mask
}
Entry::Vacant(v) => {
v.insert(f);
v.insert(KeyHandler { mask: mod_mask, cb });
true
}
}
};
if register {
self.send(&ClientMessage::AddShortcut {
seat,
mods: mod_sym.mods,
sym: mod_sym.sym,
});
let msg = if !mod_mask.0 == 0 {
ClientMessage::AddShortcut {
seat,
mods: mod_sym.mods,
sym: mod_sym.sym,
}
} else {
ClientMessage::AddShortcut2 {
seat,
mods: mod_sym.mods,
mod_mask,
sym: mod_sym.sym,
}
};
self.send(&msg);
}
}
@ -1104,7 +1124,11 @@ impl Client {
}
ServerMessage::InvokeShortcut { seat, mods, sym } => {
let ms = ModifiedKeySym { mods, sym };
let handler = self.key_handlers.borrow_mut().get(&(seat, ms)).cloned();
let handler = self
.key_handlers
.borrow_mut()
.get(&(seat, ms))
.map(|k| k.cb.clone());
if let Some(handler) = handler {
run_cb("shortcut", &handler, ());
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -42,7 +42,7 @@ use {
ModifiedKeySym,
},
smallvec::SmallVec,
std::{cell::RefCell, rc::Rc},
std::{cell::RefCell, collections::hash_map::Entry, rc::Rc},
};
#[derive(Default)]
@ -380,13 +380,18 @@ 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(ModifiedKeySym {
mods: Modifiers(key_mods),
sym: KeySym(sym),
});
}
}
}
}
}
@ -608,15 +613,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) {

View file

@ -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,18 @@ pub struct RepeatRate {
pub delay: i32,
}
#[derive(Debug, Clone)]
pub struct Shortcut {
pub mask: Modifiers,
pub keysym: ModifiedKeySym,
pub action: 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>,

View file

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

View file

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

View file

@ -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,22 @@ 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),
}
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 +52,137 @@ 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,
});
}
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) = ext.extract((opt(str("mod-mask")), opt(val("action"))))?;
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)?,
};
Ok(Shortcut {
mask: mod_mask,
keysym: self.keysym,
action,
})
}
}
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);
}

View file

@ -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},
@ -541,21 +541,22 @@ 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);
self.persistent.seat.unbind(shortcut.keysym);
binds.remove(&shortcut.keysym);
} else {
self.persistent.seat.bind(key, value.into_fn(self));
binds.insert(key);
self.persistent.seat.bind_masked(
shortcut.mask,
shortcut.keysym,
shortcut.action.into_fn(self),
);
binds.insert(shortcut.keysym);
}
}
}

View file

@ -427,6 +427,21 @@
"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"
}
},
"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 +462,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"

View file

@ -590,6 +590,56 @@ 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).
<a name="types-Config"></a>
### `Config`
@ -715,6 +765,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.

View file

@ -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,48 @@ 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"`.