use std::collections::HashMap;
use std::path::Path;
use crate::{templates, HookConfig, Result, ShellType};
#[derive(Debug, Clone)]
pub struct HookGenerator {
config: HookConfig,
}
impl HookGenerator {
pub fn new(config: HookConfig) -> Self {
Self { config }
}
pub fn for_shell(shell_type: ShellType) -> Self {
let config = HookConfig {
shell_type: shell_type.to_string(),
default_source: Some(shell_type.to_string()),
..Default::default()
};
Self::new(config)
}
pub fn generate(&self) -> Result<String> {
let shell_type: ShellType = self.config.shell_type.parse()?;
let template = match shell_type {
ShellType::Bash => templates::BASH_TEMPLATE,
ShellType::Zsh => templates::ZSH_TEMPLATE,
ShellType::Fish => templates::FISH_TEMPLATE,
ShellType::PowerShell => templates::POWERSHELL_TEMPLATE,
};
self.render_template(template, shell_type)
}
pub async fn generate_to_file(&self, output_path: &Path) -> Result<()> {
let content = self.generate()?;
tokio::fs::write(output_path, content).await?;
Ok(())
}
pub fn get_install_path(&self) -> Result<String> {
let shell_type: ShellType = self.config.shell_type.parse()?;
let path = match shell_type {
ShellType::Bash => "~/.bashrc",
ShellType::Zsh => "~/.zshrc",
ShellType::Fish => "~/.config/fish/config.fish",
ShellType::PowerShell => "$PROFILE",
};
Ok(path.to_string())
}
pub fn get_install_instructions(&self) -> Result<String> {
let shell_type: ShellType = self.config.shell_type.parse()?;
let install_path = self.get_install_path()?;
let instructions = match shell_type {
ShellType::Bash | ShellType::Zsh => {
format!(
"Add the generated hook to your {} file:\n\n\
echo '# Load cstats hook' >> {}\n\
cat hook.{} >> {}\n\n\
Then restart your shell or run: source {}",
shell_type, install_path, shell_type, install_path, install_path
)
}
ShellType::Fish => {
format!(
"Add the generated hook to your Fish configuration:\n\n\
mkdir -p ~/.config/fish\n\
echo '# Load cstats hook' >> {}\n\
cat hook.fish >> {}\n\n\
Then restart your shell.",
install_path, install_path
)
}
ShellType::PowerShell => "Add the generated hook to your PowerShell profile:\n\n\
# Create profile if it doesn't exist\n\
if (!(Test-Path -Path $PROFILE)) {\n\
New-Item -ItemType File -Path $PROFILE -Force\n\
}\n\n\
# Add hook content to profile\n\
Add-Content -Path $PROFILE -Value (Get-Content hook.ps1)\n\n\
Then restart PowerShell."
.to_string(),
};
Ok(instructions)
}
fn render_template(&self, template: &str, shell_type: ShellType) -> Result<String> {
let mut context = HashMap::new();
context.insert("shell_type", shell_type.to_string());
context.insert(
"default_source",
self.config
.default_source
.as_deref()
.unwrap_or(&shell_type.to_string())
.to_string(),
);
context.insert(
"cstats_path",
self.config
.cstats_path
.as_deref()
.unwrap_or("cstats")
.to_string(),
);
context.insert("auto_collect_all", self.config.auto_collect_all.to_string());
let auto_commands = if self.config.auto_commands.is_empty() {
"# No auto commands configured".to_string()
} else {
self.config
.auto_commands
.iter()
.map(|cmd| {
format!(
"# alias {}='{} collect --source {{}} --command {}'",
cmd,
context.get("cstats_path").unwrap(),
cmd
)
})
.collect::<Vec<_>>()
.join("\n")
};
context.insert("auto_commands", auto_commands);
let env_vars = if self.config.env_vars.is_empty() {
"# No environment variables configured".to_string()
} else {
self.config
.env_vars
.iter()
.map(|(k, v)| match shell_type {
ShellType::Fish => format!("set -gx {} {}", k, v),
ShellType::PowerShell => format!("$env:{} = '{}'", k, v),
_ => format!("export {}={}", k, v),
})
.collect::<Vec<_>>()
.join("\n")
};
context.insert("env_vars", env_vars);
let mut result = template.to_string();
for (key, value) in context {
let placeholder = format!("{{{{{}}}}}", key);
result = result.replace(&placeholder, &value);
}
Ok(result)
}
}
impl Default for HookGenerator {
fn default() -> Self {
Self::for_shell(ShellType::Bash)
}
}