Skip to main content

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//! On laptops with switchable graphics, `nvidia-smi` powers on the dedicated GPU every time it
14//! runs, which keeps the GPU awake and raises power consumption. Increase `interval` or avoid this
15//! block if that matters on battery.
16//!
17//! # Configuration
18//!
19//! Key | Values | Default
20//! ----|--------|--------
21//! `gpu_id` | GPU id in system. | `0`
22//! `format` | A string to customise the output of this block. See below for available placeholders. | `" $icon $utilization $memory $temperature "`
23//! `interval` | Update interval in seconds. | `1`
24//! `idle` | Maximum temperature, below which state is set to idle | `50`
25//! `good` | Maximum temperature, below which state is set to good | `70`
26//! `info` | Maximum temperature, below which state is set to info | `75`
27//! `warning` | Maximum temperature, below which state is set to warning | `80`
28//!
29//! Placeholder   | Type   | Unit
30//! --------------|--------|---------------
31//! `icon`        | Icon   | -
32//! `name`        | Text   | -
33//! `utilization` | Number | Percents
34//! `memory`      | Number | Bytes
35//! `temperature` | Number | Degrees
36//! `fan_speed`   | Number | Percents
37//! `clocks`      | Number | Hertz
38//! `power`       | Number | Watts
39//!
40//! Widget    | Placeholder
41//! ----------|-------------
42//! `mem_btn` | `$memory`
43//! `fan_btn` | `$fan_speed`
44//!
45//! Action                  | Default button
46//! ------------------------|----------------
47//! `toggle_mem_total`      | Left on `mem_btn`
48//! `toggle_fan_controlled` | Left on `fan_btn`
49//! `fan_speed_up`          | Wheel Up on `fan_btn`
50//! `fan_speed_down`        | Wheel Down on `fan_btn`
51//!
52//! # Example
53//!
54//! ```toml
55//! [[block]]
56//! block = "nvidia_gpu"
57//! interval = 1
58//! format = " $icon GT 1030 $utilization $temperature $clocks "
59//! ```
60//!
61//! # Icons Used
62//! - `gpu`
63//!
64//! # TODO
65//! - Provide a `mappings` option similar to `keyboard_layout`'s  to map GPU names to labels?
66
67use 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    // Run `nvidia-smi` command
112    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    // Read the initial info
128    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,   // bytes
190    mem_used: f64,    // bytes
191    utilization: f64, // percents
192    temperature: u32, // degrees
193    fan_speed: u32,   // percents
194    clocks: f64,      // hertz
195    power_draw: f64,  // watts
196}
197
198impl GpuInfo {
199    /// Read a line from provided reader and parse it
200    ///
201    /// # Cancel safety
202    ///
203    /// This method should be cancellation safe, because it has only one `.await` and it is on `next_line`, which is cancellation safe.
204    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        // `memory` and `clocks` are initially in MB and MHz, so we have to multiply them by 1_000_000
240        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}