i3status_rs/blocks/vpn/
nordvpn.rs

1use regex::Regex;
2use std::process::Stdio;
3use tokio::process::Command;
4
5use crate::blocks::prelude::*;
6use crate::util::country_flag_from_iso_code;
7
8use super::{Driver, Status};
9
10pub struct NordVpnDriver {
11    regex_country_code: Regex,
12}
13
14impl NordVpnDriver {
15    pub async fn new() -> NordVpnDriver {
16        NordVpnDriver {
17            regex_country_code: Regex::new("^.*Hostname:\\s+([a-z]{2}).*$").unwrap(),
18        }
19    }
20
21    async fn run_network_command(arg: &str) -> Result<()> {
22        Command::new("nordvpn")
23            .args([arg])
24            .stdin(Stdio::null())
25            .stdout(Stdio::null())
26            .spawn()
27            .error(format!("Problem running nordvpn command: {arg}"))?
28            .wait()
29            .await
30            .error(format!("Problem running nordvpn command: {arg}"))?;
31        Ok(())
32    }
33
34    async fn find_line(stdout: &str, needle: &str) -> Option<String> {
35        stdout
36            .lines()
37            .find(|s| s.contains(needle))
38            .map(|s| s.to_owned())
39    }
40}
41
42#[async_trait]
43impl Driver for NordVpnDriver {
44    async fn get_status(&self) -> Result<Status> {
45        let stdout = Command::new("nordvpn")
46            .args(["status"])
47            .output()
48            .await
49            .error("Problem running nordvpn command")?
50            .stdout;
51
52        let stdout = String::from_utf8(stdout).error("nordvpn produced non-UTF8 output")?;
53        let line_status = Self::find_line(&stdout, "Status:").await;
54        let line_country = Self::find_line(&stdout, "Country:").await;
55        let line_country_flag = Self::find_line(&stdout, "Hostname:").await;
56        if line_status.is_none() {
57            return Ok(Status::Error);
58        }
59        let line_status = line_status.unwrap();
60
61        if line_status.ends_with("Disconnected") {
62            return Ok(Status::Disconnected);
63        } else if line_status.ends_with("Connected") {
64            let country = match line_country {
65                Some(country_line) => country_line.rsplit(": ").next().unwrap().to_string(),
66                None => String::default(),
67            };
68            let country_flag = match line_country_flag {
69                Some(country_line_flag) => self
70                    .regex_country_code
71                    .captures_iter(&country_line_flag)
72                    .last()
73                    .map(|capture| capture[1].to_owned())
74                    .map(|code| code.to_uppercase())
75                    .map(|code| country_flag_from_iso_code(&code))
76                    .unwrap_or_default(),
77                None => String::default(),
78            };
79            return Ok(Status::Connected {
80                country,
81                country_flag,
82            });
83        }
84        Ok(Status::Error)
85    }
86
87    async fn toggle_connection(&self, status: &Status) -> Result<()> {
88        match status {
89            Status::Connected { .. } => Self::run_network_command("disconnect").await?,
90            Status::Disconnected => Self::run_network_command("connect").await?,
91            Status::Error => (),
92        }
93        Ok(())
94    }
95}