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}