i3status_rs/blocks/
nvidia_gpu.rs

1//! Display the stats of your NVidia GPU
2//!
3//! By default `show_temperature` shows the used memory. Clicking the left mouse on the
4//! "temperature" part of the block will alternate it between showing used or total available
5//! memory.
6//!
7//! Clicking the left mouse button on the "fan speed" part of the block will cause it to enter into
8//! a fan speed setting mode. In this mode you can scroll the mouse wheel over the block to change
9//! the fan speeds, and left click to exit the mode.
10//!
11//! Requires `nvidia-smi` for displaying info and `nvidia_settings` for setting fan speed.
12//!
13//! # Configuration
14//!
15//! Key | Values | Default
16//! ----|--------|--------
17//! `gpu_id` | GPU id in system. | `0`
18//! `format` | A string to customise the output of this block. See below for available placeholders. | `" $icon $utilization $memory $temperature "`
19//! `interval` | Update interval in seconds. | `1`
20//! `idle` | Maximum temperature, below which state is set to idle | `50`
21//! `good` | Maximum temperature, below which state is set to good | `70`
22//! `info` | Maximum temperature, below which state is set to info | `75`
23//! `warning` | Maximum temperature, below which state is set to warning | `80`
24//!
25//! Placeholder   | Type   | Unit
26//! --------------|--------|---------------
27//! `icon`        | Icon   | -
28//! `name`        | Text   | -
29//! `utilization` | Number | Percents
30//! `memory`      | Number | Bytes
31//! `temperature` | Number | Degrees
32//! `fan_speed`   | Number | Percents
33//! `clocks`      | Number | Hertz
34//! `power`       | Number | Watts
35//!
36//! Widget    | Placeholder
37//! ----------|-------------
38//! `mem_btn` | `$memory`
39//! `fan_btn` | `$fan_speed`
40//!
41//! Action                  | Default button
42//! ------------------------|----------------
43//! `toggle_mem_total`      | Left on `mem_btn`
44//! `toggle_fan_controlled` | Left on `fan_btn`
45//! `fan_speed_up`          | Wheel Up on `fan_btn`
46//! `fan_speed_down`        | Wheel Down on `fan_btn`
47//!
48//! # Example
49//!
50//! ```toml
51//! [[block]]
52//! block = "nvidia_gpu"
53//! interval = 1
54//! format = " $icon GT 1030 $utilization $temperature $clocks "
55//! ```
56//!
57//! # Icons Used
58//! - `gpu`
59//!
60//! # TODO
61//! - Provide a `mappings` option similar to `keyboard_layout`'s  to map GPU names to labels?
62
63use 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    // Run `nvidia-smi` command
108    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    // Read the initial info
124    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,   // bytes
186    mem_used: f64,    // bytes
187    utilization: f64, // percents
188    temperature: u32, // degrees
189    fan_speed: u32,   // percents
190    clocks: f64,      // hertz
191    power_draw: f64,  // watts
192}
193
194impl GpuInfo {
195    /// Read a line from provided reader and parse it
196    ///
197    /// # Cancel safety
198    ///
199    /// This method should be cancellation safe, because it has only one `.await` and it is on `next_line`, which is cancellation safe.
200    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        // `memory` and `clocks` are initially in MB and MHz, so we have to multiply them by 1_000_000
236        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}