2025-02-04 00:19:30 +03:00
|
|
|
use std::collections::HashMap;
|
|
|
|
|
2024-11-17 23:46:02 +00:00
|
|
|
use color_eyre::Result;
|
2025-02-21 16:35:54 +00:00
|
|
|
use color_eyre::eyre::Report;
|
2025-03-22 19:19:47 +00:00
|
|
|
use gtk::prelude::*;
|
2024-11-17 23:46:02 +00:00
|
|
|
use serde::Deserialize;
|
|
|
|
use tokio::sync::mpsc;
|
2025-02-04 00:19:30 +03:00
|
|
|
use tracing::{debug, trace};
|
2024-11-17 23:46:02 +00:00
|
|
|
|
2025-02-04 00:19:30 +03:00
|
|
|
use super::{Module, ModuleInfo, ModuleParts, WidgetContext};
|
|
|
|
use crate::clients::compositor::{self, KeyboardLayoutUpdate};
|
2024-11-17 23:46:02 +00:00
|
|
|
use crate::clients::libinput::{Event, Key, KeyEvent};
|
2025-03-22 19:19:47 +00:00
|
|
|
use crate::config::{CommonConfig, LayoutConfig};
|
2024-11-17 23:46:02 +00:00
|
|
|
use crate::gtk_helpers::IronbarGtkExt;
|
2025-03-22 19:19:47 +00:00
|
|
|
use crate::image::{IconButton, IconLabel};
|
2025-02-04 00:19:30 +03:00
|
|
|
use crate::{glib_recv, module_impl, module_update, send_async, spawn, try_send};
|
2024-11-17 23:46:02 +00:00
|
|
|
|
|
|
|
#[derive(Debug, Deserialize, Clone)]
|
|
|
|
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
2025-02-04 00:19:30 +03:00
|
|
|
pub struct KeyboardModule {
|
2024-11-17 23:46:02 +00:00
|
|
|
/// Whether to show capslock indicator.
|
|
|
|
///
|
|
|
|
/// **Default**: `true`
|
|
|
|
#[serde(default = "crate::config::default_true")]
|
|
|
|
show_caps: bool,
|
|
|
|
|
|
|
|
/// Whether to show num lock indicator.
|
|
|
|
///
|
|
|
|
/// **Default**: `true`
|
|
|
|
#[serde(default = "crate::config::default_true")]
|
|
|
|
show_num: bool,
|
|
|
|
|
|
|
|
/// Whether to show scroll lock indicator.
|
|
|
|
///
|
|
|
|
/// **Default**: `true`
|
|
|
|
#[serde(default = "crate::config::default_true")]
|
|
|
|
show_scroll: bool,
|
|
|
|
|
2025-02-04 00:19:30 +03:00
|
|
|
/// Whether to show the current keyboard layout.
|
|
|
|
///
|
|
|
|
/// **Default**: `true`
|
|
|
|
#[serde(default = "crate::config::default_true")]
|
|
|
|
show_layout: bool,
|
|
|
|
|
2024-11-17 23:46:02 +00:00
|
|
|
/// Size to render the icons at, in pixels (image icons only).
|
|
|
|
///
|
|
|
|
/// **Default** `32`
|
|
|
|
#[serde(default = "default_icon_size")]
|
|
|
|
icon_size: i32,
|
|
|
|
|
|
|
|
/// Player state icons.
|
|
|
|
///
|
|
|
|
/// See [icons](#icons).
|
|
|
|
#[serde(default)]
|
|
|
|
icons: Icons,
|
|
|
|
|
|
|
|
/// The Wayland seat to attach to.
|
|
|
|
/// You almost certainly do not need to change this.
|
|
|
|
///
|
|
|
|
/// **Default**: `seat0`
|
|
|
|
#[serde(default = "default_seat")]
|
|
|
|
seat: String,
|
|
|
|
|
2025-03-22 19:19:47 +00:00
|
|
|
// -- common --
|
|
|
|
/// See [layout options](module-level-options#layout)
|
|
|
|
#[serde(default, flatten)]
|
|
|
|
layout: LayoutConfig,
|
|
|
|
|
2024-11-17 23:46:02 +00:00
|
|
|
/// See [common options](module-level-options#common-options).
|
|
|
|
#[serde(flatten)]
|
|
|
|
pub common: Option<CommonConfig>,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Deserialize, Clone)]
|
|
|
|
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
|
|
|
struct Icons {
|
|
|
|
/// Icon to show when capslock is enabled.
|
|
|
|
///
|
|
|
|
/// **Default**: ``
|
|
|
|
#[serde(default = "default_icon_caps")]
|
|
|
|
caps_on: String,
|
|
|
|
|
|
|
|
/// Icon to show when capslock is disabled.
|
|
|
|
///
|
|
|
|
/// **Default**: `""`
|
|
|
|
#[serde(default)]
|
|
|
|
caps_off: String,
|
|
|
|
|
|
|
|
/// Icon to show when num lock is enabled.
|
|
|
|
///
|
|
|
|
/// **Default**: ``
|
|
|
|
#[serde(default = "default_icon_num")]
|
|
|
|
num_on: String,
|
|
|
|
|
|
|
|
/// Icon to show when num lock is disabled.
|
|
|
|
///
|
|
|
|
/// **Default**: `""`
|
|
|
|
#[serde(default)]
|
|
|
|
num_off: String,
|
|
|
|
|
|
|
|
/// Icon to show when scroll lock is enabled.
|
|
|
|
///
|
|
|
|
/// **Default**: ``
|
|
|
|
#[serde(default = "default_icon_scroll")]
|
|
|
|
scroll_on: String,
|
|
|
|
|
|
|
|
/// Icon to show when scroll lock is disabled.
|
|
|
|
///
|
|
|
|
/// **Default**: `""`
|
|
|
|
#[serde(default)]
|
|
|
|
scroll_off: String,
|
2025-02-04 00:19:30 +03:00
|
|
|
|
|
|
|
/// Map of icons or labels to show for a particular keyboard layout.
|
|
|
|
///
|
|
|
|
/// If a layout is not present in the map,
|
|
|
|
/// it will fall back to using its actual name.
|
|
|
|
///
|
|
|
|
/// **Default**: `{}`
|
|
|
|
///
|
|
|
|
/// # Example
|
|
|
|
///
|
|
|
|
/// ```corn
|
|
|
|
/// {
|
|
|
|
/// type = "keyboard"
|
|
|
|
/// show_layout = true
|
|
|
|
/// icons.layout_map.'English (US)' = "EN"
|
|
|
|
/// icons.layout_map.Ukrainian = "UA"
|
|
|
|
/// }
|
|
|
|
/// ```
|
|
|
|
#[serde(default)]
|
|
|
|
layout_map: HashMap<String, String>,
|
2024-11-17 23:46:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Default for Icons {
|
|
|
|
fn default() -> Self {
|
|
|
|
Self {
|
|
|
|
caps_on: default_icon_caps(),
|
|
|
|
caps_off: String::new(),
|
|
|
|
num_on: default_icon_num(),
|
|
|
|
num_off: String::new(),
|
|
|
|
scroll_on: default_icon_scroll(),
|
|
|
|
scroll_off: String::new(),
|
2025-02-04 00:19:30 +03:00
|
|
|
layout_map: HashMap::new(),
|
2024-11-17 23:46:02 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const fn default_icon_size() -> i32 {
|
|
|
|
32
|
|
|
|
}
|
|
|
|
|
|
|
|
fn default_seat() -> String {
|
|
|
|
String::from("seat0")
|
|
|
|
}
|
|
|
|
|
|
|
|
fn default_icon_caps() -> String {
|
|
|
|
String::from("")
|
|
|
|
}
|
|
|
|
|
|
|
|
fn default_icon_num() -> String {
|
|
|
|
String::from("")
|
|
|
|
}
|
|
|
|
|
|
|
|
fn default_icon_scroll() -> String {
|
|
|
|
String::from("")
|
|
|
|
}
|
|
|
|
|
2025-02-04 00:19:30 +03:00
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
pub enum KeyboardUpdate {
|
|
|
|
Key(KeyEvent),
|
|
|
|
Layout(KeyboardLayoutUpdate),
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Module<gtk::Box> for KeyboardModule {
|
|
|
|
type SendMessage = KeyboardUpdate;
|
2024-11-17 23:46:02 +00:00
|
|
|
type ReceiveMessage = ();
|
|
|
|
|
2025-02-04 00:19:30 +03:00
|
|
|
module_impl!("keyboard");
|
2024-11-17 23:46:02 +00:00
|
|
|
|
|
|
|
fn spawn_controller(
|
|
|
|
&self,
|
|
|
|
_info: &ModuleInfo,
|
|
|
|
context: &WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
2025-02-04 00:19:30 +03:00
|
|
|
mut rx: mpsc::Receiver<Self::ReceiveMessage>,
|
2024-11-17 23:46:02 +00:00
|
|
|
) -> Result<()> {
|
|
|
|
let client = context.ironbar.clients.borrow_mut().libinput(&self.seat);
|
|
|
|
|
|
|
|
let tx = context.tx.clone();
|
|
|
|
spawn(async move {
|
|
|
|
let mut rx = client.subscribe();
|
|
|
|
while let Ok(ev) = rx.recv().await {
|
|
|
|
match ev {
|
|
|
|
Event::Device => {
|
|
|
|
for key in [Key::Caps, Key::Num, Key::Scroll] {
|
2025-02-04 00:19:30 +03:00
|
|
|
let event = KeyEvent {
|
|
|
|
key,
|
|
|
|
state: client.get_state(key),
|
|
|
|
};
|
|
|
|
module_update!(tx, KeyboardUpdate::Key(event));
|
2024-11-17 23:46:02 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
Event::Key(ev) => {
|
2025-02-04 00:19:30 +03:00
|
|
|
module_update!(tx, KeyboardUpdate::Key(ev));
|
2024-11-17 23:46:02 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2025-02-04 00:19:30 +03:00
|
|
|
let client = context.try_client::<dyn compositor::KeyboardLayoutClient>()?;
|
|
|
|
{
|
|
|
|
let client = client.clone();
|
|
|
|
let tx = context.tx.clone();
|
|
|
|
spawn(async move {
|
|
|
|
let mut srx = client.subscribe();
|
|
|
|
|
|
|
|
trace!("Set up keyboard_layout subscription");
|
|
|
|
|
2025-02-16 18:01:21 -05:00
|
|
|
loop {
|
|
|
|
match srx.recv().await {
|
|
|
|
Ok(payload) => {
|
|
|
|
debug!("Received update: {payload:?}");
|
|
|
|
module_update!(tx, KeyboardUpdate::Layout(payload));
|
|
|
|
}
|
|
|
|
Err(tokio::sync::broadcast::error::RecvError::Lagged(count)) => {
|
2025-02-21 16:35:54 +00:00
|
|
|
tracing::warn!(
|
|
|
|
"Channel lagged behind by {count}, this may result in unexpected or broken behaviour"
|
|
|
|
);
|
2025-02-16 18:01:21 -05:00
|
|
|
}
|
|
|
|
Err(err) => {
|
|
|
|
tracing::error!("{err:?}");
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
};
|
2025-02-04 00:19:30 +03:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// Change keyboard layout
|
|
|
|
spawn(async move {
|
|
|
|
trace!("Setting up keyboard_layout UI event handler");
|
|
|
|
|
|
|
|
while let Some(()) = rx.recv().await {
|
|
|
|
client.set_next_active();
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok::<(), Report>(())
|
|
|
|
});
|
|
|
|
|
2024-11-17 23:46:02 +00:00
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn into_widget(
|
|
|
|
self,
|
|
|
|
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
|
|
|
info: &ModuleInfo,
|
|
|
|
) -> Result<ModuleParts<gtk::Box>> {
|
2025-03-22 19:19:47 +00:00
|
|
|
let container = gtk::Box::new(self.layout.orientation(info), 0);
|
2024-11-17 23:46:02 +00:00
|
|
|
|
|
|
|
let caps = IconLabel::new(&self.icons.caps_off, info.icon_theme, self.icon_size);
|
|
|
|
let num = IconLabel::new(&self.icons.num_off, info.icon_theme, self.icon_size);
|
|
|
|
let scroll = IconLabel::new(&self.icons.scroll_off, info.icon_theme, self.icon_size);
|
|
|
|
|
2025-03-22 19:19:47 +00:00
|
|
|
caps.label().set_angle(self.layout.angle(info));
|
|
|
|
caps.label().set_justify(self.layout.justify.into());
|
|
|
|
|
|
|
|
num.label().set_angle(self.layout.angle(info));
|
|
|
|
num.label().set_justify(self.layout.justify.into());
|
|
|
|
|
|
|
|
scroll.label().set_angle(self.layout.angle(info));
|
|
|
|
scroll.label().set_justify(self.layout.justify.into());
|
|
|
|
|
|
|
|
let layout_button = IconButton::new("", info.icon_theme, self.icon_size);
|
2025-02-04 00:19:30 +03:00
|
|
|
|
2024-11-17 23:46:02 +00:00
|
|
|
if self.show_caps {
|
|
|
|
caps.add_class("key");
|
|
|
|
caps.add_class("caps");
|
2024-12-29 00:40:12 +00:00
|
|
|
container.add(&*caps);
|
2024-11-17 23:46:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if self.show_num {
|
|
|
|
num.add_class("key");
|
|
|
|
num.add_class("num");
|
2024-12-29 00:40:12 +00:00
|
|
|
container.add(&*num);
|
2024-11-17 23:46:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if self.show_scroll {
|
|
|
|
scroll.add_class("key");
|
|
|
|
scroll.add_class("scroll");
|
2024-12-29 00:40:12 +00:00
|
|
|
container.add(&*scroll);
|
2024-11-17 23:46:02 +00:00
|
|
|
}
|
|
|
|
|
2025-02-04 00:19:30 +03:00
|
|
|
if self.show_layout {
|
2025-03-22 19:19:47 +00:00
|
|
|
layout_button.add_class("layout");
|
|
|
|
container.add(&*layout_button);
|
2025-02-04 00:19:30 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
{
|
|
|
|
let tx = context.controller_tx.clone();
|
|
|
|
layout_button.connect_clicked(move |_| {
|
|
|
|
try_send!(tx, ());
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-11-17 23:46:02 +00:00
|
|
|
let icons = self.icons;
|
2025-02-04 00:19:30 +03:00
|
|
|
let handle_event = move |ev: KeyboardUpdate| match ev {
|
|
|
|
KeyboardUpdate::Key(ev) => {
|
|
|
|
let parts = match (ev.key, ev.state) {
|
|
|
|
(Key::Caps, true) if self.show_caps => Some((&caps, icons.caps_on.as_str())),
|
|
|
|
(Key::Caps, false) if self.show_caps => Some((&caps, icons.caps_off.as_str())),
|
|
|
|
(Key::Num, true) if self.show_num => Some((&num, icons.num_on.as_str())),
|
|
|
|
(Key::Num, false) if self.show_num => Some((&num, icons.num_off.as_str())),
|
|
|
|
(Key::Scroll, true) if self.show_scroll => {
|
|
|
|
Some((&scroll, icons.scroll_on.as_str()))
|
|
|
|
}
|
|
|
|
(Key::Scroll, false) if self.show_scroll => {
|
|
|
|
Some((&scroll, icons.scroll_off.as_str()))
|
|
|
|
}
|
|
|
|
_ => None,
|
|
|
|
};
|
2024-11-17 23:46:02 +00:00
|
|
|
|
2025-02-04 00:19:30 +03:00
|
|
|
if let Some((label, input)) = parts {
|
|
|
|
label.set_label(Some(input));
|
2024-11-17 23:46:02 +00:00
|
|
|
|
2025-02-04 00:19:30 +03:00
|
|
|
if ev.state {
|
|
|
|
label.add_class("enabled");
|
|
|
|
} else {
|
|
|
|
label.remove_class("enabled");
|
|
|
|
}
|
2024-11-17 23:46:02 +00:00
|
|
|
}
|
|
|
|
}
|
2025-02-04 00:19:30 +03:00
|
|
|
KeyboardUpdate::Layout(KeyboardLayoutUpdate(language)) => {
|
|
|
|
let text = icons.layout_map.get(&language).unwrap_or(&language);
|
2025-03-22 19:19:47 +00:00
|
|
|
layout_button.set_label(text);
|
2025-02-04 00:19:30 +03:00
|
|
|
}
|
2024-11-17 23:46:02 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
glib_recv!(context.subscribe(), handle_event);
|
|
|
|
Ok(ModuleParts::new(container, None))
|
|
|
|
}
|
|
|
|
}
|