#![allow(clippy::all)] use colored::*; use just_enough_vcs::lib::data::sheet::SheetMappingMetadata; use std::{ collections::{BTreeMap, HashMap, VecDeque}, path::PathBuf, }; pub struct SimpleTable { items: Vec, line: Vec>, length: Vec, padding: usize, } impl SimpleTable { /// Create a new Table pub fn new(items: Vec>) -> Self { Self::new_with_padding(items, 2) } /// Create a new Table with padding pub fn new_with_padding(items: Vec>, padding: usize) -> Self { let items: Vec = items.into_iter().map(|v| v.into()).collect(); let mut length = Vec::with_capacity(items.len()); for item in &items { length.push(display_width(item)); } SimpleTable { items, padding, line: Vec::new(), length, } } /// Push a new row of items to the table pub fn push_item(&mut self, items: Vec>) { let items: Vec = items.into_iter().map(|v| v.into()).collect(); let mut processed_items = Vec::with_capacity(self.items.len()); for i in 0..self.items.len() { if i < items.len() { processed_items.push(items[i].clone()); } else { processed_items.push(String::new()); } } for (i, d) in processed_items.iter().enumerate() { let d_len = display_width(d); if d_len > self.length[i] { self.length[i] = d_len; } } self.line.push(processed_items); } /// Insert a new row of items at the specified index pub fn insert_item(&mut self, index: usize, items: Vec>) { let items: Vec = items.into_iter().map(|v| v.into()).collect(); let mut processed_items = Vec::with_capacity(self.items.len()); for i in 0..self.items.len() { if i < items.len() { processed_items.push(items[i].clone()); } else { processed_items.push(String::new()); } } for (i, d) in processed_items.iter().enumerate() { let d_len = display_width(d); if d_len > self.length[i] { self.length[i] = d_len; } } self.line.insert(index, processed_items); } /// Get the current maximum column widths fn get_column_widths(&self) -> &[usize] { &self.length } } impl std::fmt::Display for SimpleTable { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let column_widths = self.get_column_widths(); // Build the header row let header: Vec = self .items .iter() .enumerate() .map(|(i, item)| { let target_width = column_widths[i] + self.padding; let current_width = display_width(item); let space_count = target_width - current_width; let space = " ".repeat(space_count); let result = format!("{}{}", item, space); result }) .collect(); writeln!(f, "{}", header.join(""))?; // Build each data row for row in &self.line { let formatted_row: Vec = row .iter() .enumerate() .map(|(i, cell)| { let target_width = column_widths[i] + self.padding; let current_width = display_width(cell); let space_count = target_width - current_width; let spaces = " ".repeat(space_count); let result = format!("{}{}", cell, spaces); result }) .collect(); writeln!(f, "{}", formatted_row.join(""))?; } Ok(()) } } pub fn display_width(s: &str) -> usize { // Filter out ANSI escape sequences before calculating width let filtered_bytes = strip_ansi_escapes::strip(s); let filtered_str = match std::str::from_utf8(&filtered_bytes) { Ok(s) => s, Err(_) => s, // Fallback to original string if UTF-8 conversion fails }; let mut width = 0; for c in filtered_str.chars() { if c.is_ascii() { width += 1; } else { width += 2; } } width } /// Convert byte size to a human-readable string format /// /// Automatically selects the appropriate unit (B, KB, MB, GB, TB) based on the byte size /// and formats it as a string with two decimal places pub fn size_str(total_size: usize) -> String { if total_size < 1024 { format!("{} B", total_size) } else if total_size < 1024 * 1024 { format!("{:.2} KB", total_size as f64 / 1024.0) } else if total_size < 1024 * 1024 * 1024 { format!("{:.2} MB", total_size as f64 / (1024.0 * 1024.0)) } else if total_size < 1024 * 1024 * 1024 * 1024 { format!("{:.2} GB", total_size as f64 / (1024.0 * 1024.0 * 1024.0)) } else { format!( "{:.2} TB", total_size as f64 / (1024.0 * 1024.0 * 1024.0 * 1024.0) ) } } // Convert the Markdown formatted text into a format supported by the command line pub fn md(text: impl AsRef) -> String { let text = text.as_ref().trim(); let mut result = String::new(); let mut color_stack: VecDeque = VecDeque::new(); let mut i = 0; let chars: Vec = text.chars().collect(); while i < chars.len() { // Check for escape character \ if chars[i] == '\\' && i + 1 < chars.len() { let escaped_char = chars[i + 1]; // Only escape specific characters if matches!(escaped_char, '*' | '<' | '>' | '`') { let mut escaped_text = escaped_char.to_string(); // Apply current color stack for color in color_stack.iter().rev() { escaped_text = apply_color(&escaped_text, color); } result.push_str(&escaped_text); i += 2; continue; } } // Check for color tag start [[color]] if i + 1 < chars.len() && chars[i] == '[' && chars[i + 1] == '[' { let mut j = i + 2; while j < chars.len() && !(chars[j] == ']' && j + 1 < chars.len() && chars[j + 1] == ']') { j += 1; } if j + 1 < chars.len() { let tag_content: String = chars[i + 2..j].iter().collect(); // Check if it's a closing tag [[/]] if tag_content == "/" { color_stack.pop_back(); i = j + 2; continue; } // It's a color tag color_stack.push_back(tag_content.clone()); i = j + 2; continue; } } // Check for bold **text** if i + 1 < chars.len() && chars[i] == '*' && chars[i + 1] == '*' { let mut j = i + 2; while j + 1 < chars.len() && !(chars[j] == '*' && chars[j + 1] == '*') { j += 1; } if j + 1 < chars.len() { let bold_text: String = chars[i + 2..j].iter().collect(); let mut formatted_text = bold_text.bold().to_string(); // Apply current color stack for color in color_stack.iter().rev() { formatted_text = apply_color(&formatted_text, color); } result.push_str(&formatted_text); i = j + 2; continue; } } // Check for italic *text* if chars[i] == '*' { let mut j = i + 1; while j < chars.len() && chars[j] != '*' { j += 1; } if j < chars.len() { let italic_text: String = chars[i + 1..j].iter().collect(); let mut formatted_text = italic_text.italic().to_string(); // Apply current color stack for color in color_stack.iter().rev() { formatted_text = apply_color(&formatted_text, color); } result.push_str(&formatted_text); i = j + 1; continue; } } // Check for angle-bracketed content if chars[i] == '<' { let mut j = i + 1; while j < chars.len() && chars[j] != '>' { j += 1; } if j < chars.len() { // Include the angle brackets in the output let angle_text: String = chars[i..=j].iter().collect(); let mut formatted_text = angle_text.cyan().to_string(); // Apply current color stack for color in color_stack.iter().rev() { formatted_text = apply_color(&formatted_text, color); } result.push_str(&formatted_text); i = j + 1; continue; } } // Check for inline code `text` if chars[i] == '`' { let mut j = i + 1; while j < chars.len() && chars[j] != '`' { j += 1; } if j < chars.len() { // Include the backticks in the output let code_text: String = chars[i..=j].iter().collect(); let mut formatted_text = code_text.green().to_string(); // Apply current color stack for color in color_stack.iter().rev() { formatted_text = apply_color(&formatted_text, color); } result.push_str(&formatted_text); i = j + 1; continue; } } // Regular character let mut current_char = chars[i].to_string(); // Apply current color stack for color in color_stack.iter().rev() { current_char = apply_color(¤t_char, color); } result.push_str(¤t_char); i += 1; } result } // Helper function to apply color to text fn apply_color(text: impl AsRef, color_name: impl AsRef) -> String { let text = text.as_ref(); let color_name = color_name.as_ref(); match color_name { // Normal colors "black" => text.black().to_string(), "red" => text.red().to_string(), "green" => text.green().to_string(), "yellow" => text.yellow().to_string(), "blue" => text.blue().to_string(), "magenta" => text.magenta().to_string(), "cyan" => text.cyan().to_string(), "white" => text.white().to_string(), "bright_black" => text.bright_black().to_string(), "bright_red" => text.bright_red().to_string(), "bright_green" => text.bright_green().to_string(), "bright_yellow" => text.bright_yellow().to_string(), "bright_blue" => text.bright_blue().to_string(), "bright_magenta" => text.bright_magenta().to_string(), "bright_cyan" => text.bright_cyan().to_string(), "bright_white" => text.bright_white().to_string(), // Short aliases for bright colors "b_black" => text.bright_black().to_string(), "b_red" => text.bright_red().to_string(), "b_green" => text.bright_green().to_string(), "b_yellow" => text.bright_yellow().to_string(), "b_blue" => text.bright_blue().to_string(), "b_magenta" => text.bright_magenta().to_string(), "b_cyan" => text.bright_cyan().to_string(), "b_white" => text.bright_white().to_string(), // Gray colors using truecolor "gray" | "grey" => text.truecolor(128, 128, 128).to_string(), "bright_gray" | "bright_grey" => text.truecolor(192, 192, 192).to_string(), "b_gray" | "b_grey" => text.truecolor(192, 192, 192).to_string(), // Default to white if color not recognized _ => text.to_string(), } } /// Render a HashMap of PathBuf to SheetMappingMetadata as a tree string. pub fn render_share_path_tree(paths: &HashMap) -> String { if paths.is_empty() { return String::new(); } // Collect all path components into a tree structure let mut root = TreeNode::new("".to_string()); for (path, metadata) in paths { let mut current = &mut root; let components: Vec = path .components() .filter_map(|comp| match comp { std::path::Component::Normal(s) => s.to_str().map(|s| s.to_string()), _ => None, }) .collect(); for (i, comp) in components.iter().enumerate() { let is_leaf = i == components.len() - 1; let child = current .children .entry(comp.clone()) .or_insert_with(|| TreeNode::new(comp.clone())); // If this is the leaf node, store the metadata if is_leaf { child.metadata = Some((metadata.id.clone(), metadata.version.clone())); } current = child; } } // Convert tree to string representation let mut result = String::new(); let is_root = true; let prefix = String::new(); let last_stack = vec![true]; // Root is always "last" add_tree_node_to_string(&root, &mut result, is_root, &prefix, &last_stack); result } /// Internal tree node structure for building the path tree #[derive(Debug)] struct TreeNode { name: String, children: BTreeMap, // Use BTreeMap for sorted output metadata: Option<(String, String)>, // Store (id, version) for leaf nodes } impl TreeNode { fn new(name: String) -> Self { Self { name, children: BTreeMap::new(), metadata: None, } } } /// Recursively add tree node to string representation fn add_tree_node_to_string( node: &TreeNode, result: &mut String, is_root: bool, prefix: &str, last_stack: &[bool], ) { if !is_root { // Add the tree prefix for this node for &is_last in &last_stack[1..] { if is_last { result.push_str(" "); } else { result.push_str("│ "); } } // Add the connector for this node if let Some(&is_last) = last_stack.last() { if is_last { result.push_str("└── "); } else { result.push_str("├── "); } } // Add node name result.push_str(&node.name); // Add metadata for leaf nodes if let Some((id, version)) = &node.metadata { // Truncate id to first 11 characters let truncated_id = if id.len() > 11 { &id[..11] } else { id }; result.push_str(&format!(" [{}|{}]", truncated_id, version)); } result.push('\n'); } // Process children let child_count = node.children.len(); for (i, (_, child)) in node.children.iter().enumerate() { let is_last_child = i == child_count - 1; let mut new_last_stack = last_stack.to_vec(); new_last_stack.push(is_last_child); add_tree_node_to_string(child, result, false, prefix, &new_last_stack); } }