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` (`$icon`)
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(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    // Run `nvidia-smi` command
122    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    // Read the initial info
138    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,   // bytes
200    mem_used: f64,    // bytes
201    utilization: f64, // percents
202    temperature: u32, // degrees
203    fan_speed: u32,   // percents
204    clocks: f64,      // hertz
205    power_draw: f64,  // watts
206}
207
208impl GpuInfo {
209    /// Read a line from provided reader and parse it
210    ///
211    /// # Cancel safety
212    ///
213    /// This method should be cancellation safe, because it has only one `.await` and it is on `next_line`, which is cancellation safe.
214    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        // `memory` and `clocks` are initially in MB and MHz, so we have to multiply them by 1_000_000
250        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}