1
0
Fork 0
mirror of https://github.com/Zedfrigg/ironbar.git synced 2025-08-17 23:01:04 +02:00

feat(keyboard): ability to display and switch kb layout (#836)

This extends the existing `keys` module to be able to show the current keyboard layout, and cycle between layouts (using the `next` command) by clicking. The `keys` module has been renamed to `keyboard` to more accurately reflect its extended featureset.
This commit is contained in:
kuzy000 2025-02-04 00:19:30 +03:00 committed by GitHub
parent ee19176a2c
commit 03e6f10141
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 552 additions and 202 deletions

View file

@ -1,18 +1,23 @@
use std::collections::HashMap;
use color_eyre::eyre::Report;
use color_eyre::Result;
use gtk::prelude::*;
use gtk::{prelude::*, Button};
use serde::Deserialize;
use tokio::sync::mpsc;
use tracing::{debug, trace};
use super::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use super::{Module, ModuleInfo, ModuleParts, WidgetContext};
use crate::clients::compositor::{self, KeyboardLayoutUpdate};
use crate::clients::libinput::{Event, Key, KeyEvent};
use crate::config::CommonConfig;
use crate::gtk_helpers::IronbarGtkExt;
use crate::image::IconLabel;
use crate::{glib_recv, module_impl, module_update, send_async, spawn};
use crate::{glib_recv, module_impl, module_update, send_async, spawn, try_send};
#[derive(Debug, Deserialize, Clone)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct KeysModule {
pub struct KeyboardModule {
/// Whether to show capslock indicator.
///
/// **Default**: `true`
@ -31,6 +36,12 @@ pub struct KeysModule {
#[serde(default = "crate::config::default_true")]
show_scroll: bool,
/// Whether to show the current keyboard layout.
///
/// **Default**: `true`
#[serde(default = "crate::config::default_true")]
show_layout: bool,
/// Size to render the icons at, in pixels (image icons only).
///
/// **Default** `32`
@ -93,6 +104,26 @@ struct Icons {
/// **Default**: `""`
#[serde(default)]
scroll_off: String,
/// 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>,
}
impl Default for Icons {
@ -104,6 +135,7 @@ impl Default for Icons {
num_off: String::new(),
scroll_on: default_icon_scroll(),
scroll_off: String::new(),
layout_map: HashMap::new(),
}
}
}
@ -128,17 +160,23 @@ fn default_icon_scroll() -> String {
String::from("")
}
impl Module<gtk::Box> for KeysModule {
type SendMessage = KeyEvent;
#[derive(Debug, Clone)]
pub enum KeyboardUpdate {
Key(KeyEvent),
Layout(KeyboardLayoutUpdate),
}
impl Module<gtk::Box> for KeyboardModule {
type SendMessage = KeyboardUpdate;
type ReceiveMessage = ();
module_impl!("keys");
module_impl!("keyboard");
fn spawn_controller(
&self,
_info: &ModuleInfo,
context: &WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
_rx: mpsc::Receiver<Self::ReceiveMessage>,
mut rx: mpsc::Receiver<Self::ReceiveMessage>,
) -> Result<()> {
let client = context.ironbar.clients.borrow_mut().libinput(&self.seat);
@ -149,22 +187,47 @@ impl Module<gtk::Box> for KeysModule {
match ev {
Event::Device => {
for key in [Key::Caps, Key::Num, Key::Scroll] {
module_update!(
tx,
KeyEvent {
key: Key::Caps,
state: client.get_state(key)
}
);
let event = KeyEvent {
key,
state: client.get_state(key),
};
module_update!(tx, KeyboardUpdate::Key(event));
}
}
Event::Key(ev) => {
send_async!(tx, ModuleUpdateEvent::Update(ev));
module_update!(tx, KeyboardUpdate::Key(ev));
}
}
}
});
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");
while let Ok(payload) = srx.recv().await {
debug!("Received update: {payload:?}");
module_update!(tx, KeyboardUpdate::Layout(payload));
}
});
}
// 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>(())
});
Ok(())
}
@ -173,12 +236,16 @@ impl Module<gtk::Box> for KeysModule {
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> Result<ModuleParts<gtk::Box>> {
let container = gtk::Box::new(info.bar_position.orientation(), 5);
let container = gtk::Box::new(info.bar_position.orientation(), 0);
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);
let layout_button = Button::new();
let layout = IconLabel::new("", info.icon_theme, self.icon_size);
layout_button.add(&*layout);
if self.show_caps {
caps.add_class("key");
caps.add_class("caps");
@ -197,31 +264,49 @@ impl Module<gtk::Box> for KeysModule {
container.add(&*scroll);
}
if self.show_layout {
layout.add_class("layout");
container.add(&layout_button);
}
{
let tx = context.controller_tx.clone();
layout_button.connect_clicked(move |_| {
try_send!(tx, ());
});
}
let icons = self.icons;
let handle_event = move |ev: KeyEvent| {
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,
};
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,
};
if let Some((label, input)) = parts {
label.set_label(Some(input));
if let Some((label, input)) = parts {
label.set_label(Some(input));
if ev.state {
label.add_class("enabled");
} else {
label.remove_class("enabled");
if ev.state {
label.add_class("enabled");
} else {
label.remove_class("enabled");
}
}
}
KeyboardUpdate::Layout(KeyboardLayoutUpdate(language)) => {
let text = icons.layout_map.get(&language).unwrap_or(&language);
layout.set_label(Some(text));
}
};
glib_recv!(context.subscribe(), handle_event);

View file

@ -31,8 +31,8 @@ pub mod clock;
pub mod custom;
#[cfg(feature = "focused")]
pub mod focused;
#[cfg(feature = "keys")]
pub mod keys;
#[cfg(feature = "keyboard")]
pub mod keyboard;
pub mod label;
#[cfg(feature = "launcher")]
pub mod launcher;

View file

@ -196,7 +196,7 @@ impl Module<gtk::Box> for WorkspacesModule {
let client = context.ironbar.clients.borrow_mut().workspaces()?;
// Subscribe & send events
spawn(async move {
let mut srx = client.subscribe_workspace_change();
let mut srx = client.subscribe();
trace!("Set up workspace subscription");
@ -213,9 +213,7 @@ impl Module<gtk::Box> for WorkspacesModule {
trace!("Setting up UI event handler");
while let Some(name) = rx.recv().await {
if let Err(e) = client.focus(name.clone()) {
warn!("Couldn't focus workspace '{name}': {e:#}");
};
client.focus(name.clone());
}
Ok::<(), Report>(())