use crate::theme;
use ratatui::style::{Color, Modifier, Style};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum SigilId {
Connected,
ConnectedUnauthoritative,
Connecting,
Reconnecting,
Disconnecting,
AwaitingInput,
Failed,
PrimaryMarker,
RiskMarker,
SgOk,
SgNotApplicable,
SgAlarmWarn,
SgAlarmError,
}
#[derive(Clone, Copy, Debug)]
pub struct Sigil {
pub id: SigilId,
pub glyph: &'static str,
pub label: &'static str,
pub description: &'static str,
pub color: Color,
pub bold: bool,
pub dim: bool,
pub category: SigilCategory,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SigilCategory {
Sidebar,
SecurityGuard,
}
impl Sigil {
#[must_use]
pub fn style(&self) -> Style {
let mut s = Style::default().fg(self.color);
if self.bold {
s = s.add_modifier(Modifier::BOLD);
}
if self.dim {
s = s.add_modifier(Modifier::DIM);
}
s
}
}
#[must_use]
pub fn sigil(id: SigilId) -> &'static Sigil {
CATALOG
.iter()
.find(|s| s.id == id)
.expect("every SigilId variant must have a catalog entry — see tests")
}
pub const CATALOG: &[Sigil] = &[
Sigil {
id: SigilId::Connected,
glyph: "\u{25cf}",
label: "Connected",
description: "Tunnel is up and authoritatively tracked by vortix.",
color: theme::SUCCESS,
bold: false,
dim: false,
category: SigilCategory::Sidebar,
},
Sigil {
id: SigilId::ConnectedUnauthoritative,
glyph: "\u{25cf}",
label: "Connected (external)",
description: "Up but vortix can't reliably attribute the kernel interface to a PID — won't be elected as your exit.",
color: theme::INACTIVE,
bold: false,
dim: true,
category: SigilCategory::Sidebar,
},
Sigil {
id: SigilId::Connecting,
glyph: "\u{25d0}",
label: "Connecting",
description: "Handshake in flight. Auto-times-out per the configured connect_timeout if the protocol layer doesn't report success.",
color: theme::WARNING,
bold: false,
dim: false,
category: SigilCategory::Sidebar,
},
Sigil {
id: SigilId::Reconnecting,
glyph: "\u{21bb}",
label: "Reconnecting",
description: "Tunnel dropped; vortix is auto-retrying per the configured retry budget.",
color: theme::WARNING,
bold: false,
dim: true,
category: SigilCategory::Sidebar,
},
Sigil {
id: SigilId::Disconnecting,
glyph: "\u{25d1}",
label: "Disconnecting",
description: "Teardown in flight (wg-quick / openvpn finishing up).",
color: theme::WARNING,
bold: false,
dim: false,
category: SigilCategory::Sidebar,
},
Sigil {
id: SigilId::AwaitingInput,
glyph: "?",
label: "Awaiting input",
description: "Waiting for you to type a 2FA code, passphrase, or similar. Focus Connection Details and press Enter.",
color: theme::WARNING,
bold: false,
dim: false,
category: SigilCategory::Sidebar,
},
Sigil {
id: SigilId::Failed,
glyph: "\u{2717}",
label: "Failed",
description: "Last connect attempt failed. The badge persists until you retry or dismiss the row.",
color: theme::ERROR,
bold: false,
dim: false,
category: SigilCategory::Sidebar,
},
Sigil {
id: SigilId::PrimaryMarker,
glyph: "*",
label: "Primary marker",
description: "Sidebar suffix on the current Primary tunnel. Cross-correlates with the header's CONNECTED-name and Connection Details' Role: Primary line.",
color: theme::SUCCESS,
bold: true,
dim: false,
category: SigilCategory::Sidebar,
},
Sigil {
id: SigilId::RiskMarker,
glyph: "!",
label: "Risk annotation",
description: "Side-suffix flagging a per-tunnel risk worth attention. Today: appears on Split tunnel (yielded) to flag the mode-mismatch.",
color: theme::WARNING,
bold: true,
dim: false,
category: SigilCategory::Sidebar,
},
Sigil {
id: SigilId::SgOk,
glyph: "\u{2713}",
label: "OK",
description: "This row's check passes. Muted green so it fades into the all-OK state without competing for attention.",
color: theme::SUCCESS,
bold: false,
dim: false,
category: SigilCategory::SecurityGuard,
},
Sigil {
id: SigilId::SgNotApplicable,
glyph: "\u{2500}",
label: "Not applicable",
description: "This row's check doesn't apply on the current platform or in the current state. E.g., IPv6 when vortix's killswitch is v4-only, or IP when no primary owns the default route.",
color: theme::INACTIVE,
bold: false,
dim: false,
category: SigilCategory::SecurityGuard,
},
Sigil {
id: SigilId::SgAlarmWarn,
glyph: "\u{26a0}",
label: "Warning",
description: "Action recommended but not strictly broken. E.g., the kill switch is in Auto mode and the VPN is up but not yet armed in DROP state.",
color: theme::WARNING,
bold: true,
dim: false,
category: SigilCategory::SecurityGuard,
},
Sigil {
id: SigilId::SgAlarmError,
glyph: "\u{2717}",
label: "Alarm",
description: "Real problem the panel can quantify — IP leak, DNS leak, blocked egress. Always bold; typically has a sub-line explaining what to do.",
color: theme::ERROR,
bold: true,
dim: false,
category: SigilCategory::SecurityGuard,
},
];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_sigil_id_has_a_catalog_entry() {
let all_ids = [
SigilId::Connected,
SigilId::ConnectedUnauthoritative,
SigilId::Connecting,
SigilId::Reconnecting,
SigilId::Disconnecting,
SigilId::AwaitingInput,
SigilId::Failed,
SigilId::PrimaryMarker,
SigilId::RiskMarker,
SigilId::SgOk,
SigilId::SgNotApplicable,
SigilId::SgAlarmWarn,
SigilId::SgAlarmError,
];
#[allow(clippy::match_same_arms, clippy::ignored_unit_patterns)]
for id in &all_ids {
match id {
SigilId::Connected => (),
SigilId::ConnectedUnauthoritative => (),
SigilId::Connecting => (),
SigilId::Reconnecting => (),
SigilId::Disconnecting => (),
SigilId::AwaitingInput => (),
SigilId::Failed => (),
SigilId::PrimaryMarker => (),
SigilId::RiskMarker => (),
SigilId::SgOk => (),
SigilId::SgNotApplicable => (),
SigilId::SgAlarmWarn => (),
SigilId::SgAlarmError => (),
}
}
for id in all_ids {
let found = CATALOG.iter().find(|s| s.id == id);
assert!(
found.is_some(),
"SigilId::{id:?} has no catalog entry — every SigilId variant must be in CATALOG"
);
}
}
#[test]
fn catalog_has_no_duplicate_ids() {
let mut seen = std::collections::HashSet::new();
for entry in CATALOG {
assert!(
seen.insert(entry.id),
"duplicate catalog entry for SigilId::{:?}",
entry.id
);
}
}
#[test]
fn every_sigil_glyph_is_single_display_cell() {
use unicode_width::UnicodeWidthStr;
for entry in CATALOG {
assert_eq!(
UnicodeWidthStr::width(entry.glyph),
1,
"sigil glyph `{}` for {:?} must be 1 display cell wide (was {})",
entry.glyph,
entry.id,
UnicodeWidthStr::width(entry.glyph)
);
}
}
#[test]
fn style_combines_color_and_modifiers() {
let bold_alarm = Sigil {
id: SigilId::SgAlarmError,
glyph: "x",
label: "x",
description: "x",
color: Color::Red,
bold: true,
dim: false,
category: SigilCategory::SecurityGuard,
};
let style = bold_alarm.style();
assert_eq!(style.fg, Some(Color::Red));
assert!(style.add_modifier.contains(Modifier::BOLD));
assert!(!style.add_modifier.contains(Modifier::DIM));
}
}