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