i3status_rs/blocks/vpn/
mullvad.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 MullvadDriver {
11    regex_country_code: Regex,
12}
13
14impl MullvadDriver {
15    pub async fn new() -> MullvadDriver {
16        MullvadDriver {
17            regex_country_code: Regex::new("Connected to ([a-z]{2}).*, ([A-Z][a-z]*).*\n").unwrap(),
18        }
19    }
20
21    async fn run_network_command(arg: &str) -> Result<()> {
22        let code = Command::new("mullvad")
23            .args([arg])
24            .stdin(Stdio::null())
25            .stdout(Stdio::null())
26            .spawn()
27            .error(format!("Problem running mullvad command: {arg}"))?
28            .wait()
29            .await
30            .error(format!("Problem running mullvad command: {arg}"))?;
31
32        if code.success() {
33            Ok(())
34        } else {
35            Err(Error::new(format!(
36                "mullvad command failed with nonzero status: {code:?}"
37            )))
38        }
39    }
40}
41
42#[async_trait]
43impl Driver for MullvadDriver {
44    async fn get_status(&self) -> Result<Status> {
45        let stdout = Command::new("mullvad")
46            .args(["status"])
47            .output()
48            .await
49            .error("Problem running mullvad command")?
50            .stdout;
51
52        let status = String::from_utf8(stdout).error("mullvad produced non-UTF8 output")?;
53
54        if status.contains("Disconnected") {
55            return Ok(Status::Disconnected);
56        } else if status.contains("Connected") {
57            let (country_flag, country) = self
58                .regex_country_code
59                .captures_iter(&status)
60                .next()
61                .map(|capture| {
62                    let country_code = capture[1].to_uppercase();
63                    let country = capture[2].to_owned();
64                    let country_flag = country_flag_from_iso_code(&country_code);
65                    (country_flag, country)
66                })
67                .unwrap_or_default();
68
69            return Ok(Status::Connected {
70                country,
71                country_flag,
72            });
73        }
74        Ok(Status::Error)
75    }
76
77    async fn toggle_connection(&self, status: &Status) -> Result<()> {
78        match status {
79            Status::Connected { .. } => Self::run_network_command("disconnect").await?,
80            Status::Disconnected => Self::run_network_command("connect").await?,
81            Status::Error => (),
82        }
83        Ok(())
84    }
85}