1use 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(crate) fn prepare(config: &Config) -> Result<Arc<BlockPlan>> {
99 BlockPlan::new(vec![
100 OutputPlan::new(
101 "main",
102 config
103 .format
104 .with_default(" $icon $utilization $memory $temperature ")?,
105 )
106 .icon("icon", IconChoices::one(icons::GPU)),
107 ])
108}
109
110pub(crate) async fn run(config: &Config, api: &CommonApi, plan: &Arc<BlockPlan>) -> Result<()> {
111 let mut actions = api.get_actions()?;
112 api.set_default_actions(&[
113 (MouseButton::Left, Some(MEM_BTN), "toggle_mem_total"),
114 (MouseButton::Left, Some(FAN_BTN), "toggle_fan_controlled"),
115 (MouseButton::WheelUp, Some(FAN_BTN), "fan_speed_up"),
116 (MouseButton::WheelDown, Some(FAN_BTN), "fan_speed_down"),
117 ])?;
118
119 let output_main = plan.output("main")?;
120
121 let mut child = Command::new("nvidia-smi")
123 .args([
124 "-l",
125 &config.interval.seconds().to_string(),
126 "-i",
127 &config.gpu_id.to_string(),
128 QUERY,
129 FORMAT,
130 ])
131 .stdout(Stdio::piped())
132 .kill_on_drop(true)
133 .spawn()
134 .error("Failed to execute nvidia-smi")?;
135 let mut reader = BufReader::new(child.stdout.take().unwrap()).lines();
136
137 let mut info = GpuInfo::from_reader(&mut reader).await?;
139 let mut show_mem_total = false;
140 let mut fan_controlled = false;
141
142 loop {
143 let mut widget = output_main.new_widget();
144
145 widget.state = match info.temperature {
146 t if t <= config.idle => State::Idle,
147 t if t <= config.good => State::Good,
148 t if t <= config.info => State::Info,
149 t if t <= config.warning => State::Warning,
150 _ => State::Critical,
151 };
152
153 widget.set_values(map! {
154 "icon" => Value::icon(icons::GPU),
155 "name" => Value::text(info.name.clone()),
156 "utilization" => Value::percents(info.utilization),
157 "memory" => Value::bytes(if show_mem_total {info.mem_total} else {info.mem_used}).with_instance(MEM_BTN),
158 "temperature" => Value::degrees_c(info.temperature),
159 "fan_speed" => Value::percents(info.fan_speed).with_instance(FAN_BTN).underline(fan_controlled).italic(fan_controlled),
160 "clocks" => Value::hertz(info.clocks),
161 "power" => Value::watts(info.power_draw),
162 });
163
164 api.set_widget(widget)?;
165
166 select! {
167 new_info = GpuInfo::from_reader(&mut reader) => {
168 info = new_info?;
169 }
170 code = child.wait() => {
171 let code = code.error("failed to check nvidia-smi exit code")?;
172 return Err(Error::new(format!("nvidia-smi exited with code {code}")));
173 }
174 Some(action) = actions.recv() => match action.as_ref() {
175 "toggle_mem_total" => {
176 show_mem_total = !show_mem_total;
177 }
178 "toggle_fan_controlled" => {
179 fan_controlled = !fan_controlled;
180 set_fan_speed(config.gpu_id, fan_controlled.then_some(info.fan_speed)).await?;
181 }
182 "fan_speed_up" if fan_controlled && info.fan_speed < 100 => {
183 info.fan_speed += 1;
184 set_fan_speed(config.gpu_id, Some(info.fan_speed)).await?;
185 }
186 "fan_speed_down" if fan_controlled && info.fan_speed > 0 => {
187 info.fan_speed -= 1;
188 set_fan_speed(config.gpu_id, Some(info.fan_speed)).await?;
189 }
190 _ => (),
191 }
192 }
193 }
194}
195
196#[derive(Debug)]
197struct GpuInfo {
198 name: String,
199 mem_total: f64, mem_used: f64, utilization: f64, temperature: u32, fan_speed: u32, clocks: f64, power_draw: f64, }
207
208impl GpuInfo {
209 async fn from_reader<B: AsyncBufRead + Unpin>(reader: &mut Lines<B>) -> Result<Self> {
215 const ERR_MSG: &str = "failed to read from nvidia-smi";
216 reader
217 .next_line()
218 .await
219 .error(ERR_MSG)?
220 .error(ERR_MSG)?
221 .parse::<GpuInfo>()
222 .error("failed to parse nvidia-smi output")
223 }
224}
225
226impl FromStr for GpuInfo {
227 type Err = Error;
228
229 fn from_str(s: &str) -> Result<Self, Self::Err> {
230 macro_rules! parse {
231 ($s:ident -> $($part:ident : $t:ident $(* $mul:expr)?),*) => {{
232 let mut parts = $s.trim().split(", ");
233 let info = GpuInfo {
234 $(
235 $part: {
236 let $part = parts
237 .next()
238 .error(concat!("missing property: ", stringify!($part)))?
239 .parse::<$t>()
240 .unwrap_or_default();
241 $(let $part = $part * $mul;)?
242 $part
243 },
244 )*
245 };
246 Ok(info)
247 }}
248 }
249 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)
251 }
252}
253
254async fn set_fan_speed(id: u64, speed: Option<u32>) -> Result<()> {
255 const ERR_MSG: &str = "Failed to execute nvidia-settings";
256 let mut cmd = Command::new("nvidia-settings");
257 if let Some(speed) = speed {
258 cmd.args([
259 "-a",
260 &format!("[gpu:{id}]/GPUFanControlState=1"),
261 "-a",
262 &format!("[fan:{id}]/GPUTargetFanSpeed={speed}"),
263 ]);
264 } else {
265 cmd.args(["-a", &format!("[gpu:{id}]/GPUFanControlState=0")]);
266 }
267 if cmd
268 .spawn()
269 .error(ERR_MSG)?
270 .wait()
271 .await
272 .error(ERR_MSG)?
273 .success()
274 {
275 Ok(())
276 } else {
277 Err(Error::new(ERR_MSG))
278 }
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284
285 #[test]
286 fn plan_declares_single_output_with_gpu_icon() {
287 let plan = prepare(&Config::default()).unwrap();
288 let ids: Vec<_> = plan.outputs().map(|o| o.id()).collect();
289 assert_eq!(ids, ["main"]);
290 let main = plan.output("main").unwrap();
291 assert_eq!(main.single_icon("icon").unwrap(), "gpu");
292 assert!(main.format().contains_key("utilization"));
293 }
294
295 #[test]
296 fn plan_uses_configured_format() {
297 let config = Config {
298 format: " $icon $fan_speed ".parse().unwrap(),
299 ..Config::default()
300 };
301 let plan = prepare(&config).unwrap();
302 let main = plan.output("main").unwrap();
303 assert!(main.format().contains_key("fan_speed"));
304 assert!(!main.format().contains_key("utilization"));
305 }
306}