use std::collections::HashMap;
use crate::PathDisplayExt;
pub(super) const ENV_FILE_HEADER: &str = "# managed by cfgd \u{2014} do not edit";
pub(super) fn fish_in_use() -> bool {
if cfg!(windows) {
crate::command_available("fish")
} else {
shell_var_indicates_fish(std::env::var("SHELL").ok().as_deref())
}
}
pub(super) fn shell_var_indicates_fish(shell: Option<&str>) -> bool {
shell.unwrap_or("").contains("fish")
}
pub(super) fn generate_env_file_content(
env: &[crate::config::EnvVar],
aliases: &[crate::config::ShellAlias],
) -> String {
let mut lines = vec![ENV_FILE_HEADER.to_string()];
for ev in env {
if crate::validate_env_var_name(&ev.name).is_err() {
tracing::warn!("skipping env var with unsafe name: {}", ev.name);
continue;
}
lines.push(format!(
"export {}=\"{}\"",
ev.name,
crate::escape_double_quoted(&ev.value)
));
}
for alias in aliases {
if crate::validate_alias_name(&alias.name).is_err() {
tracing::warn!("skipping alias with unsafe name: {}", alias.name);
continue;
}
lines.push(format!(
"alias {}=\"{}\"",
alias.name,
crate::escape_double_quoted(&alias.command)
));
}
lines.push(String::new()); lines.join("\n")
}
pub(super) fn generate_fish_env_content(
env: &[crate::config::EnvVar],
aliases: &[crate::config::ShellAlias],
) -> String {
let mut lines = vec![ENV_FILE_HEADER.to_string()];
for ev in env {
if crate::validate_env_var_name(&ev.name).is_err() {
tracing::warn!("skipping env var with unsafe name: {}", ev.name);
continue;
}
if ev.name == "PATH" {
let parts: Vec<String> = ev
.value
.split(':')
.map(|p| format!("'{}'", p.replace('\'', "\\'")))
.collect();
lines.push(format!("set -gx PATH {}", parts.join(" ")));
} else {
lines.push(format!(
"set -gx {} '{}'",
ev.name,
ev.value.replace('\'', "\\'")
));
}
}
for alias in aliases {
if crate::validate_alias_name(&alias.name).is_err() {
tracing::warn!("skipping alias with unsafe name: {}", alias.name);
continue;
}
lines.push(format!(
"abbr -a {} '{}'",
alias.name,
alias.command.replace('\'', "\\'")
));
}
lines.push(String::new());
lines.join("\n")
}
pub(super) fn generate_powershell_env_content(
env: &[crate::config::EnvVar],
aliases: &[crate::config::ShellAlias],
) -> String {
let mut lines = vec![ENV_FILE_HEADER.to_string()];
for ev in env {
if crate::validate_env_var_name(&ev.name).is_err() {
tracing::warn!("skipping env var with unsafe name: {}", ev.name);
continue;
}
if ev.value.contains("$env:") {
lines.push(format!(
"$env:{} = \"{}\"",
ev.name,
ev.value.replace('"', "`\"")
));
} else {
lines.push(format!(
"$env:{} = '{}'",
ev.name,
ev.value.replace('\'', "''")
));
}
}
for alias in aliases {
if crate::validate_alias_name(&alias.name).is_err() {
tracing::warn!("skipping alias with unsafe name: {}", alias.name);
continue;
}
if alias.command.split_whitespace().count() == 1 {
lines.push(format!(
"Set-Alias -Name {} -Value {}",
alias.name, alias.command
));
} else {
lines.push(format!(
"function {} {{ {} @args }}",
alias.name, alias.command
));
}
}
lines.push(String::new()); lines.join("\n")
}
pub(super) fn detect_rc_env_conflicts(
rc_path: &std::path::Path,
env: &[crate::config::EnvVar],
aliases: &[crate::config::ShellAlias],
) -> Vec<String> {
let rc_content = match std::fs::read_to_string(rc_path) {
Ok(c) => c,
Err(_) => return Vec::new(),
};
let mut before_lines = Vec::new();
for line in rc_content.lines() {
if line.contains("cfgd.env") {
break;
}
before_lines.push(line);
}
let rc_display = rc_path.posix();
let mut warnings = Vec::new();
let env_map: HashMap<&str, &str> = env
.iter()
.map(|e| (e.name.as_str(), e.value.as_str()))
.collect();
let alias_map: HashMap<&str, &str> = aliases
.iter()
.map(|a| (a.name.as_str(), a.command.as_str()))
.collect();
for line in &before_lines {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("export ")
&& let Some((name, raw_value)) = rest.split_once('=')
{
let name = name.trim();
let value = strip_shell_quotes(raw_value);
if let Some(&cfgd_value) = env_map.get(name)
&& value != cfgd_value
{
warnings.push(format!(
"{} sets export {}={} before cfgd source line — cfgd will override to \"{}\"; move it after the source line to keep your value",
rc_display, name, raw_value, cfgd_value,
));
}
}
if let Some(rest) = trimmed.strip_prefix("alias ")
&& let Some((name, raw_value)) = rest.split_once('=')
{
let name = name.trim();
let value = strip_shell_quotes(raw_value);
if let Some(&cfgd_value) = alias_map.get(name)
&& value != cfgd_value
{
warnings.push(format!(
"{} sets alias {}={} before cfgd source line — cfgd will override to \"{}\"; move it after the source line to keep your value",
rc_display, name, raw_value, cfgd_value,
));
}
}
}
warnings
}
pub(super) fn strip_shell_quotes(s: &str) -> &str {
let s = s.trim();
if (s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\'')) {
&s[1..s.len() - 1]
} else {
s
}
}