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(None));
58        }
59        let line_status = line_status.unwrap();
60
61        if line_status.ends_with("Disconnected") {
62            return Ok(Status::Disconnected { profile: None });
63        } else if line_status.ends_with("Connected") {
64            let country = line_country
65                .map(|country_line| country_line.rsplit(": ").next().unwrap().to_string());
66            let country_flag = match line_country_flag {
67                Some(country_line_flag) => self
68                    .regex_country_code
69                    .captures_iter(&country_line_flag)
70                    .last()
71                    .map(|capture| capture[1].to_owned())
72                    .map(|code| code.to_uppercase())
73                    .map(|code| country_flag_from_iso_code(&code)),
74                None => None,
75            };
76            return Ok(Status::Connected {
77                country,
78                country_flag,
79                profile: None,
80            });
81        }
82        Ok(Status::Error(None))
83    }
84
85    async fn toggle_connection(&self, status: &Status) -> Result<()> {
86        match status {
87            Status::Connected { .. } => Self::run_network_command("disconnect").await?,
88            Status::Disconnected { .. } => Self::run_network_command("connect").await?,
89            Status::Error(_) => (),
90        }
91        Ok(())
92    }
93}