1use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
37use std::sync::Arc;
38
39static INTERRUPTED: AtomicBool = AtomicBool::new(false);
43
44pub fn is_interrupted() -> bool {
48 INTERRUPTED.load(Ordering::Relaxed)
49}
50
51pub fn reset_interrupted() {
55 INTERRUPTED.store(false, Ordering::SeqCst);
56}
57
58#[derive(Clone)]
60pub struct InterruptState {
61 interrupted: Arc<AtomicBool>,
63 completed: Arc<AtomicUsize>,
65 total: Arc<AtomicUsize>,
67}
68
69impl InterruptState {
70 pub fn new() -> Self {
72 Self {
73 interrupted: Arc::new(AtomicBool::new(false)),
74 completed: Arc::new(AtomicUsize::new(0)),
75 total: Arc::new(AtomicUsize::new(0)),
76 }
77 }
78
79 pub fn set_total(&self, total: usize) {
81 self.total.store(total, Ordering::SeqCst);
82 }
83
84 pub fn increment_completed(&self) {
86 self.completed.fetch_add(1, Ordering::SeqCst);
87 }
88
89 pub fn mark_interrupted(&self) {
91 self.interrupted.store(true, Ordering::SeqCst);
92 }
93
94 pub fn was_interrupted(&self) -> bool {
96 self.interrupted.load(Ordering::Relaxed) || is_interrupted()
97 }
98
99 pub fn files_completed(&self) -> usize {
101 self.completed.load(Ordering::Relaxed)
102 }
103
104 pub fn total_files(&self) -> usize {
106 self.total.load(Ordering::Relaxed)
107 }
108
109 pub fn check_interrupt(&self) -> bool {
113 if is_interrupted() {
114 self.mark_interrupted();
115 true
116 } else {
117 false
118 }
119 }
120}
121
122impl Default for InterruptState {
123 fn default() -> Self {
124 Self::new()
125 }
126}
127
128pub fn setup_signal_handler() -> Result<InterruptState, String> {
138 let state = InterruptState::new();
139
140 ctrlc::set_handler(move || {
141 if INTERRUPTED.load(Ordering::Relaxed) {
143 eprintln!("\nForce exit.");
144 std::process::exit(130); }
146
147 INTERRUPTED.store(true, Ordering::SeqCst);
148 eprintln!("\nInterrupted. Completing current operation...");
149 })
150 .map_err(|e| format!("Failed to set signal handler: {}", e))?;
151
152 Ok(state)
153}
154
155pub fn report_interrupt_status(state: &InterruptState) {
159 if state.was_interrupted() {
160 let completed = state.files_completed();
161 let total = state.total_files();
162 if total > 0 {
163 eprintln!(
164 "Interrupted: Analyzed {}/{} files ({:.1}%)",
165 completed,
166 total,
167 (completed as f64 / total as f64) * 100.0
168 );
169 } else {
170 eprintln!("Interrupted: Analyzed {} files", completed);
171 }
172 }
173}
174
175#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
177pub struct InterruptMetadata {
178 pub interrupted: bool,
180 pub completed: usize,
182 pub total: usize,
184 pub percent_complete: f64,
186}
187
188impl InterruptMetadata {
189 pub fn from_state(state: &InterruptState) -> Self {
191 let completed = state.files_completed();
192 let total = state.total_files();
193 let percent = if total > 0 {
194 (completed as f64 / total as f64) * 100.0
195 } else {
196 0.0
197 };
198
199 Self {
200 interrupted: state.was_interrupted(),
201 completed,
202 total,
203 percent_complete: percent,
204 }
205 }
206
207 pub fn complete(total: usize) -> Self {
209 Self {
210 interrupted: false,
211 completed: total,
212 total,
213 percent_complete: 100.0,
214 }
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221
222 #[test]
223 fn test_interrupt_state_new() {
224 let state = InterruptState::new();
225 assert!(!state.was_interrupted());
226 assert_eq!(state.files_completed(), 0);
227 assert_eq!(state.total_files(), 0);
228 }
229
230 #[test]
231 fn test_interrupt_state_tracking() {
232 let state = InterruptState::new();
233 state.set_total(100);
234 state.increment_completed();
235 state.increment_completed();
236
237 assert_eq!(state.files_completed(), 2);
238 assert_eq!(state.total_files(), 100);
239 }
240
241 #[test]
242 fn test_interrupt_state_mark_interrupted() {
243 let state = InterruptState::new();
244 assert!(!state.was_interrupted());
245
246 state.mark_interrupted();
247 assert!(state.was_interrupted());
248 }
249
250 #[test]
251 fn test_interrupt_metadata_from_state() {
252 let state = InterruptState::new();
253 state.set_total(100);
254 for _ in 0..50 {
255 state.increment_completed();
256 }
257
258 let metadata = InterruptMetadata::from_state(&state);
259 assert_eq!(metadata.completed, 50);
260 assert_eq!(metadata.total, 100);
261 assert!((metadata.percent_complete - 50.0).abs() < 0.01);
262 }
263
264 #[test]
265 fn test_interrupt_metadata_complete() {
266 let metadata = InterruptMetadata::complete(100);
267 assert!(!metadata.interrupted);
268 assert_eq!(metadata.completed, 100);
269 assert_eq!(metadata.total, 100);
270 assert_eq!(metadata.percent_complete, 100.0);
271 }
272
273 #[test]
274 fn test_reset_interrupted() {
275 reset_interrupted();
277 assert!(!is_interrupted());
278 }
279}