Skip to main content

i3status_rs/blocks/
net.rs

1//! Network information
2//!
3//! This block uses `sysfs` and `netlink` and thus does not require any external dependencies.
4//!
5//! # Configuration
6//!
7//! Key | Values | Default
8//! ----|--------|--------
9//! `device` | Network interface to monitor (as specified in `/sys/class/net/`). Supports regex. | If not set, device will be automatically selected every `interval`
10//! `interval` | Update interval in seconds | `2`
11//! `format` | A [MultiFormat][MaybeMultiFormatConfig] string to customise the output of this block. See below for available placeholders. | `[" $icon ^icon_net_down $speed_down.eng(prefix:K) ^icon_net_up $speed_up.eng(prefix:K) "]`
12//! `inactive_format` | Same as `format` but for when the interface is inactive | `" $icon Down "`
13//! `missing_format` | Same as `format` but for when the device is missing | `" × "`
14//!
15//! Action          | Description                               | Default button
16//! ----------------|-------------------------------------------|---------------
17//! `toggle_format` **DEPRECATED** | Toggles between `format` and `format_alt` | -
18//! `next_format`  | Switches to the next format in the list     | Left
19//! `prev_format`  | Switches to the previous format in the list | Right
20//!
21//! Placeholder       | Value                       | Type   | Unit
22//! ------------------|-----------------------------|--------|---------------
23//! `icon`            | Icon based on device's type | Icon   | -
24//! `speed_down`      | Download speed              | Number | Bytes per second
25//! `speed_up`        | Upload speed                | Number | Bytes per second
26//! `graph_down`      | Download speed graph        | Text   | -
27//! `graph_up`        | Upload speed graph          | Text   | -
28//! `device`          | The name of device          | Text   | -
29//! `ssid`            | Network SSID (WiFi only)    | Text   | -
30//! `frequency`       | WiFi frequency              | Number | Hz
31//! `signal_strength` | WiFi signal                 | Number | %
32//! `bitrate`         | WiFi connection bitrate     | Number | Bits per second
33//! `ip`              | IPv4 address of the iface   | Text   | -
34//! `ipv6`            | IPv6 address of the iface   | Text   | -
35//! `nameserver`      | Nameserver                  | Text   | -
36//! `enabled`         | Present if the device is switched on (not turned off with `rfkill` or `ip link set down`) | Flag | -
37//!
38//! # Example
39//!
40//! Display WiFi info if available
41//!
42//! ```toml
43//! [[block]]
44//! block = "net"
45//! format = " $icon {$signal_strength $ssid $frequency|Wired connection} via $device "
46//! ```
47//!
48//! Display exact device
49//!
50//! ```toml
51//! [[block]]
52//! block = "net"
53//! device = "^wlo0$"
54//! ```
55//!
56//! Tell a WiFi device that is not connected from one that is switched off (with `rfkill` for
57//! example)
58//!
59//! ```toml
60//! [[block]]
61//! block = "net"
62//! device = "^wlan0$"
63//! format = " $icon $signal_strength "
64//! inactive_format = " {$enabled{not connected}|switched off} "
65//! ```
66//!
67//! # Icons Used
68//! - `net_loopback` (`$icon`)
69//! - `net_vpn` (`$icon`)
70//! - `net_wired` (`$icon`)
71//! - `net_wireless` (`$icon`, as a progression)
72//! - `net_up` (`^icon_net_up`)
73//! - `net_down` (`^icon_net_down`)
74
75use super::prelude::*;
76use crate::netlink::NetDevice;
77use crate::util;
78use itertools::Itertools as _;
79use regex::Regex;
80use std::time::Instant;
81
82#[derive(Deserialize, Debug, SmartDefault)]
83#[serde(default)]
84pub struct Config {
85    pub device: Option<String>,
86    #[default(2.into())]
87    pub interval: Seconds,
88    #[serde(flatten)]
89    pub formats: MaybeMultiFormatConfig,
90    pub inactive_format: FormatConfig,
91    pub missing_format: FormatConfig,
92}
93
94pub(crate) fn prepare(config: &Config) -> Result<Arc<BlockPlan>> {
95    // Every state that renders a device sets these on every render; the
96    // wifi-only values (ssid, signal_strength, ...) and the addresses are
97    // conditional and stay undeclared.
98    let device_output =
99        |output: OutputPlan| output.icon("icon", IconChoices::fixed(NetDevice::ALL_ICONS));
100    let formats = config.formats.with_default(
101        " $icon ^icon_net_down $speed_down.eng(prefix:K) ^icon_net_up $speed_up.eng(prefix:K) ",
102    )?;
103    // One output per format the user can rotate to, plus the two states that
104    // are not format variants at all: an inactive interface and a missing
105    // device each have their own format and are selected by `run()`.
106    let mut outputs = format_outputs(formats, device_output);
107    outputs.push(device_output(OutputPlan::new(
108        "inactive",
109        config.inactive_format.with_default(" $icon Down ")?,
110    )));
111    // `missing` sets no values at all: nothing to declare.
112    outputs.push(OutputPlan::new(
113        "missing",
114        config.missing_format.with_default(" × ")?,
115    ));
116    BlockPlan::new(outputs)
117}
118
119pub(crate) async fn run(config: &Config, api: &CommonApi, plan: &Arc<BlockPlan>) -> Result<()> {
120    let mut actions = api.get_actions()?;
121    api.set_default_actions(&[
122        (MouseButton::Left, None, "next_format"),
123        (MouseButton::Right, None, "prev_format"),
124    ])?;
125
126    // The plan also holds the `inactive` and `missing` states; the rotation
127    // covers the format outputs alone.
128    let mut formats = FormatRotation::new(plan)?;
129    let output_inactive = plan.output("inactive")?;
130    let output_missing = plan.output("missing")?;
131
132    let mut timer = config.interval.timer();
133
134    let device_re = config
135        .device
136        .as_deref()
137        .map(Regex::new)
138        .transpose()
139        .error("Failed to parse device regex")?;
140
141    // Stats
142    let mut stats = None;
143    let mut stats_timer = Instant::now();
144    let mut tx_hist = [0f64; 8];
145    let mut rx_hist = [0f64; 8];
146
147    loop {
148        match NetDevice::new(device_re.as_ref()).await? {
149            None => {
150                api.set_widget(output_missing.new_widget())?;
151            }
152            Some(device) => {
153                let output = if device.is_up() {
154                    formats.current()
155                } else {
156                    &output_inactive
157                };
158                let mut widget = output.new_widget();
159
160                let mut speed_down: f64 = 0.0;
161                let mut speed_up: f64 = 0.0;
162
163                // Calculate speed
164                match (stats, device.iface.stats) {
165                    // No previous stats available
166                    (None, new_stats) => stats = new_stats,
167                    // No new stats available
168                    (Some(_), None) => stats = None,
169                    // All stats available
170                    (Some(old_stats), Some(new_stats)) => {
171                        let diff = new_stats - old_stats;
172                        let elapsed = stats_timer.elapsed().as_secs_f64();
173                        stats_timer = Instant::now();
174                        speed_down = diff.rx_bytes as f64 / elapsed;
175                        speed_up = diff.tx_bytes as f64 / elapsed;
176                        stats = Some(new_stats);
177                    }
178                }
179                push_to_hist(&mut rx_hist, speed_down);
180                push_to_hist(&mut tx_hist, speed_up);
181
182                let icon = if let Some(signal) = device.signal() {
183                    Value::icon_progression(device.icon, signal / 100.0)
184                } else {
185                    Value::icon(device.icon)
186                };
187
188                widget.set_values(map! {
189                    "icon" => icon,
190                    "speed_down" => Value::bytes(speed_down),
191                    "speed_up" => Value::bytes(speed_up),
192                    "graph_down" => Value::text(util::format_bar_graph(&rx_hist)),
193                    "graph_up" => Value::text(util::format_bar_graph(&tx_hist)),
194                    [if let Some(v) = device.ip] "ip" => Value::text(v.to_string()),
195                    [if let Some(v) = device.ipv6] "ipv6" => Value::text(v.to_string()),
196                    [if let Some(v) = device.ssid()] "ssid" => Value::text(v),
197                    [if let Some(v) = device.frequency()] "frequency" => Value::hertz(v),
198                    [if let Some(v) = device.bitrate()] "bitrate" => Value::bits(v),
199                    [if let Some(v) = device.signal()] "signal_strength" => Value::percents(v),
200                    [if !device.nameservers.is_empty()] "nameserver" => Value::text(
201                                                                            device
202                                                                                .nameservers
203                                                                                .into_iter()
204                                                                                .map(|s| s.to_string())
205                                                                                .join(" "),
206                                                                        ),
207                    [if device.iface.enabled] "enabled" => Value::flag(),
208                    "device" => Value::text(device.iface.name),
209                });
210
211                api.set_widget(widget)?;
212            }
213        }
214
215        loop {
216            select! {
217                _ = timer.tick() => break,
218                _ = api.wait_for_update_request() => break,
219                Some(action) = actions.recv() => match action.as_ref() {
220                    "next_format" | "toggle_format" => {
221                        formats.next();
222                        break;
223                    }
224                    "prev_format" => {
225                        formats.prev();
226                        break;
227                    }
228                    _ => ()
229                }
230            }
231        }
232    }
233}
234
235fn push_to_hist<T>(hist: &mut [T], elem: T) {
236    hist[0] = elem;
237    hist.rotate_left(1);
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243
244    #[test]
245    fn plan_declares_device_icon_set_on_every_rendering_state() {
246        let plan = prepare(&Config::default()).unwrap();
247        let ids: Vec<_> = plan.outputs().map(|o| o.id()).collect();
248        assert_eq!(ids, ["format", "inactive", "missing"]);
249
250        for id in ["format", "inactive"] {
251            let output = plan.output(id).unwrap();
252            let choices = output.output().choices_for("icon").unwrap();
253            for icon in NetDevice::ALL_ICONS {
254                assert!(choices.permits(icon), "{id} must permit {icon}");
255            }
256            assert!(!choices.permits("net_up"));
257        }
258
259        // `missing` renders a bare format and sets no values at all.
260        let missing = plan.output("missing").unwrap();
261        assert_eq!(missing.output().icon_placeholders().count(), 0);
262    }
263
264    #[test]
265    fn every_configured_format_becomes_an_output() {
266        let plan = prepare(&Config::default()).unwrap();
267        assert!(plan.output("format2").is_err());
268
269        let config: Config = toml::from_str(r#"format = [" $icon ", " $icon $device "]"#).unwrap();
270        let plan = prepare(&config).unwrap();
271        let ids: Vec<_> = plan.outputs().map(|o| o.id()).collect();
272        // The extra states stay alongside the per-format outputs.
273        assert_eq!(ids, ["format", "format2", "inactive", "missing"]);
274
275        let second = plan.output("format2").unwrap();
276        assert!(second.format().contains_key("device"));
277        let choices = second.output().choices_for("icon").unwrap();
278        assert!(choices.permits("net_wireless"));
279    }
280
281    #[test]
282    fn the_arrows_in_the_default_format_are_part_of_the_icon_surface() {
283        // They are `^icon_*` tokens rather than icon values, so they are not
284        // declared placeholders — but the block still draws them.
285        let plan = prepare(&Config::default()).unwrap();
286        let format = plan.output("format").unwrap();
287        assert_eq!(
288            format.output().static_icons(),
289            ["net_down", "net_up"],
290            "the default format's arrows must be visible to an inspector"
291        );
292        // The states that render no format icons say so.
293        for id in ["inactive", "missing"] {
294            let output = plan.output(id).unwrap();
295            assert!(output.output().static_icons().is_empty(), "{id}");
296        }
297    }
298
299    #[test]
300    fn every_chooser_icon_is_declared() {
301        // Exercise every branch of the runtime icon chooser and check the
302        // result against the declared set, so the two cannot drift apart.
303        let mut seen = Vec::new();
304        for is_wireless in [false, true] {
305            for tun_wg_ppp in [false, true] {
306                for name in ["lo", "eth0", "tun0", "wlan0"] {
307                    let icon = NetDevice::icon_for(is_wireless, tun_wg_ppp, name);
308                    assert!(
309                        NetDevice::ALL_ICONS.contains(&icon),
310                        "chooser returned undeclared icon {icon}"
311                    );
312                    if !seen.contains(&icon) {
313                        seen.push(icon);
314                    }
315                }
316            }
317        }
318        // ...and every declared name is actually reachable.
319        assert_eq!(seen.len(), NetDevice::ALL_ICONS.len());
320    }
321
322    #[test]
323    fn test_push_to_hist() {
324        let mut hist = [0; 4];
325        assert_eq!(&hist, &[0, 0, 0, 0]);
326        push_to_hist(&mut hist, 1);
327        assert_eq!(&hist, &[0, 0, 0, 1]);
328        push_to_hist(&mut hist, 3);
329        assert_eq!(&hist, &[0, 0, 1, 3]);
330        push_to_hist(&mut hist, 0);
331        assert_eq!(&hist, &[0, 1, 3, 0]);
332        push_to_hist(&mut hist, 10);
333        assert_eq!(&hist, &[1, 3, 0, 10]);
334        push_to_hist(&mut hist, 2);
335        assert_eq!(&hist, &[3, 0, 10, 2]);
336    }
337}