Skip to main content

claude_code_statusline_core/
engine.rs

1//! Engine - Public API facade for rendering status lines
2//!
3//! This module introduces a thin `Engine` wrapper that will later move
4//! to `claude-code-statusline-core` per the refactoring plan. For now, it delegates to
5//! existing internals to keep behavior identical while exposing a stable
6//! entrypoint for library users and tests.
7
8use crate::Config;
9use crate::debug::DebugLogger;
10use crate::error::CoreError;
11use crate::modules::render_module_with_timeout;
12use crate::parser::extract_modules_from_format;
13use crate::types::claude::ClaudeInput;
14use crate::types::context::Context;
15use std::collections::HashMap;
16
17/// Rendering engine that produces a status line from input and config.
18pub struct Engine {
19    config: Config,
20}
21
22impl Engine {
23    /// Construct a new engine with the given configuration.
24    pub fn new(config: Config) -> Self {
25        Self { config }
26    }
27
28    /// Render a status line string from the provided Claude input.
29    pub fn render(&self, input: &ClaudeInput) -> Result<String, CoreError> {
30        let logger = DebugLogger::new(self.config.debug);
31        let context = Context::new(input.clone(), self.config.clone());
32
33        let format = &context.config.format;
34        let module_names = extract_modules_from_format(format);
35
36        // Render modules (optionally in parallel when feature enabled)
37        let module_outputs: HashMap<String, String> = {
38            #[cfg(feature = "parallel")]
39            {
40                use rayon::prelude::*;
41                module_names
42                    .par_iter()
43                    .filter_map(|name| {
44                        if name == "character" {
45                            return None;
46                        }
47                        render_module_with_timeout(name, &context, &logger)
48                            .map(|out| (name.clone(), out))
49                    })
50                    .collect()
51            }
52
53            #[cfg(not(feature = "parallel"))]
54            {
55                let mut map = HashMap::new();
56                for name in &module_names {
57                    if name == "character" {
58                        continue;
59                    }
60                    if let Some(out) = render_module_with_timeout(name, &context, &logger) {
61                        map.insert(name.clone(), out);
62                    }
63                }
64                map
65            }
66        };
67
68        // Replace tokens anywhere and apply top-level bracket styles like
69        // [text](fg:.. bg:..), matching Starship-style presets.
70        let mut tokens: HashMap<&str, String> = HashMap::new();
71        for (k, v) in &module_outputs {
72            tokens.insert(k.as_str(), v.clone());
73        }
74        let mut rendered = crate::style::render_with_style_template(format, &tokens, "");
75        // Ensure a final reset to avoid leaking styles into hosts that
76        // don't strictly track nested resets.
77        rendered.push_str("\x1b[0m");
78        Ok(rendered)
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85    use crate::types::claude::{ClaudeInput, ModelInfo, WorkspaceInfo};
86
87    #[test]
88    fn engine_renders_default_format() {
89        let input = ClaudeInput {
90            hook_event_name: None,
91            session_id: "test-session".into(),
92            transcript_path: None,
93            cwd: "/tmp".into(),
94            model: ModelInfo {
95                id: "claude-opus".into(),
96                display_name: "Opus".into(),
97            },
98            workspace: Some(WorkspaceInfo {
99                current_dir: "/tmp".into(),
100                project_dir: Some("/tmp".into()),
101            }),
102            version: Some("1.0.0".into()),
103            output_style: None,
104        };
105        let cfg = Config::default();
106        let engine = Engine::new(cfg);
107        let out = engine.render(&input).expect("render ok");
108        let plain = String::from_utf8(strip_ansi_escapes::strip(out)).unwrap();
109        assert!(plain.contains("/tmp"));
110        assert!(plain.contains("Opus"));
111    }
112}