1
0
Fork 0
mirror of https://github.com/Zedfrigg/ironbar.git synced 2025-08-17 23:01:04 +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:
Jake Stanger 2025-01-04 23:08:01 +00:00
parent 49ab7e0c7b
commit 01de0ac6f5
No known key found for this signature in database
GPG key ID: C51FC8F9CB0BEA61
14 changed files with 1633 additions and 589 deletions

View file

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

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

View 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()),
}
}
}

View 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,
}