1
0
Fork 0
forked from wry/wry

toml-config: add input modes

This commit is contained in:
Julian Orth 2025-07-21 15:50:24 +02:00
parent e160aa3406
commit a57f0036a8
10 changed files with 797 additions and 46 deletions

View file

@ -8,6 +8,7 @@ mod parsers;
mod spanned;
mod value;
pub use crate::config::parsers::input_mode::InputMode;
use {
crate::{
config::{
@ -81,6 +82,7 @@ pub enum SimpleCommand {
FocusTiles,
CreateMark,
JumpToMark,
PopMode(bool),
}
#[derive(Debug, Clone)]
@ -167,6 +169,10 @@ pub enum Action {
CreateMark(u32),
JumpToMark(u32),
CopyMark(u32, u32),
SetMode {
name: String,
latch: bool,
},
}
#[derive(Debug, Clone, Default)]
@ -502,6 +508,7 @@ pub struct Config {
pub show_bar: Option<bool>,
pub focus_history: Option<FocusHistory>,
pub middle_click_paste: Option<bool>,
pub input_modes: AHashMap<String, InputMode>,
}
#[derive(Debug, Error)]

View file

@ -27,6 +27,7 @@ mod gfx_api;
mod idle;
mod input;
mod input_match;
pub mod input_mode;
pub mod keymap;
mod libei;
mod log_level;

View file

@ -151,6 +151,8 @@ impl ActionParser<'_> {
"focus-tiles" => FocusTiles,
"create-mark" => CreateMark,
"jump-to-mark" => JumpToMark,
"clear-modes" => PopMode(false),
"pop-mode" => PopMode(true),
_ => {
return Err(
ActionParserError::UnknownSimpleAction(string.to_string()).spanned(span)
@ -414,6 +416,22 @@ impl ActionParser<'_> {
.map_spanned_err(ActionParserError::CopyMark)?;
Ok(Action::CopyMark(src, dst))
}
fn parse_push_mode(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
let (name,) = ext.extract((str("name"),))?;
Ok(Action::SetMode {
name: name.value.to_string(),
latch: false,
})
}
fn parse_latch_mode(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
let (name,) = ext.extract((str("name"),))?;
Ok(Action::SetMode {
name: name.value.to_string(),
latch: true,
})
}
}
impl Parser for ActionParser<'_> {
@ -471,6 +489,8 @@ impl Parser for ActionParser<'_> {
"create-mark" => self.parse_create_mark(&mut ext),
"jump-to-mark" => self.parse_jump_to_mark(&mut ext),
"copy-mark" => self.parse_copy_mark(&mut ext),
"push-mode" => self.parse_push_mode(&mut ext),
"latch-mode" => self.parse_latch_mode(&mut ext),
v => {
ext.ignore_unused();
return Err(ActionParserError::UnknownType(v.to_string()).spanned(ty.span));

View file

@ -20,6 +20,7 @@ use {
gfx_api::GfxApiParser,
idle::IdleParser,
input::InputsParser,
input_mode::InputModesParser,
keymap::KeymapParser,
libei::LibeiParser,
log_level::LogLevelParser,
@ -44,6 +45,7 @@ use {
toml_value::Value,
},
},
ahash::AHashMap,
indexmap::IndexMap,
std::collections::HashSet,
thiserror::Error,
@ -136,7 +138,7 @@ impl Parser for ConfigParser<'_> {
show_bar,
focus_history_val,
),
(middle_click_paste,),
(middle_click_paste, input_modes_val),
) = ext.extract((
(
opt(val("keymap")),
@ -186,7 +188,7 @@ impl Parser for ConfigParser<'_> {
recover(opt(bol("show-bar"))),
opt(val("focus-history")),
),
(recover(opt(bol("middle-click-paste"))),),
(recover(opt(bol("middle-click-paste"))), opt(val("modes"))),
))?;
let mut keymap = None;
if let Some(value) = keymap_val {
@ -475,6 +477,15 @@ impl Parser for ConfigParser<'_> {
}
}
}
let mut input_modes = AHashMap::new();
if let Some(value) = input_modes_val {
match value.parse(&mut InputModesParser(self.0)) {
Ok(v) => input_modes = v,
Err(e) => {
log::warn!("Could not parse the input modes: {}", self.0.error(e),);
}
}
}
Ok(Config {
keymap,
repeat_rate,
@ -516,6 +527,7 @@ impl Parser for ConfigParser<'_> {
show_bar: show_bar.despan(),
focus_history,
middle_click_paste: middle_click_paste.despan(),
input_modes,
})
}
}

View file

@ -0,0 +1,125 @@
use {
crate::{
config::{
Shortcut,
context::Context,
extractor::{Extractor, ExtractorError, opt, recover, str, val},
parser::{DataType, ParseResult, Parser, UnexpectedDataType},
parsers::shortcuts::{ComplexShortcutsParser, ShortcutsParser, ShortcutsParserError},
spanned::SpannedErrorExt,
},
toml::{
toml_span::{DespanExt, Span, Spanned},
toml_value::Value,
},
},
ahash::{AHashMap, AHashSet},
indexmap::IndexMap,
std::collections::HashSet,
thiserror::Error,
};
#[derive(Debug, Error)]
pub enum InputModeParserError {
#[error(transparent)]
Expected(#[from] UnexpectedDataType),
#[error(transparent)]
ExtractorError(#[from] ExtractorError),
#[error("Could not parse the shortcuts")]
ParseShortcuts(#[source] ShortcutsParserError),
}
#[derive(Clone, Debug)]
pub struct InputMode {
pub parent: Option<String>,
pub shortcuts: Vec<Shortcut>,
}
pub struct InputModesParser<'a>(pub &'a Context<'a>);
impl Parser for InputModesParser<'_> {
type Value = AHashMap<String, InputMode>;
type Error = InputModeParserError;
const EXPECTED: &'static [DataType] = &[DataType::Table];
fn parse_table(
&mut self,
_span: Span,
table: &IndexMap<Spanned<String>, Spanned<Value>>,
) -> ParseResult<Self> {
let mut modes = AHashMap::new();
let mut used = AHashSet::new();
for (key, value) in table.iter() {
let mode = match value.parse(&mut InputModeParser(self.0)) {
Ok(m) => m,
Err(e) => {
log::warn!(
"Could not parse input mode {}: {}",
key.value,
self.0.error(e)
);
continue;
}
};
log_used(self.0, &mut used, key);
modes.insert(key.value.to_string(), mode);
}
Ok(modes)
}
}
pub struct InputModeParser<'a>(pub &'a Context<'a>);
impl Parser for InputModeParser<'_> {
type Value = InputMode;
type Error = InputModeParserError;
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.0, span, table);
let (parent, shortcuts_val, complex_shortcuts_val) = ext.extract((
recover(opt(str("parent"))),
opt(val("shortcuts")),
opt(val("complex-shortcuts")),
))?;
let mut used_keys = HashSet::new();
let mut shortcuts = vec![];
if let Some(value) = shortcuts_val {
value
.parse(&mut ShortcutsParser {
cx: self.0,
used_keys: &mut used_keys,
shortcuts: &mut shortcuts,
})
.map_spanned_err(InputModeParserError::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(InputModeParserError::ParseShortcuts)?;
}
Ok(InputMode {
parent: parent.despan_into(),
shortcuts,
})
}
}
fn log_used(cx: &Context<'_>, used: &mut AHashSet<Spanned<String>>, key: &Spanned<String>) {
if let Some(prev) = used.get(key) {
log::warn!(
"Duplicate input mode overrides previous definition: {}",
cx.error3(key.span)
);
log::info!("Previous definition here: {}", cx.error3(prev.span));
}
used.insert(key.clone());
}

View file

@ -7,16 +7,18 @@
mod config;
mod rules;
mod shortcuts;
mod toml;
use {
crate::{
config::{
Action, ClientRule, Config, ConfigConnector, ConfigDrmDevice, ConfigKeymap,
ConnectorMatch, DrmDeviceMatch, Exec, Input, InputMatch, Output, OutputMatch, Shortcut,
ConnectorMatch, DrmDeviceMatch, Exec, Input, InputMatch, Output, OutputMatch,
SimpleCommand, Status, Theme, WindowRule, parse_config,
},
rules::{MatcherTemp, RuleMapper},
shortcuts::ModeState,
},
ahash::{AHashMap, AHashSet},
error_reporter::Report,
@ -31,7 +33,7 @@ use {
set_libei_socket_enabled,
},
is_reload,
keyboard::{Keymap, ModifiedKeySym},
keyboard::Keymap,
logging::set_log_level,
on_devices_enumerated, on_idle, on_unload, quit, reload, set_color_management_enabled,
set_default_workspace_capture, set_explicit_sync_enabled, set_float_above_fullscreen,
@ -91,6 +93,20 @@ impl FnBuilder for RcFnBuilder {
}
}
struct ShortcutFnBuilder<'a>(&'a Rc<State>);
impl FnBuilder for ShortcutFnBuilder<'_> {
type Output = Rc<dyn Fn()>;
fn new<F: Fn() + 'static>(&self, f: F) -> Self::Output {
let state = self.0.clone();
Rc::new(move || {
state.cancel_mode_latch();
f();
})
}
}
impl Action {
fn into_fn(self, state: &Rc<State>) -> Box<dyn Fn()> {
self.into_fn_impl(&BoxFnBuilder, state)
@ -100,6 +116,10 @@ impl Action {
self.into_fn_impl(&RcFnBuilder, state)
}
fn into_shortcut_fn(self, state: &Rc<State>) -> Rc<dyn Fn()> {
self.into_fn_impl(&ShortcutFnBuilder(state), state)
}
fn into_fn_impl<B: FnBuilder>(self, b: &B, state: &Rc<State>) -> B::Output {
macro_rules! client_action {
($name:ident, $opt:expr) => {{
@ -187,6 +207,10 @@ impl Action {
let persistent = state.persistent.clone();
b.new(move || persistent.seat.jump_to_mark(None))
}
SimpleCommand::PopMode(pop) => {
let state = state.clone();
b.new(move || state.pop_mode(pop))
}
},
Action::Multi { actions } => {
let actions: Vec<_> = actions.into_iter().map(|a| a.into_fn(state)).collect();
@ -355,6 +379,18 @@ impl Action {
let persistent = state.persistent.clone();
b.new(move || persistent.seat.copy_mark(s, d))
}
Action::SetMode { name, latch } => {
let state = state.clone();
let new = state.get_mode_slot(&name);
b.new(move || {
let new = new.mode.borrow();
let Some(new) = new.as_ref() else {
log::warn!("Input mode {name} does not exist");
return;
};
state.set_mode(new, latch);
})
}
}
}
}
@ -773,43 +809,6 @@ impl Drop for State {
type SwitchActions = Vec<(InputMatch, AHashMap<SwitchEvent, Box<dyn Fn()>>)>;
impl State {
fn unbind_all(&self) {
let mut binds = self.persistent.binds.borrow_mut();
for bind in binds.drain() {
self.persistent.seat.unbind(bind);
}
}
fn apply_shortcuts(self: &Rc<Self>, shortcuts: impl IntoIterator<Item = Shortcut>) {
let mut binds = self.persistent.binds.borrow_mut();
for shortcut in shortcuts {
if let Action::SimpleCommand {
cmd: SimpleCommand::None,
} = shortcut.action
{
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, f);
binds.insert(shortcut.keysym);
}
}
fn get_keymap(&self, map: &ConfigKeymap) -> Option<Keymap> {
let map = match map {
ConfigKeymap::Named(n) => match self.keymaps.get(n) {
@ -998,13 +997,13 @@ struct PersistentState {
seen_outputs: RefCell<AHashSet<OutputId>>,
default: Config,
seat: Seat,
binds: RefCell<AHashSet<ModifiedKeySym>>,
#[expect(clippy::type_complexity)]
actions: RefCell<AHashMap<Rc<String>, Rc<dyn Fn()>>>,
client_rules: Cell<Vec<MatcherTemp<ClientRule>>>,
client_rule_mapper: RefCell<Option<RuleMapper<ClientRule>>>,
window_rules: Cell<Vec<MatcherTemp<WindowRule>>>,
mark_names: RefCell<AHashMap<String, u32>>,
mode_state: ModeState,
}
fn load_config(initial_load: bool, persistent: &Rc<PersistentState>) {
@ -1088,6 +1087,7 @@ fn load_config(initial_load: bool, persistent: &Rc<PersistentState>) {
client: Default::default(),
window: Default::default(),
});
state.clear_modes_after_reload();
let (client_rules, client_rule_mapper) = state.create_rules(&config.client_rules);
persistent.client_rules.set(client_rules);
*state.persistent.client_rule_mapper.borrow_mut() = Some(client_rule_mapper);
@ -1118,8 +1118,7 @@ fn load_config(initial_load: bool, persistent: &Rc<PersistentState>) {
None => on_idle(|| ()),
Some(a) => on_idle(a.into_fn(&state)),
}
state.unbind_all();
state.apply_shortcuts(config.shortcuts);
state.init_modes(&config.shortcuts, &config.input_modes);
if let Some(keymap) = config.keymap {
state.set_keymap(&keymap);
}
@ -1334,18 +1333,19 @@ pub fn configure() {
seen_outputs: Default::default(),
default: default.unwrap(),
seat: default_seat(),
binds: Default::default(),
actions: Default::default(),
client_rules: Default::default(),
client_rule_mapper: Default::default(),
window_rules: Default::default(),
mark_names,
mode_state: Default::default(),
});
{
let p = persistent.clone();
on_unload(move || {
p.actions.borrow_mut().clear();
p.client_rule_mapper.borrow_mut().take();
p.mode_state.clear();
});
}
load_config(true, &persistent);

View file

@ -0,0 +1,276 @@
use {
crate::{
State,
config::{Action, InputMode, Shortcut, SimpleCommand},
},
ahash::{AHashMap, AHashSet},
jay_config::keyboard::{ModifiedKeySym, mods::Modifiers},
std::{
cell::{Cell, RefCell},
collections::hash_map::Entry,
rc::Rc,
},
};
#[derive(Default)]
pub struct ModeState {
latched: Cell<bool>,
stack: RefCell<Vec<Rc<ConvertedShortcuts>>>,
slots: RefCell<AHashMap<String, Rc<ModeSlot>>>,
diffs: RefCell<AHashMap<[*const ConvertedShortcuts; 2], Rc<Vec<ModeDiff>>>>,
current: RefCell<Rc<ConvertedShortcuts>>,
}
impl ModeState {
pub fn clear(&self) {
self.slots.borrow_mut().clear();
self.stack.borrow_mut().clear();
self.diffs.borrow_mut().clear();
*self.current.borrow_mut() = Default::default();
}
}
pub type ConvertedShortcuts = AHashMap<ModifiedKeySym, ConvertedShortcut>;
#[derive(Clone)]
pub struct ConvertedShortcut {
mask: Modifiers,
shortcut: Rc<dyn Fn()>,
}
#[derive(Default)]
pub struct ModeSlot {
pub mode: RefCell<Option<Rc<ConvertedShortcuts>>>,
}
enum ModeDiff {
Bind(ModifiedKeySym, Modifiers, Rc<dyn Fn()>),
Unbind(ModifiedKeySym),
}
impl PartialEq for ConvertedShortcut {
fn eq(&self, other: &Self) -> bool {
if self.mask != other.mask {
return false;
}
Rc::ptr_eq(&self.shortcut, &other.shortcut)
}
}
impl State {
pub fn get_mode_slot(&self, name: &str) -> Rc<ModeSlot> {
let state = &self.persistent.mode_state;
state
.slots
.borrow_mut()
.entry(name.to_string())
.or_default()
.clone()
}
pub fn clear_modes_after_reload(&self) {
let state = &self.persistent.mode_state;
state.slots.borrow_mut().clear();
state.diffs.borrow_mut().clear();
}
pub fn init_modes(
self: &Rc<Self>,
shortcuts: &[Shortcut],
modes: &AHashMap<String, InputMode>,
) {
let state = &self.persistent.mode_state;
let base = self.convert_shortcuts(shortcuts);
let stack = &mut *state.stack.borrow_mut();
stack.clear();
stack.push(base.clone());
self.convert_modes(&base, modes);
self.apply_shortcuts(&base);
state.latched.set(false);
}
pub fn set_mode(&self, new: &Rc<ConvertedShortcuts>, latch: bool) {
let state = &self.persistent.mode_state;
self.cancel_mode_latch();
self.apply_shortcuts(new);
let stack = &mut *state.stack.borrow_mut();
stack.push(new.clone());
if latch {
state.latched.set(true);
}
}
pub fn pop_mode(&self, pop: bool) {
let state = &self.persistent.mode_state;
let stack = &mut *state.stack.borrow_mut();
if stack.len() < 1 + pop as usize {
log::error!("Mode stack is empty");
return;
}
self.cancel_mode_latch();
if pop {
stack.pop();
} else {
stack.truncate(1);
}
let new = stack.last().unwrap();
self.apply_shortcuts(new);
}
pub fn cancel_mode_latch(&self) {
let state = &self.persistent.mode_state;
if !state.latched.take() {
return;
}
let stack = &mut *state.stack.borrow_mut();
if stack.len() < 2 {
log::error!("Mode is latched but mode stack is empty");
return;
}
let _ = stack.pop();
let new = stack.last().unwrap();
self.apply_shortcuts(new);
}
pub fn convert_modes(
self: &Rc<Self>,
base: &ConvertedShortcuts,
modes: &AHashMap<String, InputMode>,
) {
let mut pending = AHashSet::new();
let mut out = AHashMap::new();
for (name, mode) in modes {
if !out.contains_key(name) {
self.convert_mode(&mut out, &mut pending, base, modes, name, mode);
}
}
}
fn convert_mode<'a>(
self: &Rc<Self>,
out: &'a mut AHashMap<String, Rc<ConvertedShortcuts>>,
pending: &mut AHashSet<String>,
base: &ConvertedShortcuts,
modes: &AHashMap<String, InputMode>,
mode_name: &String,
mode: &InputMode,
) -> Option<&'a ConvertedShortcuts> {
if !pending.insert(mode_name.clone()) {
log::warn!("Detected loop while converting input mode `{mode_name}`");
return None;
}
let mut shortcuts = None;
if let Some(parent) = &mode.parent {
match out.get(parent) {
Some(c) => shortcuts = Some((**c).clone()),
None => match modes.get(parent) {
None => {
log::warn!("Input mode `{parent}` does not exist");
}
Some(p) => {
if let Some(p) = self.convert_mode(out, pending, base, modes, parent, p) {
shortcuts = Some(p.clone());
}
}
},
}
}
let mut shortcuts = shortcuts.unwrap_or_else(|| base.clone());
self.convert_shortcuts_(&mode.shortcuts, &mut shortcuts);
let shortcuts = Rc::new(shortcuts);
*self.get_mode_slot(mode_name).mode.borrow_mut() = Some(shortcuts.clone());
let res = out.entry(mode_name.clone()).insert_entry(shortcuts);
Some(res.into_mut())
}
pub fn convert_shortcuts<'a>(
self: &Rc<Self>,
shortcuts: impl IntoIterator<Item = &'a Shortcut>,
) -> Rc<ConvertedShortcuts> {
let mut dst = ConvertedShortcuts::new();
self.convert_shortcuts_(shortcuts, &mut dst);
Rc::new(dst)
}
fn convert_shortcuts_<'a>(
self: &Rc<Self>,
shortcuts: impl IntoIterator<Item = &'a Shortcut>,
dst: &mut ConvertedShortcuts,
) {
for sc in shortcuts {
match self.convert_shortcut(sc.clone()) {
None => dst.remove(&sc.keysym),
Some(cs) => dst.insert(sc.keysym, cs),
};
}
}
fn convert_shortcut(self: &Rc<Self>, shortcut: Shortcut) -> Option<ConvertedShortcut> {
if let Action::SimpleCommand {
cmd: SimpleCommand::None,
} = shortcut.action
&& shortcut.latch.is_none()
{
return None;
}
let mut f = shortcut.action.into_shortcut_fn(self);
if let Some(l) = shortcut.latch {
let l = l.into_rc_fn(self);
let s = self.persistent.seat;
f = Rc::new(move || {
f();
let l = l.clone();
s.latch(move || l());
});
}
Some(ConvertedShortcut {
mask: shortcut.mask,
shortcut: f,
})
}
pub fn apply_shortcuts(&self, new: &Rc<ConvertedShortcuts>) {
let state = &self.persistent.mode_state;
let current = &mut *state.current.borrow_mut();
let diffs = self.get_or_create_mode_diffs(current, new);
let seat = &self.persistent.seat;
for diff in &*diffs {
match diff {
ModeDiff::Bind(key, mask, f) => {
let f = f.clone();
seat.bind_masked(*mask, *key, move || f());
}
ModeDiff::Unbind(key) => {
seat.unbind(*key);
}
}
}
*current = new.clone();
}
fn get_or_create_mode_diffs(
&self,
old: &Rc<ConvertedShortcuts>,
new: &Rc<ConvertedShortcuts>,
) -> Rc<Vec<ModeDiff>> {
let state = &self.persistent.mode_state;
let diffs = &mut *state.diffs.borrow_mut();
match diffs.entry([Rc::as_ptr(old), Rc::as_ptr(new)]) {
Entry::Occupied(o) => o.get().clone(),
Entry::Vacant(v) => {
let mut diffs = vec![];
for (key, sc) in new.iter() {
if old.get(key) != Some(sc) {
diffs.push(ModeDiff::Bind(*key, sc.mask, sc.shortcut.clone()));
}
}
for key in old.keys() {
if !new.contains_key(key) {
diffs.push(ModeDiff::Unbind(*key));
}
}
v.insert(Rc::new(diffs)).clone()
}
}
}
}

View file

@ -533,6 +533,40 @@
"src",
"dst"
]
},
{
"description": "Pushes an input mode on top of the input-mode stack. The mode can be popped\nwith the `pop-mode` action.\n\n- Example:\n\n ```toml\n [shortcuts]\n alt-x = { type = \"push-mode\", name = \"navigation\" }\n ```\n",
"type": "object",
"properties": {
"type": {
"const": "push-mode"
},
"name": {
"type": "string",
"description": "The name of the mode."
}
},
"required": [
"type",
"name"
]
},
{
"description": "Temporarily pushes an input mode on top of the input-mode stack. The new mode\nwill automatically be popped when the next shortcut is invoked.\n\n- Example:\n\n ```toml\n [shortcuts]\n alt-x = { type = \"latch-mode\", name = \"navigation\" }\n ```\n",
"type": "object",
"properties": {
"type": {
"const": "latch-mode"
},
"name": {
"type": "string",
"description": "The name of the mode."
}
},
"required": [
"type",
"name"
]
}
]
}
@ -949,6 +983,14 @@
"middle-click-paste": {
"type": "boolean",
"description": "Configures whether middle-click pasting is enabled.\n\nChanging this has no effect on running applications.\n\nThe default is `true`.\n"
},
"modes": {
"description": "Configures the input modes.\n\nModes can be used to define shortcuts that are only active when the mode is\nactive.\n\n- Example\n\n ```toml\n [modes.\"navigation\".shortcuts]\n w = \"focus-up\"\n a = \"focus-left\"\n s = \"focus-down\"\n d = \"focus-right\"\n r = \"focus-above\"\n f = \"focus-below\"\n q = \"focus-prev\"\n e = \"focus-next\"\n ```\n\nModes can be activated with the `push-mode` and `latch-mode` actions.\n",
"type": "object",
"additionalProperties": {
"description": "",
"$ref": "#/$defs/InputMode"
}
}
},
"required": []
@ -1420,6 +1462,33 @@
}
]
},
"InputMode": {
"description": "Defines an input mode.\n\nModes can be used to define shortcuts that are only active when the mode is active.\n\n- Example\n\n ```toml\n [modes.\"navigation\".shortcuts]\n w = \"focus-up\"\n a = \"focus-left\"\n s = \"focus-down\"\n d = \"focus-right\"\n r = \"focus-above\"\n f = \"focus-below\"\n q = \"focus-prev\"\n e = \"focus-next\"\n ```\n",
"type": "object",
"properties": {
"parent": {
"type": "string",
"description": "The parent of this input mode.\n\nThis mode inherits all shortcuts from this parent. If this field is not set, then\nit inherits the shortcuts from the top-level shortcuts.\n\nNote that you can disable a shortcut by explicitly assigning it the action `none`.\n"
},
"shortcuts": {
"description": "The shortcuts of this mode.\n\nSee the same field in the top-level `Config` object for a description.\n",
"type": "object",
"additionalProperties": {
"description": "",
"$ref": "#/$defs/Action"
}
},
"complex-shortcuts": {
"description": "The complex shortcuts of this mode.\n\nSee the same field in the top-level `Config` object for a description.\n",
"type": "object",
"additionalProperties": {
"description": "",
"$ref": "#/$defs/ComplexShortcut"
}
}
},
"required": []
},
"Keymap": {
"description": "A keymap.\n",
"anyOf": [
@ -1692,7 +1761,9 @@
"focus-above",
"focus-tiles",
"create-mark",
"jump-to-mark"
"jump-to-mark",
"clear-modes",
"pop-mode"
]
},
"Status": {

View file

@ -756,6 +756,46 @@ This table is a tagged union. The variant is determined by the `type` field. It
The value of this field should be a [MarkId](#types-MarkId).
- `push-mode`:
Pushes an input mode on top of the input-mode stack. The mode can be popped
with the `pop-mode` action.
- Example:
```toml
[shortcuts]
alt-x = { type = "push-mode", name = "navigation" }
```
The table has the following fields:
- `name` (required):
The name of the mode.
The value of this field should be a string.
- `latch-mode`:
Temporarily pushes an input mode on top of the input-mode stack. The new mode
will automatically be popped when the next shortcut is invoked.
- Example:
```toml
[shortcuts]
alt-x = { type = "latch-mode", name = "navigation" }
```
The table has the following fields:
- `name` (required):
The name of the mode.
The value of this field should be a string.
<a name="types-Brightness"></a>
### `Brightness`
@ -1911,6 +1951,31 @@ The table has the following fields:
The value of this field should be a boolean.
- `modes` (optional):
Configures the input modes.
Modes can be used to define shortcuts that are only active when the mode is
active.
- Example
```toml
[modes."navigation".shortcuts]
w = "focus-up"
a = "focus-left"
s = "focus-down"
d = "focus-right"
r = "focus-above"
f = "focus-below"
q = "focus-prev"
e = "focus-next"
```
Modes can be activated with the `push-mode` and `latch-mode` actions.
The value of this field should be a table whose values are [InputModes](#types-InputMode).
<a name="types-Connector"></a>
### `Connector`
@ -3025,6 +3090,59 @@ The table has the following fields:
The value of this field should be a boolean.
<a name="types-InputMode"></a>
### `InputMode`
Defines an input mode.
Modes can be used to define shortcuts that are only active when the mode is active.
- Example
```toml
[modes."navigation".shortcuts]
w = "focus-up"
a = "focus-left"
s = "focus-down"
d = "focus-right"
r = "focus-above"
f = "focus-below"
q = "focus-prev"
e = "focus-next"
```
Values of this type should be tables.
The table has the following fields:
- `parent` (optional):
The parent of this input mode.
This mode inherits all shortcuts from this parent. If this field is not set, then
it inherits the shortcuts from the top-level shortcuts.
Note that you can disable a shortcut by explicitly assigning it the action `none`.
The value of this field should be a string.
- `shortcuts` (optional):
The shortcuts of this mode.
See the same field in the top-level `Config` object for a description.
The value of this field should be a table whose values are [Actions](#types-Action).
- `complex-shortcuts` (optional):
The complex shortcuts of this mode.
See the same field in the top-level `Config` object for a description.
The value of this field should be a table whose values are [ComplexShortcuts](#types-ComplexShortcut).
<a name="types-Keymap"></a>
### `Keymap`
@ -3830,6 +3948,14 @@ The string should have one of the following values:
The next pressed key identifies the mark to jump to.
- `clear-modes`:
Disables all previously set input modes, clearing the input-mode stack.
- `pop-mode`:
Pops the topmost mode from the input-mode stack.
<a name="types-Status"></a>

View file

@ -675,6 +675,38 @@ Action:
description: The destination id to copy to.
required: true
ref: MarkId
push-mode:
description: |
Pushes an input mode on top of the input-mode stack. The mode can be popped
with the `pop-mode` action.
- Example:
```toml
[shortcuts]
alt-x = { type = "push-mode", name = "navigation" }
```
fields:
name:
description: The name of the mode.
required: true
kind: string
latch-mode:
description: |
Temporarily pushes an input mode on top of the input-mode stack. The new mode
will automatically be popped when the next shortcut is invoked.
- Example:
```toml
[shortcuts]
alt-x = { type = "latch-mode", name = "navigation" }
```
fields:
name:
description: The name of the mode.
required: true
kind: string
Exec:
@ -953,6 +985,10 @@ SimpleActionName:
Interactively jumps to a mark.
The next pressed key identifies the mark to jump to.
- value: clear-modes
description: Disables all previously set input modes, clearing the input-mode stack.
- value: pop-mode
description: Pops the topmost mode from the input-mode stack.
Color:
@ -2742,6 +2778,32 @@ Config:
Changing this has no effect on running applications.
The default is `true`.
modes:
kind: map
values:
ref: InputMode
required: false
description: |
Configures the input modes.
Modes can be used to define shortcuts that are only active when the mode is
active.
- Example
```toml
[modes."navigation".shortcuts]
w = "focus-up"
a = "focus-left"
s = "focus-down"
d = "focus-right"
r = "focus-above"
f = "focus-below"
q = "focus-prev"
e = "focus-next"
```
Modes can be activated with the `push-mode` and `latch-mode` actions.
Idle:
@ -3893,3 +3955,54 @@ MarkId:
Identifies a mark with an arbitrary string.
kind: string
required: false
InputMode:
kind: table
description: |
Defines an input mode.
Modes can be used to define shortcuts that are only active when the mode is active.
- Example
```toml
[modes."navigation".shortcuts]
w = "focus-up"
a = "focus-left"
s = "focus-down"
d = "focus-right"
r = "focus-above"
f = "focus-below"
q = "focus-prev"
e = "focus-next"
```
fields:
parent:
kind: string
required: false
description: |
The parent of this input mode.
This mode inherits all shortcuts from this parent. If this field is not set, then
it inherits the shortcuts from the top-level shortcuts.
Note that you can disable a shortcut by explicitly assigning it the action `none`.
shortcuts:
kind: map
values:
ref: Action
required: false
description: |
The shortcuts of this mode.
See the same field in the top-level `Config` object for a description.
complex-shortcuts:
kind: map
values:
ref: ComplexShortcut
required: false
description: |
The complex shortcuts of this mode.
See the same field in the top-level `Config` object for a description.