git_plumber/cli/
formatters.rs1use crate::git::loose_object::LooseObject;
2use 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 #[must_use]
18 pub fn format_pack_file(header: &crate::git::pack::Header, objects: &[Object]) -> String {
19 let mut output = String::new();
20
21 Self::format_pack_header(&mut output, header);
23 writeln!(&mut output).unwrap();
24
25 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 fn format_pack_header(output: &mut String, header: &crate::git::pack::Header) {
38 let educational_provider = crate::educational_content::EducationalContent::new();
40 let header_preview = educational_provider.get_pack_preview(header);
41
42 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 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 let pack_obj = Self::create_pack_object_from_object(object, index);
59
60 let mut widget = PackObjectWidget::new(pack_obj);
62 let formatted_text = widget.text();
63
64 let colored_text = Self::text_to_ansi_string(&formatted_text);
66 writeln!(output, "{colored_text}").unwrap();
67 }
68
69 #[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 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" };
83
84 write!(&mut result, "{}{}{}", ansi_start, span.content, ansi_end).unwrap();
85 }
86 result.push('\n');
87 }
88
89 if result.ends_with('\n') {
91 result.pop();
92 }
93
94 result
95 }
96
97 fn style_to_ansi_start(style: &Style) -> String {
99 let mut ansi = String::new();
100
101 if let Some(color) = style.fg {
103 ansi.push_str(&Self::color_to_ansi(color, true));
104 }
105
106 if let Some(color) = style.bg {
108 ansi.push_str(&Self::color_to_ansi(color, false));
109 }
110
111 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 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), 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 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 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 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 #[must_use]
218 pub fn format_loose_object(loose_obj: &LooseObject) -> String {
219 let mut output = String::new();
220
221 writeln!(&mut output, "\x1b[1mLOOSE OBJECT\x1b[0m").unwrap();
223 writeln!(&mut output, "{}", "─".repeat(40)).unwrap();
224 writeln!(&mut output).unwrap();
225
226 let widget = LooseObjectWidget::new(loose_obj.clone());
228 let formatted_text = widget.text();
229
230 let colored_text = CliPackFormatter::text_to_ansi_string(&formatted_text);
232 writeln!(&mut output, "{colored_text}").unwrap();
233
234 output
235 }
236}