33 Commits

Author SHA1 Message Date
Jake Stanger
3b04642148 chore(release): v0.7.0 2022-11-05 17:33:36 +00:00
Jake Stanger
0a331f3138 docs(readme): remove warning about outdated cargo package 2022-11-05 17:33:02 +00:00
Jake Stanger
bc625b929b refactor: clippy & fmt 2022-11-05 17:32:42 +00:00
Jake Stanger
ad77dc4e4c feat: improved logging & error handling 2022-11-05 17:32:29 +00:00
Jake Stanger
3a83bd31ab fix: able to insert duplicate keys into collection
This replaces the custom `Collection` implementation with `IndexMap` from the crate of the same name.

Fixes #28.
2022-11-05 17:32:01 +00:00
Jake Stanger
5ebc84c7b9 refactor(logging): consts for default log levels 2022-11-05 17:29:17 +00:00
Jake Stanger
51d1cd4a16 build: update deps 2022-11-01 22:56:47 +00:00
Jake Stanger
b7792a415e feat: env var to set custom css location
Set `IRONBAR_CSS` to load CSS from that path instead of regular path.
2022-11-01 13:25:46 +00:00
Jake Stanger
9f82ba58cd chore: cleanup println 2022-10-23 17:18:49 +01:00
Jake Stanger
a93700e8fd Merge pull request #27 from JakeStanger/feat/custom-widget
feat: new custom module
2022-10-23 17:11:17 +01:00
Jake Stanger
2a3fe33446 build: remove no longer required patch, reduce build times 2022-10-23 17:01:35 +01:00
Jake Stanger
3750124d8c feat: new custom module
Allows basic modules to be created from a config object, including popup content.
2022-10-23 17:01:35 +01:00
Jake Stanger
e693c1c166 fix(mpd): volume slider causing mpd server errors 2022-10-16 22:23:40 +01:00
Jake Stanger
cbd0c49e25 fix: css watcher not working 2022-10-16 22:21:51 +01:00
Jake Stanger
e23e691bc6 build: use latest release version of stray 2022-10-16 16:44:51 +01:00
Jake Stanger
be0f4c6366 chore: run fmt 2022-10-16 16:44:32 +01:00
Jake Stanger
493df6bb49 feat(mpd): add volume slider to popup 2022-10-16 16:00:18 +01:00
Jake Stanger
b4ac1c9850 Merge pull request #26 from JakeStanger/feat/sysinfo-tokens
Add loads more tokens & interval options to `sysinfo`, optimise info refreshing
2022-10-16 14:04:11 +01:00
Jake Stanger
27f6abad67 chore: format and fix clippy warnings 2022-10-16 13:54:48 +01:00
Jake Stanger
ec1d59677b feat(logging): IRONBAR_LOG and IRONBAR_FILE_LOG env vars 2022-10-16 13:42:59 +01:00
Jake Stanger
70e1b526a9 fix(logging): file log not capturing panics 2022-10-16 13:42:35 +01:00
Jake Stanger
3c43c20c6a fix: weird behaviour when config does not exist 2022-10-16 12:58:11 +01:00
Jake Stanger
b66bd788b2 fix: logging for creating bar incorrect still 2022-10-16 12:57:37 +01:00
Jake Stanger
f17ae7a415 fix(script): not parsing pango markup 2022-10-16 12:56:39 +01:00
Jake Stanger
a06c4bccca docs(examples): add full system info config 2022-10-16 01:03:36 +01:00
Jake Stanger
e4e72d8008 style(sys-info): fix clippy warnings & run fmt 2022-10-16 01:03:20 +01:00
Jake Stanger
9e6dbbd131 fix(sys-info): tokens not replaced if more than one in string 2022-10-16 01:00:43 +01:00
Jake Stanger
91c57edc73 feat(sys-info): pango markup support 2022-10-16 01:00:14 +01:00
Jake Stanger
dec402edd9 feat(sys-info): config options for refresh intervals 2022-10-16 00:59:28 +01:00
Jake Stanger
fad90fdad6 feat(sys-info): add loads more formatting tokens 2022-10-16 00:58:47 +01:00
Jake Stanger
35ce3b4d45 chore: use cargo patch block 2022-10-16 00:56:47 +01:00
Jake Stanger
27d04795af docs(readme): add warning about crate being outdated 2022-10-15 18:51:45 +01:00
JakeStanger
9d9c275313 docs: update CHANGELOG.md for v0.6.0 [skip ci] 2022-10-15 17:27:37 +00:00
24 changed files with 1167 additions and 586 deletions

View File

@@ -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

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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"] }

View File

@@ -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
![Screenshot of fully configured bar with MPD widget open](https://user-images.githubusercontent.com/5057870/184539623-92d56a44-a659-49a9-91f9-5cdc453e5dfb.png)
## 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
View 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
View 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}"
]
}
]
}

View File

@@ -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);
}
}
}

View File

@@ -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()
}
}

View File

@@ -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!(),

View File

@@ -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");

View File

@@ -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
View 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: &gtk::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)
}
}

View File

@@ -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)))?;
}

View File

@@ -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 {

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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(())?;
}

View File

@@ -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)
}

View File

@@ -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"))
}
}

View File

@@ -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
View 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"))
}
}

View File

@@ -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;
}
});

View File

@@ -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")
);
}
}
});