summaryrefslogtreecommitdiff
path: root/utils
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-03-12 14:28:08 +0800
committer魏曹先生 <1992414357@qq.com>2026-03-12 14:28:08 +0800
commit0a95bae451c1847f4f0b9601e60959f4e8e6b669 (patch)
tree9e1cfad4f86a73176a4d738b28e7732b66fe5f97 /utils
parent8564c8f2177dec0c2c0c031d156347fa6b4485bc (diff)
Refactor display utilities
Diffstat (limited to 'utils')
-rw-r--r--utils/Cargo.toml24
-rw-r--r--utils/src/display.rs490
-rw-r--r--utils/src/display/colorful.rs275
-rw-r--r--utils/src/display/table.rs143
-rw-r--r--utils/src/legacy.rs9
-rw-r--r--utils/src/legacy/display.rs488
-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.rs14
-rw-r--r--utils/src/macros.rs (renamed from utils/src/lazy_macros.rs)0
-rw-r--r--utils/src/math.rs1
-rw-r--r--utils/src/math/levenshtein_distance.rs38
18 files changed, 971 insertions, 515 deletions
diff --git a/utils/Cargo.toml b/utils/Cargo.toml
index c055c07..e4cc3a0 100644
--- a/utils/Cargo.toml
+++ b/utils/Cargo.toml
@@ -4,23 +4,15 @@ edition = "2024"
version.workspace = true
[dependencies]
-# Just Enough VCS
just_enough_vcs = { path = "../../VersionControl", features = ["all"] }
-# Display
-colored = "3.0"
-strip-ansi-escapes = "0.2.1"
-just_fmt = "0.1.2"
-
-# Async
-tokio = { version = "1", features = ["full"] }
+colored.workspace = true
+crossterm.workspace = true
+env_logger.workspace = true
+just_fmt.workspace = true
+log.workspace = true
-# Logging
-log = "0.4"
-env_logger = "0.11"
-
-# File & Directory
-dirs = "6.0.0"
-
-# Time
chrono = "0.4"
+dirs = "6.0.0"
+strip-ansi-escapes = "0.2.1"
+tokio = { version = "1", features = ["fs", "io-std", "net"] }
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(&current_char, color);
- }
-
- result.push_str(&current_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(&current_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(&current_char, color);
+ }
+
+ result.push_str(&current_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]
+}