i3status_rs/blocks/keyboard_layout.rs
1//! Keyboard layout indicator
2//!
3//! Six drivers are available:
4//! - `xkbevent` which can read asynchronous updates from the x11 events
5//! - `setxkbmap` (alias for `xkbevent`) *DEPRECATED*
6//! - `xkbswitch` (alias for `xkbevent`) *DEPRECATED*
7//! - `localebus` which can read asynchronous updates from the systemd `org.freedesktop.locale1` D-Bus path
8//! - `kbddbus` which uses [kbdd](https://github.com/qnikst/kbdd) to monitor per-window layout changes via DBus
9//! - `sway` which can read asynchronous updates from the sway IPC
10//!
11//! `setxkbmap` and `xkbswitch` are deprecated and will be removed in v0.35.0.
12//!
13//! Which of these methods is appropriate will depend on your system setup.
14//!
15//! # Configuration
16//!
17//! Key | Values | Default
18//! ----|--------|--------
19//! `driver` | One of `"xkbevent"`, `"setxkbmap"`, `"xkbswitch"`, `"localebus"`, `"kbddbus"` or `"sway"`, depending on your system. | `"xkbevent"`
20//! `interval` *DEPRECATED* | Update interval, in seconds. Only used by the `"setxkbmap"` driver. | `60`
21//! `format` | A string to customise the output of this block. See below for available placeholders. | `" $layout "`
22//! `sway_kb_identifier` | Identifier of the device you want to monitor, as found in the output of `swaymsg -t get_inputs`. | Defaults to first input found
23//! `mappings` | Map `layout (variant)` to custom short name. | `None`
24//!
25//! `interval` is deprecated and will be removed in v0.35.0.
26//!
27//! Key | Value | Type
28//! ---------|-------|-----
29//! `layout` | Keyboard layout name | String
30//! `variant`| Keyboard variant name or `N/A` if not applicable | String
31//!
32//! # Examples
33//!
34//! Listen to D-Bus for changes:
35//!
36//! ```toml
37//! [[block]]
38//! block = "keyboard_layout"
39//! driver = "localebus"
40//! ```
41//!
42//! Listen to kbdd for changes, the text is in the following format:
43//! "English (US)" - {$layout ($variant)}
44//! use block.mappings to override with shorter names as shown below.
45//! Also use format = " $layout ($variant) " to see the full text to map,
46//! or you can use:
47//! dbus-monitor interface=ru.gentoo.kbdd
48//! to see the exact variant spelling
49//!
50//! ```toml
51//! [[block]]
52//! block = "keyboard_layout"
53//! driver = "kbddbus"
54//! [block.mappings]
55//! "English (US)" = "us"
56//! "Bulgarian (new phonetic)" = "bg"
57//! ```
58//!
59//! Listen to sway for changes:
60//!
61//! ```toml
62//! [[block]]
63//! block = "keyboard_layout"
64//! driver = "sway"
65//! sway_kb_identifier = "1133:49706:Gaming_Keyboard_G110"
66//! ```
67//!
68//! Listen to sway for changes and override mappings:
69//! ```toml
70//! [[block]]
71//! block = "keyboard_layout"
72//! driver = "sway"
73//! format = " $layout "
74//! [block.mappings]
75//! "English (Workman)" = "EN"
76//! "Russian (N/A)" = "RU"
77//! ```
78//!
79//! Listen to xkb events for changes:
80//!
81//! ```toml
82//! [[block]]
83//! block = "keyboard_layout"
84//! driver = "xkbevent"
85//! ```
86
87mod locale_bus;
88use locale_bus::LocaleBus;
89
90mod kbdd_bus;
91use kbdd_bus::KbddBus;
92
93mod sway;
94use sway::Sway;
95
96mod xkb_event;
97use xkb_event::XkbEvent;
98
99use super::prelude::*;
100
101#[derive(Deserialize, Debug, SmartDefault)]
102#[serde(deny_unknown_fields, default)]
103pub struct Config {
104 pub format: FormatConfig,
105 pub driver: KeyboardLayoutDriver,
106 #[default(60.into())]
107 pub interval: Seconds,
108 pub sway_kb_identifier: Option<String>,
109 pub mappings: Option<HashMap<String, String>>,
110}
111
112#[derive(Deserialize, Debug, SmartDefault, Clone, Copy)]
113#[serde(rename_all = "lowercase")]
114pub enum KeyboardLayoutDriver {
115 #[default]
116 XkbEvent,
117 SetXkbMap,
118 XkbSwitch,
119 LocaleBus,
120 KbddBus,
121 Sway,
122}
123
124pub(crate) fn prepare(config: &Config) -> Result<Arc<BlockPlan>> {
125 BlockPlan::new(vec![OutputPlan::new(
126 "main",
127 config.format.with_default(" $layout ")?,
128 )])
129}
130
131pub(crate) async fn run(config: &Config, api: &CommonApi, plan: &Arc<BlockPlan>) -> Result<()> {
132 let output_main = plan.output("main")?;
133
134 let mut backend: Box<dyn Backend> = match config.driver {
135 KeyboardLayoutDriver::LocaleBus => Box::new(LocaleBus::new().await?),
136 KeyboardLayoutDriver::KbddBus => Box::new(KbddBus::new().await?),
137 KeyboardLayoutDriver::Sway => Box::new(Sway::new(config.sway_kb_identifier.clone()).await?),
138 KeyboardLayoutDriver::XkbEvent
139 | KeyboardLayoutDriver::SetXkbMap
140 | KeyboardLayoutDriver::XkbSwitch => Box::new(XkbEvent::new().await?),
141 };
142
143 loop {
144 let Info {
145 mut layout,
146 variant,
147 } = backend.get_info().await?;
148
149 let variant = variant.unwrap_or_else(|| "N/A".into());
150 if let Some(mappings) = &config.mappings
151 && let Some(mapped) = mappings.get(&format!("{layout} ({variant})"))
152 {
153 layout.clone_from(mapped);
154 }
155
156 let mut widget = output_main.new_widget();
157 widget.set_values(map! {
158 "layout" => Value::text(layout),
159 "variant" => Value::text(variant),
160 });
161 api.set_widget(widget)?;
162
163 backend.wait_for_change().await?;
164 }
165}
166
167#[async_trait]
168trait Backend {
169 async fn get_info(&mut self) -> Result<Info>;
170 async fn wait_for_change(&mut self) -> Result<()>;
171}
172
173#[derive(Clone)]
174struct Info {
175 layout: String,
176 variant: Option<String>,
177}
178
179impl Info {
180 /// Parse "layout (variant)" string
181 fn from_layout_variant_str(s: &str) -> Self {
182 if let Some((layout, rest)) = s.split_once('(') {
183 Self {
184 layout: layout.trim_end().into(),
185 variant: Some(rest.trim_end_matches(')').into()),
186 }
187 } else {
188 Self {
189 layout: s.into(),
190 variant: None,
191 }
192 }
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn plan_declares_single_output_without_icons() {
202 let plan = prepare(&Config::default()).unwrap();
203 let ids: Vec<_> = plan.outputs().map(|o| o.id()).collect();
204 assert_eq!(ids, ["main"]);
205 let main = plan.output("main").unwrap();
206 assert!(main.format().contains_key("layout"));
207 assert_eq!(main.output().icon_placeholders().count(), 0);
208 }
209
210 #[test]
211 fn plan_uses_configured_format() {
212 let config = Config {
213 format: " $layout ($variant) ".parse().unwrap(),
214 ..Config::default()
215 };
216 let plan = prepare(&config).unwrap();
217 assert!(
218 plan.output("main")
219 .unwrap()
220 .format()
221 .contains_key("variant")
222 );
223 }
224}