i3status_rs/blocks/
cpu.rs1use 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 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 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 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 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
167async 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 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
257async 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}