i3status_rs/blocks/
cpu.rs

1//! CPU statistics
2//!
3//! # Configuration
4//!
5//! Key | Values | Default
6//! ----|--------|--------
7//! `format` | A string to customise the output of this block. See below for available placeholders. | `" $icon $utilization "`
8//! `format_alt` | If set, block will switch between `format` and `format_alt` on every click | `None`
9//! `interval` | Update interval in seconds | `5`
10//! `info_cpu` | Percentage of CPU usage, where state is set to info | `30.0`
11//! `warning_cpu` | Percentage of CPU usage, where state is set to warning | `60.0`
12//! `critical_cpu` | Percentage of CPU usage, where state is set to critical | `90.0`
13//!
14//! Placeholder      | Value                                                                | Type   | Unit
15//! -----------------|----------------------------------------------------------------------|--------|---------------
16//! `icon`           | An icon                                                              | Icon   | -
17//! `utilization`    | Average CPU utilization                                              | Number | %
18//! `utilization<N>` | Utilization of Nth logical CPU                                       | Number | %
19//! `barchart`       | Utilization of all logical CPUs presented as a barchart              | Text   | -
20//! `frequency`      | Average CPU frequency (may be absent if CPU is not supported)        | Number | Hz
21//! `frequency<N>`   | Frequency of Nth logical CPU (may be absent if CPU is not supported) | Number | Hz
22//! `max_frequency`  | Max frequency of all logical CPUs                                    | Number | Hz
23//! `boost`          | CPU turbo boost status (may be absent if CPU is not supported)       | Text   | -
24//!
25//! Action          | Description                               | Default button
26//! ----------------|-------------------------------------------|---------------
27//! `toggle_format` | Toggles between `format` and `format_alt` | Left
28//!
29//! # Example
30//!
31//! ```toml
32//! [[block]]
33//! block = "cpu"
34//! interval = 1
35//! format = " $icon $barchart $utilization "
36//! format_alt = " $icon $frequency{ $boost|} "
37//! info_cpu = 20
38//! warning_cpu = 50
39//! critical_cpu = 90
40//! ```
41//!
42//! # Icons Used
43//! - `cpu` (as a progression)
44//! - `cpu_boost_on`
45//! - `cpu_boost_off`
46
47use std::str::FromStr as _;
48
49use tokio::fs::File;
50use tokio::io::{AsyncBufReadExt as _, BufReader};
51
52use super::prelude::*;
53use crate::util::read_file;
54
55const CPU_BOOST_PATH: &str = "/sys/devices/system/cpu/cpufreq/boost";
56const CPU_NO_TURBO_PATH: &str = "/sys/devices/system/cpu/intel_pstate/no_turbo";
57
58#[derive(Deserialize, Debug, SmartDefault)]
59#[serde(deny_unknown_fields, default)]
60pub struct Config {
61    pub format: FormatConfig,
62    pub format_alt: Option<FormatConfig>,
63    #[default(5.into())]
64    pub interval: Seconds,
65    #[default(30.0)]
66    pub info_cpu: f64,
67    #[default(60.0)]
68    pub warning_cpu: f64,
69    #[default(90.0)]
70    pub critical_cpu: f64,
71}
72
73pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
74    let mut actions = api.get_actions()?;
75    api.set_default_actions(&[(MouseButton::Left, None, "toggle_format")])?;
76
77    let mut format = config.format.with_default(" $icon $utilization ")?;
78    let mut format_alt = match &config.format_alt {
79        Some(f) => Some(f.with_default("")?),
80        None => None,
81    };
82
83    // Store previous /proc/stat state
84    let mut cputime = read_proc_stat().await?;
85    let cores = cputime.1.len();
86
87    if cores == 0 {
88        return Err(Error::new("/proc/stat reported zero cores"));
89    }
90
91    let mut timer = config.interval.timer();
92
93    loop {
94        let freqs = read_frequencies().await?;
95
96        // Compute utilizations
97        let new_cputime = read_proc_stat().await?;
98        let utilization_avg = new_cputime.0.utilization(cputime.0);
99        let mut utilizations = Vec::new();
100        if new_cputime.1.len() != cores {
101            return Err(Error::new("new cputime length is incorrect"));
102        }
103        for i in 0..cores {
104            utilizations.push(new_cputime.1[i].utilization(cputime.1[i]));
105        }
106        cputime = new_cputime;
107
108        // Create barchart indicating per-core utilization
109        let mut barchart = String::new();
110        const BOXCHARS: &[char] = &['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
111        for utilization in &utilizations {
112            barchart.push(BOXCHARS[(7.5 * utilization) as usize]);
113        }
114
115        // Read boost state on intel CPUs
116        let boost = boost_status().await.map(|status| match status {
117            true => "cpu_boost_on",
118            false => "cpu_boost_off",
119        });
120
121        let mut values = map!(
122            "icon" => Value::icon_progression("cpu", utilization_avg),
123            "barchart" => Value::text(barchart),
124            "utilization" => Value::percents(utilization_avg * 100.),
125            [if !freqs.is_empty()] "frequency" => Value::hertz(freqs.iter().sum::<f64>() / (freqs.len() as f64)),
126            [if !freqs.is_empty()] "max_frequency" => Value::hertz(freqs.iter().copied().max_by(f64::total_cmp).unwrap()),
127        );
128        boost.map(|b| values.insert("boost".into(), Value::icon(b)));
129        for (i, freq) in freqs.iter().enumerate() {
130            values.insert(format!("frequency{}", i + 1).into(), Value::hertz(*freq));
131        }
132        for (i, utilization) in utilizations.iter().enumerate() {
133            values.insert(
134                format!("utilization{}", i + 1).into(),
135                Value::percents(utilization * 100.),
136            );
137        }
138
139        let mut widget = Widget::new().with_format(format.clone());
140        widget.set_values(values);
141        widget.state = match utilization_avg * 100. {
142            x if x > config.critical_cpu => State::Critical,
143            x if x > config.warning_cpu => State::Warning,
144            x if x > config.info_cpu => State::Info,
145            _ => State::Idle,
146        };
147        api.set_widget(widget)?;
148
149        loop {
150            select! {
151                _ = timer.tick() => break,
152                _ = api.wait_for_update_request() => break,
153                Some(action) = actions.recv() => match action.as_ref() {
154                    "toggle_format" => {
155                        if let Some(ref mut format_alt) = format_alt {
156                            std::mem::swap(format_alt, &mut format);
157                            break;
158                        }
159                    }
160                    _ => (),
161                }
162            }
163        }
164    }
165}
166
167// Read frequencies (read in MHz, store in Hz)
168async fn read_frequencies() -> Result<Vec<f64>> {
169    let mut freqs = Vec::with_capacity(32);
170
171    let file = File::open("/proc/cpuinfo")
172        .await
173        .error("failed to read /proc/cpuinfo")?;
174    let mut file = BufReader::new(file);
175
176    let mut line = String::new();
177    while file
178        .read_line(&mut line)
179        .await
180        .error("failed to read /proc/cpuinfo")?
181        != 0
182    {
183        if line.starts_with("cpu MHz") {
184            let slice = line
185                .trim_end()
186                .trim_start_matches(|c: char| !c.is_ascii_digit());
187            freqs.push(f64::from_str(slice).error("failed to parse /proc/cpuinfo")? * 1e6);
188        }
189        line.clear();
190    }
191
192    Ok(freqs)
193}
194
195#[derive(Debug, Clone, Copy)]
196struct CpuTime {
197    idle: u64,
198    non_idle: u64,
199}
200
201impl CpuTime {
202    fn from_str(s: &str) -> Option<Self> {
203        let mut s = s.trim().split_ascii_whitespace();
204        let user = u64::from_str(s.next()?).ok()?;
205        let nice = u64::from_str(s.next()?).ok()?;
206        let system = u64::from_str(s.next()?).ok()?;
207        let idle = u64::from_str(s.next()?).ok()?;
208        let iowait = u64::from_str(s.next()?).ok()?;
209        let irq = u64::from_str(s.next()?).ok()?;
210        let softirq = u64::from_str(s.next()?).ok()?;
211
212        Some(Self {
213            idle: idle + iowait,
214            non_idle: user + nice + system + irq + softirq,
215        })
216    }
217
218    fn utilization(&self, old: Self) -> f64 {
219        let elapsed = (self.idle + self.non_idle).saturating_sub(old.idle + old.non_idle);
220        if elapsed == 0 {
221            0.0
222        } else {
223            ((self.non_idle - old.non_idle) as f64 / elapsed as f64).clamp(0., 1.)
224        }
225    }
226}
227
228async fn read_proc_stat() -> Result<(CpuTime, Vec<CpuTime>)> {
229    let mut utilizations = Vec::with_capacity(32);
230    let mut total = None;
231
232    let file = File::open("/proc/stat")
233        .await
234        .error("failed to read /proc/stat")?;
235    let mut file = BufReader::new(file);
236
237    let mut line = String::new();
238    while file
239        .read_line(&mut line)
240        .await
241        .error("failed to read /proc/stat")?
242        != 0
243    {
244        // Total time
245        let data = line.trim_start_matches(|c: char| !c.is_ascii_whitespace());
246        if line.starts_with("cpu ") {
247            total = Some(CpuTime::from_str(data).error("failed to parse /proc/stat")?);
248        } else if line.starts_with("cpu") {
249            utilizations.push(CpuTime::from_str(data).error("failed to parse /proc/stat")?);
250        }
251        line.clear();
252    }
253
254    Ok((total.error("failed to parse /proc/stat")?, utilizations))
255}
256
257/// Read the cpu turbo boost status from kernel sys interface
258/// or intel pstate interface
259async fn boost_status() -> Option<bool> {
260    if let Ok(boost) = read_file(CPU_BOOST_PATH).await {
261        Some(boost.starts_with('1'))
262    } else if let Ok(no_turbo) = read_file(CPU_NO_TURBO_PATH).await {
263        Some(no_turbo.starts_with('0'))
264    } else {
265        None
266    }
267}