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