summaryrefslogtreecommitdiff
path: root/utils/src/legacy
diff options
context:
space:
mode:
Diffstat (limited to 'utils/src/legacy')
-rw-r--r--utils/src/legacy/display.rs488
-rw-r--r--utils/src/legacy/env.rs107
-rw-r--r--utils/src/legacy/fs.rs40
-rw-r--r--utils/src/legacy/globber.rs279
-rw-r--r--utils/src/legacy/input.rs151
-rw-r--r--utils/src/legacy/levenshtein_distance.rs34
-rw-r--r--utils/src/legacy/logger.rs86
-rw-r--r--utils/src/legacy/push_version.rs30
-rw-r--r--utils/src/legacy/socket_addr_helper.rs194
9 files changed, 1409 insertions, 0 deletions
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/legacy/env.rs b/utils/src/legacy/env.rs
new file mode 100644
index 0000000..1834cd3
--- /dev/null
+++ b/utils/src/legacy/env.rs
@@ -0,0 +1,107 @@
+use std::path::PathBuf;
+
+/// Returns the current locale string based on environment variables.
+///
+/// The function checks for locale settings in the following order:
+/// 1. JV_LANG environment variable
+/// 2. APP_LANG environment variable
+/// 3. LANG environment variable (extracts base language before dot and replaces underscores with hyphens)
+/// 4. Defaults to "en" if no locale environment variables are found
+///
+/// # Returns
+/// A String containing the detected locale code
+pub fn current_locales() -> String {
+ if let Ok(lang) = std::env::var("JV_LANG") {
+ return lang;
+ }
+
+ if let Ok(lang) = std::env::var("APP_LANG") {
+ return lang;
+ }
+
+ if let Ok(lang) = std::env::var("LANG") {
+ if let Some(base_lang) = lang.split('.').next() {
+ return base_lang.replace('_', "-");
+ }
+ return lang;
+ }
+
+ "en".to_string()
+}
+
+/// Checks if auto update is enabled based on environment variables.
+///
+/// The function checks the JV_AUTO_UPDATE environment variable and compares
+/// its value (after trimming and converting to lowercase) against known
+/// positive and negative values.
+///
+/// # Returns
+/// `true` if the value matches "yes", "y", or "true"
+/// `false` if the value matches "no", "n", or "false", or if the variable is not set
+pub fn enable_auto_update() -> bool {
+ if let Ok(auto_update) = std::env::var("JV_AUTO_UPDATE") {
+ let normalized = auto_update.trim().to_lowercase();
+ match normalized.as_str() {
+ "yes" | "y" | "true" => return true,
+ "no" | "n" | "false" => return false,
+ _ => {}
+ }
+ }
+ false
+}
+
+/// Gets the auto update expiration time based on environment variables.
+///
+/// The function checks the JV_OUTDATED_MINUTES environment variable.
+/// Requires JV_AUTO_UPDATE to be enabled.
+/// Next time the `jv` command is used, if the content is outdated, `jv update` will be automatically executed.
+///
+/// # Returns
+/// - When the set number is < 0, timeout-based update is disabled
+/// - When the set number = 0, update runs every time (not recommended)
+/// - When the set number > 0, update according to the specified time
+/// - If not set or conversion error occurs, the default is -1
+pub fn auto_update_outdate() -> i64 {
+ if !enable_auto_update() {
+ return -1;
+ }
+
+ match std::env::var("JV_OUTDATED_MINUTES") {
+ Ok(value) => match value.trim().parse::<i64>() {
+ Ok(num) => num,
+ Err(_) => -1,
+ },
+ Err(_) => -1,
+ }
+}
+
+/// Gets the default text editor based on environment variables.
+///
+/// The function checks the JV_TEXT_EDITOR and EDITOR environment variables
+/// and returns their values if they are set. If neither variable is set,
+/// it returns "jvii" as the default editor.
+///
+/// # Returns
+/// A String containing the default text editor
+pub async fn get_default_editor() -> String {
+ if let Ok(editor) = std::env::var("JV_TEXT_EDITOR") {
+ return editor;
+ }
+
+ if let Ok(editor) = std::env::var("EDITOR") {
+ return editor;
+ }
+
+ "jvii".to_string()
+}
+
+/// Get temporary file path
+pub fn current_tempfile_path(name: &str) -> Option<PathBuf> {
+ dirs::config_local_dir().map(|path| {
+ if cfg!(target_os = "linux") {
+ path.join("jvcs").join(".temp").join(name)
+ } else {
+ path.join("JustEnoughVCS").join(".temp").join(name)
+ }
+ })
+}
diff --git a/utils/src/legacy/fs.rs b/utils/src/legacy/fs.rs
new file mode 100644
index 0000000..0050cf1
--- /dev/null
+++ b/utils/src/legacy/fs.rs
@@ -0,0 +1,40 @@
+pub async fn move_across_partitions(
+ source_path: impl AsRef<std::path::Path>,
+ dest_path: impl AsRef<std::path::Path>,
+) -> Result<(), std::io::Error> {
+ let source_path = source_path.as_ref();
+ let dest_path = dest_path.as_ref();
+ if !source_path.exists() {
+ return Err(std::io::Error::new(
+ std::io::ErrorKind::NotFound,
+ "Source file does not exist",
+ ));
+ }
+
+ if let Ok(()) = std::fs::rename(source_path, dest_path) {
+ return Ok(());
+ }
+
+ std::fs::copy(source_path, dest_path)?;
+ std::fs::remove_file(source_path)?;
+
+ Ok(())
+}
+
+pub async fn copy_across_partitions(
+ source_path: impl AsRef<std::path::Path>,
+ dest_path: impl AsRef<std::path::Path>,
+) -> Result<(), std::io::Error> {
+ let source_path = source_path.as_ref();
+ let dest_path = dest_path.as_ref();
+ if !source_path.exists() {
+ return Err(std::io::Error::new(
+ std::io::ErrorKind::NotFound,
+ "Source file does not exist",
+ ));
+ }
+
+ std::fs::copy(source_path, dest_path)?;
+
+ Ok(())
+}
diff --git a/utils/src/legacy/globber.rs b/utils/src/legacy/globber.rs
new file mode 100644
index 0000000..4d722db
--- /dev/null
+++ b/utils/src/legacy/globber.rs
@@ -0,0 +1,279 @@
+use std::{io::Error, path::PathBuf, str::FromStr};
+
+use just_fmt::fmt_path::fmt_path_str;
+
+use crate::legacy::globber::constants::{SPLIT_STR, get_base_dir_current};
+
+pub struct Globber {
+ pattern: String,
+ base: PathBuf,
+ names: Vec<String>,
+}
+
+#[allow(dead_code)]
+impl Globber {
+ pub fn new(pattern: String, base: PathBuf) -> Self {
+ Self {
+ pattern,
+ base,
+ names: Vec::new(),
+ }
+ }
+
+ pub fn names(&self) -> Vec<&String> {
+ self.names.iter().collect()
+ }
+
+ pub fn base(&self) -> &PathBuf {
+ &self.base
+ }
+
+ pub fn into_names(self) -> Vec<String> {
+ self.names
+ }
+
+ pub fn paths(&self) -> Vec<PathBuf> {
+ self.names.iter().map(|n| self.base.join(n)).collect()
+ }
+
+ pub fn glob<F>(mut self, get_names: F) -> Result<Self, std::io::Error>
+ where
+ F: Fn(PathBuf) -> Vec<GlobItem>,
+ {
+ let full_path = format!("{}{}{}", self.base.display(), SPLIT_STR, self.pattern);
+
+ let (path, pattern) = if let Some(last_split) = full_path.rfind(SPLIT_STR) {
+ let (path_part, pattern_part) = full_path.split_at(last_split);
+ let mut path = path_part.to_string();
+ if !path.ends_with(SPLIT_STR) {
+ path.push_str(SPLIT_STR);
+ }
+ let Ok(result) = fmt_path_str(&path) else {
+ return Err(Error::new(
+ std::io::ErrorKind::InvalidInput,
+ format!("Invalid path: \"{}\"", &path),
+ ));
+ };
+ (result, pattern_part[SPLIT_STR.len()..].to_string())
+ } else {
+ (String::default(), full_path)
+ };
+
+ self.base = match PathBuf::from_str(&path) {
+ Ok(r) => r,
+ Err(_) => {
+ return Err(Error::new(
+ std::io::ErrorKind::InvalidInput,
+ format!("Invalid path: \"{}\"", &path),
+ ));
+ }
+ };
+
+ let pattern = if pattern.is_empty() {
+ "*".to_string()
+ } else if pattern == "." {
+ "*".to_string()
+ } else if pattern.ends_with(SPLIT_STR) {
+ format!("{}*", pattern)
+ } else {
+ pattern
+ };
+
+ if !pattern.contains('*') && !pattern.contains('?') {
+ self.names = vec![pattern];
+ return Ok(self);
+ }
+
+ let mut collected = Vec::new();
+
+ collect_files(&path.into(), "./".to_string(), &mut collected, &get_names);
+ fn collect_files<F>(
+ base: &PathBuf,
+ current: String,
+ file_names: &mut Vec<String>,
+ get_names: &F,
+ ) where
+ F: Fn(PathBuf) -> Vec<GlobItem>,
+ {
+ let current_path = if current.is_empty() {
+ base.clone()
+ } else {
+ base.join(&current)
+ };
+
+ let items = get_names(current_path);
+ for item in items {
+ match item {
+ GlobItem::File(file_name) => {
+ let relative_path = {
+ fmt_path_str(format!("{}{}{}", current, SPLIT_STR, file_name))
+ .unwrap_or_default()
+ };
+ file_names.push(relative_path)
+ }
+ GlobItem::Directory(dir_name) => {
+ let new_current = {
+ fmt_path_str(format!("{}{}{}", current, SPLIT_STR, dir_name))
+ .unwrap_or_default()
+ };
+ collect_files(base, new_current, file_names, get_names);
+ }
+ }
+ }
+ }
+
+ self.names = collected
+ .iter()
+ .filter_map(|name| match_pattern(name, &pattern))
+ .collect();
+
+ Ok(self)
+ }
+}
+
+fn match_pattern(name: &str, pattern: &str) -> Option<String> {
+ if pattern.is_empty() {
+ return None;
+ }
+
+ let name_chars: Vec<char> = name.chars().collect();
+ let pattern_chars: Vec<char> = pattern.chars().collect();
+
+ let mut name_idx = 0;
+ let mut pattern_idx = 0;
+ let mut star_idx = -1;
+ let mut match_idx = -1;
+
+ while name_idx < name_chars.len() {
+ if pattern_idx < pattern_chars.len()
+ && (pattern_chars[pattern_idx] == '?'
+ || pattern_chars[pattern_idx] == name_chars[name_idx])
+ {
+ name_idx += 1;
+ pattern_idx += 1;
+ } else if pattern_idx < pattern_chars.len() && pattern_chars[pattern_idx] == '*' {
+ star_idx = pattern_idx as i32;
+ match_idx = name_idx as i32;
+ pattern_idx += 1;
+ } else if star_idx != -1 {
+ pattern_idx = (star_idx + 1) as usize;
+ match_idx += 1;
+ name_idx = match_idx as usize;
+ } else {
+ return None;
+ }
+ }
+
+ while pattern_idx < pattern_chars.len() && pattern_chars[pattern_idx] == '*' {
+ pattern_idx += 1;
+ }
+
+ if pattern_idx == pattern_chars.len() {
+ Some(name.to_string())
+ } else {
+ None
+ }
+}
+
+impl<T: AsRef<str>> From<T> for Globber {
+ fn from(pattern: T) -> Self {
+ let (base_dir, pattern) = get_base_dir_current(pattern.as_ref().to_string());
+ Self::new(pattern, base_dir)
+ }
+}
+
+#[derive(Debug, Clone, Hash)]
+pub enum GlobItem {
+ File(String),
+ Directory(String),
+}
+
+impl PartialEq for GlobItem {
+ fn eq(&self, other: &Self) -> bool {
+ match (self, other) {
+ (GlobItem::File(a), GlobItem::File(b)) => a == b,
+ (GlobItem::Directory(a), GlobItem::Directory(b)) => a == b,
+ _ => false,
+ }
+ }
+}
+
+impl std::fmt::Display for GlobItem {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ GlobItem::File(name) => write!(f, "{}", name),
+ GlobItem::Directory(name) => write!(f, "{}", name),
+ }
+ }
+}
+
+impl Eq for GlobItem {}
+
+pub mod constants {
+ use std::{env::current_dir, path::PathBuf};
+
+ #[cfg(unix)]
+ pub(crate) const CURRENT_DIR_PREFIX: &str = "./";
+ #[cfg(windows)]
+ pub(crate) const CURRENT_DIR_PREFIX: &str = ".\\";
+
+ #[cfg(unix)]
+ pub(crate) const USER_DIR_PREFIX: &str = "~";
+ #[cfg(windows)]
+ pub(crate) const USER_DIR_PREFIX: &str = "~\\";
+
+ #[cfg(unix)]
+ pub(crate) const ROOT_DIR_PREFIX: &str = "/";
+ #[cfg(windows)]
+ pub(crate) const ROOT_DIR_PREFIX: &str = "\\";
+
+ #[cfg(unix)]
+ pub(crate) const SPLIT_STR: &str = "/";
+ #[cfg(windows)]
+ pub(crate) const SPLIT_STR: &str = "\\";
+
+ pub fn get_base_dir_current(input: String) -> (PathBuf, String) {
+ get_base_dir(input, current_dir().unwrap_or_default())
+ }
+
+ pub fn get_base_dir(input: String, current_dir: PathBuf) -> (PathBuf, String) {
+ if let Some(remaining) = input.strip_prefix(CURRENT_DIR_PREFIX) {
+ (current_dir, remaining.to_string())
+ } else if let Some(remaining) = input.strip_prefix(USER_DIR_PREFIX) {
+ (dirs::home_dir().unwrap_or_default(), remaining.to_string())
+ } else if let Some(remaining) = input.strip_prefix(ROOT_DIR_PREFIX) {
+ {
+ #[cfg(unix)]
+ {
+ (PathBuf::from(ROOT_DIR_PREFIX), remaining.to_string())
+ }
+ #[cfg(windows)]
+ {
+ let current_drive = current_dir
+ .components()
+ .find_map(|comp| {
+ if let std::path::Component::Prefix(prefix_component) = comp {
+ Some(prefix_component)
+ } else {
+ None
+ }
+ })
+ .and_then(|prefix_component| match prefix_component.kind() {
+ std::path::Prefix::Disk(drive_letter)
+ | std::path::Prefix::VerbatimDisk(drive_letter) => {
+ Some((drive_letter as char).to_string())
+ }
+ _ => None,
+ })
+ .unwrap_or_else(|| "C".to_string());
+ (
+ PathBuf::from(format!("{}:{}", current_drive, ROOT_DIR_PREFIX)),
+ remaining.to_string(),
+ )
+ }
+ }
+ } else {
+ (current_dir, input)
+ }
+ }
+}
diff --git a/utils/src/legacy/input.rs b/utils/src/legacy/input.rs
new file mode 100644
index 0000000..95d53cb
--- /dev/null
+++ b/utils/src/legacy/input.rs
@@ -0,0 +1,151 @@
+use tokio::{fs, process::Command};
+
+use crate::legacy::env::get_default_editor;
+
+/// Confirm the current operation
+/// Waits for user input of 'y' or 'n'
+pub async fn confirm_hint(text: impl Into<String>) -> bool {
+ use tokio::io::{self, AsyncBufReadExt, AsyncWriteExt, BufReader};
+
+ let prompt = text.into().trim().to_string();
+
+ let mut stdout = io::stdout();
+ let mut stdin = BufReader::new(io::stdin());
+
+ stdout
+ .write_all(prompt.as_bytes())
+ .await
+ .expect("Failed to write prompt");
+ stdout.flush().await.expect("Failed to flush stdout");
+
+ let mut input = String::new();
+ stdin
+ .read_line(&mut input)
+ .await
+ .expect("Failed to read input");
+
+ input.trim().eq_ignore_ascii_case("y")
+}
+
+/// Confirm the current operation, or execute a closure if rejected
+/// Waits for user input of 'y' or 'n'
+/// If 'n' is entered, executes the provided closure and returns false
+pub async fn confirm_hint_or<F>(text: impl Into<String>, on_reject: F) -> bool
+where
+ F: FnOnce(),
+{
+ let confirmed = confirm_hint(text).await;
+ if !confirmed {
+ on_reject();
+ }
+ confirmed
+}
+
+/// Confirm the current operation, and execute a closure if confirmed
+/// Waits for user input of 'y' or 'n'
+/// If 'y' is entered, executes the provided closure and returns true
+pub async fn confirm_hint_then<F>(text: impl Into<String>, on_confirm: F) -> bool
+where
+ F: FnOnce(),
+{
+ let confirmed = confirm_hint(text).await;
+ if confirmed {
+ on_confirm();
+ }
+ confirmed
+}
+
+/// Input text using the system editor
+/// Opens the system editor (from EDITOR environment variable) with default text in a cache file,
+/// then reads back the modified content after the editor closes, removing comment lines
+pub async fn input_with_editor(
+ default_text: impl AsRef<str>,
+ cache_file: impl AsRef<std::path::Path>,
+ comment_char: impl AsRef<str>,
+) -> Result<String, std::io::Error> {
+ input_with_editor_cutsom(
+ default_text,
+ cache_file,
+ comment_char,
+ get_default_editor().await,
+ )
+ .await
+}
+
+pub async fn input_with_editor_cutsom(
+ default_text: impl AsRef<str>,
+ cache_file: impl AsRef<std::path::Path>,
+ comment_char: impl AsRef<str>,
+ editor: String,
+) -> Result<String, std::io::Error> {
+ let cache_path = cache_file.as_ref();
+ let default_content = default_text.as_ref();
+ let comment_prefix = comment_char.as_ref();
+
+ // Write default text to cache file
+ fs::write(cache_path, default_content).await?;
+
+ // Open editor with cache file
+ let status = Command::new(editor).arg(cache_path).status().await?;
+
+ if !status.success() {
+ return Err(std::io::Error::new(
+ std::io::ErrorKind::Other,
+ "Editor exited with non-zero status",
+ ));
+ }
+
+ // Read the modified content
+ let content = fs::read_to_string(cache_path).await?;
+
+ // Remove comment lines and trim
+ let processed_content: String = content
+ .lines()
+ .filter_map(|line| {
+ let trimmed = line.trim();
+ if trimmed.starts_with(comment_prefix) {
+ None
+ } else {
+ Some(line)
+ }
+ })
+ .collect::<Vec<&str>>()
+ .join("\n");
+
+ // Delete the cache file
+ let _ = fs::remove_file(cache_path).await;
+
+ Ok(processed_content)
+}
+
+/// Show text using the system pager (less)
+/// Opens the system pager (less) with the given text content written to the specified file
+/// If less is not found, directly outputs the content to stdout
+pub async fn show_in_pager(
+ content: impl AsRef<str>,
+ cache_file: impl AsRef<std::path::Path>,
+) -> Result<(), std::io::Error> {
+ let content_str = content.as_ref();
+ let cache_path = cache_file.as_ref();
+
+ // Write content to cache file
+ fs::write(cache_path, content_str).await?;
+
+ // Try to use less first
+ let status = Command::new("less").arg(cache_path).status().await;
+
+ match status {
+ Ok(status) if status.success() => Ok(()),
+ _ => {
+ // If less failed, output directly to stdout
+ use tokio::io::{self, AsyncWriteExt};
+ let mut stdout = io::stdout();
+ stdout
+ .write_all(content_str.as_bytes())
+ .await
+ .expect("Failed to write content");
+ stdout.flush().await.expect("Failed to flush stdout");
+ Ok(())
+ }
+ }
+}
diff --git a/utils/src/legacy/levenshtein_distance.rs b/utils/src/legacy/levenshtein_distance.rs
new file mode 100644
index 0000000..6bdb7e7
--- /dev/null
+++ b/utils/src/legacy/levenshtein_distance.rs
@@ -0,0 +1,34 @@
+use std::cmp::min;
+
+pub fn levenshtein_distance(a: &str, b: &str) -> usize {
+ let a_chars: Vec<char> = a.chars().collect();
+ let b_chars: Vec<char> = b.chars().collect();
+ let a_len = a_chars.len();
+ let b_len = b_chars.len();
+
+ if a_len == 0 {
+ return b_len;
+ }
+ if b_len == 0 {
+ return a_len;
+ }
+
+ let mut dp = vec![vec![0; b_len + 1]; a_len + 1];
+
+ for (i, row) in dp.iter_mut().enumerate() {
+ row[0] = i;
+ }
+
+ for (j, cell) in dp[0].iter_mut().enumerate() {
+ *cell = j;
+ }
+
+ for (i, a_char) in a_chars.iter().enumerate() {
+ for (j, b_char) in b_chars.iter().enumerate() {
+ let cost = if a_char == b_char { 0 } else { 1 };
+ dp[i + 1][j + 1] = min(dp[i][j + 1] + 1, min(dp[i + 1][j] + 1, dp[i][j] + cost));
+ }
+ }
+
+ dp[a_len][b_len]
+}
diff --git a/utils/src/legacy/logger.rs b/utils/src/legacy/logger.rs
new file mode 100644
index 0000000..1bc96c1
--- /dev/null
+++ b/utils/src/legacy/logger.rs
@@ -0,0 +1,86 @@
+use std::path::Path;
+
+use colored::Colorize;
+use env_logger::{Builder, Target};
+use just_enough_vcs::lib::data::vault::vault_config::LoggerLevel;
+use just_fmt::fmt_path::fmt_path;
+use log::{Level, LevelFilter};
+
+pub fn build_env_logger(log_path: impl AsRef<Path>, logger_level: LoggerLevel) {
+ use std::io::{self, Write};
+
+ struct MultiWriter<A, B> {
+ a: A,
+ b: B,
+ }
+
+ impl<A: Write, B: Write> MultiWriter<A, B> {
+ fn new(a: A, b: B) -> Self {
+ Self { a, b }
+ }
+ }
+
+ impl<A: Write, B: Write> Write for MultiWriter<A, B> {
+ fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+ let _ = self.a.write(buf);
+ self.b.write(buf)
+ }
+
+ fn flush(&mut self) -> io::Result<()> {
+ let _ = self.a.flush();
+ self.b.flush()
+ }
+ }
+
+ let log_path = {
+ let path = log_path.as_ref();
+ let Ok(path) = fmt_path(path) else {
+ eprintln!(
+ "Build logger failed: {} is not a vaild path.",
+ path.display()
+ );
+ return;
+ };
+ path
+ };
+
+ let mut builder = Builder::new();
+
+ let log_format = |buf: &mut env_logger::fmt::Formatter, record: &log::Record| {
+ let now = chrono::Local::now();
+
+ let level_style = match record.level() {
+ Level::Error => record.args().to_string().red().bold(),
+ Level::Warn => record.args().to_string().yellow().bold(),
+ Level::Info => record.args().to_string().white(),
+ Level::Debug => record.args().to_string().white(),
+ Level::Trace => record.args().to_string().cyan(),
+ };
+
+ writeln!(
+ buf,
+ "{} {}",
+ now.format("%H:%M:%S")
+ .to_string()
+ .truecolor(105, 105, 105)
+ .bold(),
+ level_style
+ )
+ };
+
+ let log_file = std::fs::File::create(log_path).expect("Failed to create log file");
+ let combined_target = Target::Pipe(Box::new(MultiWriter::new(std::io::stdout(), log_file)));
+
+ let level = match logger_level {
+ LoggerLevel::Debug => LevelFilter::Debug,
+ LoggerLevel::Trace => LevelFilter::Trace,
+ LoggerLevel::Info => LevelFilter::Info,
+ };
+
+ builder
+ .format(log_format)
+ .filter(None, level.clone())
+ .filter_module("just_enough_vcs", level)
+ .target(combined_target)
+ .init();
+}
diff --git a/utils/src/legacy/push_version.rs b/utils/src/legacy/push_version.rs
new file mode 100644
index 0000000..6da9039
--- /dev/null
+++ b/utils/src/legacy/push_version.rs
@@ -0,0 +1,30 @@
+pub fn push_version(current_version: impl Into<String>) -> Option<String> {
+ let version_str = current_version.into();
+ let parts: Vec<&str> = version_str.split('.').collect();
+
+ if parts.len() != 3 {
+ return None;
+ }
+
+ let major: Result<u32, _> = parts[0].parse();
+ let minor: Result<u32, _> = parts[1].parse();
+ let patch: Result<u32, _> = parts[2].parse();
+
+ if let (Ok(mut major), Ok(mut minor), Ok(mut patch)) = (major, minor, patch) {
+ patch += 1;
+
+ if patch > 99 {
+ patch = 0;
+ minor += 1;
+
+ if minor > 99 {
+ minor = 0;
+ major += 1;
+ }
+ }
+
+ Some(format!("{}.{}.{}", major, minor, patch))
+ } else {
+ None
+ }
+}
diff --git a/utils/src/legacy/socket_addr_helper.rs b/utils/src/legacy/socket_addr_helper.rs
new file mode 100644
index 0000000..29ccd9f
--- /dev/null
+++ b/utils/src/legacy/socket_addr_helper.rs
@@ -0,0 +1,194 @@
+use std::net::SocketAddr;
+use tokio::net::lookup_host;
+
+/// Helper function to parse a string into a SocketAddr with optional default port
+pub async fn get_socket_addr(
+ address_str: impl AsRef<str>,
+ default_port: u16,
+) -> Result<SocketAddr, std::io::Error> {
+ let address = address_str.as_ref().trim();
+
+ // Return error if input is empty after trimming
+ if address.is_empty() {
+ return Err(std::io::Error::new(
+ std::io::ErrorKind::InvalidInput,
+ "Empty address string",
+ ));
+ }
+
+ // Check if the address contains a port
+ if let Some((host, port_str)) = parse_host_and_port(address) {
+ let port = port_str.parse::<u16>().map_err(|e| {
+ std::io::Error::new(
+ std::io::ErrorKind::InvalidInput,
+ format!("Invalid port number '{}': {}", port_str, e),
+ )
+ })?;
+
+ return resolve_to_socket_addr(host, port).await;
+ }
+
+ // No port specified, use default port
+ resolve_to_socket_addr(address, default_port).await
+}
+
+/// Parse host and port from address string
+fn parse_host_and_port(address: &str) -> Option<(&str, &str)> {
+ if address.starts_with('[')
+ && let Some(close_bracket) = address.find(']')
+ && close_bracket + 1 < address.len()
+ && address.as_bytes()[close_bracket + 1] == b':'
+ {
+ let host = &address[1..close_bracket];
+ let port = &address[close_bracket + 2..];
+ return Some((host, port));
+ }
+
+ // Handle IPv4 addresses and hostnames with ports
+ if let Some(colon_pos) = address.rfind(':') {
+ // Check if this is not part of an IPv6 address without brackets
+ if !address.contains('[') && !address.contains(']') {
+ let host = &address[..colon_pos];
+ let port = &address[colon_pos + 1..];
+
+ // Basic validation to avoid false positives
+ if !host.is_empty() && !port.is_empty() && port.chars().all(|c| c.is_ascii_digit()) {
+ return Some((host, port));
+ }
+ }
+ }
+
+ None
+}
+
+/// Resolve host to SocketAddr, handling both IP addresses and domain names
+async fn resolve_to_socket_addr(host: &str, port: u16) -> Result<SocketAddr, std::io::Error> {
+ // First try to parse as IP address (IPv4 or IPv6)
+ if let Ok(ip_addr) = host.parse() {
+ return Ok(SocketAddr::new(ip_addr, port));
+ }
+
+ // If it's not a valid IP address, treat it as a domain name and perform DNS lookup
+ let lookup_addr = format!("{}:{}", host, port);
+ let mut addrs = lookup_host(&lookup_addr).await?;
+
+ if let Some(addr) = addrs.next() {
+ Ok(addr)
+ } else {
+ Err(std::io::Error::new(
+ std::io::ErrorKind::NotFound,
+ format!("Could not resolve host '{}'", host),
+ ))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[tokio::test]
+ async fn test_ipv4_with_port() {
+ let result = get_socket_addr("127.0.0.1:8080", 80).await;
+ assert!(result.is_ok());
+ let addr = result.unwrap();
+ assert_eq!(addr.port(), 8080);
+ assert_eq!(addr.ip().to_string(), "127.0.0.1");
+ }
+
+ #[tokio::test]
+ async fn test_ipv4_without_port() {
+ let result = get_socket_addr("192.168.1.1", 443).await;
+ assert!(result.is_ok());
+ let addr = result.unwrap();
+ assert_eq!(addr.port(), 443);
+ assert_eq!(addr.ip().to_string(), "192.168.1.1");
+ }
+
+ #[tokio::test]
+ async fn test_ipv6_with_port() {
+ let result = get_socket_addr("[::1]:8080", 80).await;
+ assert!(result.is_ok());
+ let addr = result.unwrap();
+ assert_eq!(addr.port(), 8080);
+ assert_eq!(addr.ip().to_string(), "::1");
+ }
+
+ #[tokio::test]
+ async fn test_ipv6_without_port() {
+ let result = get_socket_addr("[::1]", 443).await;
+ assert!(result.is_ok());
+ let addr = result.unwrap();
+ assert_eq!(addr.port(), 443);
+ assert_eq!(addr.ip().to_string(), "::1");
+ }
+
+ #[tokio::test]
+ async fn test_invalid_port() {
+ let result = get_socket_addr("127.0.0.1:99999", 80).await;
+ assert!(result.is_err());
+ }
+
+ #[tokio::test]
+ async fn test_empty_string() {
+ let result = get_socket_addr("", 80).await;
+ assert!(result.is_err());
+ }
+
+ #[tokio::test]
+ async fn test_whitespace_trimming() {
+ let result = get_socket_addr(" 127.0.0.1:8080 ", 80).await;
+ assert!(result.is_ok());
+ let addr = result.unwrap();
+ assert_eq!(addr.port(), 8080);
+ }
+
+ #[tokio::test]
+ async fn test_domain_name_with_port() {
+ // This test will only pass if localhost resolves
+ let result = get_socket_addr("localhost:8080", 80).await;
+ if result.is_ok() {
+ let addr = result.unwrap();
+ assert_eq!(addr.port(), 8080);
+ // localhost should resolve to 127.0.0.1 or ::1
+ assert!(addr.ip().is_loopback());
+ }
+ }
+
+ #[tokio::test]
+ async fn test_domain_name_without_port() {
+ // This test will only pass if localhost resolves
+ let result = get_socket_addr("localhost", 443).await;
+ if result.is_ok() {
+ let addr = result.unwrap();
+ assert_eq!(addr.port(), 443);
+ // localhost should resolve to 127.0.0.1 or ::1
+ assert!(addr.ip().is_loopback());
+ }
+ }
+
+ #[tokio::test]
+ async fn test_parse_host_and_port() {
+ // IPv4 with port
+ assert_eq!(
+ parse_host_and_port("192.168.1.1:8080"),
+ Some(("192.168.1.1", "8080"))
+ );
+
+ // IPv6 with port
+ assert_eq!(parse_host_and_port("[::1]:8080"), Some(("::1", "8080")));
+
+ // Hostname with port
+ assert_eq!(
+ parse_host_and_port("example.com:443"),
+ Some(("example.com", "443"))
+ );
+
+ // No port
+ assert_eq!(parse_host_and_port("192.168.1.1"), None);
+ assert_eq!(parse_host_and_port("example.com"), None);
+
+ // Invalid cases
+ assert_eq!(parse_host_and_port(":"), None);
+ assert_eq!(parse_host_and_port("192.168.1.1:"), None);
+ }
+}