stuckbar/
lib.rs

1//! # stuckbar
2//!
3//! A CLI tool for restarting Windows Explorer when the taskbar gets stuck.
4//!
5//! This crate provides functionality to kill, start, and restart the Windows Explorer
6//! process, which is useful when the Windows taskbar becomes unresponsive.
7//!
8//! ## Features
9//!
10//! - `mcp` - Enable Model Context Protocol (MCP) server support for AI agent integration
11//!
12//! ## Platform Support
13//!
14//! This tool is Windows-only. Running on other platforms will result in an error.
15
16use colored::Colorize;
17use std::process::Command;
18
19/// Delay in milliseconds before starting explorer.exe after termination
20pub const RESTART_DELAY_MS: u64 = 500;
21
22/// Result of a process operation
23#[derive(Debug, PartialEq, Clone)]
24pub struct ProcessResult {
25    pub success: bool,
26    pub message: String,
27}
28
29impl ProcessResult {
30    pub fn success(message: impl Into<String>) -> Self {
31        Self {
32            success: true,
33            message: message.into(),
34        }
35    }
36
37    pub fn failure(message: impl Into<String>) -> Self {
38        Self {
39            success: false,
40            message: message.into(),
41        }
42    }
43}
44
45/// Trait for abstracting process operations (enables testing)
46pub trait ProcessRunner {
47    fn kill_process(&self, process_name: &str) -> ProcessResult;
48    fn start_process(&self, process_name: &str) -> ProcessResult;
49    fn sleep_ms(&self, ms: u64);
50}
51
52/// Real implementation that interacts with the system
53pub struct SystemProcessRunner;
54
55impl ProcessRunner for SystemProcessRunner {
56    fn kill_process(&self, process_name: &str) -> ProcessResult {
57        let result = Command::new("taskkill")
58            .args(["/F", "/IM", process_name])
59            .output();
60
61        match result {
62            Ok(output) => {
63                if output.status.success() {
64                    ProcessResult::success(format!("Successfully terminated {}", process_name))
65                } else {
66                    let stderr = String::from_utf8_lossy(&output.stderr);
67                    ProcessResult::failure(format!(
68                        "Failed to terminate {}: {}",
69                        process_name, stderr
70                    ))
71                }
72            }
73            Err(e) => ProcessResult::failure(format!("Error executing taskkill: {}", e)),
74        }
75    }
76
77    fn start_process(&self, process_name: &str) -> ProcessResult {
78        let result = Command::new(process_name).spawn();
79
80        match result {
81            Ok(_) => ProcessResult::success(format!("Successfully started {}", process_name)),
82            Err(e) => ProcessResult::failure(format!("Error starting {}: {}", process_name, e)),
83        }
84    }
85
86    fn sleep_ms(&self, ms: u64) {
87        std::thread::sleep(std::time::Duration::from_millis(ms));
88    }
89}
90
91/// Explorer manager that handles explorer.exe operations
92pub struct ExplorerManager<R: ProcessRunner> {
93    pub runner: R,
94    pub restart_delay_ms: u64,
95}
96
97impl<R: ProcessRunner> ExplorerManager<R> {
98    pub fn new(runner: R) -> Self {
99        Self {
100            runner,
101            restart_delay_ms: RESTART_DELAY_MS,
102        }
103    }
104
105    pub fn with_restart_delay(mut self, delay_ms: u64) -> Self {
106        self.restart_delay_ms = delay_ms;
107        self
108    }
109
110    /// Kill explorer.exe process
111    pub fn kill(&self) -> bool {
112        println!("{}", "Terminating explorer.exe...".yellow());
113        let result = self.runner.kill_process("explorer.exe");
114
115        if result.success {
116            println!("{}", result.message.green());
117        } else {
118            eprintln!("{}", result.message.red());
119        }
120
121        result.success
122    }
123
124    /// Start explorer.exe process
125    pub fn start(&self) -> bool {
126        println!("{}", "Starting explorer.exe...".yellow());
127        let result = self.runner.start_process("explorer.exe");
128
129        if result.success {
130            println!("{}", result.message.green());
131        } else {
132            eprintln!("{}", result.message.red());
133        }
134
135        result.success
136    }
137
138    /// Restart explorer.exe (kill then start)
139    pub fn restart(&self) -> bool {
140        println!("{}", "Restarting explorer.exe...".cyan().bold());
141
142        if !self.kill() {
143            return false;
144        }
145
146        // Small delay to ensure explorer is fully terminated
147        self.runner.sleep_ms(self.restart_delay_ms);
148
149        if !self.start() {
150            return false;
151        }
152
153        println!("{}", "Explorer.exe restarted successfully!".green().bold());
154        true
155    }
156
157    /// Kill explorer.exe without printing (for MCP/programmatic use)
158    pub fn kill_silent(&self) -> ProcessResult {
159        self.runner.kill_process("explorer.exe")
160    }
161
162    /// Start explorer.exe without printing (for MCP/programmatic use)
163    pub fn start_silent(&self) -> ProcessResult {
164        self.runner.start_process("explorer.exe")
165    }
166
167    /// Restart explorer.exe without printing (for MCP/programmatic use)
168    pub fn restart_silent(&self) -> ProcessResult {
169        let kill_result = self.runner.kill_process("explorer.exe");
170        if !kill_result.success {
171            return kill_result;
172        }
173
174        self.runner.sleep_ms(self.restart_delay_ms);
175
176        let start_result = self.runner.start_process("explorer.exe");
177        if !start_result.success {
178            return start_result;
179        }
180
181        ProcessResult::success("Explorer.exe restarted successfully")
182    }
183}
184
185/// Check if the current platform is Windows
186pub fn is_windows() -> bool {
187    cfg!(target_os = "windows")
188}
189
190/// Check platform and return an error message if not Windows
191pub fn check_platform() -> Result<(), String> {
192    if !is_windows() {
193        Err(format!(
194            "stuckbar is a Windows-only tool.\n\
195            Current platform '{}' is not supported.\n\
196            This tool restarts explorer.exe which only exists on Windows.",
197            std::env::consts::OS
198        ))
199    } else {
200        Ok(())
201    }
202}
203
204#[cfg(feature = "mcp")]
205pub mod mcp;
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210    use std::cell::RefCell;
211
212    /// Mock process runner for testing
213    pub struct MockProcessRunner {
214        kill_results: RefCell<Vec<ProcessResult>>,
215        start_results: RefCell<Vec<ProcessResult>>,
216        sleep_calls: RefCell<Vec<u64>>,
217    }
218
219    impl MockProcessRunner {
220        pub fn new() -> Self {
221            Self {
222                kill_results: RefCell::new(Vec::new()),
223                start_results: RefCell::new(Vec::new()),
224                sleep_calls: RefCell::new(Vec::new()),
225            }
226        }
227
228        pub fn with_kill_result(self, result: ProcessResult) -> Self {
229            self.kill_results.borrow_mut().push(result);
230            self
231        }
232
233        pub fn with_start_result(self, result: ProcessResult) -> Self {
234            self.start_results.borrow_mut().push(result);
235            self
236        }
237
238        pub fn get_sleep_calls(&self) -> Vec<u64> {
239            self.sleep_calls.borrow().clone()
240        }
241    }
242
243    impl Default for MockProcessRunner {
244        fn default() -> Self {
245            Self::new()
246        }
247    }
248
249    impl ProcessRunner for MockProcessRunner {
250        fn kill_process(&self, _process_name: &str) -> ProcessResult {
251            self.kill_results
252                .borrow_mut()
253                .pop()
254                .unwrap_or_else(|| ProcessResult::failure("No mock result configured"))
255        }
256
257        fn start_process(&self, _process_name: &str) -> ProcessResult {
258            self.start_results
259                .borrow_mut()
260                .pop()
261                .unwrap_or_else(|| ProcessResult::failure("No mock result configured"))
262        }
263
264        fn sleep_ms(&self, ms: u64) {
265            self.sleep_calls.borrow_mut().push(ms);
266        }
267    }
268
269    // ProcessResult tests
270    #[test]
271    fn test_process_result_success() {
272        let result = ProcessResult::success("test message");
273        assert!(result.success);
274        assert_eq!(result.message, "test message");
275    }
276
277    #[test]
278    fn test_process_result_failure() {
279        let result = ProcessResult::failure("error message");
280        assert!(!result.success);
281        assert_eq!(result.message, "error message");
282    }
283
284    #[test]
285    fn test_process_result_clone() {
286        let result = ProcessResult::success("test");
287        let cloned = result.clone();
288        assert_eq!(result, cloned);
289    }
290
291    // ExplorerManager::kill tests
292    #[test]
293    fn test_kill_success() {
294        let runner = MockProcessRunner::new().with_kill_result(ProcessResult::success("Killed"));
295        let manager = ExplorerManager::new(runner);
296
297        assert!(manager.kill());
298    }
299
300    #[test]
301    fn test_kill_failure() {
302        let runner =
303            MockProcessRunner::new().with_kill_result(ProcessResult::failure("Failed to kill"));
304        let manager = ExplorerManager::new(runner);
305
306        assert!(!manager.kill());
307    }
308
309    // ExplorerManager::start tests
310    #[test]
311    fn test_start_success() {
312        let runner = MockProcessRunner::new().with_start_result(ProcessResult::success("Started"));
313        let manager = ExplorerManager::new(runner);
314
315        assert!(manager.start());
316    }
317
318    #[test]
319    fn test_start_failure() {
320        let runner =
321            MockProcessRunner::new().with_start_result(ProcessResult::failure("Failed to start"));
322        let manager = ExplorerManager::new(runner);
323
324        assert!(!manager.start());
325    }
326
327    // ExplorerManager::restart tests
328    #[test]
329    fn test_restart_success() {
330        let runner = MockProcessRunner::new()
331            .with_kill_result(ProcessResult::success("Killed"))
332            .with_start_result(ProcessResult::success("Started"));
333        let manager = ExplorerManager::new(runner).with_restart_delay(100);
334
335        assert!(manager.restart());
336    }
337
338    #[test]
339    fn test_restart_kill_fails() {
340        let runner =
341            MockProcessRunner::new().with_kill_result(ProcessResult::failure("Failed to kill"));
342        let manager = ExplorerManager::new(runner);
343
344        assert!(!manager.restart());
345    }
346
347    #[test]
348    fn test_restart_start_fails() {
349        let runner = MockProcessRunner::new()
350            .with_kill_result(ProcessResult::success("Killed"))
351            .with_start_result(ProcessResult::failure("Failed to start"));
352        let manager = ExplorerManager::new(runner);
353
354        assert!(!manager.restart());
355    }
356
357    #[test]
358    fn test_restart_sleeps_between_operations() {
359        let runner = MockProcessRunner::new()
360            .with_kill_result(ProcessResult::success("Killed"))
361            .with_start_result(ProcessResult::success("Started"));
362        let manager = ExplorerManager::new(runner).with_restart_delay(250);
363
364        manager.restart();
365
366        let sleep_calls = &manager.runner.get_sleep_calls();
367        assert_eq!(sleep_calls.len(), 1);
368        assert_eq!(sleep_calls[0], 250);
369    }
370
371    #[test]
372    fn test_restart_uses_default_delay() {
373        let runner = MockProcessRunner::new()
374            .with_kill_result(ProcessResult::success("Killed"))
375            .with_start_result(ProcessResult::success("Started"));
376        let manager = ExplorerManager::new(runner);
377
378        assert_eq!(manager.restart_delay_ms, RESTART_DELAY_MS);
379    }
380
381    // ExplorerManager builder pattern test
382    #[test]
383    fn test_explorer_manager_with_restart_delay() {
384        let runner = MockProcessRunner::new();
385        let manager = ExplorerManager::new(runner).with_restart_delay(1000);
386        assert_eq!(manager.restart_delay_ms, 1000);
387    }
388
389    // Silent method tests
390    #[test]
391    fn test_kill_silent_success() {
392        let runner = MockProcessRunner::new().with_kill_result(ProcessResult::success("Killed"));
393        let manager = ExplorerManager::new(runner);
394
395        let result = manager.kill_silent();
396        assert!(result.success);
397    }
398
399    #[test]
400    fn test_kill_silent_failure() {
401        let runner = MockProcessRunner::new().with_kill_result(ProcessResult::failure("Error"));
402        let manager = ExplorerManager::new(runner);
403
404        let result = manager.kill_silent();
405        assert!(!result.success);
406    }
407
408    #[test]
409    fn test_start_silent_success() {
410        let runner = MockProcessRunner::new().with_start_result(ProcessResult::success("Started"));
411        let manager = ExplorerManager::new(runner);
412
413        let result = manager.start_silent();
414        assert!(result.success);
415    }
416
417    #[test]
418    fn test_start_silent_failure() {
419        let runner = MockProcessRunner::new().with_start_result(ProcessResult::failure("Error"));
420        let manager = ExplorerManager::new(runner);
421
422        let result = manager.start_silent();
423        assert!(!result.success);
424    }
425
426    #[test]
427    fn test_restart_silent_success() {
428        let runner = MockProcessRunner::new()
429            .with_kill_result(ProcessResult::success("Killed"))
430            .with_start_result(ProcessResult::success("Started"));
431        let manager = ExplorerManager::new(runner);
432
433        let result = manager.restart_silent();
434        assert!(result.success);
435        assert_eq!(result.message, "Explorer.exe restarted successfully");
436    }
437
438    #[test]
439    fn test_restart_silent_kill_fails() {
440        let runner =
441            MockProcessRunner::new().with_kill_result(ProcessResult::failure("Kill failed"));
442        let manager = ExplorerManager::new(runner);
443
444        let result = manager.restart_silent();
445        assert!(!result.success);
446        assert_eq!(result.message, "Kill failed");
447    }
448
449    #[test]
450    fn test_restart_silent_start_fails() {
451        let runner = MockProcessRunner::new()
452            .with_kill_result(ProcessResult::success("Killed"))
453            .with_start_result(ProcessResult::failure("Start failed"));
454        let manager = ExplorerManager::new(runner);
455
456        let result = manager.restart_silent();
457        assert!(!result.success);
458        assert_eq!(result.message, "Start failed");
459    }
460
461    // Platform check tests
462    #[test]
463    fn test_is_windows() {
464        // This test verifies the function works, actual result depends on platform
465        let result = is_windows();
466        #[cfg(target_os = "windows")]
467        assert!(result);
468        #[cfg(not(target_os = "windows"))]
469        assert!(!result);
470    }
471
472    #[test]
473    fn test_check_platform() {
474        let result = check_platform();
475        #[cfg(target_os = "windows")]
476        assert!(result.is_ok());
477        #[cfg(not(target_os = "windows"))]
478        {
479            assert!(result.is_err());
480            let err = result.unwrap_err();
481            assert!(err.contains("Windows-only"));
482            assert!(err.contains(std::env::consts::OS));
483        }
484    }
485}