i3status_rs/blocks/vpn/
nordvpn.rs1use 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}