use ggen_utils::error::Result;
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ShellType {
Bash,
Zsh,
Fish,
PowerShell,
}
impl std::str::FromStr for ShellType {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"bash" => Ok(Self::Bash),
"zsh" => Ok(Self::Zsh),
"fish" => Ok(Self::Fish),
"powershell" | "pwsh" => Ok(Self::PowerShell),
_ => Err(format!("Invalid shell type: {}", s)),
}
}
}
impl ShellType {
pub fn as_str(&self) -> &'static str {
match self {
Self::Bash => "bash",
Self::Zsh => "zsh",
Self::Fish => "fish",
Self::PowerShell => "powershell",
}
}
pub fn default_completion_dir(&self) -> Option<PathBuf> {
match self {
Self::Bash => {
if let Ok(xdg_data) = std::env::var("XDG_DATA_HOME") {
Some(PathBuf::from(xdg_data).join("bash-completion/completions"))
} else if let Ok(home) = std::env::var("HOME") {
Some(PathBuf::from(home).join(".bash_completion.d"))
} else {
None
}
}
Self::Zsh => {
if let Ok(xdg_data) = std::env::var("XDG_DATA_HOME") {
Some(PathBuf::from(xdg_data).join("zsh/site-functions"))
} else if let Ok(home) = std::env::var("HOME") {
Some(PathBuf::from(home).join(".zfunc"))
} else {
None
}
}
Self::Fish => {
if let Ok(xdg_config) = std::env::var("XDG_CONFIG_HOME") {
Some(PathBuf::from(xdg_config).join("fish").join("completions"))
} else if let Ok(home) = std::env::var("HOME") {
Some(
PathBuf::from(home)
.join(".config")
.join("fish")
.join("completions"),
)
} else {
None
}
}
Self::PowerShell => {
if let Ok(profile) = std::env::var("PROFILE") {
Some(PathBuf::from(profile).parent()?.to_path_buf())
} else {
None
}
}
}
}
}
#[derive(Debug, Clone)]
pub struct CompletionResult {
pub script: String,
pub shell: ShellType,
}
pub trait CompletionGenerator {
fn generate(&self, shell: ShellType) -> Result<CompletionResult>;
}
pub trait CompletionInstaller {
fn install(&self, result: &CompletionResult, force: bool) -> Result<PathBuf>;
fn install_to(&self, result: &CompletionResult, path: PathBuf, force: bool) -> Result<PathBuf>;
}
pub trait ShellLister {
fn list_supported(&self) -> Vec<ShellType>;
fn is_installed(&self, shell: ShellType) -> bool;
}
pub struct FileSystemCompletionInstaller;
impl CompletionInstaller for FileSystemCompletionInstaller {
fn install(&self, result: &CompletionResult, force: bool) -> Result<PathBuf> {
let dir = result.shell.default_completion_dir().ok_or_else(|| {
ggen_utils::error::Error::new("Could not determine completion directory")
})?;
let filename = format!("ggen.{}", result.shell.as_str());
let path = dir.join(filename);
self.install_to(result, path, force)
}
fn install_to(&self, result: &CompletionResult, path: PathBuf, force: bool) -> Result<PathBuf> {
if path.exists() && !force {
return Err(ggen_utils::error::Error::new(&format!(
"Completion file already exists: {}. Use --force to overwrite",
path.display()
)));
}
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&path, &result.script)?;
Ok(path)
}
}
pub struct SystemShellLister;
impl ShellLister for SystemShellLister {
fn list_supported(&self) -> Vec<ShellType> {
vec![
ShellType::Bash,
ShellType::Zsh,
ShellType::Fish,
ShellType::PowerShell,
]
}
fn is_installed(&self, shell: ShellType) -> bool {
let shell_cmd = match shell {
ShellType::Bash => "bash",
ShellType::Zsh => "zsh",
ShellType::Fish => "fish",
ShellType::PowerShell => {
if cfg!(windows) {
"pwsh.exe"
} else {
"pwsh"
}
}
};
std::process::Command::new(if cfg!(windows) { "where" } else { "which" })
.arg(shell_cmd)
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
#[test]
fn test_shell_type_from_str() {
assert_eq!(ShellType::from_str("bash"), Ok(ShellType::Bash));
assert_eq!(ShellType::from_str("BASH"), Ok(ShellType::Bash));
assert_eq!(ShellType::from_str("zsh"), Ok(ShellType::Zsh));
assert_eq!(ShellType::from_str("fish"), Ok(ShellType::Fish));
assert_eq!(ShellType::from_str("powershell"), Ok(ShellType::PowerShell));
assert_eq!(ShellType::from_str("pwsh"), Ok(ShellType::PowerShell));
assert!(ShellType::from_str("invalid").is_err());
}
#[test]
fn test_shell_type_as_str() {
assert_eq!(ShellType::Bash.as_str(), "bash");
assert_eq!(ShellType::Zsh.as_str(), "zsh");
assert_eq!(ShellType::Fish.as_str(), "fish");
assert_eq!(ShellType::PowerShell.as_str(), "powershell");
}
#[test]
fn test_system_shell_lister_lists_all() {
let lister = SystemShellLister;
let shells = lister.list_supported();
assert_eq!(shells.len(), 4);
assert!(shells.contains(&ShellType::Bash));
assert!(shells.contains(&ShellType::Zsh));
assert!(shells.contains(&ShellType::Fish));
assert!(shells.contains(&ShellType::PowerShell));
}
}