1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
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());
}
}
|