1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237
//! System monitor widgets and utility functions
/// Helper functions for obtaining system information for use in status bar widgets
pub mod helpers {
use penrose::util::{spawn_for_output, spawn_for_output_with_args};
use std::{fs, path::PathBuf};
/// This finds the first battery (BAT) file it finds; so far only
/// confirmed working on Linux.
pub fn battery_file_search() -> Option<String> {
let battery_paths = vec![
// Linux
"/sys/class/power_supply",
// OpenBSD
"/var/run/apm",
// FreeBSD and DragonFlyBSD
"/dev",
// illumos
"/dev/battery",
];
battery_paths
.into_iter()
.filter_map(|base_path| {
let base_path = PathBuf::from(base_path);
if base_path.exists() && base_path.is_dir() {
fs::read_dir(base_path).ok()
} else {
None
}
})
.flat_map(|read_dir| read_dir.filter_map(Result::ok))
.filter_map(|entry| {
let file_name = entry.file_name();
let file_name_str = file_name.to_str()?;
if file_name_str.starts_with("BAT") && file_name_str[3..].parse::<u32>().is_ok() {
Some(file_name_str.to_string())
} else {
None
}
})
.next()
}
/// Fetch the requested battery's charge as a percentage of its total along with an indicator
/// of whether it is charging or discharging.
///
/// This will return `None` if it is unable to read or parse the required system files for the
/// requested battery.
pub fn battery_text(bat: &str) -> Option<String> {
let status = read_sys_file(bat, "status")?;
let energy_now: u32 = read_sys_file(bat, "charge_now")?.parse().ok()?;
let energy_full: u32 = read_sys_file(bat, "charge_full")?.parse().ok()?;
let charge = energy_now * 100 / energy_full;
let icon = if status == "Charging" {
""
} else if charge >= 90 || status == "Full" {
""
} else if charge >= 70 {
""
} else if charge >= 50 {
""
} else if charge >= 20 {
""
} else {
""
};
Some(format!("{icon} {charge}%"))
}
fn read_sys_file(bat: &str, fname: &str) -> Option<String> {
fs::read_to_string(format!("/sys/class/power_supply/{bat}/{fname}"))
.ok()
.map(|s| s.trim().to_string())
}
/// Fetch the current date and time in `YYYY-MM-DD HH:MM` format using the `date` command line
/// program.
///
/// Will return `None` if there are errors in calling `date`.
pub fn date_text() -> Option<String> {
Some(
spawn_for_output_with_args("date", &["+%F %R"])
.ok()?
.trim()
.to_string(),
)
}
/// Fetch the active ESSID and associated signal quality for the active wifi network.
///
/// Makes use of the `iwgetid` command line program and will return `None` if there are errors
/// in calling it or reading required system files to determine the signal quality.
pub fn wifi_text() -> Option<String> {
let (interface, essid) = interface_and_essid()?;
let signal = signal_quality(&interface)?;
Some(format!("<{essid} {signal}%>"))
}
// Read the interface name and essid via iwgetid.
// Output format is '$interface ESSID:"$essid"'
fn interface_and_essid() -> Option<(String, String)> {
let raw = spawn_for_output("iwgetid").ok()?;
let mut iter = raw.split(':');
// Not using split_whitespace here as the essid may contain whitespace
let interface = iter.next()?.split_whitespace().next()?.to_owned();
let essid = iter.next()?.split('"').nth(1)?.to_string();
Some((interface, essid))
}
// Parsing the format described here: https://hewlettpackard.github.io/wireless-tools/Linux.Wireless.Extensions.html
fn signal_quality(interface: &str) -> Option<String> {
let raw = fs::read_to_string("/proc/net/wireless").ok()?;
for line in raw.lines() {
if line.starts_with(interface) {
return Some(
line.split_whitespace()
.nth(2)?
.strip_suffix('.')?
.to_owned(),
);
}
}
None
}
/// Parse the current volume as a percentage from amixer.
///
/// Expected output format:
/// $ amixer sget Master
/// Simple mixer control 'Master',0
/// Capabilities: pvolume pvolume-joined pswitch pswitch-joined
/// Playback channels: Mono
/// Limits: Playback 0 - 127
/// Mono: Playback 0 [0%] [-63.50dB] [on]
pub fn amixer_text(channel: &str) -> Option<String> {
let raw = spawn_for_output(format!("amixer sget {channel}")).ok()?;
let vol = raw
.lines()
.last()?
.split_whitespace()
.find(|s| s.ends_with("%]"))?
.replace(|c| "[]%".contains(c), "");
Some(format!(" {vol}%"))
}
}
/// System information widgets provided as [RefreshText] widgets.
///
/// These will update themselves every time that the window manager refreshes its internal state.
/// To update on a specified interval instead, see the [interval] module instead.
pub mod refresh {
use crate::bar::widgets::{sys::helpers, RefreshText, TextStyle};
/// Display the current charge level and status of a named battery.
///
/// If the given battery name is not found on this system, this widget will
/// render as an empty string.
pub fn battery_summary(bat: &'static str, style: TextStyle) -> RefreshText {
RefreshText::new(style, move || {
helpers::battery_text(bat).unwrap_or_default()
})
}
/// Display the current date and time in YYYY-MM-DD HH:MM format
///
/// This widget shells out to the `date` tool to generate its output
pub fn current_date_and_time(style: TextStyle) -> RefreshText {
RefreshText::new(style, || helpers::date_text().unwrap_or_default())
}
/// Display the ESSID currently connected to and the signal quality as
/// a percentage.
pub fn wifi_network(style: TextStyle) -> RefreshText {
RefreshText::new(style, move || helpers::wifi_text().unwrap_or_default())
}
/// Display the current volume level as reported by `amixer`
pub fn amixer_volume(channel: &'static str, style: TextStyle) -> RefreshText {
RefreshText::new(style, move || {
helpers::amixer_text(channel).unwrap_or_default()
})
}
}
/// System information widgets provided as [IntervalText] widgets.
///
/// These will update themselves based on the interval provided. To update when the window manager
/// refreshes its internal state, see the [refresh] module instead.
pub mod interval {
use crate::bar::widgets::{sys::helpers, IntervalText, TextStyle};
use std::time::Duration;
/// Display the current charge level and status of a named battery.
///
/// If the given battery name is not found on this system, this widget will
/// render as an empty string.
pub fn battery_summary(
bat: &'static str,
style: TextStyle,
interval: Duration,
) -> IntervalText {
IntervalText::new(style, move || helpers::battery_text(bat), interval)
}
/// Display the current date and time in YYYY-MM-DD HH:MM format
///
/// This widget shells out to the `date` tool to generate its output
pub fn current_date_and_time(style: TextStyle, interval: Duration) -> IntervalText {
IntervalText::new(style, helpers::date_text, interval)
}
/// Display the ESSID currently connected to and the signal quality as
/// a percentage.
pub fn wifi_network(style: TextStyle, interval: Duration) -> IntervalText {
IntervalText::new(style, helpers::wifi_text, interval)
}
/// Display the current volume level as reported by `amixer`
pub fn amixer_volume(
channel: &'static str,
style: TextStyle,
interval: Duration,
) -> IntervalText {
IntervalText::new(style, move || helpers::amixer_text(channel), interval)
}
}