kindly_guard_server/shield/
cli.rs1use crossterm::terminal;
17use serde::{Deserialize, Serialize};
18use std::io::{self, Write};
19use std::sync::atomic::{AtomicBool, Ordering};
20use std::sync::Arc;
21use std::time::{Duration, Instant};
22
23use crate::scanner::ScannerStats;
24use crate::shield::Shield;
25
26pub struct CliShield {
28 shield: Arc<Shield>,
29 format: DisplayFormat,
30 last_update: Instant,
31 update_interval: Duration,
32 enabled: AtomicBool,
33 in_command: AtomicBool,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum DisplayFormat {
40 Compact,
42 StatusBar,
44 Inline,
46 Minimal,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct ShieldStatus {
53 pub active: bool,
54 pub threats_blocked: u64,
55 pub uptime_seconds: u64,
56 pub last_threat: Option<String>,
57 pub scanner_stats: ScannerStats,
58}
59
60impl CliShield {
61 pub fn new(shield: Arc<Shield>, format: DisplayFormat) -> Self {
62 Self {
63 shield,
64 format,
65 last_update: Instant::now(),
66 update_interval: Duration::from_millis(1000),
67 enabled: AtomicBool::new(true),
68 in_command: AtomicBool::new(false),
69 }
70 }
71
72 pub fn status(&self) -> ShieldStatus {
74 let stats = self.shield.stats();
75 let uptime = self.shield.start_time().elapsed().as_secs();
76
77 ShieldStatus {
78 active: self.shield.is_active(),
79 threats_blocked: stats.threats_blocked,
80 uptime_seconds: uptime,
81 last_threat: self.shield.last_threat_type(),
82 scanner_stats: self.shield.scanner_stats(),
83 }
84 }
85
86 pub fn render(&self) -> String {
88 if !self.enabled.load(Ordering::Relaxed) {
89 return String::new();
90 }
91
92 let status = self.status();
93
94 match self.format {
95 DisplayFormat::Compact => self.render_compact(&status),
96 DisplayFormat::StatusBar => self.render_status_bar(&status),
97 DisplayFormat::Inline => self.render_inline(&status),
98 DisplayFormat::Minimal => self.render_minimal(&status),
99 }
100 }
101
102 fn render_compact(&self, status: &ShieldStatus) -> String {
104 let shield_icon = if status.active { "🛡️" } else { "🔓" };
105 let status_icon = if status.active { "✓" } else { "✗" };
106 let threat_count = status.threats_blocked;
107 let uptime = format_duration(status.uptime_seconds);
108
109 if threat_count > 0 {
110 format!(
111 "[{shield_icon} KindlyGuard: {status_icon} Protected | ⚡ {threat_count} blocked | ⏱ {uptime}]"
112 )
113 } else {
114 format!("[{shield_icon} KindlyGuard: {status_icon} Protected | ⏱ {uptime}]")
115 }
116 }
117
118 fn render_status_bar(&self, status: &ShieldStatus) -> String {
120 let shield_icon = if status.active { "🛡️" } else { "🔓" };
121 let threats = status.threats_blocked;
122
123 if let Some(last_threat) = &status.last_threat {
124 format!("{shield_icon} {last_threat} ⚡{threats}")
125 } else {
126 format!("{shield_icon} Safe ⚡{threats}")
127 }
128 }
129
130 fn render_inline(&self, status: &ShieldStatus) -> String {
132 let shield_icon = if status.active { "🛡️" } else { "🔓" };
133 let status_text = if status.active { "ON" } else { "OFF" };
134
135 format!("\r{shield_icon} {status_text}")
136 }
137
138 fn render_minimal(&self, status: &ShieldStatus) -> String {
140 if status.active {
141 if status.threats_blocked > 0 {
142 format!("🛡️⚡{}", status.threats_blocked)
143 } else {
144 "🛡️".to_string()
145 }
146 } else {
147 "🔓".to_string()
148 }
149 }
150
151 pub fn update(&mut self) -> io::Result<()> {
153 if !self.should_update() {
154 return Ok(());
155 }
156
157 let display = self.render();
158 if !display.is_empty() {
159 self.write_display(&display)?;
160 }
161
162 self.last_update = Instant::now();
163 Ok(())
164 }
165
166 fn should_update(&self) -> bool {
168 self.enabled.load(Ordering::Relaxed)
169 && self.last_update.elapsed() >= self.update_interval
170 && !self.in_command.load(Ordering::Relaxed)
171 }
172
173 fn write_display(&self, display: &str) -> io::Result<()> {
175 let mut stdout = io::stdout();
176
177 if self.format == DisplayFormat::Inline {
178 write!(stdout, "\x1b7")?; write!(
181 stdout,
182 "\x1b[1;{}H",
183 terminal::size()?.0.saturating_sub(display.len() as u16)
184 )?;
185 write!(stdout, "{display}")?;
186 write!(stdout, "\x1b8")?; stdout.flush()?;
188 } else {
189 write!(stdout, "{display}")?;
191 stdout.flush()?;
192 }
193
194 Ok(())
195 }
196
197 pub fn set_enabled(&self, enabled: bool) {
199 self.enabled.store(enabled, Ordering::Relaxed);
200 }
201
202 pub fn set_in_command(&self, in_command: bool) {
204 self.in_command.store(in_command, Ordering::Relaxed);
205 }
206
207 pub fn shell_init_script(shell: &str) -> String {
209 match shell {
210 "bash" => include_str!("../../scripts/shell-init.bash").to_string(),
211 "zsh" => include_str!("../../scripts/shell-init.zsh").to_string(),
212 "fish" => include_str!("../../scripts/shell-init.fish").to_string(),
213 _ => String::new(),
214 }
215 }
216}
217
218fn format_duration(seconds: u64) -> String {
220 let hours = seconds / 3600;
221 let minutes = (seconds % 3600) / 60;
222
223 if hours > 0 {
224 format!("{hours}h{minutes}m")
225 } else if minutes > 0 {
226 format!("{minutes}m")
227 } else {
228 format!("{seconds}s")
229 }
230}
231
232pub mod hooks {
234
235 pub const fn pre_command_hook() -> &'static str {
237 r"
238 if command -v kindly-guard >/dev/null 2>&1; then
239 kindly-guard shield pre-command
240 fi
241 "
242 }
243
244 pub const fn post_command_hook() -> &'static str {
246 r"
247 if command -v kindly-guard >/dev/null 2>&1; then
248 kindly-guard shield post-command
249 fi
250 "
251 }
252
253 pub const fn prompt_command() -> &'static str {
255 r#"
256 if command -v kindly-guard >/dev/null 2>&1; then
257 KINDLY_GUARD_STATUS="$(kindly-guard shield status --format=compact)"
258 if [ -n "$KINDLY_GUARD_STATUS" ]; then
259 echo -e "\033[1;34m$KINDLY_GUARD_STATUS\033[0m"
260 fi
261 fi
262 "#
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 #[test]
271 fn test_display_formats() {
272 let shield = Arc::new(Shield::new());
273 let cli_shield = CliShield::new(shield.clone(), DisplayFormat::Compact);
274
275 let display = cli_shield.render();
276 assert!(display.contains("KindlyGuard"));
277 assert!(display.contains("Protected"));
278 }
279
280 #[test]
281 fn test_status_serialization() {
282 let shield = Arc::new(Shield::new());
283 let cli_shield = CliShield::new(shield, DisplayFormat::Minimal);
284
285 let status = cli_shield.status();
286 let json =
287 serde_json::to_string(&status).expect("Shield status should always be serializable");
288 assert!(json.contains("active"));
289 assert!(json.contains("threats_blocked"));
290 }
291}