use crate::git::loose_object::LooseObject;
use crate::git::pack::{Object, ObjectHeader};
use crate::tui::model::PackObject;
use crate::tui::widget::loose_obj_details::LooseObjectWidget;
use crate::tui::widget::pack_obj_details::PackObjectWidget;
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::Text;
use sha1::Digest;
use std::fmt::Write;
pub struct CliPackFormatter;
pub struct CliLooseFormatter;
impl CliPackFormatter {
#[must_use]
pub fn format_pack_file(header: &crate::git::pack::Header, objects: &[Object]) -> String {
let mut output = String::new();
Self::format_pack_header(&mut output, header);
writeln!(&mut output).unwrap();
for (i, object) in objects.iter().enumerate() {
if i > 0 {
writeln!(&mut output, "{}", "═".repeat(80)).unwrap();
}
Self::format_pack_object(&mut output, object, i + 1);
}
output
}
fn format_pack_header(output: &mut String, header: &crate::git::pack::Header) {
let educational_provider = crate::educational_content::EducationalContent::new();
let header_preview = educational_provider.get_pack_preview(header);
let colored_text = Self::text_to_ansi_string(&header_preview);
writeln!(output, "\x1b[1mPACK FILE HEADER\x1b[0m").unwrap();
writeln!(output, "{}", "─".repeat(50)).unwrap();
writeln!(output).unwrap();
writeln!(output, "{colored_text}").unwrap();
}
fn format_pack_object(output: &mut String, object: &Object, index: usize) {
writeln!(output).unwrap();
writeln!(output, "\x1b[1mOBJECT #{index}\x1b[0m").unwrap();
writeln!(output, "{}", "─".repeat(40)).unwrap();
writeln!(output).unwrap();
let pack_obj = Self::create_pack_object_from_object(object, index);
let mut widget = PackObjectWidget::new(pack_obj);
let formatted_text = widget.text();
let colored_text = Self::text_to_ansi_string(&formatted_text);
writeln!(output, "{colored_text}").unwrap();
}
#[must_use]
pub fn text_to_ansi_string(text: &Text) -> String {
let mut result = String::new();
for line in &text.lines {
for span in &line.spans {
let ansi_start = Self::style_to_ansi_start(&span.style);
let ansi_end = if span.style == Style::default() {
""
} else {
"\x1b[0m" };
write!(&mut result, "{}{}{}", ansi_start, span.content, ansi_end).unwrap();
}
result.push('\n');
}
if result.ends_with('\n') {
result.pop();
}
result
}
fn style_to_ansi_start(style: &Style) -> String {
let mut ansi = String::new();
if let Some(color) = style.fg {
ansi.push_str(&Self::color_to_ansi(color, true));
}
if let Some(color) = style.bg {
ansi.push_str(&Self::color_to_ansi(color, false));
}
if style.add_modifier.contains(Modifier::BOLD) {
ansi.push_str("\x1b[1m");
}
if style.add_modifier.contains(Modifier::DIM) {
ansi.push_str("\x1b[2m");
}
if style.add_modifier.contains(Modifier::ITALIC) {
ansi.push_str("\x1b[3m");
}
if style.add_modifier.contains(Modifier::UNDERLINED) {
ansi.push_str("\x1b[4m");
}
if style.add_modifier.contains(Modifier::SLOW_BLINK) {
ansi.push_str("\x1b[5m");
}
if style.add_modifier.contains(Modifier::RAPID_BLINK) {
ansi.push_str("\x1b[6m");
}
if style.add_modifier.contains(Modifier::REVERSED) {
ansi.push_str("\x1b[7m");
}
if style.add_modifier.contains(Modifier::HIDDEN) {
ansi.push_str("\x1b[8m");
}
if style.add_modifier.contains(Modifier::CROSSED_OUT) {
ansi.push_str("\x1b[9m");
}
ansi
}
fn color_to_ansi(color: Color, is_foreground: bool) -> String {
let base = if is_foreground { 30 } else { 40 };
match color {
Color::Reset => "\x1b[0m".to_string(),
Color::Black => format!("\x1b[{base}m"),
Color::Red => format!("\x1b[{}m", base + 1),
Color::Green => format!("\x1b[{}m", base + 2),
Color::Yellow => format!("\x1b[{}m", base + 3),
Color::Blue => format!("\x1b[{}m", base + 4),
Color::Magenta => format!("\x1b[{}m", base + 5),
Color::Cyan => format!("\x1b[{}m", base + 6),
Color::Gray | Color::White => format!("\x1b[{}m", base + 7),
Color::DarkGray => format!("\x1b[{}m", base + 60), Color::LightRed => format!("\x1b[{}m", base + 61),
Color::LightGreen => format!("\x1b[{}m", base + 62),
Color::LightYellow => format!("\x1b[{}m", base + 63),
Color::LightBlue => format!("\x1b[{}m", base + 64),
Color::LightMagenta => format!("\x1b[{}m", base + 65),
Color::LightCyan => format!("\x1b[{}m", base + 66),
Color::Rgb(r, g, b) => {
if is_foreground {
format!("\x1b[38;2;{r};{g};{b}m")
} else {
format!("\x1b[48;2;{r};{g};{b}m")
}
}
Color::Indexed(i) => {
if is_foreground {
format!("\x1b[38;5;{i}m")
} else {
format!("\x1b[48;5;{i}m")
}
}
}
}
fn create_pack_object_from_object(object: &Object, index: usize) -> PackObject {
let obj_type = object.header.obj_type();
let size = object.header.uncompressed_data_size();
let mut hasher = sha1::Sha1::new();
let header = format!("{obj_type} {size}\0");
hasher.update(header.as_bytes());
hasher.update(&object.uncompressed_data);
let sha1 = Some(format!("{:x}", hasher.finalize()));
let base_info = match &object.header {
ObjectHeader::OfsDelta { base_offset, .. } => {
Some(format!("Base offset: {base_offset}"))
}
ObjectHeader::RefDelta { base_ref, .. } => {
Some(format!("Base ref: {}", hex::encode(base_ref)))
}
ObjectHeader::Regular { .. } => None,
};
PackObject {
index,
obj_type: obj_type.to_string(),
size: u32::try_from(size).unwrap_or(u32::MAX),
sha1,
base_info,
object_data: Some(object.clone()),
}
}
}
impl CliLooseFormatter {
#[must_use]
pub fn format_loose_object(loose_obj: &LooseObject) -> String {
let mut output = String::new();
writeln!(&mut output, "\x1b[1mLOOSE OBJECT\x1b[0m").unwrap();
writeln!(&mut output, "{}", "─".repeat(40)).unwrap();
writeln!(&mut output).unwrap();
let widget = LooseObjectWidget::new(loose_obj.clone());
let formatted_text = widget.text();
let colored_text = CliPackFormatter::text_to_ansi_string(&formatted_text);
writeln!(&mut output, "{colored_text}").unwrap();
output
}
}