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}