From 0a95bae451c1847f4f0b9601e60959f4e8e6b669 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Thu, 12 Mar 2026 14:28:08 +0800 Subject: Refactor display utilities --- utils/src/display/colorful.rs | 275 ++++++++++++++++++++++++++++++++++++++++++ utils/src/display/table.rs | 143 ++++++++++++++++++++++ 2 files changed, 418 insertions(+) create mode 100644 utils/src/display/colorful.rs create mode 100644 utils/src/display/table.rs (limited to 'utils/src/display') 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: `` (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` +/// +/// # 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) -> String { + let text = text.as_ref().trim(); + let mut result = String::new(); + let mut color_stack: VecDeque = VecDeque::new(); + + let chars: Vec = 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 + 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 { + let delim_chars: Vec = 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 { + 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) { + 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, color_name: impl AsRef) -> 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, + line: Vec>, + length: Vec, + padding: usize, +} + +impl Table { + /// 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)); + } + + Table { + 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 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 = 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 +} -- cgit