mirror of
https://github.com/Zedfrigg/ironbar.git
synced 2025-08-16 22:31:03 +02:00
Merge pull request #783 from JakeStanger/feat/libinput
feat: libinput `keys` module
This commit is contained in:
commit
67426bd8e2
20 changed files with 799 additions and 107 deletions
1
.github/scripts/ubuntu_setup.sh
vendored
1
.github/scripts/ubuntu_setup.sh
vendored
|
@ -17,6 +17,7 @@ $SUDO apt-get update && $SUDO apt-get install --assume-yes \
|
|||
libssl-dev${CROSS_DEB_ARCH:+:$CROSS_DEB_ARCH} \
|
||||
libgtk-3-dev${CROSS_DEB_ARCH:+:$CROSS_DEB_ARCH} \
|
||||
libgtk-layer-shell-dev${CROSS_DEB_ARCH:+:$CROSS_DEB_ARCH} \
|
||||
libinput-dev${CROSS_DEB_ARCH:+:$CROSS_DEB_ARCH} \
|
||||
libdbusmenu-gtk3-dev${CROSS_DEB_ARCH:+:$CROSS_DEB_ARCH} \
|
||||
libpulse-dev${CROSS_DEB_ARCH:+:$CROSS_DEB_ARCH} \
|
||||
libluajit-5.1-dev${CROSS_DEB_ARCH:+:$CROSS_DEB_ARCH}
|
||||
|
|
78
Cargo.lock
generated
78
Cargo.lock
generated
|
@ -859,6 +859,29 @@ dependencies = [
|
|||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "evdev-rs"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9812d5790fb6fcce449333eb6713dad335e8c979225ed98755c84a3987e06dba"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"evdev-sys",
|
||||
"libc",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "evdev-sys"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14ead42b547b15d47089c1243d907bcf0eb94e457046d3b315a26ac9c9e9ea6d"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
version = "2.5.3"
|
||||
|
@ -1648,6 +1671,25 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "input"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbdc09524a91f9cacd26f16734ff63d7dc650daffadd2b6f84d17a285bd875a9"
|
||||
dependencies = [
|
||||
"bitflags 2.4.0",
|
||||
"input-sys",
|
||||
"libc",
|
||||
"log",
|
||||
"udev",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "input-sys"
|
||||
version = "1.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd4f5b4d1c00331c5245163aacfe5f20be75b564c7112d45893d4ae038119eb0"
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.12"
|
||||
|
@ -1685,6 +1727,7 @@ dependencies = [
|
|||
"color-eyre",
|
||||
"ctrlc",
|
||||
"dirs",
|
||||
"evdev-rs",
|
||||
"futures-lite 2.5.0",
|
||||
"futures-signals",
|
||||
"futures-util",
|
||||
|
@ -1693,6 +1736,8 @@ dependencies = [
|
|||
"gtk-layer-shell",
|
||||
"hyprland",
|
||||
"indexmap",
|
||||
"input",
|
||||
"libc",
|
||||
"libpulse-binding",
|
||||
"lua-src",
|
||||
"mlua",
|
||||
|
@ -1773,9 +1818,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
|||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.155"
|
||||
version = "0.2.164"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
|
||||
checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f"
|
||||
|
||||
[[package]]
|
||||
name = "libcorn"
|
||||
|
@ -1837,6 +1882,16 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libudev-sys"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.3.8"
|
||||
|
@ -1851,12 +1906,9 @@ checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456"
|
|||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.17"
|
||||
version = "0.4.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
||||
|
||||
[[package]]
|
||||
name = "lua-src"
|
||||
|
@ -3586,6 +3638,18 @@ version = "0.1.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81"
|
||||
|
||||
[[package]]
|
||||
name = "udev"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3d5c197b95f1769931c89f85c33c407801d1fb7a311113bc0b39ad036f1bd81"
|
||||
dependencies = [
|
||||
"io-lifetimes",
|
||||
"libc",
|
||||
"libudev-sys",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uds_windows"
|
||||
version = "1.0.2"
|
||||
|
|
14
Cargo.toml
14
Cargo.toml
|
@ -18,6 +18,7 @@ default = [
|
|||
"focused",
|
||||
"http",
|
||||
"ipc",
|
||||
"keys",
|
||||
"launcher",
|
||||
"music+all",
|
||||
"network_manager",
|
||||
|
@ -49,12 +50,14 @@ http = ["dep:reqwest"]
|
|||
|
||||
cairo = ["lua-src", "mlua", "cairo-rs"]
|
||||
|
||||
clipboard = ["nix"]
|
||||
clipboard = ["dep:nix"]
|
||||
|
||||
clock = ["chrono"]
|
||||
|
||||
focused = []
|
||||
|
||||
keys = ["dep:input", "dep:evdev-rs", "dep:libc", "dep:nix"]
|
||||
|
||||
launcher = []
|
||||
|
||||
music = ["regex"]
|
||||
|
@ -131,12 +134,14 @@ lua-src = { version = "547.0.0", optional = true }
|
|||
mlua = { version = "0.9.9", optional = true, features = ["luajit"] }
|
||||
cairo-rs = { version = "0.18.5", optional = true, features = ["png"] }
|
||||
|
||||
# clipboard
|
||||
nix = { version = "0.29.0", optional = true, features = ["event", "fs"] }
|
||||
|
||||
# clock
|
||||
chrono = { version = "0.4.39", optional = true, default-features = false, features = ["clock", "unstable-locales"] }
|
||||
|
||||
# keys
|
||||
input = { version = "0.9.1", optional = true }
|
||||
evdev-rs = { version = "0.6.1", optional = true }
|
||||
libc = { version = "0.2.164", optional = true }
|
||||
|
||||
# music
|
||||
mpd-utils = { version = "0.2.1", optional = true }
|
||||
mpris = { version = "2.0.1", optional = true }
|
||||
|
@ -163,6 +168,7 @@ futures-util = { version = "0.3.31", optional = true }
|
|||
|
||||
# shared
|
||||
futures-lite = { version = "2.5.0", optional = true } # network_manager, upower, workspaces
|
||||
nix = { version = "0.29.0", optional = true, features = ["event", "fs", "poll"] } # clipboard, input
|
||||
regex = { version = "1.11.1", default-features = false, features = [
|
||||
"std",
|
||||
], optional = true } # music, sys_info
|
||||
|
|
|
@ -26,6 +26,8 @@ pacman -S openssl
|
|||
pacman -S libdbusmenu-gtk3
|
||||
# for volume support
|
||||
pacman -S libpulse
|
||||
# for keys support
|
||||
pacman -S libinput
|
||||
# for lua/cairo support
|
||||
pacman -S luajit lua51-lgi
|
||||
```
|
||||
|
@ -40,6 +42,8 @@ apt install libssl-dev
|
|||
apt install libdbusmenu-gtk3-dev
|
||||
# for volume support
|
||||
apt install libpulse-dev
|
||||
# for keys support
|
||||
apt install libinput-dev
|
||||
# for lua/cairo support
|
||||
apt install luajit-dev lua-lgi
|
||||
```
|
||||
|
@ -54,6 +58,8 @@ dnf install openssl-devel
|
|||
dnf install libdbusmenu-gtk3-devel
|
||||
# for volume support
|
||||
dnf install pulseaudio-libs-devel
|
||||
# for keys support
|
||||
dnf install libinput-devel
|
||||
# for lua/cairo support
|
||||
dnf install luajit-devel lua-lgi
|
||||
```
|
||||
|
|
102
docs/modules/Keys.md
Normal file
102
docs/modules/Keys.md
Normal file
|
@ -0,0 +1,102 @@
|
|||
> [!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).
|
|
@ -33,8 +33,6 @@ in MPRIS mode, the widget will listen to all players and automatically detect/di
|
|||
| `host` | `string` | `localhost:6600` | [MPD Only] TCP or Unix socket for the MPD server. |
|
||||
| `music_dir` | `string` | `$HOME/Music` | [MPD Only] Path to MPD server's music directory on disc. Required for album art. |
|
||||
|
||||
See [here](images) for information on images.
|
||||
|
||||
<details>
|
||||
<summary>JSON</summary>
|
||||
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
let {
|
||||
$config_dir = "/home/jake/.config/ironbar"
|
||||
|
||||
$workspaces = { type = "workspaces" }
|
||||
$launcher = { type = "launcher" }
|
||||
$volume = {
|
||||
type = "volume"
|
||||
format = "{icon} {percentage}%"
|
||||
max_volume = 100
|
||||
icons.volume_high = ""
|
||||
icons.volume_medium = ""
|
||||
icons.volume_low = ""
|
||||
icons.muted = ""
|
||||
}
|
||||
$network_manager = { type = "networkmanager" }
|
||||
$clock = {
|
||||
type = "clock"
|
||||
// disable_popup = true
|
||||
// format = "<span color='#f2777a'>%d/%m/%Y</span> <span color='#69c'>%H:%M:%S</span>"
|
||||
}
|
||||
$tray = { type = "tray" prefer_theme_icons = false }
|
||||
|
||||
// $label = { type = "label" label = "<span color='#color'>hello</span>" }
|
||||
$label = { type = "label" label = "#random" }
|
||||
$clipboard = { type = "clipboard" }
|
||||
|
||||
$notifications = {
|
||||
type = "notifications"
|
||||
show_count = true
|
||||
|
||||
icons.closed_none = ""
|
||||
icons.closed_some = ""
|
||||
icons.closed_dnd = ""
|
||||
icons.open_none = ""
|
||||
icons.open_some = ""
|
||||
icons.open_dnd = ""
|
||||
}
|
||||
|
||||
$focused = { type = "focused" }
|
||||
|
||||
$cairo = { type = "cairo" path = "$config_dir/clock.lua" frequency = 50 width = 300 height = 300 }
|
||||
|
||||
$custom = {
|
||||
type = "custom"
|
||||
bar = [ { type = "button" on_click = "popup:toggle" widgets = [ $focused ] } ]
|
||||
popup = [ { type = "box" orientation = "v" widgets = [ $clock $cairo ] } ]
|
||||
}
|
||||
|
||||
$mpris = { type = "music" }
|
||||
} in {
|
||||
// ironvar_defaults.color = "red"
|
||||
|
||||
position = "bottom"
|
||||
|
||||
icon_theme = "Paper"
|
||||
|
||||
start = [ $workspaces $label ]
|
||||
center = [ $custom ]
|
||||
end = [ $notifications $clock ]
|
||||
}
|
|
@ -130,6 +130,8 @@
|
|||
libxkbcommon
|
||||
libdbusmenu-gtk3
|
||||
libpulseaudio
|
||||
libinput
|
||||
libevdev
|
||||
luajit
|
||||
luajitPackages.lgi
|
||||
];
|
||||
|
|
|
@ -14,6 +14,8 @@
|
|||
libxkbcommon,
|
||||
libdbusmenu-gtk3,
|
||||
libpulseaudio,
|
||||
libinput,
|
||||
libevdev,
|
||||
openssl,
|
||||
luajit,
|
||||
luajitPackages,
|
||||
|
@ -59,7 +61,8 @@
|
|||
++ lib.optionals (hasFeature "http") [ openssl ]
|
||||
++ lib.optionals (hasFeature "tray") [ libdbusmenu-gtk3 ]
|
||||
++ lib.optionals (hasFeature "volume")[ libpulseaudio ]
|
||||
++ lib.optionals (hasFeature "cairo") [ luajit ];
|
||||
++ lib.optionals (hasFeature "cairo") [ luajit ]
|
||||
++ lib.optionals (hasFeature "keys") [ libinput libevdev ];
|
||||
|
||||
propagatedBuildInputs = [ gtk3 ];
|
||||
|
||||
|
|
|
@ -11,6 +11,8 @@ pkgs.mkShell {
|
|||
openssl
|
||||
libdbusmenu-gtk3
|
||||
libpulseaudio
|
||||
libinput
|
||||
libevdev
|
||||
luajit
|
||||
luajitPackages.lgi
|
||||
];
|
||||
|
|
235
src/clients/libinput.rs
Normal file
235
src/clients/libinput.rs
Normal file
|
@ -0,0 +1,235 @@
|
|||
use crate::{arc_rw, read_lock, send, spawn, spawn_blocking, write_lock};
|
||||
use color_eyre::{Report, Result};
|
||||
use evdev_rs::enums::{int_to_ev_key, EventCode, EV_KEY, EV_LED};
|
||||
use evdev_rs::DeviceWrapper;
|
||||
use input::event::keyboard::{KeyState, KeyboardEventTrait};
|
||||
use input::event::{DeviceEvent, EventTrait, KeyboardEvent};
|
||||
use input::{DeviceCapability, Libinput, LibinputInterface};
|
||||
use libc::{O_ACCMODE, O_RDONLY, O_RDWR};
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::os::unix::{fs::OpenOptionsExt, io::OwnedFd};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::time::Duration;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::time::sleep;
|
||||
use tracing::{debug, error};
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum Key {
|
||||
Caps,
|
||||
Num,
|
||||
Scroll,
|
||||
}
|
||||
|
||||
impl From<Key> for EV_KEY {
|
||||
fn from(value: Key) -> Self {
|
||||
match value {
|
||||
Key::Caps => Self::KEY_CAPSLOCK,
|
||||
Key::Num => Self::KEY_NUMLOCK,
|
||||
Key::Scroll => Self::KEY_SCROLLLOCK,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<EV_KEY> for Key {
|
||||
type Error = Report;
|
||||
|
||||
fn try_from(value: EV_KEY) -> std::result::Result<Self, Self::Error> {
|
||||
match value {
|
||||
EV_KEY::KEY_CAPSLOCK => Ok(Key::Caps),
|
||||
EV_KEY::KEY_NUMLOCK => Ok(Key::Num),
|
||||
EV_KEY::KEY_SCROLLLOCK => Ok(Key::Scroll),
|
||||
_ => Err(Report::msg("provided key is not supported toggle key")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Key {
|
||||
fn get_state<P: AsRef<Path>>(self, device_path: P) -> Result<bool> {
|
||||
let device = evdev_rs::Device::new_from_path(device_path)?;
|
||||
|
||||
match self {
|
||||
Self::Caps => device.event_value(&EventCode::EV_LED(EV_LED::LED_CAPSL)),
|
||||
Self::Num => device.event_value(&EventCode::EV_LED(EV_LED::LED_NUML)),
|
||||
Self::Scroll => device.event_value(&EventCode::EV_LED(EV_LED::LED_SCROLLL)),
|
||||
}
|
||||
.map(|v| v > 0)
|
||||
.ok_or_else(|| Report::msg("failed to get key status"))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct KeyEvent {
|
||||
pub key: Key,
|
||||
pub state: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum Event {
|
||||
Device,
|
||||
Key(KeyEvent),
|
||||
}
|
||||
|
||||
struct KeyData<P: AsRef<Path>> {
|
||||
device_path: P,
|
||||
key: EV_KEY,
|
||||
}
|
||||
|
||||
impl<P: AsRef<Path>> TryFrom<KeyData<P>> for Event {
|
||||
type Error = Report;
|
||||
|
||||
fn try_from(data: KeyData<P>) -> Result<Self> {
|
||||
let key = Key::try_from(data.key)?;
|
||||
|
||||
key.get_state(data.device_path)
|
||||
.map(|state| KeyEvent { key, state })
|
||||
.map(Event::Key)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Interface;
|
||||
|
||||
impl LibinputInterface for Interface {
|
||||
fn open_restricted(&mut self, path: &Path, flags: i32) -> Result<OwnedFd, i32> {
|
||||
// No idea what these flags do honestly, just copied them from the example.
|
||||
let op = OpenOptions::new()
|
||||
.custom_flags(flags)
|
||||
.read((flags & O_ACCMODE == O_RDONLY) | (flags & O_ACCMODE == O_RDWR))
|
||||
.open(path)
|
||||
.map(OwnedFd::from);
|
||||
|
||||
if let Err(err) = &op {
|
||||
error!("error opening {}: {err:?}", path.display());
|
||||
}
|
||||
|
||||
op.map_err(|err| err.raw_os_error().unwrap_or(-1))
|
||||
}
|
||||
fn close_restricted(&mut self, fd: OwnedFd) {
|
||||
drop(File::from(fd));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Client {
|
||||
tx: broadcast::Sender<Event>,
|
||||
_rx: broadcast::Receiver<Event>,
|
||||
|
||||
seat: String,
|
||||
known_devices: Arc<RwLock<Vec<PathBuf>>>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn init(seat: String) -> Arc<Self> {
|
||||
let client = Arc::new(Self::new(seat));
|
||||
{
|
||||
let client = client.clone();
|
||||
spawn_blocking(move || {
|
||||
if let Err(err) = client.run() {
|
||||
error!("{err:?}");
|
||||
}
|
||||
});
|
||||
}
|
||||
client
|
||||
}
|
||||
|
||||
fn new(seat: String) -> Self {
|
||||
let (tx, rx) = broadcast::channel(4);
|
||||
|
||||
Self {
|
||||
tx,
|
||||
_rx: rx,
|
||||
seat,
|
||||
known_devices: arc_rw!(vec![]),
|
||||
}
|
||||
}
|
||||
|
||||
fn run(&self) -> Result<()> {
|
||||
let mut input = Libinput::new_with_udev(Interface);
|
||||
input
|
||||
.udev_assign_seat(&self.seat)
|
||||
.map_err(|()| Report::msg("failed to assign seat"))?;
|
||||
|
||||
loop {
|
||||
input.dispatch()?;
|
||||
|
||||
for event in &mut input {
|
||||
match event {
|
||||
input::Event::Keyboard(KeyboardEvent::Key(event))
|
||||
if event.key_state() == KeyState::Released =>
|
||||
{
|
||||
let Some(device) = (unsafe { event.device().udev_device() }) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(
|
||||
key @ (EV_KEY::KEY_CAPSLOCK
|
||||
| EV_KEY::KEY_NUMLOCK
|
||||
| EV_KEY::KEY_SCROLLLOCK),
|
||||
) = int_to_ev_key(event.key())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Some(device_path) = device.devnode().map(PathBuf::from) {
|
||||
let tx = self.tx.clone();
|
||||
|
||||
// need to spawn a task to avoid blocking
|
||||
spawn(async move {
|
||||
// wait for kb to change
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
|
||||
let data = KeyData { device_path, key };
|
||||
|
||||
if let Ok(event) = data.try_into() {
|
||||
send!(tx, event);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
input::Event::Device(DeviceEvent::Added(event)) => {
|
||||
let device = event.device();
|
||||
if !device.has_capability(DeviceCapability::Keyboard) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let name = device.name();
|
||||
let Some(device) = (unsafe { event.device().udev_device() }) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Some(device_path) = device.devnode() {
|
||||
// not all devices which report as keyboards actually are one -
|
||||
// fire test event so we can figure out if it is
|
||||
let caps_event: Result<Event> = KeyData {
|
||||
device_path,
|
||||
key: EV_KEY::KEY_CAPSLOCK,
|
||||
}
|
||||
.try_into();
|
||||
|
||||
if caps_event.is_ok() {
|
||||
debug!("new keyboard device: {name} | {}", device_path.display());
|
||||
write_lock!(self.known_devices).push(device_path.to_path_buf());
|
||||
send!(self.tx, Event::Device);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_state(&self, key: Key) -> bool {
|
||||
read_lock!(self.known_devices)
|
||||
.iter()
|
||||
.map(|device_path| key.get_state(device_path))
|
||||
.filter_map(Result::ok)
|
||||
.reduce(|state, curr| state || curr)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn subscribe(&self) -> broadcast::Receiver<Event> {
|
||||
self.tx.subscribe()
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
use crate::await_sync;
|
||||
use color_eyre::Result;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
@ -8,6 +9,8 @@ use std::sync::Arc;
|
|||
pub mod clipboard;
|
||||
#[cfg(feature = "workspaces")]
|
||||
pub mod compositor;
|
||||
#[cfg(feature = "keys")]
|
||||
pub mod libinput;
|
||||
#[cfg(feature = "cairo")]
|
||||
pub mod lua;
|
||||
#[cfg(feature = "music")]
|
||||
|
@ -37,10 +40,12 @@ pub struct Clients {
|
|||
sway: Option<Arc<sway::Client>>,
|
||||
#[cfg(feature = "clipboard")]
|
||||
clipboard: Option<Arc<clipboard::Client>>,
|
||||
#[cfg(feature = "keys")]
|
||||
libinput: HashMap<Box<str>, Arc<libinput::Client>>,
|
||||
#[cfg(feature = "cairo")]
|
||||
lua: Option<Rc<lua::LuaEngine>>,
|
||||
#[cfg(feature = "music")]
|
||||
music: std::collections::HashMap<music::ClientType, Arc<dyn music::MusicClient>>,
|
||||
music: HashMap<music::ClientType, Arc<dyn music::MusicClient>>,
|
||||
#[cfg(feature = "network_manager")]
|
||||
network_manager: Option<Arc<networkmanager::Client>>,
|
||||
#[cfg(feature = "notifications")]
|
||||
|
@ -111,6 +116,14 @@ impl Clients {
|
|||
.clone()
|
||||
}
|
||||
|
||||
#[cfg(feature = "keys")]
|
||||
pub fn libinput(&mut self, seat: &str) -> Arc<libinput::Client> {
|
||||
self.libinput
|
||||
.entry(seat.into())
|
||||
.or_insert_with(|| libinput::Client::init(seat.to_string()))
|
||||
.clone()
|
||||
}
|
||||
|
||||
#[cfg(feature = "music")]
|
||||
pub fn music(&mut self, client_type: music::ClientType) -> Arc<dyn music::MusicClient> {
|
||||
self.music
|
||||
|
|
|
@ -11,6 +11,8 @@ use crate::modules::clock::ClockModule;
|
|||
use crate::modules::custom::CustomModule;
|
||||
#[cfg(feature = "focused")]
|
||||
use crate::modules::focused::FocusedModule;
|
||||
#[cfg(feature = "keys")]
|
||||
use crate::modules::keys::KeysModule;
|
||||
use crate::modules::label::LabelModule;
|
||||
#[cfg(feature = "launcher")]
|
||||
use crate::modules::launcher::LauncherModule;
|
||||
|
@ -59,6 +61,8 @@ pub enum ModuleConfig {
|
|||
Custom(Box<CustomModule>),
|
||||
#[cfg(feature = "focused")]
|
||||
Focused(Box<FocusedModule>),
|
||||
#[cfg(feature = "keys")]
|
||||
Keys(Box<KeysModule>),
|
||||
Label(Box<LabelModule>),
|
||||
#[cfg(feature = "launcher")]
|
||||
Launcher(Box<LauncherModule>),
|
||||
|
@ -106,6 +110,8 @@ impl ModuleConfig {
|
|||
Self::Custom(module) => create!(module),
|
||||
#[cfg(feature = "focused")]
|
||||
Self::Focused(module) => create!(module),
|
||||
#[cfg(feature = "keys")]
|
||||
Self::Keys(module) => create!(module),
|
||||
Self::Label(module) => create!(module),
|
||||
#[cfg(feature = "launcher")]
|
||||
Self::Launcher(module) => create!(module),
|
||||
|
|
|
@ -20,6 +20,8 @@ pub struct WidgetGeometry {
|
|||
pub trait IronbarGtkExt {
|
||||
/// Adds a new CSS class to the widget.
|
||||
fn add_class(&self, class: &str);
|
||||
/// Removes a CSS class to the widget.
|
||||
fn remove_class(&self, class: &str);
|
||||
/// Gets the geometry for the widget
|
||||
fn geometry(&self, orientation: Orientation) -> WidgetGeometry;
|
||||
|
||||
|
@ -34,6 +36,10 @@ impl<W: IsA<Widget>> IronbarGtkExt for W {
|
|||
self.style_context().add_class(class);
|
||||
}
|
||||
|
||||
fn remove_class(&self, class: &str) {
|
||||
self.style_context().remove_class(class);
|
||||
}
|
||||
|
||||
fn geometry(&self, orientation: Orientation) -> WidgetGeometry {
|
||||
let allocation = self.allocation();
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
use super::ImageProvider;
|
||||
use crate::gtk_helpers::IronbarGtkExt;
|
||||
use crate::gtk_helpers::{IronbarGtkExt, IronbarLabelExt};
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Button, IconTheme, Image, Label, Orientation};
|
||||
use std::ops::Deref;
|
||||
|
||||
#[cfg(any(feature = "music", feature = "workspaces", feature = "clipboard"))]
|
||||
pub fn new_icon_button(input: &str, icon_theme: &IconTheme, size: i32) -> Button {
|
||||
|
@ -30,26 +31,79 @@ pub fn new_icon_button(input: &str, icon_theme: &IconTheme, size: i32) -> Button
|
|||
button
|
||||
}
|
||||
|
||||
#[cfg(feature = "music")]
|
||||
pub fn new_icon_label(input: &str, icon_theme: &IconTheme, size: i32) -> gtk::Box {
|
||||
#[cfg(any(feature = "music", feature = "keys"))]
|
||||
pub struct IconLabel {
|
||||
container: gtk::Box,
|
||||
label: Label,
|
||||
image: Image,
|
||||
|
||||
icon_theme: IconTheme,
|
||||
size: i32,
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "music", feature = "keys"))]
|
||||
impl IconLabel {
|
||||
pub fn new(input: &str, icon_theme: &IconTheme, size: i32) -> Self {
|
||||
let container = gtk::Box::new(Orientation::Horizontal, 0);
|
||||
|
||||
if ImageProvider::is_definitely_image_input(input) {
|
||||
let label = Label::builder().use_markup(true).build();
|
||||
label.add_class("icon");
|
||||
label.add_class("text-icon");
|
||||
|
||||
let image = Image::new();
|
||||
image.add_class("icon");
|
||||
image.add_class("image");
|
||||
|
||||
container.add(&image);
|
||||
container.add(&label);
|
||||
|
||||
if ImageProvider::is_definitely_image_input(input) {
|
||||
ImageProvider::parse(input, icon_theme, false, size)
|
||||
.map(|provider| provider.load_into_image(&image));
|
||||
|
||||
image.show();
|
||||
} else {
|
||||
let label = Label::builder().use_markup(true).label(input).build();
|
||||
label.add_class("icon");
|
||||
label.add_class("text-icon");
|
||||
|
||||
container.add(&label);
|
||||
label.set_text(input);
|
||||
label.show();
|
||||
}
|
||||
|
||||
container
|
||||
Self {
|
||||
container,
|
||||
label,
|
||||
image,
|
||||
icon_theme: icon_theme.clone(),
|
||||
size,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_label(&self, input: Option<&str>) {
|
||||
let label = &self.label;
|
||||
let image = &self.image;
|
||||
|
||||
if let Some(input) = input {
|
||||
if ImageProvider::is_definitely_image_input(input) {
|
||||
ImageProvider::parse(input, &self.icon_theme, false, self.size)
|
||||
.map(|provider| provider.load_into_image(image));
|
||||
|
||||
label.hide();
|
||||
image.show();
|
||||
} else {
|
||||
label.set_label_escaped(input);
|
||||
|
||||
image.hide();
|
||||
label.show();
|
||||
}
|
||||
} else {
|
||||
label.hide();
|
||||
image.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for IconLabel {
|
||||
type Target = gtk::Box;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.container
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ macro_rules! send_async {
|
|||
};
|
||||
}
|
||||
|
||||
/// Sends a message on an synchronous `Sender` using `send()`
|
||||
/// Sends a message on a synchronous `Sender` using `send()`
|
||||
/// Panics if the message cannot be sent.
|
||||
///
|
||||
/// # Usage:
|
||||
|
@ -56,7 +56,7 @@ macro_rules! send {
|
|||
};
|
||||
}
|
||||
|
||||
/// Sends a message on an synchronous `Sender` using `try_send()`
|
||||
/// Sends a message on a synchronous `Sender` using `try_send()`
|
||||
/// Panics if the message cannot be sent.
|
||||
///
|
||||
/// # Usage:
|
||||
|
@ -71,6 +71,26 @@ macro_rules! try_send {
|
|||
};
|
||||
}
|
||||
|
||||
/// Sends a message, wrapped inside a `ModuleUpdateEvent::Update` variant,
|
||||
/// on an asynchronous `Sender` using `send()`.
|
||||
///
|
||||
/// This is a convenience wrapper around `send_async`
|
||||
/// to avoid needing to write the full enum every time.
|
||||
///
|
||||
/// Panics if the message cannot be sent.
|
||||
///
|
||||
/// # Usage:
|
||||
///
|
||||
/// ```rs
|
||||
/// module_update!(tx, "my event");
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! module_update {
|
||||
($tx:expr, $msg:expr) => {
|
||||
send_async!($tx, $crate::modules::ModuleUpdateEvent::Update($msg))
|
||||
};
|
||||
}
|
||||
|
||||
/// Spawns a `GLib` future on the local thread, and calls `rx.recv()`
|
||||
/// in a loop.
|
||||
///
|
||||
|
|
|
@ -205,7 +205,7 @@ impl Ironbar {
|
|||
});
|
||||
|
||||
{
|
||||
let instance = instance2;
|
||||
let instance = instance2.clone();
|
||||
let app = app.clone();
|
||||
|
||||
glib::spawn_future_local(async move {
|
||||
|
|
231
src/modules/keys.rs
Normal file
231
src/modules/keys.rs
Normal file
|
@ -0,0 +1,231 @@
|
|||
use color_eyre::Result;
|
||||
use gtk::prelude::*;
|
||||
use serde::Deserialize;
|
||||
use std::ops::Deref;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use super::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
|
||||
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};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct KeysModule {
|
||||
/// Whether to show capslock indicator.
|
||||
///
|
||||
/// **Default**: `true`
|
||||
#[serde(default = "crate::config::default_true")]
|
||||
show_caps: bool,
|
||||
|
||||
/// Whether to show num lock indicator.
|
||||
///
|
||||
/// **Default**: `true`
|
||||
#[serde(default = "crate::config::default_true")]
|
||||
show_num: bool,
|
||||
|
||||
/// Whether to show scroll lock indicator.
|
||||
///
|
||||
/// **Default**: `true`
|
||||
#[serde(default = "crate::config::default_true")]
|
||||
show_scroll: bool,
|
||||
|
||||
/// Size to render the icons at, in pixels (image icons only).
|
||||
///
|
||||
/// **Default** `32`
|
||||
#[serde(default = "default_icon_size")]
|
||||
icon_size: i32,
|
||||
|
||||
/// Player state icons.
|
||||
///
|
||||
/// See [icons](#icons).
|
||||
#[serde(default)]
|
||||
icons: Icons,
|
||||
|
||||
/// The Wayland seat to attach to.
|
||||
/// You almost certainly do not need to change this.
|
||||
///
|
||||
/// **Default**: `seat0`
|
||||
#[serde(default = "default_seat")]
|
||||
seat: String,
|
||||
|
||||
/// See [common options](module-level-options#common-options).
|
||||
#[serde(flatten)]
|
||||
pub common: Option<CommonConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
struct Icons {
|
||||
/// Icon to show when capslock is enabled.
|
||||
///
|
||||
/// **Default**: ``
|
||||
#[serde(default = "default_icon_caps")]
|
||||
caps_on: String,
|
||||
|
||||
/// Icon to show when capslock is disabled.
|
||||
///
|
||||
/// **Default**: `""`
|
||||
#[serde(default)]
|
||||
caps_off: String,
|
||||
|
||||
/// Icon to show when num lock is enabled.
|
||||
///
|
||||
/// **Default**: ``
|
||||
#[serde(default = "default_icon_num")]
|
||||
num_on: String,
|
||||
|
||||
/// Icon to show when num lock is disabled.
|
||||
///
|
||||
/// **Default**: `""`
|
||||
#[serde(default)]
|
||||
num_off: String,
|
||||
|
||||
/// Icon to show when scroll lock is enabled.
|
||||
///
|
||||
/// **Default**: ``
|
||||
#[serde(default = "default_icon_scroll")]
|
||||
scroll_on: String,
|
||||
|
||||
/// Icon to show when scroll lock is disabled.
|
||||
///
|
||||
/// **Default**: `""`
|
||||
#[serde(default)]
|
||||
scroll_off: String,
|
||||
}
|
||||
|
||||
impl Default for Icons {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
caps_on: default_icon_caps(),
|
||||
caps_off: String::new(),
|
||||
num_on: default_icon_num(),
|
||||
num_off: String::new(),
|
||||
scroll_on: default_icon_scroll(),
|
||||
scroll_off: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fn default_icon_size() -> i32 {
|
||||
32
|
||||
}
|
||||
|
||||
fn default_seat() -> String {
|
||||
String::from("seat0")
|
||||
}
|
||||
|
||||
fn default_icon_caps() -> String {
|
||||
String::from("")
|
||||
}
|
||||
|
||||
fn default_icon_num() -> String {
|
||||
String::from("")
|
||||
}
|
||||
|
||||
fn default_icon_scroll() -> String {
|
||||
String::from("")
|
||||
}
|
||||
|
||||
impl Module<gtk::Box> for KeysModule {
|
||||
type SendMessage = KeyEvent;
|
||||
type ReceiveMessage = ();
|
||||
|
||||
module_impl!("keys");
|
||||
|
||||
fn spawn_controller(
|
||||
&self,
|
||||
_info: &ModuleInfo,
|
||||
context: &WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
_rx: mpsc::Receiver<Self::ReceiveMessage>,
|
||||
) -> Result<()> {
|
||||
let client = context.ironbar.clients.borrow_mut().libinput(&self.seat);
|
||||
|
||||
let tx = context.tx.clone();
|
||||
spawn(async move {
|
||||
let mut rx = client.subscribe();
|
||||
while let Ok(ev) = rx.recv().await {
|
||||
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)
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
Event::Key(ev) => {
|
||||
send_async!(tx, ModuleUpdateEvent::Update(ev));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn into_widget(
|
||||
self,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> Result<ModuleParts<gtk::Box>> {
|
||||
let container = gtk::Box::new(info.bar_position.orientation(), 5);
|
||||
|
||||
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);
|
||||
|
||||
if self.show_caps {
|
||||
caps.add_class("key");
|
||||
caps.add_class("caps");
|
||||
container.add(caps.deref());
|
||||
}
|
||||
|
||||
if self.show_num {
|
||||
num.add_class("key");
|
||||
num.add_class("num");
|
||||
container.add(num.deref());
|
||||
}
|
||||
|
||||
if self.show_scroll {
|
||||
scroll.add_class("key");
|
||||
scroll.add_class("scroll");
|
||||
container.add(scroll.deref());
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
if let Some((label, input)) = parts {
|
||||
label.set_label(Some(input));
|
||||
|
||||
if ev.state {
|
||||
label.add_class("enabled");
|
||||
} else {
|
||||
label.remove_class("enabled");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
glib_recv!(context.subscribe(), handle_event);
|
||||
Ok(ModuleParts::new(container, None))
|
||||
}
|
||||
}
|
|
@ -31,6 +31,8 @@ pub mod clock;
|
|||
pub mod custom;
|
||||
#[cfg(feature = "focused")]
|
||||
pub mod focused;
|
||||
#[cfg(feature = "keys")]
|
||||
pub mod keys;
|
||||
pub mod label;
|
||||
#[cfg(feature = "launcher")]
|
||||
pub mod launcher;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use std::cell::RefMut;
|
||||
use std::ops::Deref;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
@ -17,7 +18,7 @@ use crate::clients::music::{
|
|||
};
|
||||
use crate::clients::Clients;
|
||||
use crate::gtk_helpers::{IronbarGtkExt, IronbarLabelExt};
|
||||
use crate::image::{new_icon_button, new_icon_label, ImageProvider};
|
||||
use crate::image::{new_icon_button, IconLabel, ImageProvider};
|
||||
use crate::modules::PopupButton;
|
||||
use crate::modules::{
|
||||
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, WidgetContext,
|
||||
|
@ -187,8 +188,8 @@ impl Module<Button> for MusicModule {
|
|||
|
||||
button.add(&button_contents);
|
||||
|
||||
let icon_play = new_icon_label(&self.icons.play, info.icon_theme, self.icon_size);
|
||||
let icon_pause = new_icon_label(&self.icons.pause, info.icon_theme, self.icon_size);
|
||||
let icon_play = IconLabel::new(&self.icons.play, info.icon_theme, self.icon_size);
|
||||
let icon_pause = IconLabel::new(&self.icons.pause, info.icon_theme, self.icon_size);
|
||||
|
||||
let label = Label::builder()
|
||||
.use_markup(true)
|
||||
|
@ -199,8 +200,8 @@ impl Module<Button> for MusicModule {
|
|||
label.truncate(truncate);
|
||||
}
|
||||
|
||||
button_contents.add(&icon_pause);
|
||||
button_contents.add(&icon_play);
|
||||
button_contents.add(icon_pause.deref());
|
||||
button_contents.add(icon_play.deref());
|
||||
button_contents.add(&label);
|
||||
|
||||
{
|
||||
|
@ -282,9 +283,9 @@ impl Module<Button> for MusicModule {
|
|||
let icons = self.icons;
|
||||
|
||||
let info_box = gtk::Box::new(Orientation::Vertical, 10);
|
||||
let title_label = IconLabel::new(&icons.track, None, icon_theme);
|
||||
let album_label = IconLabel::new(&icons.album, None, icon_theme);
|
||||
let artist_label = IconLabel::new(&icons.artist, None, icon_theme);
|
||||
let title_label = IconPrefixedLabel::new(&icons.track, None, icon_theme);
|
||||
let album_label = IconPrefixedLabel::new(&icons.album, None, icon_theme);
|
||||
let artist_label = IconPrefixedLabel::new(&icons.artist, None, icon_theme);
|
||||
|
||||
title_label.container.add_class("title");
|
||||
album_label.container.add_class("album");
|
||||
|
@ -323,11 +324,11 @@ impl Module<Button> for MusicModule {
|
|||
volume_slider.set_inverted(true);
|
||||
volume_slider.add_class("slider");
|
||||
|
||||
let volume_icon = new_icon_label(&icons.volume, icon_theme, self.icon_size);
|
||||
let volume_icon = IconLabel::new(&icons.volume, icon_theme, self.icon_size);
|
||||
volume_icon.add_class("icon");
|
||||
|
||||
volume_box.pack_start(&volume_slider, true, true, 0);
|
||||
volume_box.pack_end(&volume_icon, false, false, 0);
|
||||
volume_box.pack_end(volume_icon.deref(), false, false, 0);
|
||||
|
||||
main_container.add(&album_image);
|
||||
main_container.add(&info_box);
|
||||
|
@ -496,7 +497,7 @@ impl Module<Button> for MusicModule {
|
|||
}
|
||||
}
|
||||
|
||||
fn update_popup_metadata_label(text: Option<String>, label: &IconLabel) {
|
||||
fn update_popup_metadata_label(text: Option<String>, label: &IconPrefixedLabel) {
|
||||
match text {
|
||||
Some(value) => {
|
||||
label.label.set_label_escaped(&value);
|
||||
|
@ -536,16 +537,16 @@ fn get_token_value(song: &Track, token: &str) -> String {
|
|||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct IconLabel {
|
||||
struct IconPrefixedLabel {
|
||||
label: Label,
|
||||
container: gtk::Box,
|
||||
}
|
||||
|
||||
impl IconLabel {
|
||||
impl IconPrefixedLabel {
|
||||
fn new(icon_input: &str, label: Option<&str>, icon_theme: &IconTheme) -> Self {
|
||||
let container = gtk::Box::new(Orientation::Horizontal, 5);
|
||||
|
||||
let icon = new_icon_label(icon_input, icon_theme, 24);
|
||||
let icon = IconLabel::new(icon_input, icon_theme, 24);
|
||||
|
||||
let mut builder = Label::builder().use_markup(true);
|
||||
|
||||
|
@ -558,7 +559,7 @@ impl IconLabel {
|
|||
icon.add_class("icon-box");
|
||||
label.add_class("label");
|
||||
|
||||
container.add(&icon);
|
||||
container.add(icon.deref());
|
||||
container.add(&label);
|
||||
|
||||
Self { label, container }
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue