High-performance implementation of plcbundle written in Rust
at main 238 lines 8.4 kB view raw
1// Shared utility functions for CLI commands 2 3use anyhow::Result; 4use plcbundle::BundleManager; 5use std::path::{Path, PathBuf}; 6 7/// ANSI color codes for terminal output 8/// These are shared across all CLI commands for consistent coloring 9pub mod colors { 10 /// Standard green color (used for success, matches, etc.) 11 pub const GREEN: &str = "\x1b[32m"; 12 13 /// Standard red color (used for errors, deletions, etc.) 14 pub const RED: &str = "\x1b[31m"; 15 16 /// Reset color code 17 pub const RESET: &str = "\x1b[0m"; 18 19 /// Dim/bright black color (used for context, unchanged lines, etc.) 20 pub const DIM: &str = "\x1b[2m"; 21} 22 23#[cfg(feature = "cli")] 24mod colorize { 25 use colored_json::prelude::*; 26 27 /// Colorize pretty-printed JSON string with syntax highlighting 28 /// 29 /// Uses the colored_json crate which provides jq-compatible colorization. 30 /// Automatically detects if output is a terminal and applies colors accordingly. 31 pub fn colorize_json(json: &str) -> String { 32 // Use to_colored_json_auto which automatically detects terminal and applies colors 33 // This matches jq's behavior of only coloring when output is to a terminal 34 json.to_colored_json_auto() 35 .unwrap_or_else(|_| json.to_string()) 36 } 37} 38 39#[cfg(feature = "cli")] 40pub use colorize::colorize_json; 41 42/// Check if stdout is connected to an interactive terminal (TTY) 43/// 44/// Returns true if stdout is a TTY, false otherwise. 45/// This is useful for automatically enabling pretty printing and colors 46/// when outputting to a terminal, while using raw output when piping. 47#[cfg(feature = "cli")] 48pub fn is_stdout_tty() -> bool { 49 use is_terminal::IsTerminal; 50 std::io::stdout().is_terminal() 51} 52 53pub use plcbundle::format::{format_bytes, format_bytes_per_sec, format_number}; 54 55/// Trait for extracting global flags from command objects 56/// Commands that have verbose/quiet fields should implement this trait 57pub trait HasGlobalFlags { 58 fn verbose(&self) -> bool; 59 fn quiet(&self) -> bool; 60} 61 62/// Parse bundle specification string into a vector of bundle numbers 63pub fn parse_bundle_spec(spec: Option<String>, max_bundle: u32) -> Result<Vec<u32>> { 64 match spec { 65 None => Ok((1..=max_bundle).collect()), 66 Some(s) => { 67 if s.starts_with("latest:") { 68 let count: u32 = s.strip_prefix("latest:").unwrap().parse()?; 69 let start = max_bundle.saturating_sub(count.saturating_sub(1)); 70 Ok((start..=max_bundle).collect()) 71 } else { 72 use plcbundle::processor::parse_bundle_range; 73 parse_bundle_range(&s, max_bundle) 74 } 75 } 76 } 77} 78 79/// Display path resolving "." to absolute path 80/// Per RULES.md: NEVER display "." in user-facing output 81pub fn display_path(path: &Path) -> PathBuf { 82 path.canonicalize().unwrap_or_else(|_| path.to_path_buf()) 83} 84 85/// Get number of worker threads, auto-detecting if workers == 0 86/// 87/// # Arguments 88/// * `workers` - Number of workers requested (0 = auto-detect) 89/// * `fallback` - Fallback value if auto-detection fails (default: 4) 90/// 91/// # Returns 92/// Number of worker threads to use 93pub fn get_worker_threads(workers: usize, fallback: usize) -> usize { 94 if workers == 0 { 95 std::thread::available_parallelism() 96 .map(|n| n.get()) 97 .unwrap_or(fallback) 98 } else { 99 workers 100 } 101} 102 103/// Check if repository is empty (no bundles) 104pub fn is_repository_empty(manager: &BundleManager) -> bool { 105 manager.get_last_bundle() == 0 106} 107 108/// Create BundleManager with verbose/quiet flags 109/// 110/// This is the standard way to create a BundleManager from CLI commands. 111/// It respects the verbose and quiet flags for logging. 112/// 113/// # Arguments 114/// * `dir` - Directory path 115/// * `verbose` - Enable verbose logging 116/// * `_quiet` - Quiet mode (currently unused) 117/// * `preload_mempool` - If true, preload mempool at initialization (for commands that need it) 118pub fn create_manager( 119 dir: PathBuf, 120 verbose: bool, 121 _quiet: bool, 122 preload_mempool: bool, 123) -> Result<BundleManager> { 124 use anyhow::Context; 125 126 // Check if directory exists 127 if !dir.exists() { 128 anyhow::bail!( 129 "Directory does not exist: {}\n\nHint: Make sure you're in a PLC bundle directory, or start a new repository with:\n {} init # Initialize empty repository\n {} clone <url> # Clone from remote", 130 display_path(&dir).display(), 131 plcbundle::constants::BINARY_NAME, 132 plcbundle::constants::BINARY_NAME 133 ); 134 } 135 136 // Check if it's a bundle directory (has plc_bundles.json) 137 let index_path = dir.join("plc_bundles.json"); 138 if !index_path.exists() { 139 anyhow::bail!( 140 "Not a PLC bundle directory: {}\n\nThis directory does not contain 'plc_bundles.json'.\n\nHint: Make sure you're in a PLC bundle directory, or start a new repository with:\n {} init # Initialize empty repository\n {} clone <url> # Clone from remote", 141 display_path(&dir).display(), 142 plcbundle::constants::BINARY_NAME, 143 plcbundle::constants::BINARY_NAME 144 ); 145 } 146 147 let display_dir = display_path(&dir); 148 let options = plcbundle::ManagerOptions { 149 handle_resolver_url: None, 150 preload_mempool, 151 verbose, 152 }; 153 let manager = BundleManager::new(dir, options).with_context(|| { 154 format!( 155 "Failed to load bundle repository from: {}", 156 display_dir.display() 157 ) 158 })?; 159 160 Ok(manager) 161} 162 163/// Create BundleManager with global flags extracted from command 164/// 165/// Convenience function for commands that implement `HasGlobalFlags`. 166/// The global flags (verbose, quiet) are automatically extracted from the command. 167pub fn create_manager_from_cmd<C: HasGlobalFlags>( 168 dir: PathBuf, 169 cmd: &C, 170 preload_mempool: bool, 171) -> Result<BundleManager> { 172 create_manager(dir, cmd.verbose(), cmd.quiet(), preload_mempool) 173} 174 175/// Resolve a target string (bundle number or path) into a bundle number and canonical path. 176/// This utility ensures that file existence checks for bundles are done through the BundleManager. 177pub fn resolve_bundle_target( 178 manager: &BundleManager, 179 target: &str, 180 repo_dir: &PathBuf, 181) -> Result<(Option<u32>, PathBuf)> { 182 // Try to parse as bundle number 183 if let Ok(num) = target.parse::<u32>() { 184 let path = plcbundle::constants::bundle_path(repo_dir, num); 185 // Check if bundle exists via BundleManager's index 186 if manager.get_bundle_metadata(num)?.is_some() { 187 Ok((Some(num), path)) 188 } else { 189 anyhow::bail!("Bundle {} not found in repository index", num); 190 } 191 } else { 192 // Otherwise treat as file path. For now, this is an error as direct file access is disallowed. 193 anyhow::bail!( 194 "Loading from arbitrary paths not yet implemented. Please specify a bundle number." 195 ); 196 } 197} 198 199/// Get all bundle metadata from the repository 200/// This is more efficient than iterating through bundle numbers 201pub fn get_all_bundle_metadata(manager: &BundleManager) -> Vec<plcbundle::index::BundleMetadata> { 202 manager.get_index().bundles 203} 204 205/// Check available free disk space for a given path 206/// 207/// Returns the available free space in bytes, or None if the check fails. 208/// On Unix systems (macOS, Linux), uses statvfs to get filesystem statistics. 209/// On other platforms, returns None (check is skipped). 210#[cfg(unix)] 211pub fn get_free_disk_space(path: &Path) -> Option<u64> { 212 use std::ffi::CString; 213 use std::os::unix::ffi::OsStrExt; 214 215 let c_path = match CString::new(path.as_os_str().as_bytes()) { 216 Ok(p) => p, 217 Err(_) => return None, 218 }; 219 220 unsafe { 221 let mut stat: libc::statvfs = std::mem::zeroed(); 222 if libc::statvfs(c_path.as_ptr(), &mut stat) == 0 { 223 // Calculate free space: available blocks * block size 224 let free_bytes = stat.f_bavail as u64 * stat.f_frsize; 225 Some(free_bytes) 226 } else { 227 None 228 } 229 } 230} 231 232/// Check available free disk space for a given path 233/// 234/// On non-Unix platforms, this function always returns None (check is skipped). 235#[cfg(not(unix))] 236pub fn get_free_disk_space(_path: &Path) -> Option<u64> { 237 None 238}