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}