use crate::shell::{Shell, ShellType};
use std::path::{Path, PathBuf};
use yansi::Paint;
const TARGET: &str = "zv::shell::setup::unix";
pub fn select_rc_file(shell: &Shell) -> PathBuf {
let home_dir = match dirs::home_dir() {
Some(dir) => dir,
None => {
return PathBuf::from(".profile");
}
};
match shell.shell_type {
ShellType::Bash => {
let bashrc = home_dir.join(".bashrc");
if bashrc.exists() {
return bashrc;
}
let bash_profile = home_dir.join(".bash_profile");
if bash_profile.exists() {
return bash_profile;
}
home_dir.join(".profile")
}
ShellType::Zsh => {
let zshenv = home_dir.join(".zshenv");
if zshenv.exists() {
return zshenv;
}
let zshrc = home_dir.join(".zshrc");
if zshrc.exists() {
return zshrc;
}
let zprofile = home_dir.join(".zprofile");
if zprofile.exists() {
return zprofile;
}
home_dir.join(".zshenv")
}
ShellType::Fish => {
let config_dir = home_dir.join(".config/fish");
config_dir.join("config.fish")
}
ShellType::Tcsh => {
let tcshrc = home_dir.join(".tcshrc");
if tcshrc.exists() {
return tcshrc;
}
let cshrc = home_dir.join(".cshrc");
if cshrc.exists() {
return cshrc;
}
home_dir.join(".profile")
}
ShellType::Nu => {
let config_dir = home_dir.join(".config/nushell");
config_dir.join("config.nu")
}
ShellType::PowerShell => {
if shell.is_powershell_in_unix() {
home_dir.join(".profile")
} else {
home_dir.join(".profile")
}
}
ShellType::Posix | ShellType::Unknown => {
home_dir.join(".profile")
}
ShellType::Cmd => {
home_dir.join(".profile")
}
}
}
pub async fn generate_unix_env_file(
shell: &Shell,
env_file_path: &Path,
zv_dir: &Path,
bin_path: &Path,
export_zv_dir: bool,
) -> crate::Result<()> {
use crate::shell::path_utils::{escape_path_for_shell, normalize_path_for_shell};
let zv_dir_str = normalize_path_for_shell(shell, zv_dir);
let bin_path_str = normalize_path_for_shell(shell, bin_path);
let escaped_zv_dir = escape_path_for_shell(shell, &zv_dir_str);
let escaped_bin_path = escape_path_for_shell(shell, &bin_path_str);
let content = shell.generate_env_content(&escaped_zv_dir, &escaped_bin_path, export_zv_dir);
if let Some(parent) = env_file_path.parent() {
tokio::fs::create_dir_all(parent).await.map_err(|_| {
crate::ZvError::shell_environment_file_failed(
"create_directory",
&env_file_path.display().to_string(),
)
})?;
}
crate::shell::env_export::write_shell_file_with_line_endings(env_file_path, &content)
.await
.map_err(|e| {
tracing::error!(target: TARGET,
"Failed to write environment file {}: {}",
env_file_path.display(),
e
);
crate::ZvError::shell_environment_file_failed(
"write",
&env_file_path.display().to_string(),
)
})?;
Ok(())
}
pub async fn add_source_to_rc_file(
shell: &Shell,
rc_file: &Path,
env_file_path: &Path,
) -> crate::Result<()> {
let source_line = shell.get_source_command(env_file_path);
let mut content = if rc_file.exists() {
tokio::fs::read_to_string(rc_file).await.map_err(|e| {
crate::ZvError::shell_rc_file_modification_failed(&rc_file.display().to_string(), e)
})?
} else {
String::new()
};
if content
.lines()
.any(|line| line.trim() == source_line.trim())
{
return Ok(()); }
if !content.is_empty() && !content.ends_with('\n') {
content.push('\n');
}
content.push_str("# Added by zv setup\n");
content.push_str(&source_line);
content.push('\n');
if let Some(parent) = rc_file.parent() {
tokio::fs::create_dir_all(parent).await.map_err(|e| {
crate::ZvError::shell_rc_file_modification_failed(&rc_file.display().to_string(), e)
})?;
}
write_rc_file_with_line_endings(rc_file, &content)
.await
.map_err(|e| {
crate::ZvError::shell_rc_file_modification_failed(&rc_file.display().to_string(), e)
})?;
Ok(())
}
pub async fn add_zv_dir_export_to_rc_file(
shell: &Shell,
rc_file: &Path,
zv_dir: &Path,
) -> crate::Result<()> {
use crate::shell::path_utils::{escape_path_for_shell, normalize_path_for_shell};
let zv_dir_str = normalize_path_for_shell(shell, zv_dir);
let escaped_zv_dir = escape_path_for_shell(shell, &zv_dir_str);
let export_line = match shell.shell_type {
ShellType::Fish => {
format!("set -gx ZV_DIR {}", escaped_zv_dir)
}
ShellType::Tcsh => {
format!("setenv ZV_DIR {}", escaped_zv_dir)
}
ShellType::Nu => {
format!("$env.ZV_DIR = {}", escaped_zv_dir)
}
_ => {
format!("export ZV_DIR={}", escaped_zv_dir)
}
};
let mut content = if rc_file.exists() {
tokio::fs::read_to_string(rc_file).await.map_err(|e| {
crate::ZvError::shell_rc_file_modification_failed(&rc_file.display().to_string(), e)
})?
} else {
String::new()
};
let has_zv_dir_export = content.lines().any(|line| {
let trimmed = line.trim();
trimmed.starts_with("export ZV_DIR=")
|| trimmed.starts_with("set -gx ZV_DIR ")
|| trimmed.starts_with("setenv ZV_DIR ")
|| trimmed.starts_with("$env.ZV_DIR =")
});
if has_zv_dir_export {
return Ok(()); }
if !content.is_empty() && !content.ends_with('\n') {
content.push('\n');
}
content.push_str("# Added by zv setup\n");
content.push_str(&export_line);
content.push('\n');
if let Some(parent) = rc_file.parent() {
tokio::fs::create_dir_all(parent).await.map_err(|e| {
crate::ZvError::shell_rc_file_modification_failed(&rc_file.display().to_string(), e)
})?;
}
write_rc_file_with_line_endings(rc_file, &content)
.await
.map_err(|e| {
crate::ZvError::shell_rc_file_modification_failed(&rc_file.display().to_string(), e)
})?;
Ok(())
}
pub async fn check_zv_dir_permanent_unix(shell: &Shell, zv_dir: &Path) -> crate::Result<bool> {
let rc_file = select_rc_file(shell);
if !rc_file.exists() {
return Ok(false);
}
let content = tokio::fs::read_to_string(&rc_file).await.map_err(|e| {
crate::ZvError::shell_rc_file_modification_failed(&rc_file.display().to_string(), e)
})?;
let target_path_str = crate::shell::path_utils::normalize_path_for_shell(shell, zv_dir);
let has_matching_export = content.lines().any(|line| {
let trimmed = line.trim();
if trimmed.starts_with('#') {
return false;
}
let exported_path = if let Some(path) = trimmed.strip_prefix("export ZV_DIR=") {
Some(path.trim_matches('"').trim_matches('\''))
} else if let Some(path) = trimmed.strip_prefix("set -gx ZV_DIR ") {
Some(path.trim_matches('"').trim_matches('\''))
} else if let Some(path) = trimmed.strip_prefix("setenv ZV_DIR ") {
Some(path.trim_matches('"').trim_matches('\''))
} else {
trimmed
.strip_prefix("$env.ZV_DIR = ")
.map(|path| path.trim_matches('"').trim_matches('\''))
};
if let Some(path) = exported_path {
let normalized_exported = if path.starts_with('~') {
if let Some(home) = dirs::home_dir() {
path.replacen('~', &home.to_string_lossy(), 1)
} else {
path.to_string()
}
} else {
path.to_string()
};
let normalized_exported = Path::new(&normalized_exported);
let normalized_exported_str =
crate::shell::path_utils::normalize_path_for_shell(shell, normalized_exported);
normalized_exported_str == target_path_str
} else {
false
}
});
Ok(has_matching_export)
}
pub async fn execute_zv_dir_setup_unix(
context: &crate::shell::setup::SetupContext,
zv_dir: &Path,
) -> crate::Result<()> {
if !zv_dir.exists() {
if let Err(e) = tokio::fs::create_dir_all(zv_dir).await {
return Err(crate::types::error::ShellErr::ZvDirOperationFailed {
operation: format!(
"ZV_DIR setup failed: cannot create directory {}: {}",
zv_dir.display(),
e
),
}
.into());
}
}
let rc_file = select_rc_file(&context.shell);
add_zv_dir_export_to_rc_file(&context.shell, &rc_file, zv_dir).await?;
use crate::shell::setup::instructions::{FileAction, create_rc_file_entry};
context.add_modified_file(create_rc_file_entry(rc_file.clone(), FileAction::Modified));
println!(
"✓ Added ZV_DIR export to {}",
Paint::green(&rc_file.display().to_string())
);
Ok(())
}
pub async fn execute_path_setup_unix(
context: &crate::shell::setup::SetupContext,
env_file_path: &Path,
rc_file: &Path,
bin_path: &Path,
) -> crate::Result<()> {
use crate::shell::setup::instructions::{
FileAction, create_env_file_entry, create_rc_file_entry,
};
generate_unix_env_file(
&context.shell,
env_file_path,
context.app.path(),
bin_path,
context.using_env_var,
)
.await?;
println!(
"✓ Generated environment file at {}",
Paint::green(&env_file_path.display().to_string())
);
context.add_modified_file(create_env_file_entry(
env_file_path.to_path_buf(),
FileAction::Created,
));
add_source_to_rc_file(&context.shell, rc_file, env_file_path).await?;
println!(
"✓ Added source line to {}",
Paint::green(&rc_file.display().to_string())
);
context.add_modified_file(create_rc_file_entry(
rc_file.to_path_buf(),
FileAction::SourceAdded,
));
Ok(())
}
async fn write_rc_file_with_line_endings(
file_path: &Path,
content: &str,
) -> Result<(), std::io::Error> {
let normalized_content = content.replace("\r\n", "\n");
tokio::fs::write(file_path, normalized_content).await
}