Skip to main content

boxmux_lib/components/
status_indicator.rs

1use crate::model::muxbox::MuxBox;
2use crate::pty_manager::PtyManager;
3
4/// Status indicator component for rendering process and execution status indicators
5pub struct StatusIndicator {
6    pub indicator_type: StatusType,
7    pub process_info: Option<String>,
8    pub custom_text: Option<String>,
9}
10
11/// Types of status indicators available
12#[derive(Debug, Clone, PartialEq)]
13pub enum StatusType {
14    /// Normal PTY process (⚡)
15    PtyNormal,
16    /// Dead PTY process (💀)  
17    PtyDead,
18    /// PTY process in error state (⚠️)
19    PtyError,
20    /// Script execution in progress
21    ScriptRunning,
22    /// Script execution completed
23    ScriptCompleted,
24    /// Script execution failed
25    ScriptFailed,
26    /// Custom status with user-defined indicator
27    Custom(String),
28    /// No status indicator
29    None,
30}
31
32impl StatusType {
33    /// Get the character/emoji for this status type
34    pub fn get_indicator(&self) -> &str {
35        match self {
36            StatusType::PtyNormal => "⚡",
37            StatusType::PtyDead => "💀",
38            StatusType::PtyError => "⚠️",
39            StatusType::ScriptRunning => "▶️",
40            StatusType::ScriptCompleted => "✅",
41            StatusType::ScriptFailed => "❌",
42            StatusType::Custom(indicator) => indicator,
43            StatusType::None => "",
44        }
45    }
46
47    /// Check if this status type should be displayed
48    pub fn is_visible(&self) -> bool {
49        !matches!(self, StatusType::None)
50    }
51}
52
53impl StatusIndicator {
54    /// Create status indicator from MuxBox and PTY manager state
55    pub fn from_muxbox(muxbox: &MuxBox, pty_manager: Option<&PtyManager>) -> Self {
56        if muxbox.execution_mode.is_pty() {
57            let indicator_type = if let Some(pty_manager) = pty_manager {
58                if pty_manager.is_pty_dead(&muxbox.id) {
59                    StatusType::PtyDead
60                } else if pty_manager.is_pty_in_error_state(&muxbox.id) {
61                    StatusType::PtyError
62                } else {
63                    StatusType::PtyNormal
64                }
65            } else {
66                StatusType::PtyNormal
67            };
68
69            let process_info = pty_manager.and_then(|pm| pm.get_process_status_summary(&muxbox.id));
70
71            Self {
72                indicator_type,
73                process_info,
74                custom_text: None,
75            }
76        } else {
77            Self {
78                indicator_type: StatusType::None,
79                process_info: None,
80                custom_text: None,
81            }
82        }
83    }
84
85    /// Create a custom status indicator
86    pub fn new_custom(indicator: String, text: Option<String>) -> Self {
87        Self {
88            indicator_type: StatusType::Custom(indicator),
89            process_info: None,
90            custom_text: text,
91        }
92    }
93
94    /// Create a specific status indicator type
95    pub fn new(status_type: StatusType) -> Self {
96        Self {
97            indicator_type: status_type,
98            process_info: None,
99            custom_text: None,
100        }
101    }
102
103    /// Set process information text
104    pub fn with_process_info(mut self, info: String) -> Self {
105        self.process_info = Some(info);
106        self
107    }
108
109    /// Set custom text
110    pub fn with_custom_text(mut self, text: String) -> Self {
111        self.custom_text = Some(text);
112        self
113    }
114
115    /// Render the status indicator as a string for title integration
116    pub fn render_for_title(&self, original_title: Option<&str>) -> Option<String> {
117        if !self.indicator_type.is_visible() {
118            return original_title.map(|s| s.to_string());
119        }
120
121        let mut title_parts = vec![self.indicator_type.get_indicator().to_string()];
122
123        // Add process info if available
124        if let Some(ref info) = self.process_info {
125            title_parts.push(format!("[{}]", info));
126        }
127
128        // Add custom text if available
129        if let Some(ref text) = self.custom_text {
130            title_parts.push(text.clone());
131        }
132
133        // Add original title if it exists
134        if let Some(title) = original_title {
135            title_parts.push(title.to_string());
136        } else if matches!(
137            self.indicator_type,
138            StatusType::PtyNormal | StatusType::PtyDead | StatusType::PtyError
139        ) {
140            title_parts.push("PTY".to_string());
141        }
142
143        Some(title_parts.join(" "))
144    }
145
146    /// Get just the indicator character/emoji
147    pub fn get_indicator(&self) -> &str {
148        self.indicator_type.get_indicator()
149    }
150
151    /// Check if the status indicator should be displayed
152    pub fn is_visible(&self) -> bool {
153        self.indicator_type.is_visible()
154    }
155
156    /// Get the full status text (indicator + process info + custom text)
157    pub fn get_full_status(&self) -> String {
158        if !self.is_visible() {
159            return String::new();
160        }
161
162        let mut parts = vec![self.get_indicator().to_string()];
163
164        if let Some(ref info) = self.process_info {
165            parts.push(format!("[{}]", info));
166        }
167
168        if let Some(ref text) = self.custom_text {
169            parts.push(text.clone());
170        }
171
172        parts.join(" ")
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179    use crate::model::common::ExecutionMode;
180
181    #[test]
182    fn test_status_type_indicators() {
183        assert_eq!(StatusType::PtyNormal.get_indicator(), "⚡");
184        assert_eq!(StatusType::PtyDead.get_indicator(), "💀");
185        assert_eq!(StatusType::PtyError.get_indicator(), "⚠️");
186        assert_eq!(StatusType::ScriptRunning.get_indicator(), "▶️");
187        assert_eq!(StatusType::ScriptCompleted.get_indicator(), "✅");
188        assert_eq!(StatusType::ScriptFailed.get_indicator(), "❌");
189        assert_eq!(StatusType::Custom("🚀".to_string()).get_indicator(), "🚀");
190        assert_eq!(StatusType::None.get_indicator(), "");
191    }
192
193    #[test]
194    fn test_status_type_visibility() {
195        assert!(StatusType::PtyNormal.is_visible());
196        assert!(StatusType::PtyDead.is_visible());
197        assert!(StatusType::PtyError.is_visible());
198        assert!(StatusType::Custom("⭐".to_string()).is_visible());
199        assert!(!StatusType::None.is_visible());
200    }
201
202    #[test]
203    fn test_custom_status_indicator() {
204        let indicator =
205            StatusIndicator::new_custom("🎯".to_string(), Some("Custom Status".to_string()));
206        assert_eq!(indicator.get_indicator(), "🎯");
207        assert_eq!(indicator.custom_text, Some("Custom Status".to_string()));
208        assert!(indicator.is_visible());
209    }
210
211    #[test]
212    fn test_status_indicator_with_process_info() {
213        let indicator =
214            StatusIndicator::new(StatusType::PtyNormal).with_process_info("bash:1234".to_string());
215
216        assert_eq!(indicator.get_indicator(), "⚡");
217        assert_eq!(indicator.process_info, Some("bash:1234".to_string()));
218    }
219
220    #[test]
221    fn test_render_for_title() {
222        let indicator =
223            StatusIndicator::new(StatusType::PtyNormal).with_process_info("bash:1234".to_string());
224
225        let result = indicator.render_for_title(Some("My Terminal"));
226        assert_eq!(result, Some("⚡ [bash:1234] My Terminal".to_string()));
227    }
228
229    #[test]
230    fn test_render_for_title_no_original() {
231        let indicator =
232            StatusIndicator::new(StatusType::PtyNormal).with_process_info("bash:1234".to_string());
233
234        let result = indicator.render_for_title(None);
235        assert_eq!(result, Some("⚡ [bash:1234] PTY".to_string()));
236    }
237
238    #[test]
239    fn test_render_for_title_none_status() {
240        let indicator = StatusIndicator::new(StatusType::None);
241        let result = indicator.render_for_title(Some("Regular Title"));
242        assert_eq!(result, Some("Regular Title".to_string()));
243    }
244
245    #[test]
246    fn test_get_full_status() {
247        let indicator = StatusIndicator::new(StatusType::PtyError)
248            .with_process_info("failed_cmd".to_string())
249            .with_custom_text("Connection Lost".to_string());
250
251        let result = indicator.get_full_status();
252        assert_eq!(result, "⚠️ [failed_cmd] Connection Lost");
253    }
254
255    #[test]
256    fn test_from_muxbox_pty() {
257        let mut muxbox = MuxBox::default();
258        muxbox.id = "test_pty".to_string();
259        muxbox.execution_mode = ExecutionMode::Pty;
260
261        let indicator = StatusIndicator::from_muxbox(&muxbox, None);
262        assert!(matches!(indicator.indicator_type, StatusType::PtyNormal));
263        assert_eq!(indicator.get_indicator(), "⚡");
264    }
265
266    #[test]
267    fn test_from_muxbox_non_pty() {
268        let mut muxbox = MuxBox::default();
269        muxbox.execution_mode = ExecutionMode::Thread;
270
271        let indicator = StatusIndicator::from_muxbox(&muxbox, None);
272        assert!(matches!(indicator.indicator_type, StatusType::None));
273        assert!(!indicator.is_visible());
274    }
275}