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(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}