use crate::debug::DebugLogger;
use crate::error::CoreError;
use crate::timeout::run_with_timeout;
use crate::types::context::Context;
use std::any::Any;
use std::time::Duration;
pub trait ModuleConfig: Any + Send + Sync {
#[allow(dead_code)]
fn as_any(&self) -> &dyn Any;
#[allow(dead_code)]
fn format(&self) -> &str {
""
}
#[allow(dead_code)]
fn style(&self) -> &str {
""
}
}
pub struct EmptyConfig;
impl ModuleConfig for EmptyConfig {
fn as_any(&self) -> &dyn Any {
self
}
}
pub trait Module: Send + Sync {
#[allow(dead_code)]
fn name(&self) -> &str;
fn should_display(&self, context: &Context, config: &dyn ModuleConfig) -> bool;
fn render(&self, context: &Context, config: &dyn ModuleConfig) -> String;
}
pub mod claude_model;
pub mod directory;
#[cfg(feature = "git")]
pub mod git_branch;
#[cfg(feature = "git")]
pub mod git_status;
pub mod registry;
pub use claude_model::ClaudeModelModule;
pub use directory::DirectoryModule;
pub use registry::{ModuleFactory, Registry};
pub fn handle_module(name: &str, context: &Context) -> Option<Box<dyn Module>> {
let registry = Registry::with_defaults();
registry.create(name, context)
}
fn module_config_for<'a>(name: &str, context: &'a Context) -> Option<&'a dyn ModuleConfig> {
let registry = Registry::with_defaults();
registry.config(name, context)
}
pub fn render_module_with_timeout(
name: &str,
context: &Context,
logger: &DebugLogger,
) -> Option<String> {
let timeout_ms = context.config.command_timeout;
let timeout = Duration::from_millis(timeout_ms);
match run_with_timeout(timeout, {
let ctx1 = context.clone();
let name1 = name.to_string();
move || {
let module = handle_module(&name1, &ctx1)
.ok_or_else(|| CoreError::UnknownModule(name1.clone()))?;
let cfg = module_config_for(&name1, &ctx1)
.ok_or_else(|| CoreError::MissingConfig(name1.clone()))?;
Ok(module.should_display(&ctx1, cfg))
}
}) {
Ok(Some(true)) => {}
Ok(Some(false)) => return None,
Ok(None) => {
logger.log_stderr(&format!(
"Module '{name}' timed out in should_display after {timeout_ms}ms"
));
return None;
}
Err(e) => {
logger.log_stderr(&format!("Module '{name}' error in should_display: {e}"));
return None;
}
}
match run_with_timeout(timeout, {
let ctx2 = context.clone();
let name2 = name.to_string();
move || {
let module = handle_module(&name2, &ctx2)
.ok_or_else(|| CoreError::UnknownModule(name2.clone()))?;
let cfg = module_config_for(&name2, &ctx2)
.ok_or_else(|| CoreError::MissingConfig(name2.clone()))?;
Ok(module.render(&ctx2, cfg))
}
}) {
Ok(Some(s)) => Some(s),
Ok(None) => {
logger.log_stderr(&format!(
"Module '{name}' timed out in render after {timeout_ms}ms"
));
None
}
Err(e) => {
logger.log_stderr(&format!("Module '{name}' error in render: {e}"));
None
}
}
}
#[cfg(test)]
mod timeout_tests {
use super::*;
use crate::config::Config;
use crate::types::claude::{ClaudeInput, ModelInfo, WorkspaceInfo};
#[allow(dead_code)]
struct SleepyModule;
impl SleepyModule {
#[allow(dead_code)]
fn from_context(_context: &Context) -> Self {
Self
}
}
impl Module for SleepyModule {
fn name(&self) -> &str {
"sleepy"
}
fn should_display(&self, _context: &Context, _cfg: &dyn ModuleConfig) -> bool {
true
}
fn render(&self, _context: &Context, _cfg: &dyn ModuleConfig) -> String {
std::thread::sleep(std::time::Duration::from_millis(200));
"[SLEEP]".to_string()
}
}
#[allow(dead_code)]
pub fn handle_module(name: &str, context: &Context) -> Option<Box<dyn Module>> {
match name {
"sleepy" => Some(Box::new(SleepyModule::from_context(context))),
_ => super::handle_module(name, context),
}
}
fn make_context(cwd: &str, timeout_ms: u64) -> Context {
let input = ClaudeInput {
hook_event_name: None,
session_id: "test-session".to_string(),
transcript_path: None,
cwd: cwd.to_string(),
model: ModelInfo {
id: "claude-opus".into(),
display_name: "Opus".into(),
},
workspace: Some(WorkspaceInfo {
current_dir: cwd.to_string(),
project_dir: Some(cwd.to_string()),
}),
version: Some("1.0.0".into()),
output_style: None,
};
let cfg = Config {
command_timeout: timeout_ms,
..Default::default()
};
Context::new(input, cfg)
}
#[test]
fn sleepy_module_times_out_and_is_omitted() {
let logger = DebugLogger::new(true);
let ctx = make_context("/tmp", 50);
let out = render_module_with_timeout("sleepy", &ctx, &logger);
assert!(out.is_none());
}
}