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 there's no v6 traffic to evaluate (host has no v6, or no Connected tunnel covers ::/0), 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));
}
}