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/),
|
||||
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
|
||||
### :wrench: Chores
|
||||
- [`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.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.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:
|
||||
- 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]
|
||||
name = "ironbar"
|
||||
version = "0.6.0"
|
||||
version = "0.7.0"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
description = "Customisable wlroots/sway bar"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
description = "Customisable GTK Layer Shell wlroots/sway bar"
|
||||
|
||||
[dependencies]
|
||||
derive_builder = "0.11.2"
|
||||
gtk = "0.15.5"
|
||||
gtk-layer-shell = "0.4.1"
|
||||
glib = "0.15.12"
|
||||
gtk = "0.16.0"
|
||||
gtk-layer-shell = "0.5.0"
|
||||
glib = "0.16.2"
|
||||
tokio = { version = "1.21.2", features = ["macros", "rt-multi-thread", "time"] }
|
||||
tracing = "0.1.36"
|
||||
tracing-subscriber = { version = "0.3.15", features = ["env-filter"] }
|
||||
tracing = "0.1.37"
|
||||
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
|
||||
tracing-error = "0.2.0"
|
||||
tracing-appender = "0.2.2"
|
||||
strip-ansi-escapes = "0.1.1"
|
||||
color-eyre = "0.6.2"
|
||||
futures-util = "0.3.21"
|
||||
chrono = "0.4.19"
|
||||
serde = { version = "1.0.141", features = ["derive"] }
|
||||
serde_json = "1.0.82"
|
||||
serde_yaml = "0.9.4"
|
||||
toml = "0.5.9"
|
||||
cornfig = "0.3.0"
|
||||
libcorn = "0.4.0"
|
||||
lazy_static = "1.4.0"
|
||||
async_once = "0.2.6"
|
||||
regex = "1.6.0"
|
||||
stray = { git = "https://github.com/JakeStanger/stray.git", branch = "fix/tracing" }
|
||||
indexmap = "1.9.1"
|
||||
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"
|
||||
walkdir = "2.3.2"
|
||||
notify = "5.0.0"
|
||||
notify = { version = "5.0.0", default-features = false }
|
||||
mpd_client = "1.0.0"
|
||||
swayipc-async = { git = "https://github.com/JakeStanger/swayipc-rs.git", branch = "feat/derive-clone" }
|
||||
sysinfo = "0.26.2"
|
||||
swayipc-async = { version = "2.0.1" }
|
||||
sysinfo = "0.26.4"
|
||||
wayland-client = "0.29.5"
|
||||
wayland-protocols = { version = "0.29.5", features=["unstable_protocols", "client"] }
|
||||
smithay-client-toolkit = "0.16.0"
|
||||
wayland-protocols = { version = "0.29.5", features = ["unstable_protocols", "client"] }
|
||||
smithay-client-toolkit = { version = "0.16.0", default-features = false, features=["calloop"] }
|
||||
14
README.md
14
README.md
@@ -1,6 +1,6 @@
|
||||
# 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.
|
||||
|
||||
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
|
||||
|
||||
@@ -40,6 +39,15 @@ install target/release/ironbar ~/.local/bin/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
|
||||
|
||||
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::config::{BarPosition, ModuleConfig};
|
||||
use crate::modules::custom::ExecEvent;
|
||||
use crate::modules::launcher::{ItemEvent, LauncherUpdate};
|
||||
use crate::modules::mpd::{PlayerCommand, SongUpdate};
|
||||
use crate::modules::workspaces::WorkspaceUpdate;
|
||||
@@ -236,6 +237,9 @@ fn add_modules(
|
||||
ModuleConfig::Launcher(module) => {
|
||||
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::custom::CustomModule;
|
||||
use crate::modules::focused::FocusedModule;
|
||||
use crate::modules::launcher::LauncherModule;
|
||||
use crate::modules::mpd::MpdModule;
|
||||
@@ -15,6 +16,7 @@ use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::{env, fs};
|
||||
use tracing::instrument;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[serde(tag = "type", rename_all = "kebab-case")]
|
||||
@@ -27,6 +29,7 @@ pub enum ModuleConfig {
|
||||
Launcher(LauncherModule),
|
||||
Script(ScriptModule),
|
||||
Focused(FocusedModule),
|
||||
Custom(CustomModule),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
@@ -69,7 +72,7 @@ impl BarPosition {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Default)]
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct Config {
|
||||
#[serde(default = "default_bar_position")]
|
||||
pub position: BarPosition,
|
||||
@@ -96,14 +99,18 @@ const fn default_bar_height() -> i32 {
|
||||
impl Config {
|
||||
/// Attempts to load the config file from file,
|
||||
/// parse it and return a new instance of `Self`.
|
||||
#[instrument]
|
||||
pub fn load() -> Result<Self> {
|
||||
let config_path = if let Ok(config_path) = env::var("IRONBAR_CONFIG") {
|
||||
let path = PathBuf::from(config_path);
|
||||
if path.exists() {
|
||||
Ok(path)
|
||||
} else {
|
||||
Err(Report::msg("Specified config file does not exist")
|
||||
.note("Config file was specified using `IRONBAR_CONFIG` environment variable"))
|
||||
Err(Report::msg(format!(
|
||||
"Specified config file does not exist: {}",
|
||||
path.display()
|
||||
))
|
||||
.note("Config file was specified using `IRONBAR_CONFIG` environment variable"))
|
||||
}
|
||||
} else {
|
||||
Self::try_find_config()
|
||||
@@ -116,6 +123,7 @@ impl Config {
|
||||
/// by checking each valid format's extension.
|
||||
///
|
||||
/// Returns the path of the first valid match, if any.
|
||||
#[instrument]
|
||||
fn try_find_config() -> Result<PathBuf> {
|
||||
let config_dir = config_dir().wrap_err("Failed to locate user config dir")?;
|
||||
|
||||
@@ -135,7 +143,10 @@ impl Config {
|
||||
|
||||
match 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
|
||||
let file =
|
||||
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)?)?)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
|
||||
@@ -31,10 +31,15 @@ impl<'a> MakeWriter<'a> for MakeFileWriter {
|
||||
/// The returned `WorkerGuard` must remain in scope
|
||||
/// for the lifetime of the application for logging to file to work.
|
||||
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 filter_layer = EnvFilter::try_from_default_env().or_else(|_| EnvFilter::try_new("info"))?;
|
||||
let file_filter_layer =
|
||||
EnvFilter::try_from_default_env().or_else(|_| EnvFilter::try_new("warn"))?;
|
||||
let filter_layer =
|
||||
EnvFilter::try_from_env("IRONBAR_LOG").or_else(|_| EnvFilter::try_new(DEFAULT_LOG))?;
|
||||
|
||||
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");
|
||||
|
||||
|
||||
51
src/main.rs
51
src/main.rs
@@ -1,11 +1,11 @@
|
||||
mod bar;
|
||||
mod bridge_channel;
|
||||
mod collection;
|
||||
mod config;
|
||||
mod icon;
|
||||
mod logging;
|
||||
mod modules;
|
||||
mod popup;
|
||||
mod script;
|
||||
mod style;
|
||||
mod sway;
|
||||
mod wayland;
|
||||
@@ -19,9 +19,10 @@ use dirs::config_dir;
|
||||
use gtk::gdk::Display;
|
||||
use gtk::prelude::*;
|
||||
use gtk::Application;
|
||||
use std::env;
|
||||
use std::future::Future;
|
||||
use std::path::PathBuf;
|
||||
use std::process::exit;
|
||||
use std::{env, panic};
|
||||
use tokio::runtime::Handle;
|
||||
use tokio::task::block_in_place;
|
||||
|
||||
@@ -31,6 +32,13 @@ use wayland::WaylandClient;
|
||||
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
#[repr(i32)]
|
||||
enum ErrorCode {
|
||||
GtkDisplay = 1,
|
||||
CreateBars = 2,
|
||||
Config = 3,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// Disable backtraces by default
|
||||
@@ -42,7 +50,15 @@ async fn main() -> Result<()> {
|
||||
// otherwise file logging drops
|
||||
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!("Starting application");
|
||||
@@ -58,7 +74,7 @@ async fn main() -> Result<()> {
|
||||
|| {
|
||||
let report = Report::msg("Failed to get default GTK display");
|
||||
error!("{:?}", report);
|
||||
exit(1)
|
||||
exit(ErrorCode::GtkDisplay as i32)
|
||||
},
|
||||
|display| display,
|
||||
);
|
||||
@@ -67,30 +83,34 @@ async fn main() -> Result<()> {
|
||||
Ok(config) => config,
|
||||
Err(err) => {
|
||||
error!("{:?}", err);
|
||||
Config::default()
|
||||
exit(ErrorCode::Config as i32)
|
||||
}
|
||||
};
|
||||
debug!("Loaded config file");
|
||||
|
||||
if let Err(err) = create_bars(app, &display, wayland_client, &config) {
|
||||
error!("{:?}", err);
|
||||
exit(2);
|
||||
exit(ErrorCode::CreateBars as i32);
|
||||
}
|
||||
|
||||
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");
|
||||
error!("{:?}", report);
|
||||
exit(3);
|
||||
config_dir().map_or_else(
|
||||
|| {
|
||||
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() {
|
||||
load_css(style_path);
|
||||
debug!("Loaded CSS watcher file");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -111,7 +131,7 @@ fn create_bars(
|
||||
let outputs = wl.outputs.as_slice();
|
||||
|
||||
debug!("Received {} outputs from Wayland", outputs.len());
|
||||
debug!("Output names: {:?}", outputs);
|
||||
debug!("Outputs: {:?}", outputs);
|
||||
|
||||
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?
|
||||
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| {
|
||||
let config = config.get(monitor_name);
|
||||
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")
|
||||
.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)))?;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use super::open_state::OpenState;
|
||||
use crate::collection::Collection;
|
||||
use crate::icon::get_icon;
|
||||
use crate::modules::launcher::{ItemEvent, LauncherUpdate};
|
||||
use crate::modules::ModuleUpdateEvent;
|
||||
@@ -7,6 +6,7 @@ use crate::popup::Popup;
|
||||
use crate::wayland::ToplevelInfo;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Button, IconTheme, Image, Orientation};
|
||||
use indexmap::IndexMap;
|
||||
use std::rc::Rc;
|
||||
use std::sync::RwLock;
|
||||
use tokio::sync::mpsc::Sender;
|
||||
@@ -16,17 +16,17 @@ pub struct Item {
|
||||
pub app_id: String,
|
||||
pub favorite: bool,
|
||||
pub open_state: OpenState,
|
||||
pub windows: Collection<usize, Window>,
|
||||
pub windows: IndexMap<usize, Window>,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
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 {
|
||||
app_id,
|
||||
favorite,
|
||||
open_state,
|
||||
windows: Collection::new(),
|
||||
windows: IndexMap::new(),
|
||||
name: String::new(),
|
||||
}
|
||||
}
|
||||
@@ -78,7 +78,7 @@ impl Item {
|
||||
&self
|
||||
.windows
|
||||
.iter()
|
||||
.map(|win| &win.open_state)
|
||||
.map(|(_, win)| &win.open_state)
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
self.open_state = new_state;
|
||||
@@ -91,7 +91,7 @@ impl From<ToplevelInfo> for Item {
|
||||
let name = toplevel.title.clone();
|
||||
let app_id = toplevel.app_id.clone();
|
||||
|
||||
let mut windows = Collection::new();
|
||||
let mut windows = IndexMap::new();
|
||||
windows.insert(toplevel.id, toplevel.into());
|
||||
|
||||
Self {
|
||||
|
||||
@@ -3,7 +3,6 @@ mod open_state;
|
||||
|
||||
use self::item::{Item, ItemButton, Window};
|
||||
use self::open_state::OpenState;
|
||||
use crate::collection::Collection;
|
||||
use crate::icon::find_desktop_file;
|
||||
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||
use crate::wayland;
|
||||
@@ -12,6 +11,7 @@ use color_eyre::{Help, Report};
|
||||
use glib::Continue;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Button, IconTheme, Orientation};
|
||||
use indexmap::IndexMap;
|
||||
use serde::Deserialize;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::sync::{Arc, Mutex};
|
||||
@@ -90,8 +90,8 @@ impl Module<gtk::Box> for LauncherModule {
|
||||
Item::new(app_id.to_string(), OpenState::Closed, true),
|
||||
)
|
||||
})
|
||||
.collect::<Collection<_, _>>(),
|
||||
None => Collection::new(),
|
||||
.collect::<IndexMap<_, _>>(),
|
||||
None => IndexMap::new(),
|
||||
};
|
||||
|
||||
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");
|
||||
|
||||
for (window, _) in open_windows.clone() {
|
||||
for (_, (window, _)) in open_windows.clone() {
|
||||
let item = items.get_mut(&window.app_id);
|
||||
match item {
|
||||
Some(item) => {
|
||||
@@ -121,7 +121,7 @@ impl Module<gtk::Box> for LauncherModule {
|
||||
}
|
||||
|
||||
let items = items.iter();
|
||||
for item in items {
|
||||
for (_, item) in items {
|
||||
tx.try_send(ModuleUpdateEvent::Update(LauncherUpdate::AddItem(
|
||||
item.clone(),
|
||||
)))?;
|
||||
@@ -282,7 +282,7 @@ impl Module<gtk::Box> for LauncherModule {
|
||||
let id = match event {
|
||||
ItemEvent::FocusItem(app_id) => items
|
||||
.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::OpenItem(_) => unreachable!(),
|
||||
};
|
||||
@@ -325,7 +325,7 @@ impl Module<gtk::Box> for LauncherModule {
|
||||
let show_icons = self.show_icons;
|
||||
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();
|
||||
context.widget_rx.attach(None, move |event| {
|
||||
@@ -427,11 +427,12 @@ impl Module<gtk::Box> for LauncherModule {
|
||||
.name("popup-launcher")
|
||||
.build();
|
||||
|
||||
// we need some content to force the container to have a size
|
||||
let placeholder = Button::with_label("PLACEHOLDER");
|
||||
placeholder.set_width_request(MAX_WIDTH);
|
||||
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();
|
||||
@@ -439,11 +440,12 @@ impl Module<gtk::Box> for LauncherModule {
|
||||
match event {
|
||||
LauncherUpdate::AddItem(item) => {
|
||||
let app_id = item.app_id.clone();
|
||||
trace!("Adding item with id '{app_id}' to the popup: {item:?}");
|
||||
|
||||
let window_buttons = item
|
||||
.windows
|
||||
.into_iter()
|
||||
.map(|win| {
|
||||
.map(|(_, win)| {
|
||||
let button = Button::builder()
|
||||
.label(&clamp(&win.name))
|
||||
.height_request(40)
|
||||
@@ -468,6 +470,11 @@ impl Module<gtk::Box> for LauncherModule {
|
||||
buttons.insert(app_id, window_buttons);
|
||||
}
|
||||
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) {
|
||||
let button = Button::builder()
|
||||
.height_request(40)
|
||||
@@ -490,11 +497,17 @@ impl Module<gtk::Box> for LauncherModule {
|
||||
}
|
||||
}
|
||||
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) {
|
||||
buttons.remove(&win_id);
|
||||
}
|
||||
}
|
||||
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(button) = buttons.get(&win_id) {
|
||||
button.set_label(&title);
|
||||
@@ -509,7 +522,7 @@ impl Module<gtk::Box> for LauncherModule {
|
||||
|
||||
// add app's buttons
|
||||
if let Some(buttons) = buttons.get(&app_id) {
|
||||
for button in buttons {
|
||||
for (_, button) in buttons {
|
||||
button.style_context().add_class("popup-item");
|
||||
container.add(button);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
/// Clicking the widget opens a popup containing the current time
|
||||
/// with second-level precision and a calendar.
|
||||
pub mod clock;
|
||||
pub mod custom;
|
||||
pub mod focused;
|
||||
pub mod launcher;
|
||||
pub mod mpd;
|
||||
|
||||
@@ -59,8 +59,9 @@ impl MpdClient {
|
||||
while let Some(change) = state_changes.next().await {
|
||||
debug!("Received state change: {:?}", change);
|
||||
|
||||
if let ConnectionEvent::SubsystemChange(Subsystem::Player | Subsystem::Queue) =
|
||||
change
|
||||
if let ConnectionEvent::SubsystemChange(
|
||||
Subsystem::Player | Subsystem::Queue | Subsystem::Mixer,
|
||||
) = change
|
||||
{
|
||||
tx2.send(())?;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use dirs::{audio_dir, home_dir};
|
||||
use glib::Continue;
|
||||
use gtk::gdk_pixbuf::Pixbuf;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Button, Image, Label, Orientation};
|
||||
use gtk::{Button, Image, Label, Orientation, Scale};
|
||||
use mpd_client::commands;
|
||||
use mpd_client::responses::{PlayState, Song, Status};
|
||||
use mpd_client::tag::Tag;
|
||||
@@ -26,16 +26,20 @@ pub enum PlayerCommand {
|
||||
Previous,
|
||||
Toggle,
|
||||
Next,
|
||||
Volume(u8),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct Icons {
|
||||
/// Icon to display when playing.
|
||||
#[serde(default = "default_icon_play")]
|
||||
play: Option<String>,
|
||||
play: String,
|
||||
/// Icon to display when paused.
|
||||
#[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 {
|
||||
@@ -43,6 +47,7 @@ impl Default for Icons {
|
||||
Self {
|
||||
pause: default_icon_pause(),
|
||||
play: default_icon_play(),
|
||||
volume: default_icon_volume(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,14 +78,16 @@ fn default_format() -> String {
|
||||
String::from("{icon} {title} / {artist}")
|
||||
}
|
||||
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
fn default_icon_play() -> Option<String> {
|
||||
Some(String::from(""))
|
||||
fn default_icon_play() -> String {
|
||||
String::from("")
|
||||
}
|
||||
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
fn default_icon_pause() -> Option<String> {
|
||||
Some(String::from(""))
|
||||
fn default_icon_pause() -> String {
|
||||
String::from("")
|
||||
}
|
||||
|
||||
fn default_icon_volume() -> String {
|
||||
String::from("墳")
|
||||
}
|
||||
|
||||
fn default_music_dir() -> PathBuf {
|
||||
@@ -186,6 +193,7 @@ impl Module<Button> for MpdModule {
|
||||
Err(err) => Err(err),
|
||||
},
|
||||
PlayerCommand::Next => client.command(commands::Next).await,
|
||||
PlayerCommand::Volume(vol) => client.command(commands::SetVolume(vol)).await,
|
||||
};
|
||||
|
||||
if let Err(err) = res {
|
||||
@@ -211,18 +219,21 @@ impl Module<Button> for MpdModule {
|
||||
|
||||
let orientation = info.bar_position.get_orientation();
|
||||
|
||||
button.connect_clicked(move |button| {
|
||||
context
|
||||
.tx
|
||||
.try_send(ModuleUpdateEvent::TogglePopup(Popup::button_pos(
|
||||
{
|
||||
let tx = context.tx.clone();
|
||||
|
||||
button.connect_clicked(move |button| {
|
||||
tx.try_send(ModuleUpdateEvent::TogglePopup(Popup::button_pos(
|
||||
button,
|
||||
orientation,
|
||||
)))
|
||||
.expect("Failed to send MPD popup open event");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let button = button.clone();
|
||||
let tx = context.tx.clone();
|
||||
|
||||
context.widget_rx.attach(None, move |mut event| {
|
||||
if let Some(event) = event.take() {
|
||||
@@ -230,6 +241,8 @@ impl Module<Button> for MpdModule {
|
||||
button.show();
|
||||
} else {
|
||||
button.hide();
|
||||
tx.try_send(ModuleUpdateEvent::ClosePopup)
|
||||
.expect("Failed to send close popup message");
|
||||
}
|
||||
|
||||
Continue(true)
|
||||
@@ -285,8 +298,25 @@ impl Module<Button> for MpdModule {
|
||||
|
||||
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(&info_box);
|
||||
container.add(&volume_box);
|
||||
|
||||
let tx_prev = tx.clone();
|
||||
btn_prev.connect_clicked(move |_| {
|
||||
@@ -302,13 +332,22 @@ impl Module<Button> for MpdModule {
|
||||
.expect("Failed to send play/pause track message");
|
||||
});
|
||||
|
||||
let tx_next = tx;
|
||||
let tx_next = tx.clone();
|
||||
btn_next.connect_clicked(move |_| {
|
||||
tx_next
|
||||
.try_send(PlayerCommand::Next)
|
||||
.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();
|
||||
|
||||
{
|
||||
@@ -371,6 +410,8 @@ impl Module<Button> for MpdModule {
|
||||
|
||||
btn_prev.set_sensitive(enable_prev);
|
||||
btn_next.set_sensitive(enable_next);
|
||||
|
||||
volume_slider.set_value(update.status.volume as f64);
|
||||
}
|
||||
|
||||
Continue(true)
|
||||
@@ -406,8 +447,8 @@ fn get_token_value(song: &Song, status: &Status, icons: &Icons, token: &str) ->
|
||||
"icon" => {
|
||||
let icon = match status.state {
|
||||
PlayState::Stopped => None,
|
||||
PlayState::Playing => icons.play.as_ref(),
|
||||
PlayState::Paused => icons.pause.as_ref(),
|
||||
PlayState::Playing => Some(&icons.play),
|
||||
PlayState::Paused => Some(&icons.pause),
|
||||
};
|
||||
icon.map(String::as_str)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
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::Label;
|
||||
use serde::Deserialize;
|
||||
use std::process::Command;
|
||||
use tokio::spawn;
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
use tokio::time::sleep;
|
||||
use tracing::{error, instrument};
|
||||
use tracing::error;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct ScriptModule {
|
||||
@@ -37,7 +37,7 @@ impl Module<Label> for ScriptModule {
|
||||
let path = self.path.clone();
|
||||
spawn(async move {
|
||||
loop {
|
||||
match run_script(&path) {
|
||||
match exec_command(&path) {
|
||||
Ok(stdout) => tx
|
||||
.send(ModuleUpdateEvent::Update(stdout))
|
||||
.await
|
||||
@@ -63,7 +63,7 @@ impl Module<Label> for ScriptModule {
|
||||
{
|
||||
let label = label.clone();
|
||||
context.widget_rx.attach(None, move |s| {
|
||||
label.set_label(s.as_str());
|
||||
label.set_markup(s.as_str());
|
||||
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 serde::Deserialize;
|
||||
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::sync::mpsc;
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
use tokio::time::sleep;
|
||||
|
||||
@@ -14,6 +16,96 @@ use tokio::time::sleep;
|
||||
pub struct SysInfoModule {
|
||||
/// List of formatting strings.
|
||||
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 {
|
||||
@@ -26,35 +118,115 @@ impl Module<gtk::Box> for SysInfoModule {
|
||||
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
||||
_rx: Receiver<Self::ReceiveMessage>,
|
||||
) -> 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 {
|
||||
let mut sys = System::new_all();
|
||||
let mut format_info = HashMap::new();
|
||||
|
||||
loop {
|
||||
sys.refresh_all();
|
||||
while let Some(refresh) = refresh_rx.recv().await {
|
||||
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();
|
||||
|
||||
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))
|
||||
tx.send(ModuleUpdateEvent::Update(format_info.clone()))
|
||||
.await
|
||||
.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>,
|
||||
info: &ModuleInfo,
|
||||
) -> 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 mut labels = Vec::new();
|
||||
|
||||
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());
|
||||
container.add(&label);
|
||||
labels.push(label);
|
||||
@@ -83,13 +259,13 @@ impl Module<gtk::Box> for SysInfoModule {
|
||||
let formats = self.format;
|
||||
context.widget_rx.attach(None, move |info| {
|
||||
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])
|
||||
.unwrap_or(&caps[0].to_string())
|
||||
.to_string()
|
||||
});
|
||||
|
||||
label.set_text(format_compiled.as_ref());
|
||||
label.set_markup(format_compiled.as_ref());
|
||||
}
|
||||
|
||||
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 gtk::prelude::CssProviderExt;
|
||||
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::time::Duration;
|
||||
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
|
||||
/// and attach if to the current GTK application.
|
||||
@@ -15,13 +18,14 @@ use tracing::{error, info};
|
||||
pub fn load_css(style_path: PathBuf) {
|
||||
let provider = CssProvider::new();
|
||||
|
||||
if let Err(err) = provider.load_from_file(&gio::File::for_path(&style_path)) {
|
||||
error!("{:?}", Report::new(err)
|
||||
match provider.load_from_file(&gio::File::for_path(&style_path)) {
|
||||
Ok(()) => debug!("Loaded css from '{}'", style_path.display()),
|
||||
Err(err) => error!("{:?}", Report::new(err)
|
||||
.wrap_err("Failed to load CSS")
|
||||
.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.")
|
||||
);
|
||||
}
|
||||
)
|
||||
};
|
||||
|
||||
let screen = gdk::Screen::default().expect("Failed to get default GTK screen");
|
||||
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);
|
||||
|
||||
spawn(async move {
|
||||
match notify::recommended_watcher(move |res: Result<Event>| match res {
|
||||
Ok(event) => {
|
||||
let mut watcher = recommended_watcher(move |res: Result<Event>| match res {
|
||||
Ok(event) if event.kind == EventKind::Modify(ModifyKind::Data(DataChange::Any)) => {
|
||||
debug!("{event:?}");
|
||||
if let Some(path) = event.paths.first() {
|
||||
tx.send(path.clone())
|
||||
.expect("Failed to send style changed message");
|
||||
}
|
||||
}
|
||||
Err(e) => error!("Error occurred when watching stylesheet: {:?}", e),
|
||||
}) {
|
||||
Ok(mut watcher) => {
|
||||
watcher
|
||||
.watch(&style_path, RecursiveMode::NonRecursive)
|
||||
.expect("Unexpected error when attempting to watch CSS");
|
||||
}
|
||||
Err(err) => error!(
|
||||
"{:?}",
|
||||
Report::new(err).wrap_err("Failed to start CSS watcher")
|
||||
),
|
||||
_ => {}
|
||||
})
|
||||
.expect("Failed to create CSS file watcher");
|
||||
|
||||
watcher
|
||||
.watch(&style_path, RecursiveMode::NonRecursive)
|
||||
.expect("Failed to start CSS file watcher");
|
||||
debug!("Installed CSS file watcher on '{}'", style_path.display());
|
||||
|
||||
// avoid watcher from dropping
|
||||
loop {
|
||||
sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use super::{Env, ToplevelHandler};
|
||||
use crate::collection::Collection;
|
||||
use crate::wayland::toplevel::{ToplevelEvent, ToplevelInfo};
|
||||
use crate::wayland::toplevel_manager::listen_for_toplevels;
|
||||
use crate::wayland::ToplevelChange;
|
||||
use color_eyre::Report;
|
||||
use indexmap::IndexMap;
|
||||
use smithay_client_toolkit::environment::Environment;
|
||||
use smithay_client_toolkit::output::{with_output_info, OutputInfo};
|
||||
use smithay_client_toolkit::reexports::calloop;
|
||||
@@ -11,7 +12,7 @@ use std::sync::{Arc, RwLock};
|
||||
use std::time::Duration;
|
||||
use tokio::sync::{broadcast, oneshot};
|
||||
use tokio::task::spawn_blocking;
|
||||
use tracing::trace;
|
||||
use tracing::{error, trace};
|
||||
use wayland_client::protocol::wl_seat::WlSeat;
|
||||
use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::{
|
||||
zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1,
|
||||
@@ -21,7 +22,7 @@ use wayland_protocols::wlr::unstable::foreign_toplevel::v1::client::{
|
||||
pub struct WaylandClient {
|
||||
pub outputs: Vec<OutputInfo>,
|
||||
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_rx: broadcast::Receiver<ToplevelEvent>,
|
||||
}
|
||||
@@ -35,7 +36,7 @@ impl WaylandClient {
|
||||
|
||||
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();
|
||||
|
||||
// `queue` is not send so we need to handle everything inside the task
|
||||
@@ -89,9 +90,12 @@ impl WaylandClient {
|
||||
|
||||
loop {
|
||||
// TODO: Avoid need for duration here - can we force some event when sending requests?
|
||||
event_loop
|
||||
.dispatch(Duration::from_millis(50), &mut ())
|
||||
.expect("Failed to dispatch pending wayland events");
|
||||
if let Err(err) = event_loop.dispatch(Duration::from_millis(50), &mut ()) {
|
||||
error!(
|
||||
"{:?}",
|
||||
Report::new(err).wrap_err("Failed to dispatch pending wayland events")
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user