1
0
Fork 0
forked from wry/wry

toml-config: allow specifying keymaps via RMLVO

This commit is contained in:
Julian Orth 2026-03-14 21:21:42 +01:00
parent 6e400655c6
commit d911de6007
4 changed files with 272 additions and 31 deletions

View file

@ -3,19 +3,20 @@ use {
config::{ config::{
ConfigKeymap, ConfigKeymap,
context::Context, context::Context,
extractor::{Extractor, ExtractorError, opt, str}, extractor::{Extractor, ExtractorError, opt, str, val},
parser::{DataType, ParseResult, Parser, UnexpectedDataType}, parser::{DataType, ParseResult, Parser, UnexpectedDataType},
}, },
toml::{ toml::{
toml_span::{Span, Spanned, SpannedExt}, toml_span::{DespanExt, Span, Spanned, SpannedExt},
toml_value::Value, toml_value::Value,
}, },
}, },
indexmap::IndexMap, indexmap::IndexMap,
jay_config::{ jay_config::{
config_dir, config_dir,
keyboard::{Keymap, parse_keymap}, keyboard::{Keymap, keymap_from_names, parse_keymap},
}, },
kbvm::xkb::rmlvo::Group,
std::{io, path::PathBuf}, std::{io, path::PathBuf},
thiserror::Error, thiserror::Error,
}; };
@ -28,9 +29,11 @@ pub enum KeymapParserError {
Extractor(#[from] ExtractorError), Extractor(#[from] ExtractorError),
#[error("The keymap is invalid")] #[error("The keymap is invalid")]
Invalid, Invalid,
#[error("Keymap table must contain at least one of `name`, `map`")] #[error("Keymap table must contain at least one of `name`, `map`, `path`, `rmlvo`")]
MissingField, MissingField,
#[error("Keymap must have both `name` and `map` fields in this context")] #[error(
"Keymap must have both `name` and one of `map`, `path`, `rmlvo` fields in this context"
)]
DefinitionRequired, DefinitionRequired,
#[error("Could not read {0}")] #[error("Could not read {0}")]
ReadFile(String, #[source] io::Error), ReadFile(String, #[source] io::Error),
@ -56,14 +59,27 @@ impl Parser for KeymapParser<'_> {
table: &IndexMap<Spanned<String>, Spanned<Value>>, table: &IndexMap<Spanned<String>, Spanned<Value>>,
) -> ParseResult<Self> { ) -> ParseResult<Self> {
let mut ext = Extractor::new(self.cx, span, table); let mut ext = Extractor::new(self.cx, span, table);
let (mut name_val, mut map_val, mut path) = let (mut name_val, mut map_val, mut path, mut rmlvo) = ext.extract((
ext.extract((opt(str("name")), opt(str("map")), opt(str("path"))))?; opt(str("name")),
if map_val.is_some() && path.is_some() { opt(str("map")),
opt(str("path")),
opt(val("rmlvo")),
))?;
if map_val.is_some() as u32 + path.is_some() as u32 + rmlvo.is_some() as u32 > 1 {
log::warn!( log::warn!(
"Both `name` and `path` are specified. Ignoring `path`: {}", "At most one of `map`, `path`, and `rmlvo` should be specified: {}",
self.cx.error3(span) self.cx.error3(span),
); );
path = None; let ignore_path = map_val.is_some();
let ignore_rmlvo = map_val.is_some() || path.is_some();
if ignore_path && path.is_some() {
log::warn!("Ignoring `path`");
path = None;
}
if ignore_rmlvo && rmlvo.is_some() {
log::warn!("Ignoring `rmlvo`");
rmlvo = None;
}
} }
let file_content; let file_content;
if let Some(path) = path { if let Some(path) = path {
@ -78,10 +94,17 @@ impl Parser for KeymapParser<'_> {
}; };
map_val = Some(file_content.as_str().spanned(path.span)); map_val = Some(file_content.as_str().spanned(path.span));
} }
if self.definition && (name_val.is_none() || map_val.is_none()) { let mut map = None;
if let Some(val) = &map_val {
map = Some(parse(val.span, val.value)?);
}
if let Some(val) = rmlvo {
map = Some(val.parse(&mut RmlvoParser(self.cx))?);
}
if self.definition && (name_val.is_none() || map.is_none()) {
return Err(KeymapParserError::DefinitionRequired.spanned(span)); return Err(KeymapParserError::DefinitionRequired.spanned(span));
} }
if !self.definition && map_val.is_some() { if !self.definition && map.is_some() {
if let Some(val) = name_val { if let Some(val) = name_val {
log::warn!( log::warn!(
"Cannot use both `name` and `map` in this position. Ignoring `name`: {}", "Cannot use both `name` and `map` in this position. Ignoring `name`: {}",
@ -101,19 +124,70 @@ impl Parser for KeymapParser<'_> {
self.cx.used.borrow_mut().keymaps.push(name.into()); self.cx.used.borrow_mut().keymaps.push(name.into());
} }
} }
let res = match (name_val, map_val) { let res = match (name_val, map) {
(Some(name_val), Some(map_val)) => ConfigKeymap::Defined { (Some(name_val), Some(map)) => ConfigKeymap::Defined {
name: name_val.value.to_string(), name: name_val.value.to_string(),
map: parse(map_val.span, map_val.value)?, map,
}, },
(Some(name_val), None) => ConfigKeymap::Named(name_val.value.to_string()), (Some(name_val), None) => ConfigKeymap::Named(name_val.value.to_string()),
(None, Some(map_val)) => ConfigKeymap::Literal(parse(map_val.span, map_val.value)?), (None, Some(map)) => ConfigKeymap::Literal(map),
(None, None) => return Err(KeymapParserError::MissingField.spanned(span)), (None, None) => return Err(KeymapParserError::MissingField.spanned(span)),
}; };
Ok(res) Ok(res)
} }
} }
struct RmlvoParser<'a>(&'a Context<'a>);
impl Parser for RmlvoParser<'_> {
type Value = Keymap;
type Error = KeymapParserError;
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 (rules, model, layout, variants, options) = ext.extract((
opt(str("rules")),
opt(str("model")),
opt(str("layout")),
opt(str("variants")),
opt(str("options")),
))?;
let mut groups = None::<Vec<_>>;
if layout.is_some() || variants.is_some() {
groups = Some(
Group::from_layouts_and_variants(
layout.despan().unwrap_or_default(),
variants.despan().unwrap_or_default(),
)
.map(|g| jay_config::keyboard::Group {
layout: g.layout,
variant: g.variant,
})
.collect(),
);
}
let mut options_vec = None::<Vec<_>>;
if let Some(options) = options {
options_vec = Some(options.value.split(",").collect());
}
let map = keymap_from_names(
rules.despan(),
model.despan(),
groups.as_deref(),
options_vec.as_deref(),
);
match map.is_valid() {
true => Ok(map),
false => Err(KeymapParserError::Invalid.spanned(span)),
}
}
}
fn parse(span: Span, string: &str) -> Result<Keymap, Spanned<KeymapParserError>> { fn parse(span: Span, string: &str) -> Result<Keymap, Spanned<KeymapParserError>> {
let map = parse_keymap(string); let map = parse_keymap(string);
match map.is_valid() { match map.is_valid() {

View file

@ -1643,7 +1643,7 @@
"anyOf": [ "anyOf": [
{ {
"type": "string", "type": "string",
"description": "Defines a keymap by its XKB representation.\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" "description": "Defines a keymap by its XKB representation.\n\n- Example 1:\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\n- Example 2:\n\n ```toml\n keymap.rmlvo = {\n layout = \"us,de\",\n variants = \"dvorak\",\n options = \"grp:ctrl_space_toggle\",\n }\n ```\n"
}, },
{ {
"description": "Defines or references a keymap.\n\n- Example:\n\n ```toml\n keymap.name = \"my-keymap\"\n\n [[keymaps]]\n name = \"my-keymap\"\n path = \"./my-keymap.xkb\"\n ```\n", "description": "Defines or references a keymap.\n\n- Example:\n\n ```toml\n keymap.name = \"my-keymap\"\n\n [[keymaps]]\n name = \"my-keymap\"\n path = \"./my-keymap.xkb\"\n ```\n",
@ -1655,11 +1655,15 @@
}, },
"map": { "map": {
"type": "string", "type": "string",
"description": "Defines a keymap by its XKB representation.\n\nFor each keymap defined in the top-level `keymaps` array, exactly one of `map`\nand `path` has to be defined.\n" "description": "Defines a keymap by its XKB representation.\n\nFor each keymap defined in the top-level `keymaps` array, exactly one of\n`map`, `path`, and `rmlvo` has to be defined.\n"
}, },
"path": { "path": {
"type": "string", "type": "string",
"description": "Loads a keymap's XKB representation from a file.\n\nIf the path is relative, it will be interpreted relative to the Jay config\ndirectory.\n\nFor each keymap defined in the top-level `keymaps` array, exactly one of `map`\nand `path` has to be defined.\n" "description": "Loads a keymap's XKB representation from a file.\n\nIf the path is relative, it will be interpreted relative to the Jay config\ndirectory.\n\nFor each keymap defined in the top-level `keymaps` array, exactly one of\n`map`, `path`, and `rmlvo` has to be defined.\n"
},
"rmlvo": {
"description": "Creates a keymap from RMLVO names.\n\nFor each keymap defined in the top-level `keymaps` array, exactly one of\n`map`, `path`, and `rmlvo` has to be defined.\n",
"$ref": "#/$defs/Rmlvo"
} }
}, },
"required": [] "required": []
@ -1864,6 +1868,33 @@
"delay" "delay"
] ]
}, },
"Rmlvo": {
"description": "An RMLVO keymap definition.\n\nIf a parameter is not given, a value from the environment or a default is used:\n\n| name | default |\n| ---------------------- | ---------------------- |\n| `XKB_DEFAULT_RULES` | `evdev` |\n| `XKB_DEFAULT_MODEL` | `pc105` |\n| `XKB_DEFAULT_LAYOUT` | `us` |\n| `XKB_DEFAULT_VARIANTS` | |\n| `XKB_DEFAULT_OPTIONS` | |\n\n- Example:\n\n ```toml\n keymap.rmlvo = { layout = \"us,de\", variants = \"dvorak\" }\n ```\n",
"type": "object",
"properties": {
"rules": {
"type": "string",
"description": "The name of the rules file."
},
"model": {
"type": "string",
"description": "The name of the device model."
},
"layout": {
"type": "string",
"description": "A comma-separated list of layouts."
},
"variants": {
"type": "string",
"description": "A comma-separated list of variants."
},
"options": {
"type": "string",
"description": "A comma-separated list of options."
}
},
"required": []
},
"SimpleActionName": { "SimpleActionName": {
"type": "string", "type": "string",
"description": "The name of a `simple` Action.\n\nWhen used inside a window rule, the following actions apply to the matched window\ninstead fo the focused window:\n\n- `move-left`\n- `move-down`\n- `move-up`\n- `move-right`\n- `split-horizontal`\n- `split-vertical`\n- `toggle-split`\n- `tile-horizontal`\n- `tile-vertical`\n- `toggle-split`\n- `show-single`\n- `show-all`\n- `toggle-fullscreen`\n- `enter-fullscreen`\n- `exit-fullscreen`\n- `close`\n- `toggle-floating`\n- `float`\n- `tile`\n- `toggle-float-pinned`\n- `pin-float`\n- `unpin-float`\n\n\n- Example:\n\n ```toml\n [shortcuts]\n alt-q = \"quit\"\n ```\n", "description": "The name of a `simple` Action.\n\nWhen used inside a window rule, the following actions apply to the matched window\ninstead fo the focused window:\n\n- `move-left`\n- `move-down`\n- `move-up`\n- `move-right`\n- `split-horizontal`\n- `split-vertical`\n- `toggle-split`\n- `tile-horizontal`\n- `tile-vertical`\n- `toggle-split`\n- `show-single`\n- `show-all`\n- `toggle-fullscreen`\n- `enter-fullscreen`\n- `exit-fullscreen`\n- `close`\n- `toggle-floating`\n- `float`\n- `tile`\n- `toggle-float-pinned`\n- `pin-float`\n- `unpin-float`\n\n\n- Example:\n\n ```toml\n [shortcuts]\n alt-q = \"quit\"\n ```\n",

View file

@ -3557,7 +3557,7 @@ Values of this type should have one of the following forms:
Defines a keymap by its XKB representation. Defines a keymap by its XKB representation.
- Example: - Example 1:
```toml ```toml
keymap = """ keymap = """
@ -3570,6 +3570,16 @@ Defines a keymap by its XKB representation.
""" """
``` ```
- Example 2:
```toml
keymap.rmlvo = {
layout = "us,de",
variants = "dvorak",
options = "grp:ctrl_space_toggle",
}
```
#### A table #### A table
Defines or references a keymap. Defines or references a keymap.
@ -3602,8 +3612,8 @@ The table has the following fields:
Defines a keymap by its XKB representation. Defines a keymap by its XKB representation.
For each keymap defined in the top-level `keymaps` array, exactly one of `map` For each keymap defined in the top-level `keymaps` array, exactly one of
and `path` has to be defined. `map`, `path`, and `rmlvo` has to be defined.
The value of this field should be a string. The value of this field should be a string.
@ -3614,11 +3624,20 @@ The table has the following fields:
If the path is relative, it will be interpreted relative to the Jay config If the path is relative, it will be interpreted relative to the Jay config
directory. directory.
For each keymap defined in the top-level `keymaps` array, exactly one of `map` For each keymap defined in the top-level `keymaps` array, exactly one of
and `path` has to be defined. `map`, `path`, and `rmlvo` has to be defined.
The value of this field should be a string. The value of this field should be a string.
- `rmlvo` (optional):
Creates a keymap from RMLVO names.
For each keymap defined in the top-level `keymaps` array, exactly one of
`map`, `path`, and `rmlvo` has to be defined.
The value of this field should be a [Rmlvo](#types-Rmlvo).
<a name="types-Libei"></a> <a name="types-Libei"></a>
### `Libei` ### `Libei`
@ -4099,6 +4118,62 @@ The table has the following fields:
The numbers should be integers. The numbers should be integers.
<a name="types-Rmlvo"></a>
### `Rmlvo`
An RMLVO keymap definition.
If a parameter is not given, a value from the environment or a default is used:
| name | default |
| ---------------------- | ---------------------- |
| `XKB_DEFAULT_RULES` | `evdev` |
| `XKB_DEFAULT_MODEL` | `pc105` |
| `XKB_DEFAULT_LAYOUT` | `us` |
| `XKB_DEFAULT_VARIANTS` | |
| `XKB_DEFAULT_OPTIONS` | |
- Example:
```toml
keymap.rmlvo = { layout = "us,de", variants = "dvorak" }
```
Values of this type should be tables.
The table has the following fields:
- `rules` (optional):
The name of the rules file.
The value of this field should be a string.
- `model` (optional):
The name of the device model.
The value of this field should be a string.
- `layout` (optional):
A comma-separated list of layouts.
The value of this field should be a string.
- `variants` (optional):
A comma-separated list of variants.
The value of this field should be a string.
- `options` (optional):
A comma-separated list of options.
The value of this field should be a string.
<a name="types-SimpleActionName"></a> <a name="types-SimpleActionName"></a>
### `SimpleActionName` ### `SimpleActionName`

View file

@ -7,7 +7,7 @@ Keymap:
description: | description: |
Defines a keymap by its XKB representation. Defines a keymap by its XKB representation.
- Example: - Example 1:
```toml ```toml
keymap = """ keymap = """
@ -19,6 +19,16 @@ Keymap:
}; };
""" """
``` ```
- Example 2:
```toml
keymap.rmlvo = {
layout = "us,de",
variants = "dvorak",
options = "grp:ctrl_space_toggle",
}
```
- kind: table - kind: table
description: | description: |
Defines or references a keymap. Defines or references a keymap.
@ -50,8 +60,8 @@ Keymap:
description: | description: |
Defines a keymap by its XKB representation. Defines a keymap by its XKB representation.
For each keymap defined in the top-level `keymaps` array, exactly one of `map` For each keymap defined in the top-level `keymaps` array, exactly one of
and `path` has to be defined. `map`, `path`, and `rmlvo` has to be defined.
path: path:
kind: string kind: string
required: false required: false
@ -61,8 +71,59 @@ Keymap:
If the path is relative, it will be interpreted relative to the Jay config If the path is relative, it will be interpreted relative to the Jay config
directory. directory.
For each keymap defined in the top-level `keymaps` array, exactly one of `map` For each keymap defined in the top-level `keymaps` array, exactly one of
and `path` has to be defined. `map`, `path`, and `rmlvo` has to be defined.
rmlvo:
ref: Rmlvo
required: false
description: |
Creates a keymap from RMLVO names.
For each keymap defined in the top-level `keymaps` array, exactly one of
`map`, `path`, and `rmlvo` has to be defined.
Rmlvo:
description: |
An RMLVO keymap definition.
If a parameter is not given, a value from the environment or a default is used:
| name | default |
| ---------------------- | ---------------------- |
| `XKB_DEFAULT_RULES` | `evdev` |
| `XKB_DEFAULT_MODEL` | `pc105` |
| `XKB_DEFAULT_LAYOUT` | `us` |
| `XKB_DEFAULT_VARIANTS` | |
| `XKB_DEFAULT_OPTIONS` | |
- Example:
```toml
keymap.rmlvo = { layout = "us,de", variants = "dvorak" }
```
kind: table
fields:
rules:
kind: string
required: false
description: The name of the rules file.
model:
kind: string
required: false
description: The name of the device model.
layout:
kind: string
required: false
description: A comma-separated list of layouts.
variants:
kind: string
required: false
description: A comma-separated list of variants.
options:
kind: string
required: false
description: A comma-separated list of options.
Action: Action:
@ -2998,7 +3059,7 @@ Config:
required: false required: false
description: | description: |
Sets the fallback output mode. Sets the fallback output mode.
The default is `cursor`. The default is `cursor`.
- Example: - Example: