mirror of
https://github.com/Zedfrigg/ironbar.git
synced 2025-08-16 22:31:03 +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
15
Cargo.toml
15
Cargo.toml
|
@ -18,7 +18,7 @@ default = [
|
||||||
"focused",
|
"focused",
|
||||||
"http",
|
"http",
|
||||||
"ipc",
|
"ipc",
|
||||||
"keys",
|
"keyboard+all",
|
||||||
"launcher",
|
"launcher",
|
||||||
"music+all",
|
"music+all",
|
||||||
"network_manager",
|
"network_manager",
|
||||||
|
@ -56,7 +56,10 @@ clock = ["chrono"]
|
||||||
|
|
||||||
focused = []
|
focused = []
|
||||||
|
|
||||||
keys = ["dep:input", "dep:evdev-rs", "dep:libc", "dep:nix"]
|
keyboard = ["dep:input", "dep:evdev-rs", "dep:libc", "dep:nix"]
|
||||||
|
"keyboard+all" = ["keyboard", "keyboard+sway", "keyboard+hyprland"]
|
||||||
|
"keyboard+sway" = ["keyboard", "sway"]
|
||||||
|
"keyboard+hyprland" = ["keyboard", "hyprland"]
|
||||||
|
|
||||||
launcher = []
|
launcher = []
|
||||||
|
|
||||||
|
@ -137,7 +140,7 @@ cairo-rs = { version = "0.18.5", optional = true, features = ["png"] }
|
||||||
# clock
|
# clock
|
||||||
chrono = { version = "0.4.39", optional = true, default-features = false, features = ["clock", "unstable-locales"] }
|
chrono = { version = "0.4.39", optional = true, default-features = false, features = ["clock", "unstable-locales"] }
|
||||||
|
|
||||||
# keys
|
# keyboard
|
||||||
input = { version = "0.9.1", optional = true }
|
input = { version = "0.9.1", optional = true }
|
||||||
evdev-rs = { version = "0.6.1", optional = true }
|
evdev-rs = { version = "0.6.1", optional = true }
|
||||||
libc = { version = "0.2.164", optional = true }
|
libc = { version = "0.2.164", optional = true }
|
||||||
|
@ -161,10 +164,6 @@ upower_dbus = { version = "0.3.2", optional = true }
|
||||||
# volume
|
# volume
|
||||||
libpulse-binding = { version = "2.28.2", optional = true }
|
libpulse-binding = { version = "2.28.2", optional = true }
|
||||||
|
|
||||||
# workspaces
|
|
||||||
swayipc-async = { version = "2.0.1", optional = true }
|
|
||||||
hyprland = { version = "0.4.0-alpha.3", features = ["silent"], optional = true }
|
|
||||||
|
|
||||||
# shared
|
# shared
|
||||||
futures-lite = { version = "2.6.0", optional = true } # network_manager, upower, workspaces
|
futures-lite = { version = "2.6.0", optional = true } # network_manager, upower, workspaces
|
||||||
nix = { version = "0.29.0", optional = true, features = ["event", "fs", "poll"] } # clipboard, input
|
nix = { version = "0.29.0", optional = true, features = ["event", "fs", "poll"] } # clipboard, input
|
||||||
|
@ -172,6 +171,8 @@ regex = { version = "1.11.1", default-features = false, features = [
|
||||||
"std",
|
"std",
|
||||||
], optional = true } # music, sys_info
|
], optional = true } # music, sys_info
|
||||||
zbus = { version = "3.15.2", default-features = false, features = ["tokio"], optional = true } # network_manager, notifications, upower
|
zbus = { version = "3.15.2", default-features = false, features = ["tokio"], optional = true } # network_manager, notifications, upower
|
||||||
|
swayipc-async = { version = "2.0.1", optional = true } # workspaces, keyboard
|
||||||
|
hyprland = { version = "0.4.0-alpha.3", features = ["silent"], optional = true } # workspaces, keyboard
|
||||||
|
|
||||||
# schema
|
# schema
|
||||||
schemars = { version = "0.8.21", optional = true }
|
schemars = { version = "0.8.21", optional = true }
|
||||||
|
|
|
@ -26,7 +26,7 @@ pacman -S openssl
|
||||||
pacman -S libdbusmenu-gtk3
|
pacman -S libdbusmenu-gtk3
|
||||||
# for volume support
|
# for volume support
|
||||||
pacman -S libpulse
|
pacman -S libpulse
|
||||||
# for keys support
|
# for keyboard support
|
||||||
pacman -S libinput
|
pacman -S libinput
|
||||||
# for lua/cairo support
|
# for lua/cairo support
|
||||||
pacman -S luajit lua51-lgi
|
pacman -S luajit lua51-lgi
|
||||||
|
@ -42,7 +42,7 @@ apt install libssl-dev
|
||||||
apt install libdbusmenu-gtk3-dev
|
apt install libdbusmenu-gtk3-dev
|
||||||
# for volume support
|
# for volume support
|
||||||
apt install libpulse-dev
|
apt install libpulse-dev
|
||||||
# for keys support
|
# for keyboard support
|
||||||
apt install libinput-dev
|
apt install libinput-dev
|
||||||
# for lua/cairo support
|
# for lua/cairo support
|
||||||
apt install luajit-dev lua-lgi
|
apt install luajit-dev lua-lgi
|
||||||
|
@ -58,7 +58,7 @@ dnf install openssl-devel
|
||||||
dnf install libdbusmenu-gtk3-devel
|
dnf install libdbusmenu-gtk3-devel
|
||||||
# for volume support
|
# for volume support
|
||||||
dnf install pulseaudio-libs-devel
|
dnf install pulseaudio-libs-devel
|
||||||
# for keys support
|
# for keyboard support
|
||||||
dnf install libinput-devel
|
dnf install libinput-devel
|
||||||
# for lua/cairo support
|
# for lua/cairo support
|
||||||
dnf install luajit-devel lua-lgi
|
dnf install luajit-devel lua-lgi
|
||||||
|
@ -85,7 +85,7 @@ cargo build --release --no-default-features \
|
||||||
> ⚠ Make sure you enable at least one `config` feature otherwise you will not be able to start the bar!
|
> ⚠ Make sure you enable at least one `config` feature otherwise you will not be able to start the bar!
|
||||||
|
|
||||||
| Feature | Description |
|
| Feature | Description |
|
||||||
|---------------------|-----------------------------------------------------------------------------------|
|
| ------------------- | --------------------------------------------------------------------------------- |
|
||||||
| **Core** | |
|
| **Core** | |
|
||||||
| http | Enables HTTP features. Currently this includes the ability to load remote images. |
|
| http | Enables HTTP features. Currently this includes the ability to load remote images. |
|
||||||
| ipc | Enables the IPC server. |
|
| ipc | Enables the IPC server. |
|
||||||
|
@ -101,6 +101,10 @@ cargo build --release --no-default-features \
|
||||||
| clipboard | Enables the `clipboard` module. |
|
| clipboard | Enables the `clipboard` module. |
|
||||||
| clock | Enables the `clock` module. |
|
| clock | Enables the `clock` module. |
|
||||||
| focused | Enables the `focused` module. |
|
| focused | Enables the `focused` module. |
|
||||||
|
| keyboard | Enables the `keyboard` module without keyboard layout support. |
|
||||||
|
| keyboard+all | Enables the `keyboard` module with keyboard layout support for all compositors. |
|
||||||
|
| keyboard+sway | Enables the `keyboard` module with keyboard layout support for Sway. |
|
||||||
|
| keyboard+hyprland | Enables the `keyboard` module with keyboard layout support for Hyprland. |
|
||||||
| launcher | Enables the `launcher` module. |
|
| launcher | Enables the `launcher` module. |
|
||||||
| music+all | Enables the `music` module with support for all player types. |
|
| music+all | Enables the `music` module with support for all player types. |
|
||||||
| music+mpris | Enables the `music` module with MPRIS support. |
|
| music+mpris | Enables the `music` module with MPRIS support. |
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
- [Clock](clock)
|
- [Clock](clock)
|
||||||
- [Custom](custom)
|
- [Custom](custom)
|
||||||
- [Focused](focused)
|
- [Focused](focused)
|
||||||
- [Keys](keys)
|
- [Keyboard](keyboard)
|
||||||
- [Label](label)
|
- [Label](label)
|
||||||
- [Launcher](launcher)
|
- [Launcher](launcher)
|
||||||
- [Music](music)
|
- [Music](music)
|
||||||
|
|
121
docs/modules/Keyboard.md
Normal file
121
docs/modules/Keyboard.md
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
> [!NOTE]
|
||||||
|
> This module requires your user is in the `input` group.
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> The keyboard layout feature is only available on Sway and Hyprland.
|
||||||
|
|
||||||
|
Displays the toggle state of the capslock, num lock and scroll lock keys, and the current keyboard layout.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
> Type: `keyboard`
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
| ------------------ | ------------------------------ | ------- | ------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `show_caps` | `boolean` | `true` | Whether to show capslock indicator. |
|
||||||
|
| `show_num` | `boolean` | `true` | Whether to show num lock indicator. |
|
||||||
|
| `show_scroll` | `boolean` | `true` | Whether to show scroll lock indicator. |
|
||||||
|
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
|
||||||
|
| `icons.caps_on` | `string` or [image](images) | `` | Icon to show for enabled capslock indicator. |
|
||||||
|
| `icons.caps_off` | `string` or [image](images) | `''` | Icon to show for disabled capslock indicator. |
|
||||||
|
| `icons.num_on` | `string` or [image](images) | `` | Icon to show for enabled num lock indicator. |
|
||||||
|
| `icons.num_off` | `string` or [image](images) | `''` | Icon to show for disabled num lock indicator. |
|
||||||
|
| `icons.scroll_on` | `string` or [image](images) | `` | Icon to show for enabled scroll lock indicator. |
|
||||||
|
| `icons.scroll_off` | `string` or [image](images) | `''` | Icon to show for disabled scroll lock indicator. |
|
||||||
|
| `icons.layout_map` | `Map<string, string or image>` | `{}` | Map of icons or labels to show for a particular keyboard layout. Layouts use their actual name if not present in the map. |
|
||||||
|
| `seat` | `string` | `seat0` | ID of the Wayland seat to attach to. |
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>JSON</summary>
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"end": [
|
||||||
|
{
|
||||||
|
"type": "keyboard",
|
||||||
|
"show_scroll": false,
|
||||||
|
"icons": {
|
||||||
|
"caps_on": "",
|
||||||
|
"layout_map": {
|
||||||
|
"English (US)": "🇺🇸",
|
||||||
|
"Ukrainian": "🇺🇦"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>TOML</summary>
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[[end]]
|
||||||
|
type = "keyboard"
|
||||||
|
show_scroll = false
|
||||||
|
|
||||||
|
[end.icons]
|
||||||
|
caps_on = ""
|
||||||
|
|
||||||
|
[end.icons.layout_map]
|
||||||
|
"English (US)" = "🇺🇸"
|
||||||
|
Ukrainian = "🇺🇦"
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>YAML</summary>
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
end:
|
||||||
|
- type: keyboard
|
||||||
|
show_scroll: false
|
||||||
|
icons:
|
||||||
|
caps_on:
|
||||||
|
layout_map:
|
||||||
|
"English (US)": 🇺🇸
|
||||||
|
Ukrainian: 🇺🇦
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Corn</summary>
|
||||||
|
|
||||||
|
```corn
|
||||||
|
{
|
||||||
|
end = [
|
||||||
|
{
|
||||||
|
type = "keyboard"
|
||||||
|
show_scroll = false
|
||||||
|
icons.caps_on = ""
|
||||||
|
icons.layout_map.'English (US)' = "🇺🇸"
|
||||||
|
icons.layout_map.Ukrainian = "🇺🇦"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## Styling
|
||||||
|
|
||||||
|
| Selector | Description |
|
||||||
|
| -------------------------- | ------------------------------------------ |
|
||||||
|
| `.keyboard` | Keys box container widget. |
|
||||||
|
| `.keyboard .key` | Individual key indicator container widget. |
|
||||||
|
| `.keyboard .key.enabled` | Key indicator where key is toggled on. |
|
||||||
|
| `.keyboard .key.caps` | Capslock key indicator. |
|
||||||
|
| `.keyboard .key.num` | Num lock key indicator. |
|
||||||
|
| `.keyboard .key.scroll` | Scroll lock key indicator. |
|
||||||
|
| `.keyboard .key.image` | Key indicator image icon. |
|
||||||
|
| `.keyboard .key.text-icon` | Key indicator textual icon. |
|
||||||
|
| `.keyboard .layout` | Keyboard layout indicator. |
|
||||||
|
|
||||||
|
For more information on styling, please see the [styling guide](styling-guide).
|
|
@ -1,102 +0,0 @@
|
||||||
> [!NOTE]
|
|
||||||
> This module requires your user is in the `input` group.
|
|
||||||
|
|
||||||
Displays the toggle state of the capslock, num lock and scroll lock keys.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
> Type: `keys`
|
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
|
||||||
|--------------------|-----------------------------|---------|--------------------------------------------------|
|
|
||||||
| `show_caps` | `boolean` | `true` | Whether to show capslock indicator. |
|
|
||||||
| `show_num` | `boolean` | `true` | Whether to show num lock indicator. |
|
|
||||||
| `show_scroll` | `boolean` | `true` | Whether to show scroll lock indicator. |
|
|
||||||
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
|
|
||||||
| `icons.caps_on` | `string` or [image](images) | `` | Icon to show for enabled capslock indicator. |
|
|
||||||
| `icons.caps_off` | `string` or [image](images) | `''` | Icon to show for disabled capslock indicator. |
|
|
||||||
| `icons.num_on` | `string` or [image](images) | `` | Icon to show for enabled num lock indicator. |
|
|
||||||
| `icons.num_off` | `string` or [image](images) | `''` | Icon to show for disabled num lock indicator. |
|
|
||||||
| `icons.scroll_on` | `string` or [image](images) | `` | Icon to show for enabled scroll lock indicator. |
|
|
||||||
| `icons.scroll_off` | `string` or [image](images) | `''` | Icon to show for disabled scroll lock indicator. |
|
|
||||||
| `seat` | `string` | `seat0` | ID of the Wayland seat to attach to. |
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>JSON</summary>
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"end": [
|
|
||||||
{
|
|
||||||
"type": "keys",
|
|
||||||
"show_scroll": false,
|
|
||||||
"icons": {
|
|
||||||
"caps_on": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>TOML</summary>
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[[end]]
|
|
||||||
type = "keys"
|
|
||||||
show_scroll = false
|
|
||||||
|
|
||||||
[end.icons]
|
|
||||||
caps_on = ""
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>YAML</summary>
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
end:
|
|
||||||
- type: keys
|
|
||||||
show_scroll: false
|
|
||||||
icons:
|
|
||||||
caps_on:
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Corn</summary>
|
|
||||||
|
|
||||||
```corn
|
|
||||||
{
|
|
||||||
end = [
|
|
||||||
{
|
|
||||||
type = "keys"
|
|
||||||
show_scroll = false
|
|
||||||
icons.caps_on = ""
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## Styling
|
|
||||||
|
|
||||||
| Selector | Description |
|
|
||||||
|------------------------|--------------------------------------------|
|
|
||||||
| `.keys` | Keys box container widget. |
|
|
||||||
| `.keys .key` | Individual key indicator container widget. |
|
|
||||||
| `.keys .key.enabled` | Key indicator where key is toggled on. |
|
|
||||||
| `.keys .key.caps` | Capslock key indicator. |
|
|
||||||
| `.keys .key.num` | Num lock key indicator. |
|
|
||||||
| `.keys .key.scroll` | Scroll lock key indicator. |
|
|
||||||
| `.keys .key.image` | Key indicator image icon. |
|
|
||||||
| `.keys .key.text-icon` | Key indicator textual icon. |
|
|
||||||
|
|
||||||
For more information on styling, please see the [styling guide](styling-guide).
|
|
|
@ -64,7 +64,7 @@
|
||||||
++ lib.optionals (hasFeature "tray") [ libdbusmenu-gtk3 ]
|
++ lib.optionals (hasFeature "tray") [ libdbusmenu-gtk3 ]
|
||||||
++ lib.optionals (hasFeature "volume")[ libpulseaudio ]
|
++ lib.optionals (hasFeature "volume")[ libpulseaudio ]
|
||||||
++ lib.optionals (hasFeature "cairo") [ luajit ]
|
++ lib.optionals (hasFeature "cairo") [ luajit ]
|
||||||
++ lib.optionals (hasFeature "keys") [ libinput libevdev ];
|
++ lib.optionals (hasFeature "keyboard") [ libinput libevdev ];
|
||||||
|
|
||||||
propagatedBuildInputs = [ gtk3 ];
|
propagatedBuildInputs = [ gtk3 ];
|
||||||
|
|
||||||
|
|
|
@ -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 crate::{arc_mut, lock, send, spawn_blocking};
|
||||||
use color_eyre::Result;
|
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::dispatch::{Dispatch, DispatchType, WorkspaceIdentifierWithSpecial};
|
||||||
use hyprland::event_listener::EventListener;
|
use hyprland::event_listener::EventListener;
|
||||||
use hyprland::prelude::*;
|
use hyprland::prelude::*;
|
||||||
|
@ -13,15 +17,21 @@ use tracing::{debug, error, info};
|
||||||
pub struct Client {
|
pub struct Client {
|
||||||
workspace_tx: Sender<WorkspaceUpdate>,
|
workspace_tx: Sender<WorkspaceUpdate>,
|
||||||
_workspace_rx: Receiver<WorkspaceUpdate>,
|
_workspace_rx: Receiver<WorkspaceUpdate>,
|
||||||
|
|
||||||
|
keyboard_layout_tx: Sender<KeyboardLayoutUpdate>,
|
||||||
|
_keyboard_layout_rx: Receiver<KeyboardLayoutUpdate>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
pub(crate) fn new() -> Self {
|
pub(crate) fn new() -> Self {
|
||||||
let (workspace_tx, workspace_rx) = channel(16);
|
let (workspace_tx, workspace_rx) = channel(16);
|
||||||
|
let (keyboard_layout_tx, keyboard_layout_rx) = channel(4);
|
||||||
|
|
||||||
let instance = Self {
|
let instance = Self {
|
||||||
workspace_tx,
|
workspace_tx,
|
||||||
_workspace_rx: workspace_rx,
|
_workspace_rx: workspace_rx,
|
||||||
|
keyboard_layout_tx,
|
||||||
|
_keyboard_layout_rx: keyboard_layout_rx,
|
||||||
};
|
};
|
||||||
|
|
||||||
instance.listen_workspace_events();
|
instance.listen_workspace_events();
|
||||||
|
@ -32,6 +42,7 @@ impl Client {
|
||||||
info!("Starting Hyprland event listener");
|
info!("Starting Hyprland event listener");
|
||||||
|
|
||||||
let tx = self.workspace_tx.clone();
|
let tx = self.workspace_tx.clone();
|
||||||
|
let keyboard_layout_tx = self.keyboard_layout_tx.clone();
|
||||||
|
|
||||||
spawn_blocking(move || {
|
spawn_blocking(move || {
|
||||||
let mut event_listener = EventListener::new();
|
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| {
|
event_listener.add_urgent_state_handler(move |address| {
|
||||||
let _lock = lock!(lock);
|
let _lock = lock!(lock);
|
||||||
debug!("Received urgent state: {address:?}");
|
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
|
event_listener
|
||||||
.start_listener()
|
.start_listener()
|
||||||
.expect("Failed to start listener");
|
.expect("Failed to start listener");
|
||||||
|
@ -264,36 +327,73 @@ impl Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WorkspaceClient for Client {
|
impl WorkspaceClient for Client {
|
||||||
fn focus(&self, id: String) -> Result<()> {
|
fn focus(&self, id: String) {
|
||||||
let identifier = id.parse::<i32>().map_or_else(
|
let identifier = id.parse::<i32>().map_or_else(
|
||||||
|_| WorkspaceIdentifierWithSpecial::Name(&id),
|
|_| WorkspaceIdentifierWithSpecial::Name(&id),
|
||||||
WorkspaceIdentifierWithSpecial::Id,
|
WorkspaceIdentifierWithSpecial::Id,
|
||||||
);
|
);
|
||||||
|
|
||||||
Dispatch::call(DispatchType::Workspace(identifier))?;
|
if let Err(e) = Dispatch::call(DispatchType::Workspace(identifier)) {
|
||||||
Ok(())
|
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 rx = self.workspace_tx.subscribe();
|
||||||
|
|
||||||
{
|
let active_id = HWorkspace::get_active().ok().map(|active| active.name);
|
||||||
let tx = self.workspace_tx.clone();
|
let is_visible = create_is_visible();
|
||||||
|
|
||||||
let active_id = HWorkspace::get_active().ok().map(|active| active.name);
|
let workspaces = Workspaces::get()
|
||||||
let is_visible = create_is_visible();
|
.expect("Failed to get workspaces")
|
||||||
|
.into_iter()
|
||||||
|
.map(|w| {
|
||||||
|
let vis = Visibility::from((&w, active_id.as_deref(), &is_visible));
|
||||||
|
|
||||||
let workspaces = Workspaces::get()
|
Workspace::from((vis, w))
|
||||||
.expect("Failed to get workspaces")
|
})
|
||||||
.into_iter()
|
.collect();
|
||||||
.map(|w| {
|
|
||||||
let vis = Visibility::from((&w, active_id.as_deref(), &is_visible));
|
|
||||||
|
|
||||||
Workspace::from((vis, w))
|
send!(self.workspace_tx, WorkspaceUpdate::Init(workspaces));
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
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
|
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
|
/// Creates a new instance of
|
||||||
/// the workspace client for the current compositor.
|
/// the workspace client for the current compositor.
|
||||||
pub fn create_workspace_client(
|
pub fn create_workspace_client(
|
||||||
|
@ -67,7 +87,9 @@ impl Compositor {
|
||||||
.sway()
|
.sway()
|
||||||
.map(|client| client as Arc<dyn WorkspaceClient + Send + Sync>),
|
.map(|client| client as Arc<dyn WorkspaceClient + Send + Sync>),
|
||||||
#[cfg(feature = "workspaces+hyprland")]
|
#[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")
|
Self::Unsupported => Err(Report::msg("Unsupported compositor")
|
||||||
.note("Currently workspaces are only supported by Sway and Hyprland")),
|
.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)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum WorkspaceUpdate {
|
pub enum WorkspaceUpdate {
|
||||||
/// Provides an initial list of workspaces.
|
/// Provides an initial list of workspaces.
|
||||||
|
@ -146,10 +171,20 @@ pub enum WorkspaceUpdate {
|
||||||
|
|
||||||
pub trait WorkspaceClient: Debug + Send + Sync {
|
pub trait WorkspaceClient: Debug + Send + Sync {
|
||||||
/// Requests the workspace with this name is focused.
|
/// 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.
|
/// 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);
|
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 super::{
|
||||||
use crate::{await_sync, send};
|
KeyboardLayoutClient, KeyboardLayoutUpdate, Visibility, Workspace, WorkspaceClient,
|
||||||
use color_eyre::Result;
|
WorkspaceUpdate,
|
||||||
use swayipc_async::{Node, WorkspaceChange, WorkspaceEvent};
|
};
|
||||||
|
use crate::{await_sync, error, send, spawn};
|
||||||
|
use swayipc_async::{InputChange, InputEvent, Node, WorkspaceChange, WorkspaceEvent};
|
||||||
use tokio::sync::broadcast::{channel, Receiver};
|
use tokio::sync::broadcast::{channel, Receiver};
|
||||||
|
|
||||||
use crate::clients::sway::Client;
|
use crate::clients::sway::Client;
|
||||||
|
|
||||||
impl WorkspaceClient for Client {
|
impl WorkspaceClient for Client {
|
||||||
fn focus(&self, id: String) -> Result<()> {
|
fn focus(&self, id: String) {
|
||||||
await_sync(async move {
|
let client = self.connection().clone();
|
||||||
let mut client = self.connection().lock().await;
|
spawn(async move {
|
||||||
client.run_command(format!("workspace {id}")).await
|
let mut client = client.lock().await;
|
||||||
})?;
|
if let Err(e) = client.run_command(format!("workspace {id}")).await {
|
||||||
Ok(())
|
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 (tx, rx) = channel(16);
|
||||||
|
|
||||||
let client = self.connection().clone();
|
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;
|
pub mod clipboard;
|
||||||
#[cfg(feature = "workspaces")]
|
#[cfg(feature = "workspaces")]
|
||||||
pub mod compositor;
|
pub mod compositor;
|
||||||
#[cfg(feature = "keys")]
|
#[cfg(feature = "keyboard")]
|
||||||
pub mod libinput;
|
pub mod libinput;
|
||||||
#[cfg(feature = "cairo")]
|
#[cfg(feature = "cairo")]
|
||||||
pub mod lua;
|
pub mod lua;
|
||||||
|
@ -38,10 +38,14 @@ pub struct Clients {
|
||||||
workspaces: Option<Arc<dyn compositor::WorkspaceClient>>,
|
workspaces: Option<Arc<dyn compositor::WorkspaceClient>>,
|
||||||
#[cfg(feature = "sway")]
|
#[cfg(feature = "sway")]
|
||||||
sway: Option<Arc<sway::Client>>,
|
sway: Option<Arc<sway::Client>>,
|
||||||
|
#[cfg(feature = "hyprland")]
|
||||||
|
hyprland: Option<Arc<compositor::hyprland::Client>>,
|
||||||
#[cfg(feature = "clipboard")]
|
#[cfg(feature = "clipboard")]
|
||||||
clipboard: Option<Arc<clipboard::Client>>,
|
clipboard: Option<Arc<clipboard::Client>>,
|
||||||
#[cfg(feature = "keys")]
|
#[cfg(feature = "keyboard")]
|
||||||
libinput: HashMap<Box<str>, Arc<libinput::Client>>,
|
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")]
|
#[cfg(feature = "cairo")]
|
||||||
lua: Option<Rc<lua::LuaEngine>>,
|
lua: Option<Rc<lua::LuaEngine>>,
|
||||||
#[cfg(feature = "music")]
|
#[cfg(feature = "music")]
|
||||||
|
@ -93,6 +97,19 @@ impl Clients {
|
||||||
Ok(client)
|
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")]
|
#[cfg(feature = "sway")]
|
||||||
pub fn sway(&mut self) -> ClientResult<sway::Client> {
|
pub fn sway(&mut self) -> ClientResult<sway::Client> {
|
||||||
let client = if let Some(client) = &self.sway {
|
let client = if let Some(client) = &self.sway {
|
||||||
|
@ -107,6 +124,19 @@ impl Clients {
|
||||||
Ok(client)
|
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")]
|
#[cfg(feature = "cairo")]
|
||||||
pub fn lua(&mut self, config_dir: &Path) -> Rc<lua::LuaEngine> {
|
pub fn lua(&mut self, config_dir: &Path) -> Rc<lua::LuaEngine> {
|
||||||
self.lua
|
self.lua
|
||||||
|
@ -114,7 +144,7 @@ impl Clients {
|
||||||
.clone()
|
.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "keys")]
|
#[cfg(feature = "keyboard")]
|
||||||
pub fn libinput(&mut self, seat: &str) -> Arc<libinput::Client> {
|
pub fn libinput(&mut self, seat: &str) -> Arc<libinput::Client> {
|
||||||
self.libinput
|
self.libinput
|
||||||
.entry(seat.into())
|
.entry(seat.into())
|
||||||
|
|
|
@ -11,8 +11,8 @@ use crate::modules::clock::ClockModule;
|
||||||
use crate::modules::custom::CustomModule;
|
use crate::modules::custom::CustomModule;
|
||||||
#[cfg(feature = "focused")]
|
#[cfg(feature = "focused")]
|
||||||
use crate::modules::focused::FocusedModule;
|
use crate::modules::focused::FocusedModule;
|
||||||
#[cfg(feature = "keys")]
|
#[cfg(feature = "keyboard")]
|
||||||
use crate::modules::keys::KeysModule;
|
use crate::modules::keyboard::KeyboardModule;
|
||||||
use crate::modules::label::LabelModule;
|
use crate::modules::label::LabelModule;
|
||||||
#[cfg(feature = "launcher")]
|
#[cfg(feature = "launcher")]
|
||||||
use crate::modules::launcher::LauncherModule;
|
use crate::modules::launcher::LauncherModule;
|
||||||
|
@ -61,8 +61,8 @@ pub enum ModuleConfig {
|
||||||
Custom(Box<CustomModule>),
|
Custom(Box<CustomModule>),
|
||||||
#[cfg(feature = "focused")]
|
#[cfg(feature = "focused")]
|
||||||
Focused(Box<FocusedModule>),
|
Focused(Box<FocusedModule>),
|
||||||
#[cfg(feature = "keys")]
|
#[cfg(feature = "keyboard")]
|
||||||
Keys(Box<KeysModule>),
|
Keyboard(Box<KeyboardModule>),
|
||||||
Label(Box<LabelModule>),
|
Label(Box<LabelModule>),
|
||||||
#[cfg(feature = "launcher")]
|
#[cfg(feature = "launcher")]
|
||||||
Launcher(Box<LauncherModule>),
|
Launcher(Box<LauncherModule>),
|
||||||
|
@ -110,8 +110,8 @@ impl ModuleConfig {
|
||||||
Self::Custom(module) => create!(module),
|
Self::Custom(module) => create!(module),
|
||||||
#[cfg(feature = "focused")]
|
#[cfg(feature = "focused")]
|
||||||
Self::Focused(module) => create!(module),
|
Self::Focused(module) => create!(module),
|
||||||
#[cfg(feature = "keys")]
|
#[cfg(feature = "keyboard")]
|
||||||
Self::Keys(module) => create!(module),
|
Self::Keyboard(module) => create!(module),
|
||||||
Self::Label(module) => create!(module),
|
Self::Label(module) => create!(module),
|
||||||
#[cfg(feature = "launcher")]
|
#[cfg(feature = "launcher")]
|
||||||
Self::Launcher(module) => create!(module),
|
Self::Launcher(module) => create!(module),
|
||||||
|
|
|
@ -31,7 +31,7 @@ pub fn new_icon_button(input: &str, icon_theme: &IconTheme, size: i32) -> Button
|
||||||
button
|
button
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(feature = "music", feature = "keys"))]
|
#[cfg(any(feature = "music", feature = "keyboard"))]
|
||||||
pub struct IconLabel {
|
pub struct IconLabel {
|
||||||
container: gtk::Box,
|
container: gtk::Box,
|
||||||
label: Label,
|
label: Label,
|
||||||
|
@ -41,7 +41,7 @@ pub struct IconLabel {
|
||||||
size: i32,
|
size: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(feature = "music", feature = "keys"))]
|
#[cfg(any(feature = "music", feature = "keyboard"))]
|
||||||
impl IconLabel {
|
impl IconLabel {
|
||||||
pub fn new(input: &str, icon_theme: &IconTheme, size: i32) -> Self {
|
pub fn new(input: &str, icon_theme: &IconTheme, size: i32) -> Self {
|
||||||
let container = gtk::Box::new(Orientation::Horizontal, 0);
|
let container = gtk::Box::new(Orientation::Horizontal, 0);
|
||||||
|
|
|
@ -1,18 +1,23 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use color_eyre::eyre::Report;
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use gtk::prelude::*;
|
use gtk::{prelude::*, Button};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tokio::sync::mpsc;
|
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::clients::libinput::{Event, Key, KeyEvent};
|
||||||
use crate::config::CommonConfig;
|
use crate::config::CommonConfig;
|
||||||
use crate::gtk_helpers::IronbarGtkExt;
|
use crate::gtk_helpers::IronbarGtkExt;
|
||||||
use crate::image::IconLabel;
|
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)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||||
pub struct KeysModule {
|
pub struct KeyboardModule {
|
||||||
/// Whether to show capslock indicator.
|
/// Whether to show capslock indicator.
|
||||||
///
|
///
|
||||||
/// **Default**: `true`
|
/// **Default**: `true`
|
||||||
|
@ -31,6 +36,12 @@ pub struct KeysModule {
|
||||||
#[serde(default = "crate::config::default_true")]
|
#[serde(default = "crate::config::default_true")]
|
||||||
show_scroll: bool,
|
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).
|
/// Size to render the icons at, in pixels (image icons only).
|
||||||
///
|
///
|
||||||
/// **Default** `32`
|
/// **Default** `32`
|
||||||
|
@ -93,6 +104,26 @@ struct Icons {
|
||||||
/// **Default**: `""`
|
/// **Default**: `""`
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
scroll_off: String,
|
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 {
|
impl Default for Icons {
|
||||||
|
@ -104,6 +135,7 @@ impl Default for Icons {
|
||||||
num_off: String::new(),
|
num_off: String::new(),
|
||||||
scroll_on: default_icon_scroll(),
|
scroll_on: default_icon_scroll(),
|
||||||
scroll_off: String::new(),
|
scroll_off: String::new(),
|
||||||
|
layout_map: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -128,17 +160,23 @@ fn default_icon_scroll() -> String {
|
||||||
String::from("")
|
String::from("")
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Module<gtk::Box> for KeysModule {
|
#[derive(Debug, Clone)]
|
||||||
type SendMessage = KeyEvent;
|
pub enum KeyboardUpdate {
|
||||||
|
Key(KeyEvent),
|
||||||
|
Layout(KeyboardLayoutUpdate),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Module<gtk::Box> for KeyboardModule {
|
||||||
|
type SendMessage = KeyboardUpdate;
|
||||||
type ReceiveMessage = ();
|
type ReceiveMessage = ();
|
||||||
|
|
||||||
module_impl!("keys");
|
module_impl!("keyboard");
|
||||||
|
|
||||||
fn spawn_controller(
|
fn spawn_controller(
|
||||||
&self,
|
&self,
|
||||||
_info: &ModuleInfo,
|
_info: &ModuleInfo,
|
||||||
context: &WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
context: &WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||||
_rx: mpsc::Receiver<Self::ReceiveMessage>,
|
mut rx: mpsc::Receiver<Self::ReceiveMessage>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let client = context.ironbar.clients.borrow_mut().libinput(&self.seat);
|
let client = context.ironbar.clients.borrow_mut().libinput(&self.seat);
|
||||||
|
|
||||||
|
@ -149,22 +187,47 @@ impl Module<gtk::Box> for KeysModule {
|
||||||
match ev {
|
match ev {
|
||||||
Event::Device => {
|
Event::Device => {
|
||||||
for key in [Key::Caps, Key::Num, Key::Scroll] {
|
for key in [Key::Caps, Key::Num, Key::Scroll] {
|
||||||
module_update!(
|
let event = KeyEvent {
|
||||||
tx,
|
key,
|
||||||
KeyEvent {
|
state: client.get_state(key),
|
||||||
key: Key::Caps,
|
};
|
||||||
state: client.get_state(key)
|
module_update!(tx, KeyboardUpdate::Key(event));
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::Key(ev) => {
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -173,12 +236,16 @@ impl Module<gtk::Box> for KeysModule {
|
||||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||||
info: &ModuleInfo,
|
info: &ModuleInfo,
|
||||||
) -> Result<ModuleParts<gtk::Box>> {
|
) -> 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 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 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 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 {
|
if self.show_caps {
|
||||||
caps.add_class("key");
|
caps.add_class("key");
|
||||||
caps.add_class("caps");
|
caps.add_class("caps");
|
||||||
|
@ -197,31 +264,49 @@ impl Module<gtk::Box> for KeysModule {
|
||||||
container.add(&*scroll);
|
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 icons = self.icons;
|
||||||
let handle_event = move |ev: KeyEvent| {
|
let handle_event = move |ev: KeyboardUpdate| match ev {
|
||||||
let parts = match (ev.key, ev.state) {
|
KeyboardUpdate::Key(ev) => {
|
||||||
(Key::Caps, true) if self.show_caps => Some((&caps, icons.caps_on.as_str())),
|
let parts = match (ev.key, ev.state) {
|
||||||
(Key::Caps, false) if self.show_caps => Some((&caps, icons.caps_off.as_str())),
|
(Key::Caps, true) if self.show_caps => Some((&caps, icons.caps_on.as_str())),
|
||||||
(Key::Num, true) if self.show_num => Some((&num, icons.num_on.as_str())),
|
(Key::Caps, false) if self.show_caps => Some((&caps, icons.caps_off.as_str())),
|
||||||
(Key::Num, false) if self.show_num => Some((&num, icons.num_off.as_str())),
|
(Key::Num, true) if self.show_num => Some((&num, icons.num_on.as_str())),
|
||||||
(Key::Scroll, true) if self.show_scroll => {
|
(Key::Num, false) if self.show_num => Some((&num, icons.num_off.as_str())),
|
||||||
Some((&scroll, icons.scroll_on.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()))
|
(Key::Scroll, false) if self.show_scroll => {
|
||||||
}
|
Some((&scroll, icons.scroll_off.as_str()))
|
||||||
_ => None,
|
}
|
||||||
};
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
if let Some((label, input)) = parts {
|
if let Some((label, input)) = parts {
|
||||||
label.set_label(Some(input));
|
label.set_label(Some(input));
|
||||||
|
|
||||||
if ev.state {
|
if ev.state {
|
||||||
label.add_class("enabled");
|
label.add_class("enabled");
|
||||||
} else {
|
} else {
|
||||||
label.remove_class("enabled");
|
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);
|
glib_recv!(context.subscribe(), handle_event);
|
|
@ -31,8 +31,8 @@ pub mod clock;
|
||||||
pub mod custom;
|
pub mod custom;
|
||||||
#[cfg(feature = "focused")]
|
#[cfg(feature = "focused")]
|
||||||
pub mod focused;
|
pub mod focused;
|
||||||
#[cfg(feature = "keys")]
|
#[cfg(feature = "keyboard")]
|
||||||
pub mod keys;
|
pub mod keyboard;
|
||||||
pub mod label;
|
pub mod label;
|
||||||
#[cfg(feature = "launcher")]
|
#[cfg(feature = "launcher")]
|
||||||
pub mod launcher;
|
pub mod launcher;
|
||||||
|
|
|
@ -196,7 +196,7 @@ impl Module<gtk::Box> for WorkspacesModule {
|
||||||
let client = context.ironbar.clients.borrow_mut().workspaces()?;
|
let client = context.ironbar.clients.borrow_mut().workspaces()?;
|
||||||
// Subscribe & send events
|
// Subscribe & send events
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
let mut srx = client.subscribe_workspace_change();
|
let mut srx = client.subscribe();
|
||||||
|
|
||||||
trace!("Set up workspace subscription");
|
trace!("Set up workspace subscription");
|
||||||
|
|
||||||
|
@ -213,9 +213,7 @@ impl Module<gtk::Box> for WorkspacesModule {
|
||||||
trace!("Setting up UI event handler");
|
trace!("Setting up UI event handler");
|
||||||
|
|
||||||
while let Some(name) = rx.recv().await {
|
while let Some(name) = rx.recv().await {
|
||||||
if let Err(e) = client.focus(name.clone()) {
|
client.focus(name.clone());
|
||||||
warn!("Couldn't focus workspace '{name}': {e:#}");
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok::<(), Report>(())
|
Ok::<(), Report>(())
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue