1
0
Fork 0
mirror of https://github.com/Zedfrigg/ironbar.git synced 2025-08-17 14:51: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,7 +1,11 @@
use super::{Visibility, Workspace, WorkspaceClient, WorkspaceUpdate};
use super::{
KeyboardLayoutClient, KeyboardLayoutUpdate, Visibility, Workspace, WorkspaceClient,
WorkspaceUpdate,
};
use crate::{arc_mut, lock, send, spawn_blocking};
use color_eyre::Result;
use hyprland::data::{Workspace as HWorkspace, Workspaces};
use hyprland::ctl::switch_xkb_layout;
use hyprland::data::{Devices, Workspace as HWorkspace, Workspaces};
use hyprland::dispatch::{Dispatch, DispatchType, WorkspaceIdentifierWithSpecial};
use hyprland::event_listener::EventListener;
use hyprland::prelude::*;
@ -13,15 +17,21 @@ use tracing::{debug, error, info};
pub struct Client {
workspace_tx: Sender<WorkspaceUpdate>,
_workspace_rx: Receiver<WorkspaceUpdate>,
keyboard_layout_tx: Sender<KeyboardLayoutUpdate>,
_keyboard_layout_rx: Receiver<KeyboardLayoutUpdate>,
}
impl Client {
pub(crate) fn new() -> Self {
let (workspace_tx, workspace_rx) = channel(16);
let (keyboard_layout_tx, keyboard_layout_rx) = channel(4);
let instance = Self {
workspace_tx,
_workspace_rx: workspace_rx,
keyboard_layout_tx,
_keyboard_layout_rx: keyboard_layout_rx,
};
instance.listen_workspace_events();
@ -32,6 +42,7 @@ impl Client {
info!("Starting Hyprland event listener");
let tx = self.workspace_tx.clone();
let keyboard_layout_tx = self.keyboard_layout_tx.clone();
spawn_blocking(move || {
let mut event_listener = EventListener::new();
@ -178,6 +189,9 @@ impl Client {
}
{
let tx = tx.clone();
let lock = lock.clone();
event_listener.add_urgent_state_handler(move |address| {
let _lock = lock!(lock);
debug!("Received urgent state: {address:?}");
@ -206,6 +220,55 @@ impl Client {
});
}
{
let tx = keyboard_layout_tx.clone();
let lock = lock.clone();
event_listener.add_keyboard_layout_change_handler(move |layout_event| {
let _lock = lock!(lock);
let layout = if layout_event.layout_name.is_empty() {
// FIXME: This field is empty due to bug in `hyprland-rs_0.4.0-alpha.3`. Which is already fixed in last betas
// The layout may be empty due to a bug in `hyprland-rs`, because of which the `layout_event` is incorrect.
//
// Instead of:
// ```
// LayoutEvent {
// keyboard_name: "keychron-keychron-c2",
// layout_name: "English (US)",
// }
// ```
//
// We get:
// ```
// LayoutEvent {
// keyboard_name: "keychron-keychron-c2,English (US)",
// layout_name: "",
// }
// ```
//
// Here we are trying to recover `layout_name` from `keyboard_name`
let layout = layout_event.keyboard_name.as_str().split(",").nth(1);
let Some(layout) = layout else {
error!(
"Failed to get layout from string: {}. The failed logic is a workaround for a bug in `hyprland 0.4.0-alpha.3`", layout_event.keyboard_name);
return;
};
layout.into()
}
else {
layout_event.layout_name
};
debug!("Received layout: {layout:?}");
send!(tx, KeyboardLayoutUpdate(layout));
});
}
event_listener
.start_listener()
.expect("Failed to start listener");
@ -264,36 +327,73 @@ impl Client {
}
impl WorkspaceClient for Client {
fn focus(&self, id: String) -> Result<()> {
fn focus(&self, id: String) {
let identifier = id.parse::<i32>().map_or_else(
|_| WorkspaceIdentifierWithSpecial::Name(&id),
WorkspaceIdentifierWithSpecial::Id,
);
Dispatch::call(DispatchType::Workspace(identifier))?;
Ok(())
if let Err(e) = Dispatch::call(DispatchType::Workspace(identifier)) {
error!("Couldn't focus workspace '{id}': {e:#}");
}
}
fn subscribe_workspace_change(&self) -> Receiver<WorkspaceUpdate> {
fn subscribe(&self) -> Receiver<WorkspaceUpdate> {
let rx = self.workspace_tx.subscribe();
{
let tx = self.workspace_tx.clone();
let active_id = HWorkspace::get_active().ok().map(|active| active.name);
let is_visible = create_is_visible();
let active_id = HWorkspace::get_active().ok().map(|active| active.name);
let is_visible = create_is_visible();
let workspaces = Workspaces::get()
.expect("Failed to get workspaces")
.into_iter()
.map(|w| {
let vis = Visibility::from((&w, active_id.as_deref(), &is_visible));
let workspaces = Workspaces::get()
.expect("Failed to get workspaces")
.into_iter()
.map(|w| {
let vis = Visibility::from((&w, active_id.as_deref(), &is_visible));
Workspace::from((vis, w))
})
.collect();
Workspace::from((vis, w))
})
.collect();
send!(self.workspace_tx, WorkspaceUpdate::Init(workspaces));
send!(tx, WorkspaceUpdate::Init(workspaces));
rx
}
}
impl KeyboardLayoutClient for Client {
fn set_next_active(&self) {
let device = Devices::get()
.expect("Failed to get devices")
.keyboards
.iter()
.find(|k| k.main)
.map(|k| k.name.clone());
if let Some(device) = device {
if let Err(e) =
switch_xkb_layout::call(device, switch_xkb_layout::SwitchXKBLayoutCmdTypes::Next)
{
error!("Failed to switch keyboard layout due to Hyprland error: {e}");
}
} else {
error!("Failed to get keyboard device from hyprland");
}
}
fn subscribe(&self) -> Receiver<KeyboardLayoutUpdate> {
let rx = self.keyboard_layout_tx.subscribe();
let layout = Devices::get()
.expect("Failed to get devices")
.keyboards
.iter()
.find(|k| k.main)
.map(|k| k.active_keymap.clone());
if let Some(layout) = layout {
send!(self.keyboard_layout_tx, KeyboardLayoutUpdate(layout));
} else {
error!("Failed to get current keyboard layout hyprland");
}
rx

View file

@ -54,6 +54,26 @@ impl Compositor {
}
}
pub fn create_keyboard_layout_client(
clients: &mut super::Clients,
) -> Result<Arc<dyn KeyboardLayoutClient + Send + Sync>> {
let current = Self::get_current();
debug!("Getting keyboard_layout client for: {current}");
match current {
#[cfg(feature = "keyboard+sway")]
Self::Sway => clients
.sway()
.map(|client| client as Arc<dyn KeyboardLayoutClient + Send + Sync>),
#[cfg(feature = "keyboard+hyprland")]
Self::Hyprland => clients
.hyprland()
.map(|client| client as Arc<dyn KeyboardLayoutClient + Send + Sync>),
Self::Unsupported => Err(Report::msg("Unsupported compositor").note(
"Currently keyboard layout functionality are only supported by Sway and Hyprland",
)),
}
}
/// Creates a new instance of
/// the workspace client for the current compositor.
pub fn create_workspace_client(
@ -67,7 +87,9 @@ impl Compositor {
.sway()
.map(|client| client as Arc<dyn WorkspaceClient + Send + Sync>),
#[cfg(feature = "workspaces+hyprland")]
Self::Hyprland => Ok(Arc::new(hyprland::Client::new())),
Self::Hyprland => clients
.hyprland()
.map(|client| client as Arc<dyn WorkspaceClient + Send + Sync>),
Self::Unsupported => Err(Report::msg("Unsupported compositor")
.note("Currently workspaces are only supported by Sway and Hyprland")),
}
@ -112,6 +134,9 @@ impl Visibility {
}
}
#[derive(Debug, Clone)]
pub struct KeyboardLayoutUpdate(pub String);
#[derive(Debug, Clone)]
pub enum WorkspaceUpdate {
/// Provides an initial list of workspaces.
@ -146,10 +171,20 @@ pub enum WorkspaceUpdate {
pub trait WorkspaceClient: Debug + Send + Sync {
/// Requests the workspace with this name is focused.
fn focus(&self, name: String) -> Result<()>;
fn focus(&self, name: String);
/// Creates a new to workspace event receiver.
fn subscribe_workspace_change(&self) -> broadcast::Receiver<WorkspaceUpdate>;
fn subscribe(&self) -> broadcast::Receiver<WorkspaceUpdate>;
}
register_fallible_client!(dyn WorkspaceClient, workspaces);
pub trait KeyboardLayoutClient: Debug + Send + Sync {
/// Switches to the next layout.
fn set_next_active(&self);
/// Creates a new to keyboard layout event receiver.
fn subscribe(&self) -> broadcast::Receiver<KeyboardLayoutUpdate>;
}
register_fallible_client!(dyn KeyboardLayoutClient, keyboard_layout);

View file

@ -1,21 +1,25 @@
use super::{Visibility, Workspace, WorkspaceClient, WorkspaceUpdate};
use crate::{await_sync, send};
use color_eyre::Result;
use swayipc_async::{Node, WorkspaceChange, WorkspaceEvent};
use super::{
KeyboardLayoutClient, KeyboardLayoutUpdate, Visibility, Workspace, WorkspaceClient,
WorkspaceUpdate,
};
use crate::{await_sync, error, send, spawn};
use swayipc_async::{InputChange, InputEvent, Node, WorkspaceChange, WorkspaceEvent};
use tokio::sync::broadcast::{channel, Receiver};
use crate::clients::sway::Client;
impl WorkspaceClient for Client {
fn focus(&self, id: String) -> Result<()> {
await_sync(async move {
let mut client = self.connection().lock().await;
client.run_command(format!("workspace {id}")).await
})?;
Ok(())
fn focus(&self, id: String) {
let client = self.connection().clone();
spawn(async move {
let mut client = client.lock().await;
if let Err(e) = client.run_command(format!("workspace {id}")).await {
error!("Couldn't focus workspace '{id}': {e:#}");
}
});
}
fn subscribe_workspace_change(&self) -> Receiver<WorkspaceUpdate> {
fn subscribe(&self) -> Receiver<WorkspaceUpdate> {
let (tx, rx) = channel(16);
let client = self.connection().clone();
@ -133,3 +137,77 @@ impl From<WorkspaceEvent> for WorkspaceUpdate {
}
}
}
impl KeyboardLayoutClient for Client {
fn set_next_active(&self) {
let client = self.connection().clone();
spawn(async move {
let mut client = client.lock().await;
let inputs = client.get_inputs().await.expect("to get inputs");
if let Some(keyboard) = inputs
.into_iter()
.find(|i| i.xkb_active_layout_name.is_some())
{
if let Err(e) = client
.run_command(format!(
"input {} xkb_switch_layout next",
keyboard.identifier
))
.await
{
error!("Failed to switch keyboard layout due to Sway error: {e}");
}
} else {
error!("Failed to get keyboard identifier from Sway");
}
});
}
fn subscribe(&self) -> Receiver<KeyboardLayoutUpdate> {
let (tx, rx) = channel(4);
let client = self.connection().clone();
await_sync(async {
let mut client = client.lock().await;
let inputs = client.get_inputs().await.expect("to get inputs");
if let Some(layout) = inputs.into_iter().find_map(|i| i.xkb_active_layout_name) {
send!(tx, KeyboardLayoutUpdate(layout));
} else {
error!("Failed to get keyboard layout from Sway!");
}
drop(client);
self.add_listener::<InputEvent>(move |event| {
if let Ok(layout) = KeyboardLayoutUpdate::try_from(event.clone()) {
send!(tx, layout);
}
})
.await
.expect("to add listener");
});
rx
}
}
impl TryFrom<InputEvent> for KeyboardLayoutUpdate {
type Error = ();
fn try_from(value: InputEvent) -> std::result::Result<Self, Self::Error> {
match value.change {
InputChange::XkbLayout => {
if let Some(layout) = value.input.xkb_active_layout_name {
Ok(KeyboardLayoutUpdate(layout))
} else {
Err(())
}
}
_ => Err(()),
}
}
}

View file

@ -9,7 +9,7 @@ use std::sync::Arc;
pub mod clipboard;
#[cfg(feature = "workspaces")]
pub mod compositor;
#[cfg(feature = "keys")]
#[cfg(feature = "keyboard")]
pub mod libinput;
#[cfg(feature = "cairo")]
pub mod lua;
@ -38,10 +38,14 @@ pub struct Clients {
workspaces: Option<Arc<dyn compositor::WorkspaceClient>>,
#[cfg(feature = "sway")]
sway: Option<Arc<sway::Client>>,
#[cfg(feature = "hyprland")]
hyprland: Option<Arc<compositor::hyprland::Client>>,
#[cfg(feature = "clipboard")]
clipboard: Option<Arc<clipboard::Client>>,
#[cfg(feature = "keys")]
#[cfg(feature = "keyboard")]
libinput: HashMap<Box<str>, Arc<libinput::Client>>,
#[cfg(any(feature = "keyboard+sway", feature = "keyboard+hyprland"))]
keyboard_layout: Option<Arc<dyn compositor::KeyboardLayoutClient>>,
#[cfg(feature = "cairo")]
lua: Option<Rc<lua::LuaEngine>>,
#[cfg(feature = "music")]
@ -93,6 +97,19 @@ impl Clients {
Ok(client)
}
#[cfg(any(feature = "keyboard+sway", feature = "keyboard+hyprland"))]
pub fn keyboard_layout(&mut self) -> ClientResult<dyn compositor::KeyboardLayoutClient> {
let client = if let Some(keyboard_layout) = &self.keyboard_layout {
keyboard_layout.clone()
} else {
let client = compositor::Compositor::create_keyboard_layout_client(self)?;
self.keyboard_layout.replace(client.clone());
client
};
Ok(client)
}
#[cfg(feature = "sway")]
pub fn sway(&mut self) -> ClientResult<sway::Client> {
let client = if let Some(client) = &self.sway {
@ -107,6 +124,19 @@ impl Clients {
Ok(client)
}
#[cfg(feature = "hyprland")]
pub fn hyprland(&mut self) -> ClientResult<compositor::hyprland::Client> {
let client = if let Some(client) = &self.hyprland {
client.clone()
} else {
let client = Arc::new(compositor::hyprland::Client::new());
self.hyprland.replace(client.clone());
client
};
Ok(client)
}
#[cfg(feature = "cairo")]
pub fn lua(&mut self, config_dir: &Path) -> Rc<lua::LuaEngine> {
self.lua
@ -114,7 +144,7 @@ impl Clients {
.clone()
}
#[cfg(feature = "keys")]
#[cfg(feature = "keyboard")]
pub fn libinput(&mut self, seat: &str) -> Arc<libinput::Client> {
self.libinput
.entry(seat.into())