use std::fs;
use std::io::Write;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use crate::io::atomic_file::AtomicFile;
use crate::shell::detect::{Shell, get_config_file_path, get_saved_path_file, get_sourcing_line};
pub fn save_path(shell: &Shell, path: &str) -> Result<(), String> {
let saved_path_file = get_saved_path_file(shell)?;
if let Some(parent) = saved_path_file.parent() {
fs::create_dir_all(parent).map_err(|e| format!("Failed to create .whi directory: {e}"))?;
}
if saved_path_file.exists() {
let backup_path = saved_path_file.with_extension("bak");
let mut atomic_backup = AtomicFile::new(&backup_path)
.map_err(|e| format!("Failed to create backup file: {e}"))?;
let existing_content = fs::read_to_string(&saved_path_file)
.map_err(|e| format!("Failed to read existing PATH file: {e}"))?;
atomic_backup
.write_all(existing_content.as_bytes())
.map_err(|e| format!("Failed to write backup: {e}"))?;
atomic_backup
.commit()
.map_err(|e| format!("Failed to commit backup: {e}"))?;
}
let mut formatted = String::from("!path.saved\n");
for entry in path.split(':').filter(|s| !s.is_empty()) {
formatted.push_str(entry);
formatted.push('\n');
}
let mut atomic_file = AtomicFile::new(&saved_path_file)
.map_err(|e| format!("Failed to create PATH file: {e}"))?;
atomic_file
.write_all(formatted.as_bytes())
.map_err(|e| format!("Failed to write PATH: {e}"))?;
atomic_file
.commit()
.map_err(|e| format!("Failed to commit PATH file: {e}"))?;
Ok(())
}
pub fn ensure_whi_integration(shell: &Shell) -> Result<(), String> {
let config_file = get_config_file_path(shell)?;
let sourcing_line = get_sourcing_line(shell)?;
let existing_content = if config_file.exists() {
fs::read_to_string(&config_file).map_err(|e| format!("Failed to read config file: {e}"))?
} else {
String::new()
};
if existing_content.contains("# whi: Load saved PATH") {
return Ok(()); }
if config_file.exists() {
backup_config_file(&config_file)?;
} else {
if let Some(parent) = config_file.parent() {
fs::create_dir_all(parent)
.map_err(|e| format!("Failed to create config directory: {e}"))?;
}
}
let new_content = if existing_content.is_empty() {
sourcing_line
} else if existing_content.ends_with('\n') {
format!("{existing_content}\n{sourcing_line}")
} else {
format!("{existing_content}\n\n{sourcing_line}")
};
let mut atomic_file =
AtomicFile::new(&config_file).map_err(|e| format!("Failed to create config file: {e}"))?;
atomic_file
.write_all(new_content.as_bytes())
.map_err(|e| format!("Failed to write config: {e}"))?;
atomic_file
.commit()
.map_err(|e| format!("Failed to commit config file: {e}"))?;
Ok(())
}
fn backup_config_file(path: &Path) -> Result<(), String> {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|e| format!("Failed to get timestamp: {e}"))?
.as_secs();
let backup_path = path.with_extension(format!("backup-{timestamp}"));
fs::copy(path, &backup_path).map_err(|e| format!("Failed to create backup: {e}"))?;
Ok(())
}
fn get_profiles_dir() -> Result<std::path::PathBuf, String> {
let home = std::env::var("HOME").map_err(|_| "HOME environment variable not set")?;
let profiles_dir = std::path::PathBuf::from(home).join(".whi").join("profiles");
if !profiles_dir.exists() {
fs::create_dir_all(&profiles_dir)
.map_err(|e| format!("Failed to create profiles directory: {e}"))?;
}
Ok(profiles_dir)
}
pub fn save_profile(profile_name: &str, path: &str) -> Result<(), String> {
use crate::path::file::format_path_file;
if profile_name.is_empty() {
return Err("Profile name cannot be empty".to_string());
}
if profile_name.contains('/') || profile_name.contains('\\') || profile_name.starts_with('.') {
return Err(
"Invalid profile name (cannot contain path separators or start with '.')".to_string(),
);
}
let profiles_dir = get_profiles_dir()?;
let profile_file = profiles_dir.join(profile_name);
let formatted = format_path_file(path);
let mut atomic_file = AtomicFile::new(&profile_file)
.map_err(|e| format!("Failed to create profile file: {e}"))?;
atomic_file
.write_all(formatted.as_bytes())
.map_err(|e| format!("Failed to write profile: {e}"))?;
atomic_file
.commit()
.map_err(|e| format!("Failed to commit profile file: {e}"))?;
Ok(())
}
pub fn load_profile(profile_name: &str) -> Result<crate::path::file::ParsedPathFile, String> {
use crate::path::file::parse_path_file;
if profile_name.is_empty() {
return Err("Profile name cannot be empty".to_string());
}
if profile_name.contains('/') || profile_name.contains('\\') || profile_name.starts_with('.') {
return Err(
"Invalid profile name (cannot contain path separators or start with '.')".to_string(),
);
}
let profiles_dir = get_profiles_dir()?;
let profile_file = profiles_dir.join(profile_name);
if !profile_file.exists() {
return Err(format!("Profile '{profile_name}' not found"));
}
let content = fs::read_to_string(&profile_file)
.map_err(|e| format!("Failed to read profile file: {e}"))?;
parse_path_file(&content)
}
pub fn delete_profile(profile_name: &str) -> Result<(), String> {
if profile_name.is_empty() {
return Err("Profile name cannot be empty".to_string());
}
if profile_name.contains('/') || profile_name.contains('\\') || profile_name.starts_with('.') {
return Err(
"Invalid profile name (cannot contain path separators or start with '.')".to_string(),
);
}
let profiles_dir = get_profiles_dir()?;
let profile_file = profiles_dir.join(profile_name);
if !profile_file.exists() {
return Err(format!("Profile '{profile_name}' not found"));
}
fs::remove_file(&profile_file).map_err(|e| format!("Failed to delete profile: {e}"))?;
Ok(())
}
pub fn list_profiles() -> Result<Vec<String>, String> {
let profiles_dir = get_profiles_dir()?;
if !profiles_dir.exists() {
return Ok(Vec::new());
}
let entries = fs::read_dir(&profiles_dir)
.map_err(|e| format!("Failed to read profiles directory: {e}"))?;
let mut profiles = Vec::new();
for entry in entries.flatten() {
if let Ok(file_type) = entry.file_type()
&& file_type.is_file()
&& let Some(name) = entry.file_name().to_str()
&& !name.starts_with('.')
{
profiles.push(name.to_string());
}
}
profiles.sort();
Ok(profiles)
}
pub fn load_saved_path_for_shell(shell: &Shell) -> Result<String, String> {
use crate::path::file::{apply_path_sections, parse_path_file};
let saved_path_file = get_saved_path_file(shell)?;
if !saved_path_file.exists() {
return Err(format!("No saved PATH found for {}", shell.as_str()));
}
let content = fs::read_to_string(&saved_path_file)
.map_err(|e| format!("Failed to read saved PATH file: {e}"))?;
let parsed = parse_path_file(&content)?;
apply_path_sections("", &parsed.path)
}