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:
parent
ee19176a2c
commit
03e6f10141
15 changed files with 552 additions and 202 deletions
|
@ -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);
|
|
@ -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;
|
||||
|
|
|
@ -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>(())
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue