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