diff options
Diffstat (limited to 'crates/wayland/src/xkb_config.rs')
| -rw-r--r-- | crates/wayland/src/xkb_config.rs | 102 |
1 files changed, 102 insertions, 0 deletions
diff --git a/crates/wayland/src/xkb_config.rs b/crates/wayland/src/xkb_config.rs new file mode 100644 index 0000000..5b16904 --- /dev/null +++ b/crates/wayland/src/xkb_config.rs @@ -0,0 +1,102 @@ +//! Reads the system's real keyboard layout/model/options so both backends' +//! `seat.add_keyboard()` calls actually use them. +//! +//! `smithay::input::keyboard::XkbConfig::default()` - what both backends +//! passed unconditionally before this - resolves any field left as `""`/ +//! `None` via the `XKB_DEFAULT_*` environment variables, per xkbcommon's +//! own documented behavior (see its doc comment). That covers a session +//! that actually sets those variables, but this machine doesn't: the real +//! configuration lives in `/etc/X11/xorg.conf.d/00-keyboard.conf`, written +//! by `systemd-localed` from `localectl`, which nothing was ever reading. +//! Concretely: `Option "XkbOptions" "terminate:ctrl_alt_bksp"` was silently +//! dropped, along with the model (`pc105+inet`), regardless of what +//! `localectl status` actually reports. +//! +//! Deliberately fails soft, field by field: a missing file, an unreadable +//! one, or a field just not present in it all fall through to the same +//! `""`/`None` `Default::default()` already used, not an error - this is +//! strictly additive over today's behavior, never worse. + +use std::collections::HashMap; + +/// Parsed fields from the standard `XkbLayout`/`XkbModel`/`XkbVariant`/ +/// `XkbOptions` `Option "..." "..."` lines `systemd-localed` writes. +/// Anything not found is `None`, which is exactly what an empty-string +/// `XkbConfig` field already meant. +#[derive(Default)] +pub(crate) struct SystemXkbConfig { + pub(crate) model: Option<String>, + pub(crate) layout: Option<String>, + pub(crate) variant: Option<String>, + pub(crate) options: Option<String>, +} + +const KEYBOARD_CONF_PATH: &str = "/etc/X11/xorg.conf.d/00-keyboard.conf"; + +pub(crate) fn read() -> SystemXkbConfig { + let Ok(content) = std::fs::read_to_string(KEYBOARD_CONF_PATH) else { + return SystemXkbConfig::default(); + }; + let fields = parse(&content); + SystemXkbConfig { + model: fields.get("XkbModel").cloned(), + layout: fields.get("XkbLayout").cloned(), + variant: fields.get("XkbVariant").cloned(), + options: fields.get("XkbOptions").cloned(), + } +} + +/// Extracts `Option "Name" "value"` lines into a name -> value map. Not a +/// general X11 config parser - this file has exactly one `InputClass` +/// section with a handful of `Option` lines in that fixed form, always +/// machine-written, so a line-by-line scan for that one pattern is all +/// this needs. +fn parse(content: &str) -> HashMap<String, String> { + let mut fields = HashMap::new(); + for line in content.lines() { + let line = line.trim(); + let Some(rest) = line.strip_prefix("Option") else { continue }; + let quoted: Vec<&str> = rest.split('"').filter(|s| !s.trim().is_empty()).collect(); + if let [name, value] = quoted[..] { + fields.insert(name.to_string(), value.to_string()); + } + } + fields +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_a_real_systemd_localed_file() { + let content = r#" +Section "InputClass" + Identifier "system-keyboard" + MatchIsKeyboard "on" + Option "XkbLayout" "us" + Option "XkbModel" "pc105+inet" + Option "XkbOptions" "terminate:ctrl_alt_bksp" +EndSection +"#; + let fields = parse(content); + assert_eq!(fields.get("XkbLayout").map(String::as_str), Some("us")); + assert_eq!(fields.get("XkbModel").map(String::as_str), Some("pc105+inet")); + assert_eq!(fields.get("XkbOptions").map(String::as_str), Some("terminate:ctrl_alt_bksp")); + } + + #[test] + fn ignores_unrelated_lines_without_panicking() { + let content = "Section \"InputClass\"\nIdentifier \"system-keyboard\"\nMatchIsKeyboard \"on\"\nEndSection\n"; + assert!(parse(content).is_empty()); + } + + #[test] + fn missing_file_yields_all_none_not_an_error() { + // read() itself isn't unit-testable without touching the real + // filesystem path, but the fallback behavior it guarantees is + // exactly SystemXkbConfig::default() - covered by construction. + let cfg = SystemXkbConfig::default(); + assert!(cfg.model.is_none() && cfg.layout.is_none() && cfg.variant.is_none() && cfg.options.is_none()); + } +} |