Skip to main content

claude_code_statusline_core/modules/
registry.rs

1//! Module registry and factory system
2//!
3//! Provides `Registry` and `ModuleFactory` to create modules dynamically
4//! without hard-coded dispatcher matches. This enables pluggable modules
5//! and paves the way for external/extra modules via configuration.
6
7use super::{Module, ModuleConfig, claude_model::ClaudeModelModule, directory::DirectoryModule};
8#[cfg(feature = "git")]
9use super::{git_branch::GitBranchModule, git_status::GitStatusModule};
10use crate::types::context::Context;
11
12/// Factory trait for constructing modules and exposing their config binding
13pub trait ModuleFactory: Send + Sync {
14    /// Canonical module name (e.g., "directory")
15    fn name(&self) -> &'static str;
16
17    /// Create a fresh module instance for the given context
18    fn create(&self, context: &Context) -> Box<dyn Module>;
19
20    /// Obtain the module-specific config view from Context
21    fn config<'a>(&self, context: &'a Context) -> Option<&'a dyn ModuleConfig>;
22}
23
24/// Simple in-memory registry of module factories
25pub struct Registry {
26    factories: Vec<Box<dyn ModuleFactory>>,
27}
28
29impl Registry {
30    /// Empty registry
31    pub fn new() -> Self {
32        Self {
33            factories: Vec::new(),
34        }
35    }
36
37    /// Default registry with built-in modules
38    pub fn with_defaults() -> Self {
39        let mut reg = Self::new();
40        reg.register_factory(DirectoryFactory);
41        reg.register_factory(ClaudeModelFactory);
42        #[cfg(feature = "git")]
43        {
44            reg.register_factory(GitBranchFactory);
45            reg.register_factory(GitStatusFactory);
46        }
47        reg
48    }
49
50    /// Register a factory
51    pub fn register_factory<F: ModuleFactory + 'static>(&mut self, f: F) {
52        self.factories.push(Box::new(f));
53    }
54
55    /// Create a module by name
56    pub fn create(&self, name: &str, context: &Context) -> Option<Box<dyn Module>> {
57        self.factories
58            .iter()
59            .find(|f| f.name() == name)
60            .map(|f| f.create(context))
61    }
62
63    /// Get module config by name
64    pub fn config<'a>(&self, name: &str, context: &'a Context) -> Option<&'a dyn ModuleConfig> {
65        self.factories
66            .iter()
67            .find(|f| f.name() == name)
68            .and_then(|f| f.config(context))
69    }
70
71    /// List registered module names
72    #[allow(dead_code)]
73    pub fn list(&self) -> Vec<&'static str> {
74        self.factories.iter().map(|f| f.name()).collect()
75    }
76}
77
78impl Default for Registry {
79    fn default() -> Self {
80        Self::new()
81    }
82}
83
84// Built-in factories
85
86struct DirectoryFactory;
87impl ModuleFactory for DirectoryFactory {
88    fn name(&self) -> &'static str {
89        "directory"
90    }
91    fn create(&self, context: &Context) -> Box<dyn Module> {
92        Box::new(DirectoryModule::from_context(context))
93    }
94    fn config<'a>(&self, context: &'a Context) -> Option<&'a dyn ModuleConfig> {
95        Some(&context.config.directory)
96    }
97}
98
99struct ClaudeModelFactory;
100impl ModuleFactory for ClaudeModelFactory {
101    fn name(&self) -> &'static str {
102        "claude_model"
103    }
104    fn create(&self, context: &Context) -> Box<dyn Module> {
105        Box::new(ClaudeModelModule::from_context(context))
106    }
107    fn config<'a>(&self, context: &'a Context) -> Option<&'a dyn ModuleConfig> {
108        Some(&context.config.claude_model)
109    }
110}
111
112#[cfg(feature = "git")]
113struct GitBranchFactory;
114#[cfg(feature = "git")]
115impl ModuleFactory for GitBranchFactory {
116    fn name(&self) -> &'static str {
117        "git_branch"
118    }
119    fn create(&self, context: &Context) -> Box<dyn Module> {
120        Box::new(GitBranchModule::from_context(context))
121    }
122    fn config<'a>(&self, context: &'a Context) -> Option<&'a dyn ModuleConfig> {
123        Some(&context.config.git_branch)
124    }
125}
126
127#[cfg(feature = "git")]
128struct GitStatusFactory;
129#[cfg(feature = "git")]
130impl ModuleFactory for GitStatusFactory {
131    fn name(&self) -> &'static str {
132        "git_status"
133    }
134    fn create(&self, context: &Context) -> Box<dyn Module> {
135        Box::new(GitStatusModule::from_context(context))
136    }
137    fn config<'a>(&self, context: &'a Context) -> Option<&'a dyn ModuleConfig> {
138        Some(&context.config.git_status)
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145    use crate::config::Config;
146    use crate::types::claude::{ClaudeInput, ModelInfo};
147    use crate::types::context::Context;
148
149    #[test]
150    fn default_registry_lists_core_modules() {
151        let reg = Registry::with_defaults();
152        let names = reg.list();
153        assert!(names.contains(&"directory"));
154        assert!(names.contains(&"claude_model"));
155        #[cfg(feature = "git")]
156        {
157            assert!(names.contains(&"git_branch"));
158            assert!(names.contains(&"git_status"));
159        }
160    }
161
162    #[test]
163    fn create_and_config_work_for_known_modules() {
164        let cfg = Config::default();
165        let input = ClaudeInput {
166            hook_event_name: None,
167            session_id: "s".into(),
168            transcript_path: None,
169            cwd: "/tmp".into(),
170            model: ModelInfo {
171                id: "claude-opus".into(),
172                display_name: "Opus".into(),
173            },
174            workspace: None,
175            version: None,
176            output_style: None,
177        };
178        let ctx = Context::new(input, cfg);
179        let reg = Registry::with_defaults();
180        let m = reg.create("directory", &ctx).expect("module");
181        assert_eq!(m.name(), "directory");
182        assert!(reg.config("directory", &ctx).is_some());
183        assert!(reg.create("unknown", &ctx).is_none());
184    }
185}