diff options
| author | 魏曹先生 <1992414357@qq.com> | 2026-03-12 14:28:08 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2026-03-12 14:28:08 +0800 |
| commit | 0a95bae451c1847f4f0b9601e60959f4e8e6b669 (patch) | |
| tree | 9e1cfad4f86a73176a4d738b28e7732b66fe5f97 /utils/src | |
| parent | 8564c8f2177dec0c2c0c031d156347fa6b4485bc (diff) | |
Refactor display utilities
Diffstat (limited to 'utils/src')
| -rw-r--r-- | utils/src/display.rs | 490 | ||||
| -rw-r--r-- | utils/src/display/colorful.rs | 275 | ||||
| -rw-r--r-- | utils/src/display/table.rs | 143 | ||||
| -rw-r--r-- | utils/src/legacy.rs | 9 | ||||
| -rw-r--r-- | utils/src/legacy/display.rs | 488 | ||||
| -rw-r--r-- | utils/src/legacy/env.rs (renamed from utils/src/env.rs) | 0 | ||||
| -rw-r--r-- | utils/src/legacy/fs.rs (renamed from utils/src/fs.rs) | 0 | ||||
| -rw-r--r-- | utils/src/legacy/globber.rs (renamed from utils/src/globber.rs) | 2 | ||||
| -rw-r--r-- | utils/src/legacy/input.rs (renamed from utils/src/input.rs) | 2 | ||||
| -rw-r--r-- | utils/src/legacy/levenshtein_distance.rs (renamed from utils/src/levenshtein_distance.rs) | 0 | ||||
| -rw-r--r-- | utils/src/legacy/logger.rs (renamed from utils/src/logger.rs) | 0 | ||||
| -rw-r--r-- | utils/src/legacy/push_version.rs (renamed from utils/src/push_version.rs) | 0 | ||||
| -rw-r--r-- | utils/src/legacy/socket_addr_helper.rs (renamed from utils/src/socket_addr_helper.rs) | 0 | ||||
| -rw-r--r-- | utils/src/lib.rs | 14 | ||||
| -rw-r--r-- | utils/src/macros.rs (renamed from utils/src/lazy_macros.rs) | 0 | ||||
| -rw-r--r-- | utils/src/math.rs | 1 | ||||
| -rw-r--r-- | utils/src/math/levenshtein_distance.rs | 38 |
17 files changed, 963 insertions, 499 deletions
diff --git a/utils/src/display.rs b/utils/src/display.rs index fc94d90..a9c48e8 100644 --- a/utils/src/display.rs +++ b/utils/src/display.rs @@ -1,488 +1,2 @@ -use colored::*; -use just_enough_vcs::lib::data::sheet::SheetMappingMetadata; -use std::{ - collections::{BTreeMap, HashMap, VecDeque}, - path::PathBuf, -}; - -pub struct SimpleTable { - items: Vec<String>, - line: Vec<Vec<String>>, - length: Vec<usize>, - padding: usize, -} - -impl SimpleTable { - /// Create a new Table - pub fn new(items: Vec<impl Into<String>>) -> Self { - Self::new_with_padding(items, 2) - } - - /// Create a new Table with padding - pub fn new_with_padding(items: Vec<impl Into<String>>, padding: usize) -> Self { - let items: Vec<String> = 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<impl Into<String>>) { - let items: Vec<String> = 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<impl Into<String>>) { - let items: Vec<String> = 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<String> = 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<String> = 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<str>) -> String { - let text = text.as_ref().trim(); - let mut result = String::new(); - let mut color_stack: VecDeque<String> = VecDeque::new(); - - let mut i = 0; - let chars: Vec<char> = 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 <text> - 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<str>, color_name: impl AsRef<str>) -> 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<PathBuf, SheetMappingMetadata>) -> 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<String> = 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<String, TreeNode>, // 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); - } -} +pub mod colorful; +pub mod table; diff --git a/utils/src/display/colorful.rs b/utils/src/display/colorful.rs new file mode 100644 index 0000000..7daa6f2 --- /dev/null +++ b/utils/src/display/colorful.rs @@ -0,0 +1,275 @@ +use std::collections::VecDeque; + +use crossterm::style::Stylize; + +/// Trait for adding markdown formatting to strings +pub trait Colorful { + fn colorful(&self) -> String; +} + +impl Colorful for &str { + fn colorful(&self) -> String { + colorful(self) + } +} + +impl Colorful for String { + fn colorful(&self) -> String { + colorful(self) + } +} + +/// Converts a string to colored/formatted text with ANSI escape codes. +/// +/// Supported syntax: +/// - Bold: `**text**` +/// - Italic: `*text*` +/// - Underline: `_text_` +/// - Angle-bracketed content: `<text>` (displayed as cyan) +/// - Inline code: `` `text` `` (displayed as green) +/// - Color tags: `[[color_name]]` and `[[/]]` to close color +/// - Escape characters: `\*`, `\<`, `\>`, `` \` ``, `\_` for literal characters +/// +/// Color tags support the following color names: +/// Color tags support the following color names: +/// +/// | Type | Color Names | +/// |-----------------------|-----------------------------------------------------------------------------| +/// | Standard colors | `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white` | +/// | Bright colors | `bright_black` | +/// | | `bright_red` | +/// | | `bright_green` | +/// | | `bright_yellow` | +/// | | `bright_blue` | +/// | | `bright_magenta` | +/// | | `bright_cyan` | +/// | | `bright_white` | +/// | Bright color shorthands | `b_black` | +/// | | `b_red` | +/// | | `b_green` | +/// | | `b_yellow` | +/// | | `b_blue` | +/// | | `b_magenta` | +/// | | `b_cyan` | +/// | | `b_white` | +/// | Gray colors | `gray`/`grey` | +/// | | `bright_gray`/`bright_grey` | +/// | | `b_gray`/`b_grey` | +/// +/// Color tags can be nested, `[[/]]` will close the most recently opened color tag. +/// +/// # Arguments +/// * `text` - The text to format, can be any type that implements `AsRef<str>` +/// +/// # Returns +/// Returns a `String` containing ANSI escape codes that can display colored/formatted text in ANSI-supported terminals. +/// +/// # Examples +/// ``` +/// use testing::fmt::colorful; +/// +/// let formatted = colorful("Hello **world**!"); +/// println!("{}", formatted); +/// +/// let colored = colorful("[[red]]Red text[[/]] and normal text"); +/// println!("{}", colored); +/// +/// let nested = colorful("[[blue]]Blue [[green]]Green[[/]] Blue[[/]] normal"); +/// println!("{}", nested); +/// ``` +pub fn colorful(text: impl AsRef<str>) -> String { + let text = text.as_ref().trim(); + let mut result = String::new(); + let mut color_stack: VecDeque<String> = VecDeque::new(); + + let chars: Vec<char> = text.chars().collect(); + let mut i = 0; + + 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_color_stack(&mut escaped_text, &color_stack); + result.push_str(&escaped_text); + i += 2; + continue; + } + } + + // Check for color tag start [[color]] + if i + 1 < chars.len() && chars[i] == '[' && chars[i + 1] == '[' { + if let Some(end) = find_tag_end(&chars, i) { + let tag_content: String = chars[i + 2..end].iter().collect(); + + // Check if it's a closing tag [[/]] + if tag_content == "/" { + color_stack.pop_back(); + } else { + // It's a color tag + color_stack.push_back(tag_content.clone()); + } + i = end + 2; + continue; + } + } + + // Check for bold **text** + if i + 1 < chars.len() && chars[i] == '*' && chars[i + 1] == '*' { + if let Some(end) = find_matching(&chars, i + 2, "**") { + let bold_text: String = chars[i + 2..end].iter().collect(); + let mut formatted_text = bold_text.bold().to_string(); + apply_color_stack(&mut formatted_text, &color_stack); + result.push_str(&formatted_text); + i = end + 2; + continue; + } + } + + // Check for italic *text* + if chars[i] == '*' { + if let Some(end) = find_matching(&chars, i + 1, "*") { + let italic_text: String = chars[i + 1..end].iter().collect(); + let mut formatted_text = italic_text.italic().to_string(); + apply_color_stack(&mut formatted_text, &color_stack); + result.push_str(&formatted_text); + i = end + 1; + continue; + } + } + + // Check for underline _text_ + if chars[i] == '_' { + if let Some(end) = find_matching(&chars, i + 1, "_") { + let underline_text: String = chars[i + 1..end].iter().collect(); + let mut formatted_text = format!("\x1b[4m{}\x1b[0m", underline_text); + apply_color_stack(&mut formatted_text, &color_stack); + result.push_str(&formatted_text); + i = end + 1; + continue; + } + } + + // Check for angle-bracketed content <text> + if chars[i] == '<' { + if let Some(end) = find_matching(&chars, i + 1, ">") { + // Include the angle brackets in the output + let angle_text: String = chars[i..=end].iter().collect(); + let mut formatted_text = angle_text.cyan().to_string(); + apply_color_stack(&mut formatted_text, &color_stack); + result.push_str(&formatted_text); + i = end + 1; + continue; + } + } + + // Check for inline code `text` + if chars[i] == '`' { + if let Some(end) = find_matching(&chars, i + 1, "`") { + // Include the backticks in the output + let code_text: String = chars[i..=end].iter().collect(); + let mut formatted_text = code_text.green().to_string(); + apply_color_stack(&mut formatted_text, &color_stack); + result.push_str(&formatted_text); + i = end + 1; + continue; + } + } + + // Regular character + let mut current_char = chars[i].to_string(); + apply_color_stack(&mut current_char, &color_stack); + result.push_str(¤t_char); + i += 1; + } + + result +} + +// Helper function to find matching delimiter +fn find_matching(chars: &[char], start: usize, delimiter: &str) -> Option<usize> { + let delim_chars: Vec<char> = delimiter.chars().collect(); + let delim_len = delim_chars.len(); + + let mut j = start; + while j < chars.len() { + if delim_len == 1 { + if chars[j] == delim_chars[0] { + return Some(j); + } + } else if j + 1 < chars.len() + && chars[j] == delim_chars[0] + && chars[j + 1] == delim_chars[1] + { + return Some(j); + } + j += 1; + } + None +} + +// Helper function to find color tag end +fn find_tag_end(chars: &[char], start: usize) -> Option<usize> { + let mut j = start + 2; + while j + 1 < chars.len() { + if chars[j] == ']' && chars[j + 1] == ']' { + return Some(j); + } + j += 1; + } + None +} + +// Helper function to apply color stack to text +fn apply_color_stack(text: &mut String, color_stack: &VecDeque<String>) { + let mut result = text.clone(); + for color in color_stack.iter().rev() { + result = apply_color(&result, color); + } + *text = result; +} + +// Helper function to apply color to text +fn apply_color(text: impl AsRef<str>, color_name: impl AsRef<str>) -> String { + let text = text.as_ref(); + let color_name = color_name.as_ref(); + match color_name { + // Normal colors + "black" => text.dark_grey().to_string(), + "red" => text.dark_red().to_string(), + "green" => text.dark_green().to_string(), + "yellow" => text.dark_yellow().to_string(), + "blue" => text.dark_blue().to_string(), + "magenta" => text.dark_magenta().to_string(), + "cyan" => text.dark_cyan().to_string(), + "white" => text.white().to_string(), + "bright_black" => text.black().to_string(), + "bright_red" => text.red().to_string(), + "bright_green" => text.green().to_string(), + "bright_yellow" => text.yellow().to_string(), + "bright_blue" => text.blue().to_string(), + "bright_magenta" => text.magenta().to_string(), + "bright_cyan" => text.cyan().to_string(), + "bright_white" => text.white().to_string(), + + // Short aliases for bright colors + "b_black" => text.black().to_string(), + "b_red" => text.red().to_string(), + "b_green" => text.green().to_string(), + "b_yellow" => text.yellow().to_string(), + "b_blue" => text.blue().to_string(), + "b_magenta" => text.magenta().to_string(), + "b_cyan" => text.cyan().to_string(), + "b_white" => text.white().to_string(), + + // Gray colors using truecolor + "gray" | "grey" => text.grey().to_string(), + "bright_gray" | "bright_grey" => text.white().to_string(), + "b_gray" | "b_grey" => text.white().to_string(), + + // Default to white if color not recognized + _ => text.to_string(), + } +} diff --git a/utils/src/display/table.rs b/utils/src/display/table.rs new file mode 100644 index 0000000..ae745d8 --- /dev/null +++ b/utils/src/display/table.rs @@ -0,0 +1,143 @@ +pub struct Table { + items: Vec<String>, + line: Vec<Vec<String>>, + length: Vec<usize>, + padding: usize, +} + +impl Table { + /// Create a new Table + pub fn new(items: Vec<impl Into<String>>) -> Self { + Self::new_with_padding(items, 2) + } + + /// Create a new Table with padding + pub fn new_with_padding(items: Vec<impl Into<String>>, padding: usize) -> Self { + let items: Vec<String> = 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)); + } + + Table { + items, + padding, + line: Vec::new(), + length, + } + } + + /// Push a new row of items to the table + pub fn push_item(&mut self, items: Vec<impl Into<String>>) { + let items: Vec<String> = 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<impl Into<String>>) { + let items: Vec<String> = 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 Table { + 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<String> = 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<String> = 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 +} diff --git a/utils/src/legacy.rs b/utils/src/legacy.rs new file mode 100644 index 0000000..682c679 --- /dev/null +++ b/utils/src/legacy.rs @@ -0,0 +1,9 @@ +pub mod display; +pub mod env; +pub mod fs; +pub mod globber; +pub mod input; +pub mod levenshtein_distance; +pub mod logger; +pub mod push_version; +pub mod socket_addr_helper; diff --git a/utils/src/legacy/display.rs b/utils/src/legacy/display.rs new file mode 100644 index 0000000..fc94d90 --- /dev/null +++ b/utils/src/legacy/display.rs @@ -0,0 +1,488 @@ +use colored::*; +use just_enough_vcs::lib::data::sheet::SheetMappingMetadata; +use std::{ + collections::{BTreeMap, HashMap, VecDeque}, + path::PathBuf, +}; + +pub struct SimpleTable { + items: Vec<String>, + line: Vec<Vec<String>>, + length: Vec<usize>, + padding: usize, +} + +impl SimpleTable { + /// Create a new Table + pub fn new(items: Vec<impl Into<String>>) -> Self { + Self::new_with_padding(items, 2) + } + + /// Create a new Table with padding + pub fn new_with_padding(items: Vec<impl Into<String>>, padding: usize) -> Self { + let items: Vec<String> = 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<impl Into<String>>) { + let items: Vec<String> = 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<impl Into<String>>) { + let items: Vec<String> = 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<String> = 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<String> = 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<str>) -> String { + let text = text.as_ref().trim(); + let mut result = String::new(); + let mut color_stack: VecDeque<String> = VecDeque::new(); + + let mut i = 0; + let chars: Vec<char> = 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 <text> + 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<str>, color_name: impl AsRef<str>) -> 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<PathBuf, SheetMappingMetadata>) -> 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<String> = 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<String, TreeNode>, // 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); + } +} diff --git a/utils/src/env.rs b/utils/src/legacy/env.rs index 1834cd3..1834cd3 100644 --- a/utils/src/env.rs +++ b/utils/src/legacy/env.rs diff --git a/utils/src/fs.rs b/utils/src/legacy/fs.rs index 0050cf1..0050cf1 100644 --- a/utils/src/fs.rs +++ b/utils/src/legacy/fs.rs diff --git a/utils/src/globber.rs b/utils/src/legacy/globber.rs index 7021898..4d722db 100644 --- a/utils/src/globber.rs +++ b/utils/src/legacy/globber.rs @@ -2,7 +2,7 @@ use std::{io::Error, path::PathBuf, str::FromStr}; use just_fmt::fmt_path::fmt_path_str; -use crate::globber::constants::{SPLIT_STR, get_base_dir_current}; +use crate::legacy::globber::constants::{SPLIT_STR, get_base_dir_current}; pub struct Globber { pattern: String, diff --git a/utils/src/input.rs b/utils/src/legacy/input.rs index bc67d90..95d53cb 100644 --- a/utils/src/input.rs +++ b/utils/src/legacy/input.rs @@ -1,6 +1,6 @@ use tokio::{fs, process::Command}; -use crate::env::get_default_editor; +use crate::legacy::env::get_default_editor; /// Confirm the current operation /// Waits for user input of 'y' or 'n' diff --git a/utils/src/levenshtein_distance.rs b/utils/src/legacy/levenshtein_distance.rs index 6bdb7e7..6bdb7e7 100644 --- a/utils/src/levenshtein_distance.rs +++ b/utils/src/legacy/levenshtein_distance.rs diff --git a/utils/src/logger.rs b/utils/src/legacy/logger.rs index 1bc96c1..1bc96c1 100644 --- a/utils/src/logger.rs +++ b/utils/src/legacy/logger.rs diff --git a/utils/src/push_version.rs b/utils/src/legacy/push_version.rs index 6da9039..6da9039 100644 --- a/utils/src/push_version.rs +++ b/utils/src/legacy/push_version.rs diff --git a/utils/src/socket_addr_helper.rs b/utils/src/legacy/socket_addr_helper.rs index 29ccd9f..29ccd9f 100644 --- a/utils/src/socket_addr_helper.rs +++ b/utils/src/legacy/socket_addr_helper.rs diff --git a/utils/src/lib.rs b/utils/src/lib.rs index ef56189..ca2be9c 100644 --- a/utils/src/lib.rs +++ b/utils/src/lib.rs @@ -1,10 +1,6 @@ pub mod display; -pub mod env; -pub mod fs; -pub mod globber; -pub mod input; -pub mod lazy_macros; -pub mod levenshtein_distance; -pub mod logger; -pub mod push_version; -pub mod socket_addr_helper; +pub mod macros; +pub mod math; + +// Legacy +pub mod legacy; diff --git a/utils/src/lazy_macros.rs b/utils/src/macros.rs index f1cb75e..f1cb75e 100644 --- a/utils/src/lazy_macros.rs +++ b/utils/src/macros.rs diff --git a/utils/src/math.rs b/utils/src/math.rs new file mode 100644 index 0000000..42a44a1 --- /dev/null +++ b/utils/src/math.rs @@ -0,0 +1 @@ +pub mod levenshtein_distance; diff --git a/utils/src/math/levenshtein_distance.rs b/utils/src/math/levenshtein_distance.rs new file mode 100644 index 0000000..98caa20 --- /dev/null +++ b/utils/src/math/levenshtein_distance.rs @@ -0,0 +1,38 @@ +use std::cmp::min; + +pub fn levenshtein_distance(a: &str, b: &str) -> usize { + let a_len = a.chars().count(); + let b_len = b.chars().count(); + + if a_len == 0 { + return b_len; + } + if b_len == 0 { + return a_len; + } + + let mut prev_row: Vec<usize> = (0..=b_len).collect(); + let mut curr_row = vec![0; b_len + 1]; + + let mut a_chars = a.chars(); + + for i in 1..=a_len { + let a_char = a_chars.next().unwrap(); + curr_row[0] = i; + + let mut b_chars = b.chars(); + for j in 1..=b_len { + let b_char = b_chars.next().unwrap(); + + let cost = if a_char == b_char { 0 } else { 1 }; + curr_row[j] = min( + prev_row[j] + 1, + min(curr_row[j - 1] + 1, prev_row[j - 1] + cost), + ); + } + + std::mem::swap(&mut prev_row, &mut curr_row); + } + + prev_row[b_len] +} |
