Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b04642148 | ||
|
|
0a331f3138 | ||
|
|
bc625b929b | ||
|
|
ad77dc4e4c | ||
|
|
3a83bd31ab | ||
|
|
5ebc84c7b9 | ||
|
|
51d1cd4a16 | ||
|
|
b7792a415e | ||
|
|
9f82ba58cd | ||
|
|
a93700e8fd | ||
|
|
2a3fe33446 | ||
|
|
3750124d8c | ||
|
|
e693c1c166 | ||
|
|
cbd0c49e25 | ||
|
|
e23e691bc6 | ||
|
|
be0f4c6366 | ||
|
|
493df6bb49 | ||
|
|
b4ac1c9850 | ||
|
|
27f6abad67 | ||
|
|
ec1d59677b | ||
|
|
70e1b526a9 | ||
|
|
3c43c20c6a | ||
|
|
b66bd788b2 | ||
|
|
f17ae7a415 | ||
|
|
a06c4bccca | ||
|
|
e4e72d8008 | ||
|
|
9e6dbbd131 | ||
|
|
91c57edc73 | ||
|
|
dec402edd9 | ||
|
|
fad90fdad6 | ||
|
|
35ce3b4d45 | ||
|
|
27d04795af | ||
|
|
9d9c275313 |
34
CHANGELOG.md
34
CHANGELOG.md
@@ -4,6 +4,37 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [v0.6.0] - 2022-10-15
|
||||||
|
### :sparkles: New Features
|
||||||
|
- [`b188bc7`](https://github.com/JakeStanger/ironbar/commit/b188bc714614406935d8bb88a719adab2dfce32f) - initial support for running outside sway *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||||
|
- [`324f00c`](https://github.com/JakeStanger/ironbar/commit/324f00cdf9200e3e3ecedfa68ab4c99b170242e2) - wlroots-agnostic support for `focused` module *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||||
|
- [`b1c66b9`](https://github.com/JakeStanger/ironbar/commit/b1c66b9117cf8a10350cdb857a5267a1a72ad914) - wlroots-agnostic support for `launcher` module *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||||
|
- [`1dd0a9e`](https://github.com/JakeStanger/ironbar/commit/1dd0a9e52f69e672d9ac313c1da0e201c911e6c2) - **launcher**: add popup css selectors *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||||
|
- [`06cfad6`](https://github.com/JakeStanger/ironbar/commit/06cfad62e228f7fc63938f2280206450005cb064) - more positioning options *(PR [#23](https://github.com/JakeStanger/ironbar/pull/23) by [@JakeStanger](https://github.com/JakeStanger))*
|
||||||
|
|
||||||
|
### :bug: Bug Fixes
|
||||||
|
- [`5523e9a`](https://github.com/JakeStanger/ironbar/commit/5523e9af46e457f9d45902debaaacf26b586e457) - **popup**: often opening in wrong place *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||||
|
- [`8536ad7`](https://github.com/JakeStanger/ironbar/commit/8536ad719a92aec4166e35b75cb029075ad3ae34) - **mpd**: incorrectly checking for unix sockets *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||||
|
- [`bd5bdf5`](https://github.com/JakeStanger/ironbar/commit/bd5bdf5af548304958663d593fccb454afa6c8ff) - logging for creating bar incorrect *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||||
|
- [`75339f0`](https://github.com/JakeStanger/ironbar/commit/75339f07ed164fa94838036a604a1dcb6d53564c) - vertical bars ignoring height config option *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||||
|
- [`b7b6488`](https://github.com/JakeStanger/ironbar/commit/b7b64886e3c48ace3faffbb1e277275aeeac3adf) - sometimes panicking on startup *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||||
|
|
||||||
|
### :recycle: Refactors
|
||||||
|
- [`5ce50b0`](https://github.com/JakeStanger/ironbar/commit/5ce50b0987812a1ade2d1262e8d7df6916cfc39a) - tidy and format *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||||
|
- [`1b853bc`](https://github.com/JakeStanger/ironbar/commit/1b853bcb71197a4bf3ca75725cc010b1d404c2b3) - fix clippy warning *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||||
|
|
||||||
|
### :memo: Documentation Changes
|
||||||
|
- [`daafa09`](https://github.com/JakeStanger/ironbar/commit/daafa0943e5b9886b09fd18d6fff04558fb02335) - update CHANGELOG.md for v0.5.2 [skip ci] *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||||
|
- [`b352181`](https://github.com/JakeStanger/ironbar/commit/b352181b3d232ccc79ffc1d9e22a633729d01a47) - update json example *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||||
|
- [`bb4fe7f`](https://github.com/JakeStanger/ironbar/commit/bb4fe7f7f58fa2a6d0a2259bd9442700d2c884f7) - **readme**: credit smithay client toolkit *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||||
|
- [`994d0f5`](https://github.com/JakeStanger/ironbar/commit/994d0f580b4d1b6ff750839652a7f06149743172) - **readme**: update references to sway *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||||
|
|
||||||
|
### :boom: BREAKING CHANGES
|
||||||
|
- due to [`06cfad6`](https://github.com/JakeStanger/ironbar/commit/06cfad62e228f7fc63938f2280206450005cb064) - more positioning options *(PR [#23](https://github.com/JakeStanger/ironbar/pull/23) by [@JakeStanger](https://github.com/JakeStanger))*:
|
||||||
|
|
||||||
|
The `left` and `right` config options have been renamed to `start` and `end`
|
||||||
|
|
||||||
|
|
||||||
## [v0.5.2] - 2022-09-07
|
## [v0.5.2] - 2022-09-07
|
||||||
### :wrench: Chores
|
### :wrench: Chores
|
||||||
- [`b801751`](https://github.com/JakeStanger/ironbar/commit/b801751bdabd8416084f46e6b6d803ea28a259ec) - **release**: v0.5.2 *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
- [`b801751`](https://github.com/JakeStanger/ironbar/commit/b801751bdabd8416084f46e6b6d803ea28a259ec) - **release**: v0.5.2 *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||||
@@ -45,4 +76,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
[v0.4.0]: https://github.com/JakeStanger/ironbar/compare/v0.3.0...v0.4.0
|
[v0.4.0]: https://github.com/JakeStanger/ironbar/compare/v0.3.0...v0.4.0
|
||||||
[v0.5.0]: https://github.com/JakeStanger/ironbar/compare/v0.4.0...v0.5.0
|
[v0.5.0]: https://github.com/JakeStanger/ironbar/compare/v0.4.0...v0.5.0
|
||||||
[v0.5.1]: https://github.com/JakeStanger/ironbar/compare/v0.5.0...v0.5.1
|
[v0.5.1]: https://github.com/JakeStanger/ironbar/compare/v0.5.0...v0.5.1
|
||||||
[v0.5.2]: https://github.com/JakeStanger/ironbar/compare/v0.5.1...v0.5.2
|
[v0.5.2]: https://github.com/JakeStanger/ironbar/compare/v0.5.1...v0.5.2
|
||||||
|
[v0.6.0]: https://github.com/JakeStanger/ironbar/compare/v0.5.2...v0.6.0
|
||||||
@@ -14,3 +14,4 @@ I welcome contributions of any kind with open arms. That said, please do stick t
|
|||||||
|
|
||||||
- For issues:
|
- For issues:
|
||||||
- Please provide as much information as you can - share your config, any logs, steps to reproduce...
|
- Please provide as much information as you can - share your config, any logs, steps to reproduce...
|
||||||
|
- If reporting an error, please ensure you use `IRONBAR_LOG` or `IRONBAR_FILE_LOG` set to `debug`.
|
||||||
|
|||||||
463
Cargo.lock
generated
463
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
37
Cargo.toml
37
Cargo.toml
@@ -1,41 +1,40 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "ironbar"
|
name = "ironbar"
|
||||||
version = "0.6.0"
|
version = "0.7.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
description = "Customisable wlroots/sway bar"
|
description = "Customisable GTK Layer Shell wlroots/sway bar"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
derive_builder = "0.11.2"
|
derive_builder = "0.11.2"
|
||||||
gtk = "0.15.5"
|
gtk = "0.16.0"
|
||||||
gtk-layer-shell = "0.4.1"
|
gtk-layer-shell = "0.5.0"
|
||||||
glib = "0.15.12"
|
glib = "0.16.2"
|
||||||
tokio = { version = "1.21.2", features = ["macros", "rt-multi-thread", "time"] }
|
tokio = { version = "1.21.2", features = ["macros", "rt-multi-thread", "time"] }
|
||||||
tracing = "0.1.36"
|
tracing = "0.1.37"
|
||||||
tracing-subscriber = { version = "0.3.15", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
|
||||||
tracing-error = "0.2.0"
|
tracing-error = "0.2.0"
|
||||||
tracing-appender = "0.2.2"
|
tracing-appender = "0.2.2"
|
||||||
strip-ansi-escapes = "0.1.1"
|
strip-ansi-escapes = "0.1.1"
|
||||||
color-eyre = "0.6.2"
|
color-eyre = "0.6.2"
|
||||||
futures-util = "0.3.21"
|
|
||||||
chrono = "0.4.19"
|
|
||||||
serde = { version = "1.0.141", features = ["derive"] }
|
serde = { version = "1.0.141", features = ["derive"] }
|
||||||
serde_json = "1.0.82"
|
serde_json = "1.0.82"
|
||||||
serde_yaml = "0.9.4"
|
serde_yaml = "0.9.4"
|
||||||
toml = "0.5.9"
|
toml = "0.5.9"
|
||||||
cornfig = "0.3.0"
|
libcorn = "0.4.0"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
async_once = "0.2.6"
|
async_once = "0.2.6"
|
||||||
regex = "1.6.0"
|
indexmap = "1.9.1"
|
||||||
stray = { git = "https://github.com/JakeStanger/stray.git", branch = "fix/tracing" }
|
futures-util = "0.3.21"
|
||||||
|
chrono = "0.4.19"
|
||||||
|
regex = { version = "1.6.0", default-features = false, features = ["std"] }
|
||||||
|
stray = { version = "0.1.2" }
|
||||||
dirs = "4.0.0"
|
dirs = "4.0.0"
|
||||||
walkdir = "2.3.2"
|
walkdir = "2.3.2"
|
||||||
notify = "5.0.0"
|
notify = { version = "5.0.0", default-features = false }
|
||||||
mpd_client = "1.0.0"
|
mpd_client = "1.0.0"
|
||||||
swayipc-async = { git = "https://github.com/JakeStanger/swayipc-rs.git", branch = "feat/derive-clone" }
|
swayipc-async = { version = "2.0.1" }
|
||||||
sysinfo = "0.26.2"
|
sysinfo = "0.26.4"
|
||||||
wayland-client = "0.29.5"
|
wayland-client = "0.29.5"
|
||||||
wayland-protocols = { version = "0.29.5", features=["unstable_protocols", "client"] }
|
wayland-protocols = { version = "0.29.5", features = ["unstable_protocols", "client"] }
|
||||||
smithay-client-toolkit = "0.16.0"
|
smithay-client-toolkit = { version = "0.16.0", default-features = false, features=["calloop"] }
|
||||||
14
README.md
14
README.md
@@ -1,6 +1,6 @@
|
|||||||
# Ironbar
|
# Ironbar
|
||||||
|
|
||||||
Ironbar is a customisable and feature-rich bar targeting wlroots compositors, written in Rust.
|
Ironbar is a customisable and feature-rich bar for wlroots compositors, written in Rust.
|
||||||
It uses GTK3 and gtk-layer-shell.
|
It uses GTK3 and gtk-layer-shell.
|
||||||
|
|
||||||
The bar can be styled to your liking using CSS and hot-loads style changes.
|
The bar can be styled to your liking using CSS and hot-loads style changes.
|
||||||
@@ -8,9 +8,8 @@ For information and examples on styling please see the [wiki](https://github.com
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
Run using `ironbar`.
|
## Installation
|
||||||
|
|
||||||
### Cargo
|
### Cargo
|
||||||
|
|
||||||
@@ -40,6 +39,15 @@ install target/release/ironbar ~/.local/bin/ironbar
|
|||||||
|
|
||||||
[repo](https://github.com/jakestanger/ironbar)
|
[repo](https://github.com/jakestanger/ironbar)
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
All of the above installation methods provide a binary called `ironbar`.
|
||||||
|
|
||||||
|
You can set the `IRONBAR_LOG` or `IRONBAR_FILE_LOG` environment variables to
|
||||||
|
`error`, `warn`, `info`, `debug` or `trace` to configure the log output level.
|
||||||
|
These default to `IRONBAR_LOG=info` and `IRONBAR_FILE_LOG=error`.
|
||||||
|
File output can be found at `~/.local/share/ironbar/error.log`.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Ironbar gives a lot of flexibility when configuring, including multiple file formats
|
Ironbar gives a lot of flexibility when configuring, including multiple file formats
|
||||||
|
|||||||
25
examples/custom.corn
Normal file
25
examples/custom.corn
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
let {
|
||||||
|
$power_menu = {
|
||||||
|
type = "custom"
|
||||||
|
class = "power-menu"
|
||||||
|
|
||||||
|
bar = [ { type = "button" name="power-btn" label = "" exec = "popup:toggle" } ]
|
||||||
|
|
||||||
|
popup = [ {
|
||||||
|
type = "box"
|
||||||
|
orientation = "vertical"
|
||||||
|
widgets = [
|
||||||
|
{ type = "label" name = "header" label = "Power menu" }
|
||||||
|
{
|
||||||
|
type = "box"
|
||||||
|
widgets = [
|
||||||
|
{ type = "button" class="power-btn" label = "<span font-size='40pt'></span>" exec = "!shutdown now" }
|
||||||
|
{ type = "button" class="power-btn" label = "<span font-size='40pt'></span>" exec = "!reboot" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
} ]
|
||||||
|
}
|
||||||
|
} in {
|
||||||
|
end = [ { type = "clock" } $power_menu ]
|
||||||
|
}
|
||||||
23
examples/sys-info.corn
Normal file
23
examples/sys-info.corn
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
end = [
|
||||||
|
{
|
||||||
|
type = "sys-info"
|
||||||
|
|
||||||
|
interval.memory = 30
|
||||||
|
interval.cpu = 1
|
||||||
|
interval.temps = 5
|
||||||
|
interval.disks = 300
|
||||||
|
interval.networks = 3
|
||||||
|
|
||||||
|
format = [
|
||||||
|
" {cpu-percent}% | {temp-c:k10temp-Tccd1}°C"
|
||||||
|
" {memory-used} / {memory-total} GB ({memory-percent}%)"
|
||||||
|
"| {swap-used} / {swap-total} GB ({swap-percent}%)"
|
||||||
|
" {disk-used:/} / {disk-total:/} GB ({disk-percent:/}%)"
|
||||||
|
"李 {net-down:enp39s0} / {net-up:enp39s0} Mbps"
|
||||||
|
"猪 {load-average:1} | {load-average:5} | {load-average:15}"
|
||||||
|
" {uptime}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::bridge_channel::BridgeChannel;
|
use crate::bridge_channel::BridgeChannel;
|
||||||
use crate::config::{BarPosition, ModuleConfig};
|
use crate::config::{BarPosition, ModuleConfig};
|
||||||
|
use crate::modules::custom::ExecEvent;
|
||||||
use crate::modules::launcher::{ItemEvent, LauncherUpdate};
|
use crate::modules::launcher::{ItemEvent, LauncherUpdate};
|
||||||
use crate::modules::mpd::{PlayerCommand, SongUpdate};
|
use crate::modules::mpd::{PlayerCommand, SongUpdate};
|
||||||
use crate::modules::workspaces::WorkspaceUpdate;
|
use crate::modules::workspaces::WorkspaceUpdate;
|
||||||
@@ -236,6 +237,9 @@ fn add_modules(
|
|||||||
ModuleConfig::Launcher(module) => {
|
ModuleConfig::Launcher(module) => {
|
||||||
add_module!(module, id, "launcher", LauncherUpdate, ItemEvent);
|
add_module!(module, id, "launcher", LauncherUpdate, ItemEvent);
|
||||||
}
|
}
|
||||||
|
ModuleConfig::Custom(module) => {
|
||||||
|
add_module!(module, id, "custom", (), ExecEvent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,161 +0,0 @@
|
|||||||
use serde::Serialize;
|
|
||||||
use std::slice::{Iter, IterMut};
|
|
||||||
use std::vec;
|
|
||||||
|
|
||||||
/// An ordered map.
|
|
||||||
/// Internally this is just two vectors -
|
|
||||||
/// one for keys and one for values.
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
pub struct Collection<TKey, TData> {
|
|
||||||
keys: Vec<TKey>,
|
|
||||||
values: Vec<TData>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<TKey: PartialEq, TData> Collection<TKey, TData> {
|
|
||||||
/// Creates a new empty collection.
|
|
||||||
pub const fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
keys: vec![],
|
|
||||||
values: vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Inserts a new key/value pair at the end of the collection.
|
|
||||||
pub fn insert(&mut self, key: TKey, value: TData) {
|
|
||||||
self.keys.push(key);
|
|
||||||
self.values.push(value);
|
|
||||||
|
|
||||||
assert_eq!(self.keys.len(), self.values.len());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets a reference of the value for the specified key
|
|
||||||
/// if it exists in the collection.
|
|
||||||
pub fn get(&self, key: &TKey) -> Option<&TData> {
|
|
||||||
let index = self.keys.iter().position(|k| k == key);
|
|
||||||
match index {
|
|
||||||
Some(index) => self.values.get(index),
|
|
||||||
None => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets a mutable reference for the value with the specified key
|
|
||||||
/// if it exists in the collection.
|
|
||||||
pub fn get_mut(&mut self, key: &TKey) -> Option<&mut TData> {
|
|
||||||
let index = self.keys.iter().position(|k| k == key);
|
|
||||||
match index {
|
|
||||||
Some(index) => self.values.get_mut(index),
|
|
||||||
None => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks if a value for the given key exists inside the collection
|
|
||||||
pub fn contains(&self, key: &TKey) -> bool {
|
|
||||||
self.keys.contains(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Removes the key/value from the collection
|
|
||||||
/// if it exists
|
|
||||||
/// and returns the removed value.
|
|
||||||
pub fn remove(&mut self, key: &TKey) -> Option<TData> {
|
|
||||||
assert_eq!(self.keys.len(), self.values.len());
|
|
||||||
|
|
||||||
let index = self.keys.iter().position(|k| k == key);
|
|
||||||
if let Some(index) = index {
|
|
||||||
self.keys.remove(index);
|
|
||||||
Some(self.values.remove(index))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the length of the collection.
|
|
||||||
pub fn len(&self) -> usize {
|
|
||||||
self.keys.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets a reference to the first value in the collection.
|
|
||||||
pub fn first(&self) -> Option<&TData> {
|
|
||||||
self.values.first()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the values as a slice.
|
|
||||||
pub fn as_slice(&self) -> &[TData] {
|
|
||||||
self.values.as_slice()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks whether the collection is empty.
|
|
||||||
pub fn is_empty(&self) -> bool {
|
|
||||||
self.keys.is_empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets an iterator for the collection.
|
|
||||||
pub fn iter(&self) -> Iter<'_, TData> {
|
|
||||||
self.values.iter()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets a mutable iterator for the collection
|
|
||||||
pub fn iter_mut(&mut self) -> IterMut<'_, TData> {
|
|
||||||
self.values.iter_mut()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<TKey: PartialEq, TData> From<(TKey, TData)> for Collection<TKey, TData> {
|
|
||||||
fn from((key, value): (TKey, TData)) -> Self {
|
|
||||||
let mut collection = Self::new();
|
|
||||||
collection.insert(key, value);
|
|
||||||
collection
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<TKey: PartialEq, TData> FromIterator<(TKey, TData)> for Collection<TKey, TData> {
|
|
||||||
fn from_iter<T: IntoIterator<Item = (TKey, TData)>>(iter: T) -> Self {
|
|
||||||
let mut collection = Self::new();
|
|
||||||
for (key, value) in iter {
|
|
||||||
collection.insert(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
collection
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, TKey: PartialEq, TData> IntoIterator for &'a Collection<TKey, TData> {
|
|
||||||
type Item = &'a TData;
|
|
||||||
type IntoIter = CollectionIntoIterator<'a, TKey, TData>;
|
|
||||||
|
|
||||||
fn into_iter(self) -> Self::IntoIter {
|
|
||||||
CollectionIntoIterator {
|
|
||||||
collection: self,
|
|
||||||
index: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct CollectionIntoIterator<'a, TKey, TData> {
|
|
||||||
collection: &'a Collection<TKey, TData>,
|
|
||||||
index: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, TKey: PartialEq, TData> Iterator for CollectionIntoIterator<'a, TKey, TData> {
|
|
||||||
type Item = &'a TData;
|
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
|
||||||
let res = self.collection.values.get(self.index);
|
|
||||||
self.index += 1;
|
|
||||||
res
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<TKey: PartialEq, TData> Default for Collection<TKey, TData> {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<TKey: PartialEq, TData> IntoIterator for Collection<TKey, TData> {
|
|
||||||
type Item = TData;
|
|
||||||
type IntoIter = vec::IntoIter<TData>;
|
|
||||||
|
|
||||||
fn into_iter(self) -> Self::IntoIter {
|
|
||||||
self.values.into_iter()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
use crate::modules::clock::ClockModule;
|
use crate::modules::clock::ClockModule;
|
||||||
|
use crate::modules::custom::CustomModule;
|
||||||
use crate::modules::focused::FocusedModule;
|
use crate::modules::focused::FocusedModule;
|
||||||
use crate::modules::launcher::LauncherModule;
|
use crate::modules::launcher::LauncherModule;
|
||||||
use crate::modules::mpd::MpdModule;
|
use crate::modules::mpd::MpdModule;
|
||||||
@@ -15,6 +16,7 @@ use serde::Deserialize;
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::{env, fs};
|
use std::{env, fs};
|
||||||
|
use tracing::instrument;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
#[serde(tag = "type", rename_all = "kebab-case")]
|
#[serde(tag = "type", rename_all = "kebab-case")]
|
||||||
@@ -27,6 +29,7 @@ pub enum ModuleConfig {
|
|||||||
Launcher(LauncherModule),
|
Launcher(LauncherModule),
|
||||||
Script(ScriptModule),
|
Script(ScriptModule),
|
||||||
Focused(FocusedModule),
|
Focused(FocusedModule),
|
||||||
|
Custom(CustomModule),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
@@ -69,7 +72,7 @@ impl BarPosition {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone, Default)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
#[serde(default = "default_bar_position")]
|
#[serde(default = "default_bar_position")]
|
||||||
pub position: BarPosition,
|
pub position: BarPosition,
|
||||||
@@ -96,14 +99,18 @@ const fn default_bar_height() -> i32 {
|
|||||||
impl Config {
|
impl Config {
|
||||||
/// Attempts to load the config file from file,
|
/// Attempts to load the config file from file,
|
||||||
/// parse it and return a new instance of `Self`.
|
/// parse it and return a new instance of `Self`.
|
||||||
|
#[instrument]
|
||||||
pub fn load() -> Result<Self> {
|
pub fn load() -> Result<Self> {
|
||||||
let config_path = if let Ok(config_path) = env::var("IRONBAR_CONFIG") {
|
let config_path = if let Ok(config_path) = env::var("IRONBAR_CONFIG") {
|
||||||
let path = PathBuf::from(config_path);
|
let path = PathBuf::from(config_path);
|
||||||
if path.exists() {
|
if path.exists() {
|
||||||
Ok(path)
|
Ok(path)
|
||||||
} else {
|
} else {
|
||||||
Err(Report::msg("Specified config file does not exist")
|
Err(Report::msg(format!(
|
||||||
.note("Config file was specified using `IRONBAR_CONFIG` environment variable"))
|
"Specified config file does not exist: {}",
|
||||||
|
path.display()
|
||||||
|
))
|
||||||
|
.note("Config file was specified using `IRONBAR_CONFIG` environment variable"))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Self::try_find_config()
|
Self::try_find_config()
|
||||||
@@ -116,6 +123,7 @@ impl Config {
|
|||||||
/// by checking each valid format's extension.
|
/// by checking each valid format's extension.
|
||||||
///
|
///
|
||||||
/// Returns the path of the first valid match, if any.
|
/// Returns the path of the first valid match, if any.
|
||||||
|
#[instrument]
|
||||||
fn try_find_config() -> Result<PathBuf> {
|
fn try_find_config() -> Result<PathBuf> {
|
||||||
let config_dir = config_dir().wrap_err("Failed to locate user config dir")?;
|
let config_dir = config_dir().wrap_err("Failed to locate user config dir")?;
|
||||||
|
|
||||||
@@ -135,7 +143,10 @@ impl Config {
|
|||||||
|
|
||||||
match file {
|
match file {
|
||||||
Some(file) => Ok(file),
|
Some(file) => Ok(file),
|
||||||
None => Err(Report::msg("Could not find config file")),
|
None => Err(Report::msg("Could not find config file")
|
||||||
|
.suggestion("Ironbar does not include a configuration out of the box")
|
||||||
|
.suggestion("A guide on writing a config can be found on the wiki:")
|
||||||
|
.suggestion("https://github.com/JakeStanger/ironbar/wiki/configuration-guide")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,7 +169,7 @@ impl Config {
|
|||||||
// so serialize the interpreted result then deserialize that
|
// so serialize the interpreted result then deserialize that
|
||||||
let file =
|
let file =
|
||||||
String::from_utf8(file).wrap_err("Config file contains invalid UTF-8")?;
|
String::from_utf8(file).wrap_err("Config file contains invalid UTF-8")?;
|
||||||
let config = cornfig::parse(&file).wrap_err("Invalid corn config")?.value;
|
let config = libcorn::parse(&file).wrap_err("Invalid corn config")?.value;
|
||||||
Ok(serde_json::from_str(&serde_json::to_string(&config)?)?)
|
Ok(serde_json::from_str(&serde_json::to_string(&config)?)?)
|
||||||
}
|
}
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
|
|||||||
@@ -31,10 +31,15 @@ impl<'a> MakeWriter<'a> for MakeFileWriter {
|
|||||||
/// The returned `WorkerGuard` must remain in scope
|
/// The returned `WorkerGuard` must remain in scope
|
||||||
/// for the lifetime of the application for logging to file to work.
|
/// for the lifetime of the application for logging to file to work.
|
||||||
pub fn install_tracing() -> Result<WorkerGuard> {
|
pub fn install_tracing() -> Result<WorkerGuard> {
|
||||||
|
const DEFAULT_LOG: &str = "info";
|
||||||
|
const DEFAULT_FILE_LOG: &str = "warn";
|
||||||
|
|
||||||
let fmt_layer = fmt::layer().with_target(true);
|
let fmt_layer = fmt::layer().with_target(true);
|
||||||
let filter_layer = EnvFilter::try_from_default_env().or_else(|_| EnvFilter::try_new("info"))?;
|
let filter_layer =
|
||||||
let file_filter_layer =
|
EnvFilter::try_from_env("IRONBAR_LOG").or_else(|_| EnvFilter::try_new(DEFAULT_LOG))?;
|
||||||
EnvFilter::try_from_default_env().or_else(|_| EnvFilter::try_new("warn"))?;
|
|
||||||
|
let file_filter_layer = EnvFilter::try_from_env("IRONBAR_FILE_LOG")
|
||||||
|
.or_else(|_| EnvFilter::try_new(DEFAULT_FILE_LOG))?;
|
||||||
|
|
||||||
let log_path = data_dir().unwrap_or(env::current_dir()?).join("ironbar");
|
let log_path = data_dir().unwrap_or(env::current_dir()?).join("ironbar");
|
||||||
|
|
||||||
|
|||||||
51
src/main.rs
51
src/main.rs
@@ -1,11 +1,11 @@
|
|||||||
mod bar;
|
mod bar;
|
||||||
mod bridge_channel;
|
mod bridge_channel;
|
||||||
mod collection;
|
|
||||||
mod config;
|
mod config;
|
||||||
mod icon;
|
mod icon;
|
||||||
mod logging;
|
mod logging;
|
||||||
mod modules;
|
mod modules;
|
||||||
mod popup;
|
mod popup;
|
||||||
|
mod script;
|
||||||
mod style;
|
mod style;
|
||||||
mod sway;
|
mod sway;
|
||||||
mod wayland;
|
mod wayland;
|
||||||
@@ -19,9 +19,10 @@ use dirs::config_dir;
|
|||||||
use gtk::gdk::Display;
|
use gtk::gdk::Display;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::Application;
|
use gtk::Application;
|
||||||
use std::env;
|
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
|
use std::{env, panic};
|
||||||
use tokio::runtime::Handle;
|
use tokio::runtime::Handle;
|
||||||
use tokio::task::block_in_place;
|
use tokio::task::block_in_place;
|
||||||
|
|
||||||
@@ -31,6 +32,13 @@ use wayland::WaylandClient;
|
|||||||
|
|
||||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
|
#[repr(i32)]
|
||||||
|
enum ErrorCode {
|
||||||
|
GtkDisplay = 1,
|
||||||
|
CreateBars = 2,
|
||||||
|
Config = 3,
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
// Disable backtraces by default
|
// Disable backtraces by default
|
||||||
@@ -42,7 +50,15 @@ async fn main() -> Result<()> {
|
|||||||
// otherwise file logging drops
|
// otherwise file logging drops
|
||||||
let _guard = install_tracing()?;
|
let _guard = install_tracing()?;
|
||||||
|
|
||||||
color_eyre::install()?;
|
let hook_builder = color_eyre::config::HookBuilder::default();
|
||||||
|
let (panic_hook, eyre_hook) = hook_builder.into_hooks();
|
||||||
|
|
||||||
|
eyre_hook.install()?;
|
||||||
|
|
||||||
|
// custom hook allows tracing_appender to capture panics
|
||||||
|
panic::set_hook(Box::new(move |panic_info| {
|
||||||
|
error!("{}", panic_hook.panic_report(panic_info));
|
||||||
|
}));
|
||||||
|
|
||||||
info!("Ironbar version {}", VERSION);
|
info!("Ironbar version {}", VERSION);
|
||||||
info!("Starting application");
|
info!("Starting application");
|
||||||
@@ -58,7 +74,7 @@ async fn main() -> Result<()> {
|
|||||||
|| {
|
|| {
|
||||||
let report = Report::msg("Failed to get default GTK display");
|
let report = Report::msg("Failed to get default GTK display");
|
||||||
error!("{:?}", report);
|
error!("{:?}", report);
|
||||||
exit(1)
|
exit(ErrorCode::GtkDisplay as i32)
|
||||||
},
|
},
|
||||||
|display| display,
|
|display| display,
|
||||||
);
|
);
|
||||||
@@ -67,30 +83,34 @@ async fn main() -> Result<()> {
|
|||||||
Ok(config) => config,
|
Ok(config) => config,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("{:?}", err);
|
error!("{:?}", err);
|
||||||
Config::default()
|
exit(ErrorCode::Config as i32)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
debug!("Loaded config file");
|
debug!("Loaded config file");
|
||||||
|
|
||||||
if let Err(err) = create_bars(app, &display, wayland_client, &config) {
|
if let Err(err) = create_bars(app, &display, wayland_client, &config) {
|
||||||
error!("{:?}", err);
|
error!("{:?}", err);
|
||||||
exit(2);
|
exit(ErrorCode::CreateBars as i32);
|
||||||
}
|
}
|
||||||
|
|
||||||
debug!("Created bars");
|
debug!("Created bars");
|
||||||
|
|
||||||
let style_path = config_dir().map_or_else(
|
let style_path = env::var("IRONBAR_CSS").ok().map_or_else(
|
||||||
|| {
|
|| {
|
||||||
let report = Report::msg("Failed to locate user config dir");
|
config_dir().map_or_else(
|
||||||
error!("{:?}", report);
|
|| {
|
||||||
exit(3);
|
let report = Report::msg("Failed to locate user config dir");
|
||||||
|
error!("{:?}", report);
|
||||||
|
exit(ErrorCode::CreateBars as i32);
|
||||||
|
},
|
||||||
|
|dir| dir.join("ironbar").join("style.css"),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
|dir| dir.join("ironbar").join("style.css"),
|
PathBuf::from,
|
||||||
);
|
);
|
||||||
|
|
||||||
if style_path.exists() {
|
if style_path.exists() {
|
||||||
load_css(style_path);
|
load_css(style_path);
|
||||||
debug!("Loaded CSS watcher file");
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -111,7 +131,7 @@ fn create_bars(
|
|||||||
let outputs = wl.outputs.as_slice();
|
let outputs = wl.outputs.as_slice();
|
||||||
|
|
||||||
debug!("Received {} outputs from Wayland", outputs.len());
|
debug!("Received {} outputs from Wayland", outputs.len());
|
||||||
debug!("Output names: {:?}", outputs);
|
debug!("Outputs: {:?}", outputs);
|
||||||
|
|
||||||
let num_monitors = display.n_monitors();
|
let num_monitors = display.n_monitors();
|
||||||
|
|
||||||
@@ -122,7 +142,10 @@ fn create_bars(
|
|||||||
|
|
||||||
// TODO: Could we use an Arc<Config> or `Cow<Config>` here to avoid cloning?
|
// TODO: Could we use an Arc<Config> or `Cow<Config>` here to avoid cloning?
|
||||||
config.monitors.as_ref().map_or_else(
|
config.monitors.as_ref().map_or_else(
|
||||||
|| create_bar(app, &monitor, monitor_name, config.clone()),
|
|| {
|
||||||
|
info!("Creating bar on '{}'", monitor_name);
|
||||||
|
create_bar(app, &monitor, monitor_name, config.clone())
|
||||||
|
},
|
||||||
|config| {
|
|config| {
|
||||||
let config = config.get(monitor_name);
|
let config = config.get(monitor_name);
|
||||||
match &config {
|
match &config {
|
||||||
|
|||||||
240
src/modules/custom.rs
Normal file
240
src/modules/custom.rs
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||||
|
use crate::popup::{ButtonGeometry, Popup};
|
||||||
|
use crate::script::exec_command;
|
||||||
|
use color_eyre::{Report, Result};
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use gtk::{Button, Label, Orientation};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use tokio::spawn;
|
||||||
|
use tokio::sync::mpsc::{Receiver, Sender};
|
||||||
|
use tracing::{debug, error};
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct CustomModule {
|
||||||
|
/// Container class name
|
||||||
|
class: Option<String>,
|
||||||
|
/// Widgets to add to the bar container
|
||||||
|
bar: Vec<Widget>,
|
||||||
|
/// Widgets to add to the popup container
|
||||||
|
popup: Option<Vec<Widget>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempts to parse an `Orientation` from `String`
|
||||||
|
fn try_get_orientation(orientation: &str) -> Result<Orientation> {
|
||||||
|
match orientation.to_lowercase().as_str() {
|
||||||
|
"horizontal" | "h" => Ok(Orientation::Horizontal),
|
||||||
|
"vertical" | "v" => Ok(Orientation::Vertical),
|
||||||
|
_ => Err(Report::msg("Invalid orientation string in config")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Widget attributes
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct Widget {
|
||||||
|
/// Type of GTK widget to add
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
widget_type: WidgetType,
|
||||||
|
widgets: Option<Vec<Widget>>,
|
||||||
|
label: Option<String>,
|
||||||
|
name: Option<String>,
|
||||||
|
class: Option<String>,
|
||||||
|
exec: Option<String>,
|
||||||
|
orientation: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Supported GTK widget types
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub enum WidgetType {
|
||||||
|
Box,
|
||||||
|
Label,
|
||||||
|
Button,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget {
|
||||||
|
/// Creates this widget and adds it to the parent container
|
||||||
|
fn add_to(self, parent: >k::Box, tx: Sender<ExecEvent>, bar_orientation: Orientation) {
|
||||||
|
match self.widget_type {
|
||||||
|
WidgetType::Box => parent.add(&self.into_box(&tx, bar_orientation)),
|
||||||
|
WidgetType::Label => parent.add(&self.into_label()),
|
||||||
|
WidgetType::Button => parent.add(&self.into_button(tx, bar_orientation)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a `gtk::Box` from this widget
|
||||||
|
fn into_box(self, tx: &Sender<ExecEvent>, bar_orientation: Orientation) -> gtk::Box {
|
||||||
|
let mut builder = gtk::Box::builder();
|
||||||
|
|
||||||
|
if let Some(name) = self.name {
|
||||||
|
builder = builder.name(&name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(orientation) = self.orientation {
|
||||||
|
builder = builder
|
||||||
|
.orientation(try_get_orientation(&orientation).unwrap_or(Orientation::Horizontal));
|
||||||
|
}
|
||||||
|
|
||||||
|
let container = builder.build();
|
||||||
|
|
||||||
|
if let Some(class) = self.class {
|
||||||
|
container.style_context().add_class(&class);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(widgets) = self.widgets {
|
||||||
|
widgets
|
||||||
|
.into_iter()
|
||||||
|
.for_each(|widget| widget.add_to(&container, tx.clone(), bar_orientation));
|
||||||
|
}
|
||||||
|
|
||||||
|
container
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a `gtk::Label` from this widget
|
||||||
|
fn into_label(self) -> Label {
|
||||||
|
let mut builder = Label::builder().use_markup(true);
|
||||||
|
|
||||||
|
if let Some(name) = self.name {
|
||||||
|
builder = builder.name(&name);
|
||||||
|
}
|
||||||
|
|
||||||
|
let label = builder.build();
|
||||||
|
|
||||||
|
if let Some(text) = self.label {
|
||||||
|
label.set_markup(&text);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(class) = self.class {
|
||||||
|
label.style_context().add_class(&class);
|
||||||
|
}
|
||||||
|
|
||||||
|
label
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a `gtk::Button` from this widget
|
||||||
|
fn into_button(self, tx: Sender<ExecEvent>, bar_orientation: Orientation) -> Button {
|
||||||
|
let mut builder = Button::builder();
|
||||||
|
|
||||||
|
if let Some(name) = self.name {
|
||||||
|
builder = builder.name(&name);
|
||||||
|
}
|
||||||
|
|
||||||
|
let button = builder.build();
|
||||||
|
|
||||||
|
if let Some(text) = self.label {
|
||||||
|
let label = Label::new(None);
|
||||||
|
label.set_use_markup(true);
|
||||||
|
label.set_markup(&text);
|
||||||
|
button.add(&label);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(class) = self.class {
|
||||||
|
button.style_context().add_class(&class);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(exec) = self.exec {
|
||||||
|
button.connect_clicked(move |button| {
|
||||||
|
tx.try_send(ExecEvent {
|
||||||
|
cmd: exec.clone(),
|
||||||
|
geometry: Popup::button_pos(button, bar_orientation),
|
||||||
|
})
|
||||||
|
.expect("Failed to send exec message");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
button
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ExecEvent {
|
||||||
|
cmd: String,
|
||||||
|
geometry: ButtonGeometry,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Module<gtk::Box> for CustomModule {
|
||||||
|
type SendMessage = ();
|
||||||
|
type ReceiveMessage = ExecEvent;
|
||||||
|
|
||||||
|
fn spawn_controller(
|
||||||
|
&self,
|
||||||
|
_info: &ModuleInfo,
|
||||||
|
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
||||||
|
mut rx: Receiver<Self::ReceiveMessage>,
|
||||||
|
) -> Result<()> {
|
||||||
|
spawn(async move {
|
||||||
|
while let Some(event) = rx.recv().await {
|
||||||
|
if event.cmd.starts_with('!') {
|
||||||
|
debug!("executing command: '{}'", &event.cmd[1..]);
|
||||||
|
if let Err(err) = exec_command(&event.cmd[1..]) {
|
||||||
|
error!("{err:?}");
|
||||||
|
}
|
||||||
|
} else if event.cmd == "popup:toggle" {
|
||||||
|
tx.send(ModuleUpdateEvent::TogglePopup(event.geometry))
|
||||||
|
.await
|
||||||
|
.expect("Failed to send open popup event");
|
||||||
|
} else if event.cmd == "popup:open" {
|
||||||
|
tx.send(ModuleUpdateEvent::OpenPopup(event.geometry))
|
||||||
|
.await
|
||||||
|
.expect("Failed to send open popup event");
|
||||||
|
} else if event.cmd == "popup:close" {
|
||||||
|
tx.send(ModuleUpdateEvent::ClosePopup)
|
||||||
|
.await
|
||||||
|
.expect("Failed to send open popup event");
|
||||||
|
} else {
|
||||||
|
error!("Received invalid command: '{}'", event.cmd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_widget(
|
||||||
|
self,
|
||||||
|
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||||
|
info: &ModuleInfo,
|
||||||
|
) -> Result<ModuleWidget<gtk::Box>> {
|
||||||
|
let orientation = info.bar_position.get_orientation();
|
||||||
|
let container = gtk::Box::builder().orientation(orientation).build();
|
||||||
|
|
||||||
|
if let Some(ref class) = self.class {
|
||||||
|
container.style_context().add_class(class);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.bar.clone().into_iter().for_each(|widget| {
|
||||||
|
widget.add_to(&container, context.controller_tx.clone(), orientation);
|
||||||
|
});
|
||||||
|
|
||||||
|
let popup = self.into_popup(context.controller_tx, context.popup_rx);
|
||||||
|
|
||||||
|
Ok(ModuleWidget {
|
||||||
|
widget: container,
|
||||||
|
popup,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_popup(
|
||||||
|
self,
|
||||||
|
tx: Sender<Self::ReceiveMessage>,
|
||||||
|
_rx: glib::Receiver<Self::SendMessage>,
|
||||||
|
) -> Option<gtk::Box>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
let container = gtk::Box::builder().name("popup-custom").build();
|
||||||
|
|
||||||
|
if let Some(class) = self.class {
|
||||||
|
container
|
||||||
|
.style_context()
|
||||||
|
.add_class(format!("popup-{class}").as_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(popup) = self.popup {
|
||||||
|
popup
|
||||||
|
.into_iter()
|
||||||
|
.for_each(|widget| widget.add_to(&container, tx.clone(), Orientation::Horizontal));
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(container)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,10 +47,10 @@ impl Module<gtk::Box> for FocusedModule {
|
|||||||
.expect("Failed to get read lock on toplevels")
|
.expect("Failed to get read lock on toplevels")
|
||||||
.clone();
|
.clone();
|
||||||
|
|
||||||
toplevels.into_iter().find(|(top, _)| top.active)
|
toplevels.into_iter().find(|(_, (top, _))| top.active)
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some((top, _)) = focused {
|
if let Some((_, (top, _))) = focused {
|
||||||
tx.try_send(ModuleUpdateEvent::Update((top.title.clone(), top.app_id)))?;
|
tx.try_send(ModuleUpdateEvent::Update((top.title.clone(), top.app_id)))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use super::open_state::OpenState;
|
use super::open_state::OpenState;
|
||||||
use crate::collection::Collection;
|
|
||||||
use crate::icon::get_icon;
|
use crate::icon::get_icon;
|
||||||
use crate::modules::launcher::{ItemEvent, LauncherUpdate};
|
use crate::modules::launcher::{ItemEvent, LauncherUpdate};
|
||||||
use crate::modules::ModuleUpdateEvent;
|
use crate::modules::ModuleUpdateEvent;
|
||||||
@@ -7,6 +6,7 @@ use crate::popup::Popup;
|
|||||||
use crate::wayland::ToplevelInfo;
|
use crate::wayland::ToplevelInfo;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::{Button, IconTheme, Image, Orientation};
|
use gtk::{Button, IconTheme, Image, Orientation};
|
||||||
|
use indexmap::IndexMap;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
use tokio::sync::mpsc::Sender;
|
use tokio::sync::mpsc::Sender;
|
||||||
@@ -16,17 +16,17 @@ pub struct Item {
|
|||||||
pub app_id: String,
|
pub app_id: String,
|
||||||
pub favorite: bool,
|
pub favorite: bool,
|
||||||
pub open_state: OpenState,
|
pub open_state: OpenState,
|
||||||
pub windows: Collection<usize, Window>,
|
pub windows: IndexMap<usize, Window>,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Item {
|
impl Item {
|
||||||
pub const fn new(app_id: String, open_state: OpenState, favorite: bool) -> Self {
|
pub fn new(app_id: String, open_state: OpenState, favorite: bool) -> Self {
|
||||||
Self {
|
Self {
|
||||||
app_id,
|
app_id,
|
||||||
favorite,
|
favorite,
|
||||||
open_state,
|
open_state,
|
||||||
windows: Collection::new(),
|
windows: IndexMap::new(),
|
||||||
name: String::new(),
|
name: String::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,7 +78,7 @@ impl Item {
|
|||||||
&self
|
&self
|
||||||
.windows
|
.windows
|
||||||
.iter()
|
.iter()
|
||||||
.map(|win| &win.open_state)
|
.map(|(_, win)| &win.open_state)
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
);
|
);
|
||||||
self.open_state = new_state;
|
self.open_state = new_state;
|
||||||
@@ -91,7 +91,7 @@ impl From<ToplevelInfo> for Item {
|
|||||||
let name = toplevel.title.clone();
|
let name = toplevel.title.clone();
|
||||||
let app_id = toplevel.app_id.clone();
|
let app_id = toplevel.app_id.clone();
|
||||||
|
|
||||||
let mut windows = Collection::new();
|
let mut windows = IndexMap::new();
|
||||||
windows.insert(toplevel.id, toplevel.into());
|
windows.insert(toplevel.id, toplevel.into());
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ mod open_state;
|
|||||||
|
|
||||||
use self::item::{Item, ItemButton, Window};
|
use self::item::{Item, ItemButton, Window};
|
||||||
use self::open_state::OpenState;
|
use self::open_state::OpenState;
|
||||||
use crate::collection::Collection;
|
|
||||||
use crate::icon::find_desktop_file;
|
use crate::icon::find_desktop_file;
|
||||||
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||||
use crate::wayland;
|
use crate::wayland;
|
||||||
@@ -12,6 +11,7 @@ use color_eyre::{Help, Report};
|
|||||||
use glib::Continue;
|
use glib::Continue;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::{Button, IconTheme, Orientation};
|
use gtk::{Button, IconTheme, Orientation};
|
||||||
|
use indexmap::IndexMap;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::process::{Command, Stdio};
|
use std::process::{Command, Stdio};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
@@ -90,8 +90,8 @@ impl Module<gtk::Box> for LauncherModule {
|
|||||||
Item::new(app_id.to_string(), OpenState::Closed, true),
|
Item::new(app_id.to_string(), OpenState::Closed, true),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect::<Collection<_, _>>(),
|
.collect::<IndexMap<_, _>>(),
|
||||||
None => Collection::new(),
|
None => IndexMap::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let items = Arc::new(Mutex::new(items));
|
let items = Arc::new(Mutex::new(items));
|
||||||
@@ -108,7 +108,7 @@ impl Module<gtk::Box> for LauncherModule {
|
|||||||
|
|
||||||
let mut items = items.lock().expect("Failed to get lock on items");
|
let mut items = items.lock().expect("Failed to get lock on items");
|
||||||
|
|
||||||
for (window, _) in open_windows.clone() {
|
for (_, (window, _)) in open_windows.clone() {
|
||||||
let item = items.get_mut(&window.app_id);
|
let item = items.get_mut(&window.app_id);
|
||||||
match item {
|
match item {
|
||||||
Some(item) => {
|
Some(item) => {
|
||||||
@@ -121,7 +121,7 @@ impl Module<gtk::Box> for LauncherModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let items = items.iter();
|
let items = items.iter();
|
||||||
for item in items {
|
for (_, item) in items {
|
||||||
tx.try_send(ModuleUpdateEvent::Update(LauncherUpdate::AddItem(
|
tx.try_send(ModuleUpdateEvent::Update(LauncherUpdate::AddItem(
|
||||||
item.clone(),
|
item.clone(),
|
||||||
)))?;
|
)))?;
|
||||||
@@ -282,7 +282,7 @@ impl Module<gtk::Box> for LauncherModule {
|
|||||||
let id = match event {
|
let id = match event {
|
||||||
ItemEvent::FocusItem(app_id) => items
|
ItemEvent::FocusItem(app_id) => items
|
||||||
.get(&app_id)
|
.get(&app_id)
|
||||||
.and_then(|item| item.windows.first().map(|win| win.id)),
|
.and_then(|item| item.windows.first().map(|(_, win)| win.id)),
|
||||||
ItemEvent::FocusWindow(id) => Some(id),
|
ItemEvent::FocusWindow(id) => Some(id),
|
||||||
ItemEvent::OpenItem(_) => unreachable!(),
|
ItemEvent::OpenItem(_) => unreachable!(),
|
||||||
};
|
};
|
||||||
@@ -325,7 +325,7 @@ impl Module<gtk::Box> for LauncherModule {
|
|||||||
let show_icons = self.show_icons;
|
let show_icons = self.show_icons;
|
||||||
let orientation = info.bar_position.get_orientation();
|
let orientation = info.bar_position.get_orientation();
|
||||||
|
|
||||||
let mut buttons = Collection::<String, ItemButton>::new();
|
let mut buttons = IndexMap::<String, ItemButton>::new();
|
||||||
|
|
||||||
let controller_tx2 = context.controller_tx.clone();
|
let controller_tx2 = context.controller_tx.clone();
|
||||||
context.widget_rx.attach(None, move |event| {
|
context.widget_rx.attach(None, move |event| {
|
||||||
@@ -427,11 +427,12 @@ impl Module<gtk::Box> for LauncherModule {
|
|||||||
.name("popup-launcher")
|
.name("popup-launcher")
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
// we need some content to force the container to have a size
|
||||||
let placeholder = Button::with_label("PLACEHOLDER");
|
let placeholder = Button::with_label("PLACEHOLDER");
|
||||||
placeholder.set_width_request(MAX_WIDTH);
|
placeholder.set_width_request(MAX_WIDTH);
|
||||||
container.add(&placeholder);
|
container.add(&placeholder);
|
||||||
|
|
||||||
let mut buttons = Collection::<String, Collection<usize, Button>>::new();
|
let mut buttons = IndexMap::<String, IndexMap<usize, Button>>::new();
|
||||||
|
|
||||||
{
|
{
|
||||||
let container = container.clone();
|
let container = container.clone();
|
||||||
@@ -439,11 +440,12 @@ impl Module<gtk::Box> for LauncherModule {
|
|||||||
match event {
|
match event {
|
||||||
LauncherUpdate::AddItem(item) => {
|
LauncherUpdate::AddItem(item) => {
|
||||||
let app_id = item.app_id.clone();
|
let app_id = item.app_id.clone();
|
||||||
|
trace!("Adding item with id '{app_id}' to the popup: {item:?}");
|
||||||
|
|
||||||
let window_buttons = item
|
let window_buttons = item
|
||||||
.windows
|
.windows
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|win| {
|
.map(|(_, win)| {
|
||||||
let button = Button::builder()
|
let button = Button::builder()
|
||||||
.label(&clamp(&win.name))
|
.label(&clamp(&win.name))
|
||||||
.height_request(40)
|
.height_request(40)
|
||||||
@@ -468,6 +470,11 @@ impl Module<gtk::Box> for LauncherModule {
|
|||||||
buttons.insert(app_id, window_buttons);
|
buttons.insert(app_id, window_buttons);
|
||||||
}
|
}
|
||||||
LauncherUpdate::AddWindow(app_id, win) => {
|
LauncherUpdate::AddWindow(app_id, win) => {
|
||||||
|
debug!(
|
||||||
|
"Adding new window to popup for '{app_id}': '{}' ({})",
|
||||||
|
win.name, win.id
|
||||||
|
);
|
||||||
|
|
||||||
if let Some(buttons) = buttons.get_mut(&app_id) {
|
if let Some(buttons) = buttons.get_mut(&app_id) {
|
||||||
let button = Button::builder()
|
let button = Button::builder()
|
||||||
.height_request(40)
|
.height_request(40)
|
||||||
@@ -490,11 +497,17 @@ impl Module<gtk::Box> for LauncherModule {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
LauncherUpdate::RemoveWindow(app_id, win_id) => {
|
LauncherUpdate::RemoveWindow(app_id, win_id) => {
|
||||||
|
debug!("Removing window from popup for '{app_id}': {win_id}");
|
||||||
|
|
||||||
if let Some(buttons) = buttons.get_mut(&app_id) {
|
if let Some(buttons) = buttons.get_mut(&app_id) {
|
||||||
buttons.remove(&win_id);
|
buttons.remove(&win_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LauncherUpdate::Title(app_id, win_id, title) => {
|
LauncherUpdate::Title(app_id, win_id, title) => {
|
||||||
|
debug!(
|
||||||
|
"Updating window title on popup for '{app_id}'/{win_id} to '{title}'"
|
||||||
|
);
|
||||||
|
|
||||||
if let Some(buttons) = buttons.get_mut(&app_id) {
|
if let Some(buttons) = buttons.get_mut(&app_id) {
|
||||||
if let Some(button) = buttons.get(&win_id) {
|
if let Some(button) = buttons.get(&win_id) {
|
||||||
button.set_label(&title);
|
button.set_label(&title);
|
||||||
@@ -509,7 +522,7 @@ impl Module<gtk::Box> for LauncherModule {
|
|||||||
|
|
||||||
// add app's buttons
|
// add app's buttons
|
||||||
if let Some(buttons) = buttons.get(&app_id) {
|
if let Some(buttons) = buttons.get(&app_id) {
|
||||||
for button in buttons {
|
for (_, button) in buttons {
|
||||||
button.style_context().add_class("popup-item");
|
button.style_context().add_class("popup-item");
|
||||||
container.add(button);
|
container.add(button);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
/// Clicking the widget opens a popup containing the current time
|
/// Clicking the widget opens a popup containing the current time
|
||||||
/// with second-level precision and a calendar.
|
/// with second-level precision and a calendar.
|
||||||
pub mod clock;
|
pub mod clock;
|
||||||
|
pub mod custom;
|
||||||
pub mod focused;
|
pub mod focused;
|
||||||
pub mod launcher;
|
pub mod launcher;
|
||||||
pub mod mpd;
|
pub mod mpd;
|
||||||
|
|||||||
@@ -59,8 +59,9 @@ impl MpdClient {
|
|||||||
while let Some(change) = state_changes.next().await {
|
while let Some(change) = state_changes.next().await {
|
||||||
debug!("Received state change: {:?}", change);
|
debug!("Received state change: {:?}", change);
|
||||||
|
|
||||||
if let ConnectionEvent::SubsystemChange(Subsystem::Player | Subsystem::Queue) =
|
if let ConnectionEvent::SubsystemChange(
|
||||||
change
|
Subsystem::Player | Subsystem::Queue | Subsystem::Mixer,
|
||||||
|
) = change
|
||||||
{
|
{
|
||||||
tx2.send(())?;
|
tx2.send(())?;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use dirs::{audio_dir, home_dir};
|
|||||||
use glib::Continue;
|
use glib::Continue;
|
||||||
use gtk::gdk_pixbuf::Pixbuf;
|
use gtk::gdk_pixbuf::Pixbuf;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::{Button, Image, Label, Orientation};
|
use gtk::{Button, Image, Label, Orientation, Scale};
|
||||||
use mpd_client::commands;
|
use mpd_client::commands;
|
||||||
use mpd_client::responses::{PlayState, Song, Status};
|
use mpd_client::responses::{PlayState, Song, Status};
|
||||||
use mpd_client::tag::Tag;
|
use mpd_client::tag::Tag;
|
||||||
@@ -26,16 +26,20 @@ pub enum PlayerCommand {
|
|||||||
Previous,
|
Previous,
|
||||||
Toggle,
|
Toggle,
|
||||||
Next,
|
Next,
|
||||||
|
Volume(u8),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
pub struct Icons {
|
pub struct Icons {
|
||||||
/// Icon to display when playing.
|
/// Icon to display when playing.
|
||||||
#[serde(default = "default_icon_play")]
|
#[serde(default = "default_icon_play")]
|
||||||
play: Option<String>,
|
play: String,
|
||||||
/// Icon to display when paused.
|
/// Icon to display when paused.
|
||||||
#[serde(default = "default_icon_pause")]
|
#[serde(default = "default_icon_pause")]
|
||||||
pause: Option<String>,
|
pause: String,
|
||||||
|
/// Icon to display under volume slider
|
||||||
|
#[serde(default = "default_icon_volume")]
|
||||||
|
volume: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Icons {
|
impl Default for Icons {
|
||||||
@@ -43,6 +47,7 @@ impl Default for Icons {
|
|||||||
Self {
|
Self {
|
||||||
pause: default_icon_pause(),
|
pause: default_icon_pause(),
|
||||||
play: default_icon_play(),
|
play: default_icon_play(),
|
||||||
|
volume: default_icon_volume(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,14 +78,16 @@ fn default_format() -> String {
|
|||||||
String::from("{icon} {title} / {artist}")
|
String::from("{icon} {title} / {artist}")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::unnecessary_wraps)]
|
fn default_icon_play() -> String {
|
||||||
fn default_icon_play() -> Option<String> {
|
String::from("")
|
||||||
Some(String::from(""))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::unnecessary_wraps)]
|
fn default_icon_pause() -> String {
|
||||||
fn default_icon_pause() -> Option<String> {
|
String::from("")
|
||||||
Some(String::from(""))
|
}
|
||||||
|
|
||||||
|
fn default_icon_volume() -> String {
|
||||||
|
String::from("墳")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_music_dir() -> PathBuf {
|
fn default_music_dir() -> PathBuf {
|
||||||
@@ -186,6 +193,7 @@ impl Module<Button> for MpdModule {
|
|||||||
Err(err) => Err(err),
|
Err(err) => Err(err),
|
||||||
},
|
},
|
||||||
PlayerCommand::Next => client.command(commands::Next).await,
|
PlayerCommand::Next => client.command(commands::Next).await,
|
||||||
|
PlayerCommand::Volume(vol) => client.command(commands::SetVolume(vol)).await,
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(err) = res {
|
if let Err(err) = res {
|
||||||
@@ -211,18 +219,21 @@ impl Module<Button> for MpdModule {
|
|||||||
|
|
||||||
let orientation = info.bar_position.get_orientation();
|
let orientation = info.bar_position.get_orientation();
|
||||||
|
|
||||||
button.connect_clicked(move |button| {
|
{
|
||||||
context
|
let tx = context.tx.clone();
|
||||||
.tx
|
|
||||||
.try_send(ModuleUpdateEvent::TogglePopup(Popup::button_pos(
|
button.connect_clicked(move |button| {
|
||||||
|
tx.try_send(ModuleUpdateEvent::TogglePopup(Popup::button_pos(
|
||||||
button,
|
button,
|
||||||
orientation,
|
orientation,
|
||||||
)))
|
)))
|
||||||
.expect("Failed to send MPD popup open event");
|
.expect("Failed to send MPD popup open event");
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let button = button.clone();
|
let button = button.clone();
|
||||||
|
let tx = context.tx.clone();
|
||||||
|
|
||||||
context.widget_rx.attach(None, move |mut event| {
|
context.widget_rx.attach(None, move |mut event| {
|
||||||
if let Some(event) = event.take() {
|
if let Some(event) = event.take() {
|
||||||
@@ -230,6 +241,8 @@ impl Module<Button> for MpdModule {
|
|||||||
button.show();
|
button.show();
|
||||||
} else {
|
} else {
|
||||||
button.hide();
|
button.hide();
|
||||||
|
tx.try_send(ModuleUpdateEvent::ClosePopup)
|
||||||
|
.expect("Failed to send close popup message");
|
||||||
}
|
}
|
||||||
|
|
||||||
Continue(true)
|
Continue(true)
|
||||||
@@ -285,8 +298,25 @@ impl Module<Button> for MpdModule {
|
|||||||
|
|
||||||
info_box.add(&controls_box);
|
info_box.add(&controls_box);
|
||||||
|
|
||||||
|
let volume_box = gtk::Box::builder()
|
||||||
|
.orientation(Orientation::Vertical)
|
||||||
|
.spacing(5)
|
||||||
|
.name("volume")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let volume_slider = Scale::with_range(Orientation::Vertical, 0.0, 100.0, 5.0);
|
||||||
|
volume_slider.set_inverted(true);
|
||||||
|
volume_slider.set_widget_name("scale");
|
||||||
|
|
||||||
|
let volume_icon = Label::new(Some(&self.icons.volume));
|
||||||
|
volume_icon.style_context().add_class("icon");
|
||||||
|
|
||||||
|
volume_box.pack_start(&volume_slider, true, true, 0);
|
||||||
|
volume_box.pack_end(&volume_icon, false, false, 0);
|
||||||
|
|
||||||
container.add(&album_image);
|
container.add(&album_image);
|
||||||
container.add(&info_box);
|
container.add(&info_box);
|
||||||
|
container.add(&volume_box);
|
||||||
|
|
||||||
let tx_prev = tx.clone();
|
let tx_prev = tx.clone();
|
||||||
btn_prev.connect_clicked(move |_| {
|
btn_prev.connect_clicked(move |_| {
|
||||||
@@ -302,13 +332,22 @@ impl Module<Button> for MpdModule {
|
|||||||
.expect("Failed to send play/pause track message");
|
.expect("Failed to send play/pause track message");
|
||||||
});
|
});
|
||||||
|
|
||||||
let tx_next = tx;
|
let tx_next = tx.clone();
|
||||||
btn_next.connect_clicked(move |_| {
|
btn_next.connect_clicked(move |_| {
|
||||||
tx_next
|
tx_next
|
||||||
.try_send(PlayerCommand::Next)
|
.try_send(PlayerCommand::Next)
|
||||||
.expect("Failed to send next track message");
|
.expect("Failed to send next track message");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let tx_vol = tx;
|
||||||
|
volume_slider.connect_change_value(move |_, _, val| {
|
||||||
|
tx_vol
|
||||||
|
.try_send(PlayerCommand::Volume(val as u8))
|
||||||
|
.expect("Failed to send volume message");
|
||||||
|
|
||||||
|
Inhibit(false)
|
||||||
|
});
|
||||||
|
|
||||||
container.show_all();
|
container.show_all();
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -371,6 +410,8 @@ impl Module<Button> for MpdModule {
|
|||||||
|
|
||||||
btn_prev.set_sensitive(enable_prev);
|
btn_prev.set_sensitive(enable_prev);
|
||||||
btn_next.set_sensitive(enable_next);
|
btn_next.set_sensitive(enable_next);
|
||||||
|
|
||||||
|
volume_slider.set_value(update.status.volume as f64);
|
||||||
}
|
}
|
||||||
|
|
||||||
Continue(true)
|
Continue(true)
|
||||||
@@ -406,8 +447,8 @@ fn get_token_value(song: &Song, status: &Status, icons: &Icons, token: &str) ->
|
|||||||
"icon" => {
|
"icon" => {
|
||||||
let icon = match status.state {
|
let icon = match status.state {
|
||||||
PlayState::Stopped => None,
|
PlayState::Stopped => None,
|
||||||
PlayState::Playing => icons.play.as_ref(),
|
PlayState::Playing => Some(&icons.play),
|
||||||
PlayState::Paused => icons.pause.as_ref(),
|
PlayState::Paused => Some(&icons.pause),
|
||||||
};
|
};
|
||||||
icon.map(String::as_str)
|
icon.map(String::as_str)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||||
use color_eyre::{eyre::Report, eyre::Result, eyre::WrapErr, Section};
|
use crate::script::exec_command;
|
||||||
|
use color_eyre::Result;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::Label;
|
use gtk::Label;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::process::Command;
|
|
||||||
use tokio::spawn;
|
use tokio::spawn;
|
||||||
use tokio::sync::mpsc::{Receiver, Sender};
|
use tokio::sync::mpsc::{Receiver, Sender};
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
use tracing::{error, instrument};
|
use tracing::error;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
pub struct ScriptModule {
|
pub struct ScriptModule {
|
||||||
@@ -37,7 +37,7 @@ impl Module<Label> for ScriptModule {
|
|||||||
let path = self.path.clone();
|
let path = self.path.clone();
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
loop {
|
loop {
|
||||||
match run_script(&path) {
|
match exec_command(&path) {
|
||||||
Ok(stdout) => tx
|
Ok(stdout) => tx
|
||||||
.send(ModuleUpdateEvent::Update(stdout))
|
.send(ModuleUpdateEvent::Update(stdout))
|
||||||
.await
|
.await
|
||||||
@@ -63,7 +63,7 @@ impl Module<Label> for ScriptModule {
|
|||||||
{
|
{
|
||||||
let label = label.clone();
|
let label = label.clone();
|
||||||
context.widget_rx.attach(None, move |s| {
|
context.widget_rx.attach(None, move |s| {
|
||||||
label.set_label(s.as_str());
|
label.set_markup(s.as_str());
|
||||||
Continue(true)
|
Continue(true)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -74,29 +74,3 @@ impl Module<Label> for ScriptModule {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument]
|
|
||||||
fn run_script(path: &str) -> Result<String> {
|
|
||||||
let output = Command::new("sh")
|
|
||||||
.arg("-c")
|
|
||||||
.arg(path)
|
|
||||||
.output()
|
|
||||||
.wrap_err("Failed to get script output")?;
|
|
||||||
|
|
||||||
if output.status.success() {
|
|
||||||
let stdout = String::from_utf8(output.stdout)
|
|
||||||
.map(|output| output.trim().to_string())
|
|
||||||
.wrap_err("Script stdout not valid UTF-8")?;
|
|
||||||
|
|
||||||
Ok(stdout)
|
|
||||||
} else {
|
|
||||||
let stderr = String::from_utf8(output.stderr)
|
|
||||||
.map(|output| output.trim().to_string())
|
|
||||||
.wrap_err("Script stderr not valid UTF-8")?;
|
|
||||||
|
|
||||||
Err(Report::msg(stderr)
|
|
||||||
.wrap_err("Script returned non-zero error code")
|
|
||||||
.suggestion("Check the path to your script")
|
|
||||||
.suggestion("Check the script for errors"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ use gtk::Label;
|
|||||||
use regex::{Captures, Regex};
|
use regex::{Captures, Regex};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use sysinfo::{CpuExt, System, SystemExt};
|
use std::time::Duration;
|
||||||
|
use sysinfo::{ComponentExt, CpuExt, DiskExt, NetworkExt, RefreshKind, System, SystemExt};
|
||||||
use tokio::spawn;
|
use tokio::spawn;
|
||||||
|
use tokio::sync::mpsc;
|
||||||
use tokio::sync::mpsc::{Receiver, Sender};
|
use tokio::sync::mpsc::{Receiver, Sender};
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
|
|
||||||
@@ -14,6 +16,96 @@ use tokio::time::sleep;
|
|||||||
pub struct SysInfoModule {
|
pub struct SysInfoModule {
|
||||||
/// List of formatting strings.
|
/// List of formatting strings.
|
||||||
format: Vec<String>,
|
format: Vec<String>,
|
||||||
|
/// Number of seconds between refresh
|
||||||
|
#[serde(default = "Interval::default")]
|
||||||
|
interval: Interval,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Copy, Clone)]
|
||||||
|
pub struct Intervals {
|
||||||
|
#[serde(default = "default_interval")]
|
||||||
|
memory: u64,
|
||||||
|
#[serde(default = "default_interval")]
|
||||||
|
cpu: u64,
|
||||||
|
#[serde(default = "default_interval")]
|
||||||
|
temps: u64,
|
||||||
|
#[serde(default = "default_interval")]
|
||||||
|
disks: u64,
|
||||||
|
#[serde(default = "default_interval")]
|
||||||
|
networks: u64,
|
||||||
|
#[serde(default = "default_interval")]
|
||||||
|
system: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Copy, Clone)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum Interval {
|
||||||
|
All(u64),
|
||||||
|
Individual(Intervals),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Interval {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::All(default_interval())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Interval {
|
||||||
|
const fn memory(self) -> u64 {
|
||||||
|
match self {
|
||||||
|
Self::All(n) => n,
|
||||||
|
Self::Individual(intervals) => intervals.memory,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn cpu(self) -> u64 {
|
||||||
|
match self {
|
||||||
|
Self::All(n) => n,
|
||||||
|
Self::Individual(intervals) => intervals.cpu,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn temps(self) -> u64 {
|
||||||
|
match self {
|
||||||
|
Self::All(n) => n,
|
||||||
|
Self::Individual(intervals) => intervals.temps,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn disks(self) -> u64 {
|
||||||
|
match self {
|
||||||
|
Self::All(n) => n,
|
||||||
|
Self::Individual(intervals) => intervals.disks,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn networks(self) -> u64 {
|
||||||
|
match self {
|
||||||
|
Self::All(n) => n,
|
||||||
|
Self::Individual(intervals) => intervals.networks,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn system(self) -> u64 {
|
||||||
|
match self {
|
||||||
|
Self::All(n) => n,
|
||||||
|
Self::Individual(intervals) => intervals.system,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn default_interval() -> u64 {
|
||||||
|
5
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum RefreshType {
|
||||||
|
Memory,
|
||||||
|
Cpu,
|
||||||
|
Temps,
|
||||||
|
Disks,
|
||||||
|
Network,
|
||||||
|
System,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Module<gtk::Box> for SysInfoModule {
|
impl Module<gtk::Box> for SysInfoModule {
|
||||||
@@ -26,35 +118,115 @@ impl Module<gtk::Box> for SysInfoModule {
|
|||||||
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
||||||
_rx: Receiver<Self::ReceiveMessage>,
|
_rx: Receiver<Self::ReceiveMessage>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
let interval = self.interval;
|
||||||
|
|
||||||
|
let refresh_kind = RefreshKind::everything()
|
||||||
|
.without_processes()
|
||||||
|
.without_users_list();
|
||||||
|
|
||||||
|
let mut sys = System::new_with_specifics(refresh_kind);
|
||||||
|
sys.refresh_components_list();
|
||||||
|
sys.refresh_disks_list();
|
||||||
|
sys.refresh_networks_list();
|
||||||
|
|
||||||
|
let (refresh_tx, mut refresh_rx) = mpsc::channel(16);
|
||||||
|
|
||||||
|
// memory refresh
|
||||||
|
{
|
||||||
|
let tx = refresh_tx.clone();
|
||||||
|
spawn(async move {
|
||||||
|
loop {
|
||||||
|
tx.send(RefreshType::Memory)
|
||||||
|
.await
|
||||||
|
.expect("Failed to send memory refresh");
|
||||||
|
sleep(Duration::from_secs(interval.memory())).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// cpu refresh
|
||||||
|
{
|
||||||
|
let tx = refresh_tx.clone();
|
||||||
|
spawn(async move {
|
||||||
|
loop {
|
||||||
|
tx.send(RefreshType::Cpu)
|
||||||
|
.await
|
||||||
|
.expect("Failed to send cpu refresh");
|
||||||
|
sleep(Duration::from_secs(interval.cpu())).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// temp refresh
|
||||||
|
{
|
||||||
|
let tx = refresh_tx.clone();
|
||||||
|
spawn(async move {
|
||||||
|
loop {
|
||||||
|
tx.send(RefreshType::Temps)
|
||||||
|
.await
|
||||||
|
.expect("Failed to send temperature refresh");
|
||||||
|
sleep(Duration::from_secs(interval.temps())).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// disk refresh
|
||||||
|
{
|
||||||
|
let tx = refresh_tx.clone();
|
||||||
|
spawn(async move {
|
||||||
|
loop {
|
||||||
|
tx.send(RefreshType::Disks)
|
||||||
|
.await
|
||||||
|
.expect("Failed to send disk refresh");
|
||||||
|
sleep(Duration::from_secs(interval.disks())).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// network refresh
|
||||||
|
{
|
||||||
|
let tx = refresh_tx.clone();
|
||||||
|
spawn(async move {
|
||||||
|
loop {
|
||||||
|
tx.send(RefreshType::Network)
|
||||||
|
.await
|
||||||
|
.expect("Failed to send network refresh");
|
||||||
|
sleep(Duration::from_secs(interval.networks())).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// system refresh
|
||||||
|
{
|
||||||
|
let tx = refresh_tx;
|
||||||
|
spawn(async move {
|
||||||
|
loop {
|
||||||
|
tx.send(RefreshType::System)
|
||||||
|
.await
|
||||||
|
.expect("Failed to send system refresh");
|
||||||
|
sleep(Duration::from_secs(interval.system())).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
let mut sys = System::new_all();
|
let mut format_info = HashMap::new();
|
||||||
|
|
||||||
loop {
|
while let Some(refresh) = refresh_rx.recv().await {
|
||||||
sys.refresh_all();
|
match refresh {
|
||||||
|
RefreshType::Memory => refresh_memory_tokens(&mut format_info, &mut sys),
|
||||||
|
RefreshType::Cpu => refresh_cpu_tokens(&mut format_info, &mut sys),
|
||||||
|
RefreshType::Temps => refresh_temp_tokens(&mut format_info, &mut sys),
|
||||||
|
RefreshType::Disks => refresh_disk_tokens(&mut format_info, &mut sys),
|
||||||
|
RefreshType::Network => {
|
||||||
|
refresh_network_tokens(&mut format_info, &mut sys, interval.networks());
|
||||||
|
}
|
||||||
|
RefreshType::System => refresh_system_tokens(&mut format_info, &sys),
|
||||||
|
};
|
||||||
|
|
||||||
let mut format_info = HashMap::new();
|
tx.send(ModuleUpdateEvent::Update(format_info.clone()))
|
||||||
|
|
||||||
let actual_used_memory = sys.total_memory() - sys.available_memory();
|
|
||||||
let memory_percent = actual_used_memory as f64 / sys.total_memory() as f64 * 100.0;
|
|
||||||
|
|
||||||
let cpu_percent = sys.global_cpu_info().cpu_usage();
|
|
||||||
|
|
||||||
// TODO: Add remaining format info
|
|
||||||
|
|
||||||
format_info.insert(
|
|
||||||
String::from("memory-percent"),
|
|
||||||
format!("{:0>2.0}", memory_percent),
|
|
||||||
);
|
|
||||||
format_info.insert(
|
|
||||||
String::from("cpu-percent"),
|
|
||||||
format!("{:0>2.0}", cpu_percent),
|
|
||||||
);
|
|
||||||
|
|
||||||
tx.send(ModuleUpdateEvent::Update(format_info))
|
|
||||||
.await
|
.await
|
||||||
.expect("Failed to send system info map");
|
.expect("Failed to send system info map");
|
||||||
|
|
||||||
sleep(tokio::time::Duration::from_secs(1)).await;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -66,14 +238,18 @@ impl Module<gtk::Box> for SysInfoModule {
|
|||||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||||
info: &ModuleInfo,
|
info: &ModuleInfo,
|
||||||
) -> Result<ModuleWidget<gtk::Box>> {
|
) -> Result<ModuleWidget<gtk::Box>> {
|
||||||
let re = Regex::new(r"\{([\w-]+)}")?;
|
let re = Regex::new(r"\{([^}]+)}")?;
|
||||||
|
|
||||||
let container = gtk::Box::new(info.bar_position.get_orientation(), 10);
|
let container = gtk::Box::new(info.bar_position.get_orientation(), 10);
|
||||||
|
|
||||||
let mut labels = Vec::new();
|
let mut labels = Vec::new();
|
||||||
|
|
||||||
for format in &self.format {
|
for format in &self.format {
|
||||||
let label = Label::builder().label(format).name("item").build();
|
let label = Label::builder()
|
||||||
|
.label(format)
|
||||||
|
.use_markup(true)
|
||||||
|
.name("item")
|
||||||
|
.build();
|
||||||
label.set_angle(info.bar_position.get_angle());
|
label.set_angle(info.bar_position.get_angle());
|
||||||
container.add(&label);
|
container.add(&label);
|
||||||
labels.push(label);
|
labels.push(label);
|
||||||
@@ -83,13 +259,13 @@ impl Module<gtk::Box> for SysInfoModule {
|
|||||||
let formats = self.format;
|
let formats = self.format;
|
||||||
context.widget_rx.attach(None, move |info| {
|
context.widget_rx.attach(None, move |info| {
|
||||||
for (format, label) in formats.iter().zip(labels.clone()) {
|
for (format, label) in formats.iter().zip(labels.clone()) {
|
||||||
let format_compiled = re.replace(format, |caps: &Captures| {
|
let format_compiled = re.replace_all(format, |caps: &Captures| {
|
||||||
info.get(&caps[1])
|
info.get(&caps[1])
|
||||||
.unwrap_or(&caps[0].to_string())
|
.unwrap_or(&caps[0].to_string())
|
||||||
.to_string()
|
.to_string()
|
||||||
});
|
});
|
||||||
|
|
||||||
label.set_text(format_compiled.as_ref());
|
label.set_markup(format_compiled.as_ref());
|
||||||
}
|
}
|
||||||
|
|
||||||
Continue(true)
|
Continue(true)
|
||||||
@@ -102,3 +278,175 @@ impl Module<gtk::Box> for SysInfoModule {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn refresh_memory_tokens(format_info: &mut HashMap<String, String>, sys: &mut System) {
|
||||||
|
sys.refresh_memory();
|
||||||
|
|
||||||
|
let total_memory = sys.total_memory();
|
||||||
|
let available_memory = sys.available_memory();
|
||||||
|
|
||||||
|
let actual_used_memory = total_memory - available_memory;
|
||||||
|
let memory_percent = actual_used_memory as f64 / total_memory as f64 * 100.0;
|
||||||
|
|
||||||
|
format_info.insert(
|
||||||
|
String::from("memory-free"),
|
||||||
|
(bytes_to_gigabytes(available_memory)).to_string(),
|
||||||
|
);
|
||||||
|
format_info.insert(
|
||||||
|
String::from("memory-used"),
|
||||||
|
(bytes_to_gigabytes(actual_used_memory)).to_string(),
|
||||||
|
);
|
||||||
|
format_info.insert(
|
||||||
|
String::from("memory-total"),
|
||||||
|
(bytes_to_gigabytes(total_memory)).to_string(),
|
||||||
|
);
|
||||||
|
format_info.insert(
|
||||||
|
String::from("memory-percent"),
|
||||||
|
format!("{:0>2.0}", memory_percent),
|
||||||
|
);
|
||||||
|
|
||||||
|
let used_swap = sys.used_swap();
|
||||||
|
let total_swap = sys.total_swap();
|
||||||
|
|
||||||
|
format_info.insert(
|
||||||
|
String::from("swap-free"),
|
||||||
|
(bytes_to_gigabytes(sys.free_swap())).to_string(),
|
||||||
|
);
|
||||||
|
format_info.insert(
|
||||||
|
String::from("swap-used"),
|
||||||
|
(bytes_to_gigabytes(used_swap)).to_string(),
|
||||||
|
);
|
||||||
|
format_info.insert(
|
||||||
|
String::from("swap-total"),
|
||||||
|
(bytes_to_gigabytes(total_swap)).to_string(),
|
||||||
|
);
|
||||||
|
format_info.insert(
|
||||||
|
String::from("swap-percent"),
|
||||||
|
format!("{:0>2.0}", used_swap as f64 / total_swap as f64 * 100.0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn refresh_cpu_tokens(format_info: &mut HashMap<String, String>, sys: &mut System) {
|
||||||
|
sys.refresh_cpu();
|
||||||
|
|
||||||
|
let cpu_info = sys.global_cpu_info();
|
||||||
|
let cpu_percent = cpu_info.cpu_usage();
|
||||||
|
|
||||||
|
format_info.insert(
|
||||||
|
String::from("cpu-percent"),
|
||||||
|
format!("{:0>2.0}", cpu_percent),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn refresh_temp_tokens(format_info: &mut HashMap<String, String>, sys: &mut System) {
|
||||||
|
sys.refresh_components();
|
||||||
|
|
||||||
|
let components = sys.components();
|
||||||
|
for component in components {
|
||||||
|
let key = component.label().replace(' ', "-");
|
||||||
|
let temp = component.temperature();
|
||||||
|
|
||||||
|
format_info.insert(format!("temp-c:{key}"), format!("{temp:.0}"));
|
||||||
|
format_info.insert(format!("temp-f:{key}"), format!("{:.0}", c_to_f(temp)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn refresh_disk_tokens(format_info: &mut HashMap<String, String>, sys: &mut System) {
|
||||||
|
sys.refresh_disks();
|
||||||
|
|
||||||
|
for disk in sys.disks() {
|
||||||
|
// replace braces to avoid conflict with regex
|
||||||
|
let key = disk
|
||||||
|
.mount_point()
|
||||||
|
.to_str()
|
||||||
|
.map(|s| s.replace('{', "").replace('}', ""));
|
||||||
|
|
||||||
|
if let Some(key) = key {
|
||||||
|
let total = disk.total_space();
|
||||||
|
let available = disk.available_space();
|
||||||
|
let used = total - available;
|
||||||
|
|
||||||
|
format_info.insert(
|
||||||
|
format!("disk-free:{key}"),
|
||||||
|
bytes_to_gigabytes(available).to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
format_info.insert(
|
||||||
|
format!("disk-used:{key}"),
|
||||||
|
bytes_to_gigabytes(used).to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
format_info.insert(
|
||||||
|
format!("disk-total:{key}"),
|
||||||
|
bytes_to_gigabytes(total).to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
format_info.insert(
|
||||||
|
format!("disk-percent:{key}"),
|
||||||
|
format!("{:0>2.0}", used as f64 / total as f64 * 100.0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn refresh_network_tokens(
|
||||||
|
format_info: &mut HashMap<String, String>,
|
||||||
|
sys: &mut System,
|
||||||
|
interval: u64,
|
||||||
|
) {
|
||||||
|
sys.refresh_networks();
|
||||||
|
|
||||||
|
for (iface, network) in sys.networks() {
|
||||||
|
format_info.insert(
|
||||||
|
format!("net-down:{iface}"),
|
||||||
|
format!("{:0>2.0}", bytes_to_megabits(network.received()) / interval),
|
||||||
|
);
|
||||||
|
|
||||||
|
format_info.insert(
|
||||||
|
format!("net-up:{iface}"),
|
||||||
|
format!(
|
||||||
|
"{:0>2.0}",
|
||||||
|
bytes_to_megabits(network.transmitted()) / interval
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn refresh_system_tokens(format_info: &mut HashMap<String, String>, sys: &System) {
|
||||||
|
// no refresh required for these tokens
|
||||||
|
|
||||||
|
let load_average = sys.load_average();
|
||||||
|
format_info.insert(String::from("load-average:1"), load_average.one.to_string());
|
||||||
|
|
||||||
|
format_info.insert(
|
||||||
|
String::from("load-average:5"),
|
||||||
|
load_average.five.to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
format_info.insert(
|
||||||
|
String::from("load-average:15"),
|
||||||
|
load_average.fifteen.to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let uptime = Duration::from_secs(sys.uptime()).as_secs();
|
||||||
|
let hours = uptime / 3600;
|
||||||
|
format_info.insert(
|
||||||
|
String::from("uptime"),
|
||||||
|
format!("{:0>2}:{:0>2}", hours, (uptime % 3600) / 60),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts celsius to fahrenheit.
|
||||||
|
fn c_to_f(c: f32) -> f32 {
|
||||||
|
c * 9.0 / 5.0 + 32.0
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn bytes_to_gigabytes(b: u64) -> u64 {
|
||||||
|
const BYTES_IN_GIGABYTE: u64 = 1_000_000_000;
|
||||||
|
b / BYTES_IN_GIGABYTE
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn bytes_to_megabits(b: u64) -> u64 {
|
||||||
|
const BYTES_IN_MEGABIT: u64 = 125_000;
|
||||||
|
b / BYTES_IN_MEGABIT
|
||||||
|
}
|
||||||
|
|||||||
35
src/script.rs
Normal file
35
src/script.rs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
use color_eyre::eyre::WrapErr;
|
||||||
|
use color_eyre::{Help, Report, Result};
|
||||||
|
use std::process::Command;
|
||||||
|
use tracing::instrument;
|
||||||
|
|
||||||
|
/// Attempts to execute a given command.
|
||||||
|
/// If the command returns status 0,
|
||||||
|
/// the `stdout` is returned.
|
||||||
|
/// Otherwise, an `Err` variant
|
||||||
|
/// containing the `stderr` is returned.
|
||||||
|
#[instrument]
|
||||||
|
pub fn exec_command(command: &str) -> Result<String> {
|
||||||
|
let output = Command::new("sh")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(command)
|
||||||
|
.output()
|
||||||
|
.wrap_err("Failed to get script output")?;
|
||||||
|
|
||||||
|
if output.status.success() {
|
||||||
|
let stdout = String::from_utf8(output.stdout)
|
||||||
|
.map(|output| output.trim().to_string())
|
||||||
|
.wrap_err("Script stdout not valid UTF-8")?;
|
||||||
|
|
||||||
|
Ok(stdout)
|
||||||
|
} else {
|
||||||
|
let stderr = String::from_utf8(output.stderr)
|
||||||
|
.map(|output| output.trim().to_string())
|
||||||
|
.wrap_err("Script stderr not valid UTF-8")?;
|
||||||
|
|
||||||
|
Err(Report::msg(stderr)
|
||||||
|
.wrap_err("Script returned non-zero error code")
|
||||||
|
.suggestion("Check the path to your script")
|
||||||
|
.suggestion("Check the script for errors"))
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/style.rs
43
src/style.rs
@@ -2,10 +2,13 @@ use color_eyre::{Help, Report};
|
|||||||
use glib::Continue;
|
use glib::Continue;
|
||||||
use gtk::prelude::CssProviderExt;
|
use gtk::prelude::CssProviderExt;
|
||||||
use gtk::{gdk, gio, CssProvider, StyleContext};
|
use gtk::{gdk, gio, CssProvider, StyleContext};
|
||||||
use notify::{Event, RecursiveMode, Result, Watcher};
|
use notify::event::{DataChange, ModifyKind};
|
||||||
|
use notify::{recommended_watcher, Event, EventKind, RecursiveMode, Result, Watcher};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::time::Duration;
|
||||||
use tokio::spawn;
|
use tokio::spawn;
|
||||||
use tracing::{error, info};
|
use tokio::time::sleep;
|
||||||
|
use tracing::{debug, error, info};
|
||||||
|
|
||||||
/// Attempts to load CSS file at the given path
|
/// Attempts to load CSS file at the given path
|
||||||
/// and attach if to the current GTK application.
|
/// and attach if to the current GTK application.
|
||||||
@@ -15,13 +18,14 @@ use tracing::{error, info};
|
|||||||
pub fn load_css(style_path: PathBuf) {
|
pub fn load_css(style_path: PathBuf) {
|
||||||
let provider = CssProvider::new();
|
let provider = CssProvider::new();
|
||||||
|
|
||||||
if let Err(err) = provider.load_from_file(&gio::File::for_path(&style_path)) {
|
match provider.load_from_file(&gio::File::for_path(&style_path)) {
|
||||||
error!("{:?}", Report::new(err)
|
Ok(()) => debug!("Loaded css from '{}'", style_path.display()),
|
||||||
|
Err(err) => error!("{:?}", Report::new(err)
|
||||||
.wrap_err("Failed to load CSS")
|
.wrap_err("Failed to load CSS")
|
||||||
.suggestion("Check the CSS file for errors")
|
.suggestion("Check the CSS file for errors")
|
||||||
.suggestion("GTK CSS uses a subset of the full CSS spec and many properties are not available. Ensure you are not using any unsupported property.")
|
.suggestion("GTK CSS uses a subset of the full CSS spec and many properties are not available. Ensure you are not using any unsupported property.")
|
||||||
);
|
)
|
||||||
}
|
};
|
||||||
|
|
||||||
let screen = gdk::Screen::default().expect("Failed to get default GTK screen");
|
let screen = gdk::Screen::default().expect("Failed to get default GTK screen");
|
||||||
StyleContext::add_provider_for_screen(&screen, &provider, 800);
|
StyleContext::add_provider_for_screen(&screen, &provider, 800);
|
||||||
@@ -29,24 +33,27 @@ pub fn load_css(style_path: PathBuf) {
|
|||||||
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
||||||
|
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
match notify::recommended_watcher(move |res: Result<Event>| match res {
|
let mut watcher = recommended_watcher(move |res: Result<Event>| match res {
|
||||||
Ok(event) => {
|
Ok(event) if event.kind == EventKind::Modify(ModifyKind::Data(DataChange::Any)) => {
|
||||||
|
debug!("{event:?}");
|
||||||
if let Some(path) = event.paths.first() {
|
if let Some(path) = event.paths.first() {
|
||||||
tx.send(path.clone())
|
tx.send(path.clone())
|
||||||
.expect("Failed to send style changed message");
|
.expect("Failed to send style changed message");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => error!("Error occurred when watching stylesheet: {:?}", e),
|
Err(e) => error!("Error occurred when watching stylesheet: {:?}", e),
|
||||||
}) {
|
_ => {}
|
||||||
Ok(mut watcher) => {
|
})
|
||||||
watcher
|
.expect("Failed to create CSS file watcher");
|
||||||
.watch(&style_path, RecursiveMode::NonRecursive)
|
|
||||||
.expect("Unexpected error when attempting to watch CSS");
|
watcher
|
||||||
}
|
.watch(&style_path, RecursiveMode::NonRecursive)
|
||||||
Err(err) => error!(
|
.expect("Failed to start CSS file watcher");
|
||||||
"{:?}",
|
debug!("Installed CSS file watcher on '{}'", style_path.display());
|
||||||
Report::new(err).wrap_err("Failed to start CSS watcher")
|
|
||||||
),
|
// avoid watcher from dropping
|
||||||
|
loop {
|
||||||
|
sleep(Duration::from_secs(1)).await;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
use super::{Env, ToplevelHandler};
|
use super::{Env, ToplevelHandler};
|
||||||
use crate::collection::Collection;
|
|
||||||
use crate::wayland::toplevel::{ToplevelEvent, ToplevelInfo};
|
use crate::wayland::toplevel::{ToplevelEvent, ToplevelInfo};
|
||||||
use crate::wayland::toplevel_manager::listen_for_toplevels;
|
use crate::wayland::toplevel_manager::listen_for_toplevels;
|
||||||
use crate::wayland::ToplevelChange;
|
use crate::wayland::ToplevelChange;
|
||||||
|
use color_eyre::Report;
|
||||||
|
use indexmap::IndexMap;
|
||||||
use smithay_client_toolkit::environment::Environment;
|
use smithay_client_toolkit::environment::Environment;
|
||||||
use smithay_client_toolkit::output::{with_output_info, OutputInfo};
|
use smithay_client_toolkit::output::{with_output_info, OutputInfo};
|
||||||
use smithay_client_toolkit::reexports::calloop;
|
use smithay_client_toolkit::reexports::calloop;
|
||||||
@@ -11,7 +12,7 @@ use std::sync::{Arc, RwLock};
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::sync::{broadcast, oneshot};
|
use tokio::sync::{broadcast, oneshot};
|
||||||
use tokio::task::spawn_blocking;
|
use tokio::task::spawn_blocking;
|
||||||
use tracing::trace;
|
use tracing::{error, trace};
|
||||||
use wayland_client::protocol::wl_seat::WlSeat;
|
use wayland_client::protocol::wl_seat::WlSeat;
|
||||||
use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::{
|
use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::{
|
||||||
zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1,
|
zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1,
|
||||||
@@ -21,7 +22,7 @@ use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::{
|
|||||||
pub struct WaylandClient {
|
pub struct WaylandClient {
|
||||||
pub outputs: Vec<OutputInfo>,
|
pub outputs: Vec<OutputInfo>,
|
||||||
pub seats: Vec<WlSeat>,
|
pub seats: Vec<WlSeat>,
|
||||||
pub toplevels: Arc<RwLock<Collection<usize, (ToplevelInfo, ZwlrForeignToplevelHandleV1)>>>,
|
pub toplevels: Arc<RwLock<IndexMap<usize, (ToplevelInfo, ZwlrForeignToplevelHandleV1)>>>,
|
||||||
toplevel_tx: broadcast::Sender<ToplevelEvent>,
|
toplevel_tx: broadcast::Sender<ToplevelEvent>,
|
||||||
_toplevel_rx: broadcast::Receiver<ToplevelEvent>,
|
_toplevel_rx: broadcast::Receiver<ToplevelEvent>,
|
||||||
}
|
}
|
||||||
@@ -35,7 +36,7 @@ impl WaylandClient {
|
|||||||
|
|
||||||
let toplevel_tx2 = toplevel_tx.clone();
|
let toplevel_tx2 = toplevel_tx.clone();
|
||||||
|
|
||||||
let toplevels = Arc::new(RwLock::new(Collection::new()));
|
let toplevels = Arc::new(RwLock::new(IndexMap::new()));
|
||||||
let toplevels2 = toplevels.clone();
|
let toplevels2 = toplevels.clone();
|
||||||
|
|
||||||
// `queue` is not send so we need to handle everything inside the task
|
// `queue` is not send so we need to handle everything inside the task
|
||||||
@@ -89,9 +90,12 @@ impl WaylandClient {
|
|||||||
|
|
||||||
loop {
|
loop {
|
||||||
// TODO: Avoid need for duration here - can we force some event when sending requests?
|
// TODO: Avoid need for duration here - can we force some event when sending requests?
|
||||||
event_loop
|
if let Err(err) = event_loop.dispatch(Duration::from_millis(50), &mut ()) {
|
||||||
.dispatch(Duration::from_millis(50), &mut ())
|
error!(
|
||||||
.expect("Failed to dispatch pending wayland events");
|
"{:?}",
|
||||||
|
Report::new(err).wrap_err("Failed to dispatch pending wayland events")
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user