use crossterm::terminal;
use serde::{Deserialize, Serialize};
use std::io::{self, Write};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use crate::scanner::ScannerStats;
use crate::shield::Shield;
pub struct CliShield {
shield: Arc<Shield>,
format: DisplayFormat,
last_update: Instant,
update_interval: Duration,
enabled: AtomicBool,
in_command: AtomicBool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DisplayFormat {
Compact,
StatusBar,
Inline,
Minimal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShieldStatus {
pub active: bool,
pub threats_blocked: u64,
pub uptime_seconds: u64,
pub last_threat: Option<String>,
pub scanner_stats: ScannerStats,
}
impl CliShield {
pub fn new(shield: Arc<Shield>, format: DisplayFormat) -> Self {
Self {
shield,
format,
last_update: Instant::now(),
update_interval: Duration::from_millis(1000),
enabled: AtomicBool::new(true),
in_command: AtomicBool::new(false),
}
}
pub fn status(&self) -> ShieldStatus {
let stats = self.shield.stats();
let uptime = self.shield.start_time().elapsed().as_secs();
ShieldStatus {
active: self.shield.is_active(),
threats_blocked: stats.threats_blocked,
uptime_seconds: uptime,
last_threat: self.shield.last_threat_type(),
scanner_stats: self.shield.scanner_stats(),
}
}
pub fn render(&self) -> String {
if !self.enabled.load(Ordering::Relaxed) {
return String::new();
}
let status = self.status();
match self.format {
DisplayFormat::Compact => self.render_compact(&status),
DisplayFormat::StatusBar => self.render_status_bar(&status),
DisplayFormat::Inline => self.render_inline(&status),
DisplayFormat::Minimal => self.render_minimal(&status),
}
}
fn render_compact(&self, status: &ShieldStatus) -> String {
let shield_icon = if status.active { "🛡️" } else { "🔓" };
let status_icon = if status.active { "✓" } else { "✗" };
let threat_count = status.threats_blocked;
let uptime = format_duration(status.uptime_seconds);
if threat_count > 0 {
format!(
"[{shield_icon} KindlyGuard: {status_icon} Protected | ⚡ {threat_count} blocked | ⏱ {uptime}]"
)
} else {
format!("[{shield_icon} KindlyGuard: {status_icon} Protected | ⏱ {uptime}]")
}
}
fn render_status_bar(&self, status: &ShieldStatus) -> String {
let shield_icon = if status.active { "🛡️" } else { "🔓" };
let threats = status.threats_blocked;
if let Some(last_threat) = &status.last_threat {
format!("{shield_icon} {last_threat} ⚡{threats}")
} else {
format!("{shield_icon} Safe ⚡{threats}")
}
}
fn render_inline(&self, status: &ShieldStatus) -> String {
let shield_icon = if status.active { "🛡️" } else { "🔓" };
let status_text = if status.active { "ON" } else { "OFF" };
format!("\r{shield_icon} {status_text}")
}
fn render_minimal(&self, status: &ShieldStatus) -> String {
if status.active {
if status.threats_blocked > 0 {
format!("🛡️⚡{}", status.threats_blocked)
} else {
"🛡️".to_string()
}
} else {
"🔓".to_string()
}
}
pub fn update(&mut self) -> io::Result<()> {
if !self.should_update() {
return Ok(());
}
let display = self.render();
if !display.is_empty() {
self.write_display(&display)?;
}
self.last_update = Instant::now();
Ok(())
}
fn should_update(&self) -> bool {
self.enabled.load(Ordering::Relaxed)
&& self.last_update.elapsed() >= self.update_interval
&& !self.in_command.load(Ordering::Relaxed)
}
fn write_display(&self, display: &str) -> io::Result<()> {
let mut stdout = io::stdout();
if self.format == DisplayFormat::Inline {
write!(stdout, "\x1b7")?; write!(
stdout,
"\x1b[1;{}H",
terminal::size()?.0.saturating_sub(display.len() as u16)
)?;
write!(stdout, "{display}")?;
write!(stdout, "\x1b8")?; stdout.flush()?;
} else {
write!(stdout, "{display}")?;
stdout.flush()?;
}
Ok(())
}
pub fn set_enabled(&self, enabled: bool) {
self.enabled.store(enabled, Ordering::Relaxed);
}
pub fn set_in_command(&self, in_command: bool) {
self.in_command.store(in_command, Ordering::Relaxed);
}
pub fn shell_init_script(shell: &str) -> String {
match shell {
"bash" => include_str!("../../scripts/shell-init.bash").to_string(),
"zsh" => include_str!("../../scripts/shell-init.zsh").to_string(),
"fish" => include_str!("../../scripts/shell-init.fish").to_string(),
_ => String::new(),
}
}
}
fn format_duration(seconds: u64) -> String {
let hours = seconds / 3600;
let minutes = (seconds % 3600) / 60;
if hours > 0 {
format!("{hours}h{minutes}m")
} else if minutes > 0 {
format!("{minutes}m")
} else {
format!("{seconds}s")
}
}
pub mod hooks {
pub const fn pre_command_hook() -> &'static str {
r"
if command -v kindly-guard >/dev/null 2>&1; then
kindly-guard shield pre-command
fi
"
}
pub const fn post_command_hook() -> &'static str {
r"
if command -v kindly-guard >/dev/null 2>&1; then
kindly-guard shield post-command
fi
"
}
pub const fn prompt_command() -> &'static str {
r#"
if command -v kindly-guard >/dev/null 2>&1; then
KINDLY_GUARD_STATUS="$(kindly-guard shield status --format=compact)"
if [ -n "$KINDLY_GUARD_STATUS" ]; then
echo -e "\033[1;34m$KINDLY_GUARD_STATUS\033[0m"
fi
fi
"#
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_display_formats() {
let shield = Arc::new(Shield::new());
let cli_shield = CliShield::new(shield.clone(), DisplayFormat::Compact);
let display = cli_shield.render();
assert!(display.contains("KindlyGuard"));
assert!(display.contains("Protected"));
}
#[test]
fn test_status_serialization() {
let shield = Arc::new(Shield::new());
let cli_shield = CliShield::new(shield, DisplayFormat::Minimal);
let status = cli_shield.status();
let json =
serde_json::to_string(&status).expect("Shield status should always be serializable");
assert!(json.contains("active"));
assert!(json.contains("threats_blocked"));
}
}