Skip to main content

commit_wizard/infra/ui/
mod.rs

1use std::cell::RefCell;
2use std::io::{self, Write};
3mod logger;
4mod prompt;
5
6use crate::engine::{Error, ErrorCode};
7use scriba::{Config, Format, Logger, Meta, Output, envelope, output::render};
8pub type UiResult<T> = Result<T, Error>;
9
10// Thread-local cache for the current Ui instance
11thread_local! {
12    static UI_CACHE: RefCell<Option<Ui>> = const { RefCell::new(None) };
13}
14
15#[derive(Debug, Clone)]
16pub struct Ui {
17    config: Config,
18    envelope: envelope::EnvelopeConfig,
19    prompt_theme: scriba::prompt::PromptTheme,
20}
21
22impl Ui {
23    pub fn new() -> Self {
24        Self::with_config(Config::default())
25    }
26
27    pub fn with_config(config: Config) -> Self {
28        Self {
29            config,
30            envelope: envelope::EnvelopeConfig::default(),
31            prompt_theme: scriba::prompt::PromptTheme::default(),
32        }
33    }
34
35    /// Returns a cached Ui instance configured with the given config and envelope mode.
36    /// The cache is per-thread and keyed by config. If the same config is requested,
37    /// the cached instance is returned, avoiding repeated creation.
38    pub fn cached_with_config(config: Config, envelope_mode: envelope::EnvelopeMode) -> Self {
39        UI_CACHE.with(|cache| {
40            let mut cached = cache.borrow_mut();
41            match cached.as_ref() {
42                Some(ui) if ui.config == config && ui.envelope.mode == envelope_mode => ui.clone(),
43                _ => {
44                    let ui = Self::with_config(config).with_envelope_mode(envelope_mode);
45                    *cached = Some(ui.clone());
46                    ui
47                }
48            }
49        })
50    }
51
52    /// Get reference to the current configuration.
53    pub fn config(&self) -> &Config {
54        &self.config
55    }
56
57    pub fn use_color(&self) -> bool {
58        if self.config.color == scriba::ColorMode::Always
59            || (self.config.color == scriba::ColorMode::Auto && self.config.interactive)
60        {
61            return true;
62        }
63        false
64    }
65
66    /// Get reference to the current envelope configuration.
67    pub fn envelope(&self) -> &envelope::EnvelopeConfig {
68        &self.envelope
69    }
70
71    pub fn with_envelope(mut self, config: envelope::EnvelopeConfig) -> Self {
72        self.envelope = config;
73        self
74    }
75
76    pub fn with_envelope_mode(mut self, mode: envelope::EnvelopeMode) -> Self {
77        self.envelope.mode = mode;
78        self
79    }
80
81    pub fn with_envelope_layout(mut self, layout: envelope::EnvelopeLayout) -> Self {
82        self.envelope.layout = layout;
83        self
84    }
85
86    pub fn with_envelope_fields(mut self, fields: envelope::EnvelopeFields) -> Self {
87        self.envelope.fields = fields;
88        self
89    }
90
91    pub fn with_prompt_theme(mut self, theme: scriba::prompt::PromptTheme) -> Self {
92        self.prompt_theme = theme;
93        self
94    }
95
96    pub fn prompt_theme(&self) -> &scriba::prompt::PromptTheme {
97        &self.prompt_theme
98    }
99
100    pub fn with_format(mut self, format: Format) -> Self {
101        self.config.format = format;
102        self
103    }
104
105    pub fn interactive(mut self, value: bool) -> Self {
106        self.config.interactive = value;
107        self
108    }
109
110    pub fn auto_yes(mut self, value: bool) -> Self {
111        self.config.auto_yes = value;
112        self
113    }
114
115    pub fn logger(&self) -> Logger<'_> {
116        Logger::new(&self.config)
117    }
118
119    pub fn new_output_content(&self) -> Output {
120        Output::new()
121    }
122
123    pub fn new_output_meta(&self) -> Meta {
124        Meta::default()
125    }
126
127    pub fn render(&self, output: &Output) -> UiResult<String> {
128        render::render_output(self.config.format, output).map_err(map_scriba_error_to_cw_error)
129    }
130
131    pub fn print(&self, output: &Output) -> UiResult<()> {
132        self.print_with_meta(output, None, true)
133    }
134
135    pub fn print_with_meta(&self, output: &Output, meta: Option<&Meta>, ok: bool) -> UiResult<()> {
136        let text = if self.envelope.mode.is_json() {
137            let content = render::render_output_value(self.config.format, output)
138                .map_err(map_scriba_error_to_cw_error)?;
139            let wrapped = envelope::wrap(
140                &self.envelope,
141                self.config.format.as_str(),
142                content,
143                meta,
144                ok,
145            );
146            serde_json::to_string_pretty(&wrapped)?
147        } else {
148            self.render(output)?
149        };
150        let mut stdout = io::stdout();
151        stdout.write_all(text.as_bytes())?;
152        stdout.flush()?;
153        Ok(())
154    }
155
156    pub fn git_diff(&self, diff: &str) -> String {
157        scriba::output::render_colored_diff(diff, self.use_color())
158    }
159}
160
161impl Default for Ui {
162    fn default() -> Self {
163        Self::new()
164    }
165}
166
167fn map_scriba_error_to_cw_error(err: scriba::Error) -> Error {
168    ErrorCode::UiPromptFailed
169        .error()
170        .with_context("error", err.to_string())
171}