Skip to main content

claude_code_statusline_core/modules/
mod.rs

1//! Status line module system
2//!
3//! This module provides the infrastructure for modular status line components.
4//! Each module implements the `Module` trait and can be dynamically loaded
5//! based on the format string configuration.
6//!
7//! # Architecture
8//!
9//! - `Module` trait: Core interface for all status components
10//! - `ModuleConfig` trait: Configuration interface for modules
11//! - Factory pattern: Dynamic module creation via `handle_module`
12//! - Timeout protection: Each module execution is time-bounded
13//!
14//! # Available Modules
15//!
16//! - `directory`: Current directory display
17//! - `claude_model`: Claude model information
18//! - `git_branch`: Current git branch
19//! - `git_status`: Git repository status
20
21use crate::debug::DebugLogger;
22use crate::error::CoreError;
23use crate::timeout::run_with_timeout;
24use crate::types::context::Context;
25use std::any::Any;
26use std::time::Duration;
27
28/// Trait for module-specific configuration
29///
30/// Each module can have its own configuration section in the TOML file.
31/// This trait provides a common interface for accessing module configurations.
32pub trait ModuleConfig: Any + Send + Sync {
33    /// Allow downcasting to concrete config types
34    #[allow(dead_code)]
35    fn as_any(&self) -> &dyn Any;
36
37    /// Get the format string for this module
38    #[allow(dead_code)]
39    fn format(&self) -> &str {
40        ""
41    }
42
43    /// Get the style string for this module
44    #[allow(dead_code)]
45    fn style(&self) -> &str {
46        ""
47    }
48}
49
50/// Default implementation for cases where no config is provided
51///
52/// Used as a fallback when a module doesn't have specific configuration.
53pub struct EmptyConfig;
54
55impl ModuleConfig for EmptyConfig {
56    fn as_any(&self) -> &dyn Any {
57        self
58    }
59}
60
61/// Trait that all status line modules must implement
62///
63/// This is the core interface for creating status line components.
64/// Each module determines when to display itself and how to render
65/// its output based on the current context.
66///
67/// # Implementation Notes
68///
69/// - Modules should be stateless and thread-safe
70/// - Heavy operations should be cached in Context
71/// - Rendering should complete quickly to avoid timeouts
72pub trait Module: Send + Sync {
73    /// Returns the name of the module
74    #[allow(dead_code)]
75    fn name(&self) -> &str;
76
77    /// Determines if this module should be displayed
78    fn should_display(&self, context: &Context, config: &dyn ModuleConfig) -> bool;
79
80    /// Renders the module's output as a string
81    fn render(&self, context: &Context, config: &dyn ModuleConfig) -> String;
82}
83
84// Re-export module implementations
85pub mod claude_model;
86pub mod directory;
87#[cfg(feature = "git")]
88pub mod git_branch;
89#[cfg(feature = "git")]
90pub mod git_status;
91pub mod registry;
92
93pub use claude_model::ClaudeModelModule;
94pub use directory::DirectoryModule;
95pub use registry::{ModuleFactory, Registry};
96
97/// Central module dispatcher - creates module instances based on name
98///
99/// Implements the Factory pattern for dynamic module creation.
100/// Returns a boxed module instance if the name matches a known module.
101///
102/// # Arguments
103///
104/// * `name` - Module name from the format string (e.g., "directory")
105/// * `context` - Current execution context
106///
107/// # Returns
108///
109/// * `Some(Box<dyn Module>)` - Module instance if name is recognized
110/// * `None` - If the module name is unknown
111pub fn handle_module(name: &str, context: &Context) -> Option<Box<dyn Module>> {
112    // Gradual migration: delegate to Registry with built-in factories
113    let registry = Registry::with_defaults();
114    registry.create(name, context)
115}
116
117fn module_config_for<'a>(name: &str, context: &'a Context) -> Option<&'a dyn ModuleConfig> {
118    let registry = Registry::with_defaults();
119    registry.config(name, context)
120}
121
122/// Renders a module with timeout protection
123///
124/// Executes both `should_display` and `render` methods with a timeout
125/// based on the configuration's `command_timeout` value. This ensures
126/// that slow modules don't block the status line generation.
127///
128/// # Arguments
129///
130/// * `name` - Module name to render
131/// * `context` - Current execution context
132/// * `logger` - Debug logger for error reporting
133///
134/// # Returns
135///
136/// * `Some(String)` - Rendered module output on success
137/// * `None` - On timeout, error, or when module shouldn't display
138///
139/// # Timeout Behavior
140///
141/// If a module exceeds the configured timeout (default 500ms),
142/// it will be skipped and an error logged to stderr.
143pub fn render_module_with_timeout(
144    name: &str,
145    context: &Context,
146    logger: &DebugLogger,
147) -> Option<String> {
148    let timeout_ms = context.config.command_timeout;
149    let timeout = Duration::from_millis(timeout_ms);
150
151    // should_display with timeout (fresh module instance)
152    match run_with_timeout(timeout, {
153        let ctx1 = context.clone();
154        let name1 = name.to_string();
155        move || {
156            let module = handle_module(&name1, &ctx1)
157                .ok_or_else(|| CoreError::UnknownModule(name1.clone()))?;
158            let cfg = module_config_for(&name1, &ctx1)
159                .ok_or_else(|| CoreError::MissingConfig(name1.clone()))?;
160            Ok(module.should_display(&ctx1, cfg))
161        }
162    }) {
163        Ok(Some(true)) => {}
164        Ok(Some(false)) => return None,
165        Ok(None) => {
166            logger.log_stderr(&format!(
167                "Module '{name}' timed out in should_display after {timeout_ms}ms"
168            ));
169            return None;
170        }
171        Err(e) => {
172            logger.log_stderr(&format!("Module '{name}' error in should_display: {e}"));
173            return None;
174        }
175    }
176
177    // render with timeout (fresh module instance)
178    match run_with_timeout(timeout, {
179        let ctx2 = context.clone();
180        let name2 = name.to_string();
181        move || {
182            let module = handle_module(&name2, &ctx2)
183                .ok_or_else(|| CoreError::UnknownModule(name2.clone()))?;
184            let cfg = module_config_for(&name2, &ctx2)
185                .ok_or_else(|| CoreError::MissingConfig(name2.clone()))?;
186            Ok(module.render(&ctx2, cfg))
187        }
188    }) {
189        Ok(Some(s)) => Some(s),
190        Ok(None) => {
191            logger.log_stderr(&format!(
192                "Module '{name}' timed out in render after {timeout_ms}ms"
193            ));
194            None
195        }
196        Err(e) => {
197            logger.log_stderr(&format!("Module '{name}' error in render: {e}"));
198            None
199        }
200    }
201}
202
203#[cfg(test)]
204mod timeout_tests {
205    use super::*;
206    use crate::config::Config;
207    use crate::types::claude::{ClaudeInput, ModelInfo, WorkspaceInfo};
208
209    #[allow(dead_code)]
210    struct SleepyModule;
211
212    impl SleepyModule {
213        #[allow(dead_code)]
214        fn from_context(_context: &Context) -> Self {
215            Self
216        }
217    }
218
219    impl Module for SleepyModule {
220        fn name(&self) -> &str {
221            "sleepy"
222        }
223        fn should_display(&self, _context: &Context, _cfg: &dyn ModuleConfig) -> bool {
224            true
225        }
226        fn render(&self, _context: &Context, _cfg: &dyn ModuleConfig) -> String {
227            std::thread::sleep(std::time::Duration::from_millis(200));
228            "[SLEEP]".to_string()
229        }
230    }
231
232    // Extend dispatcher only in tests
233    #[allow(dead_code)]
234    pub fn handle_module(name: &str, context: &Context) -> Option<Box<dyn Module>> {
235        match name {
236            "sleepy" => Some(Box::new(SleepyModule::from_context(context))),
237            _ => super::handle_module(name, context),
238        }
239    }
240
241    fn make_context(cwd: &str, timeout_ms: u64) -> Context {
242        let input = ClaudeInput {
243            hook_event_name: None,
244            session_id: "test-session".to_string(),
245            transcript_path: None,
246            cwd: cwd.to_string(),
247            model: ModelInfo {
248                id: "claude-opus".into(),
249                display_name: "Opus".into(),
250            },
251            workspace: Some(WorkspaceInfo {
252                current_dir: cwd.to_string(),
253                project_dir: Some(cwd.to_string()),
254            }),
255            version: Some("1.0.0".into()),
256            output_style: None,
257        };
258        let cfg = Config {
259            command_timeout: timeout_ms,
260            ..Default::default()
261        };
262        Context::new(input, cfg)
263    }
264
265    #[test]
266    fn sleepy_module_times_out_and_is_omitted() {
267        let logger = DebugLogger::new(true);
268        let ctx = make_context("/tmp", 50);
269        let out = render_module_with_timeout("sleepy", &ctx, &logger);
270        assert!(out.is_none());
271    }
272}