From a57f0036a82ee463cc79a1a42393607d6c022396 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Mon, 21 Jul 2025 15:50:24 +0200 Subject: [PATCH] toml-config: add input modes --- toml-config/src/config.rs | 7 + toml-config/src/config/parsers.rs | 1 + toml-config/src/config/parsers/action.rs | 20 ++ toml-config/src/config/parsers/config.rs | 16 +- toml-config/src/config/parsers/input_mode.rs | 125 +++++++++ toml-config/src/lib.rs | 86 +++--- toml-config/src/shortcuts.rs | 276 +++++++++++++++++++ toml-spec/spec/spec.generated.json | 73 ++++- toml-spec/spec/spec.generated.md | 126 +++++++++ toml-spec/spec/spec.yaml | 113 ++++++++ 10 files changed, 797 insertions(+), 46 deletions(-) create mode 100644 toml-config/src/config/parsers/input_mode.rs create mode 100644 toml-config/src/shortcuts.rs diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index 54eebb7d..4d08b3c2 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -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, pub focus_history: Option, pub middle_click_paste: Option, + pub input_modes: AHashMap, } #[derive(Debug, Error)] diff --git a/toml-config/src/config/parsers.rs b/toml-config/src/config/parsers.rs index c04ecaaa..a56a8bea 100644 --- a/toml-config/src/config/parsers.rs +++ b/toml-config/src/config/parsers.rs @@ -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; diff --git a/toml-config/src/config/parsers/action.rs b/toml-config/src/config/parsers/action.rs index 3797e063..1a4423d6 100644 --- a/toml-config/src/config/parsers/action.rs +++ b/toml-config/src/config/parsers/action.rs @@ -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 { + 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 { + 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)); diff --git a/toml-config/src/config/parsers/config.rs b/toml-config/src/config/parsers/config.rs index 31b3448d..f748838c 100644 --- a/toml-config/src/config/parsers/config.rs +++ b/toml-config/src/config/parsers/config.rs @@ -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, }) } } diff --git a/toml-config/src/config/parsers/input_mode.rs b/toml-config/src/config/parsers/input_mode.rs new file mode 100644 index 00000000..4a8d4b6c --- /dev/null +++ b/toml-config/src/config/parsers/input_mode.rs @@ -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, + pub shortcuts: Vec, +} + +pub struct InputModesParser<'a>(pub &'a Context<'a>); + +impl Parser for InputModesParser<'_> { + type Value = AHashMap; + type Error = InputModeParserError; + const EXPECTED: &'static [DataType] = &[DataType::Table]; + + fn parse_table( + &mut self, + _span: Span, + table: &IndexMap, Spanned>, + ) -> ParseResult { + 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>, + ) -> ParseResult { + 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>, key: &Spanned) { + 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()); +} diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs index 127a975d..d9395777 100644 --- a/toml-config/src/lib.rs +++ b/toml-config/src/lib.rs @@ -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); + +impl FnBuilder for ShortcutFnBuilder<'_> { + type Output = Rc; + + fn new(&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) -> Box { 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) -> Rc { + self.into_fn_impl(&ShortcutFnBuilder(state), state) + } + fn into_fn_impl(self, b: &B, state: &Rc) -> 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>)>; 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, shortcuts: impl IntoIterator) { - 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 { let map = match map { ConfigKeymap::Named(n) => match self.keymaps.get(n) { @@ -998,13 +997,13 @@ struct PersistentState { seen_outputs: RefCell>, default: Config, seat: Seat, - binds: RefCell>, #[expect(clippy::type_complexity)] actions: RefCell, Rc>>, client_rules: Cell>>, client_rule_mapper: RefCell>>, window_rules: Cell>>, mark_names: RefCell>, + mode_state: ModeState, } fn load_config(initial_load: bool, persistent: &Rc) { @@ -1088,6 +1087,7 @@ fn load_config(initial_load: bool, persistent: &Rc) { 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) { 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); diff --git a/toml-config/src/shortcuts.rs b/toml-config/src/shortcuts.rs new file mode 100644 index 00000000..7156433c --- /dev/null +++ b/toml-config/src/shortcuts.rs @@ -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, + stack: RefCell>>, + slots: RefCell>>, + diffs: RefCell>>>, + current: RefCell>, +} + +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; + +#[derive(Clone)] +pub struct ConvertedShortcut { + mask: Modifiers, + shortcut: Rc, +} + +#[derive(Default)] +pub struct ModeSlot { + pub mode: RefCell>>, +} + +enum ModeDiff { + Bind(ModifiedKeySym, Modifiers, Rc), + 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 { + 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, + shortcuts: &[Shortcut], + modes: &AHashMap, + ) { + 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, 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, + base: &ConvertedShortcuts, + modes: &AHashMap, + ) { + 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, + out: &'a mut AHashMap>, + pending: &mut AHashSet, + base: &ConvertedShortcuts, + modes: &AHashMap, + 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, + shortcuts: impl IntoIterator, + ) -> Rc { + let mut dst = ConvertedShortcuts::new(); + self.convert_shortcuts_(shortcuts, &mut dst); + Rc::new(dst) + } + + fn convert_shortcuts_<'a>( + self: &Rc, + shortcuts: impl IntoIterator, + 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, shortcut: Shortcut) -> Option { + 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) { + 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, + new: &Rc, + ) -> Rc> { + 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() + } + } + } +} diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index d4ab01c0..215ce1c9 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -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": { diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index 4e0d672d..e16b71a0 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -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. + ### `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). + ### `Connector` @@ -3025,6 +3090,59 @@ The table has the following fields: The value of this field should be a boolean. + +### `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). + + ### `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. + diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index 0a551532..d8bba591 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -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.