1use 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 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 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 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 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 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 match (stats, device.iface.stats) {
165 (None, new_stats) => stats = new_stats,
167 (Some(_), None) => stats = None,
169 (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 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 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 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 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 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 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}