i3status_rs/blocks/
nvidia_gpu.rs1use std::process::Stdio;
68use std::str::FromStr;
69
70use tokio::io::{BufReader, Lines};
71use tokio::process::Command;
72
73const MEM_BTN: &str = "mem_btn";
74const FAN_BTN: &str = "fan_btn";
75const QUERY: &str = "--query-gpu=name,memory.total,utilization.gpu,memory.used,temperature.gpu,fan.speed,clocks.current.graphics,power.draw,";
76const FORMAT: &str = "--format=csv,noheader,nounits";
77
78use super::prelude::*;
79
80#[derive(Deserialize, Debug, SmartDefault)]
81#[serde(deny_unknown_fields, default)]
82pub struct Config {
83 pub format: FormatConfig,
84 #[default(1.into())]
85 pub interval: Seconds,
86 #[default(0)]
87 pub gpu_id: u64,
88 #[default(50)]
89 pub idle: u32,
90 #[default(70)]
91 pub good: u32,
92 #[default(75)]
93 pub info: u32,
94 #[default(80)]
95 pub warning: u32,
96}
97
98pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
99 let mut actions = api.get_actions()?;
100 api.set_default_actions(&[
101 (MouseButton::Left, Some(MEM_BTN), "toggle_mem_total"),
102 (MouseButton::Left, Some(FAN_BTN), "toggle_fan_controlled"),
103 (MouseButton::WheelUp, Some(FAN_BTN), "fan_speed_up"),
104 (MouseButton::WheelDown, Some(FAN_BTN), "fan_speed_down"),
105 ])?;
106
107 let format = config
108 .format
109 .with_default(" $icon $utilization $memory $temperature ")?;
110
111 let mut child = Command::new("nvidia-smi")
113 .args([
114 "-l",
115 &config.interval.seconds().to_string(),
116 "-i",
117 &config.gpu_id.to_string(),
118 QUERY,
119 FORMAT,
120 ])
121 .stdout(Stdio::piped())
122 .kill_on_drop(true)
123 .spawn()
124 .error("Failed to execute nvidia-smi")?;
125 let mut reader = BufReader::new(child.stdout.take().unwrap()).lines();
126
127 let mut info = GpuInfo::from_reader(&mut reader).await?;
129 let mut show_mem_total = false;
130 let mut fan_controlled = false;
131
132 loop {
133 let mut widget = Widget::new().with_format(format.clone());
134
135 widget.state = match info.temperature {
136 t if t <= config.idle => State::Idle,
137 t if t <= config.good => State::Good,
138 t if t <= config.info => State::Info,
139 t if t <= config.warning => State::Warning,
140 _ => State::Critical,
141 };
142
143 widget.set_values(map! {
144 "icon" => Value::icon(icons::GPU),
145 "name" => Value::text(info.name.clone()),
146 "utilization" => Value::percents(info.utilization),
147 "memory" => Value::bytes(if show_mem_total {info.mem_total} else {info.mem_used}).with_instance(MEM_BTN),
148 "temperature" => Value::degrees_c(info.temperature),
149 "fan_speed" => Value::percents(info.fan_speed).with_instance(FAN_BTN).underline(fan_controlled).italic(fan_controlled),
150 "clocks" => Value::hertz(info.clocks),
151 "power" => Value::watts(info.power_draw),
152 });
153
154 api.set_widget(widget)?;
155
156 select! {
157 new_info = GpuInfo::from_reader(&mut reader) => {
158 info = new_info?;
159 }
160 code = child.wait() => {
161 let code = code.error("failed to check nvidia-smi exit code")?;
162 return Err(Error::new(format!("nvidia-smi exited with code {code}")));
163 }
164 Some(action) = actions.recv() => match action.as_ref() {
165 "toggle_mem_total" => {
166 show_mem_total = !show_mem_total;
167 }
168 "toggle_fan_controlled" => {
169 fan_controlled = !fan_controlled;
170 set_fan_speed(config.gpu_id, fan_controlled.then_some(info.fan_speed)).await?;
171 }
172 "fan_speed_up" if fan_controlled && info.fan_speed < 100 => {
173 info.fan_speed += 1;
174 set_fan_speed(config.gpu_id, Some(info.fan_speed)).await?;
175 }
176 "fan_speed_down" if fan_controlled && info.fan_speed > 0 => {
177 info.fan_speed -= 1;
178 set_fan_speed(config.gpu_id, Some(info.fan_speed)).await?;
179 }
180 _ => (),
181 }
182 }
183 }
184}
185
186#[derive(Debug)]
187struct GpuInfo {
188 name: String,
189 mem_total: f64, mem_used: f64, utilization: f64, temperature: u32, fan_speed: u32, clocks: f64, power_draw: f64, }
197
198impl GpuInfo {
199 async fn from_reader<B: AsyncBufRead + Unpin>(reader: &mut Lines<B>) -> Result<Self> {
205 const ERR_MSG: &str = "failed to read from nvidia-smi";
206 reader
207 .next_line()
208 .await
209 .error(ERR_MSG)?
210 .error(ERR_MSG)?
211 .parse::<GpuInfo>()
212 .error("failed to parse nvidia-smi output")
213 }
214}
215
216impl FromStr for GpuInfo {
217 type Err = Error;
218
219 fn from_str(s: &str) -> Result<Self, Self::Err> {
220 macro_rules! parse {
221 ($s:ident -> $($part:ident : $t:ident $(* $mul:expr)?),*) => {{
222 let mut parts = $s.trim().split(", ");
223 let info = GpuInfo {
224 $(
225 $part: {
226 let $part = parts
227 .next()
228 .error(concat!("missing property: ", stringify!($part)))?
229 .parse::<$t>()
230 .unwrap_or_default();
231 $(let $part = $part * $mul;)?
232 $part
233 },
234 )*
235 };
236 Ok(info)
237 }}
238 }
239 parse!(s -> name: String, mem_total: f64 * 1e6, utilization: f64, mem_used: f64 * 1e6, temperature: u32, fan_speed: u32, clocks: f64 * 1e6, power_draw: f64)
241 }
242}
243
244async fn set_fan_speed(id: u64, speed: Option<u32>) -> Result<()> {
245 const ERR_MSG: &str = "Failed to execute nvidia-settings";
246 let mut cmd = Command::new("nvidia-settings");
247 if let Some(speed) = speed {
248 cmd.args([
249 "-a",
250 &format!("[gpu:{id}]/GPUFanControlState=1"),
251 "-a",
252 &format!("[fan:{id}]/GPUTargetFanSpeed={speed}"),
253 ]);
254 } else {
255 cmd.args(["-a", &format!("[gpu:{id}]/GPUFanControlState=0")]);
256 }
257 if cmd
258 .spawn()
259 .error(ERR_MSG)?
260 .wait()
261 .await
262 .error(ERR_MSG)?
263 .success()
264 {
265 Ok(())
266 } else {
267 Err(Error::new(ERR_MSG))
268 }
269}