i3status_rs/blocks/
vpn.rs

1//! Shows the current connection status for VPN networks
2//!
3//! This widget toggles the connection on left click.
4//!
5//! # Configuration
6//!
7//! Key | Values | Default
8//! ----|--------|--------
9//! `driver` | Which vpn should be used . Available drivers are: `"nordvpn"` and `"mullvad"` | `"nordvpn"`
10//! `interval` | Update interval in seconds. | `10`
11//! `format_connected` | A string to customise the output in case the network is connected. See below for available placeholders. | `" VPN: $icon "`
12//! `format_disconnected` | A string to customise the output in case the network is disconnected. See below for available placeholders. | `" VPN: $icon "`
13//! `state_connected` | The widgets state if the vpn network is connected. | `info`
14//! `state_disconnected` | The widgets state if the vpn network is disconnected | `idle`
15//!
16//! Placeholder | Value                                                     | Type   | Unit
17//! ------------|-----------------------------------------------------------|--------|------
18//! `icon`      | A static icon                                             | Icon   | -
19//! `country`   | Country currently connected to                            | Text   | -
20//! `flag`      | Country specific flag (depends on a font supporting them) | Text   | -
21//!
22//! Action    | Default button | Description
23//! ----------|----------------|-----------------------------------
24//! `toggle`  | Left           | toggles the vpn network connection
25//!
26//! # Drivers
27//!
28//! ## nordvpn
29//! Behind the scenes the nordvpn driver uses the `nordvpn` command line binary. In order for this to work
30//! properly the binary should be executable without root privileges.
31//!
32//! ## Mullvad
33//! Behind the scenes the mullvad driver uses the `mullvad` command line binary. In order for this to work properly the binary should be executable and mullvad daemon should be running.
34//!
35//! # Example
36//!
37//! Shows the current vpn network state:
38//!
39//! ```toml
40//! [[block]]
41//! block = "vpn"
42//! driver = "nordvpn"
43//! interval = 10
44//! format_connected = "VPN: $icon "
45//! format_disconnected = "VPN: $icon "
46//! state_connected = "good"
47//! state_disconnected = "warning"
48//! ```
49//!
50//! Possible values for `state_connected` and `state_disconnected`:
51//!
52//! ```text
53//! warning
54//! critical
55//! good
56//! info
57//! idle
58//! ```
59//!
60//! # Icons Used
61//!
62//! - `net_vpn`
63//! - `net_wired`
64//! - `net_down`
65//! - country code flags (if supported by font)
66//!
67//! Flags: They are not icons but unicode glyphs. You will need a font that
68//! includes them. Tested with: <https://www.babelstone.co.uk/Fonts/Flags.html>
69
70mod nordvpn;
71use nordvpn::NordVpnDriver;
72mod mullvad;
73use mullvad::MullvadDriver;
74
75use super::prelude::*;
76
77#[derive(Deserialize, Debug, SmartDefault)]
78#[serde(rename_all = "snake_case")]
79pub enum DriverType {
80    #[default]
81    Nordvpn,
82    Mullvad,
83}
84
85#[derive(Deserialize, Debug, SmartDefault)]
86#[serde(deny_unknown_fields, default)]
87pub struct Config {
88    pub driver: DriverType,
89    #[default(10.into())]
90    pub interval: Seconds,
91    pub format_connected: FormatConfig,
92    pub format_disconnected: FormatConfig,
93    pub state_connected: State,
94    pub state_disconnected: State,
95}
96
97enum Status {
98    Connected {
99        country: String,
100        country_flag: String,
101    },
102    Disconnected,
103    Error,
104}
105
106impl Status {
107    fn icon(&self) -> Cow<'static, str> {
108        match self {
109            Status::Connected { .. } => "net_vpn".into(),
110            Status::Disconnected => "net_wired".into(),
111            Status::Error => "net_down".into(),
112        }
113    }
114}
115
116pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
117    let mut actions = api.get_actions()?;
118    api.set_default_actions(&[(MouseButton::Left, None, "toggle")])?;
119
120    let format_connected = config.format_connected.with_default(" VPN: $icon ")?;
121    let format_disconnected = config.format_disconnected.with_default(" VPN: $icon ")?;
122
123    let driver: Box<dyn Driver> = match config.driver {
124        DriverType::Nordvpn => Box::new(NordVpnDriver::new().await),
125        DriverType::Mullvad => Box::new(MullvadDriver::new().await),
126    };
127
128    loop {
129        let status = driver.get_status().await?;
130
131        let mut widget = Widget::new();
132
133        widget.state = match &status {
134            Status::Connected {
135                country,
136                country_flag,
137            } => {
138                widget.set_values(map!(
139                        "icon" => Value::icon(status.icon()),
140                        "country" => Value::text(country.to_string()),
141                        "flag" => Value::text(country_flag.to_string()),
142
143                ));
144                widget.set_format(format_connected.clone());
145                config.state_connected
146            }
147            Status::Disconnected => {
148                widget.set_values(map!(
149                        "icon" => Value::icon(status.icon()),
150                ));
151                widget.set_format(format_disconnected.clone());
152                config.state_disconnected
153            }
154            Status::Error => {
155                widget.set_values(map!(
156                        "icon" => Value::icon(status.icon()),
157                ));
158                widget.set_format(format_disconnected.clone());
159                State::Critical
160            }
161        };
162
163        api.set_widget(widget)?;
164
165        select! {
166            _ = sleep(config.interval.0) => (),
167            _ = api.wait_for_update_request() => (),
168            Some(action) = actions.recv() => match action.as_ref() {
169                "toggle" => driver.toggle_connection(&status).await?,
170                _ => (),
171            }
172        }
173    }
174}
175
176#[async_trait]
177trait Driver {
178    async fn get_status(&self) -> Result<Status>;
179    async fn toggle_connection(&self, status: &Status) -> Result<()>;
180}