i3status_rs/blocks/
cpu.rs1use std::str::FromStr as _;
49
50use tokio::fs::File;
51use tokio::io::{AsyncBufReadExt as _, BufReader};
52
53use super::prelude::*;
54use crate::util::read_file;
55
56const CPU_BOOST_PATH: &str = "/sys/devices/system/cpu/cpufreq/boost";
57const CPU_NO_TURBO_PATH: &str = "/sys/devices/system/cpu/intel_pstate/no_turbo";
58
59#[derive(Deserialize, Debug, SmartDefault)]
60#[serde(default)]
61pub struct Config {
62 #[serde(flatten)]
63 pub formats: MaybeMultiFormatConfig,
64 #[default(5.into())]
65 pub interval: Seconds,
66 #[default(30.0)]
67 pub info_cpu: f64,
68 #[default(60.0)]
69 pub warning_cpu: f64,
70 #[default(90.0)]
71 pub critical_cpu: f64,
72}
73
74const BOOST_ICON_NAMES: [&str; 2] = [icons::CPU_BOOST_ON, icons::CPU_BOOST_OFF];
77
78fn boost_icon(on: bool) -> &'static str {
79 match on {
80 true => icons::CPU_BOOST_ON,
81 false => icons::CPU_BOOST_OFF,
82 }
83}
84
85pub(crate) fn prepare(config: &Config) -> Result<Arc<BlockPlan>> {
86 let declare = |output: OutputPlan| {
91 output
92 .icon("icon", IconChoices::one(icons::CPU))
93 .icon("boost", IconChoices::fixed(BOOST_ICON_NAMES))
94 };
95 let formats = config.formats.with_default(" $icon $utilization ")?;
96 BlockPlan::new(format_outputs(formats, declare))
97}
98
99pub(crate) async fn run(config: &Config, api: &CommonApi, plan: &Arc<BlockPlan>) -> Result<()> {
100 let mut actions = api.get_actions()?;
101 api.set_default_actions(&[
102 (MouseButton::Left, None, "next_format"),
103 (MouseButton::Right, None, "prev_format"),
104 ])?;
105
106 let mut formats = FormatRotation::new(plan)?;
107
108 let mut cputime = read_proc_stat().await?;
110 let cores = cputime.1.len();
111
112 if cores == 0 {
113 return Err(Error::new("/proc/stat reported zero cores"));
114 }
115
116 let mut timer = config.interval.timer();
117
118 loop {
119 let freqs = read_frequencies().await?;
120
121 let new_cputime = read_proc_stat().await?;
123 let utilization_avg = new_cputime.0.utilization(cputime.0);
124 let mut utilizations = Vec::new();
125 if new_cputime.1.len() != cores {
126 return Err(Error::new("new cputime length is incorrect"));
127 }
128 for i in 0..cores {
129 utilizations.push(new_cputime.1[i].utilization(cputime.1[i]));
130 }
131 cputime = new_cputime;
132
133 let mut barchart = String::new();
135 const BOXCHARS: &[char] = &['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
136 for utilization in &utilizations {
137 barchart.push(BOXCHARS[(7.5 * utilization) as usize]);
138 }
139
140 let boost = boost_status().await.map(boost_icon);
142
143 let output = formats.current();
144
145 let mut values = map!(
146 "icon" => Value::icon_progression(icons::CPU, utilization_avg),
147 "barchart" => Value::text(barchart),
148 "utilization" => Value::percents(utilization_avg * 100.),
149 [if !freqs.is_empty()] "frequency" => Value::hertz(freqs.iter().sum::<f64>() / (freqs.len() as f64)),
150 [if !freqs.is_empty()] "max_frequency" => Value::hertz(freqs.iter().copied().max_by(f64::total_cmp).unwrap()),
151 );
152 if let Some(boost) = boost {
153 values.insert("boost".into(), Value::icon(boost));
154 }
155 for (i, freq) in freqs.iter().enumerate() {
156 values.insert(format!("frequency{}", i + 1).into(), Value::hertz(*freq));
157 }
158 for (i, utilization) in utilizations.iter().enumerate() {
159 values.insert(
160 format!("utilization{}", i + 1).into(),
161 Value::percents(utilization * 100.),
162 );
163 }
164
165 let mut widget = output.new_widget();
166 widget.set_values(values);
167 widget.state = match utilization_avg * 100. {
168 x if x > config.critical_cpu => State::Critical,
169 x if x > config.warning_cpu => State::Warning,
170 x if x > config.info_cpu => State::Info,
171 _ => State::Idle,
172 };
173 api.set_widget(widget)?;
174
175 loop {
176 select! {
177 _ = timer.tick() => break,
178 _ = api.wait_for_update_request() => break,
179 Some(action) = actions.recv() => match action.as_ref() {
180 "next_format" | "toggle_format" => {
181 formats.next();
182 break;
183 }
184 "prev_format" => {
185 formats.prev();
186 break;
187 }
188 _ => (),
189 }
190 }
191 }
192 }
193}
194
195async fn read_frequencies() -> Result<Vec<f64>> {
197 let mut freqs = Vec::with_capacity(32);
198
199 let file = File::open("/proc/cpuinfo")
200 .await
201 .error("failed to read /proc/cpuinfo")?;
202 let mut file = BufReader::new(file);
203
204 let mut line = String::new();
205 while file
206 .read_line(&mut line)
207 .await
208 .error("failed to read /proc/cpuinfo")?
209 != 0
210 {
211 if line.starts_with("cpu MHz") {
212 let slice = line
213 .trim_end()
214 .trim_start_matches(|c: char| !c.is_ascii_digit());
215 freqs.push(f64::from_str(slice).error("failed to parse /proc/cpuinfo")? * 1e6);
216 }
217 line.clear();
218 }
219
220 Ok(freqs)
221}
222
223#[derive(Debug, Clone, Copy)]
224struct CpuTime {
225 idle: u64,
226 non_idle: u64,
227}
228
229impl CpuTime {
230 fn from_str(s: &str) -> Option<Self> {
231 let mut s = s.trim().split_ascii_whitespace();
232 let user = u64::from_str(s.next()?).ok()?;
233 let nice = u64::from_str(s.next()?).ok()?;
234 let system = u64::from_str(s.next()?).ok()?;
235 let idle = u64::from_str(s.next()?).ok()?;
236 let iowait = u64::from_str(s.next()?).ok()?;
237 let irq = u64::from_str(s.next()?).ok()?;
238 let softirq = u64::from_str(s.next()?).ok()?;
239
240 Some(Self {
241 idle: idle + iowait,
242 non_idle: user + nice + system + irq + softirq,
243 })
244 }
245
246 fn utilization(&self, old: Self) -> f64 {
247 let elapsed = (self.idle + self.non_idle).saturating_sub(old.idle + old.non_idle);
248 if elapsed == 0 {
249 0.0
250 } else {
251 ((self.non_idle - old.non_idle) as f64 / elapsed as f64).clamp(0., 1.)
252 }
253 }
254}
255
256async fn read_proc_stat() -> Result<(CpuTime, Vec<CpuTime>)> {
257 let mut utilizations = Vec::with_capacity(32);
258 let mut total = None;
259
260 let file = File::open("/proc/stat")
261 .await
262 .error("failed to read /proc/stat")?;
263 let mut file = BufReader::new(file);
264
265 let mut line = String::new();
266 while file
267 .read_line(&mut line)
268 .await
269 .error("failed to read /proc/stat")?
270 != 0
271 {
272 let data = line.trim_start_matches(|c: char| !c.is_ascii_whitespace());
274 if line.starts_with("cpu ") {
275 total = Some(CpuTime::from_str(data).error("failed to parse /proc/stat")?);
276 } else if line.starts_with("cpu") {
277 utilizations.push(CpuTime::from_str(data).error("failed to parse /proc/stat")?);
278 }
279 line.clear();
280 }
281
282 Ok((total.error("failed to parse /proc/stat")?, utilizations))
283}
284
285async fn boost_status() -> Option<bool> {
288 if let Ok(boost) = read_file(CPU_BOOST_PATH).await {
289 Some(boost.starts_with('1'))
290 } else if let Ok(no_turbo) = read_file(CPU_NO_TURBO_PATH).await {
291 Some(no_turbo.starts_with('0'))
292 } else {
293 None
294 }
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300
301 #[test]
302 fn plan_declares_icon_and_boost_choices() {
303 let plan = prepare(&Config::default()).unwrap();
304 let format = plan.output("format").unwrap();
305 assert_eq!(format.single_icon("icon").unwrap(), "cpu");
306 let boost = format.output().choices_for("boost").unwrap();
307 assert!(boost.permits("cpu_boost_on"));
308 assert!(boost.permits("cpu_boost_off"));
309 assert!(!boost.permits("cpu"));
310 assert!(plan.output("format2").is_err());
312 }
313
314 #[test]
315 fn every_configured_format_becomes_an_output() {
316 let config: Config =
317 toml::from_str(r#"format = [" $icon ", " $icon $frequency "]"#).unwrap();
318 let plan = prepare(&config).unwrap();
319 let second = plan.output("format2").unwrap();
320 assert!(second.format().contains_key("frequency"));
321 assert_eq!(second.single_icon("icon").unwrap(), "cpu");
323 assert!(
324 second
325 .output()
326 .choices_for("boost")
327 .unwrap()
328 .permits("cpu_boost_off")
329 );
330 }
331
332 #[test]
333 fn every_boost_icon_is_declared() {
334 for on in [true, false] {
335 assert!(BOOST_ICON_NAMES.contains(&boost_icon(on)));
336 }
337 assert_eq!(BOOST_ICON_NAMES.len(), 2);
338 }
339
340 #[test]
341 fn legacy_format_alt_still_declares_both_formats() {
342 let split: Config =
345 toml::from_str("format = \" $icon \"\nformat_alt = \" $icon $frequency \"").unwrap();
346 let list: Config = toml::from_str(r#"format = [" $icon ", " $icon $frequency "]"#).unwrap();
347 for config in [split, list] {
348 let plan = prepare(&config).unwrap();
349 let ids: Vec<_> = plan.outputs().map(|o| o.id().to_string()).collect();
350 assert_eq!(ids, ["format", "format2"]);
351 assert!(
352 plan.output("format2")
353 .unwrap()
354 .format()
355 .contains_key("frequency")
356 );
357 }
358 }
359}