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