claude_code_statusline_core/modules/
mod.rs1use 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
28pub trait ModuleConfig: Any + Send + Sync {
33 #[allow(dead_code)]
35 fn as_any(&self) -> &dyn Any;
36
37 #[allow(dead_code)]
39 fn format(&self) -> &str {
40 ""
41 }
42
43 #[allow(dead_code)]
45 fn style(&self) -> &str {
46 ""
47 }
48}
49
50pub struct EmptyConfig;
54
55impl ModuleConfig for EmptyConfig {
56 fn as_any(&self) -> &dyn Any {
57 self
58 }
59}
60
61pub trait Module: Send + Sync {
73 #[allow(dead_code)]
75 fn name(&self) -> &str;
76
77 fn should_display(&self, context: &Context, config: &dyn ModuleConfig) -> bool;
79
80 fn render(&self, context: &Context, config: &dyn ModuleConfig) -> String;
82}
83
84pub 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
97pub fn handle_module(name: &str, context: &Context) -> Option<Box<dyn Module>> {
112 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
122pub 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 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 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 #[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}