git_plumber/cli/
formatters.rs

1use crate::git::loose_object::LooseObject;
2/// CLI formatters that reuse TUI formatting logic for consistent output
3use crate::git::pack::{Object, ObjectHeader};
4use crate::tui::model::PackObject;
5use crate::tui::widget::loose_obj_details::LooseObjectWidget;
6use crate::tui::widget::pack_obj_details::PackObjectWidget;
7use ratatui::style::{Color, Modifier, Style};
8use ratatui::text::Text;
9use sha1::Digest;
10use std::fmt::Write;
11
12pub struct CliPackFormatter;
13pub struct CliLooseFormatter;
14
15impl CliPackFormatter {
16    /// Format a complete pack file with header and all objects
17    #[must_use]
18    pub fn format_pack_file(header: &crate::git::pack::Header, objects: &[Object]) -> String {
19        let mut output = String::new();
20
21        // Format pack header using educational content system
22        Self::format_pack_header(&mut output, header);
23        writeln!(&mut output).unwrap();
24
25        // Format each object using TUI formatters
26        for (i, object) in objects.iter().enumerate() {
27            if i > 0 {
28                writeln!(&mut output, "{}", "═".repeat(80)).unwrap();
29            }
30            Self::format_pack_object(&mut output, object, i + 1);
31        }
32
33        output
34    }
35
36    /// Format pack file header information reusing educational content
37    fn format_pack_header(output: &mut String, header: &crate::git::pack::Header) {
38        // Use the educational content system for pack header preview
39        let educational_provider = crate::educational_content::EducationalContent::new();
40        let header_preview = educational_provider.get_pack_preview(header);
41
42        // Convert ratatui Text to ANSI colored string
43        let colored_text = Self::text_to_ansi_string(&header_preview);
44        writeln!(output, "\x1b[1mPACK FILE HEADER\x1b[0m").unwrap();
45        writeln!(output, "{}", "─".repeat(50)).unwrap();
46        writeln!(output).unwrap();
47        writeln!(output, "{colored_text}").unwrap();
48    }
49
50    /// Format a single pack object using TUI formatters
51    fn format_pack_object(output: &mut String, object: &Object, index: usize) {
52        writeln!(output).unwrap();
53        writeln!(output, "\x1b[1mOBJECT #{index}\x1b[0m").unwrap();
54        writeln!(output, "{}", "─".repeat(40)).unwrap();
55        writeln!(output).unwrap();
56
57        // Create a PackObject from the Object (similar to what TUI loaders do)
58        let pack_obj = Self::create_pack_object_from_object(object, index);
59
60        // Use the TUI formatter to generate rich content
61        let mut widget = PackObjectWidget::new(pack_obj);
62        let formatted_text = widget.text();
63
64        // Convert ratatui Text to ANSI colored string
65        let colored_text = Self::text_to_ansi_string(&formatted_text);
66        writeln!(output, "{colored_text}").unwrap();
67    }
68
69    /// Convert ratatui Text to ANSI colored string, preserving styling
70    #[must_use]
71    pub fn text_to_ansi_string(text: &Text) -> String {
72        let mut result = String::new();
73
74        for line in &text.lines {
75            for span in &line.spans {
76                // Convert ratatui style to ANSI escape codes
77                let ansi_start = Self::style_to_ansi_start(&span.style);
78                let ansi_end = if span.style == Style::default() {
79                    ""
80                } else {
81                    "\x1b[0m" // Reset
82                };
83
84                write!(&mut result, "{}{}{}", ansi_start, span.content, ansi_end).unwrap();
85            }
86            result.push('\n');
87        }
88
89        // Remove trailing newline if present
90        if result.ends_with('\n') {
91            result.pop();
92        }
93
94        result
95    }
96
97    /// Convert ratatui Style to ANSI escape sequence
98    fn style_to_ansi_start(style: &Style) -> String {
99        let mut ansi = String::new();
100
101        // Handle foreground color
102        if let Some(color) = style.fg {
103            ansi.push_str(&Self::color_to_ansi(color, true));
104        }
105
106        // Handle background color
107        if let Some(color) = style.bg {
108            ansi.push_str(&Self::color_to_ansi(color, false));
109        }
110
111        // Handle modifiers
112        if style.add_modifier.contains(Modifier::BOLD) {
113            ansi.push_str("\x1b[1m");
114        }
115        if style.add_modifier.contains(Modifier::DIM) {
116            ansi.push_str("\x1b[2m");
117        }
118        if style.add_modifier.contains(Modifier::ITALIC) {
119            ansi.push_str("\x1b[3m");
120        }
121        if style.add_modifier.contains(Modifier::UNDERLINED) {
122            ansi.push_str("\x1b[4m");
123        }
124        if style.add_modifier.contains(Modifier::SLOW_BLINK) {
125            ansi.push_str("\x1b[5m");
126        }
127        if style.add_modifier.contains(Modifier::RAPID_BLINK) {
128            ansi.push_str("\x1b[6m");
129        }
130        if style.add_modifier.contains(Modifier::REVERSED) {
131            ansi.push_str("\x1b[7m");
132        }
133        if style.add_modifier.contains(Modifier::HIDDEN) {
134            ansi.push_str("\x1b[8m");
135        }
136        if style.add_modifier.contains(Modifier::CROSSED_OUT) {
137            ansi.push_str("\x1b[9m");
138        }
139
140        ansi
141    }
142
143    /// Convert ratatui Color to ANSI color code
144    fn color_to_ansi(color: Color, is_foreground: bool) -> String {
145        let base = if is_foreground { 30 } else { 40 };
146
147        match color {
148            Color::Reset => "\x1b[0m".to_string(),
149            Color::Black => format!("\x1b[{base}m"),
150            Color::Red => format!("\x1b[{}m", base + 1),
151            Color::Green => format!("\x1b[{}m", base + 2),
152            Color::Yellow => format!("\x1b[{}m", base + 3),
153            Color::Blue => format!("\x1b[{}m", base + 4),
154            Color::Magenta => format!("\x1b[{}m", base + 5),
155            Color::Cyan => format!("\x1b[{}m", base + 6),
156            Color::Gray | Color::White => format!("\x1b[{}m", base + 7),
157            Color::DarkGray => format!("\x1b[{}m", base + 60), // Bright black
158            Color::LightRed => format!("\x1b[{}m", base + 61),
159            Color::LightGreen => format!("\x1b[{}m", base + 62),
160            Color::LightYellow => format!("\x1b[{}m", base + 63),
161            Color::LightBlue => format!("\x1b[{}m", base + 64),
162            Color::LightMagenta => format!("\x1b[{}m", base + 65),
163            Color::LightCyan => format!("\x1b[{}m", base + 66),
164            Color::Rgb(r, g, b) => {
165                if is_foreground {
166                    format!("\x1b[38;2;{r};{g};{b}m")
167                } else {
168                    format!("\x1b[48;2;{r};{g};{b}m")
169                }
170            }
171            Color::Indexed(i) => {
172                if is_foreground {
173                    format!("\x1b[38;5;{i}m")
174                } else {
175                    format!("\x1b[48;5;{i}m")
176                }
177            }
178        }
179    }
180
181    /// Create a `PackObject` from an Object (similar to TUI loader logic)
182    fn create_pack_object_from_object(object: &Object, index: usize) -> PackObject {
183        let obj_type = object.header.obj_type();
184        let size = object.header.uncompressed_data_size();
185
186        // Calculate SHA-1 hash like the TUI does
187        let mut hasher = sha1::Sha1::new();
188        let header = format!("{obj_type} {size}\0");
189        hasher.update(header.as_bytes());
190        hasher.update(&object.uncompressed_data);
191        let sha1 = Some(format!("{:x}", hasher.finalize()));
192
193        // Extract base info for delta objects
194        let base_info = match &object.header {
195            ObjectHeader::OfsDelta { base_offset, .. } => {
196                Some(format!("Base offset: {base_offset}"))
197            }
198            ObjectHeader::RefDelta { base_ref, .. } => {
199                Some(format!("Base ref: {}", hex::encode(base_ref)))
200            }
201            ObjectHeader::Regular { .. } => None,
202        };
203
204        PackObject {
205            index,
206            obj_type: obj_type.to_string(),
207            size: u32::try_from(size).unwrap_or(u32::MAX),
208            sha1,
209            base_info,
210            object_data: Some(object.clone()),
211        }
212    }
213}
214
215impl CliLooseFormatter {
216    /// Format a loose object with rich formatting using TUI formatters
217    #[must_use]
218    pub fn format_loose_object(loose_obj: &LooseObject) -> String {
219        let mut output = String::new();
220
221        // Format loose object header
222        writeln!(&mut output, "\x1b[1mLOOSE OBJECT\x1b[0m").unwrap();
223        writeln!(&mut output, "{}", "─".repeat(40)).unwrap();
224        writeln!(&mut output).unwrap();
225
226        // Use the TUI formatter to generate rich content
227        let widget = LooseObjectWidget::new(loose_obj.clone());
228        let formatted_text = widget.text();
229
230        // Convert ratatui Text to ANSI colored string
231        let colored_text = CliPackFormatter::text_to_ansi_string(&formatted_text);
232        writeln!(&mut output, "{colored_text}").unwrap();
233
234        output
235    }
236}