mod mullvad;
use mullvad::MullvadDriver;
mod nordvpn;
use nordvpn::NordVpnDriver;
mod tailscale;
use tailscale::TailscaleDriver;
mod warp;
use warp::WarpDriver;
use super::prelude::*;
#[derive(Deserialize, Debug, SmartDefault)]
#[serde(rename_all = "snake_case")]
pub enum DriverType {
Mullvad,
#[default]
Nordvpn,
Tailscale,
Warp,
}
#[derive(Deserialize, Debug, SmartDefault)]
#[serde(deny_unknown_fields, default)]
pub struct Config {
pub driver: DriverType,
#[default(10.into())]
pub interval: Seconds,
pub format_connected: FormatConfig,
pub format_disconnected: FormatConfig,
pub state_connected: State,
pub state_disconnected: State,
}
enum Status {
Connected {
country: Option<String>,
country_flag: Option<String>,
profile: Option<String>,
},
Disconnected {
profile: Option<String>,
},
Error(Option<String>),
}
impl Status {
fn icon(&self) -> Cow<'static, str> {
match self {
Status::Connected { .. } => "net_vpn".into(),
Status::Disconnected { .. } => "net_wired".into(),
Status::Error(_) => "net_down".into(),
}
}
}
pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
let mut actions = api.get_actions()?;
api.set_default_actions(&[(MouseButton::Left, None, "toggle")])?;
let format_connected = config.format_connected.with_default(" VPN: $icon ")?;
let format_disconnected = config.format_disconnected.with_default(" VPN: $icon ")?;
let driver: Box<dyn Driver> = match config.driver {
DriverType::Mullvad => Box::new(MullvadDriver::new().await),
DriverType::Nordvpn => Box::new(NordVpnDriver::new().await),
DriverType::Tailscale => Box::new(TailscaleDriver::new().await),
DriverType::Warp => Box::new(WarpDriver::new().await),
};
loop {
let status = driver.get_status().await?;
let mut widget = Widget::new();
widget.state = match &status {
Status::Connected {
country,
country_flag,
profile,
} => {
widget.set_values(map!(
"icon" => Value::icon(status.icon()),
[if let Some(country) = country] "country" => Value::text(country.into()),
[if let Some(flag) = country_flag] "flag" => Value::text(flag.into()),
[if let Some(profile) = profile] "profile" => Value::text(profile.into()),
));
widget.set_format(format_connected.clone());
config.state_connected
}
Status::Disconnected { profile } => {
widget.set_values(map! {
"icon" => Value::icon(status.icon()),
[if let Some(profile) = profile] "profile" => Value::text(profile.into()),
});
widget.set_format(format_disconnected.clone());
config.state_disconnected
}
Status::Error(error) => {
widget.set_values(map!(
"icon" => Value::icon(status.icon()),
[if let Some(error) = error] "error" => Value::text(error.into())
));
widget.set_format(format_disconnected.clone());
State::Critical
}
};
api.set_widget(widget)?;
select! {
_ = sleep(config.interval.0) => (),
_ = api.wait_for_update_request() => (),
Some(action) = actions.recv() => match action.as_ref() {
"toggle" => driver.toggle_connection(&status).await?,
_ => (),
}
}
}
}
#[async_trait]
trait Driver {
async fn get_status(&self) -> Result<Status>;
async fn toggle_connection(&self, status: &Status) -> Result<()>;
}