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 async fn run(config: &Config, api: &CommonApi) -> Result<()> {
125 let format = config.format.with_default(" $layout ")?;
126
127 let mut backend: Box<dyn Backend> = match config.driver {
128 KeyboardLayoutDriver::LocaleBus => Box::new(LocaleBus::new().await?),
129 KeyboardLayoutDriver::KbddBus => Box::new(KbddBus::new().await?),
130 KeyboardLayoutDriver::Sway => Box::new(Sway::new(config.sway_kb_identifier.clone()).await?),
131 KeyboardLayoutDriver::XkbEvent
132 | KeyboardLayoutDriver::SetXkbMap
133 | KeyboardLayoutDriver::XkbSwitch => Box::new(XkbEvent::new().await?),
134 };
135
136 loop {
137 let Info {
138 mut layout,
139 variant,
140 } = backend.get_info().await?;
141
142 let variant = variant.unwrap_or_else(|| "N/A".into());
143 if let Some(mappings) = &config.mappings {
144 if let Some(mapped) = mappings.get(&format!("{layout} ({variant})")) {
145 layout.clone_from(mapped);
146 }
147 }
148
149 let mut widget = Widget::new().with_format(format.clone());
150 widget.set_values(map! {
151 "layout" => Value::text(layout),
152 "variant" => Value::text(variant),
153 });
154 api.set_widget(widget)?;
155
156 backend.wait_for_change().await?;
157 }
158}
159
160#[async_trait]
161trait Backend {
162 async fn get_info(&mut self) -> Result<Info>;
163 async fn wait_for_change(&mut self) -> Result<()>;
164}
165
166#[derive(Clone)]
167struct Info {
168 layout: String,
169 variant: Option<String>,
170}
171
172impl Info {
173 /// Parse "layout (variant)" string
174 fn from_layout_variant_str(s: &str) -> Self {
175 if let Some((layout, rest)) = s.split_once('(') {
176 Self {
177 layout: layout.trim_end().into(),
178 variant: Some(rest.trim_end_matches(')').into()),
179 }
180 } else {
181 Self {
182 layout: s.into(),
183 variant: None,
184 }
185 }
186 }
187}