use crate::config::Config;
use crate::services::PackageService;
use crate::utils::profile_manifest::PackageManager;
use crate::utils::ProfileManifest;
use anyhow::{Context, Result};
use std::io::{self, Write};
use std::path::PathBuf;
pub struct CliContext {
pub config: Config,
pub manifest: ProfileManifest,
pub config_path: PathBuf,
}
impl CliContext {
pub fn load() -> Result<Self> {
let config_path = crate::utils::get_config_path();
let config =
Config::load_or_create(&config_path).context("Failed to load configuration")?;
if !config.is_repo_configured() {
print_error("Repository not configured. Please run 'dotstate' to set up repository.");
std::process::exit(1);
}
let manifest = ProfileManifest::load_or_backfill(&config.repo_path)
.context("Failed to load profile manifest")?;
Ok(Self {
config,
manifest,
config_path,
})
}
#[must_use]
pub fn resolve_profile(&self, profile: Option<&str>) -> String {
profile.map_or_else(
|| self.config.active_profile.clone(),
std::string::ToString::to_string,
)
}
#[must_use]
pub fn is_active_profile(&self, profile_name: &str) -> bool {
self.config.active_profile == profile_name
}
#[must_use]
pub fn profile_exists(&self, profile_name: &str) -> bool {
self.manifest
.profiles
.iter()
.any(|p| p.name == profile_name)
}
#[must_use]
pub fn get_profile(&self, profile_name: &str) -> Option<&crate::utils::ProfileInfo> {
self.manifest
.profiles
.iter()
.find(|p| p.name == profile_name)
}
pub fn save_manifest(&self) -> Result<()> {
self.manifest
.save(&self.config.repo_path)
.context("Failed to save profile manifest")
}
}
pub fn print_success(msg: &str) {
println!("\u{2713} {msg}");
}
pub fn print_error(msg: &str) {
eprintln!("\u{2717} {msg}");
}
pub fn print_warning(msg: &str) {
println!("\u{26A0}\u{FE0F} {msg}");
}
pub fn print_info(msg: &str) {
println!("\u{2139}\u{FE0F} {msg}");
}
pub fn prompt_string(label: &str, default: Option<&str>) -> Result<String> {
if let Some(def) = default {
print!("{label} [{def}]: ");
} else {
print!("{label}: ");
}
io::stdout().flush().context("Failed to flush stdout")?;
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.context("Failed to read input")?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(default.unwrap_or("").to_string())
} else {
Ok(trimmed.to_string())
}
}
pub fn prompt_string_optional(label: &str) -> Result<Option<String>> {
print!("{label} (optional): ");
io::stdout().flush().context("Failed to flush stdout")?;
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.context("Failed to read input")?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(None)
} else {
Ok(Some(trimmed.to_string()))
}
}
pub fn prompt_select(label: &str, options: &[&str]) -> Result<usize> {
if options.is_empty() {
print_error("No options available for selection");
std::process::exit(1);
}
println!("{label}:");
for (i, option) in options.iter().enumerate() {
println!(" {}. {}", i + 1, option);
}
print!("Enter choice [1-{}]: ", options.len());
io::stdout().flush().context("Failed to flush stdout")?;
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.context("Failed to read input")?;
let trimmed = input.trim();
match trimmed.parse::<usize>() {
Ok(n) if n >= 1 && n <= options.len() => Ok(n - 1),
_ => {
print_error(&format!(
"Invalid choice. Please enter a number between 1 and {}",
options.len()
));
std::process::exit(1);
}
}
}
pub fn prompt_select_with_suffix(label: &str, options: &[(&str, Option<&str>)]) -> Result<usize> {
if options.is_empty() {
print_error("No options available for selection");
std::process::exit(1);
}
println!("{label}:");
for (i, (name, suffix)) in options.iter().enumerate() {
if let Some(s) = suffix {
println!(" {}. {} {}", i + 1, name, s);
} else {
println!(" {}. {}", i + 1, name);
}
}
print!("Enter choice [1-{}]: ", options.len());
io::stdout().flush().context("Failed to flush stdout")?;
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.context("Failed to read input")?;
let trimmed = input.trim();
match trimmed.parse::<usize>() {
Ok(n) if n >= 1 && n <= options.len() => Ok(n - 1),
_ => {
print_error(&format!(
"Invalid choice. Please enter a number between 1 and {}",
options.len()
));
std::process::exit(1);
}
}
}
pub fn prompt_confirm(message: &str) -> Result<bool> {
print!("{message} [y/N]: ");
io::stdout().flush().context("Failed to flush stdout")?;
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.context("Failed to read input")?;
let trimmed = input.trim().to_lowercase();
Ok(trimmed == "y" || trimmed == "yes")
}
fn all_package_managers() -> Vec<PackageManager> {
vec![
PackageManager::Brew,
PackageManager::Apt,
PackageManager::Yum,
PackageManager::Dnf,
PackageManager::Pacman,
PackageManager::Snap,
PackageManager::Cargo,
PackageManager::Npm,
PackageManager::Pip,
PackageManager::Pip3,
PackageManager::Gem,
PackageManager::Custom,
]
}
pub fn prompt_manager(is_active_profile: bool) -> Result<PackageManager> {
let managers = if is_active_profile {
PackageService::get_available_managers()
} else {
all_package_managers()
};
let options: Vec<(&str, Option<&str>)> = managers
.iter()
.map(|m| {
let name = match m {
PackageManager::Brew => "brew",
PackageManager::Apt => "apt",
PackageManager::Yum => "yum",
PackageManager::Dnf => "dnf",
PackageManager::Pacman => "pacman",
PackageManager::Snap => "snap",
PackageManager::Cargo => "cargo",
PackageManager::Npm => "npm",
PackageManager::Pip => "pip",
PackageManager::Pip3 => "pip3",
PackageManager::Gem => "gem",
PackageManager::Custom => "custom",
};
let suffix = if is_active_profile && PackageService::is_manager_installed(m) {
Some("(installed)")
} else {
None
};
(name, suffix)
})
.collect();
let index = prompt_select_with_suffix("Manager", &options)?;
Ok(managers[index].clone())
}
#[must_use]
pub fn parse_manager(s: &str) -> Option<PackageManager> {
match s.to_lowercase().as_str() {
"brew" | "homebrew" => Some(PackageManager::Brew),
"apt" | "apt-get" => Some(PackageManager::Apt),
"yum" => Some(PackageManager::Yum),
"dnf" => Some(PackageManager::Dnf),
"pacman" => Some(PackageManager::Pacman),
"snap" => Some(PackageManager::Snap),
"cargo" => Some(PackageManager::Cargo),
"npm" => Some(PackageManager::Npm),
"pip" => Some(PackageManager::Pip),
"pip3" => Some(PackageManager::Pip3),
"gem" => Some(PackageManager::Gem),
"custom" => Some(PackageManager::Custom),
_ => None,
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_output_helpers_compile() {
let _ = format!("\u{2713} {}", "test"); let _ = format!("\u{2717} {}", "test"); let _ = format!("\u{26A0}\u{FE0F} {}", "test"); let _ = format!("\u{2139}\u{FE0F} {}", "test"); }
}