summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-01-27 06:02:59 +0800
committer魏曹先生 <1992414357@qq.com>2026-01-27 06:02:59 +0800
commit4eef9ce364bb660421a96052a3fb126a33b22c63 (patch)
treea36947411d83205dc743881cd2a30d8c907d4b57 /src
parent243d521fd19af169910506529e737a797e9bc583 (diff)
Extract CLI utilities into a separate crate
Diffstat (limited to 'src')
-rw-r--r--src/bin/jv.rs19
-rw-r--r--src/bin/jvii.rs6
-rw-r--r--src/bin/jvn.rs10
-rw-r--r--src/bin/jvv.rs14
-rw-r--r--src/cmds/status.rs2
-rw-r--r--src/lib.rs3
-rw-r--r--src/renderers/status.rs8
-rw-r--r--src/systems/cmd.rs1
-rw-r--r--src/systems/cmd/workspace_reader.rs (renamed from src/utils/workspace_reader.rs)0
-rw-r--r--src/utils.rs10
-rw-r--r--src/utils/display.rs486
-rw-r--r--src/utils/env.rs94
-rw-r--r--src/utils/fs.rs40
-rw-r--r--src/utils/globber.rs276
-rw-r--r--src/utils/input.rs139
-rw-r--r--src/utils/levenshtein_distance.rs34
-rw-r--r--src/utils/logger.rs87
-rw-r--r--src/utils/push_version.rs30
-rw-r--r--src/utils/socket_addr_helper.rs194
19 files changed, 29 insertions, 1424 deletions
diff --git a/src/bin/jv.rs b/src/bin/jv.rs
index a0de5a8..9593c9d 100644
--- a/src/bin/jv.rs
+++ b/src/bin/jv.rs
@@ -1,3 +1,4 @@
+use cli_utils::input::input_with_editor;
use colored::Colorize;
use just_enough_vcs::{
data::compile_info::CoreCompileInfo,
@@ -92,6 +93,15 @@ use std::{
};
use clap::{Parser, Subcommand};
+use cli_utils::{
+ display::{SimpleTable, display_width, md, render_share_path_tree, size_str},
+ env::{auto_update_outdate, current_locales, enable_auto_update},
+ fs::move_across_partitions,
+ globber::{GlobItem, Globber},
+ input::{confirm_hint, confirm_hint_or, show_in_pager},
+ push_version::push_version,
+ socket_addr_helper,
+};
use just_enough_vcs::utils::tcp_connection::error::TcpTargetError;
use just_enough_vcs_cli::{
data::{
@@ -107,15 +117,6 @@ use just_enough_vcs_cli::{
share::{SeeShareResult, ShareItem, ShareListResult},
sheets::{SheetItem, SheetListJsonResult},
},
- utils::{
- display::{SimpleTable, display_width, md, render_share_path_tree, size_str},
- env::{auto_update_outdate, current_locales, enable_auto_update},
- fs::move_across_partitions,
- globber::{GlobItem, Globber},
- input::{confirm_hint, confirm_hint_or, input_with_editor, show_in_pager},
- push_version::push_version,
- socket_addr_helper,
- },
};
use rust_i18n::{set_locale, t};
use tokio::{
diff --git a/src/bin/jvii.rs b/src/bin/jvii.rs
index 2dfbeb7..2f9cb6d 100644
--- a/src/bin/jvii.rs
+++ b/src/bin/jvii.rs
@@ -5,6 +5,9 @@ use std::path::PathBuf;
use std::time::Duration;
use clap::Parser;
+use cli_utils::display::display_width;
+use cli_utils::display::md;
+use cli_utils::env::current_locales;
use crossterm::{
QueueableCommand,
cursor::MoveTo,
@@ -16,9 +19,6 @@ use crossterm::{
enable_raw_mode,
},
};
-use just_enough_vcs_cli::utils::display::display_width;
-use just_enough_vcs_cli::utils::display::md;
-use just_enough_vcs_cli::utils::env::current_locales;
use rust_i18n::set_locale;
use rust_i18n::t;
#[cfg(windows)]
diff --git a/src/bin/jvn.rs b/src/bin/jvn.rs
index c1d90c9..598be3d 100644
--- a/src/bin/jvn.rs
+++ b/src/bin/jvn.rs
@@ -1,14 +1,12 @@
use std::process::exit;
+use cli_utils::display::md;
+use cli_utils::env::current_locales;
+use cli_utils::levenshtein_distance::levenshtein_distance;
use just_enough_vcs_cli::systems::cmd::_registry::jv_cmd_nodes;
use just_enough_vcs_cli::systems::cmd::cmd_system::JVCommandContext;
use just_enough_vcs_cli::systems::cmd::errors::{CmdExecuteError, CmdPrepareError, CmdRenderError};
-use just_enough_vcs_cli::utils::display::md;
-use just_enough_vcs_cli::utils::levenshtein_distance::levenshtein_distance;
-use just_enough_vcs_cli::{
- systems::cmd::{errors::CmdProcessError, processer::jv_cmd_process},
- utils::env::current_locales,
-};
+use just_enough_vcs_cli::systems::cmd::{errors::CmdProcessError, processer::jv_cmd_process};
use rust_i18n::{set_locale, t};
rust_i18n::i18n!("resources/locales/jvn", fallback = "en");
diff --git a/src/bin/jvv.rs b/src/bin/jvv.rs
index 0e1eaa5..1fae774 100644
--- a/src/bin/jvv.rs
+++ b/src/bin/jvv.rs
@@ -1,4 +1,9 @@
use clap::{Parser, Subcommand};
+use cli_utils::{
+ display::{md, size_str},
+ env::current_locales,
+ logger::build_env_logger,
+};
use just_enough_vcs::{
data::compile_info::CoreCompileInfo,
utils::{
@@ -15,14 +20,7 @@ use just_enough_vcs::{
env::current_vault_path,
},
};
-use just_enough_vcs_cli::{
- data::compile_info::CompileInfo,
- utils::{
- display::{md, size_str},
- env::current_locales,
- logger::build_env_logger,
- },
-};
+use just_enough_vcs_cli::data::compile_info::CompileInfo;
use log::{error, info};
use rust_i18n::{set_locale, t};
use tokio::fs::{self};
diff --git a/src/cmds/status.rs b/src/cmds/status.rs
index 42d8e27..6f04408 100644
--- a/src/cmds/status.rs
+++ b/src/cmds/status.rs
@@ -13,8 +13,8 @@ use crate::{
systems::cmd::{
cmd_system::{JVCommand, JVCommandContext},
errors::{CmdExecuteError, CmdPrepareError},
+ workspace_reader::LocalWorkspaceReader,
},
- utils::workspace_reader::LocalWorkspaceReader,
};
pub struct JVStatusCommand;
diff --git a/src/lib.rs b/src/lib.rs
index 2df67b7..d4832f5 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -2,9 +2,6 @@ rust_i18n::i18n!("resources/locales/jvn", fallback = "en");
// --- LIBS ---
-/// Utils
-pub mod utils;
-
/// Data
pub mod data;
diff --git a/src/renderers/status.rs b/src/renderers/status.rs
index d51252e..549211e 100644
--- a/src/renderers/status.rs
+++ b/src/renderers/status.rs
@@ -1,3 +1,7 @@
+use cli_utils::{
+ display::{SimpleTable, md},
+ env::auto_update_outdate,
+};
use rust_i18n::t;
use crate::{
@@ -7,10 +11,6 @@ use crate::{
errors::CmdRenderError,
renderer::{JVRenderResult, JVResultRenderer},
},
- utils::{
- display::{SimpleTable, md},
- env::auto_update_outdate,
- },
};
pub struct JVStatusRenderer;
diff --git a/src/systems/cmd.rs b/src/systems/cmd.rs
index 8983892..ea8bbd7 100644
--- a/src/systems/cmd.rs
+++ b/src/systems/cmd.rs
@@ -3,3 +3,4 @@ pub mod cmd_system;
pub mod errors;
pub mod processer;
pub mod renderer;
+pub mod workspace_reader;
diff --git a/src/utils/workspace_reader.rs b/src/systems/cmd/workspace_reader.rs
index c001330..c001330 100644
--- a/src/utils/workspace_reader.rs
+++ b/src/systems/cmd/workspace_reader.rs
diff --git a/src/utils.rs b/src/utils.rs
deleted file mode 100644
index 7d3cb5e..0000000
--- a/src/utils.rs
+++ /dev/null
@@ -1,10 +0,0 @@
-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;
-pub mod workspace_reader;
diff --git a/src/utils/display.rs b/src/utils/display.rs
deleted file mode 100644
index 835313b..0000000
--- a/src/utils/display.rs
+++ /dev/null
@@ -1,486 +0,0 @@
-use colored::*;
-use just_enough_vcs::vcs::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: &str, color_name: &str) -> String {
- 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/src/utils/env.rs b/src/utils/env.rs
deleted file mode 100644
index 81dfbd7..0000000
--- a/src/utils/env.rs
+++ /dev/null
@@ -1,94 +0,0 @@
-/// 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()
-}
diff --git a/src/utils/fs.rs b/src/utils/fs.rs
deleted file mode 100644
index 0050cf1..0000000
--- a/src/utils/fs.rs
+++ /dev/null
@@ -1,40 +0,0 @@
-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/src/utils/globber.rs b/src/utils/globber.rs
deleted file mode 100644
index dbb77ea..0000000
--- a/src/utils/globber.rs
+++ /dev/null
@@ -1,276 +0,0 @@
-use std::{io::Error, path::PathBuf, str::FromStr};
-
-use just_enough_vcs::utils::string_proc::format_path::format_path_str;
-
-use crate::utils::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);
- }
- (
- format_path_str(path)?,
- 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 = {
- format_path_str(format!("{}{}{}", current, SPLIT_STR, file_name))
- .unwrap_or_default()
- };
- file_names.push(relative_path)
- }
- GlobItem::Directory(dir_name) => {
- let new_current = {
- format_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/src/utils/input.rs b/src/utils/input.rs
deleted file mode 100644
index 1edcc5d..0000000
--- a/src/utils/input.rs
+++ /dev/null
@@ -1,139 +0,0 @@
-use tokio::{fs, process::Command};
-
-use crate::utils::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> {
- 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?;
-
- // Get editor from environment variable
- let editor = get_default_editor().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/src/utils/levenshtein_distance.rs b/src/utils/levenshtein_distance.rs
deleted file mode 100644
index 6bdb7e7..0000000
--- a/src/utils/levenshtein_distance.rs
+++ /dev/null
@@ -1,34 +0,0 @@
-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/src/utils/logger.rs b/src/utils/logger.rs
deleted file mode 100644
index 7dd4f62..0000000
--- a/src/utils/logger.rs
+++ /dev/null
@@ -1,87 +0,0 @@
-use std::path::Path;
-
-use colored::Colorize;
-use env_logger::{Builder, Target};
-use just_enough_vcs::{
- utils::string_proc::format_path::format_path, vcs::data::vault::vault_config::LoggerLevel,
-};
-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) = format_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/src/utils/push_version.rs b/src/utils/push_version.rs
deleted file mode 100644
index 6da9039..0000000
--- a/src/utils/push_version.rs
+++ /dev/null
@@ -1,30 +0,0 @@
-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/src/utils/socket_addr_helper.rs b/src/utils/socket_addr_helper.rs
deleted file mode 100644
index 29ccd9f..0000000
--- a/src/utils/socket_addr_helper.rs
+++ /dev/null
@@ -1,194 +0,0 @@
-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);
- }
-}