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"`, `"mullvad"`, `"tailscale"` | `"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//! `format_connecting` | A string to customise the output in case the network is in a connecting state. See below for available placeholders. | `" VPN: $icon "`
14//! `state_connected` | The widgets state if the vpn network is connected. | `info`
15//! `state_disconnected` | The widgets state if the vpn network is disconnected | `idle`
16//!
17//! Placeholder | Value | Type | Unit
18//! ------------|-----------------------------------------------------------|--------|------
19//! `icon` | A static icon | Icon | -
20//! `country` | Country currently connected to | Text | -
21//! `flag` | Country specific flag (depends on a font supporting them) | Text | -
22//! `profile` | Currently selected profile configuration (tailnet) | Text | -
23//! `error` | Error message if any | Text | -
24//!
25//! Action | Default button | Description
26//! ----------|----------------|-----------------------------------
27//! `toggle` | Left | toggles the vpn network connection
28//!
29//! # Drivers
30//!
31//! ## Mullvad
32//! 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.
33//!
34//! ## nordvpn
35//! Behind the scenes the nordvpn driver uses the `nordvpn` command line binary. In order for this to work
36//! properly the binary should be executable without root privileges.
37//!
38//! ## Tailscale
39//! Behind the scenes the tailscale driver uses the `tailscale` command line binary.
40//! In order for this to work properly the tailscale daemon should be running and the user must be configured as operator:
41//! ```sh
42//! sudo tailscale set --operator=$USER
43//! ```
44//!
45//! ## Cloudflare WARP
46//! Behind the scenes the WARP driver uses the `warp-cli` command line binary. Just ensure the binary is executable without root privileges.
47//!
48//! # Example
49//!
50//! Shows the current vpn network state:
51//!
52//! ```toml
53//! [[block]]
54//! block = "vpn"
55//! driver = "nordvpn"
56//! interval = 10
57//! format_connected = "VPN: $icon "
58//! format_disconnected = "VPN: $icon "
59//! format_connecting = "VPN: $icon "
60//! state_connected = "good"
61//! state_disconnected = "warning"
62//! ```
63//!
64//! Possible values for `state_connected` and `state_disconnected`:
65//!
66//! ```text
67//! warning
68//! critical
69//! good
70//! info
71//! idle
72//! ```
73//!
74//! # Icons Used
75//!
76//! - `net_vpn`
77//! - `net_wired`
78//! - `net_wireless` (for connecting state)
79//! - `net_down`
80//! - country code flags (if supported by font)
81//!
82//! Flags: They are not icons but unicode glyphs. You will need a font that
83//! includes them. Tested with: <https://www.babelstone.co.uk/Fonts/Flags.html>
84
85mod mullvad;
86use mullvad::MullvadDriver;
87mod nordvpn;
88use nordvpn::NordVpnDriver;
89mod tailscale;
90use tailscale::TailscaleDriver;
91mod warp;
92use warp::WarpDriver;
93
94use super::prelude::*;
95
96#[derive(Deserialize, Debug, SmartDefault)]
97#[serde(rename_all = "snake_case")]
98pub enum DriverType {
99 Mullvad,
100 #[default]
101 Nordvpn,
102 Tailscale,
103 Warp,
104}
105
106#[derive(Deserialize, Debug, SmartDefault)]
107#[serde(deny_unknown_fields, default)]
108pub struct Config {
109 pub driver: DriverType,
110 #[default(10.into())]
111 pub interval: Seconds,
112 pub format_connected: FormatConfig,
113 pub format_disconnected: FormatConfig,
114 pub format_connecting: FormatConfig,
115 pub state_connected: State,
116 pub state_disconnected: State,
117}
118
119enum Status {
120 Connected {
121 country: Option<String>,
122 country_flag: Option<String>,
123 profile: Option<String>,
124 },
125 Connecting {
126 profile: Option<String>,
127 },
128 Disconnected {
129 profile: Option<String>,
130 },
131 Error(Option<String>),
132}
133
134impl Status {
135 fn icon(&self) -> Cow<'static, str> {
136 match self {
137 Status::Connected { .. } => "net_vpn".into(),
138 Status::Disconnected { .. } => "net_wired".into(),
139 Status::Connecting { .. } => "net_wireless".into(),
140 Status::Error(_) => "net_down".into(),
141 }
142 }
143}
144
145pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
146 let mut actions = api.get_actions()?;
147 api.set_default_actions(&[(MouseButton::Left, None, "toggle")])?;
148
149 let format_connected = config.format_connected.with_default(" VPN: $icon ")?;
150 let format_disconnected = config.format_disconnected.with_default(" VPN: $icon ")?;
151 let format_connecting = config.format_connecting.with_default(" VPN: $icon ")?;
152
153 let driver: Box<dyn Driver> = match config.driver {
154 DriverType::Mullvad => Box::new(MullvadDriver::new().await),
155 DriverType::Nordvpn => Box::new(NordVpnDriver::new().await),
156 DriverType::Tailscale => Box::new(TailscaleDriver::new().await),
157 DriverType::Warp => Box::new(WarpDriver::new().await),
158 };
159
160 loop {
161 let status = driver.get_status().await?;
162
163 let mut widget = Widget::new();
164
165 widget.state = match &status {
166 Status::Connected {
167 country,
168 country_flag,
169 profile,
170 } => {
171 widget.set_values(map!(
172 "icon" => Value::icon(status.icon()),
173 [if let Some(country) = country] "country" => Value::text(country.into()),
174 [if let Some(flag) = country_flag] "flag" => Value::text(flag.into()),
175 [if let Some(profile) = profile] "profile" => Value::text(profile.into()),
176 ));
177 widget.set_format(format_connected.clone());
178 config.state_connected
179 }
180 Status::Disconnected { profile } => {
181 widget.set_values(map! {
182 "icon" => Value::icon(status.icon()),
183 [if let Some(profile) = profile] "profile" => Value::text(profile.into()),
184 });
185 widget.set_format(format_disconnected.clone());
186 config.state_disconnected
187 }
188 Status::Connecting { profile } => {
189 widget.set_values(map!(
190 "icon" => Value::icon(status.icon()),
191 [if let Some(profile) = profile] "profile" => Value::text(profile.into()),
192 ));
193 widget.set_format(format_connecting.clone());
194 State::Info
195 }
196 Status::Error(error) => {
197 widget.set_values(map!(
198 "icon" => Value::icon(status.icon()),
199 [if let Some(error) = error] "error" => Value::text(error.into())
200 ));
201 widget.set_format(format_disconnected.clone());
202 State::Critical
203 }
204 };
205
206 api.set_widget(widget)?;
207
208 select! {
209 _ = sleep(config.interval.0) => (),
210 _ = api.wait_for_update_request() => (),
211 Some(action) = actions.recv() => match action.as_ref() {
212 "toggle" => driver.toggle_connection(&status).await?,
213 _ => (),
214 }
215 }
216 }
217}
218
219#[async_trait]
220trait Driver {
221 async fn get_status(&self) -> Result<Status>;
222 async fn toggle_connection(&self, status: &Status) -> Result<()>;
223}