mirror of
https://github.com/Zedfrigg/ironbar.git
synced 2025-08-17 06:41:03 +02:00
feat(sysinfo): overhaul to add aggregate/unit/formatting support
This completely reworks the sysinfo module to add support for aggregate functions, better support for working with individual devices, the ability to specify units, and some string formatting support. Several new tokens have also been added, and performance should be marginally improved. BREAKING CHANGE: Use of the `sys_info` module in your config will need to be updated to use the new token format. See the wiki page for more info.
This commit is contained in:
parent
49ab7e0c7b
commit
01de0ac6f5
14 changed files with 1633 additions and 589 deletions
|
@ -21,6 +21,8 @@ pub mod networkmanager;
|
|||
pub mod sway;
|
||||
#[cfg(feature = "notifications")]
|
||||
pub mod swaync;
|
||||
#[cfg(feature = "sys_info")]
|
||||
pub mod sysinfo;
|
||||
#[cfg(feature = "tray")]
|
||||
pub mod tray;
|
||||
#[cfg(feature = "upower")]
|
||||
|
@ -54,6 +56,8 @@ pub struct Clients {
|
|||
network_manager: Option<Arc<networkmanager::Client>>,
|
||||
#[cfg(feature = "notifications")]
|
||||
notifications: Option<Arc<swaync::Client>>,
|
||||
#[cfg(feature = "sys_info")]
|
||||
sys_info: Option<Arc<sysinfo::Client>>,
|
||||
#[cfg(feature = "tray")]
|
||||
tray: Option<Arc<tray::Client>>,
|
||||
#[cfg(feature = "upower")]
|
||||
|
@ -185,6 +189,13 @@ impl Clients {
|
|||
Ok(client)
|
||||
}
|
||||
|
||||
#[cfg(feature = "sys_info")]
|
||||
pub fn sys_info(&mut self) -> Arc<sysinfo::Client> {
|
||||
self.sys_info
|
||||
.get_or_insert_with(|| Arc::new(sysinfo::Client::new()))
|
||||
.clone()
|
||||
}
|
||||
|
||||
#[cfg(feature = "tray")]
|
||||
pub fn tray(&mut self) -> ClientResult<tray::Client> {
|
||||
let client = if let Some(client) = &self.tray {
|
||||
|
|
390
src/clients/sysinfo.rs
Normal file
390
src/clients/sysinfo.rs
Normal file
|
@ -0,0 +1,390 @@
|
|||
use crate::modules::sysinfo::Interval;
|
||||
use crate::{lock, register_client};
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Debug;
|
||||
use std::sync::Mutex;
|
||||
use sysinfo::{Components, Disks, LoadAvg, Networks, RefreshKind, System};
|
||||
|
||||
#[repr(u64)]
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum Prefix {
|
||||
#[default]
|
||||
None = 1,
|
||||
|
||||
Kilo = 1000,
|
||||
Mega = Prefix::Kilo as u64 * 1000,
|
||||
Giga = Prefix::Mega as u64 * 1000,
|
||||
Tera = Prefix::Giga as u64 * 1000,
|
||||
Peta = Prefix::Tera as u64 * 1000,
|
||||
|
||||
Kibi = 1024,
|
||||
Mebi = Prefix::Kibi as u64 * 1024,
|
||||
Gibi = Prefix::Mebi as u64 * 1024,
|
||||
Tebi = Prefix::Gibi as u64 * 1024,
|
||||
Pebi = Prefix::Tebi as u64 * 1024,
|
||||
|
||||
// # Units
|
||||
// These are special cases
|
||||
// where you'd actually want to do slightly more than a prefix alone.
|
||||
// Included as part of the prefix system for simplicity.
|
||||
KiloBit = 128,
|
||||
MegaBit = Prefix::KiloBit as u64 * 1024,
|
||||
GigaBit = Prefix::MegaBit as u64 * 1024,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Function {
|
||||
None,
|
||||
Sum,
|
||||
Min,
|
||||
Max,
|
||||
Mean,
|
||||
Name(String),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ValueSet {
|
||||
values: HashMap<Box<str>, Value>,
|
||||
}
|
||||
|
||||
impl FromIterator<(Box<str>, Value)> for ValueSet {
|
||||
fn from_iter<T: IntoIterator<Item = (Box<str>, Value)>>(iter: T) -> Self {
|
||||
Self {
|
||||
values: iter.into_iter().collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ValueSet {
|
||||
fn values(&self, prefix: Prefix) -> impl Iterator<Item = f64> + use<'_> {
|
||||
self.values
|
||||
.values()
|
||||
.map(move |v| v.get(prefix))
|
||||
.filter(|v| !v.is_nan())
|
||||
}
|
||||
|
||||
pub fn apply(&self, function: &Function, prefix: Prefix) -> f64 {
|
||||
match function {
|
||||
Function::None => 0.0,
|
||||
Function::Sum => self.sum(prefix),
|
||||
Function::Min => self.min(prefix),
|
||||
Function::Max => self.max(prefix),
|
||||
Function::Mean => self.mean(prefix),
|
||||
Function::Name(name) => self
|
||||
.values
|
||||
.get(&Box::from(name.as_str()))
|
||||
.map(|v| v.get(prefix))
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn sum(&self, prefix: Prefix) -> f64 {
|
||||
self.values(prefix).sum()
|
||||
}
|
||||
|
||||
fn min(&self, prefix: Prefix) -> f64 {
|
||||
self.values(prefix)
|
||||
.min_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn max(&self, prefix: Prefix) -> f64 {
|
||||
self.values(prefix)
|
||||
.max_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn mean(&self, prefix: Prefix) -> f64 {
|
||||
self.sum(prefix) / self.values.len() as f64
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq)]
|
||||
pub struct Value {
|
||||
value: f64,
|
||||
prefix: Prefix,
|
||||
}
|
||||
|
||||
impl Value {
|
||||
pub fn new(value: f64) -> Self {
|
||||
Self::new_with_prefix(value, Prefix::None)
|
||||
}
|
||||
|
||||
pub fn new_with_prefix(value: f64, prefix: Prefix) -> Self {
|
||||
Self { value, prefix }
|
||||
}
|
||||
|
||||
pub fn get(self, prefix: Prefix) -> f64 {
|
||||
if prefix == self.prefix {
|
||||
self.value
|
||||
} else {
|
||||
let scale = self.prefix as u64 as f64 / prefix as u64 as f64;
|
||||
self.value * scale
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Client {
|
||||
system: Mutex<System>,
|
||||
disks: Mutex<Disks>,
|
||||
components: Mutex<Components>,
|
||||
networks: Mutex<Networks>,
|
||||
load_average: Mutex<LoadAvg>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new() -> Self {
|
||||
let refresh_kind = RefreshKind::everything().without_processes();
|
||||
|
||||
let system = System::new_with_specifics(refresh_kind);
|
||||
let disks = Disks::new_with_refreshed_list();
|
||||
let components = Components::new_with_refreshed_list();
|
||||
let networks = Networks::new_with_refreshed_list();
|
||||
let load_average = System::load_average();
|
||||
|
||||
Self {
|
||||
system: Mutex::new(system),
|
||||
disks: Mutex::new(disks),
|
||||
components: Mutex::new(components),
|
||||
networks: Mutex::new(networks),
|
||||
load_average: Mutex::new(load_average),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn refresh_cpu(&self) {
|
||||
lock!(self.system).refresh_cpu_all();
|
||||
}
|
||||
|
||||
pub fn refresh_memory(&self) {
|
||||
lock!(self.system).refresh_memory();
|
||||
}
|
||||
|
||||
pub fn refresh_network(&self) {
|
||||
lock!(self.networks).refresh(true);
|
||||
}
|
||||
|
||||
pub fn refresh_temps(&self) {
|
||||
lock!(self.components).refresh(true);
|
||||
}
|
||||
|
||||
pub fn refresh_disks(&self) {
|
||||
lock!(self.disks).refresh(true);
|
||||
}
|
||||
|
||||
pub fn refresh_load_average(&self) {
|
||||
*lock!(self.load_average) = System::load_average();
|
||||
}
|
||||
|
||||
pub fn cpu_frequency(&self) -> ValueSet {
|
||||
lock!(self.system)
|
||||
.cpus()
|
||||
.iter()
|
||||
.map(|cpu| {
|
||||
(
|
||||
cpu.name().into(),
|
||||
Value::new_with_prefix(cpu.frequency() as f64, Prefix::Mega),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn cpu_percent(&self) -> ValueSet {
|
||||
lock!(self.system)
|
||||
.cpus()
|
||||
.iter()
|
||||
.map(|cpu| (cpu.name().into(), Value::new(cpu.cpu_usage() as f64)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn memory_free(&self) -> Value {
|
||||
Value::new(lock!(self.system).free_memory() as f64)
|
||||
}
|
||||
|
||||
pub fn memory_available(&self) -> Value {
|
||||
Value::new(lock!(self.system).available_memory() as f64)
|
||||
}
|
||||
|
||||
pub fn memory_total(&self) -> Value {
|
||||
Value::new(lock!(self.system).total_memory() as f64)
|
||||
}
|
||||
|
||||
pub fn memory_used(&self) -> Value {
|
||||
Value::new(lock!(self.system).used_memory() as f64)
|
||||
}
|
||||
|
||||
pub fn memory_percent(&self) -> Value {
|
||||
let total = lock!(self.system).total_memory() as f64;
|
||||
let used = lock!(self.system).used_memory() as f64;
|
||||
|
||||
Value::new(used / total * 100.0)
|
||||
}
|
||||
|
||||
pub fn swap_free(&self) -> Value {
|
||||
Value::new(lock!(self.system).free_swap() as f64)
|
||||
}
|
||||
|
||||
pub fn swap_total(&self) -> Value {
|
||||
Value::new(lock!(self.system).total_swap() as f64)
|
||||
}
|
||||
|
||||
pub fn swap_used(&self) -> Value {
|
||||
Value::new(lock!(self.system).used_swap() as f64)
|
||||
}
|
||||
pub fn swap_percent(&self) -> Value {
|
||||
let total = lock!(self.system).total_swap() as f64;
|
||||
let used = lock!(self.system).used_swap() as f64;
|
||||
|
||||
Value::new(used / total * 100.0)
|
||||
}
|
||||
|
||||
pub fn temp_c(&self) -> ValueSet {
|
||||
lock!(self.components)
|
||||
.iter()
|
||||
.map(|comp| {
|
||||
(
|
||||
comp.label().into(),
|
||||
Value::new(comp.temperature().unwrap_or_default() as f64),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn temp_f(&self) -> ValueSet {
|
||||
lock!(self.components)
|
||||
.iter()
|
||||
.map(|comp| {
|
||||
(
|
||||
comp.label().into(),
|
||||
Value::new(c_to_f(comp.temperature().unwrap_or_default() as f64)),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn disk_free(&self) -> ValueSet {
|
||||
lock!(self.disks)
|
||||
.iter()
|
||||
.map(|disk| {
|
||||
(
|
||||
disk.mount_point().to_string_lossy().into(),
|
||||
Value::new(disk.available_space() as f64),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn disk_total(&self) -> ValueSet {
|
||||
lock!(self.disks)
|
||||
.iter()
|
||||
.map(|disk| {
|
||||
(
|
||||
disk.mount_point().to_string_lossy().into(),
|
||||
Value::new(disk.total_space() as f64),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn disk_used(&self) -> ValueSet {
|
||||
lock!(self.disks)
|
||||
.iter()
|
||||
.map(|disk| {
|
||||
(
|
||||
disk.mount_point().to_string_lossy().into(),
|
||||
Value::new((disk.total_space() - disk.available_space()) as f64),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn disk_percent(&self) -> ValueSet {
|
||||
lock!(self.disks)
|
||||
.iter()
|
||||
.map(|disk| {
|
||||
(
|
||||
disk.mount_point().to_string_lossy().into(),
|
||||
Value::new(
|
||||
(disk.total_space() - disk.available_space()) as f64
|
||||
/ disk.total_space() as f64
|
||||
* 100.0,
|
||||
),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn disk_read(&self, interval: Interval) -> ValueSet {
|
||||
lock!(self.disks)
|
||||
.iter()
|
||||
.map(|disk| {
|
||||
(
|
||||
disk.mount_point().to_string_lossy().into(),
|
||||
Value::new(disk.usage().read_bytes as f64 / interval.disks() as f64),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn disk_write(&self, interval: Interval) -> ValueSet {
|
||||
lock!(self.disks)
|
||||
.iter()
|
||||
.map(|disk| {
|
||||
(
|
||||
disk.mount_point().to_string_lossy().into(),
|
||||
Value::new(disk.usage().written_bytes as f64 / interval.disks() as f64),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn net_down(&self, interval: Interval) -> ValueSet {
|
||||
lock!(self.networks)
|
||||
.iter()
|
||||
.map(|(name, net)| {
|
||||
(
|
||||
name.as_str().into(),
|
||||
Value::new(net.received() as f64 / interval.networks() as f64),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn net_up(&self, interval: Interval) -> ValueSet {
|
||||
lock!(self.networks)
|
||||
.iter()
|
||||
.map(|(name, net)| {
|
||||
(
|
||||
name.as_str().into(),
|
||||
Value::new(net.transmitted() as f64 / interval.networks() as f64),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn load_average_1(&self) -> Value {
|
||||
Value::new(lock!(self.load_average).one)
|
||||
}
|
||||
|
||||
pub fn load_average_5(&self) -> Value {
|
||||
Value::new(lock!(self.load_average).five)
|
||||
}
|
||||
|
||||
pub fn load_average_15(&self) -> Value {
|
||||
Value::new(lock!(self.load_average).fifteen)
|
||||
}
|
||||
|
||||
/// Gets system uptime formatted as `HH:mm`.
|
||||
pub fn uptime(&self) -> String {
|
||||
let uptime = System::uptime();
|
||||
let hours = uptime / 3600;
|
||||
format!("{:0>2}:{:0>2}", hours, (uptime % 3600) / 60)
|
||||
}
|
||||
}
|
||||
|
||||
register_client!(Client, sys_info);
|
||||
|
||||
const fn c_to_f(c: f64) -> f64 {
|
||||
c / 5.0 * 9.0 + 32.0
|
||||
}
|
|
@ -1,451 +0,0 @@
|
|||
use crate::config::{CommonConfig, ModuleOrientation};
|
||||
use crate::gtk_helpers::{IronbarGtkExt, IronbarLabelExt};
|
||||
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
|
||||
use crate::{glib_recv, module_impl, send_async, spawn};
|
||||
use color_eyre::Result;
|
||||
use gtk::prelude::*;
|
||||
use gtk::Label;
|
||||
use regex::{Captures, Regex};
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
use sysinfo::{ComponentExt, CpuExt, DiskExt, NetworkExt, RefreshKind, System, SystemExt};
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::time::sleep;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct SysInfoModule {
|
||||
/// List of strings including formatting tokens.
|
||||
/// For available tokens, see [below](#formatting-tokens).
|
||||
///
|
||||
/// **Required**
|
||||
format: Vec<String>,
|
||||
|
||||
/// Number of seconds between refresh.
|
||||
///
|
||||
/// This can be set as a global interval,
|
||||
/// or passed as an object to customize the interval per-system.
|
||||
///
|
||||
/// **Default**: `5`
|
||||
#[serde(default = "Interval::default")]
|
||||
interval: Interval,
|
||||
|
||||
/// The orientation of text for the labels.
|
||||
///
|
||||
/// **Valid options**: `horizontal`, `vertical`, `h`, `v`
|
||||
/// <br>
|
||||
/// **Default** : `horizontal`
|
||||
#[serde(default)]
|
||||
orientation: ModuleOrientation,
|
||||
|
||||
/// The orientation by which the labels are laid out.
|
||||
///
|
||||
/// **Valid options**: `horizontal`, `vertical`, `h`, `v`
|
||||
/// <br>
|
||||
/// **Default** : `horizontal`
|
||||
direction: Option<ModuleOrientation>,
|
||||
|
||||
/// See [common options](module-level-options#common-options).
|
||||
#[serde(flatten)]
|
||||
pub common: Option<CommonConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Copy, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct Intervals {
|
||||
/// The number of seconds between refreshing memory data.
|
||||
///
|
||||
/// **Default**: `5`
|
||||
#[serde(default = "default_interval")]
|
||||
memory: u64,
|
||||
|
||||
/// The number of seconds between refreshing CPU data.
|
||||
///
|
||||
/// **Default**: `5`
|
||||
#[serde(default = "default_interval")]
|
||||
cpu: u64,
|
||||
|
||||
/// The number of seconds between refreshing temperature data.
|
||||
///
|
||||
/// **Default**: `5`
|
||||
#[serde(default = "default_interval")]
|
||||
temps: u64,
|
||||
|
||||
/// The number of seconds between refreshing disk data.
|
||||
///
|
||||
/// **Default**: `5`
|
||||
#[serde(default = "default_interval")]
|
||||
disks: u64,
|
||||
|
||||
/// The number of seconds between refreshing network data.
|
||||
///
|
||||
/// **Default**: `5`
|
||||
#[serde(default = "default_interval")]
|
||||
networks: u64,
|
||||
|
||||
/// The number of seconds between refreshing system data.
|
||||
///
|
||||
/// **Default**: `5`
|
||||
#[serde(default = "default_interval")]
|
||||
system: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Copy, Clone)]
|
||||
#[serde(untagged)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
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 {
|
||||
type SendMessage = HashMap<String, String>;
|
||||
type ReceiveMessage = ();
|
||||
|
||||
module_impl!("sysinfo");
|
||||
|
||||
fn spawn_controller(
|
||||
&self,
|
||||
_info: &ModuleInfo,
|
||||
context: &WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
_rx: mpsc::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);
|
||||
|
||||
macro_rules! spawn_refresh {
|
||||
($refresh_type:expr, $func:ident) => {{
|
||||
let tx = refresh_tx.clone();
|
||||
spawn(async move {
|
||||
loop {
|
||||
send_async!(tx, $refresh_type);
|
||||
sleep(Duration::from_secs(interval.$func())).await;
|
||||
}
|
||||
});
|
||||
}};
|
||||
}
|
||||
|
||||
spawn_refresh!(RefreshType::Memory, memory);
|
||||
spawn_refresh!(RefreshType::Cpu, cpu);
|
||||
spawn_refresh!(RefreshType::Temps, temps);
|
||||
spawn_refresh!(RefreshType::Disks, disks);
|
||||
spawn_refresh!(RefreshType::Network, networks);
|
||||
spawn_refresh!(RefreshType::System, system);
|
||||
|
||||
let tx = context.tx.clone();
|
||||
spawn(async move {
|
||||
let mut format_info = HashMap::new();
|
||||
|
||||
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),
|
||||
};
|
||||
|
||||
send_async!(tx, ModuleUpdateEvent::Update(format_info.clone()));
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn into_widget(
|
||||
self,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
_info: &ModuleInfo,
|
||||
) -> Result<ModuleParts<gtk::Box>> {
|
||||
let re = Regex::new(r"\{([^}]+)}")?;
|
||||
|
||||
let layout = match self.direction {
|
||||
Some(orientation) => orientation,
|
||||
None => self.orientation,
|
||||
};
|
||||
|
||||
let container = gtk::Box::new(layout.into(), 10);
|
||||
|
||||
let mut labels = Vec::new();
|
||||
|
||||
for format in &self.format {
|
||||
let label = Label::builder().label(format).use_markup(true).build();
|
||||
|
||||
label.add_class("item");
|
||||
label.set_angle(self.orientation.to_angle());
|
||||
|
||||
container.add(&label);
|
||||
labels.push(label);
|
||||
}
|
||||
|
||||
{
|
||||
let formats = self.format;
|
||||
glib_recv!(context.subscribe(), info => {
|
||||
for (format, label) in formats.iter().zip(labels.clone()) {
|
||||
let format_compiled = re.replace_all(format, |caps: &Captures| {
|
||||
info.get(&caps[1])
|
||||
.unwrap_or(&caps[0].to_string())
|
||||
.to_string()
|
||||
});
|
||||
|
||||
label.set_label_escaped(format_compiled.as_ref());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Ok(ModuleParts {
|
||||
widget: container,
|
||||
popup: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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!("{memory_percent:0>2.0}"),
|
||||
);
|
||||
|
||||
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!("{cpu_percent:0>2.0}"));
|
||||
}
|
||||
|
||||
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(['{', '}'], ""));
|
||||
|
||||
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"),
|
||||
format!("{:.2}", load_average.one),
|
||||
);
|
||||
|
||||
format_info.insert(
|
||||
String::from("load_average:5"),
|
||||
format!("{:.2}", load_average.five),
|
||||
);
|
||||
|
||||
format_info.insert(
|
||||
String::from("load_average:15"),
|
||||
format!("{:.2}", load_average.fifteen),
|
||||
);
|
||||
|
||||
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
|
||||
}
|
314
src/modules/sysinfo/mod.rs
Normal file
314
src/modules/sysinfo/mod.rs
Normal file
|
@ -0,0 +1,314 @@
|
|||
mod parser;
|
||||
mod renderer;
|
||||
mod token;
|
||||
|
||||
use crate::config::{CommonConfig, ModuleOrientation};
|
||||
use crate::gtk_helpers::{IronbarGtkExt, IronbarLabelExt};
|
||||
use crate::modules::sysinfo::token::{Part, TokenType};
|
||||
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
|
||||
use crate::{clients, glib_recv, module_impl, send_async, spawn, try_send};
|
||||
use color_eyre::Result;
|
||||
use gtk::prelude::*;
|
||||
use gtk::Label;
|
||||
use serde::Deserialize;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::time::sleep;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct SysInfoModule {
|
||||
/// List of strings including formatting tokens.
|
||||
/// For available tokens, see [below](#formatting-tokens).
|
||||
///
|
||||
/// **Required**
|
||||
format: Vec<String>,
|
||||
|
||||
/// Number of seconds between refresh.
|
||||
///
|
||||
/// This can be set as a global interval,
|
||||
/// or passed as an object to customize the interval per-system.
|
||||
///
|
||||
/// **Default**: `5`
|
||||
#[serde(default = "Interval::default")]
|
||||
interval: Interval,
|
||||
|
||||
/// The orientation of text for the labels.
|
||||
///
|
||||
/// **Valid options**: `horizontal`, `vertical`, `h`, `v`
|
||||
/// <br>
|
||||
/// **Default** : `horizontal`
|
||||
#[serde(default)]
|
||||
orientation: ModuleOrientation,
|
||||
|
||||
/// The orientation by which the labels are laid out.
|
||||
///
|
||||
/// **Valid options**: `horizontal`, `vertical`, `h`, `v`
|
||||
/// <br>
|
||||
/// **Default** : `horizontal`
|
||||
direction: Option<ModuleOrientation>,
|
||||
|
||||
/// See [common options](module-level-options#common-options).
|
||||
#[serde(flatten)]
|
||||
pub common: Option<CommonConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Copy, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct Intervals {
|
||||
/// The number of seconds between refreshing memory data.
|
||||
///
|
||||
/// **Default**: `5`
|
||||
#[serde(default = "default_interval")]
|
||||
memory: u64,
|
||||
|
||||
/// The number of seconds between refreshing CPU data.
|
||||
///
|
||||
/// **Default**: `5`
|
||||
#[serde(default = "default_interval")]
|
||||
cpu: u64,
|
||||
|
||||
/// The number of seconds between refreshing temperature data.
|
||||
///
|
||||
/// **Default**: `5`
|
||||
#[serde(default = "default_interval")]
|
||||
temps: u64,
|
||||
|
||||
/// The number of seconds between refreshing disk data.
|
||||
///
|
||||
/// **Default**: `5`
|
||||
#[serde(default = "default_interval")]
|
||||
disks: u64,
|
||||
|
||||
/// The number of seconds between refreshing network data.
|
||||
///
|
||||
/// **Default**: `5`
|
||||
#[serde(default = "default_interval")]
|
||||
networks: u64,
|
||||
|
||||
/// The number of seconds between refreshing system data.
|
||||
///
|
||||
/// **Default**: `5`
|
||||
#[serde(default = "default_interval")]
|
||||
system: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Copy, Clone)]
|
||||
#[serde(untagged)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn disks(self) -> u64 {
|
||||
match self {
|
||||
Self::All(n) => n,
|
||||
Self::Individual(intervals) => intervals.disks,
|
||||
}
|
||||
}
|
||||
|
||||
pub 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, Clone, Copy, Eq, PartialEq)]
|
||||
enum RefreshType {
|
||||
Memory,
|
||||
Cpu,
|
||||
Temps,
|
||||
Disks,
|
||||
Network,
|
||||
System,
|
||||
}
|
||||
|
||||
impl TokenType {
|
||||
fn is_affected_by(self, refresh_type: RefreshType) -> bool {
|
||||
match self {
|
||||
Self::CpuFrequency | Self::CpuPercent => refresh_type == RefreshType::Cpu,
|
||||
Self::MemoryFree
|
||||
| Self::MemoryAvailable
|
||||
| Self::MemoryTotal
|
||||
| Self::MemoryUsed
|
||||
| Self::MemoryPercent
|
||||
| Self::SwapFree
|
||||
| Self::SwapTotal
|
||||
| Self::SwapUsed
|
||||
| Self::SwapPercent => refresh_type == RefreshType::Memory,
|
||||
Self::TempC | Self::TempF => refresh_type == RefreshType::Temps,
|
||||
Self::DiskFree
|
||||
| Self::DiskTotal
|
||||
| Self::DiskUsed
|
||||
| Self::DiskPercent
|
||||
| Self::DiskRead
|
||||
| Self::DiskWrite => refresh_type == RefreshType::Disks,
|
||||
Self::NetDown | Self::NetUp => refresh_type == RefreshType::Network,
|
||||
Self::LoadAverage1 | Self::LoadAverage5 | Self::LoadAverage15 => {
|
||||
refresh_type == RefreshType::System
|
||||
}
|
||||
Self::Uptime => refresh_type == RefreshType::System,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Module<gtk::Box> for SysInfoModule {
|
||||
type SendMessage = (usize, String);
|
||||
type ReceiveMessage = ();
|
||||
|
||||
module_impl!("sysinfo");
|
||||
|
||||
fn spawn_controller(
|
||||
&self,
|
||||
_info: &ModuleInfo,
|
||||
context: &WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
_rx: mpsc::Receiver<Self::ReceiveMessage>,
|
||||
) -> Result<()> {
|
||||
let interval = self.interval;
|
||||
|
||||
let client = context.client::<clients::sysinfo::Client>();
|
||||
|
||||
let format_tokens = self
|
||||
.format
|
||||
.iter()
|
||||
.map(|format| parser::parse_input(format.as_str()))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
for (i, token_set) in format_tokens.iter().enumerate() {
|
||||
let rendered = Part::render_all(token_set, &client, interval);
|
||||
try_send!(context.tx, ModuleUpdateEvent::Update((i, rendered)));
|
||||
}
|
||||
|
||||
let (refresh_tx, mut refresh_rx) = mpsc::channel(16);
|
||||
|
||||
macro_rules! spawn_refresh {
|
||||
($refresh_type:expr, $func:ident) => {{
|
||||
let tx = refresh_tx.clone();
|
||||
spawn(async move {
|
||||
loop {
|
||||
send_async!(tx, $refresh_type);
|
||||
sleep(Duration::from_secs(interval.$func())).await;
|
||||
}
|
||||
});
|
||||
}};
|
||||
}
|
||||
|
||||
spawn_refresh!(RefreshType::Memory, memory);
|
||||
spawn_refresh!(RefreshType::Cpu, cpu);
|
||||
spawn_refresh!(RefreshType::Temps, temps);
|
||||
spawn_refresh!(RefreshType::Disks, disks);
|
||||
spawn_refresh!(RefreshType::Network, networks);
|
||||
spawn_refresh!(RefreshType::System, system);
|
||||
|
||||
let tx = context.tx.clone();
|
||||
spawn(async move {
|
||||
while let Some(refresh) = refresh_rx.recv().await {
|
||||
match refresh {
|
||||
RefreshType::Memory => client.refresh_memory(),
|
||||
RefreshType::Cpu => client.refresh_cpu(),
|
||||
RefreshType::Temps => client.refresh_temps(),
|
||||
RefreshType::Disks => client.refresh_disks(),
|
||||
RefreshType::Network => client.refresh_network(),
|
||||
RefreshType::System => client.refresh_load_average(),
|
||||
};
|
||||
|
||||
for (i, token_set) in format_tokens.iter().enumerate() {
|
||||
let is_affected = token_set
|
||||
.iter()
|
||||
.filter_map(|part| {
|
||||
if let Part::Token(token) = part {
|
||||
Some(token)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.any(|t| t.token.is_affected_by(refresh));
|
||||
|
||||
if is_affected {
|
||||
let rendered = Part::render_all(token_set, &client, interval);
|
||||
send_async!(tx, ModuleUpdateEvent::Update((i, rendered)));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn into_widget(
|
||||
self,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
_info: &ModuleInfo,
|
||||
) -> Result<ModuleParts<gtk::Box>> {
|
||||
let layout = match self.direction {
|
||||
Some(orientation) => orientation,
|
||||
None => self.orientation,
|
||||
};
|
||||
|
||||
let container = gtk::Box::new(layout.into(), 10);
|
||||
|
||||
let mut labels = Vec::new();
|
||||
|
||||
for _ in &self.format {
|
||||
let label = Label::builder().use_markup(true).build();
|
||||
|
||||
label.add_class("item");
|
||||
label.set_angle(self.orientation.to_angle());
|
||||
|
||||
container.add(&label);
|
||||
labels.push(label);
|
||||
}
|
||||
|
||||
glib_recv!(context.subscribe(), data => {
|
||||
let label = &labels[data.0];
|
||||
label.set_label_escaped(&data.1);
|
||||
});
|
||||
|
||||
Ok(ModuleParts {
|
||||
widget: container,
|
||||
popup: None,
|
||||
})
|
||||
}
|
||||
}
|
460
src/modules/sysinfo/parser.rs
Normal file
460
src/modules/sysinfo/parser.rs
Normal file
|
@ -0,0 +1,460 @@
|
|||
use crate::clients::sysinfo::{Function, Prefix};
|
||||
use crate::modules::sysinfo::token::{Alignment, Formatting, Part, Token, TokenType};
|
||||
use color_eyre::{Report, Result};
|
||||
use std::iter::Peekable;
|
||||
use std::str::{Chars, FromStr};
|
||||
|
||||
impl FromStr for TokenType {
|
||||
type Err = Report;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
match s {
|
||||
"cpu_frequency" => Ok(Self::CpuFrequency),
|
||||
"cpu_percent" => Ok(Self::CpuPercent),
|
||||
|
||||
"memory_free" => Ok(Self::MemoryFree),
|
||||
"memory_available" => Ok(Self::MemoryAvailable),
|
||||
"memory_total" => Ok(Self::MemoryTotal),
|
||||
"memory_used" => Ok(Self::MemoryUsed),
|
||||
"memory_percent" => Ok(Self::MemoryPercent),
|
||||
|
||||
"swap_free" => Ok(Self::SwapFree),
|
||||
"swap_total" => Ok(Self::SwapTotal),
|
||||
"swap_used" => Ok(Self::SwapUsed),
|
||||
"swap_percent" => Ok(Self::SwapPercent),
|
||||
|
||||
"temp_c" => Ok(Self::TempC),
|
||||
"temp_f" => Ok(Self::TempF),
|
||||
|
||||
"disk_free" => Ok(Self::DiskFree),
|
||||
"disk_total" => Ok(Self::DiskTotal),
|
||||
"disk_used" => Ok(Self::DiskUsed),
|
||||
"disk_percent" => Ok(Self::DiskPercent),
|
||||
"disk_read" => Ok(Self::DiskRead),
|
||||
"disk_write" => Ok(Self::DiskWrite),
|
||||
|
||||
"net_down" => Ok(Self::NetDown),
|
||||
"net_up" => Ok(Self::NetUp),
|
||||
|
||||
"load_average_1" => Ok(Self::LoadAverage1),
|
||||
"load_average_5" => Ok(Self::LoadAverage5),
|
||||
"load_average_15" => Ok(Self::LoadAverage15),
|
||||
"uptime" => Ok(Self::Uptime),
|
||||
_ => Err(Report::msg(format!("invalid token type: '{s}'"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Function {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"sum" => Ok(Self::Sum),
|
||||
"min" => Ok(Self::Min),
|
||||
"max" => Ok(Self::Max),
|
||||
"mean" => Ok(Self::Mean),
|
||||
"" => Err(()),
|
||||
_ => Ok(Self::Name(s.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Function {
|
||||
pub(crate) fn default_for(token_type: TokenType) -> Self {
|
||||
match token_type {
|
||||
TokenType::CpuFrequency
|
||||
| TokenType::CpuPercent
|
||||
| TokenType::TempC
|
||||
| TokenType::DiskPercent => Self::Mean,
|
||||
TokenType::DiskFree
|
||||
| TokenType::DiskTotal
|
||||
| TokenType::DiskUsed
|
||||
| TokenType::DiskRead
|
||||
| TokenType::DiskWrite
|
||||
| TokenType::NetDown
|
||||
| TokenType::NetUp => Self::Sum,
|
||||
_ => Self::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Prefix {
|
||||
pub(crate) fn default_for(token_type: TokenType) -> Self {
|
||||
match token_type {
|
||||
TokenType::CpuFrequency
|
||||
| TokenType::MemoryFree
|
||||
| TokenType::MemoryAvailable
|
||||
| TokenType::MemoryTotal
|
||||
| TokenType::MemoryUsed
|
||||
| TokenType::SwapFree
|
||||
| TokenType::SwapTotal
|
||||
| TokenType::SwapUsed
|
||||
| TokenType::DiskFree
|
||||
| TokenType::DiskTotal
|
||||
| TokenType::DiskUsed => Self::Giga,
|
||||
TokenType::DiskRead | TokenType::DiskWrite => Self::Mega,
|
||||
TokenType::NetDown | TokenType::NetUp => Self::MegaBit,
|
||||
_ => Self::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Prefix {
|
||||
type Err = Report;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
match s {
|
||||
"k" => Ok(Prefix::Kilo),
|
||||
"M" => Ok(Prefix::Mega),
|
||||
"G" => Ok(Prefix::Giga),
|
||||
"T" => Ok(Prefix::Tera),
|
||||
"P" => Ok(Prefix::Peta),
|
||||
|
||||
"ki" => Ok(Prefix::Kibi),
|
||||
"Mi" => Ok(Prefix::Mebi),
|
||||
"Gi" => Ok(Prefix::Gibi),
|
||||
"Ti" => Ok(Prefix::Tebi),
|
||||
"Pi" => Ok(Prefix::Pebi),
|
||||
|
||||
"kb" => Ok(Prefix::KiloBit),
|
||||
"Mb" => Ok(Prefix::MegaBit),
|
||||
"Gb" => Ok(Prefix::GigaBit),
|
||||
|
||||
_ => Err(Report::msg(format!("invalid prefix: {s}"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<char> for Alignment {
|
||||
type Error = Report;
|
||||
|
||||
fn try_from(value: char) -> Result<Self> {
|
||||
match value {
|
||||
'<' => Ok(Self::Left),
|
||||
'^' => Ok(Self::Center),
|
||||
'>' => Ok(Self::Right),
|
||||
_ => Err(Report::msg(format!("Unknown alignment: {value}"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Formatting {
|
||||
fn default_for(token_type: TokenType) -> Self {
|
||||
match token_type {
|
||||
TokenType::CpuFrequency
|
||||
| TokenType::LoadAverage1
|
||||
| TokenType::LoadAverage5
|
||||
| TokenType::LoadAverage15 => Self {
|
||||
width: 0,
|
||||
fill: '0',
|
||||
align: Alignment::default(),
|
||||
precision: 2,
|
||||
},
|
||||
TokenType::CpuPercent => Self {
|
||||
width: 2,
|
||||
fill: '0',
|
||||
align: Alignment::default(),
|
||||
precision: 0,
|
||||
},
|
||||
TokenType::MemoryFree
|
||||
| TokenType::MemoryAvailable
|
||||
| TokenType::MemoryTotal
|
||||
| TokenType::MemoryUsed
|
||||
| TokenType::MemoryPercent
|
||||
| TokenType::SwapFree
|
||||
| TokenType::SwapTotal
|
||||
| TokenType::SwapUsed
|
||||
| TokenType::SwapPercent => Self {
|
||||
width: 4,
|
||||
fill: '0',
|
||||
align: Alignment::default(),
|
||||
precision: 1,
|
||||
},
|
||||
_ => Self {
|
||||
width: 0,
|
||||
fill: '0',
|
||||
align: Alignment::default(),
|
||||
precision: 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_input(input: &str) -> Result<Vec<Part>> {
|
||||
let mut tokens = vec![];
|
||||
|
||||
let mut chars = input.chars().peekable();
|
||||
|
||||
let mut next_char = chars.peek().copied();
|
||||
while let Some(char) = next_char {
|
||||
let token = if char == '{' {
|
||||
chars.next();
|
||||
parse_dynamic(&mut chars)?
|
||||
} else {
|
||||
parse_static(&mut chars)
|
||||
};
|
||||
|
||||
tokens.push(token);
|
||||
next_char = chars.peek().copied();
|
||||
}
|
||||
|
||||
Ok(tokens)
|
||||
}
|
||||
|
||||
fn parse_static(chars: &mut Peekable<Chars>) -> Part {
|
||||
let mut str = String::new();
|
||||
|
||||
let mut next_char = chars.next_if(|&c| c != '{');
|
||||
while let Some(char) = next_char {
|
||||
if char == '{' {
|
||||
break;
|
||||
}
|
||||
|
||||
str.push(char);
|
||||
next_char = chars.next_if(|&c| c != '{');
|
||||
}
|
||||
|
||||
Part::Static(str)
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
enum DynamicMode {
|
||||
Token,
|
||||
Name,
|
||||
Prefix,
|
||||
}
|
||||
|
||||
fn parse_dynamic(chars: &mut Peekable<Chars>) -> Result<Part> {
|
||||
let mut mode = DynamicMode::Token;
|
||||
|
||||
let mut token_str = String::new();
|
||||
let mut func_str = String::new();
|
||||
let mut prefix_str = String::new();
|
||||
|
||||
// we don't want to peek here as that would be the same char as the outer loop
|
||||
let mut next_char = chars.next();
|
||||
while let Some(char) = next_char {
|
||||
match char {
|
||||
'}' | ':' => break,
|
||||
'@' => mode = DynamicMode::Name,
|
||||
'#' => mode = DynamicMode::Prefix,
|
||||
_ => match mode {
|
||||
DynamicMode::Token => token_str.push(char),
|
||||
DynamicMode::Name => func_str.push(char),
|
||||
DynamicMode::Prefix => prefix_str.push(char),
|
||||
},
|
||||
}
|
||||
|
||||
next_char = chars.next();
|
||||
}
|
||||
|
||||
let token_type = token_str.parse()?;
|
||||
let mut formatting = Formatting::default_for(token_type);
|
||||
|
||||
if next_char == Some(':') {
|
||||
formatting = parse_formatting(chars, formatting)?;
|
||||
}
|
||||
|
||||
let token = Token {
|
||||
token: token_type,
|
||||
function: func_str
|
||||
.parse()
|
||||
.unwrap_or_else(|()| Function::default_for(token_type)),
|
||||
prefix: prefix_str
|
||||
.parse()
|
||||
.unwrap_or_else(|_| Prefix::default_for(token_type)),
|
||||
formatting,
|
||||
};
|
||||
|
||||
Ok(Part::Token(token))
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
enum FormattingMode {
|
||||
WidthFillAlign,
|
||||
Precision,
|
||||
}
|
||||
|
||||
fn parse_formatting(chars: &mut Peekable<Chars>, mut formatting: Formatting) -> Result<Formatting> {
|
||||
let mut width_string = String::new();
|
||||
let mut precision_string = String::new();
|
||||
|
||||
let mut mode = FormattingMode::WidthFillAlign;
|
||||
|
||||
let mut next_char = chars.next();
|
||||
while let Some(char) = next_char {
|
||||
match (char, mode) {
|
||||
('}', _) => break,
|
||||
('.', _) => mode = FormattingMode::Precision,
|
||||
(_, FormattingMode::Precision) => precision_string.push(char),
|
||||
('1'..='9', FormattingMode::WidthFillAlign) => width_string.push(char),
|
||||
('<' | '^' | '>', FormattingMode::WidthFillAlign) => {
|
||||
formatting.align = Alignment::try_from(char)?;
|
||||
}
|
||||
(_, FormattingMode::WidthFillAlign) => formatting.fill = char,
|
||||
};
|
||||
|
||||
next_char = chars.next();
|
||||
}
|
||||
|
||||
if !width_string.is_empty() {
|
||||
formatting.width = width_string.parse()?;
|
||||
}
|
||||
|
||||
if !precision_string.is_empty() {
|
||||
formatting.precision = precision_string.parse()?;
|
||||
}
|
||||
|
||||
Ok(formatting)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn static_only() {
|
||||
let tokens = parse_input("hello world").unwrap();
|
||||
println!("{tokens:?}");
|
||||
|
||||
assert_eq!(tokens.len(), 1);
|
||||
assert!(matches!(&tokens[0], Part::Static(str) if str == "hello world"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic() {
|
||||
let tokens = parse_input("{cpu_frequency}").unwrap();
|
||||
println!("{tokens:?}");
|
||||
|
||||
assert_eq!(tokens.len(), 1);
|
||||
|
||||
assert!(matches!(&tokens[0], Part::Token(_)));
|
||||
let Part::Token(token) = tokens.get(0).unwrap() else {
|
||||
return;
|
||||
};
|
||||
|
||||
assert_eq!(token.token, TokenType::CpuFrequency);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn named() {
|
||||
let tokens = parse_input("{cpu_frequency@cpu0}").unwrap();
|
||||
println!("{tokens:?}");
|
||||
|
||||
assert_eq!(tokens.len(), 1);
|
||||
|
||||
assert!(matches!(&tokens[0], Part::Token(_)));
|
||||
let Part::Token(token) = tokens.get(0).unwrap() else {
|
||||
return;
|
||||
};
|
||||
|
||||
assert_eq!(token.token, TokenType::CpuFrequency);
|
||||
assert!(matches!(&token.function, Function::Name(n) if n == "cpu0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conversion() {
|
||||
let tokens = parse_input("{cpu_frequency#G}").unwrap();
|
||||
println!("{tokens:?}");
|
||||
|
||||
assert_eq!(tokens.len(), 1);
|
||||
|
||||
assert!(matches!(&tokens[0], Part::Token(_)));
|
||||
let Part::Token(token) = tokens.get(0).unwrap() else {
|
||||
return;
|
||||
};
|
||||
|
||||
assert_eq!(token.token, TokenType::CpuFrequency);
|
||||
assert_eq!(token.prefix, Prefix::Giga);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formatting_basic() {
|
||||
let tokens = parse_input("{cpu_frequency:.2}").unwrap();
|
||||
println!("{tokens:?}");
|
||||
|
||||
assert_eq!(tokens.len(), 1);
|
||||
|
||||
assert!(matches!(&tokens[0], Part::Token(_)));
|
||||
let Part::Token(token) = tokens.get(0).unwrap() else {
|
||||
return;
|
||||
};
|
||||
|
||||
assert_eq!(token.token, TokenType::CpuFrequency);
|
||||
assert_eq!(token.formatting.precision, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formatting_complex() {
|
||||
let tokens = parse_input("{cpu_frequency:0<5.2}").unwrap();
|
||||
println!("{tokens:?}");
|
||||
|
||||
assert_eq!(tokens.len(), 1);
|
||||
|
||||
assert!(matches!(&tokens[0], Part::Token(_)));
|
||||
let Part::Token(token) = tokens.get(0).unwrap() else {
|
||||
return;
|
||||
};
|
||||
|
||||
assert_eq!(token.token, TokenType::CpuFrequency);
|
||||
assert_eq!(token.formatting.fill, '0');
|
||||
assert_eq!(token.formatting.align, Alignment::Left);
|
||||
assert_eq!(token.formatting.width, 5);
|
||||
assert_eq!(token.formatting.precision, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complex() {
|
||||
let tokens = parse_input("{cpu_frequency@cpu0#G:.2}").unwrap();
|
||||
println!("{tokens:?}");
|
||||
|
||||
assert_eq!(tokens.len(), 1);
|
||||
|
||||
assert!(matches!(&tokens[0], Part::Token(_)));
|
||||
let Part::Token(token) = tokens.get(0).unwrap() else {
|
||||
return;
|
||||
};
|
||||
|
||||
assert_eq!(token.token, TokenType::CpuFrequency);
|
||||
assert!(matches!(&token.function, Function::Name(n) if n == "cpu0"));
|
||||
assert_eq!(token.prefix, Prefix::Giga);
|
||||
assert_eq!(token.formatting.precision, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn static_then_token() {
|
||||
let tokens = parse_input("Freq: {cpu_frequency#G:.2}").unwrap();
|
||||
println!("{tokens:?}");
|
||||
|
||||
assert_eq!(tokens.len(), 2);
|
||||
|
||||
assert!(matches!(&tokens[0], Part::Static(str) if str == "Freq: "));
|
||||
|
||||
assert!(matches!(&tokens[1], Part::Token(_)));
|
||||
let Part::Token(token) = tokens.get(1).unwrap() else {
|
||||
return;
|
||||
};
|
||||
|
||||
assert_eq!(token.token, TokenType::CpuFrequency);
|
||||
assert_eq!(token.formatting.precision, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn token_then_static() {
|
||||
let tokens = parse_input("{cpu_frequency#G:.2} GHz").unwrap();
|
||||
println!("{tokens:?}");
|
||||
|
||||
assert_eq!(tokens.len(), 2);
|
||||
|
||||
assert!(matches!(&tokens[0], Part::Token(_)));
|
||||
let Part::Token(token) = tokens.get(0).unwrap() else {
|
||||
return;
|
||||
};
|
||||
|
||||
assert_eq!(token.token, TokenType::CpuFrequency);
|
||||
assert_eq!(token.formatting.precision, 2);
|
||||
|
||||
assert!(matches!(&tokens[1], Part::Static(str) if str == " GHz"));
|
||||
}
|
||||
}
|
91
src/modules/sysinfo/renderer.rs
Normal file
91
src/modules/sysinfo/renderer.rs
Normal file
|
@ -0,0 +1,91 @@
|
|||
use super::token::{Alignment, Part, Token, TokenType};
|
||||
use super::Interval;
|
||||
use crate::clients;
|
||||
use crate::clients::sysinfo::{Value, ValueSet};
|
||||
|
||||
pub enum TokenValue {
|
||||
Number(f64),
|
||||
String(String),
|
||||
}
|
||||
|
||||
impl Part {
|
||||
pub fn render_all(
|
||||
tokens: &[Self],
|
||||
client: &clients::sysinfo::Client,
|
||||
interval: Interval,
|
||||
) -> String {
|
||||
tokens
|
||||
.iter()
|
||||
.map(|part| part.render(client, interval))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn render(&self, client: &clients::sysinfo::Client, interval: Interval) -> String {
|
||||
match self {
|
||||
Part::Static(str) => str.clone(),
|
||||
Part::Token(token) => {
|
||||
match token.get(client, interval) {
|
||||
TokenValue::Number(value) => {
|
||||
let fmt = token.formatting;
|
||||
let mut str = format!("{value:.precision$}", precision = fmt.precision);
|
||||
|
||||
// fill/align doesn't support parameterization so we need our own impl
|
||||
let mut add_to_end = fmt.align == Alignment::Right;
|
||||
while str.len() < fmt.width {
|
||||
if add_to_end {
|
||||
str.push(fmt.fill);
|
||||
} else {
|
||||
str.insert(0, fmt.fill);
|
||||
}
|
||||
|
||||
if fmt.align == Alignment::Center {
|
||||
add_to_end = !add_to_end;
|
||||
}
|
||||
}
|
||||
|
||||
str
|
||||
}
|
||||
TokenValue::String(value) => value,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Token {
|
||||
pub fn get(&self, client: &clients::sysinfo::Client, interval: Interval) -> TokenValue {
|
||||
let get = |value: Value| TokenValue::Number(value.get(self.prefix));
|
||||
let apply = |set: ValueSet| TokenValue::Number(set.apply(&self.function, self.prefix));
|
||||
|
||||
match self.token {
|
||||
// Number tokens
|
||||
TokenType::CpuFrequency => apply(client.cpu_frequency()),
|
||||
TokenType::CpuPercent => apply(client.cpu_percent()),
|
||||
TokenType::MemoryFree => get(client.memory_free()),
|
||||
TokenType::MemoryAvailable => get(client.memory_available()),
|
||||
TokenType::MemoryTotal => get(client.memory_total()),
|
||||
TokenType::MemoryUsed => get(client.memory_used()),
|
||||
TokenType::MemoryPercent => get(client.memory_percent()),
|
||||
TokenType::SwapFree => get(client.swap_free()),
|
||||
TokenType::SwapTotal => get(client.swap_total()),
|
||||
TokenType::SwapUsed => get(client.swap_used()),
|
||||
TokenType::SwapPercent => get(client.swap_percent()),
|
||||
TokenType::TempC => apply(client.temp_c()),
|
||||
TokenType::TempF => apply(client.temp_f()),
|
||||
TokenType::DiskFree => apply(client.disk_free()),
|
||||
TokenType::DiskTotal => apply(client.disk_total()),
|
||||
TokenType::DiskUsed => apply(client.disk_used()),
|
||||
TokenType::DiskPercent => apply(client.disk_percent()),
|
||||
TokenType::DiskRead => apply(client.disk_read(interval)),
|
||||
TokenType::DiskWrite => apply(client.disk_write(interval)),
|
||||
TokenType::NetDown => apply(client.net_down(interval)),
|
||||
TokenType::NetUp => apply(client.net_up(interval)),
|
||||
TokenType::LoadAverage1 => get(client.load_average_1()),
|
||||
TokenType::LoadAverage5 => get(client.load_average_5()),
|
||||
TokenType::LoadAverage15 => get(client.load_average_15()),
|
||||
|
||||
// String tokens
|
||||
TokenType::Uptime => TokenValue::String(client.uptime()),
|
||||
}
|
||||
}
|
||||
}
|
66
src/modules/sysinfo/token.rs
Normal file
66
src/modules/sysinfo/token.rs
Normal file
|
@ -0,0 +1,66 @@
|
|||
use crate::clients::sysinfo::{Function, Prefix};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum TokenType {
|
||||
CpuFrequency,
|
||||
CpuPercent,
|
||||
|
||||
MemoryFree,
|
||||
MemoryAvailable,
|
||||
MemoryTotal,
|
||||
MemoryUsed,
|
||||
MemoryPercent,
|
||||
|
||||
SwapFree,
|
||||
SwapTotal,
|
||||
SwapUsed,
|
||||
SwapPercent,
|
||||
|
||||
TempC,
|
||||
TempF,
|
||||
|
||||
DiskFree,
|
||||
DiskTotal,
|
||||
DiskUsed,
|
||||
DiskPercent,
|
||||
DiskRead,
|
||||
DiskWrite,
|
||||
|
||||
NetDown,
|
||||
NetUp,
|
||||
|
||||
LoadAverage1,
|
||||
LoadAverage5,
|
||||
LoadAverage15,
|
||||
Uptime,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Token {
|
||||
pub token: TokenType,
|
||||
pub function: Function,
|
||||
pub prefix: Prefix,
|
||||
pub formatting: Formatting,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Part {
|
||||
Static(String),
|
||||
Token(Token),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Formatting {
|
||||
pub width: usize,
|
||||
pub fill: char,
|
||||
pub align: Alignment,
|
||||
pub precision: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub enum Alignment {
|
||||
#[default]
|
||||
Left,
|
||||
Center,
|
||||
Right,
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue